about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.envrc7
-rw-r--r--.git-blame-ignore-revs26
-rw-r--r--.gitignore7
-rw-r--r--.hgignore1
-rw-r--r--.mailmap1
-rw-r--r--.nixery/README.md13
-rw-r--r--.nixery/default.nix6
-rw-r--r--.rgignore1
-rw-r--r--LICENSE22
-rw-r--r--OWNERS6
-rw-r--r--README.md124
-rw-r--r--RULES1
-rw-r--r--buf.yaml13
-rw-r--r--corp/LICENSE4
-rw-r--r--corp/OWNERS3
-rw-r--r--corp/website/content.md26
-rw-r--r--corp/website/default.nix37
-rw-r--r--default.nix122
-rw-r--r--docs/CODE_OF_CONDUCT.md29
-rw-r--r--docs/CONTRIBUTING.md120
-rw-r--r--docs/REVIEWS.md154
-rw-r--r--docs/designs/SPARSE_CHECKOUTS.md37
-rw-r--r--fun/clbot/backoffutil/backoffutil.go43
-rw-r--r--fun/clbot/backoffutil/default.nix14
-rw-r--r--fun/clbot/clbot.go289
-rw-r--r--fun/clbot/default.nix19
-rw-r--r--fun/clbot/gerrit/default.nix18
-rw-r--r--fun/clbot/gerrit/gerritevents/default.nix10
-rw-r--r--fun/clbot/gerrit/gerritevents/events.go321
-rw-r--r--fun/clbot/gerrit/gerritevents/time.go38
-rw-r--r--fun/clbot/gerrit/gerritevents/types.go221
-rw-r--r--fun/clbot/gerrit/watcher.go252
-rw-r--r--fun/clbot/gerrit/watcher_test.go190
-rw-r--r--fun/clbot/go.mod12
-rw-r--r--fun/clbot/go.sum31
-rw-r--r--fun/defer_rs/.gitignore3
-rw-r--r--fun/defer_rs/Cargo.toml6
-rw-r--r--fun/defer_rs/README.md53
-rw-r--r--fun/defer_rs/examples/defer-with-error.rs72
-rw-r--r--fun/defer_rs/examples/defer.rs31
-rw-r--r--fun/defer_rs/examples/undefer.rs40
-rw-r--r--fun/elblog/.gitignore1
-rw-r--r--fun/elblog/README.md11
-rw-r--r--fun/elblog/blog.css37
-rw-r--r--fun/elblog/blog.el123
-rw-r--r--fun/elblog/postamble.html9
-rw-r--r--fun/elblog/preamble.html6
-rw-r--r--fun/gemma/CODE_OF_CONDUCT.md20
-rw-r--r--fun/gemma/LICENSE674
-rw-r--r--fun/gemma/README.md96
-rw-r--r--fun/gemma/config.lisp21
-rw-r--r--fun/gemma/default.nix57
-rw-r--r--fun/gemma/frontend/Main.elm221
-rw-r--r--fun/gemma/frontend/elm-package.json17
-rw-r--r--fun/gemma/src/gemma.lisp192
-rw-r--r--fun/idual/README.md34
-rw-r--r--fun/idual/default.nix25
-rw-r--r--fun/idual/idual/__init__.py65
-rw-r--r--fun/idual/idualctl39
-rw-r--r--fun/idual/setup.py15
-rw-r--r--fun/owothia/.envrc1
-rw-r--r--fun/owothia/.gitignore30
-rw-r--r--fun/owothia/chatter.patch19
-rw-r--r--fun/owothia/default.nix13
-rw-r--r--fun/owothia/hie.yaml4
-rw-r--r--fun/owothia/owothia.cabal53
-rw-r--r--fun/owothia/pkg.nix34
-rw-r--r--fun/owothia/regex-tdfa-text.patch40
-rw-r--r--fun/owothia/shell.nix22
-rw-r--r--fun/owothia/src/Main.hs168
-rw-r--r--fun/paroxysm/.gitignore5
-rw-r--r--fun/paroxysm/Cargo.lock1639
-rw-r--r--fun/paroxysm/Cargo.toml22
-rw-r--r--fun/paroxysm/OWNERS3
-rw-r--r--fun/paroxysm/README.md19
-rw-r--r--fun/paroxysm/default.nix14
-rw-r--r--fun/paroxysm/docker/default.nix7
-rw-r--r--fun/paroxysm/migrations/20181209140247_initial/down.sql2
-rw-r--r--fun/paroxysm/migrations/20181209140247_initial/up.sql15
-rw-r--r--fun/paroxysm/migrations/20181218142013_fix_unique/down.sql1
-rw-r--r--fun/paroxysm/migrations/20181218142013_fix_unique/up.sql1
-rw-r--r--fun/paroxysm/src/cfg.rs12
-rw-r--r--fun/paroxysm/src/keyword.rs219
-rw-r--r--fun/paroxysm/src/main.rs395
-rw-r--r--fun/paroxysm/src/models.rs36
-rw-r--r--fun/paroxysm/src/schema.rs18
-rw-r--r--fun/quinistry/.gitignore2
-rw-r--r--fun/quinistry/README.md63
-rw-r--r--fun/quinistry/const.go12
-rw-r--r--fun/quinistry/default.nix11
-rw-r--r--fun/quinistry/image.go150
-rw-r--r--fun/quinistry/k8s/child.yaml27
-rw-r--r--fun/quinistry/k8s/parent.yaml27
-rw-r--r--fun/quinistry/main.go57
-rw-r--r--fun/quinistry/types.go79
-rw-r--r--fun/tvl-ebooks/OWNERS3
-rw-r--r--fun/tvl-ebooks/default.nix7
-rw-r--r--fun/tvl-ebooks/ebook-client/main.go170
-rw-r--r--fun/tvl-ebooks/go.mod8
-rw-r--r--fun/tvl-ebooks/go.sum11
-rw-r--r--fun/tvl-ebooks/irc-ingest/main.go99
-rw-r--r--fun/tvl-ebooks/make-v6/main.go26
-rw-r--r--fun/tvl-ebooks/mkov-engine/main.go246
-rw-r--r--fun/tvl-ebooks/parse-logs/main.go174
-rw-r--r--fun/uggc/default.nix21
-rw-r--r--fun/uggc/main.go38
-rw-r--r--fun/uggc/uggc.desktop7
-rw-r--r--fun/uggc/uggc.reg23
-rw-r--r--fun/watchblob/README.md35
-rw-r--r--fun/watchblob/default.nix13
-rw-r--r--fun/watchblob/main.go108
-rw-r--r--fun/watchblob/main_test.go96
-rw-r--r--fun/watchblob/urls.go37
-rw-r--r--fun/wcl/default.nix14
-rw-r--r--fun/wcl/wc.lisp34
-rw-r--r--fun/🕰️/OWNERS3
-rw-r--r--fun/🕰️/bin.lisp91
-rw-r--r--fun/🕰️/default.nix44
-rw-r--r--fun/🕰️/lib.lisp32
-rw-r--r--lisp/dns/README.md75
-rw-r--r--lisp/dns/client.lisp85
-rw-r--r--lisp/dns/default.nix21
-rw-r--r--lisp/dns/message.lisp407
-rw-r--r--lisp/dns/package.lisp11
-rw-r--r--lisp/klatre/OWNERS3
-rw-r--r--lisp/klatre/default.nix14
-rw-r--r--lisp/klatre/klatre.lisp119
-rw-r--r--lisp/klatre/package.lisp16
-rw-r--r--net/alcoholic_jwt/.gitignore4
-rw-r--r--net/alcoholic_jwt/Cargo.lock197
-rw-r--r--net/alcoholic_jwt/Cargo.toml17
-rw-r--r--net/alcoholic_jwt/LICENSE674
-rw-r--r--net/alcoholic_jwt/README.md72
-rw-r--r--net/alcoholic_jwt/default.nix9
-rw-r--r--net/alcoholic_jwt/src/lib.rs509
-rw-r--r--net/alcoholic_jwt/src/tests.rs69
-rw-r--r--net/crimp/.gitignore3
-rw-r--r--net/crimp/Cargo.lock180
-rw-r--r--net/crimp/Cargo.toml20
-rw-r--r--net/crimp/LICENSE674
-rw-r--r--net/crimp/README.md26
-rw-r--r--net/crimp/default.nix9
-rw-r--r--net/crimp/src/lib.rs537
-rw-r--r--net/crimp/src/tests.rs185
-rw-r--r--net/stomp_erl/.gitignore10
-rw-r--r--net/stomp_erl/Makefile8
-rw-r--r--net/stomp_erl/README.md78
-rw-r--r--net/stomp_erl/include/stomp.hrl22
-rw-r--r--net/stomp_erl/src/stomp.app.src7
-rw-r--r--net/stomp_erl/src/stomp_app.erl11
-rw-r--r--net/stomp_erl/src/stomp_sup.erl22
-rw-r--r--net/stomp_erl/src/stomp_worker.erl193
-rw-r--r--nix/OWNERS3
-rw-r--r--nix/binify/default.nix16
-rw-r--r--nix/bufCheck/default.nix9
-rw-r--r--nix/buildGo/.skip-subtree2
-rw-r--r--nix/buildGo/README.md140
-rw-r--r--nix/buildGo/default.nix140
-rw-r--r--nix/buildGo/example/default.nix48
-rw-r--r--nix/buildGo/example/lib.go9
-rw-r--r--nix/buildGo/example/main.go25
-rw-r--r--nix/buildGo/example/thing.proto10
-rw-r--r--nix/buildGo/external/default.nix109
-rw-r--r--nix/buildGo/external/main.go201
-rw-r--r--nix/buildGo/proto.nix87
-rw-r--r--nix/buildLisp/README.md254
-rw-r--r--nix/buildLisp/default.nix780
-rw-r--r--nix/buildLisp/example/default.nix33
-rw-r--r--nix/buildLisp/example/lib.lisp6
-rw-r--r--nix/buildLisp/example/main.lisp7
-rw-r--r--nix/buildLisp/tests/argv0.nix36
-rw-r--r--nix/buildManPages/OWNERS3
-rw-r--r--nix/buildManPages/default.nix103
-rw-r--r--nix/buildkite/default.nix335
-rwxr-xr-xnix/buildkite/fetch-parent-targets.sh44
-rw-r--r--nix/drvSeqL/default.nix47
-rw-r--r--nix/emptyDerivation/OWNERS3
-rw-r--r--nix/emptyDerivation/default.nix21
-rw-r--r--nix/emptyDerivation/emptyDerivation.nix38
-rw-r--r--nix/emptyDerivation/tests.nix40
-rw-r--r--nix/escapeExecline/default.nix32
-rw-r--r--nix/getBins/default.nix51
-rw-r--r--nix/getBins/tests.nix40
-rw-r--r--nix/lazy-deps/default.nix75
-rw-r--r--nix/mergePatch/default.nix192
-rw-r--r--nix/netstring/attrsToKeyValList.nix33
-rw-r--r--nix/netstring/fromString.nix10
-rw-r--r--nix/nint/OWNERS3
-rw-r--r--nix/nint/README.md93
-rw-r--r--nix/nint/default.nix16
-rw-r--r--nix/nint/nint.rs149
-rw-r--r--nix/readTree/README.md92
-rw-r--r--nix/readTree/default.nix284
-rw-r--r--nix/readTree/tests/.skip-subtree1
-rw-r--r--nix/readTree/tests/default.nix129
-rw-r--r--nix/readTree/tests/test-example/third_party/default.nix5
-rw-r--r--nix/readTree/tests/test-example/third_party/rustpkgs/aho-corasick.nix1
-rw-r--r--nix/readTree/tests/test-example/third_party/rustpkgs/serde.nix1
-rw-r--r--nix/readTree/tests/test-example/tools/cheddar/default.nix1
-rw-r--r--nix/readTree/tests/test-example/tools/roquefort.nix1
-rw-r--r--nix/readTree/tests/test-marker/directory-marked/default.nix3
-rw-r--r--nix/readTree/tests/test-marker/directory-marked/nested/default.nix3
-rw-r--r--nix/readTree/tests/test-marker/file-children/one.nix3
-rw-r--r--nix/readTree/tests/test-marker/file-children/two.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/default.nix7
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/subdir/a.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/default.nix5
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/no-merge/default.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/no-merge/subdir/a.nix1
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/sibling.nix1
-rw-r--r--nix/readTree/tests/test-tree-traversal/default-nix/subdir/a.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/no-skip-subtree/a/default.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/no-skip-subtree/b/c.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/no-skip-subtree/default.nix5
-rw-r--r--nix/readTree/tests/test-tree-traversal/skip-subtree/.skip-subtree1
-rw-r--r--nix/readTree/tests/test-tree-traversal/skip-subtree/a/default.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/skip-subtree/b/c.nix3
-rw-r--r--nix/readTree/tests/test-tree-traversal/skip-subtree/default.nix5
-rw-r--r--nix/readTree/tests/test-wrong-no-dots/no-dots-in-function.nix3
-rw-r--r--nix/readTree/tests/test-wrong-not-a-function/not-a-function.nix1
-rw-r--r--nix/renderMarkdown/default.nix8
-rw-r--r--nix/runExecline/default.nix31
-rw-r--r--nix/runExecline/runExecline.nix122
-rw-r--r--nix/runExecline/tests.nix117
-rw-r--r--nix/runTestsuite/default.nix199
-rw-r--r--nix/sparseTree/OWNERS3
-rw-r--r--nix/sparseTree/default.nix74
-rw-r--r--nix/tag/default.nix166
-rw-r--r--nix/tag/tests.nix94
-rw-r--r--nix/tailscale/default.nix31
-rw-r--r--nix/utils/OWNERS3
-rw-r--r--nix/utils/default.nix176
-rw-r--r--nix/utils/tests/.skip-subtree1
-rw-r--r--nix/utils/tests/default.nix110
-rw-r--r--nix/utils/tests/directory/file0
l---------nix/utils/tests/missing1
l---------nix/utils/tests/symlink-directory1
l---------nix/utils/tests/symlink-file1
l---------nix/utils/tests/symlink-symlink-directory1
l---------nix/utils/tests/symlink-symlink-file1
-rw-r--r--nix/writeElispBin/default.nix20
-rw-r--r--nix/writeExecline/default.nix39
-rw-r--r--nix/writeScript/default.nix35
-rw-r--r--nix/writeScriptBin/default.nix12
-rw-r--r--nix/writers/default.nix113
-rw-r--r--nix/writers/tests/rust.nix76
-rw-r--r--nix/yants/README.md88
-rw-r--r--nix/yants/default.nix368
-rw-r--r--nix/yants/screenshots/enums.pngbin0 -> 41305 bytes
-rw-r--r--nix/yants/screenshots/functions.pngbin0 -> 32907 bytes
-rw-r--r--nix/yants/screenshots/nested-structs.pngbin0 -> 70264 bytes
-rw-r--r--nix/yants/screenshots/simple.pngbin0 -> 43010 bytes
-rw-r--r--nix/yants/screenshots/structs.pngbin0 -> 69499 bytes
-rw-r--r--nix/yants/tests/default.nix158
-rw-r--r--ops/besadii/default.nix8
-rw-r--r--ops/besadii/main.go558
-rw-r--r--ops/deploy-whitby/default.nix31
-rwxr-xr-xops/deploy-whitby/deploy-whitby.sh46
-rw-r--r--ops/dns/README.md11
-rw-r--r--ops/dns/default.nix14
-rw-r--r--ops/dns/nixery.dev.zone10
-rw-r--r--ops/dns/tvl.fyi.zone39
-rw-r--r--ops/dns/tvl.su.zone51
-rw-r--r--ops/gerrit-tvl/HttpModule.java14
-rw-r--r--ops/gerrit-tvl/MANIFEST.MF2
-rw-r--r--ops/gerrit-tvl/README.md6
-rw-r--r--ops/gerrit-tvl/default.nix33
-rw-r--r--ops/gerrit-tvl/static/tvl.js189
-rw-r--r--ops/glesys/.gitignore3
-rw-r--r--ops/glesys/README.md20
-rw-r--r--ops/glesys/default.nix8
-rw-r--r--ops/glesys/dns-nixery-dev.tf44
-rw-r--r--ops/glesys/dns-tvl-fyi.tf99
-rw-r--r--ops/glesys/dns-tvl-su.tf122
-rw-r--r--ops/glesys/main.tf72
-rw-r--r--ops/journaldriver/.gitignore3
-rw-r--r--ops/journaldriver/Cargo.lock511
-rw-r--r--ops/journaldriver/Cargo.toml22
-rw-r--r--ops/journaldriver/README.md152
-rw-r--r--ops/journaldriver/build.rs5
-rw-r--r--ops/journaldriver/default.nix11
-rw-r--r--ops/journaldriver/src/main.rs638
-rw-r--r--ops/journaldriver/src/tests.rs131
-rw-r--r--ops/keycloak/.gitignore3
-rw-r--r--ops/keycloak/README.md18
-rw-r--r--ops/keycloak/clients.tf106
-rw-r--r--ops/keycloak/default.nix8
-rw-r--r--ops/keycloak/main.tf34
-rw-r--r--ops/keycloak/user_sources.tf21
-rw-r--r--ops/kontemplate/.gitignore2
-rw-r--r--ops/kontemplate/LICENSE674
-rw-r--r--ops/kontemplate/README.md185
-rwxr-xr-xops/kontemplate/build-release.sh75
-rw-r--r--ops/kontemplate/context/context.go266
-rw-r--r--ops/kontemplate/context/context_test.go353
-rw-r--r--ops/kontemplate/context/testdata/collections-test.yaml15
-rw-r--r--ops/kontemplate/context/testdata/default-loading.yaml6
-rw-r--r--ops/kontemplate/context/testdata/default/default.yaml2
-rw-r--r--ops/kontemplate/context/testdata/explicit-path.yaml11
-rw-r--r--ops/kontemplate/context/testdata/explicit-subresource-path.yaml8
-rw-r--r--ops/kontemplate/context/testdata/flat-test.yaml10
-rw-r--r--ops/kontemplate/context/testdata/flat-with-args-test.yaml9
-rw-r--r--ops/kontemplate/context/testdata/import-vars-simple.yaml5
-rw-r--r--ops/kontemplate/context/testdata/merging/context.yaml15
-rw-r--r--ops/kontemplate/context/testdata/merging/import-vars.yaml4
-rw-r--r--ops/kontemplate/context/testdata/merging/resource/default.yaml5
-rw-r--r--ops/kontemplate/context/testdata/merging/resource/output.yaml5
-rw-r--r--ops/kontemplate/context/testdata/parent-variable-override.yaml10
-rw-r--r--ops/kontemplate/context/testdata/parent-variables.yaml10
-rw-r--r--ops/kontemplate/context/testdata/test-vars-override.yaml3
-rw-r--r--ops/kontemplate/context/testdata/test-vars.yaml5
-rw-r--r--ops/kontemplate/default.nix36
-rw-r--r--ops/kontemplate/deps.nix111
-rw-r--r--ops/kontemplate/docs/cluster-config.md106
-rw-r--r--ops/kontemplate/docs/resource-sets.md170
-rw-r--r--ops/kontemplate/docs/templates.md153
-rw-r--r--ops/kontemplate/docs/tips-and-tricks.md77
-rw-r--r--ops/kontemplate/example/other-config.yaml7
-rw-r--r--ops/kontemplate/example/prod-cluster.json16
-rw-r--r--ops/kontemplate/example/prod-cluster.yaml17
-rw-r--r--ops/kontemplate/example/some-api/some-api.yaml52
-rw-r--r--ops/kontemplate/example/some-api/some.cfg4
-rw-r--r--ops/kontemplate/image/Dockerfile15
-rw-r--r--ops/kontemplate/image/README.md12
-rw-r--r--ops/kontemplate/image/hashes2
-rw-r--r--ops/kontemplate/main.go242
-rw-r--r--ops/kontemplate/release.nix58
-rw-r--r--ops/kontemplate/templater/dns.go35
-rw-r--r--ops/kontemplate/templater/pass.go34
-rw-r--r--ops/kontemplate/templater/templater.go236
-rw-r--r--ops/kontemplate/templater/templater_test.go205
-rw-r--r--ops/kontemplate/templater/testdata/test-default.txt1
-rw-r--r--ops/kontemplate/templater/testdata/test-insertTemplate.txt1
-rw-r--r--ops/kontemplate/templater/testdata/test-template.txt1
-rw-r--r--ops/kontemplate/util/util.go58
-rw-r--r--ops/kontemplate/util/util_test.go83
-rw-r--r--ops/machines/all-systems.nix24
-rw-r--r--ops/machines/sanduny/default.nix111
-rw-r--r--ops/machines/whitby/OWNERS6
-rw-r--r--ops/machines/whitby/README.md5
-rw-r--r--ops/machines/whitby/default.nix653
-rw-r--r--ops/modules/.skip-subtree1
-rw-r--r--ops/modules/README.md7
-rw-r--r--ops/modules/atward.nix38
-rw-r--r--ops/modules/auto-deploy.nix104
-rw-r--r--ops/modules/automatic-gc.nix92
-rw-r--r--ops/modules/cgit/default.nix92
-rw-r--r--ops/modules/cgit/thttpd_cgi_idx.patch13
-rw-r--r--ops/modules/clbot.nix82
-rw-r--r--ops/modules/default-imports.nix14
-rw-r--r--ops/modules/default.nix2
-rw-r--r--ops/modules/gerrit-queue.nix52
-rw-r--r--ops/modules/irccat.nix62
-rw-r--r--ops/modules/josh.nix33
-rw-r--r--ops/modules/journaldriver.nix26
-rw-r--r--ops/modules/known-hosts.nix21
-rw-r--r--ops/modules/monorepo-gerrit.nix152
-rw-r--r--ops/modules/nixery.nix43
-rw-r--r--ops/modules/oauth2_proxy.nix60
-rw-r--r--ops/modules/open_eid.nix37
-rw-r--r--ops/modules/owothia.nix68
-rw-r--r--ops/modules/panettone.nix108
-rw-r--r--ops/modules/paroxysm.nix28
-rw-r--r--ops/modules/prometheus-fail2ban-exporter.nix52
-rw-r--r--ops/modules/quassel.nix79
-rw-r--r--ops/modules/restic.nix62
-rw-r--r--ops/modules/smtprelay.nix61
-rw-r--r--ops/modules/sourcegraph.nix60
-rw-r--r--ops/modules/tvl-buildkite.nix77
-rw-r--r--ops/modules/tvl-cache.nix19
-rw-r--r--ops/modules/tvl-slapd/default.nix81
-rw-r--r--ops/modules/tvl-users.nix94
-rw-r--r--ops/modules/v4l2loopback.nix12
-rw-r--r--ops/modules/www/atward.tvl.fyi.nix33
-rw-r--r--ops/modules/www/auth.tvl.fyi.nix24
-rw-r--r--ops/modules/www/b.tvl.fyi.nix32
-rw-r--r--ops/modules/www/base.nix41
-rw-r--r--ops/modules/www/cache.tvl.su.nix31
-rw-r--r--ops/modules/www/cl.tvl.fyi.nix30
-rw-r--r--ops/modules/www/code.tvl.fyi.nix45
-rw-r--r--ops/modules/www/cs.tvl.fyi.nix31
-rw-r--r--ops/modules/www/deploys.tvl.fyi.nix22
-rw-r--r--ops/modules/www/images.tvl.fyi.nix22
-rw-r--r--ops/modules/www/nixery.dev.nix21
-rw-r--r--ops/modules/www/self-redirect.nix27
-rw-r--r--ops/modules/www/static.tvl.fyi.nix42
-rw-r--r--ops/modules/www/status.tvl.su.nix25
-rw-r--r--ops/modules/www/tazj.in.nix40
-rw-r--r--ops/modules/www/todo.tvl.fyi.nix25
-rw-r--r--ops/modules/www/tvl.fyi.nix47
-rw-r--r--ops/modules/www/tvl.su.nix20
-rw-r--r--ops/modules/www/wigglydonke.rs.nix15
-rw-r--r--ops/mq_cli/.gitignore4
-rw-r--r--ops/mq_cli/CODE_OF_CONDUCT.md20
-rw-r--r--ops/mq_cli/Cargo.lock168
-rw-r--r--ops/mq_cli/Cargo.toml14
-rw-r--r--ops/mq_cli/LICENSE22
-rw-r--r--ops/mq_cli/README.md42
-rw-r--r--ops/mq_cli/default.nix3
-rw-r--r--ops/mq_cli/src/main.rs235
-rw-r--r--ops/nixos.nix63
-rw-r--r--ops/pipelines/README.md5
-rw-r--r--ops/pipelines/depot.nix49
-rw-r--r--ops/pipelines/static-pipeline.yaml116
-rw-r--r--ops/posix_mq.rs/.gitignore3
-rw-r--r--ops/posix_mq.rs/CODE_OF_CONDUCT.md20
-rw-r--r--ops/posix_mq.rs/Cargo.lock63
-rw-r--r--ops/posix_mq.rs/Cargo.toml12
-rw-r--r--ops/posix_mq.rs/LICENSE21
-rw-r--r--ops/posix_mq.rs/README.md44
-rw-r--r--ops/posix_mq.rs/default.nix3
-rw-r--r--ops/posix_mq.rs/src/error.rs122
-rw-r--r--ops/posix_mq.rs/src/lib.rs247
-rw-r--r--ops/posix_mq.rs/src/tests.rs21
-rw-r--r--ops/secrets/.skip-subtree2
-rw-r--r--ops/secrets/README.md1
-rw-r--r--ops/secrets/besadii.age19
-rw-r--r--ops/secrets/buildkite-agent-token.agebin0 -> 736 bytes
-rw-r--r--ops/secrets/buildkite-graphql-token.age16
-rw-r--r--ops/secrets/buildkite-ssh-private-key.agebin0 -> 1166 bytes
-rw-r--r--ops/secrets/clbot-ssh.agebin0 -> 1090 bytes
-rw-r--r--ops/secrets/clbot.age15
-rw-r--r--ops/secrets/default.nix3
-rw-r--r--ops/secrets/gerrit-queue.age17
-rw-r--r--ops/secrets/gerrit-secrets.age15
-rw-r--r--ops/secrets/grafana.age17
-rw-r--r--ops/secrets/irccat.age16
-rw-r--r--ops/secrets/journaldriver.agebin0 -> 3228 bytes
-rw-r--r--ops/secrets/keycloak-db.age15
-rw-r--r--ops/secrets/mkSecrets.nix27
-rw-r--r--ops/secrets/nix-cache-priv.agebin0 -> 848 bytes
-rw-r--r--ops/secrets/nix-cache-pub.age16
-rw-r--r--ops/secrets/oauth2_proxy.age16
-rw-r--r--ops/secrets/owothia.agebin0 -> 754 bytes
-rw-r--r--ops/secrets/panettone.age17
-rw-r--r--ops/secrets/secrets.nix46
-rw-r--r--ops/secrets/smtprelay.age16
-rw-r--r--ops/secrets/tf-glesys.agebin0 -> 874 bytes
-rw-r--r--ops/secrets/tf-keycloak.agebin0 -> 981 bytes
-rw-r--r--ops/secrets/tvl-alerts-bot-telegram-token.age16
-rw-r--r--ops/users/default.nix167
-rw-r--r--rustfmt.toml1
-rw-r--r--third_party/README.md13
-rw-r--r--third_party/agenix/default.nix13
-rw-r--r--third_party/alsi/OWNERS3
-rw-r--r--third_party/alsi/default.nix25
-rw-r--r--third_party/bat_syntaxes/Prolog.sublime-syntax1319
-rw-r--r--third_party/bat_syntaxes/default.nix18
-rw-r--r--third_party/bufbuild/default.nix29
-rw-r--r--third_party/buzz/default.nix30
-rw-r--r--third_party/cgit/.gitignore12
-rw-r--r--third_party/cgit/.mailmap10
-rw-r--r--third_party/cgit/.skip-subtree1
-rw-r--r--third_party/cgit/AUTHORS13
-rw-r--r--third_party/cgit/COPYING339
-rw-r--r--third_party/cgit/Makefile168
-rw-r--r--third_party/cgit/README88
-rw-r--r--third_party/cgit/cache.c480
-rw-r--r--third_party/cgit/cache.h37
-rw-r--r--third_party/cgit/cgit.c1107
-rw-r--r--third_party/cgit/cgit.css881
-rw-r--r--third_party/cgit/cgit.h397
-rw-r--r--third_party/cgit/cgit.mk114
-rw-r--r--third_party/cgit/cgit.pngbin0 -> 1366 bytes
-rw-r--r--third_party/cgit/cgitrc.5.txt977
-rw-r--r--third_party/cgit/cmd.c186
-rw-r--r--third_party/cgit/cmd.h16
-rw-r--r--third_party/cgit/configfile.c90
-rw-r--r--third_party/cgit/configfile.h10
-rwxr-xr-xthird_party/cgit/contrib/hooks/post-receive.agefile19
-rw-r--r--third_party/cgit/default.nix51
-rw-r--r--third_party/cgit/filter.c222
-rwxr-xr-xthird_party/cgit/filters/about-formatting.sh27
-rwxr-xr-xthird_party/cgit/filters/commit-links.sh28
-rwxr-xr-xthird_party/cgit/filters/email-gravatar.py36
-rwxr-xr-xthird_party/cgit/filters/html-converters/man2html4
-rwxr-xr-xthird_party/cgit/filters/html-converters/md2html304
-rwxr-xr-xthird_party/cgit/filters/html-converters/rst2html2
-rwxr-xr-xthird_party/cgit/filters/html-converters/txt2html4
-rwxr-xr-xthird_party/cgit/filters/syntax-highlighting.py55
-rwxr-xr-xthird_party/cgit/filters/syntax-highlighting.sh121
-rwxr-xr-xthird_party/cgit/gen-version.sh20
-rw-r--r--third_party/cgit/html.c344
-rw-r--r--third_party/cgit/html.h37
-rw-r--r--third_party/cgit/parsing.c223
-rw-r--r--third_party/cgit/robots.txt4
-rw-r--r--third_party/cgit/scan-tree.c268
-rw-r--r--third_party/cgit/scan-tree.h2
-rw-r--r--third_party/cgit/shared.c577
-rw-r--r--third_party/cgit/tests/.gitignore2
-rw-r--r--third_party/cgit/tests/Makefile17
-rwxr-xr-xthird_party/cgit/tests/filters/dump.sh4
-rwxr-xr-xthird_party/cgit/tests/setup.sh161
-rwxr-xr-xthird_party/cgit/tests/t0001-validate-git-versions.sh45
-rwxr-xr-xthird_party/cgit/tests/t0010-validate-html.sh40
-rwxr-xr-xthird_party/cgit/tests/t0020-validate-cache.sh78
-rwxr-xr-xthird_party/cgit/tests/t0101-index.sh17
-rwxr-xr-xthird_party/cgit/tests/t0102-summary.sh25
-rwxr-xr-xthird_party/cgit/tests/t0103-log.sh24
-rwxr-xr-xthird_party/cgit/tests/t0104-tree.sh32
-rwxr-xr-xthird_party/cgit/tests/t0105-commit.sh36
-rwxr-xr-xthird_party/cgit/tests/t0106-diff.sh19
-rwxr-xr-xthird_party/cgit/tests/t0107-snapshot.sh205
-rwxr-xr-xthird_party/cgit/tests/t0108-patch.sh62
-rwxr-xr-xthird_party/cgit/tests/t0109-gitconfig.sh48
-rwxr-xr-xthird_party/cgit/tests/t0110-rawdiff.sh42
-rwxr-xr-xthird_party/cgit/tests/t0111-filter.sh43
-rwxr-xr-xthird_party/cgit/tests/valgrind/bin/cgit12
-rw-r--r--third_party/cgit/tvl-extra.css35
-rw-r--r--third_party/cgit/ui-atom.c157
-rw-r--r--third_party/cgit/ui-atom.h6
-rw-r--r--third_party/cgit/ui-blame.c315
-rw-r--r--third_party/cgit/ui-blame.h6
-rw-r--r--third_party/cgit/ui-blob.c182
-rw-r--r--third_party/cgit/ui-blob.h8
-rw-r--r--third_party/cgit/ui-clone.c126
-rw-r--r--third_party/cgit/ui-clone.h8
-rw-r--r--third_party/cgit/ui-commit.c148
-rw-r--r--third_party/cgit/ui-commit.h6
-rw-r--r--third_party/cgit/ui-diff.c505
-rw-r--r--third_party/cgit/ui-diff.h15
-rw-r--r--third_party/cgit/ui-log.c553
-rw-r--r--third_party/cgit/ui-log.h9
-rw-r--r--third_party/cgit/ui-patch.c98
-rw-r--r--third_party/cgit/ui-patch.h7
-rw-r--r--third_party/cgit/ui-plain.c207
-rw-r--r--third_party/cgit/ui-plain.h6
-rw-r--r--third_party/cgit/ui-refs.c219
-rw-r--r--third_party/cgit/ui-refs.h8
-rw-r--r--third_party/cgit/ui-repolist.c381
-rw-r--r--third_party/cgit/ui-repolist.h7
-rw-r--r--third_party/cgit/ui-shared.c1239
-rw-r--r--third_party/cgit/ui-shared.h87
-rw-r--r--third_party/cgit/ui-snapshot.c319
-rw-r--r--third_party/cgit/ui-snapshot.h7
-rw-r--r--third_party/cgit/ui-ssdiff.c420
-rw-r--r--third_party/cgit/ui-ssdiff.h25
-rw-r--r--third_party/cgit/ui-stats.c425
-rw-r--r--third_party/cgit/ui-stats.h28
-rw-r--r--third_party/cgit/ui-summary.c163
-rw-r--r--third_party/cgit/ui-summary.h7
-rw-r--r--third_party/cgit/ui-tag.c120
-rw-r--r--third_party/cgit/ui-tag.h6
-rw-r--r--third_party/cgit/ui-tree.c411
-rw-r--r--third_party/cgit/ui-tree.h6
-rw-r--r--third_party/clj2nix/OWNERS3
-rw-r--r--third_party/clj2nix/default.nix9
-rw-r--r--third_party/default.nix56
-rw-r--r--third_party/elmPackages_0_18/default.nix17
-rw-r--r--third_party/emacs/rcirc/default.nix7
-rw-r--r--third_party/emacs/rcirc/rcirc.el3133
-rw-r--r--third_party/exwm/.elpaignore1
-rw-r--r--third_party/exwm/.gitignore3
-rw-r--r--third_party/exwm/LICENSE674
-rw-r--r--third_party/exwm/README.md21
-rw-r--r--third_party/exwm/exwm-cm.el50
-rw-r--r--third_party/exwm/exwm-config.el131
-rw-r--r--third_party/exwm/exwm-core.el390
-rw-r--r--third_party/exwm/exwm-floating.el783
-rw-r--r--third_party/exwm/exwm-input.el1243
-rw-r--r--third_party/exwm/exwm-layout.el620
-rw-r--r--third_party/exwm/exwm-manage.el802
-rw-r--r--third_party/exwm/exwm-randr.el375
-rw-r--r--third_party/exwm/exwm-systemtray.el587
-rw-r--r--third_party/exwm/exwm-workspace.el1780
-rw-r--r--third_party/exwm/exwm-xim.el807
-rw-r--r--third_party/exwm/exwm.el1019
-rw-r--r--third_party/exwm/xinitrc20
-rwxr-xr-xthird_party/gerrit-queue/.buildkite/build.sh4
-rw-r--r--third_party/gerrit-queue/.buildkite/pipeline.yml13
-rw-r--r--third_party/gerrit-queue/.gitignore4
-rw-r--r--third_party/gerrit-queue/LICENSE201
-rw-r--r--third_party/gerrit-queue/README.md80
-rw-r--r--third_party/gerrit-queue/default.nix14
-rw-r--r--third_party/gerrit-queue/frontend/frontend.go113
-rw-r--r--third_party/gerrit-queue/frontend/templates/changeset.tmpl.html15
-rw-r--r--third_party/gerrit-queue/frontend/templates/index.tmpl.html76
-rw-r--r--third_party/gerrit-queue/frontend/templates/serie.tmpl.html19
-rw-r--r--third_party/gerrit-queue/gerrit/changeset.go117
-rw-r--r--third_party/gerrit-queue/gerrit/client.go220
-rw-r--r--third_party/gerrit-queue/gerrit/serie.go112
-rw-r--r--third_party/gerrit-queue/gerrit/series.go126
-rw-r--r--third_party/gerrit-queue/go.mod10
-rw-r--r--third_party/gerrit-queue/go.sum69
-rw-r--r--third_party/gerrit-queue/main.go137
-rw-r--r--third_party/gerrit-queue/misc/rotatingloghandler.go34
-rw-r--r--third_party/gerrit-queue/submitqueue/runner.go220
-rw-r--r--third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch25
-rw-r--r--third_party/gerrit/0002-Syntax-highlight-nix.patch24
-rw-r--r--third_party/gerrit/0003-Syntax-highlight-rules.pl.patch46
-rw-r--r--third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch217
-rw-r--r--third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch26
-rw-r--r--third_party/gerrit/0006-Always-use-Google-Fonts.patch28
-rw-r--r--third_party/gerrit/default.nix150
-rw-r--r--third_party/gerrit/detzip.go97
-rw-r--r--third_party/gerrit_plugins/builder.nix35
-rw-r--r--third_party/gerrit_plugins/default.nix23
-rw-r--r--third_party/gerrit_plugins/oauth/cas-6x.patch69
-rw-r--r--third_party/gerrit_plugins/oauth/default.nix27
-rw-r--r--third_party/git/0001-feat-third_party-git-date-add-dottime-format.patch119
-rw-r--r--third_party/git/default.nix9
-rw-r--r--third_party/gitignoreSource/default.nix21
-rw-r--r--third_party/gopkgs/cloud.google.com/go/default.nix11
-rw-r--r--third_party/gopkgs/github.com/cenkalti/backoff/default.nix12
-rw-r--r--third_party/gopkgs/github.com/charmbracelet/bubbles/default.nix16
-rw-r--r--third_party/gopkgs/github.com/charmbracelet/bubbletea/default.nix30
-rw-r--r--third_party/gopkgs/github.com/charmbracelet/lipgloss/default.nix21
-rw-r--r--third_party/gopkgs/github.com/containerd/console/default.nix15
-rw-r--r--third_party/gopkgs/github.com/davecgh/go-spew/default.nix11
-rw-r--r--third_party/gopkgs/github.com/emirpasic/gods/default.nix12
-rw-r--r--third_party/gopkgs/github.com/golang/glog/default.nix11
-rw-r--r--third_party/gopkgs/github.com/golang/groupcache/default.nix15
-rw-r--r--third_party/gopkgs/github.com/golang/protobuf/default.nix11
-rw-r--r--third_party/gopkgs/github.com/google/uuid/default.nix12
-rw-r--r--third_party/gopkgs/github.com/googleapis/gax-go/default.nix19
-rw-r--r--third_party/gopkgs/github.com/hashicorp/golang-lru/default.nix16
-rw-r--r--third_party/gopkgs/github.com/jbenet/go-context/default.nix16
-rw-r--r--third_party/gopkgs/github.com/kevinburke/ssh_config/default.nix12
-rw-r--r--third_party/gopkgs/github.com/lucasb-eyer/go-colorful/default.nix12
-rw-r--r--third_party/gopkgs/github.com/mattn/go-isatty/default.nix15
-rw-r--r--third_party/gopkgs/github.com/mattn/go-runewidth/default.nix15
-rw-r--r--third_party/gopkgs/github.com/mitchellh/go-homedir/default.nix12
-rw-r--r--third_party/gopkgs/github.com/muesli/reflow/default.nix16
-rw-r--r--third_party/gopkgs/github.com/muesli/termenv/default.nix19
-rw-r--r--third_party/gopkgs/github.com/pkg/browser/default.nix12
-rw-r--r--third_party/gopkgs/github.com/rivo/uniseg/default.nix14
-rw-r--r--third_party/gopkgs/github.com/sergi/go-diff/default.nix12
-rw-r--r--third_party/gopkgs/github.com/src-d/gcfg/default.nix16
-rw-r--r--third_party/gopkgs/github.com/xanzy/ssh-agent/default.nix16
-rw-r--r--third_party/gopkgs/go.opencensus.io/default.nix17
-rw-r--r--third_party/gopkgs/golang.org/x/crypto/default.nix15
-rw-r--r--third_party/gopkgs/golang.org/x/net/default.nix17
-rw-r--r--third_party/gopkgs/golang.org/x/oauth2/default.nix16
-rw-r--r--third_party/gopkgs/golang.org/x/sys/default.nix11
-rw-r--r--third_party/gopkgs/golang.org/x/text/default.nix11
-rw-r--r--third_party/gopkgs/golang.org/x/time/default.nix11
-rw-r--r--third_party/gopkgs/google.golang.org/api/default.nix22
-rw-r--r--third_party/gopkgs/google.golang.org/genproto/default.nix16
-rw-r--r--third_party/gopkgs/google.golang.org/grpc/default.nix23
-rw-r--r--third_party/gopkgs/googlemaps.github.io/maps.nix17
-rw-r--r--third_party/gopkgs/gopkg.in/irc.v3/default.nix12
-rw-r--r--third_party/gopkgs/gopkg.in/src-d/go-billy/default.nix16
-rw-r--r--third_party/gopkgs/gopkg.in/src-d/go-git/default.nix31
-rw-r--r--third_party/gopkgs/gopkg.in/warnings/default.nix12
-rw-r--r--third_party/hii/OWNERS3
-rw-r--r--third_party/impermanence/default.nix12
-rw-r--r--third_party/irccat/default.nix16
-rw-r--r--third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch43
-rw-r--r--third_party/josh/default.nix38
-rw-r--r--third_party/kernelPatches/trx40_usb_audio/default.nix9
-rw-r--r--third_party/kernelPatches/trx40_usb_audio/trx40_usb_audio.patch16
-rw-r--r--third_party/lisp/OWNERS5
-rw-r--r--third_party/lisp/alexandria.nix28
-rw-r--r--third_party/lisp/anaphora.nix13
-rw-r--r--third_party/lisp/asdf-flv/.gitattributes2
-rw-r--r--third_party/lisp/asdf-flv/.gitignore3
-rw-r--r--third_party/lisp/asdf-flv/Makefile77
-rw-r--r--third_party/lisp/asdf-flv/README.md7
-rw-r--r--third_party/lisp/asdf-flv/asdf-flv.lisp64
-rw-r--r--third_party/lisp/asdf-flv/default.nix13
-rw-r--r--third_party/lisp/asdf-flv/net.didierverna.asdf-flv.asd43
-rw-r--r--third_party/lisp/asdf-flv/package.lisp28
-rw-r--r--third_party/lisp/babel.nix31
-rw-r--r--third_party/lisp/bordeaux-threads.nix24
-rw-r--r--third_party/lisp/cffi.nix34
-rw-r--r--third_party/lisp/chipz.nix26
-rw-r--r--third_party/lisp/chunga.nix22
-rw-r--r--third_party/lisp/cl-ansi-text.nix16
-rw-r--r--third_party/lisp/cl-base64.nix14
-rw-r--r--third_party/lisp/cl-colors.nix16
-rw-r--r--third_party/lisp/cl-colors2.nix18
-rw-r--r--third_party/lisp/cl-date-time-parser.nix21
-rw-r--r--third_party/lisp/cl-fad.nix27
-rw-r--r--third_party/lisp/cl-json.nix29
-rw-r--r--third_party/lisp/cl-plus-ssl.nix50
-rw-r--r--third_party/lisp/cl-ppcre.nix27
-rw-r--r--third_party/lisp/cl-prevalence.nix25
-rw-r--r--third_party/lisp/cl-smtp.nix24
-rw-r--r--third_party/lisp/cl-unicode.nix80
-rw-r--r--third_party/lisp/cl-who.nix13
-rw-r--r--third_party/lisp/cl-yacc.nix17
-rw-r--r--third_party/lisp/closer-mop.nix19
-rw-r--r--third_party/lisp/closure-common.nix36
-rw-r--r--third_party/lisp/closure-html/default.nix65
-rw-r--r--third_party/lisp/closure-html/dtds-from-store.patch16
-rw-r--r--third_party/lisp/closure-html/no-double-defun.patch78
-rw-r--r--third_party/lisp/defclass-std.nix16
-rw-r--r--third_party/lisp/drakma.nix34
-rw-r--r--third_party/lisp/easy-routes.nix30
-rw-r--r--third_party/lisp/fiveam.nix29
-rw-r--r--third_party/lisp/flexi-streams.nix33
-rw-r--r--third_party/lisp/global-vars.nix7
-rw-r--r--third_party/lisp/hunchentoot.nix62
-rw-r--r--third_party/lisp/ironclad.nix163
-rw-r--r--third_party/lisp/iterate.nix12
-rw-r--r--third_party/lisp/lass.nix35
-rw-r--r--third_party/lisp/let-plus.nix15
-rw-r--r--third_party/lisp/lisp-binary.nix37
-rw-r--r--third_party/lisp/local-time.nix22
-rw-r--r--third_party/lisp/marshal.nix13
-rw-r--r--third_party/lisp/md5.nix16
-rw-r--r--third_party/lisp/metabang-bind.nix16
-rw-r--r--third_party/lisp/mime4cl/.skip-subtree1
-rw-r--r--third_party/lisp/mime4cl/OWNERS3
-rw-r--r--third_party/lisp/mime4cl/README7
-rw-r--r--third_party/lisp/mime4cl/address.lisp300
-rw-r--r--third_party/lisp/mime4cl/default.nix50
-rw-r--r--third_party/lisp/mime4cl/endec.lisp697
-rw-r--r--third_party/lisp/mime4cl/mime.lisp1075
-rw-r--r--third_party/lisp/mime4cl/mime4cl-tests.asd55
-rw-r--r--third_party/lisp/mime4cl/mime4cl.asd49
-rw-r--r--third_party/lisp/mime4cl/package.lisp110
-rw-r--r--third_party/lisp/mime4cl/streams.lisp355
-rw-r--r--third_party/lisp/mime4cl/test/address.lisp123
-rw-r--r--third_party/lisp/mime4cl/test/endec.lisp166
-rw-r--r--third_party/lisp/mime4cl/test/mime.lisp54
-rw-r--r--third_party/lisp/mime4cl/test/package.lisp27
-rw-r--r--third_party/lisp/mime4cl/test/rt.lisp254
-rw-r--r--third_party/lisp/mime4cl/test/sample1.msg86
-rw-r--r--third_party/lisp/moptilities.nix13
-rw-r--r--third_party/lisp/nibbles.nix26
-rw-r--r--third_party/lisp/npg/.project1
-rw-r--r--third_party/lisp/npg/.skip-subtree1
-rw-r--r--third_party/lisp/npg/COPYING504
-rw-r--r--third_party/lisp/npg/OWNERS3
-rw-r--r--third_party/lisp/npg/README48
-rw-r--r--third_party/lisp/npg/default.nix14
-rw-r--r--third_party/lisp/npg/examples/python.lisp336
-rw-r--r--third_party/lisp/npg/examples/vs-cobol-ii.lisp1901
-rw-r--r--third_party/lisp/npg/npg.asd55
-rw-r--r--third_party/lisp/npg/src/common.lisp79
-rw-r--r--third_party/lisp/npg/src/define.lisp408
-rw-r--r--third_party/lisp/npg/src/package.lisp50
-rw-r--r--third_party/lisp/npg/src/parser.lisp234
-rw-r--r--third_party/lisp/parse-float.nix15
-rw-r--r--third_party/lisp/parse-number.nix9
-rw-r--r--third_party/lisp/parseq.nix13
-rw-r--r--third_party/lisp/physical-quantities.nix24
-rw-r--r--third_party/lisp/postmodern.nix94
-rw-r--r--third_party/lisp/prove.nix29
-rw-r--r--third_party/lisp/puri.nix10
-rw-r--r--third_party/lisp/quasiquote_2/README.md258
-rw-r--r--third_party/lisp/quasiquote_2/default.nix17
-rw-r--r--third_party/lisp/quasiquote_2/macros.lisp15
-rw-r--r--third_party/lisp/quasiquote_2/package.lisp11
-rw-r--r--third_party/lisp/quasiquote_2/quasiquote-2.0.asd30
-rw-r--r--third_party/lisp/quasiquote_2/quasiquote-2.0.lisp340
-rw-r--r--third_party/lisp/quasiquote_2/readers.lisp77
-rw-r--r--third_party/lisp/quasiquote_2/tests-macro.lisp21
-rw-r--r--third_party/lisp/quasiquote_2/tests.lisp143
-rw-r--r--third_party/lisp/rfc2388.nix12
-rw-r--r--third_party/lisp/routes.nix39
-rw-r--r--third_party/lisp/s-sysdeps.nix18
-rw-r--r--third_party/lisp/s-xml/0001-fix-definition-order-in-xml.lisp.patch26
-rw-r--r--third_party/lisp/s-xml/default.nix25
-rw-r--r--third_party/lisp/sclf/.skip-subtree1
-rw-r--r--third_party/lisp/sclf/OWNERS3
-rw-r--r--third_party/lisp/sclf/README6
-rw-r--r--third_party/lisp/sclf/default.nix28
-rw-r--r--third_party/lisp/sclf/directory.lisp404
-rw-r--r--third_party/lisp/sclf/lazy.lisp134
-rw-r--r--third_party/lisp/sclf/mp/README6
-rw-r--r--third_party/lisp/sclf/mp/cmu.lisp115
-rw-r--r--third_party/lisp/sclf/mp/sbcl.lisp235
-rw-r--r--third_party/lisp/sclf/package.lisp258
-rw-r--r--third_party/lisp/sclf/sclf.asd58
-rw-r--r--third_party/lisp/sclf/sclf.lisp1717
-rw-r--r--third_party/lisp/sclf/serial.lisp62
-rw-r--r--third_party/lisp/sclf/sysproc.lisp295
-rw-r--r--third_party/lisp/sclf/time.lisp311
-rw-r--r--third_party/lisp/split-sequence.nix15
-rw-r--r--third_party/lisp/trivial-backtrace.nix15
-rw-r--r--third_party/lisp/trivial-features.nix13
-rw-r--r--third_party/lisp/trivial-garbage.nix9
-rw-r--r--third_party/lisp/trivial-gray-streams.nix13
-rw-r--r--third_party/lisp/trivial-indent.nix10
-rw-r--r--third_party/lisp/trivial-ldap.nix28
-rw-r--r--third_party/lisp/trivial-mimes.nix26
-rw-r--r--third_party/lisp/uax-15.nix43
-rw-r--r--third_party/lisp/unix-opts.nix12
-rw-r--r--third_party/lisp/usocket-server.nix19
-rw-r--r--third_party/lisp/usocket.nix46
-rw-r--r--third_party/naersk/default.nix3
-rw-r--r--third_party/nixpkgs/default.nix61
-rw-r--r--third_party/nsfv/default.nix23
-rw-r--r--third_party/overlays/dhall/OWNERS3
-rw-r--r--third_party/overlays/dhall/default.nix27
-rw-r--r--third_party/overlays/ecl-static.nix37
-rw-r--r--third_party/overlays/emacs.nix4
-rw-r--r--third_party/overlays/haskell/.skip-subtree1
-rw-r--r--third_party/overlays/haskell/default.nix31
-rw-r--r--third_party/overlays/haskell/extra-pkgs/random-fu-0.2.nix41
-rw-r--r--third_party/overlays/haskell/extra-pkgs/rvar-0.2.nix25
-rw-r--r--third_party/overlays/patches/notmuch-dottime.patch81
-rw-r--r--third_party/overlays/tvl.nix114
-rw-r--r--third_party/prometheus-fail2ban-exporter/default.nix18
-rw-r--r--third_party/python/broadlink/.gitignore1
-rw-r--r--third_party/python/broadlink/LICENSE22
-rw-r--r--third_party/python/broadlink/README.md112
-rw-r--r--third_party/python/broadlink/broadlink/__init__.py1118
-rw-r--r--third_party/python/broadlink/cli/README.md85
-rwxr-xr-xthird_party/python/broadlink/cli/broadlink_cli239
-rwxr-xr-xthird_party/python/broadlink/cli/broadlink_discovery27
-rw-r--r--third_party/python/broadlink/default.nix16
-rw-r--r--third_party/python/broadlink/protocol.md202
-rw-r--r--third_party/python/broadlink/requirements.txt1
-rw-r--r--third_party/python/broadlink/setup.py29
-rw-r--r--third_party/rust-crates/OWNERS5
-rw-r--r--third_party/rust-crates/default.nix423
-rw-r--r--third_party/rustsec-advisory-db/default.nix19
-rw-r--r--third_party/smtprelay/default.nix21
-rw-r--r--third_party/sources/default.nix151
-rw-r--r--third_party/sources/sources.json74
-rw-r--r--third_party/terraform-provider-glesys/default.nix19
-rw-r--r--tools/cheddar/.gitignore1
-rw-r--r--tools/cheddar/.skip-subtree0
-rw-r--r--tools/cheddar/Cargo.lock1194
-rw-r--r--tools/cheddar/Cargo.toml18
-rw-r--r--tools/cheddar/README.md21
-rw-r--r--tools/cheddar/build.rs55
-rw-r--r--tools/cheddar/default.nix23
-rw-r--r--tools/cheddar/src/bin/cheddar.rs134
-rw-r--r--tools/cheddar/src/lib.rs339
-rw-r--r--tools/cheddar/src/tests.rs105
-rw-r--r--tools/crfo-approve.nix52
-rw-r--r--tools/depot-deps.nix27
-rw-r--r--tools/depot-scanner/OWNERS3
-rw-r--r--tools/depot-scanner/default.nix18
-rw-r--r--tools/depot-scanner/depot_scanner.proto46
-rw-r--r--tools/depot-scanner/go.mod3
-rw-r--r--tools/depot-scanner/main.go227
-rw-r--r--tools/depotfmt.nix60
-rw-r--r--tools/emacs-pkgs/buildEmacsPackage.nix38
-rw-r--r--tools/emacs-pkgs/defzone/defzone.el60
-rw-r--r--tools/emacs-pkgs/defzone/example.el45
-rw-r--r--tools/emacs-pkgs/dottime/default.nix7
-rw-r--r--tools/emacs-pkgs/dottime/dottime.el81
-rw-r--r--tools/emacs-pkgs/nix-util/default.nix8
-rw-r--r--tools/emacs-pkgs/nix-util/nix-util.el69
-rw-r--r--tools/emacs-pkgs/notable/OWNERS2
-rw-r--r--tools/emacs-pkgs/notable/default.nix17
-rw-r--r--tools/emacs-pkgs/notable/notable.el251
-rw-r--r--tools/emacs-pkgs/passively/OWNERS3
-rw-r--r--tools/emacs-pkgs/passively/README.md76
-rw-r--r--tools/emacs-pkgs/passively/default.nix8
-rw-r--r--tools/emacs-pkgs/passively/passively.el121
-rw-r--r--tools/emacs-pkgs/term-switcher/default.nix8
-rw-r--r--tools/emacs-pkgs/term-switcher/term-switcher.el57
-rw-r--r--tools/emacs-pkgs/tvl/OWNERS3
-rw-r--r--tools/emacs-pkgs/tvl/default.nix8
-rw-r--r--tools/emacs-pkgs/tvl/tvl.el222
-rw-r--r--tools/eprintf.nix15
-rw-r--r--tools/gerrit-cli.nix13
-rw-r--r--tools/gerrit-update.nix34
-rw-r--r--tools/hash-password.nix15
-rw-r--r--tools/magrathea/default.nix23
-rw-r--r--tools/magrathea/mg.scm359
-rw-r--r--tools/nixery/.gitignore12
-rw-r--r--tools/nixery/.skip-subtree1
-rw-r--r--tools/nixery/LICENSE202
-rw-r--r--tools/nixery/README.md156
-rw-r--r--tools/nixery/builder/archive.go104
-rw-r--r--tools/nixery/builder/builder.go519
-rw-r--r--tools/nixery/builder/builder_test.go112
-rw-r--r--tools/nixery/builder/cache.go225
-rw-r--r--tools/nixery/cmd/server/main.go281
-rw-r--r--tools/nixery/config/config.go73
-rw-r--r--tools/nixery/config/pkgsource.go148
-rw-r--r--tools/nixery/default.nix129
-rw-r--r--tools/nixery/docs/.gitignore1
-rw-r--r--tools/nixery/docs/book.toml8
-rw-r--r--tools/nixery/docs/default.nix26
-rw-r--r--tools/nixery/docs/src/SUMMARY.md8
-rw-r--r--tools/nixery/docs/src/caching.md69
-rw-r--r--tools/nixery/docs/src/nix-1p.md2
-rw-r--r--tools/nixery/docs/src/nix.md31
-rw-r--r--tools/nixery/docs/src/nixery-logo.pngbin0 -> 194098 bytes
-rw-r--r--tools/nixery/docs/src/nixery.md80
-rw-r--r--tools/nixery/docs/src/run-your-own.md194
-rw-r--r--tools/nixery/docs/src/under-the-hood.md129
-rw-r--r--tools/nixery/docs/theme/favicon.pngbin0 -> 16053 bytes
-rw-r--r--tools/nixery/docs/theme/nixery.css3
-rw-r--r--tools/nixery/go.mod13
-rw-r--r--tools/nixery/go.sum706
-rw-r--r--tools/nixery/layers/layers.go353
-rw-r--r--tools/nixery/logs/logs.go108
-rw-r--r--tools/nixery/manifest/manifest.go135
-rw-r--r--tools/nixery/popcount/README.md39
-rw-r--r--tools/nixery/popcount/default.nix13
-rw-r--r--tools/nixery/popcount/popcount.go280
-rw-r--r--tools/nixery/prepare-image/default.nix18
-rw-r--r--tools/nixery/prepare-image/load-pkgs.nix36
-rw-r--r--tools/nixery/prepare-image/prepare-image.nix185
-rwxr-xr-xtools/nixery/scripts/integration-test.sh59
-rw-r--r--tools/nixery/shell.nix13
-rw-r--r--tools/nixery/storage/filesystem.go99
-rw-r--r--tools/nixery/storage/gcs.go231
-rw-r--r--tools/nixery/storage/storage.go40
-rw-r--r--tools/nsfv-setup/default.nix29
-rw-r--r--tools/perf-flamegraph.nix12
-rw-r--r--tools/releases/default.nix37
-rw-r--r--tools/rust-crates-advisory/OWNERS4
-rw-r--r--tools/rust-crates-advisory/check-security-advisory.rs119
-rw-r--r--tools/rust-crates-advisory/default.nix200
-rw-r--r--tools/rust-crates-advisory/format-audit-result.jq75
-rw-r--r--tools/tvlc/OWNERS3
-rw-r--r--tools/tvlc/common.sh33
-rw-r--r--tools/tvlc/default.nix51
-rwxr-xr-xtools/tvlc/tvlc-new103
-rw-r--r--tvix/.envrc10
-rw-r--r--tvix/.gitignore3
-rw-r--r--tvix/.vscode/extensions.json8
-rw-r--r--tvix/Cargo.lock248
-rw-r--r--tvix/Cargo.toml14
-rw-r--r--tvix/LICENSE674
-rw-r--r--tvix/OWNERS5
-rw-r--r--tvix/README.md20
-rw-r--r--tvix/default.nix2
-rw-r--r--tvix/docs/.gitignore2
-rw-r--r--tvix/docs/Makefile12
-rw-r--r--tvix/docs/component-flow.puml74
-rw-r--r--tvix/docs/components.md114
-rw-r--r--tvix/docs/default.nix47
-rw-r--r--tvix/docs/language-spec.md78
-rw-r--r--tvix/proto/LICENSE21
-rw-r--r--tvix/proto/default.nix9
-rw-r--r--tvix/proto/evaluator.proto144
-rw-r--r--tvix/shell.nix11
-rw-r--r--tvix/src/bin/nix-store.rs104
-rw-r--r--tvix/src/main.rs3
-rw-r--r--users/Profpatsch/OWNERS4
-rw-r--r--users/Profpatsch/advent-of-code/2020/01/main.py22
-rw-r--r--users/Profpatsch/advent-of-code/2020/02/main.py77
-rw-r--r--users/Profpatsch/advent-of-code/2020/03/main.py66
-rw-r--r--users/Profpatsch/advent-of-code/2020/04/main.py104
-rw-r--r--users/Profpatsch/aerc-no-config-perms.patch12
-rw-r--r--users/Profpatsch/aerc.dhall157
-rw-r--r--users/Profpatsch/aerc.nix51
-rw-r--r--users/Profpatsch/alacritty.dhall48
-rw-r--r--users/Profpatsch/alacritty.nix34
-rw-r--r--users/Profpatsch/aliases.nix75
-rw-r--r--users/Profpatsch/arglib/netencode.nix42
-rw-r--r--users/Profpatsch/atomically-write.nix29
-rw-r--r--users/Profpatsch/blog/default.nix472
-rw-r--r--users/Profpatsch/blog/notes/an-idealized-conflang.md298
-rw-r--r--users/Profpatsch/blog/notes/preventing-oom.md33
-rw-r--r--users/Profpatsch/blog/notes/rust-string-conversions.md53
-rw-r--r--users/Profpatsch/blog/posts/2017-05-04-ligature-emulation-in-emacs.md123
-rw-r--r--users/Profpatsch/cdb.nix93
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/default.nix3
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/shell.nix17
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/test.json14
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/test.py13
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/test.sh14
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/tmp.el28
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/tree-sitter-move.el139
-rw-r--r--users/Profpatsch/exactSource.nix90
-rw-r--r--users/Profpatsch/execline/default.nix37
-rw-r--r--users/Profpatsch/execline/exec_helpers.rs149
-rw-r--r--users/Profpatsch/git-db/default.nix10
-rw-r--r--users/Profpatsch/git-db/git-db.rs90
-rw-r--r--users/Profpatsch/imap-idle.nix17
-rw-r--r--users/Profpatsch/imap-idle.rs140
-rw-r--r--users/Profpatsch/importDhall.nix93
-rw-r--r--users/Profpatsch/lens.nix137
-rw-r--r--users/Profpatsch/lib.nix108
-rw-r--r--users/Profpatsch/netencode/README.md115
-rw-r--r--users/Profpatsch/netencode/default.nix160
-rw-r--r--users/Profpatsch/netencode/gen.nix73
-rw-r--r--users/Profpatsch/netencode/netencode-mustache.rs52
-rw-r--r--users/Profpatsch/netencode/netencode.rs891
-rw-r--r--users/Profpatsch/netencode/pretty.rs163
-rw-r--r--users/Profpatsch/netstring/README.md18
-rw-r--r--users/Profpatsch/netstring/default.nix69
-rw-r--r--users/Profpatsch/netstring/tests/default.nix64
-rw-r--r--users/Profpatsch/nix-home/default.nix203
-rw-r--r--users/Profpatsch/nixpkgs-rewriter/MetaStdenvLib.hs80
-rw-r--r--users/Profpatsch/nixpkgs-rewriter/default.nix148
-rw-r--r--users/Profpatsch/read-http.nix19
-rw-r--r--users/Profpatsch/read-http.rs249
-rw-r--r--users/Profpatsch/reverse-haskell-deps.hs72
-rw-r--r--users/Profpatsch/reverse-haskell-deps.nix31
-rw-r--r--users/Profpatsch/solarized.dhall39
-rw-r--r--users/Profpatsch/struct-edit/default.nix13
-rw-r--r--users/Profpatsch/struct-edit/main.go431
-rw-r--r--users/Profpatsch/toINI.nix79
-rw-r--r--users/Profpatsch/tree-sitter.nix209
-rw-r--r--users/Profpatsch/writers/default.nix97
-rw-r--r--users/Profpatsch/writers/tests/default.nix56
-rw-r--r--users/Profpatsch/ytextr/create-symlink-farm.nix19
-rw-r--r--users/Profpatsch/ytextr/default.nix82
-rw-r--r--users/cynthia/OWNERS3
-rw-r--r--users/cynthia/keys.nix7
-rw-r--r--users/edef/OWNERS3
-rw-r--r--users/edef/depot-scan/default.nix12
-rwxr-xr-xusers/edef/depot-scan/depot-scan.pl11
-rw-r--r--users/edef/depot-scan/wrap.nix16
-rw-r--r--users/edef/keys.nix7
-rw-r--r--users/ericvolp12/OWNERS3
-rw-r--r--users/eta/OWNERS3
-rw-r--r--users/eta/keys.nix12
-rw-r--r--users/firefly/OWNERS3
-rw-r--r--users/firefly/keys.nix7
-rw-r--r--users/flokli/OWNERS3
-rw-r--r--users/flokli/keys.nix7
-rw-r--r--users/grfn/OWNERS3
-rw-r--r--users/grfn/achilles/.envrc1
-rw-r--r--users/grfn/achilles/.gitignore1
-rw-r--r--users/grfn/achilles/Cargo.lock885
-rw-r--r--users/grfn/achilles/Cargo.toml26
-rw-r--r--users/grfn/achilles/ach/.gitignore7
-rw-r--r--users/grfn/achilles/ach/Makefile15
-rw-r--r--users/grfn/achilles/ach/externs.ach5
-rw-r--r--users/grfn/achilles/ach/functions.ach8
-rw-r--r--users/grfn/achilles/ach/simple.ach1
-rw-r--r--users/grfn/achilles/ach/units.ach7
-rw-r--r--users/grfn/achilles/default.nix27
-rw-r--r--users/grfn/achilles/shell.nix18
-rw-r--r--users/grfn/achilles/src/ast/hir.rs364
-rw-r--r--users/grfn/achilles/src/ast/mod.rs484
-rw-r--r--users/grfn/achilles/src/codegen/llvm.rs486
-rw-r--r--users/grfn/achilles/src/codegen/mod.rs25
-rw-r--r--users/grfn/achilles/src/commands/check.rs39
-rw-r--r--users/grfn/achilles/src/commands/compile.rs31
-rw-r--r--users/grfn/achilles/src/commands/eval.rs28
-rw-r--r--users/grfn/achilles/src/commands/mod.rs7
-rw-r--r--users/grfn/achilles/src/common/env.rs59
-rw-r--r--users/grfn/achilles/src/common/error.rs59
-rw-r--r--users/grfn/achilles/src/common/mod.rs6
-rw-r--r--users/grfn/achilles/src/common/namer.rs122
-rw-r--r--users/grfn/achilles/src/compiler.rs89
-rw-r--r--users/grfn/achilles/src/interpreter/error.rs19
-rw-r--r--users/grfn/achilles/src/interpreter/mod.rs203
-rw-r--r--users/grfn/achilles/src/interpreter/value.rs224
-rw-r--r--users/grfn/achilles/src/main.rs36
-rw-r--r--users/grfn/achilles/src/parser/expr.rs717
-rw-r--r--users/grfn/achilles/src/parser/macros.rs16
-rw-r--r--users/grfn/achilles/src/parser/mod.rs240
-rw-r--r--users/grfn/achilles/src/parser/type_.rs152
-rw-r--r--users/grfn/achilles/src/parser/util.rs8
-rw-r--r--users/grfn/achilles/src/passes/hir/mod.rs211
-rw-r--r--users/grfn/achilles/src/passes/hir/monomorphize.rs139
-rw-r--r--users/grfn/achilles/src/passes/hir/strip_positive_units.rs191
-rw-r--r--users/grfn/achilles/src/passes/mod.rs1
-rw-r--r--users/grfn/achilles/src/tc/mod.rs808
-rw-r--r--users/grfn/achilles/tests/compile.rs79
-rw-r--r--users/grfn/bbbg/.clj-kondo/config.edn1
-rw-r--r--users/grfn/bbbg/.envrc1
-rw-r--r--users/grfn/bbbg/.gitignore9
-rw-r--r--users/grfn/bbbg/Makefile2
-rw-r--r--users/grfn/bbbg/README.md129
-rw-r--r--users/grfn/bbbg/arion-compose.nix15
-rw-r--r--users/grfn/bbbg/arion-pkgs.nix2
-rw-r--r--users/grfn/bbbg/default.nix82
-rw-r--r--users/grfn/bbbg/deps.edn70
-rw-r--r--users/grfn/bbbg/deps.nix1494
-rw-r--r--users/grfn/bbbg/env/dev/bbbg-signup/env.clj3
-rw-r--r--users/grfn/bbbg/env/dev/logback.xml15
-rw-r--r--users/grfn/bbbg/env/prod/bbbg-signup/env.clj3
-rw-r--r--users/grfn/bbbg/env/prod/logback.xml31
-rw-r--r--users/grfn/bbbg/env/test/bbbg-signup/env.clj3
-rw-r--r--users/grfn/bbbg/env/test/logback.xml11
-rw-r--r--users/grfn/bbbg/module.nix137
-rw-r--r--users/grfn/bbbg/pom.xml42
-rw-r--r--users/grfn/bbbg/resources/base.css152
-rw-r--r--users/grfn/bbbg/resources/migrations/20211212165646-init-schema.down.sql14
-rw-r--r--users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql32
-rw-r--r--users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql1
-rw-r--r--users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql7
-rw-r--r--users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql1
-rw-r--r--users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql2
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woffbin0 -> 23576 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2bin0 -> 19272 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woffbin0 -> 24056 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2bin0 -> 19624 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woffbin0 -> 23628 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2bin0 -> 19264 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woffbin0 -> 23872 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2bin0 -> 19440 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woffbin0 -> 24404 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2bin0 -> 19836 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woffbin0 -> 24012 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2bin0 -> 19660 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woffbin0 -> 23480 bytes
-rw-r--r--users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2bin0 -> 19172 bytes
-rw-r--r--users/grfn/bbbg/resources/public/main.js73
-rw-r--r--users/grfn/bbbg/resources/public/robots.txt2
-rw-r--r--users/grfn/bbbg/shell.nix29
-rw-r--r--users/grfn/bbbg/src/bbbg/attendee.clj10
-rw-r--r--users/grfn/bbbg/src/bbbg/attendee_check.clj4
-rw-r--r--users/grfn/bbbg/src/bbbg/core.clj69
-rw-r--r--users/grfn/bbbg/src/bbbg/db.clj366
-rw-r--r--users/grfn/bbbg/src/bbbg/db/attendee.clj85
-rw-r--r--users/grfn/bbbg/src/bbbg/db/attendee_check.clj55
-rw-r--r--users/grfn/bbbg/src/bbbg/db/event.clj94
-rw-r--r--users/grfn/bbbg/src/bbbg/db/event_attendee.clj17
-rw-r--r--users/grfn/bbbg/src/bbbg/db/user.clj19
-rw-r--r--users/grfn/bbbg/src/bbbg/discord.clj44
-rw-r--r--users/grfn/bbbg/src/bbbg/discord/auth.clj90
-rw-r--r--users/grfn/bbbg/src/bbbg/event.clj4
-rw-r--r--users/grfn/bbbg/src/bbbg/event_attendee.clj6
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj68
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/attendees.clj162
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/core.clj91
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/events.clj259
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/home.clj52
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/signup_form.clj93
-rw-r--r--users/grfn/bbbg/src/bbbg/meetup/import.clj125
-rw-r--r--users/grfn/bbbg/src/bbbg/meetup_user.clj6
-rw-r--r--users/grfn/bbbg/src/bbbg/styles.clj407
-rw-r--r--users/grfn/bbbg/src/bbbg/user.clj8
-rw-r--r--users/grfn/bbbg/src/bbbg/util/core.clj138
-rw-r--r--users/grfn/bbbg/src/bbbg/util/dev_secrets.clj59
-rw-r--r--users/grfn/bbbg/src/bbbg/util/display.clj23
-rw-r--r--users/grfn/bbbg/src/bbbg/util/spec.clj16
-rw-r--r--users/grfn/bbbg/src/bbbg/util/sql.clj5
-rw-r--r--users/grfn/bbbg/src/bbbg/util/time.clj152
-rw-r--r--users/grfn/bbbg/src/bbbg/views/flash.clj39
-rw-r--r--users/grfn/bbbg/src/bbbg/web.clj140
-rw-r--r--users/grfn/bbbg/test/bbbg/meetup/import_test.clj7
-rw-r--r--users/grfn/bbbg/tf.nix96
-rw-r--r--users/grfn/emacs.d/+bindings.el1430
-rw-r--r--users/grfn/emacs.d/+commands.el149
-rw-r--r--users/grfn/emacs.d/+private.el.gpgbin0 -> 1115 bytes
-rw-r--r--users/grfn/emacs.d/.gitignore2
-rw-r--r--users/grfn/emacs.d/autoload/evil.el37
-rw-r--r--users/grfn/emacs.d/autoload/hlissner.el53
-rw-r--r--users/grfn/emacs.d/clocked-in-elt.el17
-rw-r--r--users/grfn/emacs.d/clojure.el53
-rw-r--r--users/grfn/emacs.d/company-sql.el299
-rw-r--r--users/grfn/emacs.d/config.el1121
-rw-r--r--users/grfn/emacs.d/cpp.el39
-rw-r--r--users/grfn/emacs.d/email.el42
-rw-r--r--users/grfn/emacs.d/github-org.el99
-rw-r--r--users/grfn/emacs.d/google-c-style.el151
-rw-r--r--users/grfn/emacs.d/grid.el128
-rw-r--r--users/grfn/emacs.d/init.el172
-rw-r--r--users/grfn/emacs.d/irc.el131
-rw-r--r--users/grfn/emacs.d/lisp.el38
-rwxr-xr-xusers/grfn/emacs.d/nix-clangd.sh7
-rw-r--r--users/grfn/emacs.d/nix.el30
-rw-r--r--users/grfn/emacs.d/org-alerts.el188
-rw-r--r--users/grfn/emacs.d/org-config.el193
-rw-r--r--users/grfn/emacs.d/org-gcal.el181
-rw-r--r--users/grfn/emacs.d/org-query.el131
-rw-r--r--users/grfn/emacs.d/packages.el153
-rw-r--r--users/grfn/emacs.d/rust.el40
-rw-r--r--users/grfn/emacs.d/show-matching-paren.el61
-rw-r--r--users/grfn/emacs.d/slack-snippets.el227
-rw-r--r--users/grfn/emacs.d/slack.el24
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/annotation5
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/benchmark-module26
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/header5
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/hedgehog-generator8
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/hedgehog-property9
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/hlint8
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/import-i4
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/inl6
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/inline5
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/language pragma6
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/lens.field7
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/module32
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/shut up, hlint6
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/test-group9
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/test-module27
-rw-r--r--users/grfn/emacs.d/snippets/haskell-mode/undefined6
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/action-type4
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/before7
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/context7
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/describe6
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/expect5
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/function6
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/header6
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/it7
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/it-pending5
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/module12
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/record7
-rw-r--r--users/grfn/emacs.d/snippets/js2-mode/test7
-rw-r--r--users/grfn/emacs.d/snippets/nix-mode/fetchFromGitHub12
-rw-r--r--users/grfn/emacs.d/snippets/nix-mode/pythonPackage16
-rw-r--r--users/grfn/emacs.d/snippets/nix-mode/sha2567
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/SQL source block6
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/combat13
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/date5
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/date-time5
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/description7
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/nologdone5
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/python source block6
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/reveal6
-rw-r--r--users/grfn/emacs.d/snippets/org-mode/transaction7
-rw-r--r--users/grfn/emacs.d/snippets/prolog-mode/use-module7
-rw-r--r--users/grfn/emacs.d/snippets/python-mode/add_column5
-rw-r--r--users/grfn/emacs.d/snippets/python-mode/decorate15
-rw-r--r--users/grfn/emacs.d/snippets/python-mode/dunder7
-rw-r--r--users/grfn/emacs.d/snippets/python-mode/name7
-rw-r--r--users/grfn/emacs.d/snippets/python-mode/op.get_bind.execute7
-rw-r--r--users/grfn/emacs.d/snippets/python-mode/pdb7
-rw-r--r--users/grfn/emacs.d/snippets/rust-mode/#[macro_use]5
-rw-r--r--users/grfn/emacs.d/snippets/rust-mode/async test10
-rw-r--r--users/grfn/emacs.d/snippets/rust-mode/benchmark10
-rw-r--r--users/grfn/emacs.d/snippets/rust-mode/proptest10
-rw-r--r--users/grfn/emacs.d/snippets/rust-mode/tests9
-rw-r--r--users/grfn/emacs.d/snippets/snippet-mode/indent5
-rw-r--r--users/grfn/emacs.d/snippets/sql-mode/count(*) group by5
-rw-r--r--users/grfn/emacs.d/snippets/terraform-mode/variable11
-rw-r--r--users/grfn/emacs.d/snippets/text-mode/date5
-rw-r--r--users/grfn/emacs.d/splitjoin.el192
-rw-r--r--users/grfn/emacs.d/sql-strings.el75
-rw-r--r--users/grfn/emacs.d/terraform.el31
-rw-r--r--users/grfn/emacs.d/tests/splitjoin_test.el68
-rw-r--r--users/grfn/emacs.d/themes/grfn-solarized-light-theme.el115
-rw-r--r--users/grfn/emacs.d/utils.el114
-rw-r--r--users/grfn/emacs.d/vterm.el24
-rw-r--r--users/grfn/gws.fyi/.envrc1
-rw-r--r--users/grfn/gws.fyi/.gitignore3
-rw-r--r--users/grfn/gws.fyi/Makefile31
-rw-r--r--users/grfn/gws.fyi/config.el6
-rw-r--r--users/grfn/gws.fyi/default.nix37
-rw-r--r--users/grfn/gws.fyi/index.org40
-rw-r--r--users/grfn/gws.fyi/main.css139
-rw-r--r--users/grfn/gws.fyi/orgExportHTML.nix67
-rw-r--r--users/grfn/gws.fyi/recipes/tomato-sauce.org102
-rw-r--r--users/grfn/gws.fyi/shell.nix9
-rw-r--r--users/grfn/gws.fyi/site.nix12
-rw-r--r--users/grfn/keyboard/.gitignore1
-rw-r--r--users/grfn/keyboard/README.org10
-rw-r--r--users/grfn/keyboard/default.nix63
-rwxr-xr-xusers/grfn/keyboard/flash2
-rw-r--r--users/grfn/keyboard/increase-tapping-delay.patch13
-rw-r--r--users/grfn/keyboard/keymap.c206
-rw-r--r--users/grfn/keys.nix6
-rw-r--r--users/grfn/org-clubhouse/.gitignore3
-rw-r--r--users/grfn/org-clubhouse/CODE_OF_CONDUCT.org101
-rw-r--r--users/grfn/org-clubhouse/LICENSE7
-rw-r--r--users/grfn/org-clubhouse/README.org142
-rw-r--r--users/grfn/org-clubhouse/org-clubhouse.el1241
-rw-r--r--users/grfn/resume/chimera.pngbin0 -> 40602 bytes
-rw-r--r--users/grfn/resume/collection.sty85
-rw-r--r--users/grfn/resume/default.nix40
-rw-r--r--users/grfn/resume/helvetica.sty32
-rw-r--r--users/grfn/resume/moderncv.cls585
-rw-r--r--users/grfn/resume/moderncvcolorblack.sty27
-rw-r--r--users/grfn/resume/moderncvcolorblue.sty27
-rw-r--r--users/grfn/resume/moderncvcolorgreen.sty27
-rw-r--r--users/grfn/resume/moderncvcolorgrey.sty27
-rw-r--r--users/grfn/resume/moderncvcolororange.sty27
-rw-r--r--users/grfn/resume/moderncvcolorpurple.sty27
-rw-r--r--users/grfn/resume/moderncvcolorred.sty27
-rw-r--r--users/grfn/resume/moderncvcompatibility.sty104
-rw-r--r--users/grfn/resume/moderncviconsletters.sty50
-rw-r--r--users/grfn/resume/moderncviconsmarvosym.sty48
-rw-r--r--users/grfn/resume/moderncvstylebanking.sty287
-rw-r--r--users/grfn/resume/moderncvstylecasual.sty182
-rw-r--r--users/grfn/resume/moderncvstyleclassic.sty294
-rw-r--r--users/grfn/resume/moderncvstyleempty.sty34
-rw-r--r--users/grfn/resume/moderncvstyleoldstyle.sty306
-rw-r--r--users/grfn/resume/picture.pngbin0 -> 14848 bytes
-rw-r--r--users/grfn/resume/resume.tex212
-rw-r--r--users/grfn/resume/tweaklist.sty56
-rw-r--r--users/grfn/secrets/.envrc1
-rw-r--r--users/grfn/secrets/bbbg.age12
-rw-r--r--users/grfn/secrets/buildkite-ssh-key.agebin0 -> 3853 bytes
-rw-r--r--users/grfn/secrets/buildkite-token.age12
-rw-r--r--users/grfn/secrets/cloudflare.age9
-rw-r--r--users/grfn/secrets/ddclient-password.age9
-rw-r--r--users/grfn/secrets/default.nix2
-rw-r--r--users/grfn/secrets/secrets.nix13
-rw-r--r--users/grfn/secrets/shell.nix8
-rw-r--r--users/grfn/system/.gitignore1
-rw-r--r--users/grfn/system/home/.skip-subtree0
-rw-r--r--users/grfn/system/home/common/solarized.nix18
-rw-r--r--users/grfn/system/home/default.nix32
-rw-r--r--users/grfn/system/home/home.nix20
-rw-r--r--users/grfn/system/home/machines/dobharchu.nix17
-rw-r--r--users/grfn/system/home/machines/roswell.nix55
-rw-r--r--users/grfn/system/home/machines/yeren.nix82
-rw-r--r--users/grfn/system/home/modules/alacritty.nix56
-rw-r--r--users/grfn/system/home/modules/alsi.nix58
-rw-r--r--users/grfn/system/home/modules/common.nix109
-rw-r--r--users/grfn/system/home/modules/development.nix210
-rw-r--r--users/grfn/system/home/modules/development/agda.nix58
-rw-r--r--users/grfn/system/home/modules/development/kube.nix34
-rw-r--r--users/grfn/system/home/modules/development/readyset.nix31
-rw-r--r--users/grfn/system/home/modules/development/rust.nix34
-rw-r--r--users/grfn/system/home/modules/emacs.nix109
-rw-r--r--users/grfn/system/home/modules/email.nix93
-rw-r--r--users/grfn/system/home/modules/firefox.nix22
-rw-r--r--users/grfn/system/home/modules/games.nix61
-rw-r--r--users/grfn/system/home/modules/i3.nix380
-rw-r--r--users/grfn/system/home/modules/lib/cloneRepo.nix73
-rw-r--r--users/grfn/system/home/modules/lib/zshFunctions.nix23
-rw-r--r--users/grfn/system/home/modules/nixos-logo.txt26
-rw-r--r--users/grfn/system/home/modules/obs.nix68
-rw-r--r--users/grfn/system/home/modules/ptt.nix44
-rwxr-xr-xusers/grfn/system/home/modules/pure.zsh-theme151
-rw-r--r--users/grfn/system/home/modules/rtlsdr.nix23
-rw-r--r--users/grfn/system/home/modules/shell.nix185
-rw-r--r--users/grfn/system/home/modules/tarsnap.nix64
-rw-r--r--users/grfn/system/home/modules/twitter.nix23
-rw-r--r--users/grfn/system/home/modules/vim.nix48
-rw-r--r--users/grfn/system/home/modules/vimrc1121
-rw-r--r--users/grfn/system/home/modules/zshrc327
-rw-r--r--users/grfn/system/home/platforms/darwin.nix26
-rw-r--r--users/grfn/system/home/platforms/linux.nix92
-rwxr-xr-xusers/grfn/system/install35
-rw-r--r--users/grfn/system/system/.skip-subtree0
-rw-r--r--users/grfn/system/system/configuration.nix11
-rw-r--r--users/grfn/system/system/default.nix38
-rw-r--r--users/grfn/system/system/iso.nix18
-rw-r--r--users/grfn/system/system/machines/bumblebee.nix23
-rw-r--r--users/grfn/system/system/machines/mugwump.nix291
-rw-r--r--users/grfn/system/system/machines/roswell.nix17
-rw-r--r--users/grfn/system/system/machines/yeren.nix137
-rw-r--r--users/grfn/system/system/modules/common.nix87
-rw-r--r--users/grfn/system/system/modules/desktop.nix19
-rw-r--r--users/grfn/system/system/modules/development.nix6
-rw-r--r--users/grfn/system/system/modules/fcitx.nix10
-rw-r--r--users/grfn/system/system/modules/fonts.nix12
-rw-r--r--users/grfn/system/system/modules/laptop.nix15
-rw-r--r--users/grfn/system/system/modules/reusable/README.org2
-rw-r--r--users/grfn/system/system/modules/reusable/battery.nix32
-rw-r--r--users/grfn/system/system/modules/rtlsdr.nix17
-rw-r--r--users/grfn/system/system/modules/sound.nix16
-rw-r--r--users/grfn/system/system/modules/tvl.nix39
-rw-r--r--users/grfn/system/system/modules/work/kolide.debbin0 -> 25094998 bytes
-rw-r--r--users/grfn/system/system/modules/work/kolide.nix51
-rw-r--r--users/grfn/system/system/modules/xserver.nix16
-rw-r--r--users/grfn/terraform/globals.nix27
-rw-r--r--users/grfn/terraform/nixosMachine.nix208
-rw-r--r--users/grfn/terraform/workspace.nix107
-rw-r--r--users/grfn/wigglydonke.rs/index.html16
-rw-r--r--users/grfn/wigglydonke.rs/wd.pngbin0 -> 1624030 bytes
-rw-r--r--users/grfn/xanthous/.envrc1
-rw-r--r--users/grfn/xanthous/.github/actions/nix-build/Dockerfile23
-rwxr-xr-xusers/grfn/xanthous/.github/actions/nix-build/entrypoint.sh24
-rw-r--r--users/grfn/xanthous/.github/workflows/haskell.yml15
-rw-r--r--users/grfn/xanthous/.gitignore37
-rw-r--r--users/grfn/xanthous/LICENSE674
-rw-r--r--users/grfn/xanthous/README.org36
-rw-r--r--users/grfn/xanthous/Setup.hs2
-rw-r--r--users/grfn/xanthous/app/Main.hs171
-rw-r--r--users/grfn/xanthous/bench/Bench.hs12
-rw-r--r--users/grfn/xanthous/bench/Bench/Prelude.hs9
-rw-r--r--users/grfn/xanthous/bench/Xanthous/Generators/UtilBench.hs37
-rw-r--r--users/grfn/xanthous/bench/Xanthous/RandomBench.hs32
-rw-r--r--users/grfn/xanthous/build/generic-arbitrary-export-garbitrary.patch12
-rw-r--r--users/grfn/xanthous/build/hgeometry-fix-haddock.patch13
-rw-r--r--users/grfn/xanthous/build/update-comonad-extras.patch92
-rw-r--r--users/grfn/xanthous/default.nix27
-rw-r--r--users/grfn/xanthous/docs/raw-types.org24
-rw-r--r--users/grfn/xanthous/hie.yaml10
-rw-r--r--users/grfn/xanthous/nixpkgs.nix3
-rw-r--r--users/grfn/xanthous/package.yaml156
-rw-r--r--users/grfn/xanthous/pkg.nix349
-rw-r--r--users/grfn/xanthous/server/.envrc1
-rw-r--r--users/grfn/xanthous/server/.gitignore1
-rw-r--r--users/grfn/xanthous/server/Cargo.lock1841
-rw-r--r--users/grfn/xanthous/server/Cargo.toml29
-rw-r--r--users/grfn/xanthous/server/default.nix21
-rw-r--r--users/grfn/xanthous/server/docker.nix21
-rw-r--r--users/grfn/xanthous/server/module.nix49
-rw-r--r--users/grfn/xanthous/server/shell.nix11
-rw-r--r--users/grfn/xanthous/server/src/main.rs385
-rw-r--r--users/grfn/xanthous/server/src/metrics.rs24
-rw-r--r--users/grfn/xanthous/server/src/pty.rs172
-rw-r--r--users/grfn/xanthous/shell.nix23
-rw-r--r--users/grfn/xanthous/src/Data/Aeson/Generic/DerivingVia.hs168
-rw-r--r--users/grfn/xanthous/src/Xanthous/AI/Gormlak.hs201
-rw-r--r--users/grfn/xanthous/src/Xanthous/App.hs647
-rw-r--r--users/grfn/xanthous/src/Xanthous/App/Autocommands.hs76
-rw-r--r--users/grfn/xanthous/src/Xanthous/App/Common.hs67
-rw-r--r--users/grfn/xanthous/src/Xanthous/App/Prompt.hs228
-rw-r--r--users/grfn/xanthous/src/Xanthous/App/Time.hs42
-rw-r--r--users/grfn/xanthous/src/Xanthous/Command.hs145
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data.hs817
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/App.hs47
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/Entities.hs68
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/EntityChar.hs56
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/EntityMap.hs276
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/EntityMap/Graphics.hs72
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/Levels.hs180
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/Memo.hs98
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/NestedMap.hs227
-rw-r--r--users/grfn/xanthous/src/Xanthous/Data/VectorBag.hs100
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Character.hs241
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Common.hs290
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Creature.hs88
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Creature/Hippocampus.hs71
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Draw/Util.hs31
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Entities.hs63
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Entities.hs-boot14
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Environment.hs160
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Item.hs76
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Marker.hs41
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/RawTypes.hs286
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws.hs49
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/broken-dagger.yaml24
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/gormlak.yaml20
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/husk.yaml26
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/noodles.yaml14
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/ooze.yaml15
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/rock.yaml10
-rw-r--r--users/grfn/xanthous/src/Xanthous/Entities/Raws/stick.yaml22
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game.hs73
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/Arbitrary.hs53
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/Draw.hs224
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/Env.hs37
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/Lenses.hs178
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/Memo.hs52
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/Prompt.hs359
-rw-r--r--users/grfn/xanthous/src/Xanthous/Game/State.hs572
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Level.hs172
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Level/CaveAutomata.hs112
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Level/Dungeon.hs190
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Level/LevelContents.hs182
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Level/Util.hs236
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Level/Village.hs126
-rw-r--r--users/grfn/xanthous/src/Xanthous/Generators/Speech.hs181
-rw-r--r--users/grfn/xanthous/src/Xanthous/Messages.hs114
-rw-r--r--users/grfn/xanthous/src/Xanthous/Messages/Template.hs275
-rw-r--r--users/grfn/xanthous/src/Xanthous/Monad.hs76
-rw-r--r--users/grfn/xanthous/src/Xanthous/Orphans.hs490
-rw-r--r--users/grfn/xanthous/src/Xanthous/Physics.hs71
-rw-r--r--users/grfn/xanthous/src/Xanthous/Prelude.hs48
-rw-r--r--users/grfn/xanthous/src/Xanthous/Random.hs186
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util.hs351
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/Comonad.hs24
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/Graph.hs33
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/Graphics.hs177
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/Inflection.hs14
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/JSON.hs19
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/Optparse.hs21
-rw-r--r--users/grfn/xanthous/src/Xanthous/Util/QuickCheck.hs32
-rw-r--r--users/grfn/xanthous/src/Xanthous/keybindings.yaml22
-rw-r--r--users/grfn/xanthous/src/Xanthous/messages.yaml161
-rw-r--r--users/grfn/xanthous/test/Spec.hs61
-rw-r--r--users/grfn/xanthous/test/Test/Prelude.hs34
-rw-r--r--users/grfn/xanthous/test/Xanthous/CommandSpec.hs40
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/EntitiesSpec.hs28
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/EntityCharSpec.hs18
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/EntityMap/GraphicsSpec.hs57
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/EntityMapSpec.hs69
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/LevelsSpec.hs66
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/MemoSpec.hs19
-rw-r--r--users/grfn/xanthous/test/Xanthous/Data/NestedMapSpec.hs20
-rw-r--r--users/grfn/xanthous/test/Xanthous/DataSpec.hs109
-rw-r--r--users/grfn/xanthous/test/Xanthous/Entities/CharacterSpec.hs24
-rw-r--r--users/grfn/xanthous/test/Xanthous/Entities/CommonSpec.hs65
-rw-r--r--users/grfn/xanthous/test/Xanthous/Entities/RawTypesSpec.hs45
-rw-r--r--users/grfn/xanthous/test/Xanthous/Entities/RawsSpec.hs30
-rw-r--r--users/grfn/xanthous/test/Xanthous/Game/PromptSpec.hs19
-rw-r--r--users/grfn/xanthous/test/Xanthous/Game/StateSpec.hs30
-rw-r--r--users/grfn/xanthous/test/Xanthous/GameSpec.hs55
-rw-r--r--users/grfn/xanthous/test/Xanthous/Generators/Level/UtilSpec.hs127
-rw-r--r--users/grfn/xanthous/test/Xanthous/MessageSpec.hs59
-rw-r--r--users/grfn/xanthous/test/Xanthous/Messages/TemplateSpec.hs80
-rw-r--r--users/grfn/xanthous/test/Xanthous/OrphansSpec.hs72
-rw-r--r--users/grfn/xanthous/test/Xanthous/RandomSpec.hs45
-rw-r--r--users/grfn/xanthous/test/Xanthous/Util/GraphSpec.hs39
-rw-r--r--users/grfn/xanthous/test/Xanthous/Util/GraphicsSpec.hs72
-rw-r--r--users/grfn/xanthous/test/Xanthous/Util/InflectionSpec.hs18
-rw-r--r--users/grfn/xanthous/test/Xanthous/UtilSpec.hs46
-rw-r--r--users/grfn/xanthous/xanthous.cabal529
-rw-r--r--users/isomer/OWNERS3
-rw-r--r--users/isomer/keys.nix7
-rw-r--r--users/lukegb/OWNERS3
-rw-r--r--users/lukegb/hgext/gerrithook.py63
-rw-r--r--users/lukegb/keys.nix10
-rw-r--r--users/qyliss/OWNERS3
-rw-r--r--users/qyliss/keys.nix8
-rw-r--r--users/riking/OWNERS3
-rw-r--r--users/riking/adventofcode-2020/.gitignore2
-rw-r--r--users/riking/adventofcode-2020/day01/Cargo.lock14
-rw-r--r--users/riking/adventofcode-2020/day01/Cargo.toml10
-rw-r--r--users/riking/adventofcode-2020/day01/default.nix10
-rw-r--r--users/riking/adventofcode-2020/day01/src/main.rs85
-rw-r--r--users/riking/dotfiles/.mybashrc53
-rw-r--r--users/riking/dotfiles/fish/conf.d/nix-env.fish141
-rw-r--r--users/riking/dotfiles/fish/config.fish8
-rw-r--r--users/riking/dotfiles/fish/fish_variables32
-rw-r--r--users/riking/dotfiles/fish/functions/ddate.fish3
-rw-r--r--users/riking/dotfiles/fish/functions/gh-clone.fish18
-rw-r--r--users/riking/dotfiles/fish/functions/prodaccess.fish6
-rw-r--r--users/riking/dotfiles/fish/functions/reset-audio.fish4
-rw-r--r--users/riking/dotfiles/fish/functions/tvl-push.fish3
-rw-r--r--users/riking/dotfiles/regolith/Xresources5
-rw-r--r--users/riking/dotfiles/regolith/flags/first-time-setup-r1-4-10
-rw-r--r--users/riking/dotfiles/regolith/flags/show-shortcuts0
-rw-r--r--users/riking/dotfiles/regolith/flags/term-profile0
-rw-r--r--users/riking/dotfiles/regolith/flags/ui-fingerprint1
-rwxr-xr-xusers/riking/dotfiles/regolith/initrc3
-rw-r--r--users/riking/dotfiles/tmux.conf6
-rw-r--r--users/riking/keys.nix20
-rw-r--r--users/sterni/OWNERS3
-rw-r--r--users/sterni/clhs-lookup/README.md13
-rw-r--r--users/sterni/clhs-lookup/clhs-lookup.lisp46
-rw-r--r--users/sterni/clhs-lookup/default.nix39
-rw-r--r--users/sterni/clhs-lookup/packages.lisp10
-rw-r--r--users/sterni/dot-time-man-pages/OWNERS3
-rw-r--r--users/sterni/dot-time-man-pages/default.nix70
-rw-r--r--users/sterni/emacs/default.nix94
-rw-r--r--users/sterni/emacs/init.el355
-rw-r--r--users/sterni/emacs/subscriptions.el95
-rw-r--r--users/sterni/exercises/aoc/.gitignore1
-rw-r--r--users/sterni/exercises/aoc/2021/default.nix10
-rwxr-xr-xusers/sterni/exercises/aoc/2021/solutions.bqn490
-rw-r--r--users/sterni/htmlman/README.md36
-rw-r--r--users/sterni/htmlman/default.nix268
-rw-r--r--users/sterni/htmlman/defaultStyle.nix49
-rw-r--r--users/sterni/keys.nix7
-rw-r--r--users/sterni/mblog/.gitignore5
-rw-r--r--users/sterni/mblog/cli.lisp71
-rw-r--r--users/sterni/mblog/default.nix44
-rw-r--r--users/sterni/mblog/maildir.lisp17
-rw-r--r--users/sterni/mblog/mblog.lisp140
-rw-r--r--users/sterni/mblog/note.lisp118
-rw-r--r--users/sterni/mblog/packages.lisp49
-rw-r--r--users/sterni/mblog/transformer.lisp127
-rw-r--r--users/sterni/nix/char/all-chars.bin2
-rw-r--r--users/sterni/nix/char/default.nix99
-rw-r--r--users/sterni/nix/char/tests/default.nix31
-rw-r--r--users/sterni/nix/flow/default.nix83
-rw-r--r--users/sterni/nix/flow/tests/default.nix39
-rw-r--r--users/sterni/nix/fun/default.nix257
-rw-r--r--users/sterni/nix/fun/tests/default.nix82
-rw-r--r--users/sterni/nix/html/README.md148
-rw-r--r--users/sterni/nix/html/default.nix122
-rw-r--r--users/sterni/nix/html/tests/default.nix93
-rw-r--r--users/sterni/nix/int/default.nix126
-rw-r--r--users/sterni/nix/int/tests/default.nix459
-rw-r--r--users/sterni/nix/string/default.nix122
-rw-r--r--users/sterni/nix/string/tests/default.nix72
-rw-r--r--users/sterni/nix/url/default.nix100
-rw-r--r--users/sterni/nix/url/tests/default.nix58
-rw-r--r--users/sterni/nix/utf8/default.nix325
-rw-r--r--users/sterni/nix/utf8/tests/default.nix148
-rw-r--r--users/sterni/nixpkgs-crate-holes/default.nix348
-rw-r--r--users/tazjin/OWNERS3
-rw-r--r--users/tazjin/aoc2019/default.nix26
-rw-r--r--users/tazjin/aoc2019/solution-day1.el28
-rw-r--r--users/tazjin/aoc2019/solution-day2.el53
-rw-r--r--users/tazjin/aoc2019/solution-day3.el64
-rw-r--r--users/tazjin/aoc2019/solution-day4.el73
-rw-r--r--users/tazjin/aoc2020/default.nix26
-rw-r--r--users/tazjin/aoc2020/solution-day1.el44
-rw-r--r--users/tazjin/aoc2020/solution-day2.el54
-rw-r--r--users/tazjin/aoc2020/solution-day3.el43
-rw-r--r--users/tazjin/aoc2020/solution-day4.el98
-rw-r--r--users/tazjin/aoc2020/solution-day5.el61
-rw-r--r--users/tazjin/aoc2020/solution-day6.el40
-rw-r--r--users/tazjin/aoc2020/solution-day7.el92
-rw-r--r--users/tazjin/aoc2020/solution-day8.el63
-rw-r--r--users/tazjin/avatar.jpegbin0 -> 81953 bytes
-rw-r--r--users/tazjin/blog/.skip-subtree1
-rw-r--r--users/tazjin/blog/default.nix46
-rw-r--r--users/tazjin/blog/posts.nix57
-rw-r--r--users/tazjin/blog/posts/best-tools.md183
-rw-r--r--users/tazjin/blog/posts/emacs-is-underrated.md233
-rw-r--r--users/tazjin/blog/posts/make-object-t-again.md98
-rw-r--r--users/tazjin/blog/posts/nixery-layers.md272
-rw-r--r--users/tazjin/blog/posts/nsa-zettabytes.md93
-rw-r--r--users/tazjin/blog/posts/reversing-watchguard-vpn.md158
-rw-r--r--users/tazjin/blog/posts/sick-in-sweden.md26
-rw-r--r--users/tazjin/blog/posts/the-smu-problem.md151
-rw-r--r--users/tazjin/default.nix30
-rw-r--r--users/tazjin/dns/default.nix13
-rwxr-xr-xusers/tazjin/dns/import12
-rw-r--r--users/tazjin/dns/kontemplate.works.zone15
-rw-r--r--users/tazjin/dns/tazj.in.zone33
-rw-r--r--users/tazjin/docs/install-zfs.md116
-rw-r--r--users/tazjin/dotfiles/config.fish40
-rw-r--r--users/tazjin/dotfiles/default.nix3
-rw-r--r--users/tazjin/dotfiles/dunstrc54
-rw-r--r--users/tazjin/dotfiles/msmtprc15
-rw-r--r--users/tazjin/dotfiles/notmuch-config21
-rw-r--r--users/tazjin/emacs/.gitignore11
-rw-r--r--users/tazjin/emacs/README.md7
-rw-r--r--users/tazjin/emacs/config/bindings.el65
-rw-r--r--users/tazjin/emacs/config/custom.el27
-rw-r--r--users/tazjin/emacs/config/desktop.el372
-rw-r--r--users/tazjin/emacs/config/eshell-setup.el68
-rw-r--r--users/tazjin/emacs/config/functions.el343
-rw-r--r--users/tazjin/emacs/config/init.el308
-rw-r--r--users/tazjin/emacs/config/look-and-feel.el131
-rw-r--r--users/tazjin/emacs/config/mail-setup.el85
-rw-r--r--users/tazjin/emacs/config/modes.el37
-rw-r--r--users/tazjin/emacs/config/settings.el48
-rw-r--r--users/tazjin/emacs/default.nix179
-rw-r--r--users/tazjin/finito/.gitignore3
-rw-r--r--users/tazjin/finito/Cargo.lock773
-rw-r--r--users/tazjin/finito/Cargo.toml6
-rw-r--r--users/tazjin/finito/README.md27
-rw-r--r--users/tazjin/finito/default.nix5
-rw-r--r--users/tazjin/finito/finito-core/Cargo.toml7
-rw-r--r--users/tazjin/finito/finito-core/src/lib.rs248
-rw-r--r--users/tazjin/finito/finito-door/Cargo.toml12
-rw-r--r--users/tazjin/finito/finito-door/src/lib.rs333
-rw-r--r--users/tazjin/finito/finito-postgres/Cargo.toml25
-rw-r--r--users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql4
-rw-r--r--users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql37
-rw-r--r--users/tazjin/finito/finito-postgres/src/error.rs103
-rw-r--r--users/tazjin/finito/finito-postgres/src/lib.rs456
-rw-r--r--users/tazjin/finito/finito-postgres/src/tests.rs54
-rw-r--r--users/tazjin/gruber-darker.qss508
-rw-r--r--users/tazjin/hanebuschtag.txt66
-rw-r--r--users/tazjin/home/shared.nix87
-rw-r--r--users/tazjin/home/tverskoy.nix17
-rw-r--r--users/tazjin/home/zamalek.nix10
-rw-r--r--users/tazjin/homepage/default.nix78
-rw-r--r--users/tazjin/homepage/entries.nix103
-rw-r--r--users/tazjin/homepage/feed.nix43
-rw-r--r--users/tazjin/homepage/footer.html2
-rw-r--r--users/tazjin/homepage/header.html36
-rw-r--r--users/tazjin/homepage/static/favicon.webpbin0 -> 11554 bytes
-rw-r--r--users/tazjin/homepage/static/img/nixery/dominator.webpbin0 -> 12020 bytes
-rw-r--r--users/tazjin/homepage/static/img/nixery/example_extra.webpbin0 -> 10854 bytes
-rw-r--r--users/tazjin/homepage/static/img/nixery/example_plain.webpbin0 -> 9610 bytes
-rw-r--r--users/tazjin/homepage/static/img/nixery/ideal_layout.webpbin0 -> 8334 bytes
-rw-r--r--users/tazjin/homepage/static/img/watchblob_1.webpbin0 -> 32310 bytes
-rw-r--r--users/tazjin/homepage/static/img/watchblob_2.webpbin0 -> 22958 bytes
-rw-r--r--users/tazjin/homepage/static/img/watchblob_3.webpbin0 -> 28614 bytes
-rw-r--r--users/tazjin/homepage/static/img/watchblob_4.webpbin0 -> 52224 bytes
-rw-r--r--users/tazjin/homepage/static/img/watchblob_5.webpbin0 -> 13492 bytes
-rw-r--r--users/tazjin/homepage/static/img/watchblob_6.webpbin0 -> 31048 bytes
-rw-r--r--users/tazjin/keys/default.nix11
-rw-r--r--users/tazjin/nisp/transform.el137
-rw-r--r--users/tazjin/nix.svg50
-rw-r--r--users/tazjin/nixos/.gitignore1
-rw-r--r--users/tazjin/nixos/README.md17
-rw-r--r--users/tazjin/nixos/camden/default.nix346
-rw-r--r--users/tazjin/nixos/default.nix10
-rw-r--r--users/tazjin/nixos/frog/default.nix287
-rw-r--r--users/tazjin/nixos/modules/default.nix2
-rw-r--r--users/tazjin/nixos/modules/desktop.nix53
-rw-r--r--users/tazjin/nixos/modules/fonts.nix24
-rw-r--r--users/tazjin/nixos/modules/hidpi.nix17
-rw-r--r--users/tazjin/nixos/modules/home-config.nix21
-rw-r--r--users/tazjin/nixos/modules/laptop.nix14
-rw-r--r--users/tazjin/nixos/modules/persistence.nix26
-rw-r--r--users/tazjin/nixos/modules/physical.nix90
-rw-r--r--users/tazjin/nixos/modules/tgsa.nix24
-rw-r--r--users/tazjin/nixos/modules/zerotier.nix14
-rw-r--r--users/tazjin/nixos/polyanka/default.nix122
-rw-r--r--users/tazjin/nixos/tverskoy/default.nix163
-rw-r--r--users/tazjin/nixos/zamalek/default.nix82
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/README.md5
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/default.nix52
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/drake-meme.pngbin0 -> 246872 bytes
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/nixos-logo.pngbin0 -> 90542 bytes
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/notes.org89
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/presentation.pdfbin0 -> 527371 bytes
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/presentation.tex251
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/quine-relay.pngbin0 -> 52350 bytes
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/result.pdfpc142
-rw-r--r--users/tazjin/presentations/erlang-2016/.skip-subtree0
-rw-r--r--users/tazjin/presentations/erlang-2016/README.md6
-rw-r--r--users/tazjin/presentations/erlang-2016/presentation.md222
-rw-r--r--users/tazjin/presentations/erlang-2016/presentation.pdfbin0 -> 1777976 bytes
-rw-r--r--users/tazjin/presentations/erlang-2016/src/hello.erl5
-rw-r--r--users/tazjin/presentations/erlang-2016/src/hello1.erl5
-rw-r--r--users/tazjin/presentations/erlang-2016/src/hello2.erl11
-rw-r--r--users/tazjin/presentations/erlang-2016/src/hello_server.erl12
-rw-r--r--users/tazjin/presentations/erlang-2016/src/hello_server2.erl36
-rw-r--r--users/tazjin/presentations/erlang-2016/src/hello_sup.erl24
-rw-r--r--users/tazjin/presentations/servant-2016/Makefile8
-rw-r--r--users/tazjin/presentations/servant-2016/README.md7
-rw-r--r--users/tazjin/presentations/servant-2016/slides.pdfbin0 -> 71174 bytes
-rw-r--r--users/tazjin/presentations/servant-2016/slides.pdfpc75
-rw-r--r--users/tazjin/presentations/servant-2016/slides.tex137
-rw-r--r--users/tazjin/presentations/systemd-2016/.gitignore6
-rw-r--r--users/tazjin/presentations/systemd-2016/.skip-subtree1
-rw-r--r--users/tazjin/presentations/systemd-2016/Makefile11
-rw-r--r--users/tazjin/presentations/systemd-2016/README.md6
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo-error.service7
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo-limits.slice7
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo-notify@.service6
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo-path.path6
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo-stress.service6
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo-timer.timer12
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/demo.service6
-rw-r--r--users/tazjin/presentations/systemd-2016/demo/notes.md27
-rw-r--r--users/tazjin/presentations/systemd-2016/slides.pdfbin0 -> 258221 bytes
-rw-r--r--users/tazjin/presentations/systemd-2016/slides.pdfpc85
-rw-r--r--users/tazjin/presentations/systemd-2016/slides.tex160
-rw-r--r--users/tazjin/presentations/systemd-2016/systemdcomponents.pngbin0 -> 233143 bytes
-rw-r--r--users/tazjin/rlox/.gitignore3
-rw-r--r--users/tazjin/rlox/Cargo.lock6
-rw-r--r--users/tazjin/rlox/Cargo.toml10
-rw-r--r--users/tazjin/rlox/README.md7
-rw-r--r--users/tazjin/rlox/default.nix5
-rw-r--r--users/tazjin/rlox/examples/builtins.lox1
-rw-r--r--users/tazjin/rlox/examples/fib.lox6
-rw-r--r--users/tazjin/rlox/examples/func.lox5
-rw-r--r--users/tazjin/rlox/examples/hello.lox34
-rw-r--r--users/tazjin/rlox/examples/if.lox7
-rw-r--r--users/tazjin/rlox/examples/scope.lox19
-rw-r--r--users/tazjin/rlox/examples/scope2.lox10
-rw-r--r--users/tazjin/rlox/examples/slow.lox9
-rw-r--r--users/tazjin/rlox/examples/var.lox8
-rw-r--r--users/tazjin/rlox/rustfmt.toml1
-rw-r--r--users/tazjin/rlox/src/bytecode/chunk.rs93
-rw-r--r--users/tazjin/rlox/src/bytecode/compiler.rs702
-rw-r--r--users/tazjin/rlox/src/bytecode/errors.rs51
-rw-r--r--users/tazjin/rlox/src/bytecode/interner/mod.rs87
-rw-r--r--users/tazjin/rlox/src/bytecode/interner/tests.rs24
-rw-r--r--users/tazjin/rlox/src/bytecode/mod.rs30
-rw-r--r--users/tazjin/rlox/src/bytecode/opcode.rs56
-rw-r--r--users/tazjin/rlox/src/bytecode/tests.rs152
-rw-r--r--users/tazjin/rlox/src/bytecode/value.rs37
-rw-r--r--users/tazjin/rlox/src/bytecode/vm.rs272
-rw-r--r--users/tazjin/rlox/src/main.rs71
-rw-r--r--users/tazjin/rlox/src/scanner.rs284
-rw-r--r--users/tazjin/rlox/src/treewalk/errors.rs59
-rw-r--r--users/tazjin/rlox/src/treewalk/interpreter.rs498
-rw-r--r--users/tazjin/rlox/src/treewalk/interpreter/builtins.rs25
-rw-r--r--users/tazjin/rlox/src/treewalk/interpreter/tests.rs97
-rw-r--r--users/tazjin/rlox/src/treewalk/mod.rs6
-rw-r--r--users/tazjin/rlox/src/treewalk/parser.rs700
-rw-r--r--users/tazjin/rlox/src/treewalk/resolver.rs199
-rw-r--r--users/tazjin/russian/helpers.el7
-rw-r--r--users/tazjin/russian/roots.el28
-rw-r--r--users/tazjin/russian/russian.el97
-rw-r--r--users/tazjin/russian/words.el723
-rw-r--r--users/tazjin/rustfmt.toml22
-rw-r--r--users/tazjin/tgsa/.gitignore3
-rw-r--r--users/tazjin/tgsa/Cargo.lock1296
-rw-r--r--users/tazjin/tgsa/Cargo.toml12
-rw-r--r--users/tazjin/tgsa/default.nix10
-rw-r--r--users/tazjin/tgsa/src/main.rs343
-rw-r--r--users/tazjin/wallpapers/bio_thehost_1920.webpbin0 -> 553262 bytes
-rw-r--r--users/tazjin/wallpapers/busride2_1920.webpbin0 -> 618772 bytes
-rw-r--r--users/tazjin/wallpapers/by_belltowers_2880.webpbin0 -> 989158 bytes
-rw-r--r--users/tazjin/wallpapers/by_crossing_2560.webpbin0 -> 609420 bytes
-rw-r--r--users/tazjin/wallpapers/by_gathering3_2880.webpbin0 -> 780892 bytes
-rw-r--r--users/tazjin/wallpapers/by_mainservers1_1920.webpbin0 -> 953456 bytes
-rw-r--r--users/tazjin/wallpapers/by_warmachines1_2560.webpbin0 -> 674584 bytes
-rw-r--r--users/tazjin/wallpapers/by_warmachines3_1920.webpbin0 -> 641936 bytes
-rw-r--r--users/tazjin/wallpapers/clever-man_2880.webpbin0 -> 223302 bytes
-rw-r--r--users/tazjin/wallpapers/december1994_1920.webpbin0 -> 234808 bytes
-rw-r--r--users/tazjin/wallpapers/flyby_1920.webpbin0 -> 604920 bytes
-rw-r--r--users/tazjin/wallpapers/gaussfraktarna_1920_badge.webpbin0 -> 110222 bytes
-rw-r--r--users/tazjin/wallpapers/kraftahq_1920.webpbin0 -> 287324 bytes
-rw-r--r--users/tazjin/wallpapers/peripheral2_1920.webpbin0 -> 183288 bytes
-rw-r--r--users/tazjin/wallpapers/ship14_1920.webpbin0 -> 333712 bytes
-rw-r--r--users/tazjin/wallpapers/shipyard_1920.webpbin0 -> 339402 bytes
-rw-r--r--users/tazjin/wallpapers/specky_1920.webpbin0 -> 329806 bytes
-rw-r--r--users/tazjin/wallpapers/summerlove2_1920.webpbin0 -> 580520 bytes
-rw-r--r--users/tazjin/wallpapers/t50_1920_badge.webpbin0 -> 313842 bytes
-rw-r--r--users/tazjin/wallpapers/theflood1_1920.webpbin0 -> 265516 bytes
-rw-r--r--users/tazjin/wallpapers/thelan_1920.webpbin0 -> 399884 bytes
-rw-r--r--users/tazjin/wallpapers/vadrare_1920_badge.webpbin0 -> 154384 bytes
-rw-r--r--users/tvlbot.jpgbin0 -> 16932 bytes
-rw-r--r--users/wpcarro/.envrc3
-rw-r--r--users/wpcarro/.gitignore32
-rw-r--r--users/wpcarro/.gitsecret/keys/pubring.kbxbin0 -> 6799 bytes
-rw-r--r--users/wpcarro/.gitsecret/keys/pubring.kbx~bin0 -> 32 bytes
-rw-r--r--users/wpcarro/.gitsecret/keys/trustdb.gpgbin0 -> 1200 bytes
-rw-r--r--users/wpcarro/.gitsecret/paths/mapping.cfg1
-rw-r--r--users/wpcarro/OWNERS3
-rw-r--r--users/wpcarro/README.md48
-rw-r--r--users/wpcarro/assessments/brilliant/.ghci2
-rw-r--r--users/wpcarro/assessments/brilliant/App.hs41
-rw-r--r--users/wpcarro/assessments/brilliant/Keyboard.hs58
-rw-r--r--users/wpcarro/assessments/brilliant/Main.hs43
-rw-r--r--users/wpcarro/assessments/brilliant/README.md82
-rw-r--r--users/wpcarro/assessments/brilliant/Spec.hs103
-rw-r--r--users/wpcarro/assessments/brilliant/Transforms.hs52
-rw-r--r--users/wpcarro/assessments/brilliant/Utils.hs13
-rw-r--r--users/wpcarro/assessments/brilliant/default.nix16
-rw-r--r--users/wpcarro/assessments/brilliant/shell.nix12
-rw-r--r--users/wpcarro/assessments/dotted-squares/.envrc2
-rw-r--r--users/wpcarro/assessments/dotted-squares/.ghci1
-rw-r--r--users/wpcarro/assessments/dotted-squares/Main.hs218
-rw-r--r--users/wpcarro/assessments/dotted-squares/README.md21
-rw-r--r--users/wpcarro/assessments/dotted-squares/Spec.hs80
-rw-r--r--users/wpcarro/assessments/dotted-squares/colliding-moves.txt7
-rw-r--r--users/wpcarro/assessments/dotted-squares/game.txt7
-rw-r--r--users/wpcarro/assessments/dotted-squares/input-a.txt5
-rw-r--r--users/wpcarro/assessments/dotted-squares/shell.nix8
-rw-r--r--users/wpcarro/assessments/dotted-squares/too-few-moves.txt6
-rw-r--r--users/wpcarro/assessments/dotted-squares/too-many-moves.txt7
-rw-r--r--users/wpcarro/assessments/ramp/solution-emacs-elixir-format.py29
-rw-r--r--users/wpcarro/assessments/ramp/solution.py87
-rw-r--r--users/wpcarro/assessments/semiprimes/.gitignore1
-rw-r--r--users/wpcarro/assessments/semiprimes/README.md44
-rw-r--r--users/wpcarro/assessments/semiprimes/server/.formatter.exs4
-rw-r--r--users/wpcarro/assessments/semiprimes/server/.gitignore24
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/app.ex8
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/cache.ex41
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/extras.ex22
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/math.ex26
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/router.ex86
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/server.ex33
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/sup.ex23
-rw-r--r--users/wpcarro/assessments/semiprimes/server/mix.exs32
-rw-r--r--users/wpcarro/assessments/semiprimes/server/mix.lock14
-rw-r--r--users/wpcarro/assessments/semiprimes/server/test/extras_test.exs18
-rw-r--r--users/wpcarro/assessments/semiprimes/server/test/math_test.exs30
-rw-r--r--users/wpcarro/assessments/semiprimes/server/test/server_test.exs34
-rw-r--r--users/wpcarro/assessments/semiprimes/server/test/test_helper.exs1
-rw-r--r--users/wpcarro/assessments/tt/.gitignore6
-rw-r--r--users/wpcarro/assessments/tt/README.md50
-rw-r--r--users/wpcarro/assessments/tt/client/.gitignore3
-rw-r--r--users/wpcarro/assessments/tt/client/README.md18
-rw-r--r--users/wpcarro/assessments/tt/client/elm.json40
-rw-r--r--users/wpcarro/assessments/tt/client/index.css142
-rw-r--r--users/wpcarro/assessments/tt/client/index.html38
-rw-r--r--users/wpcarro/assessments/tt/client/print.css3
-rw-r--r--users/wpcarro/assessments/tt/client/shell.nix10
-rw-r--r--users/wpcarro/assessments/tt/client/src/Admin.elm189
-rw-r--r--users/wpcarro/assessments/tt/client/src/Common.elm37
-rw-r--r--users/wpcarro/assessments/tt/client/src/Login.elm199
-rw-r--r--users/wpcarro/assessments/tt/client/src/Main.elm62
-rw-r--r--users/wpcarro/assessments/tt/client/src/Manager.elm70
-rw-r--r--users/wpcarro/assessments/tt/client/src/Shared.elm7
-rw-r--r--users/wpcarro/assessments/tt/client/src/State.elm1014
-rw-r--r--users/wpcarro/assessments/tt/client/src/Tailwind.elm29
-rw-r--r--users/wpcarro/assessments/tt/client/src/UI.elm318
-rw-r--r--users/wpcarro/assessments/tt/client/src/User.elm245
-rw-r--r--users/wpcarro/assessments/tt/client/src/Utils.elm109
-rw-r--r--users/wpcarro/assessments/tt/data/accounts.csv2
-rw-r--r--users/wpcarro/assessments/tt/data/trips.csv3
-rw-r--r--users/wpcarro/assessments/tt/populate.sqlite37
-rw-r--r--users/wpcarro/assessments/tt/shell.nix18
-rw-r--r--users/wpcarro/assessments/tt/src/.ghci2
-rw-r--r--users/wpcarro/assessments/tt/src/API.hs75
-rw-r--r--users/wpcarro/assessments/tt/src/Accounts.hs49
-rw-r--r--users/wpcarro/assessments/tt/src/App.hs270
-rw-r--r--users/wpcarro/assessments/tt/src/Auth.hs64
-rw-r--r--users/wpcarro/assessments/tt/src/Email.hs46
-rw-r--r--users/wpcarro/assessments/tt/src/Invitations.hs21
-rw-r--r--users/wpcarro/assessments/tt/src/LoginAttempts.hs30
-rw-r--r--users/wpcarro/assessments/tt/src/Main.hs13
-rw-r--r--users/wpcarro/assessments/tt/src/PendingAccounts.hs32
-rw-r--r--users/wpcarro/assessments/tt/src/Sessions.hs74
-rw-r--r--users/wpcarro/assessments/tt/src/Trips.hs42
-rw-r--r--users/wpcarro/assessments/tt/src/Types.hs544
-rw-r--r--users/wpcarro/assessments/tt/src/Utils.hs9
-rw-r--r--users/wpcarro/assessments/tt/src/init.sql67
-rwxr-xr-xusers/wpcarro/assessments/tt/tests/create-accounts.sh21
-rw-r--r--users/wpcarro/assessments/tt/todo.org18
-rwxr-xr-xusers/wpcarro/bin/__dispatch.sh33
l---------users/wpcarro/bin/deploy-diogenes1
l---------users/wpcarro/bin/export-gpg1
l---------users/wpcarro/bin/import-gpg1
l---------users/wpcarro/bin/rebuild-diogenes1
-rw-r--r--users/wpcarro/boilerplate/README.md21
-rw-r--r--users/wpcarro/boilerplate/clojure/.envrc2
-rw-r--r--users/wpcarro/boilerplate/clojure/.gitignore4
-rw-r--r--users/wpcarro/boilerplate/clojure/README.md33
-rw-r--r--users/wpcarro/boilerplate/clojure/project.clj2
-rw-r--r--users/wpcarro/boilerplate/clojure/shell.nix7
-rw-r--r--users/wpcarro/boilerplate/clojure/src/main.clj8
-rw-r--r--users/wpcarro/boilerplate/elm/.envrc2
-rw-r--r--users/wpcarro/boilerplate/elm/.gitignore3
-rw-r--r--users/wpcarro/boilerplate/elm/README.md18
-rw-r--r--users/wpcarro/boilerplate/elm/elm.json30
-rw-r--r--users/wpcarro/boilerplate/elm/index.css3
-rw-r--r--users/wpcarro/boilerplate/elm/index.html15
-rw-r--r--users/wpcarro/boilerplate/elm/shell.nix9
-rw-r--r--users/wpcarro/boilerplate/elm/src/Landing.elm13
-rw-r--r--users/wpcarro/boilerplate/elm/src/Login.elm13
-rw-r--r--users/wpcarro/boilerplate/elm/src/Main.elm31
-rw-r--r--users/wpcarro/boilerplate/elm/src/State.elm43
-rw-r--r--users/wpcarro/boilerplate/typescript/.envrc2
-rw-r--r--users/wpcarro/boilerplate/typescript/.gitignore3
-rw-r--r--users/wpcarro/boilerplate/typescript/README.md26
-rw-r--r--users/wpcarro/boilerplate/typescript/default.nix23
-rw-r--r--users/wpcarro/boilerplate/typescript/package.json27
-rw-r--r--users/wpcarro/boilerplate/typescript/postcss.config.js7
-rw-r--r--users/wpcarro/boilerplate/typescript/shell.nix8
-rw-r--r--users/wpcarro/boilerplate/typescript/src/App.tsx52
-rw-r--r--users/wpcarro/boilerplate/typescript/src/index.css3
-rw-r--r--users/wpcarro/boilerplate/typescript/src/index.html11
-rw-r--r--users/wpcarro/boilerplate/typescript/src/index.tsx12
-rw-r--r--users/wpcarro/boilerplate/typescript/src/store.ts26
-rw-r--r--users/wpcarro/boilerplate/typescript/tailwind.config.js7
-rw-r--r--users/wpcarro/boilerplate/typescript/tsconfig.json25
-rw-r--r--users/wpcarro/boilerplate/typescript/yarn.lock5670
-rw-r--r--users/wpcarro/buildHaskell/default.nix35
-rw-r--r--users/wpcarro/ci/pipelines/post-receive.nix14
-rw-r--r--users/wpcarro/ci/secret-patterns.txt9
-rw-r--r--users/wpcarro/common.nix71
-rw-r--r--users/wpcarro/configs/.config/nixpkgs/config.nix3
-rw-r--r--users/wpcarro/configs/.config/nvim/init.vim668
-rw-r--r--users/wpcarro/configs/.config/nvim/templates/boilerplate.c6
-rw-r--r--users/wpcarro/configs/.config/nvim/templates/boilerplate.rs5
-rw-r--r--users/wpcarro/configs/.config/systemd/user/clipmenud.service18
l---------users/wpcarro/configs/.config/systemd/user/default.target.wants/clipmenud.service1
-rw-r--r--users/wpcarro/configs/.config/systemd/user/lieer-google.service7
-rw-r--r--users/wpcarro/configs/.config/systemd/user/lieer-google.timer9
l---------users/wpcarro/configs/.config/systemd/user/timers.target.wants/lieer-google.timer1
-rw-r--r--users/wpcarro/configs/.gitconfig3
-rw-r--r--users/wpcarro/configs/.gnupg/crls.d/DIR.txt1
-rwxr-xr-xusers/wpcarro/configs/.gnupg/export.sh29
-rw-r--r--users/wpcarro/configs/.gnupg/exported/ownertrust.txt3
-rw-r--r--users/wpcarro/configs/.gnupg/exported/public.asc225
-rwxr-xr-xusers/wpcarro/configs/.gnupg/import.sh28
-rw-r--r--users/wpcarro/configs/.gnupg/pubring.kbxbin0 -> 11397 bytes
-rw-r--r--users/wpcarro/configs/.gnupg/trustdb.gpgbin0 -> 1280 bytes
-rw-r--r--users/wpcarro/configs/.sqliterc2
-rw-r--r--users/wpcarro/configs/.xsecurelockrc5
-rw-r--r--users/wpcarro/configs/default.nix73
-rwxr-xr-xusers/wpcarro/configs/install5
-rwxr-xr-xusers/wpcarro/configs/uninstall5
-rw-r--r--users/wpcarro/dotfiles/config.fish34
-rw-r--r--users/wpcarro/dotfiles/default.nix5
-rw-r--r--users/wpcarro/dotfiles/dunstrc53
-rw-r--r--users/wpcarro/dotfiles/prompt.fish87
-rw-r--r--users/wpcarro/emacs/.emacs.d/init.el16
-rw-r--r--users/wpcarro/emacs/.emacs.d/opam-user-setup.el145
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/c-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdio5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdlib5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/c-mode/struct7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/elisp-module-docs11
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/function8
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/generic-header7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/library-header7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/provide-footer6
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/derive-safe-copy5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/import-qualified5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/instance-defn6
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/language-extension5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/separator5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/undefined5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/html-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/html-mode/index-boilerplate18
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/java-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/java-mode/public-static-void-main7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/defpackage9
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/function7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/typed-function8
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/nix-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/nix-mode/shell-nix12
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/org-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/org-mode/code-snippet7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/org-mode/href5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/dunder-main6
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/function6
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/header7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/init6
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/shebang6
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/python-mode/utf-85
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/racket-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/racket-mode/function5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda-symbol5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/reason-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/reason-mode/function7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/reason-mode/switch7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/action-extractor5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/console-log5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-defn5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-function7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/destructure-const5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow-function7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-destructured5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-react5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-type5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-x-from-y5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-y5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-describe-test10
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-test7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/react-class-component11
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/redux-action5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/typed-redux-action5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rust-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rust-mode/for-loop7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/rust-mode/match7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/sh-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/sh-mode/function7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/text-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/text-mode/check-mark5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/text-mode/x-mark5
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/web-mode/.yas-parents1
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/web-mode/header7
-rw-r--r--users/wpcarro/emacs/.emacs.d/snippets/web-mode/index-boilerplate18
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/>.el28
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/al.el254
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/bag.el70
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/bookmark.el50
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/buffer.el173
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/bytes.el112
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/cache.el88
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/clipboard.el40
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/colorscheme.el85
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/constants.el26
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/cycle.el224
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/device.el62
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/display.el103
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/dotted.el57
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/email.el76
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/fonts.el167
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/fs.el69
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/functions.el46
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/graph.el94
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/irc.el170
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/ivy-helpers.el67
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/kbd.el85
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/keybindings.el437
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/keyboard.el139
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/laptop-battery.el63
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/list.el221
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/macros.el63
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/math.el62
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/maybe.el78
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/modeline.el68
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/number.el142
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/prelude.el144
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/pulse-audio.el69
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/random.el80
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/region.el23
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/scope.el106
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/screen-brightness.el57
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/scrot.el54
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/sequence.el108
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/series.el92
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/set.el174
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/ssh.el67
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/stack.el101
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/string.el110
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/struct.el85
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/symbol.el48
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/timestring.el77
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/tree.el199
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/tuple.el93
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/vector.el84
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/vterm-mgt.el142
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/window-manager.el228
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/window.el40
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-clojure.el71
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-company.el41
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-dired.el52
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-elixir.el27
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-flycheck.el17
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-golang.el42
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-haskell.el53
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-javascript.el98
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-language-support.el36
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-lisp.el123
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-misc.el340
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-nix.el37
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-org.el39
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-package.el32
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-prolog.el19
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-python.el24
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-rust.el47
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-shell.el31
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-ui.el182
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/zle.el91
-rw-r--r--users/wpcarro/emacs/README.md15
-rw-r--r--users/wpcarro/emacs/ci.el44
-rw-r--r--users/wpcarro/emacs/default.nix179
-rw-r--r--users/wpcarro/emacs/elisp-conventions.md20
-rw-r--r--users/wpcarro/emacs/keybindings.md47
-rw-r--r--users/wpcarro/emacs/snippets.md22
-rw-r--r--users/wpcarro/emacs/workspace.josh0
-rw-r--r--users/wpcarro/go/.envrc2
-rw-r--r--users/wpcarro/go/actors.go45
-rw-r--r--users/wpcarro/go/atomic-counters.go26
-rw-r--r--users/wpcarro/go/channels.go81
-rw-r--r--users/wpcarro/go/mutex.go53
-rw-r--r--users/wpcarro/go/shell.nix9
-rw-r--r--users/wpcarro/go/waitgroups.go24
-rw-r--r--users/wpcarro/gopkgs/kv/default.nix8
-rw-r--r--users/wpcarro/gopkgs/kv/kv.go39
-rw-r--r--users/wpcarro/gopkgs/utils/default.nix8
-rw-r--r--users/wpcarro/gopkgs/utils/utils.go131
-rw-r--r--users/wpcarro/haskell-file/.envrc2
-rw-r--r--users/wpcarro/haskell-file/README.md7
-rw-r--r--users/wpcarro/haskell-file/f-todo.org67
-rw-r--r--users/wpcarro/haskell-file/f.hs64
-rw-r--r--users/wpcarro/haskell-file/shell.nix5
-rw-r--r--users/wpcarro/haskell-file/tests.hs39
-rw-r--r--users/wpcarro/keys.nix18
-rw-r--r--users/wpcarro/lisp/README.md16
-rw-r--r--users/wpcarro/lisp/prelude.lisp14
-rw-r--r--users/wpcarro/lisp/prelude.nix8
-rw-r--r--users/wpcarro/nixos/ava/ava.el57
-rw-r--r--users/wpcarro/nixos/ava/default.nix136
-rw-r--r--users/wpcarro/nixos/ava/hardware.nix31
-rw-r--r--users/wpcarro/nixos/default.nix59
-rw-r--r--users/wpcarro/nixos/diogenes/README.md13
-rw-r--r--users/wpcarro/nixos/diogenes/default.nix160
-rw-r--r--users/wpcarro/nixos/iso.nix17
-rw-r--r--users/wpcarro/nixos/marcus/default.nix172
-rw-r--r--users/wpcarro/nixos/marcus/hardware.nix29
-rw-r--r--users/wpcarro/nixos/marcus/marcus.el37
-rw-r--r--users/wpcarro/nixos/tarasco/default.nix158
-rw-r--r--users/wpcarro/nixos/tarasco/hardware.nix24
-rw-r--r--users/wpcarro/nixos/tarasco/tarasco.el57
-rw-r--r--users/wpcarro/playbooks/README.md3
-rw-r--r--users/wpcarro/playbooks/first-of-the-month.org12
-rw-r--r--users/wpcarro/playbooks/habits.org49
-rw-r--r--users/wpcarro/playbooks/hip_opening_challenge/poses.pdfbin0 -> 2853812 bytes
-rw-r--r--users/wpcarro/playbooks/hip_opening_challenge/progress.org65
-rw-r--r--users/wpcarro/playbooks/nix_gcr/README.md62
-rw-r--r--users/wpcarro/playbooks/nix_gcr/cloud_run.nix14
-rw-r--r--users/wpcarro/playbooks/nix_gcr/config.lisp21
-rw-r--r--users/wpcarro/playbooks/shell.md12
-rw-r--r--users/wpcarro/playbooks/sqlite3.md115
-rw-r--r--users/wpcarro/scratch/README.md6
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/README.md4
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_1.py119
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_2.py32
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_3.py137
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_4.py35
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_5.py170
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_6.py155
-rw-r--r--users/wpcarro/scratch/advent-of-code-2019/day_7.py49
-rw-r--r--users/wpcarro/scratch/blockchain/default.nix14
-rw-r--r--users/wpcarro/scratch/blockchain/main.py263
-rw-r--r--users/wpcarro/scratch/blockchain/setup.py10
-rw-r--r--users/wpcarro/scratch/crack_the_coding_interview/11_1.py40
-rw-r--r--users/wpcarro/scratch/crack_the_coding_interview/to_tree.hs11
-rw-r--r--users/wpcarro/scratch/cryptopals/.gitignore1
-rw-r--r--users/wpcarro/scratch/cryptopals/README.md3
-rw-r--r--users/wpcarro/scratch/cryptopals/set1/4.txt327
-rw-r--r--users/wpcarro/scratch/cryptopals/set1/c1.py19
-rw-r--r--users/wpcarro/scratch/cryptopals/set1/c2.py20
-rw-r--r--users/wpcarro/scratch/cryptopals/set1/c3.py50
-rw-r--r--users/wpcarro/scratch/cryptopals/set1/c4.py23
-rw-r--r--users/wpcarro/scratch/cryptopals/set1/c5.py16
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/array-traversals.py87
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/balanced-binary-tree.py145
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/bit-manipulation.py32
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/bracket-validator.py63
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/bst-checker.py121
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/cafe-order-checker.py91
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/cake-thief.py71
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/coins.py57
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/conways-game-of-life.py78
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/delete-node.py60
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/dft.py65
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/dijkstra-shortest-path.py48
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space-beast.py56
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space.py61
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/find-rotation-point.py59
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/find-unique-int-among-duplicates.py45
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/fixtures.py110
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/graph-coloring.py180
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/graph-to-graphviz.py39
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/highest-product-of-3.py89
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/inflight-entertainment.py35
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/knapsack-0-1.py38
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/kth-to-last.py82
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/largest-stack.py107
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/linked-list-cycles.py88
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/memo.py60
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/merge-sort.py28
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/merging-ranges.py94
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.gv11
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.py97
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/norman.py78
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/nth-fibonacci.py59
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/optimal-stopping.py49
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/perm-tree.py83
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/permutation-palindrome.py49
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/permutations.py55
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/plot.py9
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/product-of-other-numbers.py68
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/queue-two-stacks.py66
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/rectangular-love.py246
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/recursive-string-permutations.py37
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/reverse-linked-list.py79
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/reverse-words.py181
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/second-largest-item-bst.py179
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/shortest-path-inject-vertices.py94
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/shuffle.py34
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/string-reverse.py22
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/temperature-tracker.py84
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/test.txt1
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/top-scores.py25
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/topo-sort.py31
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/trickling-water.py38
-rw-r--r--users/wpcarro/scratch/data_structures_and_algorithms/which-appears-twice.py33
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/balanced-binary-tree.py123
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/dijkstra.py26
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/efficiency.org6
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/find-rotation-point.py55
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/inflight-entertainment.py51
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/kth-to-last.py64
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/merging-ranges.py59
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/recursive-string-permutations.py56
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/reverse-linked-list.py74
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/stock-price.py51
-rw-r--r--users/wpcarro/scratch/deepmind/part_one/which-appears-twice.py29
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/.envrc2
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/balanced-binary-tree.py126
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/bst-checker.py110
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/cafe-order-checker.py64
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/coin.ts102
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/delete-node.py57
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space-beast-mode.py114
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space.ts70
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/find-rotation-point.ts68
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/graph-coloring.ts232
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/highest-product-of-3.py81
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/inflight-entertainment.ts85
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/merge-sorted-arrays.ts63
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/merging-ranges.py115
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/mesh-message.py183
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/misc/matrix-traversals.py104
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/nth-fibonacci.py72
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/package-lock.json79
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/package.json16
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/permutation-palindrome.py37
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/product-of-other-numbers.py68
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/recursive-string-permutations.ts85
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/reverse-string-in-place.ts13
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/reverse-words.py74
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/second-largest-item-in-bst.ts219
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/shell.nix10
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/shuffle.py20
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/stock-price.py54
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/todo.org77
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/top-scores.py47
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/top-scores.ts57
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/tsconfig.json7
-rw-r--r--users/wpcarro/scratch/deepmind/part_two/word-cloud.py79
-rw-r--r--users/wpcarro/scratch/facebook/anglocize-int.py71
-rw-r--r--users/wpcarro/scratch/facebook/balanced-binary-tree.py70
-rw-r--r--users/wpcarro/scratch/facebook/breakfast-generator.py112
-rw-r--r--users/wpcarro/scratch/facebook/bst-checker.py49
-rw-r--r--users/wpcarro/scratch/facebook/cafe-order-checker.py19
-rw-r--r--users/wpcarro/scratch/facebook/cake_thief.py61
-rw-r--r--users/wpcarro/scratch/facebook/camping-knapsack.py46
-rw-r--r--users/wpcarro/scratch/facebook/coin.py50
-rw-r--r--users/wpcarro/scratch/facebook/count-islands.py53
-rw-r--r--users/wpcarro/scratch/facebook/delete-node.py19
-rw-r--r--users/wpcarro/scratch/facebook/dijkstras.py38
-rw-r--r--users/wpcarro/scratch/facebook/edit-distance.py47
-rw-r--r--users/wpcarro/scratch/facebook/evaluator.hs39
-rw-r--r--users/wpcarro/scratch/facebook/evaluator.py234
-rw-r--r--users/wpcarro/scratch/facebook/find-duplicate-beast-mode.py57
-rw-r--r--users/wpcarro/scratch/facebook/find-duplicate-optimize-for-space.py22
-rw-r--r--users/wpcarro/scratch/facebook/find-rotation-point.py47
-rw-r--r--users/wpcarro/scratch/facebook/find-unique-int-among-duplicates.py17
-rw-r--r--users/wpcarro/scratch/facebook/graph-coloring.py60
-rw-r--r--users/wpcarro/scratch/facebook/hard/binary-adder.py22
-rw-r--r--users/wpcarro/scratch/facebook/hard/fisher-yates.py7
-rw-r--r--users/wpcarro/scratch/facebook/hard/random-choice.py50
-rw-r--r--users/wpcarro/scratch/facebook/hard/suffix-tree.py93
-rw-r--r--users/wpcarro/scratch/facebook/heap.py30
-rw-r--r--users/wpcarro/scratch/facebook/highest-product-of-3.py20
-rw-r--r--users/wpcarro/scratch/facebook/infix-to-postfix.py51
-rw-r--r--users/wpcarro/scratch/facebook/inflight-entertainment.py29
-rw-r--r--users/wpcarro/scratch/facebook/intersecting-linked-lists.py34
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/bst-checker.py14
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/cafe-order-checker.py34
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/linked-list-cycles.py70
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/merge-sorted-arrays.py30
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/nth-fibonacci.py6
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/permutation-palindrome.py8
-rw-r--r--users/wpcarro/scratch/facebook/interview-cake/queue-two-stacks.py17
-rw-r--r--users/wpcarro/scratch/facebook/knapsack-faq.py42
-rw-r--r--users/wpcarro/scratch/facebook/kth-to-last-node-in-singly-linked-list.py26
-rw-r--r--users/wpcarro/scratch/facebook/language.py70
-rw-r--r--users/wpcarro/scratch/facebook/language2.py50
-rw-r--r--users/wpcarro/scratch/facebook/largest-contiguous-sum.py15
-rw-r--r--users/wpcarro/scratch/facebook/largest-stack.py49
-rw-r--r--users/wpcarro/scratch/facebook/leetcode.org163
-rw-r--r--users/wpcarro/scratch/facebook/linked-list-cycles.py26
-rw-r--r--users/wpcarro/scratch/facebook/linked_list.py22
-rw-r--r--users/wpcarro/scratch/facebook/london-knapsack.py42
-rw-r--r--users/wpcarro/scratch/facebook/longest-common-substring.py20
-rw-r--r--users/wpcarro/scratch/facebook/merge-sorted-arrays.py44
-rw-r--r--users/wpcarro/scratch/facebook/merging-ranges.py23
-rw-r--r--users/wpcarro/scratch/facebook/mesh-message.py40
-rw-r--r--users/wpcarro/scratch/facebook/moderate/decompress-xml.py98
-rw-r--r--users/wpcarro/scratch/facebook/moderate/find-pairs-for-sum.py19
-rw-r--r--users/wpcarro/scratch/facebook/moderate/parser.py37
-rw-r--r--users/wpcarro/scratch/facebook/moderate/rand7.py25
-rw-r--r--users/wpcarro/scratch/facebook/moderate/tic-tac-toe-checker.py99
-rw-r--r--users/wpcarro/scratch/facebook/moderate/unsorted-substring.py67
-rw-r--r--users/wpcarro/scratch/facebook/move-zeroes-to-end.py62
-rw-r--r--users/wpcarro/scratch/facebook/mst.py71
-rw-r--r--users/wpcarro/scratch/facebook/n-queens.py46
-rw-r--r--users/wpcarro/scratch/facebook/nearby-words.py33
-rw-r--r--users/wpcarro/scratch/facebook/node.py38
-rw-r--r--users/wpcarro/scratch/facebook/nth-fibonacci.py13
-rw-r--r--users/wpcarro/scratch/facebook/onsite.txt22
-rw-r--r--users/wpcarro/scratch/facebook/parsing/json.py121
-rw-r--r--users/wpcarro/scratch/facebook/parsing/parser.py28
-rw-r--r--users/wpcarro/scratch/facebook/parsing/regex.py184
-rw-r--r--users/wpcarro/scratch/facebook/permutation-palindrome.py17
-rw-r--r--users/wpcarro/scratch/facebook/polynomial-rolling-hash.py72
-rw-r--r--users/wpcarro/scratch/facebook/product-of-all-other-numbers.py33
-rw-r--r--users/wpcarro/scratch/facebook/queue-two-stacks.py20
-rw-r--r--users/wpcarro/scratch/facebook/rabin-karp.py27
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/magic-index.py33
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/making-change.py56
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/paint-fill.py36
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/parenthesize-bools.py114
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/permutations.py13
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/robot-grid-traversal.py28
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/staircase.py1
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/subsets.py41
-rw-r--r--users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/valid-parens.py50
-rw-r--r--users/wpcarro/scratch/facebook/recursive-string-permutations.py19
-rw-r--r--users/wpcarro/scratch/facebook/reverse-linked-list.py25
-rw-r--r--users/wpcarro/scratch/facebook/reverse-string-in-place.py14
-rw-r--r--users/wpcarro/scratch/facebook/reverse-words.py8
-rw-r--r--users/wpcarro/scratch/facebook/scratch.py94
-rw-r--r--users/wpcarro/scratch/facebook/second-largest-item-in-bst.py22
-rw-r--r--users/wpcarro/scratch/facebook/shuffle.py17
-rw-r--r--users/wpcarro/scratch/facebook/stack.py25
-rw-r--r--users/wpcarro/scratch/facebook/stacking-boxes.py50
-rw-r--r--users/wpcarro/scratch/facebook/stock-price.py16
-rw-r--r--users/wpcarro/scratch/facebook/todo.org60
-rw-r--r--users/wpcarro/scratch/facebook/top-scores.py20
-rw-r--r--users/wpcarro/scratch/facebook/topo-sort.py61
-rw-r--r--users/wpcarro/scratch/facebook/traversals.py100
-rw-r--r--users/wpcarro/scratch/facebook/utils.py19
-rw-r--r--users/wpcarro/scratch/facebook/word-cloud.py32
-rw-r--r--users/wpcarro/scratch/groceries/.envrc2
-rw-r--r--users/wpcarro/scratch/groceries/export.hs22
-rw-r--r--users/wpcarro/scratch/groceries/list.org112
-rw-r--r--users/wpcarro/scratch/groceries/shell.nix5
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/.envrc2
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/.ghci1
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/applicative.hs213
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/basic-libraries.hs60
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/composing-types.hs75
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/foldable.hs107
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/io.hs35
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/monad-transformers.hs183
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/monad.hs178
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/non-strictness.hs6
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/reader.hs149
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/shell.nix8
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/state.hs93
-rw-r--r--users/wpcarro/scratch/haskell-programming-from-first-principles/traversable.hs131
-rw-r--r--users/wpcarro/scratch/picoctf/.skip-subtree0
-rw-r--r--users/wpcarro/scratch/picoctf/README.md3
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_144.py11
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_156.py13
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_166/ende.py60
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_166/flag.txt.en1
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_166/pw.txt1
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_166/shell.nix8
-rw-r--r--users/wpcarro/scratch/picoctf/challenge_170/README.md11
-rw-r--r--users/wpcarro/scratch/simple-select/README.md71
-rw-r--r--users/wpcarro/scratch/simple-select/main.py262
-rw-r--r--users/wpcarro/scratch/simple-select/parser.py31
-rw-r--r--users/wpcarro/scratch/simple-select/scanner.py27
-rw-r--r--users/wpcarro/secrets.json.secretbin0 -> 1142 bytes
-rw-r--r--users/wpcarro/terraform/.gitignore4
-rw-r--r--users/wpcarro/terraform/default.nix192
-rw-r--r--users/wpcarro/todo-lists/cta-curriculum.csv108
-rw-r--r--users/wpcarro/todo-lists/imdb/db.sqlite3bin0 -> 24576 bytes
-rw-r--r--users/wpcarro/todo-lists/imdb/imdb-top-250.org256
-rw-r--r--users/wpcarro/todo-lists/imdb/scratch.sql65
-rw-r--r--users/wpcarro/todo-lists/paul-graham-essays.org190
-rw-r--r--users/wpcarro/todo-lists/travel-hitlist.md83
-rw-r--r--users/wpcarro/tools/monzo_ynab/.envrc9
-rw-r--r--users/wpcarro/tools/monzo_ynab/.gitignore3
-rw-r--r--users/wpcarro/tools/monzo_ynab/.skip-subtree2
-rw-r--r--users/wpcarro/tools/monzo_ynab/README.md41
-rw-r--r--users/wpcarro/tools/monzo_ynab/auth.go101
-rw-r--r--users/wpcarro/tools/monzo_ynab/job.nix15
-rw-r--r--users/wpcarro/tools/monzo_ynab/main.go44
-rw-r--r--users/wpcarro/tools/monzo_ynab/monzo/client.go52
-rw-r--r--users/wpcarro/tools/monzo_ynab/monzo/serde.go82
-rw-r--r--users/wpcarro/tools/monzo_ynab/requests.txt80
-rw-r--r--users/wpcarro/tools/monzo_ynab/shell.nix9
-rw-r--r--users/wpcarro/tools/monzo_ynab/tokens.go283
-rw-r--r--users/wpcarro/tools/monzo_ynab/tokens.nix26
-rw-r--r--users/wpcarro/tools/monzo_ynab/ynab/client.go24
-rw-r--r--users/wpcarro/tools/monzo_ynab/ynab/serde.go52
-rw-r--r--users/wpcarro/tools/rfcToKindle/LICENSE202
-rw-r--r--users/wpcarro/tools/rfcToKindle/README.md30
-rw-r--r--users/wpcarro/tools/rfcToKindle/default.nix11
-rw-r--r--users/wpcarro/tools/rfcToKindle/main.go89
-rw-r--r--users/wpcarro/tools/run/.envrc2
-rw-r--r--users/wpcarro/tools/run/README.md30
-rw-r--r--users/wpcarro/tools/run/default.nix11
-rw-r--r--users/wpcarro/tools/run/main.go49
-rw-r--r--users/wpcarro/tools/run/shell.nix9
-rw-r--r--users/wpcarro/tools/simple_vim/config.vim101
-rw-r--r--users/wpcarro/tools/simple_vim/default.nix5
-rw-r--r--users/wpcarro/tools/symlinkManager/README.md12
-rw-r--r--users/wpcarro/tools/symlinkManager/default.nix14
-rw-r--r--users/wpcarro/tools/symlinkManager/main.go61
-rw-r--r--users/wpcarro/tools/url-blocker/.envrc2
-rw-r--r--users/wpcarro/tools/url-blocker/Main.hs205
-rw-r--r--users/wpcarro/tools/url-blocker/README.md47
-rw-r--r--users/wpcarro/tools/url-blocker/default.nix34
-rw-r--r--users/wpcarro/tools/url-blocker/rules.json28
-rw-r--r--users/wpcarro/tools/url-blocker/shell.nix10
-rw-r--r--users/wpcarro/utils/README.md8
-rw-r--r--users/wpcarro/utils/builder.nix12
-rw-r--r--users/wpcarro/utils/default.nix15
-rw-r--r--users/wpcarro/utils/fs.nix42
-rw-r--r--users/wpcarro/website/README.md3
-rw-r--r--users/wpcarro/website/blog/.skip-subtree1
-rw-r--r--users/wpcarro/website/blog/default.nix46
-rw-r--r--users/wpcarro/website/blog/fragments/.skip-subtree0
-rw-r--r--users/wpcarro/website/blog/fragments/post.html8
-rw-r--r--users/wpcarro/website/blog/fragments/posts.html10
-rw-r--r--users/wpcarro/website/blog/posts.nix32
-rw-r--r--users/wpcarro/website/blog/posts/auto-reboot-nixos.md40
-rw-r--r--users/wpcarro/website/blog/posts/cell-phone-experiment.md274
-rw-r--r--users/wpcarro/website/blog/posts/quassel-google-vm.md34
-rw-r--r--users/wpcarro/website/blog/posts/send-mail-as-2fa.md43
-rw-r--r--users/wpcarro/website/default.nix43
-rw-r--r--users/wpcarro/website/fragments/.skip-subtree0
-rw-r--r--users/wpcarro/website/fragments/homepage.html20
-rw-r--r--users/wpcarro/website/fragments/template.html101
-rw-r--r--users/wpcarro/website/habit-screens/.envrc2
-rw-r--r--users/wpcarro/website/habit-screens/.gitignore2
-rw-r--r--users/wpcarro/website/habit-screens/README.md31
-rw-r--r--users/wpcarro/website/habit-screens/default.nix63
-rw-r--r--users/wpcarro/website/habit-screens/design.md43
-rw-r--r--users/wpcarro/website/habit-screens/elm-srcs.nix77
-rw-r--r--users/wpcarro/website/habit-screens/elm.json32
-rw-r--r--users/wpcarro/website/habit-screens/index.css3
-rw-r--r--users/wpcarro/website/habit-screens/index.html21
-rw-r--r--users/wpcarro/website/habit-screens/output.css103571
-rw-r--r--users/wpcarro/website/habit-screens/registry.datbin0 -> 103324 bytes
-rw-r--r--users/wpcarro/website/habit-screens/shell.nix9
-rw-r--r--users/wpcarro/website/habit-screens/src/Habits.elm463
-rw-r--r--users/wpcarro/website/habit-screens/src/Main.elm29
-rw-r--r--users/wpcarro/website/habit-screens/src/State.elm195
-rw-r--r--users/wpcarro/website/habit-screens/src/UI.elm9
-rw-r--r--users/wpcarro/website/habit-screens/src/Utils.elm37
-rw-r--r--users/wpcarro/website/sandbox/contentful/.envrc5
-rw-r--r--users/wpcarro/website/sandbox/contentful/.gitignore2
-rw-r--r--users/wpcarro/website/sandbox/contentful/README.md18
-rw-r--r--users/wpcarro/website/sandbox/contentful/default.nix24
-rw-r--r--users/wpcarro/website/sandbox/contentful/package.json26
-rw-r--r--users/wpcarro/website/sandbox/contentful/postcss.config.js5
-rw-r--r--users/wpcarro/website/sandbox/contentful/shell.nix8
-rw-r--r--users/wpcarro/website/sandbox/contentful/src/App.tsx49
-rw-r--r--users/wpcarro/website/sandbox/contentful/src/contentful.ts27
-rw-r--r--users/wpcarro/website/sandbox/contentful/src/index.css3
-rw-r--r--users/wpcarro/website/sandbox/contentful/src/index.html11
-rw-r--r--users/wpcarro/website/sandbox/contentful/src/index.tsx12
-rw-r--r--users/wpcarro/website/sandbox/contentful/src/store.ts36
-rw-r--r--users/wpcarro/website/sandbox/contentful/tailwind.config.js7
-rw-r--r--users/wpcarro/website/sandbox/contentful/tsconfig.json19
-rw-r--r--users/wpcarro/website/sandbox/contentful/yarn.lock5717
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/default.nix.ignore16
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/index.html99
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/package.json16
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/shell.nix8
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/styles.css28
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/tailwind.config.js7
-rw-r--r--users/wpcarro/website/sandbox/covid-uk/yarn.lock542
-rw-r--r--users/wpcarro/website/sandbox/default.nix.ignore13
-rw-r--r--users/wpcarro/website/sandbox/github-issues-service/README.md28
-rw-r--r--users/wpcarro/website/sandbox/index.html15
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/.envrc2
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/.gitignore2
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/README.md57
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/default.nix63
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/elm-srcs.nix67
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/elm.json30
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/ideas.org3
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/index.css3
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/index.html15
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/output.css103571
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/registry.datbin0 -> 93710 bytes
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/shell.nix9
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/FlashCard.elm42
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Icon.elm44
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Main.elm44
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Misc.elm59
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Overview.elm122
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Piano.elm194
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Practice.elm61
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Preferences.elm148
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Responsive.elm19
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/State.elm179
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Tailwind.elm29
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Tempo.elm33
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/Theory.elm1100
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/UI.elm159
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/.envrc7
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/.ghci7
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/API.hs16
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/App.hs57
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/Fixtures.hs67
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/GoogleSignIn.hs111
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/Main.hs37
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/Spec.hs74
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/Stripe.hs29
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/TestUtils.hs17
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/Types.hs146
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/Utils.hs8
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/default.nix28
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/index.html35
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/init.sql41
-rw-r--r--users/wpcarro/website/sandbox/learnpianochords/src/server/shell.nix18
-rw-r--r--users/wpcarro/website/sandbox/typo-po/README.md10
-rw-r--r--users/wpcarro/zoo/.envrc2
-rw-r--r--users/wpcarro/zoo/.ghci5
-rw-r--r--users/wpcarro/zoo/Main.hs160
-rw-r--r--users/wpcarro/zoo/Spec.hs54
-rw-r--r--users/wpcarro/zoo/default.nix21
-rw-r--r--users/wpcarro/zoo/shell.nix10
-rw-r--r--users/zseri/.gitignore2
-rw-r--r--users/zseri/OWNERS3
-rw-r--r--users/zseri/dbwospof.md112
-rw-r--r--users/zseri/store-ref-scanner/.gitignore1
-rw-r--r--users/zseri/store-ref-scanner/Cargo.toml11
-rw-r--r--users/zseri/store-ref-scanner/default.nix49
-rw-r--r--users/zseri/store-ref-scanner/fuzz/.gitignore2
-rw-r--r--users/zseri/store-ref-scanner/fuzz/Cargo.lock44
-rw-r--r--users/zseri/store-ref-scanner/fuzz/Cargo.toml36
-rw-r--r--users/zseri/store-ref-scanner/fuzz/fuzz_targets/hbm-roundtrip.rs10
-rw-r--r--users/zseri/store-ref-scanner/fuzz/fuzz_targets/nocrash.rs9
-rw-r--r--users/zseri/store-ref-scanner/src/hbm.rs167
-rw-r--r--users/zseri/store-ref-scanner/src/lib.rs215
-rw-r--r--users/zseri/store-ref-scanner/src/spec.rs40
-rw-r--r--views/.skip-subtree2
-rw-r--r--views/README.md6
-rw-r--r--views/kit/README.md25
-rw-r--r--views/kit/buildkite.yml16
-rw-r--r--views/kit/default.nix31
-rw-r--r--views/kit/workspace.josh13
-rw-r--r--web/atom-feed/default.nix153
-rw-r--r--web/atward/.gitignore3
-rw-r--r--web/atward/Cargo.lock668
-rw-r--r--web/atward/Cargo.toml9
-rw-r--r--web/atward/build.rs55
-rw-r--r--web/atward/default.nix8
-rw-r--r--web/atward/indexHtml/default.nix99
-rw-r--r--web/atward/src/main.rs200
-rw-r--r--web/atward/src/opensearch.xml8
-rw-r--r--web/atward/src/tests.rs187
-rw-r--r--web/blog/default.nix63
-rw-r--r--web/blog/fragments.nix96
-rw-r--r--web/bubblegum/OWNERS3
-rw-r--r--web/bubblegum/README.md68
-rw-r--r--web/bubblegum/default.nix267
-rw-r--r--web/bubblegum/examples/blog.nix143
-rw-r--r--web/bubblegum/examples/default.nix82
-rw-r--r--web/bubblegum/examples/derivation-svg.nix13
-rw-r--r--web/bubblegum/examples/hello.nix94
-rw-r--r--web/bubblegum/examples/posts/2021-04-01-hello.html3
-rw-r--r--web/bubblegum/examples/posts/2021-04-02-a second post.html7
-rw-r--r--web/converse/.gitignore3
-rw-r--r--web/converse/Cargo.lock3069
-rw-r--r--web/converse/Cargo.toml37
-rw-r--r--web/converse/LICENSE674
-rw-r--r--web/converse/README.md14
-rw-r--r--web/converse/build.rs5
-rw-r--r--web/converse/default.nix7
-rw-r--r--web/converse/envrc.example14
-rw-r--r--web/converse/migrations/.gitkeep0
-rw-r--r--web/converse/migrations/00000000000000_diesel_initial_setup/down.sql6
-rw-r--r--web/converse/migrations/00000000000000_diesel_initial_setup/up.sql36
-rw-r--r--web/converse/migrations/2018-04-08-133240_create_posts/down.sql2
-rw-r--r--web/converse/migrations/2018-04-08-133240_create_posts/up.sql13
-rw-r--r--web/converse/migrations/2018-04-08-161017_joinable_posts/down.sql1
-rw-r--r--web/converse/migrations/2018-04-08-161017_joinable_posts/up.sql1
-rw-r--r--web/converse/migrations/2018-04-08-172739_default_posted/down.sql2
-rw-r--r--web/converse/migrations/2018-04-08-172739_default_posted/up.sql2
-rw-r--r--web/converse/migrations/2018-04-08-182319_add_authors/down.sql5
-rw-r--r--web/converse/migrations/2018-04-08-182319_add_authors/up.sql10
-rw-r--r--web/converse/migrations/2018-04-14-140818_posts_only_in_posts/down.sql1
-rw-r--r--web/converse/migrations/2018-04-14-140818_posts_only_in_posts/up.sql6
-rw-r--r--web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/down.sql2
-rw-r--r--web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/up.sql21
-rw-r--r--web/converse/migrations/2018-04-14-170750_search-index/down.sql2
-rw-r--r--web/converse/migrations/2018-04-14-170750_search-index/up.sql21
-rw-r--r--web/converse/migrations/2018-05-01-141548_add-users/down.sql63
-rw-r--r--web/converse/migrations/2018-05-01-141548_add-users/up.sql83
-rw-r--r--web/converse/migrations/2018-05-01-183232_simplified-post-view/down.sql1
-rw-r--r--web/converse/migrations/2018-05-01-183232_simplified-post-view/up.sql11
-rw-r--r--web/converse/migrations/2018-05-25-160648_add_closed_column/down.sql1
-rw-r--r--web/converse/migrations/2018-05-25-160648_add_closed_column/up.sql1
-rw-r--r--web/converse/migrations/2018-05-25-161939_add_closed_to_index/down.sql30
-rw-r--r--web/converse/migrations/2018-05-25-161939_add_closed_to_index/up.sql35
-rw-r--r--web/converse/src/db.rs317
-rw-r--r--web/converse/src/errors.rs139
-rw-r--r--web/converse/src/handlers.rs391
-rw-r--r--web/converse/src/main.rs239
-rw-r--r--web/converse/src/models.rs127
-rw-r--r--web/converse/src/oidc.rs170
-rw-r--r--web/converse/src/render.rs245
-rw-r--r--web/converse/src/schema.rs83
-rw-r--r--web/converse/static/highlight.css99
-rw-r--r--web/converse/static/highlight.js2
-rw-r--r--web/converse/static/styles.css145
-rw-r--r--web/converse/templates/index.html80
-rw-r--r--web/converse/templates/post.html124
-rw-r--r--web/converse/templates/search.html67
-rw-r--r--web/converse/templates/thread.html111
-rw-r--r--web/converse/todo.org13
-rw-r--r--web/panettone/.envrc1
-rw-r--r--web/panettone/.gitignore1
-rw-r--r--web/panettone/OWNERS5
-rw-r--r--web/panettone/default.nix55
-rw-r--r--web/panettone/docker-compose.yml11
-rw-r--r--web/panettone/panettone.asd6
-rw-r--r--web/panettone/shell.nix15
-rw-r--r--web/panettone/src/.gitignore2
-rw-r--r--web/panettone/src/authentication.lisp115
-rw-r--r--web/panettone/src/css.lisp223
-rw-r--r--web/panettone/src/email.lisp49
-rw-r--r--web/panettone/src/inline-markdown.lisp127
-rw-r--r--web/panettone/src/irc.lisp35
-rw-r--r--web/panettone/src/model.lisp420
-rw-r--r--web/panettone/src/packages.lisp87
-rw-r--r--web/panettone/src/panettone.lisp650
-rw-r--r--web/panettone/src/util.lisp15
-rw-r--r--web/panettone/test/inline-markdown_test.lisp54
-rw-r--r--web/panettone/test/irc_test.lisp5
-rw-r--r--web/panettone/test/model_test.lisp13
-rw-r--r--web/panettone/test/package.lisp3
-rw-r--r--web/panettone/test/util_test.lisp9
-rw-r--r--web/static/default.nix29
-rw-r--r--web/static/favicon.webpbin0 -> 18750 bytes
-rw-r--r--web/static/files/adisbladis_tazjin_tvix.webpbin0 -> 107066 bytes
-rw-r--r--web/static/files/flokli_tazjin_tvix.webpbin0 -> 73790 bytes
-rw-r--r--web/static/jetbrains-mono-bold-italic.woff2bin0 -> 53364 bytes
-rw-r--r--web/static/jetbrains-mono-bold.woff2bin0 -> 49892 bytes
-rw-r--r--web/static/jetbrains-mono-italic.woff2bin0 -> 50936 bytes
-rw-r--r--web/static/jetbrains-mono.woff2bin0 -> 48700 bytes
-rw-r--r--web/static/tvl.css183
-rw-r--r--web/todolist/default.nix116
-rw-r--r--web/todolist/extract-todos.jq30
-rw-r--r--web/tvl/blog/default.nix18
-rw-r--r--web/tvl/blog/rewriting-nix.md90
-rw-r--r--web/tvl/default.nix131
-rw-r--r--web/tvl/footer/default.nix21
-rw-r--r--web/tvl/logo/default.nix97
-rw-r--r--web/tvl/logo/logo-shapes.svg27
-rw-r--r--web/tvl/template/default.nix52
-rw-r--r--web/tvl/tvl.dot172
2639 files changed, 432665 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000000..71a05d58b7
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,7 @@
+# Configure the local PATH to contain tools which are fetched ad-hoc
+# from Nix.
+
+out=$(nix-build -A tools.depot-deps --no-out-link)
+PATH_add "$out/bin"
+
+watch_file tools/depot-deps.nix
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000000..ca28d690e8
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,26 @@
+# This file contains commits that should be ignored when calculating
+# git blame layers.
+#
+# You should add it to your local configuration using:
+#    git config blame.ignoreRevsFile .git-blame-ignore-revs
+
+# style(3p/nix): Reformat project in Google C++ style
+0f2cf531f705d370321843e5ba9135b2ebdb5d19
+
+# style(3p/nix): Reformat all includes to match new style
+c758de9d22506eb279c5abe61f621e5c8f61af95
+
+# style(3p/nix): Reformat all includes to match new style
+c758de9d22506eb279c5abe61f621e5c8f61af95
+
+# style(3p/nix): Add braces around single-line conditionals
+867055133d3f487e52dd44149f76347c2c28bf10
+
+# style(3p/nix): Add braces around single-line for-loops
+1841d93ccbe5792a17f5b9a22e65ec898c7c2668
+
+# style(3p/nix): Final act in the brace-wrapping saga
+39087321811e81e26a1a47d6967df1088dcf0e95
+
+# style: format entire depot with nixpkgs-fmt
+aa122cbae78ce97d60c0c98ba14df753d97e40b1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..0b135e7034
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+# Ignore the garbage folder, in which I slowly assemble a bunch of
+# trash locally that might be valuable in the future.
+garbage/
+
+# Ignore Nix result symlinks
+result
+result-*
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000000..6b8710a711
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1 @@
+.git
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000000..06e6c9a449
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1 @@
+Alyssa Ross <hi@alyssa.is>
diff --git a/.nixery/README.md b/.nixery/README.md
new file mode 100644
index 0000000000..1762091300
--- /dev/null
+++ b/.nixery/README.md
@@ -0,0 +1,13 @@
+Nixery set
+==========
+
+This folder exports a special import of the depot Nix structure that is
+compatible with Nixery, by extending nixpkgs with a `tvl` attribute containing
+the depot.
+
+This is required because Nixery expects its package set to look like nixpkgs at
+the top-level.
+
+In the future we might want to patch Nixery to not require this (e.g. make it
+possible to pass `third_party.nixpkgs` as a key at which to find the nixpkgs
+structure).
diff --git a/.nixery/default.nix b/.nixery/default.nix
new file mode 100644
index 0000000000..19da286ee3
--- /dev/null
+++ b/.nixery/default.nix
@@ -0,0 +1,6 @@
+# See README.md
+{ depot ? import ../. {}, ... }:
+
+depot.third_party.nixpkgs.extend(_: _: {
+  tvl = depot;
+})
diff --git a/.rgignore b/.rgignore
new file mode 100644
index 0000000000..6c19dfbda4
--- /dev/null
+++ b/.rgignore
@@ -0,0 +1 @@
+third_party/git
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..bdc72a2e03
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Vincent Ambo
+Copyright (c) 2020-2021 The TVL Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000000..cc2aa26cf2
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,6 @@
+inherited: false
+owners:
+  - tazjin
+  - lukegb
+  - grfn
+  - sterni
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..986b736275
--- /dev/null
+++ b/README.md
@@ -0,0 +1,124 @@
+depot
+=====
+
+[![Build status](https://badge.buildkite.com/016bff4b8ae2704a3bbbb0a250784e6692007c582983b6dea7.svg?branch=refs/heads/canon)](https://buildkite.com/tvl/depot)
+
+This repository is the [monorepo][] for the community around [The
+Virus Lounge][tvl], containing our personal tools and infrastructure.
+Everything in here is built using [Nix][].
+
+A large portion of the software here is very self-referential, meaning that it
+exists to sustain the operation of the repository. This is the case because we
+partially see this as [an experiment][] in tooling for monorepos.
+
+# Highlights
+
+## Services
+
+* Source code is available primarily through Sourcegraph on
+  [cs.tvl.fyi](https://cs.tvl.fyi), where it is searchable and even semantically
+  indexed. A lower-tech view of the repository is also available via cgit-pink
+  on [code.tvl.fyi](https://code.tvl.fyi).
+
+  The repository can be cloned using `git` from `https://cl.tvl.fyi/depot`.
+
+* All code in the depot, with the exception of code that is checked in to
+  individual `//users` folders, needs to be reviewed. We use Gerrit on
+  [cl.tvl.fyi](https://cl.tvl.fyi) for this.
+
+* Issues are tracked via our own issue tracker on
+  [b.tvl.fyi](https://b.tvl.fyi). Its source code lives at
+  [`//web/panettone/`][panettone].
+
+* Smaller todo-list entries which do not warrant a separate issue are listed at
+  [todo.tvl.fyi](https://todo.tvl.fyi).
+
+* We use Buildkite for CI. Recent builds are listed on
+  [tvl.fyi/builds](https://tvl.fyi/builds) and pipelines are configured
+  dynamically via
+  [`//ops/pipelines`](https://cs.tvl.fyi/depot/-/tree/ops/pipelines).
+
+* A search service that makes TVL services available via textual
+  shortcuts is available: [atward](https://at.tvl.fyi)
+
+All services that we host are deployed on NixOS machines that we manage. Their
+configuration is tracked in `//ops/{modules,machines}`.
+
+## Nix
+
+* [`//nix/readTree`](https://cs.tvl.fyi/depot/-/blob/nix/readTree/README.md)
+  contains the Nix code which automatically registers projects in our Nix
+  attribute hierarchy based on their in-tree location
+* [`//tools/nixery`](https://cs.tvl.fyi/depot/-/tree/tools/nixery)
+  contains the source code of [Nixery][], a container registry that
+  can build images ad-hoc from Nix packages
+* `//nix/yants` contains **Y**et **A**nother **N**ix **T**ype **S**ystem, which
+  we use for a variety of things throughout the repository
+* `//nix/buildGo` implements a Nix library that can build Go software in the
+  style of Bazel's `rules_go`. Go programs in this repository are built using
+  this library.
+* `//nix/buildLisp` implements a Nix library that can build Common Lisp
+  software. Currently only SBCL is supported. Lisp programs in this repository
+  are built using this library.
+* `//web/blog` and `//web/atom-feed`: A Nix-based static site generator which
+  generates the web page and Atom feed for [tazj.in](https://tazj.in)
+  (`//users/tazjin/homepage`) and [tvl.fyi](https://tvl.fyi) (`//web/tvl`)
+* `//web/bubblegum` contains a CGI-based web framework written in Nix.
+* `//nix/nint`: A shebang-compatible interpreter wrapper for Nix.
+* `//tvix` contains initial work towards a modular architecture for Nix.
+
+We have a variety of other tools and libraries in the `//nix` folder which may
+be of interest.
+
+## Packages / Libraries
+
+* `//net/alcoholic_jwt` contains an easy-to-use JWT-validation library for Rust
+* `//net/crimp` contains a high-level HTTP client using cURL for Rust
+* `//tools/emacs-pkgs` contains various useful Emacs libraries, for example:
+  * `dottime.el` provides [dottime][] in the Emacs modeline
+  * `nix-util.el` provides editing utilities for Nix files
+  * `term-switcher.el` is an ivy-function for switching between vterm buffers
+  * `tvl.el` provides helper functions for interacting with the TVL monorepo
+* `//lisp/klatre` provides a grab-bag utility library for Common Lisp
+
+## User packages
+
+Contributors to the repository have user directories under
+[`//users`](https://cs.tvl.fyi/depot@canon/-/tree/users), which can be used for
+personal or experimental code that does not require review.
+
+Some examples:
+
+* `//users/grfn/xanthous`: A (WIP) TUI RPG, written in Haskell.
+* `//users/tazjin/emacs`: tazjin's Emacs & EXWM configuration
+* `//users/tazjin/finito`: A persistent finite-state machine library for Rust.
+
+# Licensing
+
+Unless otherwise stated in a subdirectory, all code is licensed under the MIT
+license. See [LICENSE](./LICENSE) for details.
+
+# Contributing
+
+If you'd like to contribute to any of the tools in here, please check out the
+[contribution guidelines](./docs/CONTRIBUTING.md) and our [code of
+conduct](./docs/CODE_OF_CONDUCT.md).
+
+IRC users can find us in [`#tvl`][tvl-irc] on [hackint][], which is also
+reachable [via XMPP][hackint-xmpp] at [`#tvl@irc.hackint.org`][tvl-xmpp] (sic!).
+
+Hackint also provide a [web chat][tvl-webchat].
+
+[monorepo]: https://en.wikipedia.org/wiki/Monorepo
+[tvl]: https://tvl.fyi
+[Nix]: https://nixos.org/nix
+[an experiment]: https://tvl.fyi/monorepo-doc
+[panettone]: https://cs.tvl.fyi/depot@canon/-/tree/web/panettone
+[tvix]: https://cs.tvl.fyi/depot/-/blob/third_party/nix/README.md
+[dottime]: https://dotti.me
+[tvl-irc]: ircs://irc.hackint.org:6697/#tvl
+[hackint]: https://hackint.org/
+[hackint-xmpp]: https://hackint.org/transport/xmpp
+[tvl-xmpp]: xmpp:#tvl@irc.hackint.org?join
+[tvl-webchat]: https://webirc.hackint.org/#ircs://irc.hackint.org/#tvl
+[Nixery]: https://nixery.dev
diff --git a/RULES b/RULES
new file mode 100644
index 0000000000..c6089c4bfd
--- /dev/null
+++ b/RULES
@@ -0,0 +1 @@
+1. Only those who pay the Golden Price may bear The Wheel (for the most part).
diff --git a/buf.yaml b/buf.yaml
new file mode 100644
index 0000000000..42c769f2e4
--- /dev/null
+++ b/buf.yaml
@@ -0,0 +1,13 @@
+build:
+  roots:
+    #- proto
+    - third_party
+lint:
+  ignore:
+    - nix/buildGo
+  use:
+    - BASIC
+    - FILE_LOWER_SNAKE_CASE
+  except:
+    - ENUM_VALUE_UPPER_SNAKE_CASE
+    - PACKAGE_DIRECTORY_MATCH
diff --git a/corp/LICENSE b/corp/LICENSE
new file mode 100644
index 0000000000..f29fc84038
--- /dev/null
+++ b/corp/LICENSE
@@ -0,0 +1,4 @@
+Copyright 2021 ООО ТВЛ
+
+Code under this folder may be redistributed as part of the TVL depot
+repository. All other usage rights for this code are reserved.
diff --git a/corp/OWNERS b/corp/OWNERS
new file mode 100644
index 0000000000..4bc08d35f6
--- /dev/null
+++ b/corp/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - tvl-employees
diff --git a/corp/website/content.md b/corp/website/content.md
new file mode 100644
index 0000000000..f7ca9786fb
--- /dev/null
+++ b/corp/website/content.md
@@ -0,0 +1,26 @@
+The Virus Lounge
+================
+
+----------------
+
+<img class="tvl-logo" src="https://static.tvl.su/latest/logo-animated.svg"
+     alt="Virus with lambda-shaped spike proteins sitting on an armchair">
+
+Welcome to the corporate face of [The Virus Lounge][tvl-fyi].
+
+We provide technology consulting around a variety of topics, for
+example:
+
+* Advice and setup of organisation-wide monorepos for effective
+  developer workflows, including associated tooling like CI/CD
+* Assistance with anything related to Nix/NixOS
+* Software development in various languages (Rust, Common Lisp,
+  Erlang, Java and more)
+
+We might be able to help you with other things on request.
+
+Note: We are still in the process of getting started and have limited
+capacity at the moment. If you would like our help, please reach out
+at **contact {at} tvl.su** for a discussion.
+
+[tvl-fyi]: https://tvl.fyi
diff --git a/corp/website/default.nix b/corp/website/default.nix
new file mode 100644
index 0000000000..2011e2a376
--- /dev/null
+++ b/corp/website/default.nix
@@ -0,0 +1,37 @@
+{ depot, pkgs, ... }:
+
+
+let
+  # https://developers.google.com/search/docs/advanced/structured-data/logo
+  structuredData = {
+    "@context" = "https://schema.org";
+    "@type" = "Organisation";
+    url = "https://tvl.su";
+    logo = "https://static.tvl.fyi/${depot.web.static.drvHash}/logo-animated.svg";
+  };
+  index = depot.web.tvl.template {
+    title = "TVL (The Virus Lounge) - Software consulting";
+    content = builtins.readFile ./content.md;
+    extraFooter = "\n|\n © ООО ТВЛ";
+
+    # TODO(tazjin): The `.tvl-logo` thing can probably go in the shared CSS.
+    extraHead = ''
+      <meta name="description" content="TVL provides technology consulting for monorepos, Nix, and other SRE/DevOps/Software Engineering topics.">
+      <script type="application/ld+json">
+        ${builtins.toJSON structuredData}
+      </script>
+      <style>
+        .tvl-logo {
+          width: 60%;
+          display: block;
+          margin-left: auto;
+          margin-right: auto;
+        }
+      </style>
+    '';
+  };
+in
+pkgs.runCommandNoCC "corp-website" { } ''
+  mkdir $out
+  cp ${index} $out/index.html
+''
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000000..7cdf32bef9
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,122 @@
+# This file sets up the top-level package set by traversing the package tree
+# (see //nix/readTree for details) and constructing a matching attribute set
+# tree.
+
+{ nixpkgsBisectPath ? null
+, parentTargetMap ? null
+, nixpkgsConfig ? { }
+, ...
+}@args:
+
+let
+  inherit (builtins)
+    filter
+    ;
+
+  readTree = import ./nix/readTree { };
+
+  # Disallow access to //users from other depot parts.
+  usersFilter = readTree.restrictFolder {
+    folder = "users";
+    reason = ''
+      Code under //users is not considered stable or dependable in the
+      wider depot context. If a project under //users is required by
+      something else, please move it to a different depot path.
+    '';
+
+    exceptions = [
+      # whitby is allowed to access //users for several reasons:
+      #
+      # 1. User SSH keys are set in //users.
+      # 2. Some personal websites or demo projects are served from it.
+      [ "ops" "machines" "whitby" ]
+
+      # Due to evaluation order this also affects these targets.
+      # TODO(tazjin): Can this one be removed somehow?
+      [ "ops" "nixos" ]
+      [ "ops" "machines" "all-systems" ]
+    ];
+  };
+
+  # Disallow access to //corp from other depot parts.
+  corpFilter = readTree.restrictFolder {
+    folder = "corp";
+    reason = ''
+      Code under //corp may use incompatible licensing terms with
+      other depot parts and should not be used anywhere else.
+    '';
+
+    exceptions = [
+      # For the same reason as above, whitby is exempt to serve the
+      # corp website.
+      [ "ops" "machines" "whitby" ]
+      [ "ops" "nixos" ]
+      [ "ops" "machines" "all-systems" ]
+    ];
+  };
+
+  readDepot = depotArgs: readTree {
+    args = depotArgs;
+    path = ./.;
+    filter = parts: args: corpFilter parts (usersFilter parts args);
+    scopedArgs = {
+      __findFile = _: _: throw "Do not import from NIX_PATH in the depot!";
+    };
+  };
+
+  # To determine build targets, we walk through the depot tree and
+  # fetch attributes that were imported by readTree and are buildable.
+  #
+  # Any build target that contains `meta.ci.skip = true` will be skipped.
+
+  # Is this tree node eligible for build inclusion?
+  eligible = node: (node ? outPath) && !(node.meta.ci.skip or false);
+
+in
+readTree.fix (self: (readDepot {
+  depot = self;
+
+  # Pass third_party as 'pkgs' (for compatibility with external
+  # imports for certain subdirectories)
+  pkgs = self.third_party.nixpkgs;
+
+  # Expose lib attribute to packages.
+  lib = self.third_party.nixpkgs.lib;
+
+  # Pass arguments passed to the entire depot through, for packages
+  # that would like to add functionality based on this.
+  #
+  # Note that it is intended for exceptional circumstance, such as
+  # debugging by bisecting nixpkgs.
+  externalArgs = args;
+}) // {
+  # Make the path to the depot available for things that might need it
+  # (e.g. NixOS module inclusions)
+  path = self.third_party.nixpkgs.lib.cleanSourceWith {
+    name = "depot";
+    src = ./.;
+    filter = self.third_party.nixpkgs.lib.cleanSourceFilter;
+  };
+
+  # List of all buildable targets, for CI purposes.
+  #
+  # Note: To prevent infinite recursion, this *must* be a nested
+  # attribute set (which does not have a __readTree attribute).
+  ci.targets = readTree.gather eligible (self // {
+    # remove the pipelines themselves from the set over which to
+    # generate pipelines because that also leads to infinite
+    # recursion.
+    ops = self.ops // { pipelines = null; };
+
+    # remove nixpkgs from the set, for obvious reasons.
+    third_party = self.third_party // { nixpkgs = null; };
+  });
+
+  # Derivation that gcroots all depot targets.
+  ci.gcroot = with self.third_party.nixpkgs; makeSetupHook
+    {
+      name = "depot-gcroot";
+      deps = self.ci.targets;
+    }
+    emptyFile;
+})
diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..0e46bbedb0
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT.md
@@ -0,0 +1,29 @@
+A SERMON ON ETHICS AND LOVE
+===========================
+
+One day Mal-2 asked the messenger spirit Saint Gulik to approach the
+Goddess and request Her presence for some desperate advice. Shortly
+afterwards the radio came on by itself, and an ethereal female Voice
+said **YES?**
+
+"O! Eris! Blessed Mother of Man! Queen of Chaos! Daughter of Discord!
+Concubine of Confusion! O! Exquisite Lady, I beseech You to lift a
+heavy burden from my heart!"
+
+**WHAT BOTHERS YOU, MAL? YOU DON'T SOUND WELL.**
+
+"I am filled with fear and tormented with terrible visions of pain.
+Everywhere people are hurting one another, the planet is rampant with
+injustices, whole societies plunder groups of their own people,
+mothers imprison sons, children perish while brothers war. O, woe."
+
+**WHAT IS THE MATTER WITH THAT, IF IT IS WHAT YOU WANT TO DO?**
+
+"But nobody Wants it! Everybody hates it."
+
+**OH. WELL, THEN *STOP*.**
+
+At which moment She turned herself into an aspirin commercial and left
+The Polyfather stranded alone with his species.
+
+SINISTER DEXTER HAS A BROKEN SPIROMETER.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000000..dd138d3bfc
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,120 @@
+Contribution Guidelines
+=======================
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Contribution Guidelines](#contribution-guidelines)
+    - [Before making a change](#before-making-a-change)
+    - [Commit messages](#commit-messages)
+    - [Commit content](#commit-content)
+    - [Code quality](#code-quality)
+    - [Builds & tests](#builds--tests)
+    - [Submitting changes](#submitting-changes)
+
+<!-- markdown-toc end -->
+
+This is a loose set of "guidelines" for contributing to the depot. Please note
+that we will not accept any patches that don't follow these guidelines.
+
+Also consider the [code of conduct](./CODE_OF_CONDUCT.md). No really,
+you should.
+
+## Before making a change
+
+Before making a change, consider your motivation for making the change.
+Documentation updates, bug fixes and the like are *always* welcome.
+
+When adding a feature you should consider whether it is only useful for your
+particular use-case or whether it is generally applicable for other users of the
+project.
+
+When in doubt - just ask! You can reach out to us at
+[depot@tazj.in](mailto:depot@tazj.in) or on Twitter, IRC, etc.
+
+## Commit messages
+
+All commit messages should be structured like this:
+
+```
+type(scope): Subject line with at most a 68 character length
+
+Body of the commit message with an empty line between subject and
+body. This text should explain what the change does and why it has
+been made, *especially* if it introduces a new feature.
+
+Relevant issues should be mentioned if they exist.
+```
+
+Where `type` can be one of:
+
+* `feat`: A new feature has been introduced
+* `fix`: An issue of some kind has been fixed
+* `docs`: Documentation or comments have been updated
+* `style`: Formatting changes only
+* `refactor`: Hopefully self-explanatory!
+* `test`: Added missing tests / fixed tests
+* `chore`: Maintenance work
+* `subtree`: Operations involving `git subtree`
+
+And `scope` should refer to some kind of logical grouping inside of the project.
+
+It does not make sense to include the full path unless it aids in
+disambiguating. For example, when changing the configuration of the host
+`whitby` at `//ops/machines/whitby` it is enough to write `feat(whitby): ...`.
+
+Please take a look at the existing commit log for examples.
+
+## Commit content
+
+Multiple changes should be divided into multiple git commits whenever possible.
+Common sense applies.
+
+The fix for a single-line whitespace issue is fine to include in a different
+commit. Introducing a new feature and refactoring (unrelated) code in the same
+commit is not fine.
+
+`git commit -a` is generally **taboo**.
+
+In my experience making "sane" commits becomes *significantly* easier as
+developer tooling is improved. The interface to `git` that I recommend is
+[magit][]. Even if you are not yet an Emacs user, it makes sense to install
+Emacs just to be able to use magit - it is really that good.
+
+For staging sane chunks on the command line with only git, consider `git add
+-p`.
+
+## Code quality
+
+This one should go without saying - but please ensure that your code quality
+does not fall below the rest of the project. This is of course very subjective,
+but as an example if you place code that throws away errors into a block in
+which errors are handled properly your change will be rejected.
+
+In my experience there is a strong correlation between the visual appearance of
+a code block and its quality. This is a simple way to sanity-check your work
+while squinting and keeping some distance from your screen ;-)
+
+## Builds & tests
+
+All projects are built using [Nix][] to avoid "build pollution" via the user's
+environment.
+
+If you have Nix installed and are contributing to a project tracked in this
+repository, you can usually build the project by calling `nix-build -A
+path.to.project`.
+
+For example, to build a project located at `//tools/foo` you would call
+`nix-build -A tools.foo`
+
+If the project has tests, check that they still work before submitting your
+change.
+
+## Submitting changes
+
+The code review & change submission process is described in the [code
+review][] documentation.
+
+[magit]: https://magit.vc/
+[Nix]: https://nixos.org/nix/
+[code review]: ./REVIEWS.md
diff --git a/docs/REVIEWS.md b/docs/REVIEWS.md
new file mode 100644
index 0000000000..e1c657b333
--- /dev/null
+++ b/docs/REVIEWS.md
@@ -0,0 +1,154 @@
+TVL Code Reviews
+================
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [TVL Code Reviews](#tvl-code-reviews)
+    - [Gerrit setup](#gerrit-setup)
+    - [Gerrit workflows](#gerrit-workflows)
+    - [Review process & approvals](#review-process--approvals)
+    - [Registration](#registration)
+    - [Submitting changes via email](#submitting-changes-via-email)
+
+<!-- markdown-toc end -->
+
+
+This document describes the TVL code review process & tooling. If you are
+looking for general contribution guidelines, please look at the [general
+contribution guidelines](./CONTRIBUTING.md).
+
+All changes are tracked at [cl.tvl.fyi](https://cl.tvl.fyi) using Gerrit. See
+[Registration](#registration) for information on how to register an account.
+
+## Gerrit setup
+
+Gerrit uses the concept of change IDs to track commits across rebases and other
+operations that might change their hashes, and link them to unique changes in
+Gerrit.
+
+First, [tell Gerrit][Gerrit SSH] about your SSH keys.
+
+Then, to make using Gerrit smooth for users, the repository should be cloned and
+a commit hook should be installed as follows:
+
+```
+git clone "ssh://$USER@code.tvl.fyi:29418/depot"
+scp -p -P 29418 $USER@code.tvl.fyi:hooks/commit-msg "depot/.git/hooks/"
+```
+
+If you have a previous clone of the depot via HTTP you can use `git remote
+set-url` to update the origin URL and install the hook in the same way as above.
+
+## Gerrit workflows
+
+The developer workflow on Gerrit is quite different from what GitHub-users are
+used to.
+
+The depot does not have branches (other than Gerrit's internal metadata refs)
+and all development happens at `HEAD`.
+
+Every time you create a new commit the change hook will insert a unique
+`Change-Id` tag into the commit message. Once you are satisfied with the state
+of your commit and want to submit it for review, you push it to a git ref called
+`refs/for/canon`. This designates the commits as changelists (CLs) targeted for
+the `canon` branch.
+
+Sending a change for review is done by pushing to a special target. You can set
+this to be the default push target through your git configuration:
+
+```
+git config remote.origin.url "ssh://$USER@code.tvl.fyi:29418/depot"
+git config remote.origin.push HEAD:refs/for/canon
+```
+
+Then, after making your change, push to the default, or to a special target:
+
+```
+Example:
+git commit -m 'docs(REVIEWS): Fixed all the errors in the reviews docs'
+git push origin
+
+# Uploading a work-in-progress CL:
+git push origin HEAD:refs/for/canon%wip
+```
+
+TIP: Every individual commit will become a separate change. We do not merge
+related commits, but instead submit them one by one. Be aware that if you are
+expecting a different behaviour and attempt something like an unsquashed subtree
+merge, you will produce a *lot* of CLs. This is strongly discouraged.
+
+During your review, the reviewer(s) might ask you to make changes. You can
+simply amend your commit(s) and push to the same ref. Gerrit will automatically
+update your changes.
+
+Read more about the Gerrit workflow in the [Gerrit walkthrough][].
+
+## Review process & approvals
+
+Each user has the ability to create their own users directory in
+`//users/<username>` in which they can submit code without review from other
+contributors (they will still need to +2 their own changes, and the initial
+check-in of the `OWNERS` file needs to be reviewed).
+
+You can set a directory like this up for yourself by proposing a change similar
+to [CL/246](https://cl.tvl.fyi/c/depot/+/246).
+
+For all paths outside of `//users`, code review is required. We have no strict
+guidelines about the review process itself, as we're not a megacorp, but we have
+formalised checks before submitting:
+
+* At least one person who is [an owner][OWNERS] of the codepath must have given
+  a +2 review
+* The commit message must conform to our [guidelines][]
+* No code review comments must be left unresolved
+
+If all these conditions are fulfilled, the **change author submits their change
+themselves**.
+
+## Registration
+
+If you would like to have an account on the Gerrit instance, follow these
+instructions:
+
+1. Be a member of `#tvl` on [hackint][].
+2. Clone the depot locally (via `git clone "https://cl.tvl.fyi/depot"`).
+3. Create a user entry in our LDAP server in [ops/users][ops-users].
+
+   We recommend using ARGON2 password hashes, which can be created
+   with the `slappasswd` tool if OpenLDAP was compiled with ARGON2
+   support.
+
+   For convenience, we provide a wrapper script for this that you can
+   build with `nix-build -A tools.hash-password` in a depot checkout.
+   Alternatively, if you have `direnv` installed, you can add the
+   depot to your allowlist and just run `hash-password` which should
+   be added to your `$PATH` by `direnv`.
+
+   You can probably create ARGON2 hashes with other tools, but that is
+   your job to figure out.
+4. Create a commit adding yourself (see e.g.
+   [CL/2671](https://cl.tvl.fyi/c/depot/+/2671))
+5. Submit the commit via email (see below).
+
+## Submitting changes via email
+
+You can submit a patch via email to `depot@tazj.in` and it will be added to
+Gerrit by a contributor.
+
+Create an appropriate commit locally and send it us using either of these options:
+
+* `git format-patch`: This will create a `.patch` file which you should email to
+  us.
+* `git send-email`: If configured on your system, this will take care of the
+  whole emailing process for you.
+
+The email address is a [public group][].
+
+[Gerrit SSH]: https://cl.tvl.fyi/settings/#SSHKeys
+[Gerrit walkthrough]: https://gerrit-review.googlesource.com/Documentation/intro-gerrit-walkthrough.html
+[OWNERS]: https://cl.tvl.fyi/plugins/owners/Documentation/config.md
+[guidelines]: ./CONTRIBUTING.md#commit-messages
+[ops-users]: ../ops/users/default.nix
+[public group]: https://groups.google.com/a/tazj.in/forum/?hl=en#!forum/depot
+[hackint]: https://hackint.org
diff --git a/docs/designs/SPARSE_CHECKOUTS.md b/docs/designs/SPARSE_CHECKOUTS.md
new file mode 100644
index 0000000000..7bd4963f61
--- /dev/null
+++ b/docs/designs/SPARSE_CHECKOUTS.md
@@ -0,0 +1,37 @@
+Below is a prototype for a script to create Git sparse checkouts of the depot.
+The script below works today with relatively recent versions of git.
+
+Open items:
+
+ - Read-increment-write the checkout ID from a file in .git.
+ - `NICE_CHECKOUT_ROOT` should be a git configuration value.
+ - `tvl-get-depends` will be a script that contacts the build farm and asks for
+   the closure of a given source directory, using [depot-scan].
+
+```bash
+DEPOT_ROOT="${depot.path}"
+XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
+CLIENT_ROOT="$XDG_DATA_HOME/tvlc/clients"
+NICE_CHECKOUT_ROOT="$HOME/tvlc"
+CHECKOUT_ID=1
+CHECKOUT_NAME=myclient # command line variables
+
+assertAbsolutePath "$CLIENT_ROOT"
+
+mkdir "$CLIENT_ROOT"/"$CHECKOUT_ID"
+ln -s "$CLIENT_ROOT"/"$CHECKOUT_ID" "$NICE_CHECKOUT_ROOT"/"$CHECKOUT_NAME"
+
+cd "$DEPOT_ROOT"
+git worktree add --no-checkout -b "tvlc-$CHECKOUT_ID" "$CLIENT_ROOT/$CHECKOUT_ID/" canon
+# BUG: git not creating the /info/ subdir
+mkdir "$DEPOT_ROOT"/.git/worktrees/"$CHECKOUT_ID"/info
+
+cd "$CLIENT_ROOT/$CHECKOUT_ID"
+git sparse-checkout init --cone
+git sparse-checkout set "$TARGET_DIR" nix/readTree overrides
+tvl-get-depends "$TARGET_DIR" | xargs git sparse-checkout add
+
+cd "$NICE_CHECKOUT_ROOT"/"$CHECKOUT_NAME"
+```
+
+[depot-scan]: ../users/edef/depot-scan.nix
diff --git a/fun/clbot/backoffutil/backoffutil.go b/fun/clbot/backoffutil/backoffutil.go
new file mode 100644
index 0000000000..1b1ea5f9d0
--- /dev/null
+++ b/fun/clbot/backoffutil/backoffutil.go
@@ -0,0 +1,43 @@
+// Package backoffutil provides useful utilities for backoff.
+package backoffutil
+
+import (
+	"time"
+
+	backoff "github.com/cenkalti/backoff/v4"
+)
+
+// ZeroStartingBackOff is a backoff.BackOff that returns "0" as the first Duration after a reset.
+// This is useful for constructing loops and just enforcing a backoff duration on every loop, rather than incorporating this logic into the loop directly.
+type ZeroStartingBackOff struct {
+	bo      backoff.BackOff
+	initial bool
+}
+
+// NewZeroStartingBackOff creates a new ZeroStartingBackOff.
+func NewZeroStartingBackOff(bo backoff.BackOff) *ZeroStartingBackOff {
+	return &ZeroStartingBackOff{bo: bo, initial: true}
+}
+
+// NewDefaultBackOff creates a sensibly configured BackOff that starts at zero.
+func NewDefaultBackOff() backoff.BackOff {
+	ebo := backoff.NewExponentialBackOff()
+	ebo.MaxElapsedTime = 0
+	return NewZeroStartingBackOff(ebo)
+}
+
+// NextBackOff returns the next back off duration to use.
+// For the first call after a call to Reset(), this is 0. For each subsequent duration, the underlying BackOff is consulted.
+func (bo *ZeroStartingBackOff) NextBackOff() time.Duration {
+	if bo.initial == true {
+		bo.initial = false
+		return 0
+	}
+	return bo.bo.NextBackOff()
+}
+
+// Reset resets to the initial state, and also passes a Reset through to the underlying BackOff.
+func (bo *ZeroStartingBackOff) Reset() {
+	bo.initial = true
+	bo.bo.Reset()
+}
diff --git a/fun/clbot/backoffutil/default.nix b/fun/clbot/backoffutil/default.nix
new file mode 100644
index 0000000000..78585da236
--- /dev/null
+++ b/fun/clbot/backoffutil/default.nix
@@ -0,0 +1,14 @@
+{ depot, ... }:
+
+let
+  inherit (depot.third_party) gopkgs;
+in
+depot.nix.buildGo.package {
+  name = "code.tvl.fyi/fun/clbot/backoffutil";
+  srcs = [
+    ./backoffutil.go
+  ];
+  deps = [
+    gopkgs."github.com".cenkalti.backoff.gopkg
+  ];
+}
diff --git a/fun/clbot/clbot.go b/fun/clbot/clbot.go
new file mode 100644
index 0000000000..e5a5990ef2
--- /dev/null
+++ b/fun/clbot/clbot.go
@@ -0,0 +1,289 @@
+package main
+
+import (
+	"context"
+	"crypto/tls"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os"
+	"os/signal"
+	"strings"
+	"time"
+
+	"code.tvl.fyi/fun/clbot/backoffutil"
+	"code.tvl.fyi/fun/clbot/gerrit"
+	"code.tvl.fyi/fun/clbot/gerrit/gerritevents"
+	log "github.com/golang/glog"
+	"golang.org/x/crypto/ssh"
+	"gopkg.in/irc.v3"
+)
+
+var (
+	gerritAddr       = flag.String("gerrit_host", "cl.tvl.fyi:29418", "Gerrit SSH host:port")
+	gerritSSHHostKey = flag.String("gerrit_ssh_pubkey", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIUNYBYPCCBNDFSd0BuCR+8kgeuJ7IA5S2nTNQmkQUYNyXK+ot5os7rHtCk96+grd5+J8jFCuFBWisUe8h8NC0Q=", "Gerrit SSH public key")
+	gerritSSHTimeout = flag.Duration("gerrit_tcp_timeout", 5*time.Second, "Gerrit SSH TCP connect timeout")
+
+	gerritAuthUsername = flag.String("gerrit_ssh_auth_username", "", "Gerrit SSH username")
+	gerritAuthKeyPath  = flag.String("gerrit_ssh_auth_key", "", "Gerrit SSH private key path")
+
+	ircServer    = flag.String("irc_server", "irc.hackint.org:6697", "IRC server to connect to")
+	ircTls       = flag.Bool("irc_tls", false, "Does the server connection need TLS?")
+	ircNick      = flag.String("irc_nick", "clbot", "Nick to use when connecting to IRC")
+	ircUser      = flag.String("irc_user", "clbot", "User string to use for IRC")
+	ircName      = flag.String("irc_name", "clbot", "Name string to use for IRC")
+	ircChannel   = flag.String("irc_channel", "#tvl", "Channel to send messages to")
+	ircPassword  = flag.String("irc_pass", "", "Password to use for IRC")
+	ircSendLimit = flag.Duration("irc_send_limit", 100*time.Millisecond, "Delay between messages")
+	ircSendBurst = flag.Int("irc_send_burst", 10, "Number of messages which can be sent in a burst")
+
+	notifyRepo     = flag.String("notify_repo", "depot", "Repo name to notify about")
+	notifyBranches = stringSetFlag{}
+
+	neverPing = flag.String("never_ping", "marcus", "Comma-separated terms that should never ping users")
+)
+
+func init() {
+	flag.Var(&notifyBranches, "notify_branches", "Branch names (comma-separated, or repeated flags, or both) to notify users about")
+}
+
+type stringSetFlag map[string]bool
+
+func (f stringSetFlag) String() string {
+	return fmt.Sprintf("%q", map[string]bool(f))
+}
+func (f stringSetFlag) Set(s string) error {
+	if s == "" {
+		return nil
+	}
+	for _, k := range strings.Split(s, ",") {
+		if k != "" {
+			f[k] = true
+		}
+	}
+	return nil
+}
+
+func mustFixedHostKey(f string) ssh.HostKeyCallback {
+	pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(f))
+	if err != nil {
+		log.Exitf("ParseAuthorizedKey(%q): %v", f, err)
+	}
+	return ssh.FixedHostKey(pk)
+}
+
+func mustPrivateKey(p string) ssh.AuthMethod {
+	pkBytes, err := ioutil.ReadFile(p)
+	if err != nil {
+		log.Exitf("reading SSH private key from %q: %v", p, err)
+	}
+	pk, err := ssh.ParsePrivateKey(pkBytes)
+	if err != nil {
+		log.Exitf("parsing private key from %q: %v", p, err)
+	}
+	return ssh.PublicKeys(pk)
+}
+
+var shutdownFuncs []func()
+
+func callOnShutdown(f func()) {
+	shutdownFuncs = append(shutdownFuncs, f)
+}
+
+// Unicode U+200B zero-width-space, to avoid triggering other bots
+// or highlighting people on IRC.
+const zeroWidthSpace = "\u200b"
+
+func runIRC(ctx context.Context, ircCfg irc.ClientConfig, sendMsg <-chan string) {
+	bo := backoffutil.NewDefaultBackOff()
+	ircCfg.Handler = irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
+		if m.Command == "NOTICE" && m.Prefix.Name == "NickServ" && strings.Contains(m.Trailing(), "dentified") {
+			// We're probably identified now, go join the channel.
+			c.Writef("JOIN %s", *ircChannel)
+		}
+	})
+	for {
+		timer := time.NewTimer(bo.NextBackOff())
+		select {
+		case <-ctx.Done():
+			timer.Stop()
+			return
+		case <-timer.C:
+			break
+		}
+
+		(func() {
+			connectedStart := time.Now()
+
+			var ircConn net.Conn
+			var err error
+
+			if *ircTls {
+				ircConn, err = tls.Dial("tcp", *ircServer, nil)
+			} else {
+				ircConn, err = net.Dial("tcp", *ircServer)
+			}
+
+			if err != nil {
+				log.Errorf("connecting to IRC at tcp/%s (tls: %v): %v", *ircServer, *ircTls, err)
+				return
+			}
+
+			ircClient := irc.NewClient(ircConn, ircCfg)
+			ircClientCtx, cancel := context.WithCancel(ctx)
+			defer cancel()
+			go func() {
+				for {
+					select {
+					case <-ircClientCtx.Done():
+						return
+					case msg := <-sendMsg:
+						log.Infof("sending message %q to %v", msg, *ircChannel)
+						ircClient.Writef("PRIVMSG %s :%s%s", *ircChannel, zeroWidthSpace, msg)
+					}
+				}
+			}()
+			log.Infof("connecting to IRC on tcp/%s", *ircServer)
+			if err := ircClient.RunContext(ircClientCtx); err != nil {
+				connectedEnd := time.Now()
+				connectedFor := connectedEnd.Sub(connectedStart)
+				if connectedFor > 60*time.Second {
+					bo.Reset()
+				}
+				log.Errorf("IRC RunContext: %v", err)
+				return
+			}
+		})()
+	}
+}
+
+func username(a gerritevents.Account) string {
+	options := []string{
+		a.Username,
+		a.Name,
+		a.Email,
+	}
+	for _, opt := range options {
+		if opt != "" {
+			return opt
+		}
+	}
+	return "UNKNOWN USER"
+}
+
+// noping inserts a Unicode zero-width space between the first and rest characters of `user`
+// in an effort to avoid pinging that user on IRC.
+func noping(user string) string {
+	un := []rune(user)
+	return string(un[0:1]) + zeroWidthSpace + string(un[1:])
+}
+
+// Apply noping to each instance of the username in the supplied
+// message. With this users will not be pinged for their own CLs, but
+// they will be notified if someone else writes a CL that includes
+// their username.
+//
+// Also applies noping to all instances of the words in `neverPing`.
+func nopingAll(username, message string) string {
+	for _, word := range strings.Split(*neverPing, ",") {
+		message = strings.ReplaceAll(message, word, noping(word))
+	}
+
+	return strings.ReplaceAll(message, username, noping(username))
+}
+
+func patchSetURL(c gerritevents.Change, p gerritevents.PatchSet) string {
+	return fmt.Sprintf("https://cl.tvl.fyi/%d", c.Number)
+}
+
+func main() {
+	flag.Parse()
+	failed := false
+	if *gerritAuthUsername == "" {
+		log.Errorf("gerrit_ssh_auth_username must be set")
+		failed = true
+	}
+	if *gerritAuthKeyPath == "" {
+		log.Errorf("gerrit_ssh_auth_key must be set")
+		failed = true
+	}
+	if failed {
+		os.Exit(2)
+	}
+
+	shutdownCh := make(chan os.Signal)
+	signal.Notify(shutdownCh, os.Interrupt)
+	go func() {
+		<-shutdownCh
+		signal.Reset(os.Interrupt)
+		for n := len(shutdownFuncs) - 1; n >= 0; n-- {
+			shutdownFuncs[n]()
+		}
+	}()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	callOnShutdown(cancel)
+	cfg := &ssh.ClientConfig{
+		User:            *gerritAuthUsername,
+		Auth:            []ssh.AuthMethod{mustPrivateKey(*gerritAuthKeyPath)},
+		HostKeyCallback: mustFixedHostKey(*gerritSSHHostKey),
+		Timeout:         *gerritSSHTimeout,
+	}
+	cfg.SetDefaults()
+
+	gw, err := gerrit.New(ctx, "tcp", *gerritAddr, cfg)
+	if err != nil {
+		log.Exitf("gerrit.New(%q): %v", *gerritAddr, err)
+	}
+	callOnShutdown(func() {
+		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+		defer cancel()
+		gw.Close(ctx)
+	})
+
+	sendMsgChan := make(chan string, 5)
+	go func() {
+		for e := range gw.Events() {
+			var parsedMsg string
+			switch e := e.(type) {
+			case *gerritevents.PatchSetCreated:
+				if e.Change.Project != *notifyRepo || !notifyBranches[e.Change.Branch] || e.PatchSet.Number != 1 {
+					continue
+				}
+				user := username(e.PatchSet.Uploader)
+				parsedMsg = nopingAll(user, fmt.Sprintf("CL/%d proposed by %s - %s - %s", e.Change.Number, user, e.Change.Subject, patchSetURL(e.Change, e.PatchSet)))
+			case *gerritevents.ChangeMerged:
+				if e.Change.Project != *notifyRepo || !notifyBranches[e.Change.Branch] {
+					continue
+				}
+				owner := username(e.Change.Owner)
+				submitter := e.Submitter.Username
+				url := patchSetURL(e.Change, e.PatchSet)
+
+				if submitter != owner && submitter == "clbot" {
+					parsedMsg = nopingAll(owner, fmt.Sprintf("CL/%d by %s autosubmitted - %s - %s", e.Change.Number, owner, e.Change.Subject, url))
+				} else {
+					parsedMsg = nopingAll(owner, fmt.Sprintf("CL/%d applied by %s - %s - %s", e.Change.Number, owner, e.Change.Subject, url))
+				}
+			}
+			if parsedMsg != "" {
+				sendMsgChan <- parsedMsg
+			}
+		}
+	}()
+
+	ircCtx, ircCancel := context.WithCancel(ctx)
+	callOnShutdown(ircCancel)
+	go runIRC(ircCtx, irc.ClientConfig{
+		Nick: *ircNick,
+		User: *ircUser,
+		Name: *ircName,
+		Pass: *ircPassword,
+
+		SendLimit: *ircSendLimit,
+		SendBurst: *ircSendBurst,
+	}, sendMsgChan)
+
+	<-ctx.Done()
+}
diff --git a/fun/clbot/default.nix b/fun/clbot/default.nix
new file mode 100644
index 0000000000..e6b9c2fb9b
--- /dev/null
+++ b/fun/clbot/default.nix
@@ -0,0 +1,19 @@
+{ depot, ... }@args:
+
+let
+  clbot = depot.fun.clbot;
+  gopkgs = depot.third_party.gopkgs;
+in
+depot.nix.buildGo.program {
+  name = "clbot";
+  srcs = [
+    ./clbot.go
+  ];
+  deps = [
+    clbot.gerrit
+    gopkgs."github.com".davecgh.go-spew.spew.gopkg
+    gopkgs."github.com".golang.glog.gopkg
+    gopkgs."golang.org".x.crypto.ssh.gopkg
+    gopkgs."gopkg.in"."irc.v3".gopkg
+  ];
+}
diff --git a/fun/clbot/gerrit/default.nix b/fun/clbot/gerrit/default.nix
new file mode 100644
index 0000000000..3b6ce0a739
--- /dev/null
+++ b/fun/clbot/gerrit/default.nix
@@ -0,0 +1,18 @@
+{ depot, ... }:
+
+let
+  inherit (depot.fun) clbot;
+  inherit (depot.third_party) gopkgs;
+in
+depot.nix.buildGo.package {
+  name = "code.tvl.fyi/fun/clbot/gerrit";
+  srcs = [
+    ./watcher.go
+  ];
+  deps = [
+    clbot.gerrit.gerritevents
+    clbot.backoffutil
+    gopkgs."github.com".golang.glog.gopkg
+    gopkgs."golang.org".x.crypto.ssh.gopkg
+  ];
+}
diff --git a/fun/clbot/gerrit/gerritevents/default.nix b/fun/clbot/gerrit/gerritevents/default.nix
new file mode 100644
index 0000000000..024451858b
--- /dev/null
+++ b/fun/clbot/gerrit/gerritevents/default.nix
@@ -0,0 +1,10 @@
+{ depot, ... }:
+
+depot.nix.buildGo.package {
+  name = "code.tvl.fyi/fun/clbot/gerrit/gerritevents";
+  srcs = [
+    ./time.go
+    ./types.go
+    ./events.go
+  ];
+}
diff --git a/fun/clbot/gerrit/gerritevents/events.go b/fun/clbot/gerrit/gerritevents/events.go
new file mode 100644
index 0000000000..c02b30f76e
--- /dev/null
+++ b/fun/clbot/gerrit/gerritevents/events.go
@@ -0,0 +1,321 @@
+package gerritevents
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+var events = map[string]func() Event{}
+
+func registerEvent(e func() Event) {
+	t := e().EventType()
+	if _, ok := events[t]; ok {
+		panic(fmt.Sprintf("%s already registered", t))
+	}
+	events[t] = e
+}
+
+// These events are taken from https://cl.tvl.fyi/Documentation/cmd-stream-events.html.
+
+// Event is implemented by Gerrit event structs.
+type Event interface {
+	EventType() string
+}
+
+type simpleEvent struct {
+	Type string `json:"type"`
+}
+
+// Parse parses a Gerrit event from JSON.
+func Parse(bs []byte) (Event, error) {
+	var s simpleEvent
+	if err := json.Unmarshal(bs, &s); err != nil {
+		return nil, fmt.Errorf("unmarshalling %q as Gerrit Event: %v", string(bs), err)
+	}
+	ef, ok := events[s.Type]
+	if !ok {
+		return nil, fmt.Errorf("unknown event type %q", s.Type)
+	}
+	e := ef()
+	if err := json.Unmarshal(bs, e); err != nil {
+		return nil, fmt.Errorf("unmarshalling %q as Gerrit Event %q: %v", string(bs), e.EventType(), err)
+	}
+	return e, nil
+}
+
+// AssigneeChanged indicates that a change's assignee has been changed.
+type AssigneeChanged struct {
+	Type           string  `json:"type"`
+	Change         Change  `json:"change"`
+	Changer        Account `json:"changer"`
+	OldAssignee    Account `json:"oldAssignee"`
+	EventCreatedOn Time    `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (AssigneeChanged) EventType() string { return "assignee-changed" }
+
+func init() {
+	registerEvent(func() Event { return &AssigneeChanged{} })
+}
+
+// ChangeAbandoned indicates that a change has been abandoned.
+type ChangeAbandoned struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Abandoner      Account  `json:"abandoner"`
+	Reason         string   `json:"reason"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (ChangeAbandoned) EventType() string { return "change-abandoned" }
+
+func init() {
+	registerEvent(func() Event { return &ChangeAbandoned{} })
+}
+
+// ChangeDeleted indicates that a change has been deleted.
+type ChangeDeleted struct {
+	Type    string  `json:"type"`
+	Change  Change  `json:"change"`
+	Deleter Account `json:"deleter"`
+}
+
+// EventType implements Event.
+func (ChangeDeleted) EventType() string { return "change-deleted" }
+
+func init() {
+	registerEvent(func() Event { return &ChangeDeleted{} })
+}
+
+// ChangeMerged indicates that a change has been merged into the target branch.
+type ChangeMerged struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Submitter      Account  `json:"submitter"`
+	NewRev         string   `json:"newRev"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (ChangeMerged) EventType() string { return "change-merged" }
+
+func init() {
+	registerEvent(func() Event { return &ChangeMerged{} })
+}
+
+// ChangeRestored indicates a change has been restored (i.e. un-abandoned).
+type ChangeRestored struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Restorer       Account  `json:"restorer"`
+	Reason         string   `json:"reason"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (ChangeRestored) EventType() string { return "change-restored" }
+
+func init() {
+	registerEvent(func() Event { return &ChangeRestored{} })
+}
+
+// CommentAdded indicates someone has commented on a patchset.
+type CommentAdded struct {
+	Type           string     `json:"type"`
+	Change         Change     `json:"change"`
+	PatchSet       PatchSet   `json:"patchSet"`
+	Author         Account    `json:"author"`
+	Approvals      []Approval `json:"approvals"`
+	Comment        string     `json:"comment"`
+	EventCreatedOn Time       `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (CommentAdded) EventType() string { return "comment-added" }
+
+func init() {
+	registerEvent(func() Event { return &CommentAdded{} })
+}
+
+// DroppedOutput indicates that some events may be missing from the stream.
+type DroppedOutput struct {
+	Type string `json:"type"`
+}
+
+// EventType implements Event.
+func (DroppedOutput) EventType() string { return "dropped-output" }
+
+func init() {
+	registerEvent(func() Event { return &DroppedOutput{} })
+}
+
+// HashtagsChanged indicates that someone has added or removed hashtags from a change.
+type HashtagsChanged struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	Editor         Account  `json:"editor"`
+	Added          []string `json:"added"`
+	Removed        []string `json:"removed"`
+	Hashtags       []string `json:"hashtags"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (HashtagsChanged) EventType() string { return "hashtags-changed" }
+
+func init() {
+	registerEvent(func() Event { return &HashtagsChanged{} })
+}
+
+// ProjectCreated indicates that a new project has been created.
+type ProjectCreated struct {
+	Type           string `json:"type"`
+	ProjectName    string `json:"projectName"`
+	ProjectHead    string `json:"projectHead"`
+	EventCreatedOn Time   `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (ProjectCreated) EventType() string { return "project-created" }
+
+func init() {
+	registerEvent(func() Event { return &ProjectCreated{} })
+}
+
+// PatchSetCreated indicates that a new patchset has been added to a change.
+type PatchSetCreated struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Uploader       Account  `json:"uploader"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (PatchSetCreated) EventType() string { return "patchset-created" }
+
+func init() {
+	registerEvent(func() Event { return &PatchSetCreated{} })
+}
+
+// RefUpdated indicates that a ref has been updated.
+type RefUpdated struct {
+	Type           string    `json:"type"`
+	Submitter      Account   `json:"submitter"`
+	RefUpdate      RefUpdate `json:"refUpdate"`
+	EventCreatedOn Time      `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (RefUpdated) EventType() string { return "ref-updated" }
+
+func init() {
+	registerEvent(func() Event { return &RefUpdated{} })
+}
+
+// ReviewerAdded indicates that a reviewer has been added to a change.
+type ReviewerAdded struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Reviewer       Account  `json:"reviewer"`
+	Adder          Account  `json:"adder"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (ReviewerAdded) EventType() string { return "reviewer-added" }
+
+func init() {
+	registerEvent(func() Event { return &ReviewerAdded{} })
+}
+
+// ReviewerDeleted indicates that a reviewer has been removed from a change, possibly removing one or more approvals.
+type ReviewerDeleted struct {
+	Type           string     `json:"type"`
+	Change         Change     `json:"change"`
+	PatchSet       PatchSet   `json:"patchSet"`
+	Reviewer       Account    `json:"reviewer"`
+	Remover        Account    `json:"remover"`
+	Approvals      []Approval `json:"approvals"`
+	Comment        string     `json:"comment"`
+	EventCreatedOn Time       `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (ReviewerDeleted) EventType() string { return "reviewer-deleted" }
+
+func init() {
+	registerEvent(func() Event { return &ReviewerDeleted{} })
+}
+
+// TopicChanged indicates that the topic attached to a change has been changed.
+type TopicChanged struct {
+	Type           string  `json:"type"`
+	Change         Change  `json:"change"`
+	Changer        Account `json:"changer"`
+	OldTopic       string  `json:"oldTopic"`
+	EventCreatedOn Time    `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (TopicChanged) EventType() string { return "topic-changed" }
+
+func init() {
+	registerEvent(func() Event { return &TopicChanged{} })
+}
+
+// WIPStateChanged indicates that the work-in-progress state of a change has changed.
+type WIPStateChanged struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Changer        Account  `json:"changer"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (WIPStateChanged) EventType() string { return "wip-state-changed" }
+
+func init() {
+	registerEvent(func() Event { return &WIPStateChanged{} })
+}
+
+// PrivateStateChanged indicates that the private state of a change has changed.
+type PrivateStateChanged struct {
+	Type           string   `json:"type"`
+	Change         Change   `json:"change"`
+	PatchSet       PatchSet `json:"patchSet"`
+	Changer        Account  `json:"changer"`
+	EventCreatedOn Time     `json:"eventCreatedOn"`
+}
+
+// EventType implements Event.
+func (PrivateStateChanged) EventType() string { return "private-state-changed" }
+
+func init() {
+	registerEvent(func() Event { return &PrivateStateChanged{} })
+}
+
+// VoteDeleted indicates that an approval vote has been deleted from a change.
+type VoteDeleted struct {
+	Type      string     `json:"type"`
+	Change    Change     `json:"change"`
+	PatchSet  PatchSet   `json:"patchSet"`
+	Reviewer  Account    `json:"reviewer"`
+	Remover   Account    `json:"remover"`
+	Approvals []Approval `json:"approvals"`
+	Comment   string     `json:"comment"`
+}
+
+// EventType implements Event.
+func (VoteDeleted) EventType() string { return "vote-deleted" }
+
+func init() {
+	registerEvent(func() Event { return &VoteDeleted{} })
+}
diff --git a/fun/clbot/gerrit/gerritevents/time.go b/fun/clbot/gerrit/gerritevents/time.go
new file mode 100644
index 0000000000..7fbfaa3f5c
--- /dev/null
+++ b/fun/clbot/gerrit/gerritevents/time.go
@@ -0,0 +1,38 @@
+package gerritevents
+
+import (
+	"fmt"
+	"strconv"
+	"time"
+)
+
+// Time is a time.Time that is formatted as a Unix timestamp in JSON.
+type Time struct {
+	time.Time
+}
+
+// UnmarshalJSON unmarshals a Unix timestamp into a Time.
+func (t *Time) UnmarshalJSON(bs []byte) error {
+	if string(bs) == "null" {
+		return nil
+	}
+	u, err := strconv.ParseInt(string(bs), 10, 64)
+	if err != nil {
+		return err
+	}
+	t.Time = time.Unix(u, 0)
+	return nil
+}
+
+// MarshalJSON marshals a Time into a Unix timestamp.
+func (t *Time) MarshalJSON() ([]byte, error) {
+	if t.IsZero() {
+		return []byte("null"), nil
+	}
+	return []byte(fmt.Sprintf("%d", t.Unix())), nil
+}
+
+// IsSet returns true if the time.Time is non-zero.
+func (t *Time) IsSet() bool {
+	return !t.IsZero()
+}
diff --git a/fun/clbot/gerrit/gerritevents/types.go b/fun/clbot/gerrit/gerritevents/types.go
new file mode 100644
index 0000000000..75987a2da6
--- /dev/null
+++ b/fun/clbot/gerrit/gerritevents/types.go
@@ -0,0 +1,221 @@
+package gerritevents
+
+// These types are taken from https://cl.tvl.fyi/Documentation/json.html.
+
+// Account is a Gerrit account (or just a Git name+email pair).
+type Account struct {
+	Name     string `json:"name"`
+	Email    string `json:"email"`
+	Username string `json:"username"`
+}
+
+// ChangeStatus represents the states a change can be in.
+type ChangeStatus string
+
+const (
+	// ChangeStatusNew is the state a change is in during review.
+	ChangeStatusNew ChangeStatus = "NEW"
+
+	// ChangeStatusMerged indicates a change was merged to the target branch.
+	ChangeStatusMerged ChangeStatus = "MERGED"
+
+	// ChangeStatusAbandoned indicates a change was marked as abandoned.
+	ChangeStatusAbandoned ChangeStatus = "ABANDONED"
+)
+
+// Message is a message left by a reviewer.
+type Message struct {
+	Timestamp Time    `json:"timestamp"`
+	Reviewer  Account `json:"reviewer"`
+	Message   string  `json:"message"`
+}
+
+// TrackingID allows storing identifiers from external systems, i.e. bug trackers.
+type TrackingID struct {
+	System string `json:"system"`
+	ID     string `json:"id"`
+}
+
+// ChangeKind indicates the different changes that can be made to a change.
+type ChangeKind string
+
+const (
+	// ChangeKindRework indicates a non-trivial content change.
+	ChangeKindRework ChangeKind = "REWORK"
+
+	// ChangeKindTrivialRebase indicates a conflict-free merge between the new parent and the prior patch set.
+	ChangeKindTrivialRebase ChangeKind = "TRIVIAL_REBASE"
+
+	// ChangeKindMergeFirstParentUpdate indicates a conflict-free change of the first parent of a merge commit.
+	ChangeKindMergeFirstParentUpdate ChangeKind = "MERGE_FIRST_PARENT_UPDATE"
+
+	// ChangeKindNoCodeChange indicates no code change (the tree and parent trees are unchanged) - commit message probably changed.
+	ChangeKindNoCodeChange ChangeKind = "NO_CODE_CHANGE"
+
+	// ChangeKindNoChange indicates nothing changes: the commit message, tree, and parent tree are unchanged.
+	ChangeKindNoChange ChangeKind = "NO_CHANGE"
+)
+
+// Approval represents the current and past state of an approval label.
+type Approval struct {
+	Type        string   `json:"type"`
+	Description string   `json:"description"`
+	Value       string   `json:"value"`
+	OldValue    *string  `json:"oldValue"`
+	GrantedOn   *Time    `json:"grantedOn"`
+	By          *Account `json:"by"`
+}
+
+// PatchSetComment is a single comment left on a patchset.
+type PatchSetComment struct {
+	File     string  `json:"file"`
+	Line     int     `json:"line"`
+	Reviewer Account `json:"reviewer"`
+	Message  string  `json:"message"`
+}
+
+// FilePatchType represents the different modifications that can be made to a file by a patchset.
+type FilePatchType string
+
+const (
+	// FilePatchTypeAdded indicates the file did not exist, and this patchset adds it to the tree.
+	FilePatchTypeAdded FilePatchType = "ADDED"
+
+	// FilePatchTypeModified indicates the file exists before and after this patchset.
+	FilePatchTypeModified FilePatchType = "MODIFIED"
+
+	// FilePatchTypeDeleted indicates the file is removed by this patchset.
+	FilePatchTypeDeleted FilePatchType = "DELETED"
+
+	// FilePatchTypeRenamed indicates the file has a different name before this patchset than after.
+	FilePatchTypeRenamed FilePatchType = "RENAMED"
+
+	// FilePatchTypeCopied indicates the file was copied from a different file.
+	FilePatchTypeCopied FilePatchType = "COPIED"
+
+	// FilePatchTypeRewrite indicates the file had a significant quantity of content changed.
+	FilePatchTypeRewrite FilePatchType = "REWRITE"
+)
+
+// File represents a file in a patchset as well as how it is being modified.
+type File struct {
+	File    string        `json:"file"`
+	FileOld string        `json:"fileOld"`
+	Type    FilePatchType `json:"type"`
+}
+
+// PatchSet represents a single patchset within a change.
+type PatchSet struct {
+	Number         int               `json:"number"`
+	Revision       string            `json:"revision"`
+	Parents        []string          `json:"parents"`
+	Ref            string            `json:"ref"`
+	Uploader       Account           `json:"uploader"`
+	Author         Account           `json:"author"`
+	CreatedOn      Time              `json:"createdOn"`
+	Kind           ChangeKind        `json:"kind"`
+	Approvals      []Approval        `json:"approvals"`
+	Comments       []PatchSetComment `json:"comments"`
+	Files          []File            `json:"file"`
+	SizeInsertions int               `json:"sizeInsertions"`
+	SizeDeletions  int               `json:"sizeDeletions"`
+}
+
+// Dependency represents a change on which this change is dependent.
+type Dependency struct {
+	ID                string `json:"id"`
+	Number            int    `json:"number"`
+	Revision          string `json:"revision"`
+	Ref               string `json:"ref"`
+	IsCurrentPatchSet bool   `json:"isCurrentPatchSet"`
+}
+
+// SubmitStatus indicates whether this change has met the submit conditions and is ready to submit.
+type SubmitStatus string
+
+const (
+	// SubmitStatusOK indicates this change is ready to submit - all submit requirements are met.
+	SubmitStatusOK SubmitStatus = "OK"
+
+	// SubmitStatusNotReady indicates this change cannot yet be submitted.
+	SubmitStatusNotReady SubmitStatus = "NOT_READY"
+
+	// SubmitStatusRuleError indicates the submit rules could not be evaluted. Administrator intervention is required.
+	SubmitStatusRuleError SubmitStatus = "RULE_ERROR"
+)
+
+// LabelStatus indicates whether this label permits submission and if the label can be granted by anyone.
+type LabelStatus string
+
+const (
+	// LabelStatusOK indicates that this label provides what is necessary for submission (e.g. CR+2).
+	LabelStatusOK LabelStatus = "OK"
+
+	// LabelStatusReject indicates this label prevents submission (e.g. CR-2).
+	LabelStatusReject LabelStatus = "REJECT"
+
+	// LabelStatusNeed indicates this label is required for submission, but has not been satisfied (e.g. CR0).
+	LabelStatusNeed LabelStatus = "NEED"
+
+	// LabelStatusMay indicates this label is not required for submission. It may or may not be set.
+	LabelStatusMay LabelStatus = "MAY"
+
+	// LabelStatusImpossible indicates this label is required for submission, but cannot be satisfied. The ACLs on this label may be set incorrectly.
+	LabelStatusImpossible LabelStatus = "IMPOSSIBLE"
+)
+
+// Label represents the status of a particular label.
+type Label struct {
+	Label  string      `json:"label"`
+	Status LabelStatus `json:"status"`
+	By     Account     `json:"by"`
+}
+
+// Requirement represents a submit requirement.
+type Requirement struct {
+	FallbackText string `json:"fallbackText"`
+	Type         string `json:"type"`
+	// TODO(lukegb): data
+}
+
+// SubmitRecord represents the current submission state of a change.
+type SubmitRecord struct {
+	Status       SubmitStatus  `json:"status"`
+	Labels       []Label       `json:"labels"`
+	Requirements []Requirement `json:"requirements"`
+}
+
+// Change represents a Gerrit CL.
+type Change struct {
+	Project         string         `json:"project"`
+	Branch          string         `json:"branch"`
+	Topic           string         `json:"topic"`
+	ID              string         `json:"id"`
+	Number          int            `json:"number"`
+	Subject         string         `json:"subject"`
+	Owner           Account        `json:"owner"`
+	URL             string         `json:"url"`
+	CommitMessage   string         `json:"commitMessage"`
+	CreatedOn       Time           `json:"createdOn"`
+	LastUpdated     *Time          `json:"lastUpdated"`
+	Open            bool           `json:"open"`
+	Status          ChangeStatus   `json:"status"`
+	Private         bool           `json:"private"`
+	WIP             bool           `json:"wip"`
+	Comments        []Message      `json:"comments"`
+	TrackingIDs     []TrackingID   `json:"trackingIds"`
+	CurrentPatchSet *PatchSet      `json:"currentPatchSet"`
+	PatchSets       []PatchSet     `json:"patchSets"`
+	DependsOn       []Dependency   `json:"dependsOn"`
+	NeededBy        []Dependency   `json:"neededBy"`
+	SubmitRecords   []SubmitRecord `json:"submitRecord"`
+	AllReviewers    []Account      `json:"allReviewers"`
+}
+
+// RefUpdate represents a change in a ref.
+type RefUpdate struct {
+	OldRev  string `json:"oldRev"`
+	NewRev  string `json:"newRev"`
+	RefName string `json:"refName"`
+	Project string `json:"project"`
+}
diff --git a/fun/clbot/gerrit/watcher.go b/fun/clbot/gerrit/watcher.go
new file mode 100644
index 0000000000..d45876129b
--- /dev/null
+++ b/fun/clbot/gerrit/watcher.go
@@ -0,0 +1,252 @@
+// Package gerrit implements a watcher for Gerrit events.
+package gerrit
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"strings"
+	"time"
+
+	"code.tvl.fyi/fun/clbot/backoffutil"
+	"code.tvl.fyi/fun/clbot/gerrit/gerritevents"
+	log "github.com/golang/glog"
+	"golang.org/x/crypto/ssh"
+)
+
+// closer provides an embeddable implementation of Close which awaits a main loop acknowledging it has stopped.
+type closer struct {
+	stop    chan struct{}
+	stopped chan struct{}
+}
+
+// newCloser returns a closer with the channels initialised.
+func newCloser() closer {
+	return closer{
+		stop:    make(chan struct{}),
+		stopped: make(chan struct{}),
+	}
+}
+
+// Close stops the main loop, waiting for the main loop to stop until it stops or the context is cancelled, whichever happens first.
+func (c *closer) Close(ctx context.Context) error {
+	select {
+	case <-c.stopped:
+		return nil
+	case <-c.stop:
+		return nil
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+	}
+	close(c.stop)
+	select {
+	case <-c.stopped:
+		return nil
+	case <-ctx.Done():
+		return ctx.Err()
+	}
+}
+
+// lineWriter is an io.Writer which splits on \n and outputs each line (with no trailing newline) to its output channel.
+type lineWriter struct {
+	buf string
+	out chan string
+}
+
+// Write accepts a slice of bytes containing zero or more new lines.
+// If the contained channel is non-buffering or is full, this will block.
+func (w *lineWriter) Write(p []byte) (n int, err error) {
+	w.buf += string(p)
+	pieces := strings.Split(w.buf, "\n")
+	w.buf = pieces[len(pieces)-1]
+	for n := 0; n < len(pieces)-1; n++ {
+		w.out <- pieces[n]
+	}
+	return len(p), nil
+}
+
+// restartingClient is a simple SSH client that repeatedly connects to an SSH server, runs a command, and outputs the lines output by it on stdout onto a channel.
+type restartingClient struct {
+	closer
+
+	network string
+	addr    string
+	cfg     *ssh.ClientConfig
+
+	exec     string
+	output   chan string
+	shutdown func()
+}
+
+var (
+	errStopConnect = errors.New("gerrit: told to stop reconnecting by remote server")
+)
+
+func (c *restartingClient) runOnce() error {
+	netConn, err := net.Dial(c.network, c.addr)
+	if err != nil {
+		return fmt.Errorf("connecting to %v/%v: %w", c.network, c.addr, err)
+	}
+	defer netConn.Close()
+
+	sshConn, newCh, newReq, err := ssh.NewClientConn(netConn, c.addr, c.cfg)
+	if err != nil {
+		return fmt.Errorf("creating SSH connection to %v/%v: %w", c.network, c.addr, err)
+	}
+	defer sshConn.Close()
+
+	goAway := false
+	passedThroughReqs := make(chan *ssh.Request)
+	go func() {
+		defer close(passedThroughReqs)
+		for req := range newReq {
+			if req.Type == "goaway" {
+				goAway = true
+				log.Warningf("remote end %v/%v told me to go away!", c.network, c.addr)
+				sshConn.Close()
+				netConn.Close()
+			}
+			passedThroughReqs <- req
+		}
+	}()
+
+	cl := ssh.NewClient(sshConn, newCh, passedThroughReqs)
+
+	sess, err := cl.NewSession()
+	if err != nil {
+		return fmt.Errorf("NewSession on %v/%v: %w", c.network, c.addr, err)
+	}
+	defer sess.Close()
+
+	sess.Stdout = &lineWriter{out: c.output}
+
+	if err := sess.Start(c.exec); err != nil {
+		return fmt.Errorf("Start(%q) on %v/%v: %w", c.exec, c.network, c.addr, err)
+	}
+
+	log.Infof("connected to %v/%v", c.network, c.addr)
+
+	done := make(chan struct{})
+	go func() {
+		sess.Wait()
+		close(done)
+	}()
+	go func() {
+		select {
+		case <-c.stop:
+			sess.Close()
+		case <-done:
+		}
+		return
+	}()
+	<-done
+
+	if goAway {
+		return errStopConnect
+	}
+	return nil
+}
+
+func (c *restartingClient) run() {
+	defer close(c.stopped)
+	bo := backoffutil.NewDefaultBackOff()
+	for {
+		timer := time.NewTimer(bo.NextBackOff())
+		select {
+		case <-c.stop:
+			timer.Stop()
+			return
+		case <-timer.C:
+			break
+		}
+		if err := c.runOnce(); err == errStopConnect {
+			if c.shutdown != nil {
+				c.shutdown()
+				return
+			}
+		} else if err != nil {
+			log.Errorf("SSH: %v", err)
+		} else {
+			bo.Reset()
+		}
+	}
+}
+
+// Output returns the channel on which each newline-delimited string output by the executed command's stdout can be received.
+func (c *restartingClient) Output() <-chan string {
+	return c.output
+}
+
+// dialRestartingClient creates a new restartingClient.
+func dialRestartingClient(network, addr string, config *ssh.ClientConfig, exec string, shutdown func()) (*restartingClient, error) {
+	c := &restartingClient{
+		closer:   newCloser(),
+		network:  network,
+		addr:     addr,
+		cfg:      config,
+		exec:     exec,
+		output:   make(chan string),
+		shutdown: shutdown,
+	}
+	go c.run()
+	return c, nil
+}
+
+// Watcher watches
+type Watcher struct {
+	closer
+	c *restartingClient
+
+	output chan gerritevents.Event
+}
+
+// Close shuts down the SSH client connection, if any, and closes the output channel.
+// It blocks until shutdown is complete or until the context is cancelled, whichever comes first.
+func (w *Watcher) Close(ctx context.Context) {
+	w.c.Close(ctx)
+	w.closer.Close(ctx)
+}
+
+func (w *Watcher) run() {
+	defer close(w.stopped)
+	defer close(w.output)
+	for {
+		select {
+		case <-w.stop:
+			return
+		case o := <-w.c.Output():
+			ev, err := gerritevents.Parse([]byte(o))
+			if err != nil {
+				log.Errorf("failed to parse event %v: %v", o, err)
+				continue
+			}
+			w.output <- ev
+		}
+	}
+}
+
+// Events returns the channel upon which parsed Gerrit events can be received.
+func (w *Watcher) Events() <-chan gerritevents.Event {
+	return w.output
+}
+
+// New returns a running Watcher from which events can be read.
+// It will begin connecting to the provided address immediately.
+func New(ctx context.Context, network, addr string, cfg *ssh.ClientConfig) (*Watcher, error) {
+	wc := newCloser()
+	rc, err := dialRestartingClient(network, addr, cfg, "gerrit stream-events", func() {
+		wc.Close(context.Background())
+	})
+	if err != nil {
+		return nil, fmt.Errorf("dialRestartingClient: %w", err)
+	}
+	w := &Watcher{
+		closer: wc,
+		c:      rc,
+		output: make(chan gerritevents.Event),
+	}
+	go w.run()
+	return w, nil
+}
diff --git a/fun/clbot/gerrit/watcher_test.go b/fun/clbot/gerrit/watcher_test.go
new file mode 100644
index 0000000000..ae69b2fc4c
--- /dev/null
+++ b/fun/clbot/gerrit/watcher_test.go
@@ -0,0 +1,190 @@
+package gerrit
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/subtle"
+	"fmt"
+	"net"
+	"testing"
+	"time"
+
+	"code.tvl.fyi/fun/clbot/gerrit/gerritevents"
+	log "github.com/golang/glog"
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/crypto/ssh"
+)
+
+var (
+	sshServerSigner, sshServerPublicKey = mustNewKey()
+	sshClientSigner, sshClientPublicKey = mustNewKey()
+)
+
+func mustNewKey() (ssh.Signer, ssh.PublicKey) {
+	key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	if err != nil {
+		panic(err)
+	}
+	signer, err := ssh.NewSignerFromKey(key)
+	if err != nil {
+		panic(err)
+	}
+	publicKey, err := ssh.NewPublicKey(key.Public())
+	if err != nil {
+		panic(err)
+	}
+	return signer, publicKey
+}
+
+func newSSHServer(lines string) (addr string, cleanup func(), err error) {
+	config := &ssh.ServerConfig{
+		PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
+			pkBytes := pubKey.Marshal()
+			wantPKBytes := sshClientPublicKey.Marshal()
+			if subtle.ConstantTimeCompare(pkBytes, wantPKBytes) == 0 {
+				return nil, fmt.Errorf("unauthorized")
+			}
+			return &ssh.Permissions{}, nil
+		},
+	}
+	config.AddHostKey(sshServerSigner)
+
+	ln, err := net.Listen("tcp", ":0")
+	if err != nil {
+		log.Fatalf("Listen on tcp/:0: %v", err)
+	}
+	handle := func(conn net.Conn) {
+		defer conn.Close()
+
+		sc, newchch, newreqch, err := ssh.NewServerConn(conn, config)
+		if err != nil {
+			log.Fatalf("NewServerConn: %v", err)
+		}
+		go ssh.DiscardRequests(newreqch)
+		for newCh := range newchch {
+			if newCh.ChannelType() != "session" {
+				newCh.Reject(ssh.UnknownChannelType, "unknown channel type")
+				continue
+			}
+
+			channel, reqs, err := newCh.Accept()
+			if err != nil {
+				log.Fatalf("Could not accept channel: %v", err)
+			}
+			go func(in <-chan *ssh.Request) {
+				for req := range in {
+					req.Reply(req.Type == "exec", nil)
+				}
+			}(reqs)
+			channel.Write([]byte(lines))
+			sc.SendRequest("goaway", false, nil)
+		}
+	}
+	go func() {
+		for {
+			conn, err := ln.Accept()
+			if err != nil {
+				return
+			}
+			go handle(conn)
+		}
+	}()
+
+	cleanup = func() {
+		ln.Close()
+	}
+	return ln.Addr().String(), cleanup, err
+}
+
+func ts(s string) gerritevents.Time {
+	t, err := time.Parse("2006-01-02 15:04:05 -0700 MST", s)
+	if err != nil {
+		panic(err)
+	}
+	return gerritevents.Time{t}
+}
+
+func optStr(s string) *string { return &s }
+
+func TestWatcher(t *testing.T) {
+	tcs := []struct {
+		name  string
+		lines string
+		want  []gerritevents.Event
+	}{{
+		name: "no events",
+	}, {
+		name: "single test event",
+		lines: `{"author":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"0"}],"comment":"Patch Set 3: Code-Review+2","patchSet":{"number":3,"revision":"6fe272d3f82c6efdfe1167fab98bf918efc03fe5","parents":["d984b6018cf68c7e8b7169b475d90134fbcee767"],"ref":"refs/changes/44/244/3","uploader":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"createdOn":1592081910,"author":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"kind":"REWORK","sizeInsertions":83,"sizeDeletions":-156},"change":{"project":"depot","branch":"master","id":"I546c701145fa204b7ba7518a8a56a783588629e0","number":244,"subject":"refactor(ops/nixos): Move my NixOS configurations to //users/tazjin","owner":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"url":"https://cl.tvl.fyi/c/depot/+/244","commitMessage":"refactor(ops/nixos): Move my NixOS configurations to //users/tazjin\n\nNixOS modules move one level up because it\u0027s unlikely that //ops/nixos\nwill contain actual systems at this point (they\u0027re user-specific).\n\nThis is the first users folder, so it is also added to the root\nreadTree invocation for the repository.\n\nChange-Id: I546c701145fa204b7ba7518a8a56a783588629e0\n","createdOn":1592081577,"status":"NEW"},"project":"depot","refName":"refs/heads/master","changeKey":{"id":"I546c701145fa204b7ba7518a8a56a783588629e0"},"type":"comment-added","eventCreatedOn":1592081929}
+`,
+		want: []gerritevents.Event{
+			&gerritevents.CommentAdded{
+				Type: "comment-added",
+				Change: gerritevents.Change{
+					Project:       "depot",
+					Branch:        "master",
+					ID:            "I546c701145fa204b7ba7518a8a56a783588629e0",
+					Number:        244,
+					Subject:       "refactor(ops/nixos): Move my NixOS configurations to //users/tazjin",
+					Owner:         gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
+					URL:           "https://cl.tvl.fyi/c/depot/+/244",
+					CommitMessage: "refactor(ops/nixos): Move my NixOS configurations to //users/tazjin\n\nNixOS modules move one level up because it's unlikely that //ops/nixos\nwill contain actual systems at this point (they're user-specific).\n\nThis is the first users folder, so it is also added to the root\nreadTree invocation for the repository.\n\nChange-Id: I546c701145fa204b7ba7518a8a56a783588629e0\n",
+					CreatedOn:     ts("2020-06-13 21:52:57 +0100 BST"),
+					Status:        "NEW",
+				},
+				PatchSet: gerritevents.PatchSet{
+					Number:         3,
+					Revision:       "6fe272d3f82c6efdfe1167fab98bf918efc03fe5",
+					Parents:        []string{"d984b6018cf68c7e8b7169b475d90134fbcee767"},
+					Ref:            "refs/changes/44/244/3",
+					Uploader:       gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
+					Author:         gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
+					CreatedOn:      ts("2020-06-13 21:58:30 +0100 BST"),
+					Kind:           "REWORK",
+					SizeInsertions: 83,
+					SizeDeletions:  -156,
+				},
+				Author:         gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
+				Approvals:      []gerritevents.Approval{{Type: "Code-Review", Description: "Code-Review", Value: "2", OldValue: optStr("0")}},
+				Comment:        "Patch Set 3: Code-Review+2",
+				EventCreatedOn: ts("2020-06-13 21:58:49 +0100 BST"),
+			},
+		},
+	}}
+	for _, tc := range tcs {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			serverAddr, cleanup, err := newSSHServer(tc.lines)
+			if err != nil {
+				t.Fatalf("newSSHServer: %v", err)
+			}
+			t.Cleanup(cleanup)
+
+			config := &ssh.ClientConfig{
+				User:            "bert",
+				Auth:            []ssh.AuthMethod{ssh.PublicKeys(sshClientSigner)},
+				HostKeyCallback: ssh.FixedHostKey(sshServerPublicKey),
+				Timeout:         10 * time.Millisecond,
+			}
+			w, err := New(ctx, "tcp", serverAddr, config)
+			if err != nil {
+				t.Fatalf("New: %v", err)
+			}
+
+			var gotEvents []gerritevents.Event
+			for ev := range w.Events() {
+				gotEvents = append(gotEvents, ev)
+			}
+			if diff := cmp.Diff(gotEvents, tc.want); diff != "" {
+				t.Errorf("got events != want events: diff:\n%v", diff)
+			}
+		})
+	}
+}
diff --git a/fun/clbot/go.mod b/fun/clbot/go.mod
new file mode 100644
index 0000000000..f1e366ba9d
--- /dev/null
+++ b/fun/clbot/go.mod
@@ -0,0 +1,12 @@
+module code.tvl.fyi/fun/clbot
+
+go 1.14
+
+require (
+	github.com/cenkalti/backoff/v4 v4.0.2
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
+	github.com/google/go-cmp v0.4.1
+	golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
+	gopkg.in/irc.v3 v3.1.3
+)
diff --git a/fun/clbot/go.sum b/fun/clbot/go.sum
new file mode 100644
index 0000000000..d5434526e8
--- /dev/null
+++ b/fun/clbot/go.sum
@@ -0,0 +1,31 @@
+github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs=
+github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/irc.v3 v3.1.3 h1:yeTiJ365882L8h4AnBKYfesD92y5R5ZhGiylu9DfcPY=
+gopkg.in/irc.v3 v3.1.3/go.mod h1:shO2gz8+PVeS+4E6GAny88Z0YVVQSxQghdrMVGQsR9s=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/fun/defer_rs/.gitignore b/fun/defer_rs/.gitignore
new file mode 100644
index 0000000000..6aa106405a
--- /dev/null
+++ b/fun/defer_rs/.gitignore
@@ -0,0 +1,3 @@
+/target/
+**/*.rs.bk
+Cargo.lock
diff --git a/fun/defer_rs/Cargo.toml b/fun/defer_rs/Cargo.toml
new file mode 100644
index 0000000000..0fcd60373f
--- /dev/null
+++ b/fun/defer_rs/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "defer"
+version = "0.1.0"
+authors = ["Vincent Ambo <tazjin@gmail.com>"]
+
+[dependencies]
diff --git a/fun/defer_rs/README.md b/fun/defer_rs/README.md
new file mode 100644
index 0000000000..160158d177
--- /dev/null
+++ b/fun/defer_rs/README.md
@@ -0,0 +1,53 @@
+defer in Rust
+=============
+
+After a Hacker News discussion about implementing Go's `defer` keyword in C++,
+I stumbled upon [this comment](https://news.ycombinator.com/item?id=15523589)
+and more specifically this response to it by "Occivink":
+
+> There's plenty of one-time cases where you don't want to declare an entire
+> class but still enjoy scope-based functions.
+
+Specificall the "don't want to declare an entire class" suggests that languages
+like C++ have high friction for explaining your desired invariant (cleanup is
+run when `$thing` is destroyed) to the compiler.
+
+It seems like most languages either hand-wave this away (*cough* Java *cough*)
+or use what seems like a workaround (`defer`).
+
+Rust has the so-called `Drop` trait, which is a typeclass that contains a single
+method with no return value that is run when a variable is dropped (i.e. goes out
+of scope).
+
+This works fine for most general cases - i.e. closing file handlers - but can
+get complicated if other use-cases of `defer` are considered:
+
+* returning an error-value by mutating a reference in the enclosing scope (oh boy)
+* deferring a decision about when/whether to run cleanup to the caller
+
+While thinking about how to do this with the `Drop` trait I realised that `defer`
+can actually be trivially implemented in Rust, using `Drop`.
+
+A simple implementation of `defer` can be seen in [defer.rs](examples/defer.rs),
+an implementation using shared mutable state for error returns is in the file
+[defer-with-error.rs](examples/defer-with-error.rs) and an implementation that
+allows cleanup to be *cancelled* (don't _actually_ do this, it leaks a pointer)
+is in [undefer.rs](examples/undefer.rs).
+
+Whether any of this is actually useful is not up to me to decide. I haven't
+actually had a real-life need for this.
+
+You can run the examples with `cargo run --example defer`, etc.
+
+## Notes
+
+* `Drop` is not guaranteed to run in case of panics or program aborts, if you
+  need support for that check out [scopeguard](https://github.com/bluss/scopeguard)
+* `undefer` could be implemented safely by, for example, carrying a boolean that
+  by default causes execution to happen but can be flipped to disable it
+
+## Further reading:
+
+* [The Pain Of Real Linear Types in Rust](https://gankro.github.io/blah/linear-rust/)
+* [Go's defer](https://tour.golang.org/flowcontrol/12)
+* [Rust's Drop](https://doc.rust-lang.org/std/ops/trait.Drop.html)
diff --git a/fun/defer_rs/examples/defer-with-error.rs b/fun/defer_rs/examples/defer-with-error.rs
new file mode 100644
index 0000000000..f8b8a01413
--- /dev/null
+++ b/fun/defer_rs/examples/defer-with-error.rs
@@ -0,0 +1,72 @@
+// Go's defer in Rust, with error value return.
+
+use std::rc::Rc;
+use std::sync::RwLock;
+
+struct Defer<F: Fn()> {
+    f: F,
+}
+
+impl<F: Fn()> Drop for Defer<F> {
+    fn drop(&mut self) {
+        (self.f)()
+    }
+}
+
+// Only added this for Go-syntax familiarity ;-)
+fn defer<F: Fn()>(f: F) -> Defer<F> {
+    Defer { f }
+}
+
+// Convenience type synonym. This is a reference-counted smart pointer to
+// a shareable, mutable variable.
+// Rust does not allow willy-nilly mutation of shared variables, so explicit
+// write-locking must be performed.
+type ErrorHandle<T> = Rc<RwLock<Option<T>>>;
+
+///////////////////
+// Usage example //
+///////////////////
+
+#[derive(Debug)] // Debug trait for some default way to print the type.
+enum Error {
+    DropError,
+}
+
+fn main() {
+    // Create a place to store the error.
+    let drop_err: ErrorHandle<Error> = Default::default(); // create empty error
+
+    // Introduce an arbitrary scope block (so that we still have control after
+    // the defer runs):
+    {
+        let mut i = 1;
+
+        // Rc types are safe to clone and share for multiple ownership.
+        let err_handle = drop_err.clone();
+
+        // Call defer and let the closure own the cloned handle to the error:
+        let token = defer(move || {
+            // do something!
+            println!("Value is: {}", i);
+
+            // ... oh no, it went wrong!
+            *err_handle.write().unwrap() = Some(Error::DropError);
+        });
+
+        i += 1;
+        println!("Value is: {}", i);
+
+        // token goes out of scope here - drop() is called.
+    }
+
+    match *drop_err.read().unwrap() {
+        Some(ref err) => println!("Oh no, an error occured: {:?}!", err),
+        None => println!("Phew, everything went well."),
+    };
+}
+
+// Prints:
+// Value is: 2
+// Value is: 1
+// Oh no, an error occured: DropError!
diff --git a/fun/defer_rs/examples/defer.rs b/fun/defer_rs/examples/defer.rs
new file mode 100644
index 0000000000..0c99d00c82
--- /dev/null
+++ b/fun/defer_rs/examples/defer.rs
@@ -0,0 +1,31 @@
+// Go's defer in Rust!
+
+struct Defer<F: Fn()> {
+    f: F,
+}
+
+impl<F: Fn()> Drop for Defer<F> {
+    fn drop(&mut self) {
+        (self.f)()
+    }
+}
+
+// Only added this for Go-syntax familiarity ;-)
+fn defer<F: Fn()>(f: F) -> Defer<F> {
+    Defer { f }
+}
+
+fn main() {
+    let mut i = 1;
+
+    // Calling it "token" ... could be something else. The lifetime of this
+    // controls when the action is run.
+    let _token = defer(move || println!("Value is: {}", i));
+
+    i += 1;
+    println!("Value is: {}", i);
+}
+
+// Prints:
+// Value is: 2
+// Value is: 1
diff --git a/fun/defer_rs/examples/undefer.rs b/fun/defer_rs/examples/undefer.rs
new file mode 100644
index 0000000000..fa659de891
--- /dev/null
+++ b/fun/defer_rs/examples/undefer.rs
@@ -0,0 +1,40 @@
+// Go's defer in Rust, with a little twist!
+
+struct Defer<F: Fn()> {
+    f: F,
+}
+
+impl<F: Fn()> Drop for Defer<F> {
+    fn drop(&mut self) {
+        (self.f)()
+    }
+}
+
+// Only added this for Go-syntax familiarity ;-)
+fn defer<F: Fn()>(f: F) -> Defer<F> {
+    Defer { f }
+}
+
+// Changed your mind about the defer?
+// (Note: This leaks the closure! Don't actually do this!)
+fn undefer<F: Fn()>(token: Defer<F>) {
+    use std::mem;
+    mem::forget(token);
+}
+
+fn main() {
+    let mut i = 1;
+
+    // Calling it "token" ... could be something else. The lifetime of this
+    // controls when the action is run.
+    let token = defer(move || println!("Value is: {}", i));
+
+    i += 1;
+    println!("Value is: {}", i);
+
+    // Oh, now I changed my mind about the previous defer:
+    undefer(token);
+}
+
+// Prints:
+// Value is: 2
diff --git a/fun/elblog/.gitignore b/fun/elblog/.gitignore
new file mode 100644
index 0000000000..c531d9867f
--- /dev/null
+++ b/fun/elblog/.gitignore
@@ -0,0 +1 @@
+*.elc
diff --git a/fun/elblog/README.md b/fun/elblog/README.md
new file mode 100644
index 0000000000..994b1138ef
--- /dev/null
+++ b/fun/elblog/README.md
@@ -0,0 +1,11 @@
+elblog
+======
+
+This is a simple blogging software written in Emacs Lisp.
+
+The idea is that it should be able to do most of the things [my actual blog][]
+does at the moment.
+
+No documentation exists for now besides the commit messages, but it works!
+
+[my actual blog]: https://www.tazj.in/
diff --git a/fun/elblog/blog.css b/fun/elblog/blog.css
new file mode 100644
index 0000000000..0d021f78e8
--- /dev/null
+++ b/fun/elblog/blog.css
@@ -0,0 +1,37 @@
+<style type="text/css">
+body {
+    margin: 40px auto;
+    max-width: 800px;
+    line-height: 1.6;
+    font-size: 18px;
+    color: #383838;
+    padding: 0 10px
+}
+h1, h2, h3 {
+    line-height: 1.2
+}
+.footer {
+    text-align: right;
+}
+.lod {
+    text-align: center;
+}
+.unstyled-link {
+    color: inherit;
+    text-decoration: none;
+}
+.uncoloured-link {
+    color: inherit;
+}
+.date {
+    text-align: right;
+    font-style: italic;
+    float: right;
+}
+.inline {
+    display: inline;
+}
+.navigation {
+    text-align: center;
+}
+</style>
diff --git a/fun/elblog/blog.el b/fun/elblog/blog.el
new file mode 100644
index 0000000000..102aa37914
--- /dev/null
+++ b/fun/elblog/blog.el
@@ -0,0 +1,123 @@
+;;; blog.el --- A simple org-mode & elnode blog software.
+;;; -*- lexical-binding: t; -*-
+
+(require 'dash)
+(require 'elnode)
+(require 'f)
+(require 'ht)
+
+;; Definition of customization options
+
+(defgroup elblog nil
+  "Configuration for the Emacs Lisp blog software"
+  :link '(url-link "https://github.com/tazjin/elblog"))
+
+(defcustom elblog-port 8010
+  "Port to run elblog's HTTP server on"
+  :group 'elblog
+  :type 'integer)
+
+(defcustom elblog-host "localhost"
+  "Host for elblog's HTTP server to listen on"
+  :group 'elblog
+  :type 'string)
+
+(defcustom elblog-title "Elblog"
+  "Title text for this elblog instance"
+  :group 'elblog
+  :type 'string)
+
+(defcustom elblog-article-directory nil
+  "Directory in which elblog articles are stored"
+  :group 'elblog
+  :type 'string)
+
+(defcustom elblog-additional-routes '()
+  "Additional Elnode routes to register in the Elblog instance"
+  :group 'elblog
+  :type '(alist :key-type regexp :value-type function))
+
+;; Declare user-configurable variables needed at runtime.
+
+(defvar elblog-articles (ht-create)
+  "A hash-table of blog articles. This is used for looking up articles from
+   URL fragments as well as for rendering the index.")
+
+;; HTML templating setup
+
+(defun template-preamble ()
+  "Templates the preamble snippet with the correct blog title."
+  (format (f-read-text "preamble.html") elblog-title))
+
+(defun configure-org-html-export ()
+  "Configure org-mode settings for elblog's HTML templating to work correctly."
+  (setq org-html-postamble t)
+  (setq org-html-doctype "html5")
+  (setq org-html-head-include-scripts nil)
+  (setq org-html-style-default (f-read-text "blog.css"))
+  (setq org-html-preamble-format `(("en" ,(template-preamble))))
+  (setq org-html-postamble-format `(("en" ,(f-read-text "postamble.html")))))
+
+;; Article fetching & rendering functions
+
+(defun render-org-buffer (input-buffer &optional force)
+  "Renders an org-mode buffer as HTML and returns the name of the output buffer."
+  (letrec ((output-buffer (concat (buffer-name input-buffer) "-rendered"))
+           ;; Don't re-render articles unless forced.
+           (must-render (or force
+                            (not (get-buffer output-buffer)))))
+    (if (and input-buffer must-render)
+        (with-current-buffer input-buffer
+          (org-export-to-buffer 'html output-buffer nil nil t)))
+    (if input-buffer output-buffer nil)))
+
+(defun get-buffer-string (buffer)
+  "Returns the contents of the specified buffer as a string."
+  (with-current-buffer (get-buffer buffer)
+    (buffer-string)))
+
+(defvar-local article-not-found
+  '(404 . "<html><body><p>Oh no, the article was not found.</p></body></html>"))
+
+(defvar-local text-html '("Content-Type" . "text/html"))
+
+(defun render-article (article)
+  "Renders an article, if it exists."
+  (letrec ((rendered (-some->>
+                      (ht-get elblog-articles article)
+                      (concat elblog-article-directory)
+                      (find-file)
+                      (render-org-buffer))))
+    (if rendered `(200 . ,(get-buffer-string rendered))
+      article-not-found)))
+
+(defun blog-post-handler (httpcon)
+  "This handler servers a blog post from the configured blog post directory."
+  (let ((response (render-article (elnode-http-mapping httpcon 1))))
+    (elnode-http-start httpcon  (car response) text-html)
+    (elnode-http-return httpcon (cdr response))))
+
+;; Web server implementation
+
+(defvar elblog-routes
+  '(("^.*//\\(.*\\)" . blog-post-handler))
+  "The default routes available in elblog. They can be extended by the user
+by setting the elblog-additional-routes customize option.")
+
+(defun elblog-handler (httpcon)
+  (elnode-hostpath-dispatcher
+   httpcon
+   (-concat elblog-additional-routes elblog-routes)))
+
+(defun start-elblog ()
+  (interactive)
+  (configure-org-html-export)
+  (elnode-start 'elblog-handler
+              :port elblog-port
+              :host elblog-host))
+
+(defun stop-elblog ()
+  (interactive)
+  (elnode-stop elblog-port))
+
+(provide 'elblog)
diff --git a/fun/elblog/postamble.html b/fun/elblog/postamble.html
new file mode 100644
index 0000000000..16a26218a0
--- /dev/null
+++ b/fun/elblog/postamble.html
@@ -0,0 +1,9 @@
+<hr>
+<footer><p class="footer">Served with <a class="uncoloured-link" href="https://github.com/tazjin/elblog">Emacs</a>.</p>
+  <p class="footer">
+    <a class="uncoloured-link" href="https://twitter.com/tazjin">Twitter</a>
+    |
+    <a class="uncoloured-link" href="mailto:blog@tazj.in">Mail</a>
+  </p>
+  <p class="lod">ಠ_ಠ</p>
+</footer>
diff --git a/fun/elblog/preamble.html b/fun/elblog/preamble.html
new file mode 100644
index 0000000000..be74b9207e
--- /dev/null
+++ b/fun/elblog/preamble.html
@@ -0,0 +1,6 @@
+<header>
+  <h1>
+    <a class="unstyled-link" href="/">%s</a>
+  </h1>
+  <hr>
+</header>
diff --git a/fun/gemma/CODE_OF_CONDUCT.md b/fun/gemma/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..c4013ac13e
--- /dev/null
+++ b/fun/gemma/CODE_OF_CONDUCT.md
@@ -0,0 +1,20 @@
+A SERMON ON ETHICS AND LOVE
+===========================
+
+One day Mal-2 asked the messenger spirit Saint Gulik to approach the Goddess and request Her presence for some desperate advice. Shortly afterwards the radio came on by itself, and an ethereal female Voice said **YES?**
+
+"O! Eris! Blessed Mother of Man! Queen of Chaos! Daughter of Discord! Concubine of Confusion! O! Exquisite Lady, I beseech You to lift a heavy burden from my heart!"
+
+**WHAT BOTHERS YOU, MAL? YOU DON'T SOUND WELL.**
+
+"I am filled with fear and tormented with terrible visions of pain. Everywhere people are hurting one another, the planet is rampant with injustices, whole societies plunder groups of their own people, mothers imprison sons, children perish while brothers war. O, woe."
+
+**WHAT IS THE MATTER WITH THAT, IF IT IS WHAT YOU WANT TO DO?**
+
+"But nobody Wants it! Everybody hates it."
+
+**OH. WELL, THEN *STOP*.**
+
+At which moment She turned herself into an aspirin commercial and left The Polyfather stranded alone with his species.
+
+SINISTER DEXTER HAS A BROKEN SPIROMETER.
diff --git a/fun/gemma/LICENSE b/fun/gemma/LICENSE
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/fun/gemma/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/fun/gemma/README.md b/fun/gemma/README.md
new file mode 100644
index 0000000000..064742c009
--- /dev/null
+++ b/fun/gemma/README.md
@@ -0,0 +1,96 @@
+# Gemma
+
+Gemma is a simple application to track *recurring* tasks, named after Gemma
+Hartley who [wrote an article][] about task distribution issues in households.
+
+## Background
+
+(Skip this if you just want the technical bits)
+
+Gemma's article launched a discussion in my friend circle about what causes an
+uneven distribution of household workload. I theorised that this is not so much
+a gender issue, but mostly a discoverability issue.
+
+Usually one person in a household is aware of what needs to be done, but in many
+cases the "overhead" of delegating the tasks would actually take more time than
+simply doing the task.
+
+I theorise further that the person (or people) who do a smaller share of the
+household work would often do the work if they had a convenient way to become
+aware of what needs to be done. Many times the "household manager" has the
+function of tracking non-obvious tasks like when bedsheets were last changed -
+shouldn't it be possible to actually distribute this information somehow?
+
+## The Project
+
+This project is an initial attempt at sketching out a little application that
+aids with reminding users of recurring tasks. Some basic ideas:
+
+* The system should be blame-free.
+* There should be as little usage overhead as possible so that people actually
+  do use it.
+* It should work mostly passively without much user interaction.
+
+I believe that the basic (*very* simple) idea behind Gemma solves these issues.
+Unfortunately my living situation changed before I actually got to test this out
+in a real-life situation involving multiple people, but feedback from other
+potential test subjects would be welcome! :)
+
+## Overview
+
+Gemma is a Common Lisp application in which a list of recurring tasks is
+declared, together with the *maximum interval* at which they should be completed
+(in days). Example:
+
+```lisp
+;; Bathroom tasks
+(deftask bathroom/wipe-mirror 7)
+(deftask bathroom/wipe-counter 7)
+
+;; Bedroom tasks
+(deftask bedroom/change-sheets 7)
+(deftask bedroom/vacuum 10)
+
+;; Kitchen tasks
+(deftask kitchen/trash 3)
+(deftask kitchen/wipe-counters 3)
+(deftask kitchen/vacuum 5 "Kitchen has more crumbs and such!")
+
+;; Entire place
+(deftask clean-windows 60)
+```
+
+These tasks are marked with their last completion time and tracked by Gemma. A
+simple Elm-based frontend application displays the tasks sorted by their
+"urgency" and features a button to mark a task as completed:
+
+![Gemma screenshot](http://i.imgur.com/n7FFMJH.png)
+
+Marking a task as completed resets its counter and moves it to the bottom of the
+task list.
+
+In theory this *should be it*, the frontend is made available to household
+members in some easily accessible place (e.g. an old phone glued to the fridge!)
+and people should attempt to develop a habit of checking what needs to be done
+occasionally.
+
+The "household manager" still exists as a role of the household because someone
+is entering the tasks into the application, but if my theory about people not
+actually being actively *unwilling* to do tasks is correct this could help a
+lot.
+
+## Usage
+
+(*Note*: Gemma is alpha software so the below is clearly not the final goal)
+
+Right now using this is non-trivial, but I'll eventually make a better
+distribution. Basically you need to know Common Lisp (in which case you'll know
+how to get the backend running) and have `elm-reactor` installed to run the
+development version of the frontend application.
+
+Gemma is configured via a configuration file that should be located either at
+`/etc/gemma/config.lisp` or at a custom location specified via the environment
+variable `GEMMA_CONFIG`. Have a look at the `config.lisp` file in the repository
+root for an example.
+
+[wrote an article]: http://www.harpersbazaar.com/culture/features/a12063822/emotional-labor-gender-equality/
diff --git a/fun/gemma/config.lisp b/fun/gemma/config.lisp
new file mode 100644
index 0000000000..54f8e5f344
--- /dev/null
+++ b/fun/gemma/config.lisp
@@ -0,0 +1,21 @@
+;; Example configuration file for Gemma
+
+(config :port 4242
+        :data-dir "/tmp/gemma/")
+
+(deftask bathroom/wipe-mirror 7)
+(deftask bathroom/wipe-counter 7)
+
+;; Bedroom tasks
+(deftask bedroom/change-sheets 7)
+(deftask bedroom/vacuum 10)
+
+;; Kitchen tasks
+(deftask kitchen/normal-trash 3)
+(deftask kitchen/green-trash 5)
+(deftask kitchen/blue-trash 5)
+(deftask kitchen/wipe-counters 3)
+(deftask kitchen/vacuum 5 "Kitchen has more crumbs and such!")
+
+;; Entire place
+(deftask clean-windows 60)
diff --git a/fun/gemma/default.nix b/fun/gemma/default.nix
new file mode 100644
index 0000000000..4a26005852
--- /dev/null
+++ b/fun/gemma/default.nix
@@ -0,0 +1,57 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs) cacert iana-etc libredirect stdenv runCommandNoCC writeText;
+  elmPackages = depot.third_party.elmPackages_0_18;
+
+  frontend = stdenv.mkDerivation {
+    name = "gemma-frontend.html";
+    src = ./frontend;
+    buildInputs = [ cacert iana-etc elmPackages.elm ];
+
+    # The individual Elm packages this requires are not packaged and I
+    # can't be bothered to do that now, so lets open the escape hatch:
+    outputHashAlgo = "sha256";
+    outputHash = "000xhds5bsig3kbi7dhgbv9h7myacf34bqvw7avvz7m5mwnqlqg7";
+
+    phases = [ "unpackPhase" "buildPhase" ];
+    buildPhase = ''
+      export NIX_REDIRECTS=/etc/protocols=${iana-etc}/etc/protocols \
+        LD_PRELOAD=${libredirect}/lib/libredirect.so
+
+      export SYSTEM_CERTIFICATE_PATH=${cacert}/etc/ssl/certs
+
+      mkdir .home && export HOME="$PWD/.home"
+      elm-make --yes Main.elm --output $out
+    '';
+  };
+
+  injectFrontend = writeText "gemma-frontend.lisp" ''
+    (in-package :gemma)
+    (setq *static-file-location* "${runCommandNoCC "frontend" {} ''
+      mkdir -p $out
+      cp ${frontend} $out/index.html
+    ''}/")
+  '';
+in
+depot.nix.buildLisp.program {
+  name = "gemma";
+
+  deps = with depot.third_party.lisp; [
+    cl-json
+    cl-prevalence
+    hunchentoot
+    local-time
+  ];
+
+  srcs = [
+    ./src/gemma.lisp
+    injectFrontend
+  ];
+
+  # depends on SBCL
+  brokenOn = [
+    "ccl"
+    "ecl"
+  ];
+}
diff --git a/fun/gemma/frontend/Main.elm b/fun/gemma/frontend/Main.elm
new file mode 100644
index 0000000000..e449908e49
--- /dev/null
+++ b/fun/gemma/frontend/Main.elm
@@ -0,0 +1,221 @@
+-- Copyright (C) 2016-2017  Vincent Ambo <mail@tazj.in>
+--
+-- This file is part of Gemma.
+--
+-- Gemma is free software: you can redistribute it and/or modify it
+-- under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+
+
+module Main exposing (..)
+
+import Html exposing (Html, text, div, span)
+import Html.Attributes exposing (style)
+import Json.Decode exposing (..)
+import Http
+import Time
+
+
+--  Material design imports
+
+import Material
+import Material.Card as Card
+import Material.Color as Color
+import Material.Grid exposing (grid, cell, size, Device(..))
+import Material.Layout as Layout
+import Material.Scheme as Scheme
+import Material.Options as Options
+import Material.Elevation as Elevation
+import Material.Button as Button
+
+
+-- API interface to Gemma
+
+
+type alias Task =
+    { name : String
+    , description : Maybe String
+    , remaining : Int
+    }
+
+
+emptyStringFilter s =
+    if s == "" then
+        Nothing
+    else
+        Just s
+
+
+decodeEmptyString : Decoder (Maybe String)
+decodeEmptyString =
+    map emptyStringFilter string
+
+
+decodeTask : Decoder Task
+decodeTask =
+    map3 Task
+        (field "name" string)
+        (field "description" decodeEmptyString)
+        (field "remaining" int)
+
+
+loadTasks : Cmd Msg
+loadTasks =
+    let
+        request =
+            Http.get "/tasks" (list decodeTask)
+    in
+        Http.send NewTasks request
+
+
+completeTask : Task -> Cmd Msg
+completeTask task =
+    let
+        request =
+            Http.getString
+                (String.concat
+                    [ "/complete?task="
+                    , task.name
+                    ]
+                )
+    in
+        Http.send (\_ -> LoadTasks) request
+
+
+
+-- Elm architecture implementation
+
+
+type Msg
+    = None
+    | LoadTasks
+    | NewTasks (Result Http.Error (List Task))
+    | Mdl (Material.Msg Msg)
+    | Complete Task
+
+
+type alias Model =
+    { tasks : List Task
+    , error : Maybe String
+    , mdl : Material.Model
+    }
+
+
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        LoadTasks ->
+            ( model, loadTasks )
+
+        Complete task ->
+            ( model, completeTask task )
+
+        NewTasks (Ok tasks) ->
+            ( { model | tasks = tasks, error = Nothing }, Cmd.none )
+
+        NewTasks (Err err) ->
+            ( { model | error = Just (toString err) }, Cmd.none )
+
+        _ ->
+            ( model, Cmd.none )
+
+
+
+-- View implementation
+
+
+white =
+    Color.text Color.white
+
+
+taskColor : Task -> Color.Hue
+taskColor task =
+    if task.remaining > 2 then
+        Color.Green
+    else if task.remaining < 0 then
+        Color.Red
+    else
+        Color.Yellow
+
+
+within : Task -> String
+within task =
+    if task.remaining < 0 then
+        "This task is overdue!"
+    else if task.remaining > 2 then
+        String.concat
+            [ "Relax, this task has "
+            , toString task.remaining
+            , " days left before it is due."
+            ]
+    else
+        String.concat
+            [ "This task should be completed within "
+            , toString task.remaining
+            , " days. Consider doing it now!"
+            ]
+
+
+renderTask : Model -> Task -> Html Msg
+renderTask model task =
+    Card.view
+        [ Color.background (Color.color (taskColor task) Color.S800)
+        , Elevation.e3
+        ]
+        [ Card.title [] [ Card.head [ white ] [ text task.name ] ]
+        , Card.text [ white ]
+            [ text (Maybe.withDefault "" task.description)
+            , Html.br [] []
+            , text (within task)
+            ]
+        , Card.actions
+            [ Card.border ]
+            [ Button.render Mdl
+                [ 0 ]
+                model.mdl
+                [ white, Button.ripple, Button.accent, Options.onClick (Complete task) ]
+                [ text "Completed" ]
+            ]
+        ]
+
+
+gemmaView : Model -> Html Msg
+gemmaView model =
+    grid []
+        (List.map (\t -> cell [ size All 4 ] [ renderTask model t ])
+            model.tasks
+        )
+
+
+view : Model -> Html Msg
+view model =
+    gemmaView model |> Scheme.top
+
+
+
+-- subscriptions : Model -> Sub Msg
+
+
+subscriptions model =
+    Sub.batch
+        [ Material.subscriptions Mdl model
+        , Time.every (15 * Time.second) (\_ -> LoadTasks)
+        ]
+
+
+main : Program Never Model Msg
+main =
+    let
+        model =
+            { tasks = []
+            , error = Nothing
+            , mdl = Material.model
+            }
+    in
+        Html.program
+            { init = ( model, Cmd.batch [ loadTasks, Material.init Mdl ] )
+            , view = view
+            , update = update
+            , subscriptions = subscriptions
+            }
diff --git a/fun/gemma/frontend/elm-package.json b/fun/gemma/frontend/elm-package.json
new file mode 100644
index 0000000000..2ae541ae0b
--- /dev/null
+++ b/fun/gemma/frontend/elm-package.json
@@ -0,0 +1,17 @@
+{
+    "version": "1.0.0",
+    "summary": "helpful summary of your project, less than 80 characters",
+    "repository": "https://github.com/user/project.git",
+    "license": "BSD3",
+    "source-directories": [
+        "."
+    ],
+    "exposed-modules": [],
+    "dependencies": {
+        "elm-lang/core": "5.1.1 <= v < 6.0.0",
+        "elm-lang/html": "2.0.0 <= v < 3.0.0",
+        "elm-lang/http": "1.0.0 <= v < 2.0.0",
+        "debois/elm-mdl": "8.1.0 <= v < 9.0.0"
+    },
+    "elm-version": "0.18.0 <= v < 0.19.0"
+}
diff --git a/fun/gemma/src/gemma.lisp b/fun/gemma/src/gemma.lisp
new file mode 100644
index 0000000000..20a76caae6
--- /dev/null
+++ b/fun/gemma/src/gemma.lisp
@@ -0,0 +1,192 @@
+;; Copyright (C) 2016-2020  Vincent Ambo <mail@tazj.in>
+;;
+;; This file is part of Gemma.
+;;
+;; Gemma is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+(defpackage gemma
+  (:use :cl
+        :local-time
+        :cl-json)
+  (:import-from :sb-posix :getenv)
+  (:shadowing-import-from :sb-posix :getcwd)
+  (:export :start-gemma :config :main))
+(in-package :gemma)
+
+;; TODO: Store an average of how many days it was between task
+;; completions. Some of the current numbers are just guesses
+;; anyways.
+
+(defmacro in-case-of (x &body body)
+  "Evaluate BODY if X is non-nil, binding the value of X to IT."
+  `(let ((it ,x))
+     (when it ,@body)))
+
+;; Set default configuration parameters
+(defvar *gemma-port* 4242
+  "Port on which the Gemma web server listens.")
+
+(defvar *gemma-acceptor* nil
+  "Hunchentoot acceptor for Gemma's web server.")
+
+(defvar *static-file-location* "frontend/"
+  "Folder from which to serve static assets. If built inside of Nix,
+  the path is injected during the build.")
+
+(defvar *p-tasks* nil
+    "All tasks registered in this Gemma instance.")
+
+(defun initialise-persistence (data-dir)
+  (setq *p-tasks* (cl-prevalence:make-prevalence-system data-dir))
+
+  ;; Initialise database ID counter
+  (or (> (length (cl-prevalence:find-all-objects *p-tasks* 'task)) 0)
+      (cl-prevalence:tx-create-id-counter *p-tasks*)))
+
+(defun config (&key port data-dir)
+  "Configuration function for use in the Gemma configuration file."
+
+  (in-package :gemma)
+  (in-case-of port (defparameter *gemma-port* it))
+  (initialise-persistence (or data-dir "data/")))
+
+;;
+;; Define task management system
+;;
+
+(defclass task ()
+  ((id :reader id
+       :initarg :id)
+
+   ;; (Unique) name of the task
+   (name :type symbol
+         :initarg :name
+         :accessor name-of)
+
+   ;; Maximum completion interval
+   (days :type integer
+         :initarg :days
+         :accessor days-of)
+
+   ;; Optional description
+   (description :type string
+                :initarg :description
+                :accessor description-of)
+
+   ;; Last completion time
+   (done-at :type timestamp
+            :initarg :done-at
+            :accessor last-done-at)))
+
+(defmacro deftask (task-name days &optional description)
+  (unless (get-task task-name)
+    `(progn (cl-prevalence:tx-create-object
+             *p-tasks*
+             'task
+             (quote ((name ,task-name)
+                     (days ,days)
+                     (description ,(or description ""))
+                     (done-at ,(now)))))
+            (cl-prevalence:snapshot *p-tasks*))))
+
+(defun get-task (name)
+  (cl-prevalence:find-object-with-slot *p-tasks* 'task 'name name))
+
+(defun list-tasks ()
+  (cl-prevalence:find-all-objects *p-tasks* 'task))
+
+(defun days-remaining (task)
+  "Returns the number of days remaining before the supplied TASK reaches its
+maximum interval."
+  (let* ((expires-at (timestamp+ (last-done-at task)
+                                 (days-of task) :day))
+         (secs-until-expiry (timestamp-difference expires-at (now))))
+    (round (/ secs-until-expiry 60 60 24))))
+
+(defun sort-tasks (tasks)
+  "Sorts TASKS in descending order by number of days remaining."
+  (sort (copy-list tasks)
+        (lambda (t1 t2) (< (days-remaining t1)
+                           (days-remaining t2)))))
+
+(defun complete-task (name &optional at)
+  "Mark the task with NAME as completed, either now or AT specified time."
+  (cl-prevalence:tx-change-object-slots *p-tasks* 'task
+                                        (id (get-task name))
+                                        `((done-at ,(or at (now)))))
+  (cl-prevalence:snapshot *p-tasks*))
+
+;;
+;; Define web API
+;;
+
+(defun response-for (task)
+  "Create a response object to be JSON encoded for TASK."
+  `((:name . ,(name-of task))
+    (:description . ,(description-of task))
+    (:remaining . ,(days-remaining task))))
+
+(defun start-gemma ()
+  (in-package :gemma)
+
+  ;; Load configuration
+  (load (pathname (or (getenv "GEMMA_CONFIG")
+                      "/etc/gemma/config.lisp")))
+
+  ;; Set up web server
+  (setq *gemma-acceptor* (make-instance 'hunchentoot:easy-acceptor
+                                        :port *gemma-port*
+                                        :document-root *static-file-location*))
+  (hunchentoot:start *gemma-acceptor*)
+
+  ;; Task listing handler
+  (hunchentoot:define-easy-handler
+   (get-tasks :uri "/tasks") ()
+
+   (setf (hunchentoot:content-type*) "application/json")
+   (setf (hunchentoot:header-out "Access-Control-Allow-Origin") "*")
+   (encode-json-to-string
+    ;; Construct a frontend-friendly representation of the tasks.
+    (mapcar #'response-for (sort-tasks (list-tasks)))))
+
+  ;; Task completion handler
+  (hunchentoot:define-easy-handler
+   (complete-task-handler :uri "/complete") (task)
+   (setf (hunchentoot:content-type*) "application/json")
+   (let* ((key (find-symbol (camel-case-to-lisp task) "GEMMA")))
+     (format t "Marking task ~A as completed" key)
+     (complete-task key)
+     (encode-json-to-string (response-for (get-task key))))))
+
+(defun main ()
+  "This function serves as the entrypoint for ASDF-built executables.
+  It joins the Hunchentoot server thread to keep the process running
+  for as long as the server is alive."
+
+  (start-gemma)
+  (sb-thread:join-thread
+   (find-if (lambda (th)
+              (string= (sb-thread:thread-name th)
+                       (format nil "hunchentoot-listener-*:~A" *gemma-port*)))
+            (sb-thread:list-all-threads))))
+
+;; Experimentation / testing stuff
+
+(defun randomise-completion-times ()
+  "Set some random completion timestamps for all tasks"
+  (mapcar
+   (lambda (task)
+     (complete-task (name-of task)
+                    (timestamp- (now)
+                                (random 14)
+                                :day)))
+   (cl-prevalence:find-all-objects *p-tasks* 'task)))
+
+(defun clear-all-tasks ()
+  (mapcar (lambda (task) (cl-prevalence:tx-delete-object *p-tasks* 'task (id task)))
+          (cl-prevalence:find-all-objects *p-tasks* 'task)))
+
+;; (randomise-completion-times)
diff --git a/fun/idual/README.md b/fun/idual/README.md
new file mode 100644
index 0000000000..922047617f
--- /dev/null
+++ b/fun/idual/README.md
@@ -0,0 +1,34 @@
+# iDual light control
+
+This folder contains some tooling for controlling iDual LED lights
+(which use infrared controls) using a "Broadlink RM Pro" infrared
+controller.
+
+The supported colour codes of the iDual remote are stored in
+`codes.txt`.
+
+The point of this is to make it possible for me to automate my lights
+in the morning, so that I can actually get out of bed.
+
+## Capturing codes
+
+Capturing codes is relatively easy, assuming that the broadlink device
+is set up:
+
+```python
+import broadlink
+import base64
+
+devices = broadlink.discover(timeout=5)
+devices[0].auth()
+```
+
+For each code, the procedure is as follows:
+
+```python
+devices[0].find_rf_packet()
+# wait until this returns True
+
+devices[0].check_data()
+# this will return the code
+```
diff --git a/fun/idual/default.nix b/fun/idual/default.nix
new file mode 100644
index 0000000000..1acf287bfb
--- /dev/null
+++ b/fun/idual/default.nix
@@ -0,0 +1,25 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  inherit (pkgs) python3 python3Packages;
+
+  opts = {
+    pname = "idualctl";
+    version = "0.1";
+    src = ./.;
+
+    propagatedBuildInputs = [
+      depot.third_party.python.broadlink
+    ];
+  };
+  package = python3Packages.buildPythonPackage opts;
+  script = python3Packages.buildPythonApplication opts;
+in
+depot.nix.readTree.drvTargets {
+  inherit script;
+  python = python3.withPackages (_: [ package ]);
+  setAlarm = pkgs.writeShellScriptBin "set-alarm" ''
+    echo "setting an alarm for ''${1}"
+    ${pkgs.systemd}/bin/systemd-run --user --on-calendar="''${1} Europe/London" --unit=light-alarm.service
+  '';
+}
diff --git a/fun/idual/idual/__init__.py b/fun/idual/idual/__init__.py
new file mode 100644
index 0000000000..35c3bbae90
--- /dev/null
+++ b/fun/idual/idual/__init__.py
@@ -0,0 +1,65 @@
+import base64
+import broadlink
+import time
+import sys
+
+commands = {
+    # system commands
+    'on'       : 'JgBIAAABK5AVERQ2FBEUERQSFBEUERQSFBEUNhQ2FDUUNhQ2FDYUNRU1FBIUERQRFBIUERQRFBIUERQ2FDYUNRQ2FDYUNhQ1FQANBQ==',
+    'off'      : 'JgBIAAABLJAUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDUVNRQ2FDYUNhQRFDYUERQSFBEUERQSFBEUNhQRFDYUNhQ2FDUUNhQ2FAANBQ==',
+    'darker'   : 'JgBIAAABLI8VERQ2FBEUERURFBEUEhQRFBEUNhQ2FDYUNRU1FDYUNhQRFBIUERQRFDYUEhQRFBEVNRQ2FDYUNhQRFDYUNhQ1FQANBQ==',
+    'brighter' : 'JgBIAAABLI8VERQ2FBEUERQSFBEUEhQRFBEUNhQ2FDUVNRQ2FDYUNRU1FBIUERQ2FBEUEhQRFBEUEhQ1FTUUEhQ1FTUUNhQ2FAANBQ==',
+
+    # presets
+    'candle'   : 'JgBQAAABKZISExI4EhMSFBITEhQRFBITEhQROBI4EjgSOBE4EjgSOBI4ERQSExIUEjgRFBITEhQSExI4EjgROBIUEjcSOBI4EgAFJgABKUkSAA0FAAAAAAAAAAA=',
+    'bulb'     : 'JgBYAAABK5AUERQ2FBEVERQRFBEVERQRFBEVNRQ2FDYUNRU1FDYUNhQRFDYUNRURFBEUEhQRFBEUNhQRFREUNhQ1FDYUNhQ2FAAFIwABKkgVAAxOAAErRxUADQU=',
+    'sun'      : 'JgBQAAABLI8VERQ2FBEUERURFBEUEhQRFBEUNhQ2FDYUNRU1FTUUNhQSFDUUEhQ2FBEUERURFBEUNhQSFDUUEhQ2FDUVNRQ2FAAFJQABK0cVAA0FAAAAAAAAAAA=',
+    'cold'     : 'JgBQAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDYUNRQ2FDYUNhQ1FTUUEhQRFBEUEhQRFBIUERQRFDYUNhQ2FDYUNRQ2FAAFJAABK0cVAA0FAAAAAAAAAAA=',
+    'eve_dark' : 'JgBQAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDYUNRU1FDYUNhQRFDYUERQSFDUUEhQRFBIUNRQSFDUUNhQSFDUUNhQ2FAAFIwABLEYVAA0FAAAAAAAAAAA=',
+    'eve_fade' : 'JgBIAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDYUNRQ2FDYUNhQ1FDYUNhQRFDYUERQSFBEUEhQRFBEUNhQSEzYUNhQ2FAANBQ==',
+    'reading'  : 'JgBQAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDUVNRQ2FDYUNhQ1FDYUEhQ1FBIUERQRFBIUERQSFDUUEhQ1FTUUNhQ2FAAFJAABK0YVAA0FAAAAAAAAAAA=',
+    'yoga'     : 'JgBYAAABLI8VERQ1FREUERQSFBEUERURFBEUNhQ1FTUUNhQ2FDYUNRURFBEUNhQRFBIUERMSExMTNxM2ExMTNxM2EzcTNxM3EwAFJQABKkgTAAxRAAErRxUADQU=',
+    'morning'  : 'JgBQAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDUVNRQ2FDYUNRU1FDYUERURFDUVERQRFBIUERQRFTUUNhQRFDYUNhQ2FAAFIwABK0cVAA0FAAAAAAAAAAA=',
+    'colours'  : 'JgBQAAABLI8VERQ2FBEUERQSFBEUERURFBEUNhQ1FTUUNhQ2FDYUNRQSFBEUERQ2FDYUERQSFBEUNhQ1FTUUEhQRFDYUNRU1FAAFJAABKkcVAA0FAAAAAAAAAAA=',
+    'random'   : 'JgBQAAABK5AUEhQ1FREUERQSFBEUERQSExIUNhQ1EzcTNxM3EzYTNxMTEhMTNxM2ExMTEhMSExMTNxM2ExMTEhM3EzcTNhM3EwAFJQABK0cVAA0FAAAAAAAAAAA=',
+    'island'   : 'JgBQAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDUVNRQ2FDYUNRU1FBIUNRURFBEUEhQRFBEUEhQ1FREUNhQ1FDYUNhQ2FAAFIwABK0cVAA0FAAAAAAAAAAA=',
+    'forest'   : 'JgBQAAABK5AUEhQ1FREUERQSFBEUERQSFBEUNhQ1FTUUNhQ2FDUVNRQSFBEUNhQRFDYUERQSFBEUNhQ2FBEUNhQRFDYUNhQ1FQAFIwABK0cVAA0FAAAAAAAAAAA=',
+    'ocean'    : 'JgBQAAABK5AUEhQ1FREUERQSFBEUERQRFREUNhQ1FTUUNhQ2FDUVNRQ2FBEUEhQ1FTUUEhQRFBEUEhQ1FTUUEhQRFDYUNRU1FAAFJAABK0cVAA0FAAAAAAAAAAA=',
+    'fire'     : 'JgBQAAABK5AUERQ2FBEUEhQRFBEUEhQRFBEUNhQ2FDUVNRQ2FDYUNRU1FBIUNRU1FBIUERQRFBIUERQ2FBEUEhQ1FTUUNhQ2FAAFIwABLEYVAA0FAAAAAAAAAAA=',
+    'love'     : 'JgBQAAABL5AUEhQ1FREUERQSFBEUERQSFBEUNhQ1FDYUNhQ2FDUVNRQSFDUUNhQSFBEUERQSFBEUNhQRFBIUNRQ2FDYUNhQ1FAAFJAABK0cVAA0FAAAAAAAAAAA=',
+
+    # colour commands
+    'red'        : 'JgBIAAABK5AUERQ2FBEUEhQRFBEVERQRFBEUNhQ2FDYUNRU1FDYUNhQRFDYUNhQ1FREUERQSFBEUNhQRFBIUERQ2FDUVNRQ2FAANBQ==',
+    'yellow'     : 'JgBIAAABLI8UEhQ2FBEUERQSFBEUEhMSFBEUNhQ2FDUUNhQ2FDYUNRQ2FDYUNhQRFBIUERQRFBIUERQSExIUNhQ1FDYUNhQ2FAANBQ==',
+    'green'      : 'JgBIAAABK5AUEhQ1FREUERQSFBEUERQSFBEUNhQ1FDYUNhQ2FDUVNRQSFBEUERQ2FBIUERQRFBIUNRU1FDYUERQ2FDYUNhQ1FQANBQ==',
+    'blue'       : 'JgBIAAABK5AUERQ2FBEUEhQRFBITEhQRFBEUNhQ2FDYUNRQ2FDYUNhQ2ExIUNhQRFDYUERQSExIUERQ2FBEUNhQSEzYUNhQ2FAANBQ==',
+    'saturate'   : 'JgBIAAABK5AUERQ2FBEUEhQRFBIUERQRFBEUNhQ2FDYUNRU1FDYUNhQRFDYUERQ2FDYUERQSFBEUNhQRFDYUERQSFDUUNhQ2FAANBQ==',
+    'desaturate' : 'JgBIAAABLI8VERQ2FBEUERQSFBEUERURFBEUNhQ1FTUUNhQ2FDYUNRQ2FDYUNhQ1FREUERQSFBEUERQSFBEUERQ2FDYUNhQ1FQANBQ==',
+}
+
+def cmd(name):
+    return base64.b64decode(commands[name])
+
+class LightController:
+    def __init__(self):
+        self.devices = broadlink.discover(timeout=10, max_devices=2)
+        if self.devices == []:
+            raise Exception('no devices found')
+        for device in self.devices:
+            device.auth()
+
+    def send_cmd(self, name, iterations=5):
+        "Send a command, repeatedly for reliability"
+        packet = cmd(name)
+        for i in range(iterations):
+            for device in self.devices:
+                device.send_data(packet)
+
+    def wakey(self):
+        "Turn on the lights in the morning, try repeatedly for reasons."
+        print('Turning on the lights. Wakey, wakey!')
+        for i in range(5):
+            self.send_cmd('random')
+            time.sleep(0.3)
+            self.send_cmd('on')
+            time.sleep(0.3)
diff --git a/fun/idual/idualctl b/fun/idual/idualctl
new file mode 100644
index 0000000000..10a85eba0a
--- /dev/null
+++ b/fun/idual/idualctl
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+import idual
+import sys
+
+def help():
+    print('Available commands:')
+    for cmd in idual.commands:
+        print('- ' + cmd)
+    sys.exit(0)
+
+def handle(ctrl, cmd):
+    if cmd == 'help':
+        help()
+    elif cmd == 'wakey':
+        ctrl.wakey()
+        sys.exit(0)
+    elif cmd == 'on':
+        print('Turning on the lights')
+        ctrl.send_cmd(cmd)
+    elif cmd == 'off':
+        print('Turning off the lights')
+        ctrl.send_cmd(cmd)
+    elif cmd in idual.commands:
+        print('Sending ' + cmd + '-command')
+        ctrl.send_cmd(cmd)
+    else:
+        print('unknown command \'' + cmd + '\'')
+        sys.exit(1)
+
+if __name__ == "__main__":
+    if len(sys.argv) == 1:
+        help()
+
+    print('Initialising light controller')
+    ctrl = idual.LightController()
+
+    for cmd in sys.argv[1:]:
+        handle(ctrl, cmd)
diff --git a/fun/idual/setup.py b/fun/idual/setup.py
new file mode 100644
index 0000000000..f10c3b86f2
--- /dev/null
+++ b/fun/idual/setup.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from setuptools import setup
+
+setup(
+    name='idualctl',
+    version='0.1',
+    author='Vincent Ambo',
+    author_email='mail@tazj.in',
+    url='https://git.tazj.in/about/fun/idual',
+    packages=['idual'],
+    scripts = ['idualctl'],
+    install_requires=['broadlink>=0.13.2'],
+    include_package_data=True,
+)
diff --git a/fun/owothia/.envrc b/fun/owothia/.envrc
new file mode 100644
index 0000000000..051d09d292
--- /dev/null
+++ b/fun/owothia/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
diff --git a/fun/owothia/.gitignore b/fun/owothia/.gitignore
new file mode 100644
index 0000000000..8e850e7a0a
--- /dev/null
+++ b/fun/owothia/.gitignore
@@ -0,0 +1,30 @@
+dist
+dist-*
+build/
+cabal-dev
+*.o
+*.hi
+*.hie
+*.chi
+*.chs.h
+*.dyn_o
+*.dyn_hi
+.hpc
+.hsenv
+.cabal-sandbox/
+cabal.sandbox.config
+*.prof
+*.aux
+*.hp
+*.eventlog
+.stack-work/
+cabal.project.local
+cabal.project.local~
+.HTF/
+.ghc.environment.*
+
+# from nix-build
+result
+
+# grr
+*_flymake.hs
diff --git a/fun/owothia/chatter.patch b/fun/owothia/chatter.patch
new file mode 100644
index 0000000000..c2a6179bfb
--- /dev/null
+++ b/fun/owothia/chatter.patch
@@ -0,0 +1,19 @@
+diff --git a/src/NLP/POS/LiteralTagger.hs b/src/NLP/POS/LiteralTagger.hs
+index 913bee8..3c2f71d 100644
+--- a/src/NLP/POS/LiteralTagger.hs
++++ b/src/NLP/POS/LiteralTagger.hs
+@@ -1,4 +1,4 @@
+-{-# LANGUAGE OverloadedStrings #-}
++{-# LANGUAGE OverloadedStrings, PackageImports #-}
+ module NLP.POS.LiteralTagger
+     ( tag
+     , tagSentence
+@@ -27,7 +27,7 @@ import NLP.FullStop (segment)
+ import NLP.Types ( tagUNK, Sentence, TaggedSentence(..), applyTags
+                  , Tag, POSTagger(..), CaseSensitive(..), tokens, showTok)
+ import Text.Regex.TDFA
+-import Text.Regex.TDFA.Text (compile)
++import "regex-tdfa" Text.Regex.TDFA.Text (compile)
+ 
+ taggerID :: ByteString
+ taggerID = pack "NLP.POS.LiteralTagger"
diff --git a/fun/owothia/default.nix b/fun/owothia/default.nix
new file mode 100644
index 0000000000..04f98e97fb
--- /dev/null
+++ b/fun/owothia/default.nix
@@ -0,0 +1,13 @@
+{ depot ? (import ../../../. { })
+, pkgs ? depot.third_party.nixpkgs
+, ...
+}:
+
+let
+  basePkg = pkgs.haskellPackages.callPackage ./pkg.nix { };
+in
+
+pkgs.haskell.lib.overrideSrc basePkg {
+  src = depot.third_party.gitignoreSource ./.;
+  version = "canon";
+}
diff --git a/fun/owothia/hie.yaml b/fun/owothia/hie.yaml
new file mode 100644
index 0000000000..16a6c15262
--- /dev/null
+++ b/fun/owothia/hie.yaml
@@ -0,0 +1,4 @@
+cradle:
+  cabal:
+    - path: './app'
+      component: 'exe:owothia'
diff --git a/fun/owothia/owothia.cabal b/fun/owothia/owothia.cabal
new file mode 100644
index 0000000000..ef5477ea1b
--- /dev/null
+++ b/fun/owothia/owothia.cabal
@@ -0,0 +1,53 @@
+cabal-version:       2.2
+name:                owothia
+version:             0.0.1.0
+
+executable owothia
+  main-is:             Main.hs
+  build-depends:       base
+                     , relude
+                     , irc-client
+                     , lens
+                     , chatter
+                     , containers
+                     , text
+                     , bytestring
+                     , random
+                     , envy
+
+  mixins:              base hiding (Prelude)
+                     , relude (Relude as Prelude)
+
+  hs-source-dirs:
+    src
+
+  default-extensions:
+    BlockArguments
+    ConstraintKinds
+    DataKinds
+    DeriveAnyClass
+    DeriveGeneric
+    DerivingStrategies
+    DerivingVia
+    FlexibleContexts
+    FlexibleInstances
+    FunctionalDependencies
+    GADTSyntax
+    GeneralizedNewtypeDeriving
+    KindSignatures
+    LambdaCase
+    MultiWayIf
+    NoStarIsType
+    OverloadedStrings
+    PolyKinds
+    RankNTypes
+    ScopedTypeVariables
+    TupleSections
+    TypeApplications
+    TypeFamilies
+    TypeOperators
+    ViewPatterns
+
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -O2
+
+  default-language:    Haskell2010
diff --git a/fun/owothia/pkg.nix b/fun/owothia/pkg.nix
new file mode 100644
index 0000000000..c812e5e116
--- /dev/null
+++ b/fun/owothia/pkg.nix
@@ -0,0 +1,34 @@
+{ mkDerivation
+, base
+, bytestring
+, chatter
+, containers
+, envy
+, irc-client
+, lens
+, lib
+, random
+, relude
+, text
+}:
+mkDerivation {
+  pname = "owothia";
+  version = "0.0.1.0";
+  src = ./.;
+  isLibrary = false;
+  isExecutable = true;
+  executableHaskellDepends = [
+    base
+    bytestring
+    chatter
+    containers
+    envy
+    irc-client
+    lens
+    random
+    relude
+    text
+  ];
+  license = "unknown";
+  hydraPlatforms = lib.platforms.none;
+}
diff --git a/fun/owothia/regex-tdfa-text.patch b/fun/owothia/regex-tdfa-text.patch
new file mode 100644
index 0000000000..6b2c346543
--- /dev/null
+++ b/fun/owothia/regex-tdfa-text.patch
@@ -0,0 +1,40 @@
+diff --git a/Text/Regex/TDFA/Text.hs b/Text/Regex/TDFA/Text.hs
+index c4ef9db..9299272 100644
+--- a/Text/Regex/TDFA/Text.hs
++++ b/Text/Regex/TDFA/Text.hs
+@@ -38,13 +38,6 @@ import Text.Regex.TDFA.NewDFA.Uncons(Uncons(uncons))
+ import qualified Text.Regex.TDFA.NewDFA.Engine as Engine(execMatch)
+ import qualified Text.Regex.TDFA.NewDFA.Tester as Tester(matchTest)
+ 
+-instance Extract T.Text where
+-  before = T.take; after = T.drop; empty = T.empty
+-
+-instance Uncons T.Text where
+-  {- INLINE uncons #-}
+-  uncons = T.uncons
+-
+ instance RegexContext Regex T.Text T.Text where
+   match = polymatch
+   matchM = polymatchM
+diff --git a/Text/Regex/TDFA/Text/Lazy.hs b/Text/Regex/TDFA/Text/Lazy.hs
+index 73ca4a0..52958fb 100644
+--- a/Text/Regex/TDFA/Text/Lazy.hs
++++ b/Text/Regex/TDFA/Text/Lazy.hs
+@@ -38,17 +38,10 @@ import Text.Regex.TDFA.NewDFA.Uncons(Uncons(uncons))
+ import qualified Text.Regex.TDFA.NewDFA.Engine as Engine(execMatch)
+ import qualified Text.Regex.TDFA.NewDFA.Tester as Tester(matchTest)
+ 
+-instance Extract L.Text where
+-  before = L.take . toEnum; after = L.drop . toEnum; empty = L.empty
+-
+ instance RegexContext Regex L.Text L.Text where
+   match = polymatch
+   matchM = polymatchM
+ 
+-instance Uncons L.Text where
+-  {- INLINE uncons #-}
+-  uncons = L.uncons
+-
+ instance RegexMaker Regex CompOption ExecOption L.Text where
+   makeRegexOptsM c e source = makeRegexOptsM c e (L.unpack source)
+ 
diff --git a/fun/owothia/shell.nix b/fun/owothia/shell.nix
new file mode 100644
index 0000000000..0304581d9d
--- /dev/null
+++ b/fun/owothia/shell.nix
@@ -0,0 +1,22 @@
+{ pkgs ? (import ../../../. { }).third_party, ... }:
+
+let
+  inherit (pkgs)
+    haskellPackages
+    haskell
+    gitignoreSource
+    ;
+in
+
+(haskellPackages.extend (haskell.lib.packageSourceOverrides {
+  owothia = gitignoreSource ./.;
+})).shellFor {
+  packages = p: [ p.owothia ];
+  withHoogle = true;
+  doBenchmark = true;
+  buildInputs = with haskellPackages; [
+    cabal-install
+    hlint
+    haskell-language-server
+  ];
+}
diff --git a/fun/owothia/src/Main.hs b/fun/owothia/src/Main.hs
new file mode 100644
index 0000000000..3bf5e51dba
--- /dev/null
+++ b/fun/owothia/src/Main.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Main where
+
+import           Network.IRC.Client
+import           Control.Lens
+import           NLP.POS
+import           NLP.Types (POSTagger)
+import qualified NLP.Types.Tags as Tags
+import           NLP.Types.Tree
+import qualified NLP.Corpora.Conll as Conll
+import           NLP.Corpora.Conll (Tag)
+import qualified Data.ByteString as BS
+import           System.Random
+import           System.Envy
+import           System.IO as S
+import           Data.Maybe
+import           Data.Foldable (traverse_)
+import qualified Data.Text
+--------------------------------------------------------------------------------
+
+data Config = Config
+  { _owoChance :: Int
+  , _ircServer :: ByteString
+  , _ircPort :: Int
+  , _ircServerPassword :: Maybe Text
+  , _nickservPassword :: Maybe Text
+  , _ircNick :: Maybe Text
+  , _ircIdent :: Maybe Text
+  , _ircChannels :: [Text]
+  }
+  deriving stock (Show, Eq, Generic)
+makeLenses ''Config
+
+instance Var [Text] where
+  toVar ts = show ts
+  fromVar s = readMaybe s >>= (pure . map Data.Text.pack)
+
+instance FromEnv Config where
+  fromEnv _ =
+    Config <$> env "OWO_CHANCE"
+       <*> env "IRC_SERVER"
+       <*> env "IRC_PORT"
+       <*> envMaybe "IRC_SERVER_PASSWORD"
+       <*> envMaybe "NICKSERV_PASSWORD"
+       <*> envMaybe "IRC_NICK"
+       <*> envMaybe "IRC_IDENT"
+       <*> env "IRC_CHANNELS"
+
+stopWord :: Text -> Bool
+stopWord "'s"   = True
+stopWord "\""   = True
+stopWord "is"   = True
+stopWord "are"  = True
+stopWord "am"   = True
+stopWord "were" = True
+stopWord "was"  = True
+stopWord "be"   = True
+stopWord _      = False
+
+pickVerb :: POS Tag -> Maybe Text
+pickVerb (POS Conll.VB (Token verb)) = Just verb
+pickVerb (POS Conll.VBD (Token verb)) = Just verb
+pickVerb (POS Conll.VBG (Token verb)) = Just verb
+pickVerb (POS Conll.VBN (Token verb)) = Just verb
+pickVerb (POS Conll.VBZ (Token verb)) = Just verb
+pickVerb _ = Nothing
+
+pickNoun :: POS Tag -> Maybe Text
+pickNoun (POS Conll.NN (Token noun)) = Just noun
+pickNoun _ = Nothing
+
+randomPOS
+  :: Tags.Tag tag
+  => (POS tag -> Maybe Text)
+  -> POSTagger tag
+  -> Text
+  -> IO (Maybe Text)
+randomPOS pickPOS tagger s = do
+  let candidates
+        = filter (not . stopWord)
+        . mapMaybe pickPOS
+        $ tag tagger s >>= \(TaggedSent ps) -> ps
+  i <- randomRIO (0, length candidates - 1)
+  pure $ candidates ^? ix i
+
+doOwo :: MonadIO m => Config -> m Bool
+doOwo conf = do
+  n <- liftIO (randomRIO @Int (0, conf ^. owoChance))
+  pure $ n == 0
+
+data OwoType = Noun | Verb
+  deriving stock (Show, Eq)
+
+instance Random OwoType where
+  random = over _1 (bool Noun Verb) . random
+  randomR = const random
+
+vowels :: [Char]
+vowels = "aeiou"
+
+article :: Text -> Text
+article (x :< _) | x `elem` vowels = "an"
+article _ = "a"
+
+owo :: OwoType -> Text -> Text
+owo Noun n = mconcat
+  [ "I'm "
+  , article n
+  , " "
+  , n
+  , if "o" `Data.Text.isSuffixOf` n
+    then "wo"
+    else " owo"
+  ]
+owo Verb v = v <> " me owo"
+
+pickOwo :: OwoType -> POS Tag -> Maybe Text
+pickOwo Verb = pickVerb
+pickOwo Noun = pickNoun
+
+randomOwo :: OwoType -> POSTagger Tag -> Text -> IO (Maybe Text)
+randomOwo = randomPOS . pickOwo
+
+owothiaHandler :: Config -> Text -> IORef Bool -> POSTagger Tag -> EventHandler s
+owothiaHandler conf nick state tagger = EventHandler Just $ \src ev -> do
+  hasIdentified <- readIORef state
+  when (not hasIdentified) $ do
+    nickservAuth
+    traverse_ (send . Join) (conf ^. ircChannels)
+    writeIORef state True
+
+  when ("You are now identified" `BS.isInfixOf` (ev ^. raw)) $
+    traverse_ (send . Join) (conf ^. ircChannels)
+
+  case (src, ev ^. message) of
+    (Channel chan nick, Privmsg _ (Right m)) -> do
+      willOwo <- doOwo conf
+      when willOwo $ owoMessage chan m
+    _ -> pure()
+
+  pure ()
+
+  where
+    owoMessage chan m = do
+      owoType <- liftIO randomIO
+      mWord <- liftIO $ randomOwo owoType tagger m
+      for_ mWord $ \word -> send $ Privmsg chan $ Right $ owo owoType word
+    nickservAuthMsg = "IDENTIFY " <> nick <> " " <> fromJust (conf ^. nickservPassword)
+    nickservAuth = send $ Privmsg "NickServ" $ Right nickservAuthMsg
+
+main :: IO ()
+main = do
+  conf <- either fail pure =<< decodeEnv
+  tagger <- defaultTagger
+  state <- newIORef $ not . isJust $ (conf ^. nickservPassword)
+  S.hSetBuffering stdout LineBuffering
+  let nick = fromMaybe "owothia" (conf ^. ircNick)
+      conn =
+        plainConnection (conf ^. ircServer) (conf ^. ircPort)
+          & realname .~ "Owothia Revströwö"
+          & password .~ (conf ^. ircServerPassword)
+          & username .~ fromMaybe "owothia" (conf ^. ircIdent)
+          & logfunc .~ stdoutLogger
+      cfg =
+        defaultInstanceConfig nick
+          & channels .~ (conf ^. ircChannels)
+          & handlers %~ (owothiaHandler conf nick state tagger : )
+  runClient conn cfg ()
diff --git a/fun/paroxysm/.gitignore b/fun/paroxysm/.gitignore
new file mode 100644
index 0000000000..8cb01f2e3d
--- /dev/null
+++ b/fun/paroxysm/.gitignore
@@ -0,0 +1,5 @@
+/target
+irc.toml
+paroxysm-irc.toml
+paroxysm.toml
+**/*.rs.bk
diff --git a/fun/paroxysm/Cargo.lock b/fun/paroxysm/Cargo.lock
new file mode 100644
index 0000000000..70177ce9bb
--- /dev/null
+++ b/fun/paroxysm/Cargo.lock
@@ -0,0 +1,1639 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bufstream"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
+dependencies = [
+ "byteorder",
+ "iovec",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits 0.2.15",
+ "time",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "config"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9107d78ed62b3fa5a86e7d18e647abed48cfd8f8fab6c72f4cdb982d196f7e6"
+dependencies = [
+ "lazy_static 1.4.0",
+ "nom",
+ "rust-ini",
+ "serde 1.0.137",
+ "serde-hjson",
+ "serde_json",
+ "toml",
+ "yaml-rust",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "crimp"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe8f9a320ad9c1a2e3bacedaa281587bd297fb10a10179fd39f777049d04794"
+dependencies = [
+ "curl",
+ "serde 1.0.137",
+ "serde_json",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
+dependencies = [
+ "autocfg",
+ "cfg-if 0.1.10",
+ "crossbeam-utils",
+ "lazy_static 1.4.0",
+ "maybe-uninit",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
+dependencies = [
+ "cfg-if 0.1.10",
+ "crossbeam-utils",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg",
+ "cfg-if 0.1.10",
+ "lazy_static 1.4.0",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.55+curl-7.83.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "diesel"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "chrono",
+ "diesel_derives",
+ "pq-sys",
+ "r2d2",
+]
+
+[[package]]
+name = "diesel_derives"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "encoding"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
+dependencies = [
+ "encoding-index-japanese",
+ "encoding-index-korean",
+ "encoding-index-simpchinese",
+ "encoding-index-singlebyte",
+ "encoding-index-tradchinese",
+]
+
+[[package]]
+name = "encoding-index-japanese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-korean"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-simpchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-singlebyte"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-tradchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding_index_tests"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
+
+[[package]]
+name = "env_logger"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "failure"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
+dependencies = [
+ "backtrace",
+ "failure_derive",
+]
+
+[[package]]
+name = "failure_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
+[[package]]
+name = "futures"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gimli"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+dependencies = [
+ "quick-error",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "irc"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eb7666c9ae95dc77b874467e347bde3789773b6f48887fb3384bfe29552b466"
+dependencies = [
+ "bufstream",
+ "bytes",
+ "chrono",
+ "encoding",
+ "failure",
+ "futures",
+ "log",
+ "native-tls",
+ "serde 1.0.137",
+ "serde_derive",
+ "tokio-codec",
+ "tokio-core",
+ "tokio-io",
+ "tokio-mockstream",
+ "tokio-timer 0.1.2",
+ "tokio-tls",
+ "toml",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "lazy_static"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e7e15d7610cce1d9752e137625f14e61a28cd45929b6e12e47b50fe154ee2e"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
+dependencies = [
+ "serde 0.8.23",
+ "serde_test",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
+[[package]]
+name = "lock_api"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab 0.4.6",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static 1.4.0",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "nom"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+dependencies = [
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits 0.2.15",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
+dependencies = [
+ "num-traits 0.2.15",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.28.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "openssl"
+version = "0.10.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
+dependencies = [
+ "lock_api 0.3.4",
+ "parking_lot_core 0.6.2",
+ "rustc_version",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api 0.4.7",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
+dependencies = [
+ "cfg-if 0.1.10",
+ "cloudabi",
+ "libc",
+ "redox_syscall 0.1.57",
+ "rustc_version",
+ "smallvec 0.6.14",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.13",
+ "smallvec 1.8.0",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "paroxysm"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "config",
+ "crimp",
+ "diesel",
+ "env_logger",
+ "failure",
+ "irc",
+ "lazy_static 1.4.0",
+ "log",
+ "rand",
+ "regex",
+ "serde 1.0.137",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "pq-sys"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
+dependencies = [
+ "vcpkg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r2d2"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
+dependencies = [
+ "log",
+ "parking_lot 0.11.2",
+ "scheduled-thread-pool",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom",
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static 1.4.0",
+ "windows-sys",
+]
+
+[[package]]
+name = "scheduled-thread-pool"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
+dependencies = [
+ "parking_lot 0.11.2",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-hjson"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b833c5ad67d52ced5f5938b2980f32a9c1c5ef047f0b4fb3127e7a423c76153"
+dependencies = [
+ "lazy_static 0.2.11",
+ "linked-hash-map 0.3.0",
+ "num-traits 0.1.43",
+ "regex",
+ "serde 0.8.23",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde 1.0.137",
+]
+
+[[package]]
+name = "serde_test"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5"
+dependencies = [
+ "serde 0.8.23",
+]
+
+[[package]]
+name = "slab"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
+
+[[package]]
+name = "slab"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
+
+[[package]]
+name = "smallvec"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
+dependencies = [
+ "maybe-uninit",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if 1.0.0",
+ "fastrand",
+ "libc",
+ "redox_syscall 0.2.13",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tokio"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
+dependencies = [
+ "bytes",
+ "futures",
+ "mio",
+ "num_cpus",
+ "tokio-codec",
+ "tokio-current-thread",
+ "tokio-executor",
+ "tokio-fs",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-sync",
+ "tokio-tcp",
+ "tokio-threadpool",
+ "tokio-timer 0.2.13",
+ "tokio-udp",
+ "tokio-uds",
+]
+
+[[package]]
+name = "tokio-codec"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b"
+dependencies = [
+ "bytes",
+ "futures",
+ "tokio-io",
+]
+
+[[package]]
+name = "tokio-core"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87b1395334443abca552f63d4f61d0486f12377c2ba8b368e523f89e828cffd4"
+dependencies = [
+ "bytes",
+ "futures",
+ "iovec",
+ "log",
+ "mio",
+ "scoped-tls",
+ "tokio",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-timer 0.2.13",
+]
+
+[[package]]
+name = "tokio-current-thread"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e"
+dependencies = [
+ "futures",
+ "tokio-executor",
+]
+
+[[package]]
+name = "tokio-executor"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671"
+dependencies = [
+ "crossbeam-utils",
+ "futures",
+]
+
+[[package]]
+name = "tokio-fs"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4"
+dependencies = [
+ "futures",
+ "tokio-io",
+ "tokio-threadpool",
+]
+
+[[package]]
+name = "tokio-io"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674"
+dependencies = [
+ "bytes",
+ "futures",
+ "log",
+]
+
+[[package]]
+name = "tokio-mockstream"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41bfc436ef8b7f60c19adf3df086330ae9992385e4d8c53b17a323cad288e155"
+dependencies = [
+ "futures",
+ "tokio-io",
+]
+
+[[package]]
+name = "tokio-reactor"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351"
+dependencies = [
+ "crossbeam-utils",
+ "futures",
+ "lazy_static 1.4.0",
+ "log",
+ "mio",
+ "num_cpus",
+ "parking_lot 0.9.0",
+ "slab 0.4.6",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-sync",
+]
+
+[[package]]
+name = "tokio-sync"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee"
+dependencies = [
+ "fnv",
+ "futures",
+]
+
+[[package]]
+name = "tokio-tcp"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72"
+dependencies = [
+ "bytes",
+ "futures",
+ "iovec",
+ "mio",
+ "tokio-io",
+ "tokio-reactor",
+]
+
+[[package]]
+name = "tokio-threadpool"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "futures",
+ "lazy_static 1.4.0",
+ "log",
+ "num_cpus",
+ "slab 0.4.6",
+ "tokio-executor",
+]
+
+[[package]]
+name = "tokio-timer"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6131e780037787ff1b3f8aad9da83bca02438b72277850dd6ad0d455e0e20efc"
+dependencies = [
+ "futures",
+ "slab 0.3.0",
+]
+
+[[package]]
+name = "tokio-timer"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296"
+dependencies = [
+ "crossbeam-utils",
+ "futures",
+ "slab 0.4.6",
+ "tokio-executor",
+]
+
+[[package]]
+name = "tokio-tls"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "354b8cd83825b3c20217a9dc174d6a0c67441a2fae5c41bcb1ea6679f6ae0f7c"
+dependencies = [
+ "futures",
+ "native-tls",
+ "tokio-io",
+]
+
+[[package]]
+name = "tokio-udp"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82"
+dependencies = [
+ "bytes",
+ "futures",
+ "log",
+ "mio",
+ "tokio-codec",
+ "tokio-io",
+ "tokio-reactor",
+]
+
+[[package]]
+name = "tokio-uds"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0"
+dependencies = [
+ "bytes",
+ "futures",
+ "iovec",
+ "libc",
+ "log",
+ "mio",
+ "mio-uds",
+ "tokio-codec",
+ "tokio-io",
+ "tokio-reactor",
+]
+
+[[package]]
+name = "toml"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f"
+dependencies = [
+ "serde 1.0.137",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map 0.5.4",
+]
diff --git a/fun/paroxysm/Cargo.toml b/fun/paroxysm/Cargo.toml
new file mode 100644
index 0000000000..4d282285fd
--- /dev/null
+++ b/fun/paroxysm/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+authors = ["eeeeeta <eta@theta.eu.org>"]
+edition = "2018"
+name = "paroxysm"
+version = "0.1.0"
+
+[dependencies]
+chrono = "0.4"
+config = "0.9"
+crimp = "0.2"
+env_logger = "0.7"
+failure = "0.1"
+irc = "0.13"
+lazy_static = "1.4"
+log = "0.4"
+rand = "0.7"
+regex = "1.3"
+serde = { version = "1.0", features = [ "derive" ] }
+
+[dependencies.diesel]
+features = [ "postgres", "chrono", "r2d2" ]
+version = "1.4"
diff --git a/fun/paroxysm/OWNERS b/fun/paroxysm/OWNERS
new file mode 100644
index 0000000000..7f8beb1aa7
--- /dev/null
+++ b/fun/paroxysm/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - eta
diff --git a/fun/paroxysm/README.md b/fun/paroxysm/README.md
new file mode 100644
index 0000000000..a595530982
--- /dev/null
+++ b/fun/paroxysm/README.md
@@ -0,0 +1,19 @@
+paroxysm
+========
+
+`paroxysm` is a bot for [internet relay chat
+(IRC)](https://en.wikipedia.org/wiki/Internet_Relay_Chat) that lets you store
+small pieces of information, called *factoids*, and retrieve them later. It's
+useful for organising frequently-used information to avoid repeating oneself in
+a busy chatroom, as well as making little todo lists or notes to self in a
+private chatroom.
+
+It was directly inspired by the
+[LearnDB](https://github.com/crawl/sequell/blob/master/docs/learndb.md)
+functionality offered in `##crawl` on chat.freenode.net, and uses similar
+syntax.
+
+## Usage instructions
+
+Will come soon; the project is very much still in beta, and is subject to
+change.
diff --git a/fun/paroxysm/default.nix b/fun/paroxysm/default.nix
new file mode 100644
index 0000000000..e4ce4df1ae
--- /dev/null
+++ b/fun/paroxysm/default.nix
@@ -0,0 +1,14 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  name = "paroxysm";
+  version = "0.0.2";
+  src = ./.;
+
+  buildInputs = with pkgs; [
+    openssl
+    pkgconfig
+    postgresql.lib
+    curl
+  ];
+}
diff --git a/fun/paroxysm/docker/default.nix b/fun/paroxysm/docker/default.nix
new file mode 100644
index 0000000000..cb5b2758ec
--- /dev/null
+++ b/fun/paroxysm/docker/default.nix
@@ -0,0 +1,7 @@
+{ depot, pkgs, ... }:
+
+pkgs.dockerTools.buildLayeredImage {
+  name = "paroxysm";
+  contents = [ depot.fun.paroxysm ];
+  config.Entrypoint = [ "${depot.fun.paroxysm}/bin/paroxysm" ];
+}
diff --git a/fun/paroxysm/migrations/20181209140247_initial/down.sql b/fun/paroxysm/migrations/20181209140247_initial/down.sql
new file mode 100644
index 0000000000..aa02f4f63f
--- /dev/null
+++ b/fun/paroxysm/migrations/20181209140247_initial/down.sql
@@ -0,0 +1,2 @@
+DROP TABLE entries;
+DROP TABLE keywords;
diff --git a/fun/paroxysm/migrations/20181209140247_initial/up.sql b/fun/paroxysm/migrations/20181209140247_initial/up.sql
new file mode 100644
index 0000000000..e8b52d5a9b
--- /dev/null
+++ b/fun/paroxysm/migrations/20181209140247_initial/up.sql
@@ -0,0 +1,15 @@
+CREATE TABLE keywords (
+	id SERIAL PRIMARY KEY,
+	name VARCHAR UNIQUE NOT NULL,
+	chan VARCHAR NOT NULL,
+	UNIQUE(name, chan)
+);
+
+CREATE TABLE entries (
+	id SERIAL PRIMARY KEY,
+	keyword_id INT NOT NULL REFERENCES keywords ON DELETE CASCADE,
+	idx INT NOT NULL,
+	text VARCHAR NOT NULL,
+	creation_ts TIMESTAMP NOT NULL,
+	created_by VARCHAR NOT NULL
+);
diff --git a/fun/paroxysm/migrations/20181218142013_fix_unique/down.sql b/fun/paroxysm/migrations/20181218142013_fix_unique/down.sql
new file mode 100644
index 0000000000..291a97c5ce
--- /dev/null
+++ b/fun/paroxysm/migrations/20181218142013_fix_unique/down.sql
@@ -0,0 +1 @@
+-- This file should undo anything in `up.sql`
\ No newline at end of file
diff --git a/fun/paroxysm/migrations/20181218142013_fix_unique/up.sql b/fun/paroxysm/migrations/20181218142013_fix_unique/up.sql
new file mode 100644
index 0000000000..4885ae5ede
--- /dev/null
+++ b/fun/paroxysm/migrations/20181218142013_fix_unique/up.sql
@@ -0,0 +1 @@
+ALTER TABLE keywords DROP CONSTRAINT IF EXISTS keywords_name_key;
diff --git a/fun/paroxysm/src/cfg.rs b/fun/paroxysm/src/cfg.rs
new file mode 100644
index 0000000000..cfb2e2073e
--- /dev/null
+++ b/fun/paroxysm/src/cfg.rs
@@ -0,0 +1,12 @@
+use serde::Deserialize;
+use std::collections::HashSet;
+
+#[derive(Deserialize)]
+pub struct Config {
+    pub database_url: String,
+    pub irc_config_path: String,
+    #[serde(default)]
+    pub admins: HashSet<String>,
+    #[serde(default)]
+    pub log_filter: Option<String>,
+}
diff --git a/fun/paroxysm/src/keyword.rs b/fun/paroxysm/src/keyword.rs
new file mode 100644
index 0000000000..fa40f5347a
--- /dev/null
+++ b/fun/paroxysm/src/keyword.rs
@@ -0,0 +1,219 @@
+use crate::models::{Entry, Keyword, NewEntry, NewKeyword};
+use diesel::pg::PgConnection;
+use diesel::prelude::*;
+use failure::{format_err, Error};
+use std::borrow::Cow;
+
+/// Maximum number of times we'll follow a `see: ` pointer.
+const RECURSION_LIMIT: usize = 5;
+
+pub struct KeywordDetails {
+    pub keyword: Keyword,
+    pub entries: Vec<Entry>,
+}
+
+impl KeywordDetails {
+    pub fn learn(&mut self, nick: &str, text: &str, dbc: &PgConnection) -> Result<usize, Error> {
+        let now = ::chrono::Utc::now().naive_utc();
+        let ins = NewEntry {
+            keyword_id: self.keyword.id,
+            idx: (self.entries.len() + 1) as _,
+            text,
+            creation_ts: now,
+            created_by: nick,
+        };
+        let new = {
+            use crate::schema::entries;
+            ::diesel::insert_into(entries::table)
+                .values(ins)
+                .get_result(dbc)?
+        };
+        self.entries.push(new);
+        Ok(self.entries.len())
+    }
+
+    pub fn process_moves(&mut self, moves: &[(i32, i32)], dbc: &PgConnection) -> Result<(), Error> {
+        for (oid, new_idx) in moves {
+            {
+                use crate::schema::entries::dsl::*;
+                ::diesel::update(entries.filter(id.eq(oid)))
+                    .set(idx.eq(new_idx))
+                    .execute(dbc)?;
+            }
+        }
+        self.entries = Self::get_entries(self.keyword.id, dbc)?;
+        Ok(())
+    }
+
+    pub fn swap(&mut self, idx_a: usize, idx_b: usize, dbc: &PgConnection) -> Result<(), Error> {
+        let mut moves = vec![];
+        for ent in self.entries.iter() {
+            if ent.idx == idx_a as i32 {
+                moves.push((ent.id, idx_b as i32));
+            }
+            if ent.idx == idx_b as i32 {
+                moves.push((ent.id, idx_a as i32));
+            }
+        }
+        if moves.len() != 2 {
+            Err(format_err!("Invalid swap operation."))?;
+        }
+        self.process_moves(&moves, dbc)?;
+        Ok(())
+    }
+
+    pub fn update(&mut self, idx: usize, val: &str, dbc: &PgConnection) -> Result<(), Error> {
+        let ent = self
+            .entries
+            .get_mut(idx.saturating_sub(1))
+            .ok_or(format_err!("No such element to update."))?;
+        {
+            use crate::schema::entries::dsl::*;
+            ::diesel::update(entries.filter(id.eq(ent.id)))
+                .set(text.eq(val))
+                .execute(dbc)?;
+        }
+        ent.text = val.to_string();
+        Ok(())
+    }
+
+    pub fn delete(&mut self, idx: usize, dbc: &PgConnection) -> Result<(), Error> {
+        // step 1: delete the element
+        {
+            let ent = self
+                .entries
+                .get(idx.saturating_sub(1))
+                .ok_or(format_err!("No such element to delete."))?;
+            {
+                use crate::schema::entries::dsl::*;
+                ::diesel::delete(entries.filter(id.eq(ent.id))).execute(dbc)?;
+            }
+        }
+        // step 2: move all the elements in front of it back one
+        let mut moves = vec![];
+        for ent in self.entries.iter() {
+            if idx > ent.idx as _ {
+                moves.push((ent.id, ent.idx.saturating_sub(1)));
+            }
+        }
+        self.process_moves(&moves, dbc)?;
+        Ok(())
+    }
+
+    pub fn add_zwsp_to_name(name: &str) -> Option<String> {
+        let second_index = name.char_indices().nth(1).map(|(i, _)| i)?;
+        let (start, end) = name.split_at(second_index);
+        Some(format!("{}​{}", start, end))
+    }
+
+    pub fn format_entry(&self, idx: usize) -> Option<String> {
+        self.format_entry_colours(idx, true)
+    }
+
+    pub fn format_entry_colours(&self, idx: usize, with_colours: bool) -> Option<String> {
+        if let Some(ent) = self.entries.get(idx.saturating_sub(1)) {
+            let gen_clr = if self.keyword.chan == "*" && with_colours {
+                "\x0307"
+            } else {
+                ""
+            };
+            let zwsp_name = Self::add_zwsp_to_name(&self.keyword.name)
+                .unwrap_or_else(|| self.keyword.name.clone());
+            Some(format!(
+                "{}{}{name}{}[{idx}/{total}]{}: {text} {}[{date}]{}",
+                if with_colours { "\x02" } else { "" },
+                gen_clr,
+                if with_colours { "\x0f\x0315" } else { "" },
+                if with_colours { "\x0f" } else { "" },
+                if with_colours { "\x0f\x0314" } else { "" },
+                if with_colours { "\x0f" } else { "" },
+                name = zwsp_name,
+                idx = idx,
+                total = self.entries.len(),
+                text = ent.text,
+                date = ent.creation_ts.date()
+            ))
+        } else {
+            None
+        }
+    }
+
+    pub fn get_or_create(word: &str, c: &str, dbc: &PgConnection) -> Result<Self, Error> {
+        if let Some(ret) = Self::get(word, c, dbc)? {
+            Ok(ret)
+        } else {
+            Ok(Self::create(word, c, dbc)?)
+        }
+    }
+
+    pub fn create(word: &str, c: &str, dbc: &PgConnection) -> Result<Self, Error> {
+        let val = NewKeyword {
+            name: word,
+            chan: c,
+        };
+        let ret: Keyword = {
+            use crate::schema::keywords;
+            ::diesel::insert_into(keywords::table)
+                .values(val)
+                .get_result(dbc)?
+        };
+        Ok(KeywordDetails {
+            keyword: ret,
+            entries: vec![],
+        })
+    }
+
+    fn get_entries(kid: i32, dbc: &PgConnection) -> Result<Vec<Entry>, Error> {
+        let entries: Vec<Entry> = {
+            use crate::schema::entries::dsl::*;
+            entries
+                .filter(keyword_id.eq(kid))
+                .order_by(idx.asc())
+                .load(dbc)?
+        };
+        Ok(entries)
+    }
+
+    fn get_inner<'a, T: Into<Cow<'a, str>>>(
+        word: T,
+        c: &str,
+        dbc: &PgConnection,
+        recursion_count: usize,
+    ) -> Result<Option<Self>, Error> {
+        let word = word.into();
+        let keyword: Option<Keyword> = {
+            use crate::schema::keywords::dsl::*;
+            keywords
+                .filter(name.ilike(word).and(chan.eq(c).or(chan.eq("*"))))
+                .first(dbc)
+                .optional()?
+        };
+        if let Some(k) = keyword {
+            let entries = Self::get_entries(k.id, dbc)?;
+            if let Some(e0) = entries.get(0) {
+                if e0.text.starts_with("see: ") {
+                    if recursion_count > RECURSION_LIMIT {
+                        // Oh dear.
+                        Err(format_err!("Halt. You're having a bit too much fun."))?
+                    }
+                    let new_word = e0.text.replace("see: ", "");
+                    return Self::get_inner(new_word, c, dbc, recursion_count + 1);
+                }
+            }
+            Ok(Some(KeywordDetails {
+                keyword: k,
+                entries,
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn get<'a, T: Into<Cow<'a, str>>>(
+        word: T,
+        c: &str,
+        dbc: &PgConnection,
+    ) -> Result<Option<Self>, Error> {
+        Self::get_inner(word, c, dbc, 0)
+    }
+}
diff --git a/fun/paroxysm/src/main.rs b/fun/paroxysm/src/main.rs
new file mode 100644
index 0000000000..46a215c225
--- /dev/null
+++ b/fun/paroxysm/src/main.rs
@@ -0,0 +1,395 @@
+// TODO(tazjin): Upgrade to a Diesel version with public derive
+// macros.
+#[macro_use]
+extern crate diesel;
+
+use crate::cfg::Config;
+use crate::keyword::KeywordDetails;
+use diesel::pg::PgConnection;
+use diesel::r2d2::{ConnectionManager, Pool};
+use failure::{format_err, Error};
+use irc::client::prelude::*;
+use lazy_static::lazy_static;
+use log::{debug, info, warn};
+use rand::rngs::ThreadRng;
+use rand::{thread_rng, Rng};
+use regex::{Captures, Regex};
+use std::collections::HashMap;
+use std::fmt::Display;
+
+mod cfg;
+mod keyword;
+mod models;
+mod schema;
+
+lazy_static! {
+    static ref LEARN_RE: Regex =
+        Regex::new(r#"^\?\?(?P<gen>!)?\s*(?P<subj>[^\[:]*):\s*(?P<val>.*)"#).unwrap();
+    static ref QUERY_RE: Regex =
+        Regex::new(r#"^\?\?\s*(?P<subj>[^\[:]*)(?P<idx>\[[^\]]+\])?"#).unwrap();
+    static ref QLAST_RE: Regex = Regex::new(r#"^\?\?\s*(?P<subj>[^\[:]*)!"#).unwrap();
+    static ref INCREMENT_RE: Regex =
+        Regex::new(r#"^\?\?(?P<gen>!)?\s*(?P<subj>[^\[:]*)(?P<incrdecr>\+\+|\-\-)"#).unwrap();
+    static ref MOVE_RE: Regex =
+        Regex::new(r#"^\?\?(?P<gen>!)?\s*(?P<subj>[^\[:]*)(?P<idx>\[[^\]]+\])->(?P<new_idx>.*)"#)
+            .unwrap();
+}
+
+pub struct App {
+    client: IrcClient,
+    pg: Pool<ConnectionManager<PgConnection>>,
+    rng: ThreadRng,
+    cfg: Config,
+    last_msgs: HashMap<String, HashMap<String, String>>,
+}
+
+impl App {
+    pub fn report_error<T: Display>(
+        &mut self,
+        nick: &str,
+        chan: &str,
+        msg: T,
+    ) -> Result<(), Error> {
+        self.client
+            .send_notice(nick, format!("[{}] \x0304Error:\x0f {}", chan, msg))?;
+        Ok(())
+    }
+
+    pub fn keyword_from_captures(
+        &mut self,
+        learn: &::regex::Captures,
+        nick: &str,
+        chan: &str,
+    ) -> Result<KeywordDetails, Error> {
+        let db = self.pg.get()?;
+        debug!("Fetching keyword for captures: {:?}", learn);
+        let subj = &learn["subj"];
+        let learn_chan = if learn.name("gen").is_some() {
+            "*"
+        } else {
+            chan
+        };
+        if !chan.starts_with("#") && learn_chan != "*" {
+            Err(format_err!("Only general entries may be taught via PM."))?;
+        }
+        debug!("Fetching keyword '{}' for chan {}", subj, learn_chan);
+        let kwd = KeywordDetails::get_or_create(subj, learn_chan, &db)?;
+        if kwd.keyword.chan == "*" && !self.cfg.admins.contains(nick) {
+            Err(format_err!(
+                "Only administrators can create or modify general entries."
+            ))?;
+        }
+        Ok(kwd)
+    }
+
+    pub fn handle_move(
+        &mut self,
+        target: &str,
+        nick: &str,
+        chan: &str,
+        mv: Captures,
+    ) -> Result<(), Error> {
+        let db = self.pg.get()?;
+        let idx = &mv["idx"];
+        let idx = match idx[1..(idx.len() - 1)].parse::<usize>() {
+            Ok(i) => i,
+            Err(e) => Err(format_err!("Could not parse index: {}", e))?,
+        };
+        let new_idx = match mv["new_idx"].parse::<i32>() {
+            Ok(i) => i,
+            Err(e) => Err(format_err!("Could not parse target index: {}", e))?,
+        };
+        let mut kwd = self.keyword_from_captures(&mv, nick, chan)?;
+        if new_idx < 0 {
+            kwd.delete(idx, &db)?;
+            self.client.send_notice(
+                target,
+                format!("\x02{}\x0f: Deleted entry {}.", kwd.keyword.name, idx),
+            )?;
+        } else {
+            kwd.swap(idx, new_idx as _, &db)?;
+            self.client.send_notice(
+                target,
+                format!(
+                    "\x02{}\x0f: Swapped entries {} and {}.",
+                    kwd.keyword.name, idx, new_idx
+                ),
+            )?;
+        }
+        Ok(())
+    }
+
+    pub fn handle_learn(
+        &mut self,
+        target: &str,
+        nick: &str,
+        chan: &str,
+        learn: Captures,
+    ) -> Result<(), Error> {
+        let db = self.pg.get()?;
+        let val = &learn["val"];
+        let mut kwd = self.keyword_from_captures(&learn, nick, chan)?;
+        let idx = kwd.learn(nick, val, &db)?;
+        self.client
+            .send_notice(target, kwd.format_entry(idx).unwrap())?;
+        Ok(())
+    }
+
+    pub fn handle_insert_last_quote(
+        &mut self,
+        target: &str,
+        nick: &str,
+        chan: &str,
+        qlast: Captures,
+    ) -> Result<(), Error> {
+        let db = self.pg.get()?;
+        let nick_to_grab = &qlast["subj"].to_ascii_lowercase();
+        let mut kwd = self.keyword_from_captures(&qlast, nick, chan)?;
+        let chan_lastmsgs = self
+            .last_msgs
+            .entry(chan.to_string())
+            .or_insert(HashMap::new());
+        // Use `nick` here, so things like "grfn: see glittershark" work.
+        let val = if let Some(last) = chan_lastmsgs.get(nick_to_grab) {
+            if last.starts_with("\x01ACTION ") {
+                // Yes, this is inefficient, but it's better than writing some hacky CTCP parsing
+                // code I guess (also, characters are hard, so just blindly slicing
+                // seems like a bad idea)
+                format!(
+                    "* {} {}",
+                    nick_to_grab,
+                    last.replace("\x01ACTION ", "").replace("\x01", "")
+                )
+            } else {
+                format!("<{}> {}", nick_to_grab, last)
+            }
+        } else {
+            Err(format_err!("I dunno what {} said...", kwd.keyword.name))?
+        };
+        let idx = kwd.learn(nick, &val, &db)?;
+        self.client
+            .send_notice(target, kwd.format_entry(idx).unwrap())?;
+        Ok(())
+    }
+
+    pub fn handle_increment(
+        &mut self,
+        target: &str,
+        nick: &str,
+        chan: &str,
+        icr: Captures,
+    ) -> Result<(), Error> {
+        let db = self.pg.get()?;
+        let mut kwd = self.keyword_from_captures(&icr, nick, chan)?;
+        let is_incr = &icr["incrdecr"] == "++";
+        let now = chrono::Utc::now().naive_utc().date();
+        let mut idx = None;
+        for (i, ent) in kwd.entries.iter().enumerate() {
+            if ent.creation_ts.date() == now {
+                if let Ok(val) = ent.text.parse::<i32>() {
+                    let val = if is_incr { val + 1 } else { val - 1 };
+                    idx = Some((i + 1, val));
+                }
+            }
+        }
+        if let Some((i, val)) = idx {
+            kwd.update(i, &val.to_string(), &db)?;
+            self.client
+                .send_notice(target, kwd.format_entry(i).unwrap())?;
+        } else {
+            let val = if is_incr { 1 } else { -1 };
+            let idx = kwd.learn(nick, &val.to_string(), &db)?;
+            self.client
+                .send_notice(target, kwd.format_entry(idx).unwrap())?;
+        }
+        Ok(())
+    }
+
+    pub fn handle_query(
+        &mut self,
+        target: &str,
+        _nick: &str,
+        chan: &str,
+        query: Captures,
+    ) -> Result<(), Error> {
+        let db = self.pg.get()?;
+        let subj = &query["subj"];
+        let idx = match query.name("idx") {
+            Some(i) => {
+                let i = i.as_str();
+                match &i[1..(i.len() - 1)] {
+                    "*" => Some(-1),
+                    x => x.parse::<usize>().map(|x| x as i32).ok(),
+                }
+            }
+            None => None,
+        };
+        debug!("Querying {} with idx {:?}", subj, idx);
+        match KeywordDetails::get(subj, chan, &db)? {
+            Some(kwd) => {
+                if let Some(mut idx) = idx {
+                    if idx == -1 {
+                        // 'get all entries' ('*' parses into this)
+                        // step 1: make a blob of all the quotes
+                        let mut data_to_upload = String::new();
+                        for i in 0..kwd.entries.len() {
+                            data_to_upload
+                                .push_str(&kwd.format_entry_colours(i + 1, false).unwrap());
+                            data_to_upload.push('\n');
+                        }
+                        // step 2: attempt to POST it to eta's pastebin
+                        // TODO(eta): make configurable
+                        let response = crimp::Request::put("https://eta.st/lx/upload")
+                            .user_agent("paroxysm/0.0.2 crimp/0.2")?
+                            .header("Linx-Expiry", "7200")? // 2 hours
+                            .body("text/plain", data_to_upload.as_bytes())
+                            .timeout(std::time::Duration::from_secs(2))?
+                            .send()?
+                            .as_string()?;
+                        // step 3: tell the world about it
+                        if response.status != 200 {
+                            Err(format_err!(
+                                "upload returned {}: {}",
+                                response.status,
+                                response.body
+                            ))?
+                        }
+                        self.client.send_notice(
+                            target,
+                            format!(
+                                "\x02{}\x0f: uploaded {} quotes to \x02\x0311{}\x0f (will expire in \x0224\x0f hours)",
+                                subj,
+                                kwd.entries.len(),
+                                response.body
+                            )
+                        )?;
+                    } else {
+                        if idx == 0 {
+                            idx = 1;
+                        }
+                        if let Some(ent) = kwd.format_entry(idx as _) {
+                            self.client.send_notice(target, ent)?;
+                        } else {
+                            let pluralised = if kwd.entries.len() == 1 {
+                                "entry"
+                            } else {
+                                "entries"
+                            };
+                            self.client.send_notice(
+                                target,
+                                format!(
+                                    "\x02{}\x0f: only has \x02\x0304{}\x0f {}",
+                                    subj,
+                                    kwd.entries.len(),
+                                    pluralised
+                                ),
+                            )?;
+                        }
+                    }
+                } else {
+                    let entry = if kwd.entries.len() < 2 {
+                        1 // because [1, 1) does not a range make
+                    } else {
+                        self.rng.gen_range(1, kwd.entries.len())
+                    };
+                    if let Some(ent) = kwd.format_entry(entry) {
+                        self.client.send_notice(target, ent)?;
+                    } else {
+                        self.client
+                            .send_notice(target, format!("\x02{}\x0f: no entries yet", subj))?;
+                    }
+                }
+            }
+            None => {
+                // If someone just posts "??????????", don't spam the channel with
+                // an error message (but do allow joke entries to appear if set).
+                if !subj.chars().all(|c| c == '?' || c == ' ') {
+                    self.client
+                        .send_notice(target, format!("\x02{}\x0f: never heard of it", subj))?;
+                }
+            }
+        }
+        Ok(())
+    }
+
+    pub fn handle_privmsg(&mut self, from: &str, chan: &str, msg: &str) -> Result<(), Error> {
+        let nick = from.split("!").next().ok_or(format_err!(
+            "Received PRIVMSG from a source without nickname (failed to split n!u@h)"
+        ))?;
+        let target = if chan.starts_with("#") { chan } else { nick };
+        debug!("[{}] <{}> {}", chan, nick, msg);
+        if let Some(learn) = LEARN_RE.captures(msg) {
+            self.handle_learn(target, nick, chan, learn)?;
+        } else if let Some(qlast) = QLAST_RE.captures(msg) {
+            self.handle_insert_last_quote(target, nick, chan, qlast)?;
+        } else if let Some(mv) = MOVE_RE.captures(msg) {
+            self.handle_move(target, nick, chan, mv)?;
+        } else if let Some(icr) = INCREMENT_RE.captures(msg) {
+            self.handle_increment(target, nick, chan, icr)?;
+        } else if let Some(query) = QUERY_RE.captures(msg) {
+            self.handle_query(target, nick, chan, query)?;
+        } else {
+            let chan_lastmsgs = self
+                .last_msgs
+                .entry(chan.to_string())
+                .or_insert(HashMap::new());
+            chan_lastmsgs.insert(nick.to_string().to_ascii_lowercase(), msg.to_string());
+        }
+        Ok(())
+    }
+
+    pub fn handle_msg(&mut self, m: Message) -> Result<(), Error> {
+        match m.command {
+            Command::PRIVMSG(channel, message) => {
+                if let Some(src) = m.prefix {
+                    if let Err(e) = self.handle_privmsg(&src, &channel, &message) {
+                        warn!("error handling command in {} (src {}): {}", channel, src, e);
+                        if let Some(nick) = src.split("!").next() {
+                            self.report_error(nick, &channel, e)?;
+                        }
+                    }
+                }
+            }
+            Command::INVITE(nick, channel) => {
+                if self.cfg.admins.contains(&nick) {
+                    info!("Joining {} after admin invite", channel);
+                    self.client.send_join(channel)?;
+                }
+            }
+            _ => {}
+        }
+        Ok(())
+    }
+}
+
+fn main() -> Result<(), Error> {
+    println!("[+] loading configuration");
+    let default_log_filter = "paroxysm=info".to_string();
+    let mut settings = config::Config::default();
+    settings.merge(config::Environment::with_prefix("PARX"))?;
+    let cfg: Config = settings.try_into()?;
+    let env = env_logger::Env::new()
+        .default_filter_or(cfg.log_filter.clone().unwrap_or(default_log_filter));
+    env_logger::init_from_env(env);
+    info!("paroxysm starting up");
+    info!("connecting to database at {}", cfg.database_url);
+    let pg = Pool::new(ConnectionManager::new(&cfg.database_url))?;
+    info!("connecting to IRC using config {}", cfg.irc_config_path);
+    let client = IrcClient::new(&cfg.irc_config_path)?;
+    client.identify()?;
+    let st = client.stream();
+    let mut app = App {
+        client,
+        pg,
+        cfg,
+        rng: thread_rng(),
+        last_msgs: HashMap::new(),
+    };
+    info!("running!");
+    st.for_each_incoming(|m| {
+        if let Err(e) = app.handle_msg(m) {
+            warn!("Error processing message: {}", e);
+        }
+    })?;
+    Ok(())
+}
diff --git a/fun/paroxysm/src/models.rs b/fun/paroxysm/src/models.rs
new file mode 100644
index 0000000000..721efbbb2e
--- /dev/null
+++ b/fun/paroxysm/src/models.rs
@@ -0,0 +1,36 @@
+use crate::schema::{entries, keywords};
+use chrono::NaiveDateTime;
+
+#[derive(Queryable)]
+pub struct Keyword {
+    pub id: i32,
+    pub name: String,
+    pub chan: String,
+}
+
+#[derive(Queryable)]
+pub struct Entry {
+    pub id: i32,
+    pub keyword_id: i32,
+    pub idx: i32,
+    pub text: String,
+    pub creation_ts: NaiveDateTime,
+    pub created_by: String,
+}
+
+#[derive(Insertable)]
+#[table_name = "keywords"]
+pub struct NewKeyword<'a> {
+    pub name: &'a str,
+    pub chan: &'a str,
+}
+
+#[derive(Insertable)]
+#[table_name = "entries"]
+pub struct NewEntry<'a> {
+    pub keyword_id: i32,
+    pub idx: i32,
+    pub text: &'a str,
+    pub creation_ts: NaiveDateTime,
+    pub created_by: &'a str,
+}
diff --git a/fun/paroxysm/src/schema.rs b/fun/paroxysm/src/schema.rs
new file mode 100644
index 0000000000..ef4044531e
--- /dev/null
+++ b/fun/paroxysm/src/schema.rs
@@ -0,0 +1,18 @@
+table! {
+    entries (id) {
+        id -> Int4,
+        keyword_id -> Int4,
+        idx -> Int4,
+        text -> Varchar,
+        creation_ts -> Timestamp,
+        created_by -> Varchar,
+    }
+}
+
+table! {
+    keywords (id) {
+        id -> Int4,
+        name -> Varchar,
+        chan -> Varchar,
+    }
+}
diff --git a/fun/quinistry/.gitignore b/fun/quinistry/.gitignore
new file mode 100644
index 0000000000..622119552e
--- /dev/null
+++ b/fun/quinistry/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+quinistry
\ No newline at end of file
diff --git a/fun/quinistry/README.md b/fun/quinistry/README.md
new file mode 100644
index 0000000000..de197a219e
--- /dev/null
+++ b/fun/quinistry/README.md
@@ -0,0 +1,63 @@
+Quinistry
+=========
+
+*A simple Docker registry quine.*
+
+## What?
+
+This is an example project for a from-scratch implementation of an HTTP server compatible with the [Docker Registry V2][]
+protocol.
+
+It serves a single image called `quinistry:latest` which is a Docker image that runs quinistry itself, therefore it is a
+sort of Docker registry [quine][].
+
+The official documentation does not contain enough information to actually implement this protocol (which I assume is
+intentional), but a bit of trial&error lead there anyways. I've added comments to parts of the code to clear up things
+that may be helpful to other developers in the future.
+
+## Example
+
+```
+# Run quinistry:
+vincent@urdhva ~/go/src/github.com/tazjin/quinistry (git)-[master] % ./quinistry
+2017/03/16 14:11:56 Starting quinistry
+
+# Pull the quinistry image from itself:
+vincent@urdhva ~ % docker pull localhost:8080/quinistry
+Using default tag: latest
+latest: Pulling from quinistry
+7bf1a8b18466: Already exists
+Digest: sha256:d5cd4490901ef04b4e28e4ccc03a1d25fe3461200cf4d7166aab86fcd495e22e
+Status: Downloaded newer image for localhost:8080/quinistry:latest
+
+# Quinistry will log:
+2017/03/16 14:14:03 Acknowleding V2 API: GET /v2/
+2017/03/16 14:14:03 Serving manifest: GET /v2/quinistry/manifests/latest
+2017/03/16 14:14:03 Serving config: GET /v2/quinistry/blobs/sha256:fbb165c48849de16017aa398aa9bb08fd1c00eaa7c150b6c2af37312913db279
+
+# Run the downloaded image:
+vincent@urdhva ~ % docker run -p 8090:8080 localhost:8080/quinistry
+2017/03/16 13:15:18 Starting quinistry
+
+# And download it again from itself:
+vincent@urdhva ~ % docker pull localhost:8090/quinistry
+Using default tag: latest
+latest: Pulling from quinistry
+7bf1a8b18466: Already exists
+Digest: sha256:11141d95ddce0bac9ffa32ab1e8bc94748ed923e87762c68858dc41d11a46d3f
+Status: Downloaded newer image for localhost:8090/quinistry:latest
+```
+
+## Building
+
+Quinistry creates a Docker image that only contains a statically linked `main` binary. As this package makes use of
+`net/http`, Go will (by default) link against `libc` for DNS resolution and create a dynamic binary instead.
+
+To disable this, `build` the project with `-tags netgo`:
+
+```
+go build -tags netgo
+```
+
+[Docker Registry V2]: https://docs.docker.com/registry/spec/api/
+[quine]: https://en.wikipedia.org/wiki/Quine_(computing)
\ No newline at end of file
diff --git a/fun/quinistry/const.go b/fun/quinistry/const.go
new file mode 100644
index 0000000000..173fa9efc3
--- /dev/null
+++ b/fun/quinistry/const.go
@@ -0,0 +1,12 @@
+package main
+
+// HTTP content types
+
+const ImageConfigMediaType string = "application/vnd.docker.container.image.v1+json"
+const ManifestMediaType string = "application/vnd.docker.distribution.manifest.v2+json"
+const LayerMediaType string = "application/vnd.docker.image.rootfs.diff.tar.gzip"
+
+// HTTP header names
+
+const ContentType string = "Content-Type"
+const DigestHeader string = "Docker-Content-Digest"
diff --git a/fun/quinistry/default.nix b/fun/quinistry/default.nix
new file mode 100644
index 0000000000..b12c5e6f0c
--- /dev/null
+++ b/fun/quinistry/default.nix
@@ -0,0 +1,11 @@
+{ depot, ... }:
+
+depot.nix.buildGo.program {
+  name = "quinistry";
+  srcs = [
+    ./const.go
+    ./image.go
+    ./main.go
+    ./types.go
+  ];
+}
diff --git a/fun/quinistry/image.go b/fun/quinistry/image.go
new file mode 100644
index 0000000000..3daeac34d6
--- /dev/null
+++ b/fun/quinistry/image.go
@@ -0,0 +1,150 @@
+// The code in this file creates a Docker image layer containing the binary of the
+// application itself.
+
+package main
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"time"
+)
+
+// This function creates a Docker-image digest (i.e. SHA256 hash with
+// algorithm-specification prefix)
+func Digest(b []byte) string {
+	hash := sha256.New()
+	hash.Write(b)
+
+	return fmt.Sprintf("sha256:%x", hash.Sum(nil))
+}
+
+func GetImageOfCurrentExecutable() Image {
+	binary := getCurrentBinary()
+	tarArchive := createTarArchive(&map[string][]byte{
+		"/main": binary,
+	})
+
+	configJson, configElem := createConfig([]string{Digest(tarArchive)})
+	compressed := gzipArchive("Quinistry image", tarArchive)
+	manifest := createManifest(&configElem, &compressed)
+	manifestJson, _ := json.Marshal(manifest)
+
+	return Image{
+		Layer:          compressed,
+		LayerDigest:    Digest(compressed),
+		Manifest:       manifestJson,
+		ManifestDigest: Digest(manifestJson),
+		Config:         configJson,
+		ConfigDigest:   Digest(configJson),
+	}
+
+}
+
+func getCurrentBinary() []byte {
+	path, _ := os.Executable()
+	file, _ := ioutil.ReadFile(path)
+	return file
+}
+
+func createTarArchive(files *map[string][]byte) []byte {
+	buf := new(bytes.Buffer)
+	w := tar.NewWriter(buf)
+
+	for name, file := range *files {
+		hdr := &tar.Header{
+			Name: name,
+			// Everything is executable \o/
+			Mode: 0755,
+			Size: int64(len(file)),
+		}
+		w.WriteHeader(hdr)
+		w.Write(file)
+	}
+
+	if err := w.Close(); err != nil {
+		log.Fatalln(err)
+		os.Exit(1)
+	}
+
+	return buf.Bytes()
+}
+
+func gzipArchive(name string, archive []byte) []byte {
+	buf := new(bytes.Buffer)
+	w := gzip.NewWriter(buf)
+	w.Name = name
+	w.Write(archive)
+
+	if err := w.Close(); err != nil {
+		log.Fatalln(err)
+		os.Exit(1)
+	}
+
+	return buf.Bytes()
+}
+
+func createConfig(layerDigests []string) (configJson []byte, elem Element) {
+	now := time.Now()
+
+	imageConfig := &ImageConfig{
+		Cmd: []string{"/main"},
+		Env: []string{"PATH=/"},
+	}
+
+	rootFs := RootFs{
+		DiffIds: layerDigests,
+		Type:    "layers",
+	}
+
+	history := []History{
+		{
+			Created:   now,
+			CreatedBy: "Quinistry magic",
+		},
+	}
+
+	config := Config{
+		Created:      now,
+		Author:       "tazjin",
+		Architecture: "amd64",
+		Os:           "linux",
+		Config:       imageConfig,
+		RootFs:       rootFs,
+		History:      history,
+	}
+
+	configJson, _ = json.Marshal(config)
+
+	elem = Element{
+		MediaType: ImageConfigMediaType,
+		Size:      len(configJson),
+		Digest:    Digest(configJson),
+	}
+
+	return
+}
+
+func createManifest(config *Element, layer *[]byte) Manifest {
+	layers := []Element{
+		{
+			MediaType: LayerMediaType,
+			Size:      len(*layer),
+			// Layers must contain the digest of the *gzipped* layer.
+			Digest: Digest(*layer),
+		},
+	}
+
+	return Manifest{
+		SchemaVersion: 2,
+		MediaType:     ManifestMediaType,
+		Config:        *config,
+		Layers:        layers,
+	}
+}
diff --git a/fun/quinistry/k8s/child.yaml b/fun/quinistry/k8s/child.yaml
new file mode 100644
index 0000000000..aa2e318262
--- /dev/null
+++ b/fun/quinistry/k8s/child.yaml
@@ -0,0 +1,27 @@
+# This is a child quinistry, running via an image served off the parent.
+---
+apiVersion: extensions/v1beta1
+kind: DaemonSet
+metadata:
+  name: quinistry-gen2
+  labels:
+    k8s-app: quinistry
+    quinistry/role: child
+    quinistry/generation: '2'
+spec:
+  template:
+    metadata:
+      labels:
+        k8s-app: quinistry
+        quinistry/role: child
+        quinistry/generation: '2'
+    spec:
+      containers:
+        - name: quinistry
+          # Bootstrap via Docker Hub (or any other registry)
+          image: localhost:5000/quinistry
+          ports:
+            - name: registry
+              containerPort: 8080
+              # Incremented hostPort, 
+              hostPort: 5001
diff --git a/fun/quinistry/k8s/parent.yaml b/fun/quinistry/k8s/parent.yaml
new file mode 100644
index 0000000000..0db2fe300e
--- /dev/null
+++ b/fun/quinistry/k8s/parent.yaml
@@ -0,0 +1,27 @@
+# This is a bootstrapped Quinistry DaemonSet. The initial image
+# comes from Docker Hub
+---
+apiVersion: extensions/v1beta1
+kind: DaemonSet
+metadata:
+  name: quinistry
+  labels:
+    k8s-app: quinistry
+    quinistry/role: parent
+    quinistry/generation: '1'
+spec:
+  template:
+    metadata:
+      labels:
+        k8s-app: quinistry
+        quinistry/role: parent
+        quinistry/generation: '1'
+    spec:
+      containers:
+        - name: quinistry
+          # Bootstrap via Docker Hub (or any other registry)
+          image: tazjin/quinistry
+          ports:
+            - name: registry
+              containerPort: 8080
+              hostPort: 5000
diff --git a/fun/quinistry/main.go b/fun/quinistry/main.go
new file mode 100644
index 0000000000..50b47418d1
--- /dev/null
+++ b/fun/quinistry/main.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+)
+
+func main() {
+	log.Println("Starting quinistry")
+
+	image := GetImageOfCurrentExecutable()
+
+	layerUri := fmt.Sprintf("/v2/quinistry/blobs/%s", image.LayerDigest)
+	configUri := fmt.Sprintf("/v2/quinistry/blobs/%s", image.ConfigDigest)
+
+	log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// Acknowledge that we speak V2
+		if r.RequestURI == "/v2/" {
+			logRequest("Acknowleding V2 API", r)
+			fmt.Fprintln(w)
+			return
+		}
+
+		// Serve manifest
+		if r.RequestURI == "/v2/quinistry/manifests/latest" {
+			logRequest("Serving manifest", r)
+			w.Header().Set(ContentType, ManifestMediaType)
+			w.Header().Add(DigestHeader, image.ManifestDigest)
+			w.Write(image.Manifest)
+			return
+		}
+
+		// Serve actual image layer
+		if r.RequestURI == layerUri {
+			logRequest("Serving image layer blob", r)
+			w.Header().Add(DigestHeader, image.LayerDigest)
+			w.Write(image.Layer)
+			return
+		}
+
+		// Serve image config
+		if r.RequestURI == configUri {
+			logRequest("Serving config", r)
+			w.Header().Set("Content-Type", ImageConfigMediaType)
+			w.Header().Set(DigestHeader, image.ConfigDigest)
+			w.Write(image.Config)
+			return
+		}
+
+		log.Printf("Unhandled request: %v\n", *r)
+	})))
+}
+
+func logRequest(msg string, r *http.Request) {
+	log.Printf("%s: %s %s\n", msg, r.Method, r.RequestURI)
+}
diff --git a/fun/quinistry/types.go b/fun/quinistry/types.go
new file mode 100644
index 0000000000..498cbac2f2
--- /dev/null
+++ b/fun/quinistry/types.go
@@ -0,0 +1,79 @@
+package main
+
+import "time"
+
+// This type represents the rootfs-key of the Docker image config.
+// It specifies the digest (i.e. usually SHA256 hash) of the tar'ed, but NOT
+// compressed image layers.
+type RootFs struct {
+	// The digests of the non-compressed FS layers.
+	DiffIds []string `json:"diff_ids"`
+
+	// Type should always be set to "layers"
+	Type string `json:"type"`
+}
+
+// This type represents an entry in the Docker image config's history key.
+// Every history element "belongs" to a filesystem layer.
+type History struct {
+	Created   time.Time `json:"created"`
+	CreatedBy string    `json:"created_by"`
+}
+
+// This type represents runtime-configuration for the Docker image.
+// A lot of possible keys are omitted here, see:
+// https://github.com/docker/docker/blob/master/image/spec/v1.2.md#image-json-description
+type ImageConfig struct {
+	Cmd []string
+	Env []string
+}
+
+// This type represents the Docker image configuration
+type Config struct {
+	Created time.Time `json:"created"`
+	Author  string    `json:"author"`
+
+	// Architecture should be "amd64"
+	Architecture string `json:"architecture"`
+
+	// OS should be "linux"
+	Os string `json:"os"`
+
+	// Configuration can be set to 'nil', in which case all options have to be
+	// supplied at container launch time.
+	Config *ImageConfig `json:"config"`
+
+	// Filesystem layers and history elements have to be in the same order.
+	RootFs  RootFs    `json:"rootfs"`
+	History []History `json:"history"`
+}
+
+// This type represents any manifest
+type Element struct {
+	MediaType string `json:"mediaType"`
+	Size      int    `json:"size"`
+	Digest    string `json:"digest"`
+}
+
+// This type represents a Docker image manifest as used by the registry
+// protocol V2.
+type Manifest struct {
+	SchemaVersion int       `json:"schemaVersion"` // Must be 2
+	MediaType     string    `json:"mediaType"`     // Use ManifestMediaType const
+	Config        Element   `json:"config"`
+	Layers        []Element `json:"layers"`
+}
+
+// A really "dumb" representation of an image, with its data blob and related
+// metadata.
+// Note: This is not a registry API type.
+type Image struct {
+	Layer       []byte
+	LayerDigest string
+
+	Manifest       []byte
+	ManifestDigest string
+
+	Config       []byte
+	ConfigDigest string
+}
diff --git a/fun/tvl-ebooks/OWNERS b/fun/tvl-ebooks/OWNERS
new file mode 100644
index 0000000000..7dd8c27a57
--- /dev/null
+++ b/fun/tvl-ebooks/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - ben
\ No newline at end of file
diff --git a/fun/tvl-ebooks/default.nix b/fun/tvl-ebooks/default.nix
new file mode 100644
index 0000000000..fde6e05822
--- /dev/null
+++ b/fun/tvl-ebooks/default.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+
+pkgs.buildGoModule {
+  name = "tvl-ebooks";
+  vendorSha256 = "1p7bazh2vbhvvm559bcvfff9s4yy4q9jmklxr3sfp97inwpv6hzy";
+  src = ./.;
+}
diff --git a/fun/tvl-ebooks/ebook-client/main.go b/fun/tvl-ebooks/ebook-client/main.go
new file mode 100644
index 0000000000..7954184dd7
--- /dev/null
+++ b/fun/tvl-ebooks/ebook-client/main.go
@@ -0,0 +1,170 @@
+package main
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"sync"
+	"time"
+
+	"github.com/go-redis/redis"
+	"gopkg.in/irc.v3"
+)
+
+var messageBeat chan bool
+var firstMessage chan bool
+var client *irc.Client
+var safeLock sync.Mutex
+
+func main() {
+	nick := flag.String("nick", "NONE", "the ircnick you want")
+	from := flag.String("ip", "[::1]", "src address")
+	flag.Parse()
+
+	localAddrDialier := &net.Dialer{
+		LocalAddr: &net.TCPAddr{
+			IP:   net.ParseIP(*from),
+			Port: 0,
+		},
+	}
+
+	conn, err := tls.DialWithDialer(localAddrDialier, "tcp", "chat.freenode.net:6697", &tls.Config{})
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	messageBeat = make(chan bool)
+	firstMessage = make(chan bool, 10)
+	go ircKeepalive()
+
+	redisc := redis.NewClient(&redis.Options{
+		Addr:     fmt.Sprintf("127.0.0.1:%d", 6379),
+		Password: "", // no password set
+		DB:       0,  // use default DB
+	})
+
+	go func() {
+		for {
+			time.Sleep(time.Second)
+			r := redisc.Ping()
+			if r.Err() != nil {
+				redisc = redis.NewClient(&redis.Options{
+					Addr:     fmt.Sprintf("127.0.0.1:%d", 6379),
+					Password: "", // no password set
+					DB:       0,  // use default DB
+				})
+			}
+			redisc.Set(fmt.Sprintf("alive-%s", *nick), "yes", time.Second*5)
+		}
+	}()
+
+	if *nick == "NONE" {
+		log.Fatalf("You must set a nick")
+	}
+
+	go func() {
+		<-firstMessage
+		for {
+			psub := redisc.Subscribe(fmt.Sprintf("irc-%s", *nick))
+
+			for {
+				msg, err := psub.ReceiveMessage()
+				if err != nil {
+					break
+				}
+				client.WriteMessage(&irc.Message{
+					Command: "PRIVMSG",
+					Params: []string{
+						"##tvl-ebooks",
+						msg.Payload,
+					},
+				})
+			}
+			time.Sleep(time.Second * 10)
+		}
+
+	}()
+
+	go func() {
+		<-firstMessage
+		for {
+			psub := redisc.Subscribe(fmt.Sprintf("raw-irc-%s", *nick))
+
+			for {
+				msg, err := psub.ReceiveMessage()
+				if err != nil {
+					break
+				}
+				im := irc.Message{}
+				err = json.Unmarshal([]byte(msg.Payload), &im)
+				if err == nil {
+					client.WriteMessage(&im)
+				}
+			}
+			time.Sleep(time.Second * 10)
+		}
+
+	}()
+
+	seenMsgBefore := false
+	config := irc.ClientConfig{
+		Nick: *nick,
+		User: *nick,
+		Name: fmt.Sprintf("%s Ebooks", *nick),
+		Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
+			b, _ := json.Marshal(m)
+			log.Printf("%#v", string(b))
+
+			messageBeat <- true
+
+			if !seenMsgBefore {
+				firstMessage <- true
+				seenMsgBefore = true
+			}
+			res := redisc.Publish("ebook", string(b))
+			if res.Err() != nil {
+				log.Printf("Publish error! %#v", err)
+			}
+			if m.Command == "001" {
+				// 001 is a welcome event, so we join channels there
+				c.Write("JOIN ##tvl-ebooks")
+			}
+			// else if m.Command == "PRIVMSG" && c.FromChannel(m) {
+			// 	// // Create a handler on all messages.
+			// 	// c.WriteMessage(&irc.Message{
+			// 	// 	Command: "PRIVMSG",
+			// 	// 	Params: []string{
+			// 	// 		m.Params[0],
+			// 	// 		m.Trailing(),
+			// 	// 	},
+			// 	// })
+			// }
+		}),
+	}
+
+	// Create the client
+	client = irc.NewClient(conn, config)
+	err = client.Run()
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+func ircKeepalive() {
+	tt := time.NewTimer(time.Second)
+	lastPing := time.Now()
+	for {
+		select {
+		case <-tt.C:
+			if time.Since(lastPing) > time.Minute*5 {
+				log.Fatalf("It's been too long since the last IRC message, blowing up")
+			}
+			break
+		case <-messageBeat:
+			lastPing = time.Now()
+		}
+	}
+}
diff --git a/fun/tvl-ebooks/go.mod b/fun/tvl-ebooks/go.mod
new file mode 100644
index 0000000000..154fbf8216
--- /dev/null
+++ b/fun/tvl-ebooks/go.mod
@@ -0,0 +1,8 @@
+module github.com/benjojo/tvl-ebooks
+
+go 1.14
+
+require (
+	github.com/go-redis/redis v6.15.9+incompatible
+	gopkg.in/irc.v3 v3.1.3
+)
diff --git a/fun/tvl-ebooks/go.sum b/fun/tvl-ebooks/go.sum
new file mode 100644
index 0000000000..bd3ff87613
--- /dev/null
+++ b/fun/tvl-ebooks/go.sum
@@ -0,0 +1,11 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/irc.v3 v3.1.3 h1:yeTiJ365882L8h4AnBKYfesD92y5R5ZhGiylu9DfcPY=
+gopkg.in/irc.v3 v3.1.3/go.mod h1:shO2gz8+PVeS+4E6GAny88Z0YVVQSxQghdrMVGQsR9s=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/fun/tvl-ebooks/irc-ingest/main.go b/fun/tvl-ebooks/irc-ingest/main.go
new file mode 100644
index 0000000000..bbd607405c
--- /dev/null
+++ b/fun/tvl-ebooks/irc-ingest/main.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"time"
+
+	"github.com/go-redis/redis"
+	"gopkg.in/irc.v3"
+)
+
+var messageBeat chan bool
+
+func main() {
+	conn, err := tls.Dial("tcp", "bnc.irccloud.com:6697", nil)
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	messageBeat = make(chan bool)
+	go ircKeepalive()
+
+	redisc := redis.NewClient(&redis.Options{
+		Addr:     fmt.Sprintf("127.0.0.1:%d", 6379),
+		Password: "", // no password set
+		DB:       0,  // use default DB
+	})
+
+	go func() {
+		for {
+			time.Sleep(time.Second)
+			r := redisc.Ping()
+			if r.Err() != nil {
+				redisc = redis.NewClient(&redis.Options{
+					Addr:     fmt.Sprintf("127.0.0.1:%d", 6379),
+					Password: "", // no password set
+					DB:       0,  // use default DB
+				})
+			}
+		}
+	}()
+
+	config := irc.ClientConfig{
+		Nick: "Benjojo",
+		Pass: os.Getenv("IRCCLOUD"),
+		User: "Benjojo",
+		Name: "Ben Cox",
+		Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
+			b, _ := json.Marshal(m)
+			// log.Printf("%#v", string(b))
+
+			messageBeat <- true
+			res := redisc.Publish("irccloud", string(b))
+			if res.Err() != nil {
+				log.Printf("Publish error! %#v", err)
+			}
+			// if m.Command == "001" {
+			// 	// 001 is a welcome event, so we join channels there
+			// 	// c.Write("JOIN #bot-test-chan")
+			// } else if m.Command == "PRIVMSG" && c.FromChannel(m) {
+			// 	// // Create a handler on all messages.
+			// 	// c.WriteMessage(&irc.Message{
+			// 	// 	Command: "PRIVMSG",
+			// 	// 	Params: []string{
+			// 	// 		m.Params[0],
+			// 	// 		m.Trailing(),
+			// 	// 	},
+			// 	// })
+			// }
+		}),
+	}
+
+	// Create the client
+	client := irc.NewClient(conn, config)
+	err = client.Run()
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+func ircKeepalive() {
+	tt := time.NewTimer(time.Second)
+	lastPing := time.Now()
+	for {
+		select {
+		case <-tt.C:
+			if time.Since(lastPing) > time.Minute*5 {
+				log.Fatalf("It's been too long since the last IRC message, blowing up")
+			}
+			break
+		case <-messageBeat:
+			lastPing = time.Now()
+		}
+	}
+
+}
diff --git a/fun/tvl-ebooks/make-v6/main.go b/fun/tvl-ebooks/make-v6/main.go
new file mode 100644
index 0000000000..6d4d0047bb
--- /dev/null
+++ b/fun/tvl-ebooks/make-v6/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"crypto/rand"
+	"log"
+	"net"
+)
+
+func main() {
+	// 2a0c:2f07:29:9999:6564:5298:8413:4652
+	ip := net.ParseIP("2a0c:2f07:29::")
+
+	rand.Read(ip[6:])
+
+	if ip[7] > 0xaa {
+		ip[4] = 0x03
+		ip[5] = 0x84
+		if ip[7] > 0xdd {
+			ip[4] = 0x08
+			ip[5] = 0x64
+		}
+	}
+
+	log.Printf("%s\n", ip)
+	//
+}
diff --git a/fun/tvl-ebooks/mkov-engine/main.go b/fun/tvl-ebooks/mkov-engine/main.go
new file mode 100644
index 0000000000..ba48634140
--- /dev/null
+++ b/fun/tvl-ebooks/mkov-engine/main.go
@@ -0,0 +1,246 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"math/rand"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-redis/redis"
+)
+
+type incomingIRC struct {
+	Command string   `json:"Command"`
+	Host    string   `json:"Host"`
+	Name    string   `json:"Name"`
+	Params  []string `json:"Params"`
+	User    string   `json:"User"`
+}
+
+var suppressionUsernames map[string]bool
+var noMkov map[string]bool
+
+func main() {
+	redisc := redis.NewClient(&redis.Options{
+		Addr:     fmt.Sprintf("127.0.0.1:%d", 6379),
+		Password: "", // no password set
+		DB:       0,  // use default DB
+	})
+
+	fireaway := make(chan incomingIRC, 10)
+	suppressionUsernames = make(map[string]bool)
+
+	suppressionList := redisc.HGetAll("suppressionList")
+	suppressionListA, _ := suppressionList.Result()
+
+	suppressionListMap, _ := stringMaptoIntMap(suppressionListA)
+	for v, _ := range suppressionListMap {
+		suppressionUsernames[v] = true
+		suppressionUsernames[strings.ToLower(v)] = true
+	}
+
+	noMkov = make(map[string]bool)
+
+	noMkovRedis := redisc.HGetAll("nomkov")
+	noMkovRedisA, _ := noMkovRedis.Result()
+
+	noMkovMap, _ := stringMaptoIntMap(noMkovRedisA)
+	for v, _ := range noMkovMap {
+		noMkov[v] = true
+		noMkov[strings.ToLower(v)] = true
+	}
+
+	go func() {
+		for {
+			irccloudFeed := redisc.Subscribe("irccloud")
+			for {
+				msg, err := irccloudFeed.ReceiveMessage()
+				if err != nil {
+					break
+				}
+				imsg := incomingIRC{}
+				err = json.Unmarshal([]byte(msg.Payload), &imsg)
+				if err != nil {
+					log.Printf("Json decoding error from irccloud feed %s", err)
+					continue
+				}
+
+				if imsg.Command == "PRIVMSG" {
+					if len(imsg.Params) == 2 {
+						if imsg.Params[0] == "##tvl" || imsg.Params[0] == "##tvlbot" {
+							fireaway <- imsg
+						}
+					}
+				}
+			}
+			time.Sleep(time.Second)
+		}
+	}()
+
+	for msg := range fireaway {
+		// Learn
+		learnFromMessage(msg, redisc)
+		msg2 := generateMesasge(msg, redisc)
+
+		// Check if we have a active log in for that user
+		ttl := redisc.TTL("alive-" + msg.Name + "-eb")
+		ttld, err := ttl.Result()
+		if err == nil {
+			redisc.Publish("irc-"+msg.Name+"-eb", msg2)
+			if ttld == 0 || ttld.Seconds() == -2 {
+				redisc.Publish("irc-tvlebooks-eb", "<"+fmt.Sprintf("%s.%s", string(msg.Name[0]), msg.Name[1:])+"-eb> "+msg2)
+			}
+		} else {
+			redisc.Publish("irc-tvlebooks-eb", "<"+fmt.Sprintf("%s.%s", string(msg.Name[0]), msg.Name[1:])+"-eb> "+msg2)
+		}
+	}
+}
+
+func generateMesasge(msg incomingIRC, redisc *redis.Client) string {
+	text := msg.Params[1]
+	username := strings.ToLower(msg.Name)
+	suppressionUsernames[username] = true
+	suppressionUsernames[username+":"] = true
+	suppressionUsernames[msg.Name] = true
+	suppressionUsernames[msg.Name+":"] = true
+	redisc.HIncrBy("suppressionList", msg.Name, 1)
+
+	text = strings.ToLower(text)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ".", "", -1)
+	text = strings.Replace(text, "!", "", -1)
+	text = strings.Replace(text, "?", "", -1)
+
+	words := strings.Split(text, " ")
+	lastWord := propwords(msg.Name, words[0], redisc)
+
+	if noMkov[username] {
+		lastWord = blockoutWord(lastWord)
+		words[0] = blockoutWord(words[0])
+	}
+
+	lastWord = filterHighlights(lastWord)
+
+	if lastWord == "_END_" {
+		if noMkov[username] {
+			return blockoutWord(words[0])
+		}
+		return words[0]
+	}
+	outputMsg := words[0] + " " + lastWord + " "
+
+	for {
+		lastWord = propwords(username, lastWord, redisc)
+		if lastWord == "" || lastWord == "_END_" {
+			return outputMsg
+		}
+
+		if noMkov[username] {
+			lastWord = blockoutWord(lastWord)
+		}
+
+		lastWord = filterHighlights(lastWord)
+
+		outputMsg += lastWord + " "
+		if len(outputMsg) > 100 {
+			return outputMsg
+		}
+	}
+}
+
+// filterHighlights: tries to prevent highlights by checking against
+// a map called suppressionUsernames
+func filterHighlights(in string) string {
+	for username := range suppressionUsernames {
+		if strings.Contains(in, username) {
+			if len(in) < 2 {
+				in = fmt.Sprintf("%s.%s", string(in[0]), in[1:])
+				return in
+			}
+		}
+	}
+	return in
+}
+
+func blockoutWord(in string) string {
+	block := ""
+	for i := 0; i < len(in); i++ {
+		block += "█"
+	}
+	return block
+}
+
+func propwords(username string, start string, redisc *redis.Client) string {
+	userHash := redisc.HGetAll(fmt.Sprintf("%s-%s", username, start))
+	userHashMap, err := userHash.Result()
+	if err != nil {
+		genericHash := redisc.HGetAll(fmt.Sprintf("generic-%s", start))
+		userHashMap, err = genericHash.Result()
+	}
+
+	userIntHashMap, totalVectors := stringMaptoIntMap(userHashMap)
+	if totalVectors == 0 {
+		return ""
+	}
+	targetRand := rand.Intn(totalVectors)
+	progresRand := 0
+
+	for k, v := range userIntHashMap {
+		progresRand += v
+		if targetRand > progresRand {
+			return k
+		}
+	}
+
+	for k, _ := range userIntHashMap {
+		return k
+	}
+
+	return ""
+}
+
+func stringMaptoIntMap(in map[string]string) (outMap map[string]int, total int) {
+	outMap = make(map[string]int)
+
+	for k, v := range in {
+		i, err := strconv.ParseInt(v, 10, 64)
+		if err != nil {
+			continue
+		}
+		total += int(i)
+		outMap[k] = int(i)
+	}
+
+	return outMap, total
+}
+
+func learnFromMessage(msg incomingIRC, redisc *redis.Client) {
+	text := msg.Params[1]
+
+	text = strings.ToLower(text)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ".", "", -1)
+	text = strings.Replace(text, "!", "", -1)
+	text = strings.Replace(text, "?", "", -1)
+
+	words := strings.Split(text, " ")
+	username := msg.Name
+
+	for k, word := range words {
+		// HINCRBY myhash field 1
+		nextWord := "_END_"
+		if len(words)-1 != k {
+			nextWord = words[k+1]
+		}
+
+		if !noMkov[username] {
+			redisc.HIncrBy(fmt.Sprintf("%s-%s", username, word), nextWord, 1)
+		}
+		redisc.HIncrBy(fmt.Sprintf("generic-%s", word), nextWord, 1)
+	}
+}
diff --git a/fun/tvl-ebooks/parse-logs/main.go b/fun/tvl-ebooks/parse-logs/main.go
new file mode 100644
index 0000000000..a2d0d20eeb
--- /dev/null
+++ b/fun/tvl-ebooks/parse-logs/main.go
@@ -0,0 +1,174 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"log"
+	"math/rand"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/go-redis/redis"
+)
+
+type incomingIRC struct {
+	Command string   `json:"Command"`
+	Host    string   `json:"Host"`
+	Name    string   `json:"Name"`
+	Params  []string `json:"Params"`
+	User    string   `json:"User"`
+}
+
+var quicklogMatch = regexp.MustCompile(`<(\w+)> (.+)`)
+
+func main() {
+	redisc := redis.NewClient(&redis.Options{
+		Addr:     fmt.Sprintf("127.0.0.1:%d", 6379),
+		Password: "", // no password set
+		DB:       0,  // use default DB
+	})
+
+	fireaway := make(chan incomingIRC, 10)
+	go func() {
+		f, err := os.Open("tvl.txt")
+		if err != nil {
+			log.Printf("aaa %v", err)
+			os.Exit(0)
+		}
+
+		bio := bufio.NewReader(f)
+		for {
+			line, _, err := bio.ReadLine()
+			if err != nil {
+				break
+			}
+
+			sline := string(line)
+
+			offset := strings.Index(sline, "]")
+
+			notime := sline[offset+1:]
+
+			if quicklogMatch.MatchString(notime) {
+				bits := quicklogMatch.FindAllStringSubmatch(notime, -1)
+				if len(bits) != 0 {
+					if len(bits[0]) != 0 {
+						a := make([]string, 2)
+						a[1] = bits[0][2]
+						ic := incomingIRC{
+							Name:   bits[0][1],
+							Params: a,
+						}
+						log.Printf("aa %#v", ic)
+
+						fireaway <- ic
+					}
+				}
+			}
+
+		}
+
+	}()
+
+	for msg := range fireaway {
+		// Learn
+		learnFromMessage(msg, redisc)
+		// os.Exit(0)
+	}
+}
+
+func generateMesasge(msg incomingIRC, redisc *redis.Client) string {
+	text := msg.Params[1]
+	username := msg.Name
+
+	text = strings.ToLower(text)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ".", "", -1)
+	text = strings.Replace(text, "!", "", -1)
+	text = strings.Replace(text, "?", "", -1)
+
+	words := strings.Split(text, " ")
+	lastWord := propwords(username, words[0], redisc)
+	outputMsg := words[0] + " " + lastWord + " "
+
+	for {
+		lastWord = propwords(username, words[0], redisc)
+		if lastWord == "" || lastWord == "_END_" {
+			return outputMsg
+		}
+
+		outputMsg += lastWord + " "
+		if len(outputMsg) > 100 {
+			return outputMsg
+		}
+	}
+}
+
+func propwords(username string, start string, redisc *redis.Client) string {
+	userHash := redisc.HGetAll(fmt.Sprintf("%s-%s", username, start))
+	userHashMap, err := userHash.Result()
+	if err != nil {
+		genericHash := redisc.HGetAll(fmt.Sprintf("generic-%s", start))
+		userHashMap, err = genericHash.Result()
+	}
+
+	userIntHashMap, totalVectors := stringMaptoIntMap(userHashMap)
+	targetRand := rand.Intn(totalVectors)
+	progresRand := 0
+
+	for k, v := range userIntHashMap {
+		progresRand += v
+		if targetRand > progresRand {
+			return k
+		}
+	}
+
+	for k, _ := range userIntHashMap {
+		return k
+	}
+
+	return ""
+}
+
+func stringMaptoIntMap(in map[string]string) (outMap map[string]int, total int) {
+	outMap = make(map[string]int)
+
+	for k, v := range in {
+		i, err := strconv.ParseInt(v, 10, 64)
+		if err != nil {
+			continue
+		}
+		total += int(i)
+		outMap[k] = int(i)
+	}
+
+	return outMap, total
+}
+
+func learnFromMessage(msg incomingIRC, redisc *redis.Client) {
+	text := msg.Params[1]
+
+	text = strings.ToLower(text)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ",", "", -1)
+	text = strings.Replace(text, ".", "", -1)
+	text = strings.Replace(text, "!", "", -1)
+	text = strings.Replace(text, "?", "", -1)
+
+	words := strings.Split(text, " ")
+	username := msg.Name
+
+	for k, word := range words {
+		// HINCRBY myhash field 1
+		nextWord := "_END_"
+		if len(words)-1 != k {
+			nextWord = words[k+1]
+		}
+
+		redisc.HIncrBy(fmt.Sprintf("%s-%s", username, word), nextWord, 1)
+		redisc.HIncrBy(fmt.Sprintf("generic-%s", word), nextWord, 1)
+	}
+}
diff --git a/fun/uggc/default.nix b/fun/uggc/default.nix
new file mode 100644
index 0000000000..980ad16bcc
--- /dev/null
+++ b/fun/uggc/default.nix
@@ -0,0 +1,21 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.third_party) gopkgs;
+
+  uggc = depot.nix.buildGo.program {
+    name = "uggc";
+    srcs = [
+      ./main.go
+    ];
+    deps = [
+      gopkgs."github.com".pkg.browser.gopkg
+    ];
+  };
+in
+uggc.overrideAttrs (old: {
+  buildCommand = old.buildCommand + ''
+    install -D ${./uggc.desktop} $out/share/applications/uggc.desktop
+    sed "s|@out@|$out|g" -i $out/share/applications/uggc.desktop
+  '';
+})
diff --git a/fun/uggc/main.go b/fun/uggc/main.go
new file mode 100644
index 0000000000..94f8cf0925
--- /dev/null
+++ b/fun/uggc/main.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/pkg/browser"
+)
+
+func rot13(r rune) rune {
+	if 'a' <= r && r <= 'm' {
+		return r + ('n' - 'a')
+	} else if 'n' <= r && r <= 'z' {
+		return r - ('n' - 'a')
+	}
+	if 'A' <= r && r <= 'M' {
+		return r + ('N' - 'A')
+	} else if 'N' <= r && r <= 'Z' {
+		return r - ('N' - 'A')
+	}
+	return r
+}
+
+func main() {
+	if len(os.Args) == 0 {
+		fmt.Println("usage: uggc [rot13-encoded URL]")
+		return
+	}
+	urlText := strings.Join(os.Args[1:], " ")
+	corrected := strings.Map(rot13, urlText)
+
+	err := browser.OpenURL(corrected)
+
+	if err != nil {
+		fmt.Println("could not launch browser:", err)
+	}
+}
diff --git a/fun/uggc/uggc.desktop b/fun/uggc/uggc.desktop
new file mode 100644
index 0000000000..c4eeff7f58
--- /dev/null
+++ b/fun/uggc/uggc.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Type=Application
+Name=Rot13 URL Handler
+Exec=@out@/bin/uggc
+StartupNotify=false
+MimeType=x-scheme-handler/uggc;
+MimeType=x-scheme-handler/uggcf;
diff --git a/fun/uggc/uggc.reg b/fun/uggc/uggc.reg
new file mode 100644
index 0000000000..bfb8e1952f
--- /dev/null
+++ b/fun/uggc/uggc.reg
@@ -0,0 +1,23 @@
+Windows Registry Editor Version 5.00
+
+[HKEY_CLASSES_ROOT\uggc]
+"URL Protocol"=""
+@="URL:Rot13 HTTP URL Protocol"
+
+[HKEY_CLASSES_ROOT\uggcf]
+"URL Protocol"=""
+@="URL:Rot13 HTTPS URL Protocol"
+
+[HKEY_CLASSES_ROOT\uggc\shell]
+
+[HKEY_CLASSES_ROOT\uggcf\shell]
+
+[HKEY_CLASSES_ROOT\uggc\shell\open]
+
+[HKEY_CLASSES_ROOT\uggcf\shell\open]
+
+[HKEY_CLASSES_ROOT\uggc\shell\open\command]
+@="\"C:\\Program Files\\uggc\\uggc.exe\" \"%1\""
+
+[HKEY_CLASSES_ROOT\uggcf\shell\open\command]
+@="\"C:\\Program Files\\uggc\\uggc.exe\" \"%1\""
diff --git a/fun/watchblob/README.md b/fun/watchblob/README.md
new file mode 100644
index 0000000000..712c96cd95
--- /dev/null
+++ b/fun/watchblob/README.md
@@ -0,0 +1,35 @@
+Watchblob - WatchGuard VPN on Linux
+===================================
+
+This tiny helper tool makes it possible to use WatchGuard / Firebox / <<whatever
+they are actually called>> VPNs that use multi-factor authentication on Linux.
+
+Rather than using OpenVPN's built-in dynamic challenge/response protocol, WatchGuard
+has opted for a separate implementation negotiating credentials outside of the
+OpenVPN protocol, which makes it impossible to start those connections solely by
+using the `openvpn` CLI and configuration files.
+
+What this application does has been reverse-engineered from the "WatchGuard Mobile VPN
+with SSL" application on OS X.
+
+I've published a [blog post](https://www.tazj.in/en/1486830338) describing the process
+and what is actually going on in this protocol.
+
+## Installation
+
+Make sure you have Go installed and `GOPATH` configured, then simply
+`go get github.com/tazjin/watchblob/...`.
+
+## Usage
+
+Right now the usage is very simple. Make sure you have the correct OpenVPN client
+config ready (this is normally supplied by the WatchGuard UI) simply run:
+
+```
+watchblob vpnserver.somedomain.org username p4ssw0rd
+```
+
+The server responds with a challenge which is displayed to the user, wait until you
+receive the SMS code or whatever and enter it. `watchblob` then completes the
+credential negotiation and you may proceed to log in with OpenVPN using your username
+and *the OTP token* (**not**  your password) as credentials.
diff --git a/fun/watchblob/default.nix b/fun/watchblob/default.nix
new file mode 100644
index 0000000000..dc32e4d1ce
--- /dev/null
+++ b/fun/watchblob/default.nix
@@ -0,0 +1,13 @@
+{ depot, ... }:
+
+depot.nix.buildGo.program {
+  name = "watchblob";
+  srcs = [
+    ./main.go
+    ./urls.go
+  ];
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.crypto.ssh.terminal.gopkg
+  ];
+}
diff --git a/fun/watchblob/main.go b/fun/watchblob/main.go
new file mode 100644
index 0000000000..a7ab65d3d1
--- /dev/null
+++ b/fun/watchblob/main.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+	"bufio"
+	"encoding/xml"
+	"fmt"
+	"golang.org/x/crypto/ssh/terminal"
+	"net/http"
+	"os"
+	"strings"
+	"syscall"
+)
+
+// The XML response returned by the WatchGuard server
+type Resp struct {
+	Action      string `xml:"action"`
+	LogonStatus int    `xml:"logon_status"`
+	LogonId     int    `xml:"logon_id"`
+	Error       string `xml:"errStr"`
+	Challenge   string `xml:"chaStr"`
+}
+
+func main() {
+	args := os.Args[1:]
+
+	if len(args) != 1 {
+		fmt.Fprintln(os.Stderr, "Usage: watchblob <vpn-host>")
+		os.Exit(1)
+	}
+
+	host := args[0]
+
+	username, password, err := readCredentials()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Could not read credentials: %v\n", err)
+	}
+
+	fmt.Printf("Requesting challenge from %s as user %s\n", host, username)
+	challenge, err := triggerChallengeResponse(&host, &username, &password)
+
+	if err != nil || challenge.LogonStatus != 4 {
+		fmt.Fprintln(os.Stderr, "Did not receive challenge from server")
+		fmt.Fprintf(os.Stderr, "Response: %v\nError: %v\n", challenge, err)
+		os.Exit(1)
+	}
+
+	token := getToken(&challenge)
+	err = logon(&host, &challenge, &token)
+
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Logon failed: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %s as your password\n", token)
+}
+
+func readCredentials() (string, string, error) {
+	fmt.Printf("Username: ")
+	reader := bufio.NewReader(os.Stdin)
+	username, err := reader.ReadString('\n')
+
+	fmt.Printf("Password: ")
+	password, err := terminal.ReadPassword(syscall.Stdin)
+	fmt.Println()
+
+	// If an error occured, I don't care about which one it is.
+	return strings.TrimSpace(username), strings.TrimSpace(string(password)), err
+}
+
+func triggerChallengeResponse(host *string, username *string, password *string) (r Resp, err error) {
+	return request(templateUrl(host, templateChallengeTriggerUri(username, password)))
+}
+
+func getToken(challenge *Resp) string {
+	fmt.Println(challenge.Challenge)
+
+	reader := bufio.NewReader(os.Stdin)
+	token, _ := reader.ReadString('\n')
+
+	return strings.TrimSpace(token)
+}
+
+func logon(host *string, challenge *Resp, token *string) (err error) {
+	resp, err := request(templateUrl(host, templateResponseUri(challenge.LogonId, token)))
+	if err != nil {
+		return
+	}
+
+	if resp.LogonStatus != 1 {
+		err = fmt.Errorf("Challenge/response authentication failed: %v", resp)
+	}
+
+	return
+}
+
+func request(url string) (r Resp, err error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return
+	}
+
+	defer resp.Body.Close()
+	decoder := xml.NewDecoder(resp.Body)
+
+	err = decoder.Decode(&r)
+	return
+}
diff --git a/fun/watchblob/main_test.go b/fun/watchblob/main_test.go
new file mode 100644
index 0000000000..1af52d0cd4
--- /dev/null
+++ b/fun/watchblob/main_test.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+	"encoding/xml"
+	"reflect"
+	"testing"
+)
+
+func TestUnmarshalChallengeRespones(t *testing.T) {
+	var testXml string = `
+<?xml version="1.0" encoding="UTF-8"?>
+<resp>
+  <action>sslvpn_logon</action>
+  <logon_status>4</logon_status>
+  <auth-domain-list>
+    <auth-domain>
+      <name>RADIUS</name>
+    </auth-domain>
+  </auth-domain-list>
+  <logon_id>441</logon_id>
+  <chaStr>Enter Your 6 Digit Passcode </chaStr>
+</resp>`
+
+	var r Resp
+	xml.Unmarshal([]byte(testXml), &r)
+
+	expected := Resp{
+		Action:      "sslvpn_logon",
+		LogonStatus: 4,
+		LogonId:     441,
+		Challenge:   "Enter Your 6 Digit Passcode ",
+	}
+
+	assertEqual(t, expected, r)
+}
+
+func TestUnmarshalLoginError(t *testing.T) {
+	var testXml string = `
+<?xml version="1.0" encoding="UTF-8"?>
+<resp>
+  <action>sslvpn_logon</action>
+  <logon_status>2</logon_status>
+  <auth-domain-list>
+    <auth-domain>
+      <name>RADIUS</name>
+    </auth-domain>
+  </auth-domain-list>
+  <errStr>501</errStr>
+</resp>`
+
+	var r Resp
+	xml.Unmarshal([]byte(testXml), &r)
+
+	expected := Resp{
+		Action:      "sslvpn_logon",
+		LogonStatus: 2,
+		Error:       "501",
+	}
+
+	assertEqual(t, expected, r)
+}
+
+func TestUnmarshalLoginSuccess(t *testing.T) {
+	var testXml string = `
+<?xml version="1.0" encoding="UTF-8"?>
+<resp>
+  <action>sslvpn_logon</action>
+  <logon_status>1</logon_status>
+  <auth-domain-list>
+    <auth-domain>
+      <name>RADIUS</name>
+    </auth-domain>
+  </auth-domain-list>
+</resp>
+`
+	var r Resp
+	xml.Unmarshal([]byte(testXml), &r)
+
+	expected := Resp{
+		Action:      "sslvpn_logon",
+		LogonStatus: 1,
+	}
+
+	assertEqual(t, expected, r)
+}
+
+func assertEqual(t *testing.T, expected interface{}, result interface{}) {
+	if !reflect.DeepEqual(expected, result) {
+		t.Errorf(
+			"Unmarshaled values did not match.\nExpected: %v\nResult: %v\n",
+			expected, result,
+		)
+
+		t.Fail()
+	}
+}
diff --git a/fun/watchblob/urls.go b/fun/watchblob/urls.go
new file mode 100644
index 0000000000..37f65e0fae
--- /dev/null
+++ b/fun/watchblob/urls.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"fmt"
+	"net/url"
+	"strconv"
+)
+
+const urlFormat string = "https://%s%s"
+const uriFormat = "/?%s"
+
+func templateChallengeTriggerUri(username *string, password *string) string {
+	v := url.Values{}
+	v.Set("action", "sslvpn_logon")
+	v.Set("style", "fw_logon_progress.xsl")
+	v.Set("fw_logon_type", "logon")
+	v.Set("fw_domain", "Firebox-DB")
+	v.Set("fw_username", *username)
+	v.Set("fw_password", *password)
+
+	return fmt.Sprintf(uriFormat, v.Encode())
+}
+
+func templateResponseUri(logonId int, token *string) string {
+	v := url.Values{}
+	v.Set("action", "sslvpn_logon")
+	v.Set("style", "fw_logon_progress.xsl")
+	v.Set("fw_logon_type", "response")
+	v.Set("response", *token)
+	v.Set("fw_logon_id", strconv.Itoa(logonId))
+
+	return fmt.Sprintf(uriFormat, v.Encode())
+}
+
+func templateUrl(baseUrl *string, uri string) string {
+	return fmt.Sprintf(urlFormat, *baseUrl, uri)
+}
diff --git a/fun/wcl/default.nix b/fun/wcl/default.nix
new file mode 100644
index 0000000000..e4d9804c80
--- /dev/null
+++ b/fun/wcl/default.nix
@@ -0,0 +1,14 @@
+{ depot, ... }:
+
+depot.nix.buildLisp.program {
+  name = "wc";
+
+  srcs = [
+    ./wc.lisp
+  ];
+
+  deps = with depot.third_party.lisp; [
+    unix-opts
+    iterate
+  ];
+}
diff --git a/fun/wcl/wc.lisp b/fun/wcl/wc.lisp
new file mode 100644
index 0000000000..83c7e8c4f1
--- /dev/null
+++ b/fun/wcl/wc.lisp
@@ -0,0 +1,34 @@
+(defpackage wc
+  (:use #:cl #:iterate)
+  (:export :main))
+(in-package :wc)
+(declaim (optimize (speed 3) (safety 0)))
+
+(defun main ()
+  (let ((filename (cadr (opts:argv)))
+        (space (char-code #\Space))
+        (newline (char-code #\Newline)))
+    (with-open-file (file-stream filename :element-type '(unsigned-byte 8))
+      (iter
+        (for byte in-stream file-stream using #'read-byte)
+        (for previous-byte previous byte)
+        (for is-newline = (eql newline byte))
+
+        ;; Count each byte
+        (sum 1 into bytes)
+
+        ;; Count every newline
+        (counting is-newline into newlines)
+
+        ;; Count every "word", unless the preceding character already
+        ;; was a space or we are at the beginning of the file.
+        (when (or (eql space previous-byte)
+                  (eql newline previous-byte)
+                  (not previous-byte))
+          (next-iteration))
+
+        (counting (or is-newline (eql space byte))
+                  into words)
+
+        (declare (fixnum bytes newlines words))
+        (finally (format t "  ~A ~A ~A ~A~%" newlines words bytes filename))))))
diff --git a/fun/🕰️/OWNERS b/fun/🕰️/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/fun/🕰️/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/fun/🕰️/bin.lisp b/fun/🕰️/bin.lisp
new file mode 100644
index 0000000000..1f5a2b2e05
--- /dev/null
+++ b/fun/🕰️/bin.lisp
@@ -0,0 +1,91 @@
+(defpackage 🕰️.bin
+  (:shadow :describe)
+  (:use :cl :opts)
+  (:import-from :uiop :quit)
+  (:import-from :local-time
+   :now :timestamp-subtimezone :+utc-zone+
+   :*default-timezone* :define-timezone)
+  (:import-from :klatre :format-dottime-offset)
+  (:import-from :🕰️ :⌚)
+  (:export :🚂))
+
+(in-package :🕰️.bin)
+(declaim (optimize (safety 3)))
+
+(opts:define-opts
+  (:name :help
+   :description "Print this help text"
+   :short #\h
+   :long "help")
+  (:name :dot-time
+   :description "Use pseudo dot-time format (implies -u)"
+   :short #\d
+   :long "dot-time")
+  (:name :utc
+   :description "Display time in UTC instead of local time"
+   :short #\u
+   :long "utc")
+  (:name :no-newline
+   :description "Don't print a trailing newline"
+   :short #\n
+   :long "no-newline"))
+
+(defun make-slash-terminated (str)
+  (if (eq (char str (1- (length str))) #\/)
+    str
+    (concatenate 'string str "/")))
+
+; TODO(sterni): upstream this into local-time
+(defun setup-default-timezone ()
+  (let* ((tz (remove #\: (uiop:getenv "TZ") :count 1))
+         (tz-dir (uiop:getenv "TZDIR"))
+         (tz-file (if (and tz tz-dir)
+                    (merge-pathnames
+                      (pathname tz)
+                      (pathname (make-slash-terminated tz-dir)))
+                    (pathname "/etc/localtime"))))
+    (handler-case
+      (define-timezone *default-timezone* tz-file :load t)
+      (t () (setf *default-timezone* +utc-zone+)))))
+
+
+(defun 🚂 ()
+  (let ((ts (now)))
+    (multiple-value-bind (options free-args)
+      (handler-case (opts:get-opts)
+        ; only handle subset of conditions that can happen here
+        (opts:unknown-option (c)
+          (format t "error: unknown option ~s~%" (opts:option c))
+          (quit 100)))
+
+      ; check if we have any free args we don't know what to do with
+      (when (> (length free-args) 0)
+        (write-string "error: unexpected command line argument(s): ")
+        (loop for arg in free-args
+              do (progn (write-string arg) (write-char #\space)))
+        (write-char #\newline)
+        (quit 100))
+
+      ; print help and exit
+      (when (getf options :help)
+        (opts:describe :usage-of "🕰️")
+        (quit 0))
+
+      ; reinit *default-timezone* as it is cached from compilation
+      (setup-default-timezone)
+      ; dot-time implies UTC, naturally
+      (when (getf options :dot-time)
+        (setf (getf options :utc) t))
+      ; print clock face
+      (format t "~A" (⌚ ts (if (getf options :utc)
+                               local-time:+utc-zone+
+                               local-time:*default-timezone*)))
+      ; render dot-time offset if necessary
+      (when (getf options :dot-time)
+        (multiple-value-bind (offset-secs _dst _name)
+          (timestamp-subtimezone ts local-time:*default-timezone*)
+          (write-string
+            (format-dottime-offset (round (/ offset-secs 3600))))))
+      ; write newline if necessary
+      (when (not (getf options :no-newline))
+        (write-char #\newline)))))
diff --git a/fun/🕰️/default.nix b/fun/🕰️/default.nix
new file mode 100644
index 0000000000..2b1a946400
--- /dev/null
+++ b/fun/🕰️/default.nix
@@ -0,0 +1,44 @@
+{ depot, ... }:
+
+let
+  inherit (depot.nix)
+    buildLisp
+    ;
+
+  lib = buildLisp.library {
+    name = "lib🕰️";
+    deps = [
+      depot.third_party.lisp.local-time
+    ];
+
+    srcs = [
+      ./lib.lisp
+    ];
+  };
+
+  bin = buildLisp.program {
+    name = "🕰️";
+    deps = [
+      depot.third_party.lisp.unix-opts
+      depot.lisp.klatre
+      {
+        default = buildLisp.bundled "asdf";
+        sbcl = buildLisp.bundled "uiop";
+      }
+      lib
+    ];
+
+    srcs = [
+      ./bin.lisp
+    ];
+
+    main = "🕰️.bin:🚂";
+
+    brokenOn = [
+      "ecl" # refuses to create non-ASCII paths even on POSIX…
+    ];
+  };
+in
+bin // {
+  inherit lib;
+}
diff --git a/fun/🕰️/lib.lisp b/fun/🕰️/lib.lisp
new file mode 100644
index 0000000000..d23db03374
--- /dev/null
+++ b/fun/🕰️/lib.lisp
@@ -0,0 +1,32 @@
+(defpackage 🕰️
+  (:use :cl)
+  (:import-from :local-time
+   :timestamp-subtimezone :*default-timezone* :sec-of)
+  (:export :⌚))
+
+(in-package :🕰️)
+(declaim (optimize (safety 3)))
+
+(defparameter *clock-emojis*
+  (vector #\🕛 #\🕧   ; 00:00 - 00:30
+          #\🕐 #\🕜   ; 01:00 - 01:30
+          #\🕑 #\🕝   ; 00:00 - 00:30
+          #\🕒 #\🕞   ; 00:00 - 00:30
+          #\🕓 #\🕟   ; 00:00 - 00:30
+          #\🕔 #\🕠   ; 00:00 - 00:30
+          #\🕕 #\🕡   ; 00:00 - 00:30
+          #\🕖 #\🕢   ; 00:00 - 00:30
+          #\🕗 #\🕣   ; 00:00 - 00:30
+          #\🕘 #\🕤   ; 00:00 - 00:30
+          #\🕙 #\🕥   ; 00:00 - 00:30
+          #\🕚 #\🕦)) ; 11:00 - 11:30
+
+(defun ⌚ (timestamp &optional (tz *default-timezone*))
+  "Convert a LOCAL-TIME:TIMESTAMP into the nearest Unicode clock face.
+  Use TZ (which defaults to LOCAL-TIME:*DEFAULT-TIMEZONE*) to determine
+  the UTC offset to factor when determining the local clock face."
+  (let* ((offset (multiple-value-bind (offset-secs _dst _name)
+                   (timestamp-subtimezone timestamp tz)
+                   offset-secs))
+         (secs (+ (sec-of timestamp) offset)))
+    (elt *clock-emojis* (mod (round (/ secs 1800)) 24))))
diff --git a/lisp/dns/README.md b/lisp/dns/README.md
new file mode 100644
index 0000000000..c95d2e9a68
--- /dev/null
+++ b/lisp/dns/README.md
@@ -0,0 +1,75 @@
+dns
+===
+
+This library is a DNS-over-HTTPS client for Common Lisp.
+
+The ambition is to transform it into a fully-featured DNS resolver
+instead of piggy-backing on the HTTPS implementation, but ...
+baby-steps!
+
+Note that there is no Common Lisp HTTP client that fully supports the
+HTTP2 protocol at the moment, so you can not expect this library to
+provide equivalent performance to a native DNS resolver (yet).
+
+## API
+
+The API is kept as simple as it can be.
+
+### Types
+
+The types of this library are implemented as several structs that
+support binary (de-)serialisation via [lisp-binary][].
+
+The existing structs are as follows and directly implement the
+corresponding definitions from [RFC 1035][]:
+
+* `dns-header`
+* `dns-question`
+* `dns-rr`
+* `dns-message`
+
+All relevant field accessors for these structs are exported and can be
+used to inspect query results.
+
+### Functions
+
+All lookup functions are of the type `(function (string &key doh-url)
+(dns-message))` and signal a `dns:doh-error` condition for
+unsuccessful requests.
+
+If `:doh-url` is unspecified, Google's public DNS-over-HTTPS servers
+at [dns.google][https://dns.google] will be used.
+
+Currently implemented lookup functions:
+
+* `lookup-a`
+* `lookup-mx`
+* `lookup-txt`
+
+## Example usage
+
+```lisp
+DNS> (dns-message-answer (lookup-a "git.tazj.in."))
+#(#S(DNS-RR
+     :NAME #S(QNAME :START-AT 29 :NAMES #(12))
+     :TYPE A
+     :CLASS 1
+     :TTL 286
+     :RDLENGTH 4
+     :RDATA #(34 98 120 189)))
+```
+
+## TODO
+
+Various things in this library are currently broken because I only
+implemented it to work for my blog setup, but these things will be
+ironed out.
+
+Most importantly, the following needs to be fixed:
+
+* Each qname *fragment* needs to track its offset, not each qname.
+* The RDATA for a TXT record can have multiple counted strings.
+* qnames should be canonicalised after parsing.
+
+[lisp-binary]: https://github.com/j3pic/lisp-binary
+[RFC 1035]: https://tools.ietf.org/html/rfc1035
diff --git a/lisp/dns/client.lisp b/lisp/dns/client.lisp
new file mode 100644
index 0000000000..cee7bceb54
--- /dev/null
+++ b/lisp/dns/client.lisp
@@ -0,0 +1,85 @@
+;; Implementation of a DoH-client, see RFC 8484 (DNS Queries over
+;; HTTPS (DoH))
+
+(in-package #:dns)
+
+(defvar *doh-base-url* "https://dns.google/resolve"
+  "Base URL of the service providing DNS-over-HTTP(S). Defaults to the
+  Google-hosted API.")
+
+(define-condition doh-error (error)
+  ((query-name :initarg :query-name
+               :reader doh-error-query-name
+               :type string)
+   (query-type :initarg :query-type
+               :reader doh-error-query-type
+               :type string)
+   (doh-url :initarg :doh-url
+            :reader doh-error-doh-url
+            :type string)
+   (status-code :initarg :status-code
+                :reader doh-error-status-code
+                :type integer)
+   (response-body :initarg :response-body
+                  :reader doh-error-response-body
+                  :type (or nil (vector (unsigned-byte 8)) string)))
+
+  (:report (lambda (condition stream)
+             (let ((url (doh-error-doh-url condition))
+                   (status (doh-error-status-code condition))
+                   (body (doh-error-response-body condition)))
+               (format stream "DoH service at '~A' responded with non-success (~A): ~%~%~A"
+                       url status body)))))
+
+(defun lookup-generic (name type doh-url)
+  (multiple-value-bind (body status)
+      (drakma:http-request doh-url
+                           :decode-content t
+                           ;; TODO(tazjin): Figure out why 'want-stream' doesn't work
+                           :parameters `(("type" . ,type)
+                                         ("name" . ,name)
+                                         ("ct" . "application/dns-message")))
+    (if (= 200 status)
+        (dns-message-answer
+         (read-binary 'dns-message (flexi-streams:make-in-memory-input-stream body)))
+
+        (restart-case (error 'doh-error
+                             :query-name name
+                             :query-type type
+                             :doh-url doh-url
+                             :status-code status
+                             :response-body body)
+          (call-with-other-name (new-name)
+            :interactive (lambda () (list (the string (read))))
+            :test (lambda (c) (typep c 'doh-error))
+            (lookup-generic new-name type doh-url))
+
+          (call-with-other-type (new-type)
+            :interactive (lambda () (list (the string (read))))
+            :test (lambda (c) (typep c 'doh-error))
+            (lookup-generic name new-type doh-url))
+
+          (call-with-other-url (new-url)
+            :interactive (lambda () (list (the string (read))))
+            :test (lambda (c) (typep c 'doh-error))
+            (lookup-generic name type new-url))))))
+
+(defun lookup-a (name &key (doh-url *doh-base-url*))
+  "Look up the A records at NAME."
+  (lookup-generic name "A" doh-url))
+
+(defun lookup-txt (name &key (doh-url *doh-base-url*))
+  "Look up the TXT records at NAME."
+  (lookup-generic name "TXT" doh-url))
+
+(defun lookup-mx (name &key (doh-url *doh-base-url*))
+  "Look up the MX records at NAME."
+  (lookup-generic name "MX" doh-url))
+
+(defun lookup-cname (name &key (doh-url *doh-base-url*))
+  "Look up the CNAME records at NAME."
+  (lookup-generic name "CNAME" doh-url))
+
+(defun lookup-ns (name &key (doh-url *doh-base-url*))
+  "Look up the NS records at NAME."
+  (lookup-generic name "NS" doh-url))
diff --git a/lisp/dns/default.nix b/lisp/dns/default.nix
new file mode 100644
index 0000000000..cb2445b460
--- /dev/null
+++ b/lisp/dns/default.nix
@@ -0,0 +1,21 @@
+{ depot, ... }:
+
+depot.nix.buildLisp.library {
+  name = "dns";
+
+  deps = with depot.third_party.lisp; [
+    drakma
+    lisp-binary
+    iterate
+  ];
+
+  srcs = [
+    ./package.lisp
+    ./message.lisp
+    ./client.lisp
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/lisp/dns/message.lisp b/lisp/dns/message.lisp
new file mode 100644
index 0000000000..46243ac8d3
--- /dev/null
+++ b/lisp/dns/message.lisp
@@ -0,0 +1,407 @@
+(in-package :dns)
+
+;; 3.3. Standard RRs
+
+;; The following RR definitions are expected to occur, at least
+;; potentially, in all classes.  In particular, NS, SOA, CNAME, and PTR
+;; will be used in all classes, and have the same format in all classes.
+;; Because their RDATA format is known, all domain names in the RDATA
+;; section of these RRs may be compressed.
+
+;; <domain-name> is a domain name represented as a series of labels, and
+;; terminated by a label with zero length.  <character-string> is a single
+;; length octet followed by that number of characters.  <character-string>
+;; is treated as binary information, and can be up to 256 characters in
+;; length (including the length octet).
+
+
+;; 3.3.11. NS RDATA format
+
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     /                   NSDNAME                     /
+;;     /                                               /
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+
+;; where:
+
+;; NSDNAME         A <domain-name> which specifies a host which should be
+;;                 authoritative for the specified class and domain.
+
+;; NS records cause both the usual additional section processing to locate
+;; a type A record, and, when used in a referral, a special search of the
+;; zone in which they reside for glue information.
+
+;; The NS RR states that the named host should be expected to have a zone
+;; starting at owner name of the specified class.  Note that the class may
+;; not indicate the protocol family which should be used to communicate
+;; with the host, although it is typically a strong hint.  For example,
+;; hosts which are name servers for either Internet (IN) or Hesiod (HS)
+;; class information are normally queried using IN class protocols.
+
+;; 3.3.12. PTR RDATA format
+
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     /                   PTRDNAME                    /
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+
+;; where:
+
+;; PTRDNAME        A <domain-name> which points to some location in the
+;;                 domain name space.
+
+;; PTR records cause no additional section processing.  These RRs are used
+;; in special domains to point to some other location in the domain space.
+;; These records are simple data, and don't imply any special processing
+;; similar to that performed by CNAME, which identifies aliases.  See the
+;; description of the IN-ADDR.ARPA domain for an example.
+
+;; 3.3.13. SOA RDATA format
+
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     /                     MNAME                     /
+;;     /                                               /
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     /                     RNAME                     /
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     |                    SERIAL                     |
+;;     |                                               |
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     |                    REFRESH                    |
+;;     |                                               |
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     |                     RETRY                     |
+;;     |                                               |
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     |                    EXPIRE                     |
+;;     |                                               |
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     |                    MINIMUM                    |
+;;     |                                               |
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+
+;; where:
+
+;; MNAME           The <domain-name> of the name server that was the
+;;                 original or primary source of data for this zone.
+
+;; RNAME           A <domain-name> which specifies the mailbox of the
+;;                 person responsible for this zone.
+
+;; SERIAL          The unsigned 32 bit version number of the original copy
+;;                 of the zone.  Zone transfers preserve this value.  This
+;;                 value wraps and should be compared using sequence space
+;;                 arithmetic.
+
+;; REFRESH         A 32 bit time interval before the zone should be
+;;                 refreshed.
+
+;; RETRY           A 32 bit time interval that should elapse before a
+;;                 failed refresh should be retried.
+
+;; EXPIRE          A 32 bit time value that specifies the upper limit on
+;;                 the time interval that can elapse before the zone is no
+;;                 longer authoritative.
+
+;; MINIMUM         The unsigned 32 bit minimum TTL field that should be
+;;                 exported with any RR from this zone.
+
+;; SOA records cause no additional section processing.
+
+;; All times are in units of seconds.
+
+;; Most of these fields are pertinent only for name server maintenance
+;; operations.  However, MINIMUM is used in all query operations that
+;; retrieve RRs from a zone.  Whenever a RR is sent in a response to a
+;; query, the TTL field is set to the maximum of the TTL field from the RR
+;; and the MINIMUM field in the appropriate SOA.  Thus MINIMUM is a lower
+;; bound on the TTL field for all RRs in a zone.  Note that this use of
+;; MINIMUM should occur when the RRs are copied into the response and not
+;; when the zone is loaded from a master file or via a zone transfer.  The
+;; reason for this provison is to allow future dynamic update facilities to
+;; change the SOA RR with known semantics.
+
+
+;; 3.3.14. TXT RDATA format
+
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+;;     /                   TXT-DATA                    /
+;;     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+
+;; where:
+
+;; TXT-DATA
+
+;; TXT RRs are used to hold descriptive text.  The semantics of the text
+;; depends on the domain where it is found.
+
+(defbinary dns-header (:byte-order :big-endian)
+           ;; A 16 bit identifier assigned by the program that
+           ;; generates any kind of query. This identifier is copied
+           ;; the corresponding reply and can be used by the requester
+           ;; to match up replies to outstanding queries.
+           (id 0 :type 16)
+
+           ;; A one bit field that specifies whether this message is a
+           ;; query (0), or a response (1).
+           (qr 0 :type 1)
+
+           ;; A four bit field that specifies kind of query in this
+           ;; message. This value is set by the originator of a query
+           ;; and copied into the response. The values are:
+           ;;
+           ;; 0               a standard query (QUERY)
+           ;; 1               an inverse query (IQUERY)
+           ;; 2               a server status request (STATUS)
+           ;; 3-15            reserved for future use
+           (opcode 0 :type 4)
+
+           ;; Authoritative Answer - this bit is valid in responses,
+           ;; and specifies that the responding name server is an
+           ;; authority for the domain name in question section.
+           (aa nil :type 1)
+
+           ;; TrunCation - specifies that this message was truncated
+           ;; due to length greater than that permitted on the
+           ;; transmission channel.
+           (tc nil :type 1)
+
+           ;; Recursion Desired - this bit may be set in a query and
+           ;; is copied into the response.  If RD is set, it directs
+           ;; the name server to pursue the query recursively.
+           ;; Recursive query support is optional.
+           (rd nil :type 1)
+
+           ;; Recursion Available - this be is set or cleared in a
+           ;; response, and denotes whether recursive query support is
+           ;; available in the name server.
+           (ra nil :type 1)
+
+           ;; Reserved for future use. Must be zero in all queries and
+           ;; responses.
+           (z 0 :type 3)
+
+           ;; Response code - this 4 bit field is set as part of
+           ;; responses.  The values have the following
+           ;; interpretation:
+           ;; 0               No error condition
+           ;; 1               Format error - The name server was
+           ;;                 unable to interpret the query.
+           ;; 2               Server failure - The name server was
+           ;;                 unable to process this query due to a
+           ;;                 problem with the name server.
+           ;; 3               Name Error - Meaningful only for
+           ;;                 responses from an authoritative name
+           ;;                 server, this code signifies that the
+           ;;                 domain name referenced in the query does
+           ;;                 not exist.
+           ;; 4               Not Implemented - The name server does
+           ;;                 not support the requested kind of query.
+           ;; 5               Refused - The name server refuses to
+           ;;                 perform the specified operation for
+           ;;                 policy reasons.  For example, a name
+           ;;                 server may not wish to provide the
+           ;;                 information to the particular requester,
+           ;;                 or a name server may not wish to perform
+           ;;                 a particular operation (e.g., zone
+           ;;                 transfer) for particular data.
+           ;; 6-15            Reserved for future use.
+           (rcode 0 :type 4)
+
+           ;; an unsigned 16 bit integer specifying the number of
+           ;; entries in the question section.
+           (qdcount 0 :type 16)
+
+           ;; an unsigned 16 bit integer specifying the number of
+           ;; resource records in the answer section.
+           (ancount 0 :type 16)
+
+           ;; an unsigned 16 bit integer specifying the number of name
+           ;; server resource records in the authority records
+           ;; section.
+           (nscount 0 :type 16)
+
+           ;; an unsigned 16 bit integer specifying the number of
+           ;; resource records in the additional records section.
+           (arcount 0 :type 16))
+
+
+;; Representation of DNS QNAMEs.
+;;
+;; A QNAME can be either made up entirely of labels, which is
+;; basically a list of strings, or be terminated with a pointer to an
+;; offset within the original message.
+
+(deftype qname-field ()
+  '(or
+    ;; pointer
+    (unsigned-byte 14)
+    ;; label
+    string))
+
+(defstruct qname
+  (start-at 0 :type (unsigned-byte 14))
+  (names #() :type (vector qname-field)))
+
+;; Domain names in questions and resource records are represented as a
+;; sequence of labels, where each label consists of a length octet
+;; followed by that number of octets.
+;;
+;; The domain name terminates with the zero length octet for the null
+;; label of the root. Note that this field may be an odd number of
+;; octets; no padding is used.
+(declaim (ftype (function (stream) (values qname integer)) read-qname))
+(defun read-qname (stream)
+  "Reads a DNS QNAME from STREAM."
+
+  (let ((start-at (file-position stream)))
+    (iter (for byte next (read-byte stream))
+      ;; Each fragment is collected into this byte vector pre-allocated
+      ;; with the correct size.
+      (for fragment = (make-array byte :element-type '(unsigned-byte 8)
+                                       :fill-pointer 0))
+
+      ;; If the bit sequence (1 1) is encountered at the beginning of
+      ;; the fragment, a qname pointer is being read.
+      (let ((byte-copy byte))
+        (when (equal #b11 (lisp-binary/integer:pop-bits 2 8 byte-copy))
+          (let ((next (read-byte stream)))
+            (lisp-binary/integer:push-bits byte-copy 8 next)
+            (collect next into fragments result-type vector)
+            (sum 2 into size)
+            (finish))))
+
+      ;; Total size is needed, count for each iteration byte, plus its
+      ;; own value.
+      (sum (+ 1 byte) into size)
+      (until (equal byte 0))
+
+      ;; On each iteration, this will interpret the current byte as an
+      ;; unsigned integer and read from STREAM an equivalent amount of
+      ;; times to assemble the current fragment.
+      ;;
+      ;; Advancing the stream like this also ensures that the next
+      ;; iteration occurs on a new fragment or the final terminating
+      ;; byte.
+      (dotimes (_ byte (collect (babel:octets-to-string fragment)
+                         into fragments result-type vector))
+        (vector-push (read-byte stream) fragment))
+
+      (finally (return (values (make-qname :start-at start-at
+                                           :names fragments)
+                               size))))))
+
+(declaim (ftype (function (stream qname)) write-qname))
+(defun write-qname (stream qname)
+  "Write a DNS qname to STREAM."
+
+  ;; Write each fragment starting with its (byte-) length, followed by
+  ;; the bytes.
+  (iter (for fragment in-vector (qname-names qname))
+    (for bytes = (babel:string-to-octets fragment))
+    (write-byte (length bytes) stream)
+    (iter (for byte in-vector bytes)
+      (write-byte byte stream)))
+
+  ;; Always finish off the serialisation with a null-byte!
+  (write-byte 0 stream))
+
+(define-enum dns-type 2
+    (:byte-order :big-endian)
+
+    ;; http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml
+    (A 1)
+    (NS 2)
+    (CNAME 5)
+    (SOA 6)
+    (PTR 12)
+    (MX 15)
+    (TXT 16)
+    (SRV 33)
+    (AAAA 28)
+
+    ;; ANY typically wants SOA, MX, NS and MX
+    (ANY 255))
+
+(defbinary dns-question (:byte-order :big-endian :export t)
+           ;; a domain name represented
+           (qname "" :type (custom :lisp-type qname
+                                   :reader #'read-qname
+                                   :writer #'write-qname))
+
+           ;; a two octet code which specifies the type of the query.
+           (qtype 0 :type dns-type)
+
+           ;; a two octet code that specifies the class of the query. For
+           ;; example, the QCLASS field is IN for the Internet.
+           (qclass 0 :type 16))
+
+(defbinary dns-rr (:byte-order :big-endian :export t)
+           (name nil :type (custom :lisp-type qname
+                                   :reader #'read-qname
+                                   :writer #'write-qname))
+
+           ;; two octets containing one of the RR type codes. This field
+           ;; specifies the meaning of the data in the RDATA field.
+           (type 0 :type dns-type)
+
+           ;; two octets which specify the class of the data in the RDATA
+           ;; field.
+           (class 0 :type 16)
+
+           ;; a 32 bit unsigned integer that specifies the time interval (in
+           ;; seconds) that the resource record may be cached before it should
+           ;; be discarded. Zero values are interpreted to mean that the RR
+           ;; can only be used for the transaction in progress, and should not
+           ;; be cached.
+           (ttl 0 :type 32)
+
+           ;; an unsigned 16 bit integer that specifies the length in octets
+           ;; of the RDATA field.
+           (rdlength 0 :type 16)
+
+           ;; a variable length string of octets that describes the resource.
+           ;; The format of this information varies according to the TYPE and
+           ;; CLASS of the resource record. For example, the if the TYPE is A
+           ;; and the CLASS is IN, the RDATA field is a 4 octet ARPA Internet
+           ;; address.
+           (rdata #() :type (eval (case type
+                                    ;; A 32-bit internet address in its
+                                    ;; canonical representation of 4 integers.
+                                    ((A) '(simple-array (unsigned-byte 8) (4)))
+
+                                    ;; TODO(tazjin): Deal with multiple strings in single RRDATA
+                                    ;; One or more <character-string>s.
+                                    ((TXT) '(counted-string 1))
+
+                                    ;; A <domain-name> which specifies the
+                                    ;; canonical or primary name for the
+                                    ;; owner. The owner name is an alias.
+                                    ((CNAME) '(custom
+                                               :lisp-type qname
+                                               :reader #'read-qname
+                                               :writer #'write-qname))
+
+                                    ;; A <domain-name> which specifies a host
+                                    ;; which should be authoritative for the
+                                    ;; specified class and domain.
+                                    ((NS) '(custom
+                                            :lisp-type qname
+                                            :reader #'read-qname
+                                            :writer #'write-qname))
+                                    (otherwise `(simple-array (unsigned-byte 8) (,rdlength)))))))
+
+(defbinary dns-message (:byte-order :big-endian :export t)
+           (header nil :type dns-header)
+
+           ;; the question for the name server
+           (question #() :type (simple-array dns-question ((dns-header-qdcount header))))
+
+           ;; ;; RRs answering the question
+           ;; (answer #() :type (simple-array (unsigned-byte 8) (16)))
+           (answer #() :type (simple-array dns-rr ((dns-header-ancount header))))
+
+           ;; ;; ;; RRs pointing toward an authority
+           (authority #() :type (simple-array dns-rr ((dns-header-nscount header))))
+
+           ;; ;; RRs holding additional information
+           (additional #() :type (simple-array dns-rr ((dns-header-arcount header)))))
diff --git a/lisp/dns/package.lisp b/lisp/dns/package.lisp
new file mode 100644
index 0000000000..2b8bfaa8bc
--- /dev/null
+++ b/lisp/dns/package.lisp
@@ -0,0 +1,11 @@
+(defpackage #:dns
+  (:documentation "Simple DNS resolver in Common Lisp")
+  (:use #:cl #:iterate #:lisp-binary)
+  (:export
+   ;; Individual lookup functions
+   #:lookup-txt #:lookup-mx #:lookup-cname #:lookup-a #:lookup-ns
+
+   ;; Useful accessors
+   #:dns-message-header #:dns-message-answer #:dns-message-question
+   #:dns-rr-name #:dns-rr-type #:dns-rr-ttl #:dns-rr-rdata
+   #:dns-question-qname #:dns-question-qtype))
diff --git a/lisp/klatre/OWNERS b/lisp/klatre/OWNERS
new file mode 100644
index 0000000000..ce7e0e37ee
--- /dev/null
+++ b/lisp/klatre/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - grfn
diff --git a/lisp/klatre/default.nix b/lisp/klatre/default.nix
new file mode 100644
index 0000000000..2c7bb6490a
--- /dev/null
+++ b/lisp/klatre/default.nix
@@ -0,0 +1,14 @@
+{ depot, ... }:
+
+depot.nix.buildLisp.library {
+  name = "klatre";
+
+  deps = with depot.third_party.lisp; [
+    local-time
+  ];
+
+  srcs = [
+    ./package.lisp
+    ./klatre.lisp
+  ];
+}
diff --git a/lisp/klatre/klatre.lisp b/lisp/klatre/klatre.lisp
new file mode 100644
index 0000000000..79c7259752
--- /dev/null
+++ b/lisp/klatre/klatre.lisp
@@ -0,0 +1,119 @@
+(in-package #:klatre)
+(declaim (optimize (safety 3)))
+
+(defmacro comment (&rest _)
+  (declare (ignore _)))
+
+(defun posp (n) (> n 0))
+
+;;; Sequence utilities
+
+(defun slice (vector start end)
+  (make-array (- end start)
+              :element-type (array-element-type vector)
+              :displaced-to vector
+              :displaced-index-offset start))
+
+(defun chunk-vector (size vector &key start end sharedp)
+  (check-type size (integer 1))
+  (loop
+     with slicer = (if sharedp #'slice #'subseq)
+     and low = (or start 0)
+     and high = (or end (length vector))
+     for s from low below high by size
+     for e from (+ low size) by size
+     collect (funcall slicer vector s (min e high))))
+
+(defun chunk-list/unbounded (size list)
+  (loop
+     for front = list then next
+     for next = (nthcdr size front)
+     collect (ldiff front next)
+     while next))
+
+(defun chunk-list/bounded (size list upper-limit)
+  (loop
+     for front = list then next
+     for next = (nthcdr (min size upper-limit) front)
+     collect (ldiff front next)
+     do (decf upper-limit size)
+     while (and next (plusp upper-limit))))
+
+(defun chunk-list (size list &key (start 0) end)
+  "Returns successive chunks of list of size SIZE, starting at START and ending
+at END."
+  (declare (inline chunk-list/bounded chunk-list/unbounded))
+  (check-type size (integer 1))
+  (let ((list (nthcdr start list)))
+    (when list
+      (if end
+          (chunk-list/bounded size list (- end start))
+          (chunk-list/unbounded size list)))))
+
+(defun mapconcat (func lst sep)
+  "Apply FUNC to each element of LST, and concat the results as strings,
+separated by SEP."
+  (check-type lst cons)
+  (check-type sep (simple-array character (*)))
+  (let ((vs (make-array 0
+                        :element-type 'character
+                        :fill-pointer 0
+                        :adjustable t))
+        (lsep (length sep)))
+    (mapcar #'(lambda (str)
+                (let ((nstr (the (simple-array character (*))
+                                 (funcall func str))))
+                  (dotimes (j (length nstr) j)
+                    (vector-push-extend (char nstr (the fixnum j)) vs))
+                  (dotimes (k lsep k)
+                    (vector-push-extend (char sep (the fixnum k)) vs))))
+                lst)
+    vs))
+
+;;;
+;;; String handling
+;;;
+
+(defparameter dottime-format
+  '((:year 4) #\- (:month 2) #\- (:day 2)
+    #\T
+    (:hour 2) #\· (:min 2))
+  "`:LOCAL-TIME' format specifier for dottime")
+
+(defun format-dottime (timestamp &optional (offset 0))
+  "Return TIMESTAMP formatted as dottime, with a specified offset or +00"
+  (check-type timestamp local-time:timestamp)
+  (concatenate 'string
+    (local-time:format-timestring nil timestamp
+                                  :format dottime-format
+                                  :timezone local-time:+utc-zone+)
+    (format-dottime-offset offset)))
+
+(defun format-dottime-offset (offset)
+  "Render OFFSET in hours in the format specified by dottime."
+  (check-type offset integer)
+  (concatenate 'string
+    ; render sign manually since format prints it after padding
+    (if (>= offset 0) "+" "-")
+    (format nil "~2,'0D" (abs offset))))
+
+(comment
+ (format-dottime (local-time:now))
+ (format-dottime (local-time:now) 2))
+
+(defun try-parse-integer (str)
+  "Attempt to parse STR as an integer, returning nil if it is invalid."
+  (check-type str string)
+  (handler-case (parse-integer str)
+    (#+sbcl sb-int:simple-parse-error
+     #-sbcl parse-error (_) (declare (ignore _)) nil)))
+
+;;;
+;;; Function utilities
+;;;
+
+(defun partial (f &rest args)
+  "Return a function that calls F with ARGS prepended to any remaining
+  arguments"
+  (lambda (&rest more-args)
+    (apply f (append args more-args))))
diff --git a/lisp/klatre/package.lisp b/lisp/klatre/package.lisp
new file mode 100644
index 0000000000..41174bbb3c
--- /dev/null
+++ b/lisp/klatre/package.lisp
@@ -0,0 +1,16 @@
+(defpackage #:klatre
+  (:documentation "Grab-bag utility library for Common Lisp")
+  (:use #:cl)
+  (:export
+   ;; Miscellanious utilities
+   #:comment #:posp
+
+   ;; Sequence functions
+   #:chunk-list #:mapconcat
+
+   ;; String handling
+   #:+dottime-format+ #:format-dottime
+   #:try-parse-integer #:format-dottime-offset
+
+   ;; Function utilities
+   #:partial))
diff --git a/net/alcoholic_jwt/.gitignore b/net/alcoholic_jwt/.gitignore
new file mode 100644
index 0000000000..143b1ca014
--- /dev/null
+++ b/net/alcoholic_jwt/.gitignore
@@ -0,0 +1,4 @@
+
+/target/
+**/*.rs.bk
+Cargo.lock
diff --git a/net/alcoholic_jwt/Cargo.lock b/net/alcoholic_jwt/Cargo.lock
new file mode 100644
index 0000000000..9986cda174
--- /dev/null
+++ b/net/alcoholic_jwt/Cargo.lock
@@ -0,0 +1,197 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "alcoholic_jwt"
+version = "4091.0.0"
+dependencies = [
+ "base64",
+ "openssl",
+ "serde",
+ "serde_derive",
+ "serde_json",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "libc"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "openssl"
+version = "0.10.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
diff --git a/net/alcoholic_jwt/Cargo.toml b/net/alcoholic_jwt/Cargo.toml
new file mode 100644
index 0000000000..b5135d14d1
--- /dev/null
+++ b/net/alcoholic_jwt/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "alcoholic_jwt"
+description = "Library for validation of RS256 JWTs"
+version = "4091.0.0"
+authors = ["Vincent Ambo <tazjin@tvl.su>"]
+keywords = ["jwt", "token", "jwks"]
+categories = ["authentication"]
+license = "GPL-3.0-or-later"
+homepage = "https://code.tvl.fyi/about/net/alcoholic_jwt"
+repository = "https://code.tvl.fyi/depot.git:/net/alcoholic_jwt.git"
+
+[dependencies]
+base64 = "0.13"
+openssl = "0.10"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
diff --git a/net/alcoholic_jwt/LICENSE b/net/alcoholic_jwt/LICENSE
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/net/alcoholic_jwt/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/net/alcoholic_jwt/README.md b/net/alcoholic_jwt/README.md
new file mode 100644
index 0000000000..c6da0ab8da
--- /dev/null
+++ b/net/alcoholic_jwt/README.md
@@ -0,0 +1,72 @@
+alcoholic_jwt
+=============
+
+This is a library for **validation** of **RS256** JWTs using keys from
+a JWKS. Nothing more, nothing less.
+
+RS256 is the most commonly used asymmetric signature mechanism for
+JWTs, encountered in for example [Google][]'s or [Aprila][]'s APIs.
+
+The name of the library stems from the potential side-effects of
+trying to use the other Rust libraries that are made for similar
+purposes.
+
+## Usage overview
+
+You are retrieving JWTs from some authentication provider that uses
+`RS256` signatures and provides its public keys in [JWKS][] format.
+
+Example for a token that provides the key ID used for signing in the
+[`kid` claim][]:
+
+```rust
+extern crate alcoholic_jwt;
+
+use alcoholic_jwt::{JWKS, Validation, validate, token_kid};
+
+// The function implied here would usually perform an HTTP-GET
+// on the JWKS-URL for an authentication provider and deserialize
+// the result into the `alcoholic_jwt::JWKS`-struct.
+let jwks: JWKS = jwks_fetching_function();
+
+let token: String = some_token_fetching_function();
+
+// Several types of built-in validations are provided:
+let validations = vec![
+  Validation::Issuer("auth.test.aprila.no".into()),
+  Validation::SubjectPresent,
+];
+
+// If a JWKS contains multiple keys, the correct KID first
+// needs to be fetched from the token headers.
+let kid = token_kid(&token)
+    .expect("Failed to decode token headers")
+    .expect("No 'kid' claim present in token");
+
+let jwk = jwks.find(&kid).expect("Specified key not found in set");
+
+validate(token, jwk, validations).expect("Token validation has failed!");
+```
+
+## Under the hood
+
+This library aims to only use trustworthy off-the-shelf components to
+do the work. Cryptographic operations are provided by the `openssl`
+crate, JSON-serialisation is provided by `serde_json`.
+
+## Contributing
+
+This project is developed in the [TVL monorepo][depot]. To work on it,
+you can either use a local clone of the entire repository or clone
+just the `alcoholic_jwt` subtree:
+
+    https://code.tvl.fyi/depot.git:/net/alcoholic_jwt.git
+
+Please follow the TVL [contribution guidelines][contributing].
+
+[Google]: https://www.google.com/
+[Aprila]: https://www.aprila.no/
+[JWKS]: https://tools.ietf.org/html/rfc7517
+[`kid` claim]: https://tools.ietf.org/html/rfc7515#section-4.1.4
+[depot]: https://code.tvl.fyi/
+[contributing]: https://code.tvl.fyi/about/docs/CONTRIBUTING.md
diff --git a/net/alcoholic_jwt/default.nix b/net/alcoholic_jwt/default.nix
new file mode 100644
index 0000000000..c6b84fb7ab
--- /dev/null
+++ b/net/alcoholic_jwt/default.nix
@@ -0,0 +1,9 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  buildInputs = with pkgs; [
+    openssl
+    pkgconfig
+  ];
+}
diff --git a/net/alcoholic_jwt/src/lib.rs b/net/alcoholic_jwt/src/lib.rs
new file mode 100644
index 0000000000..4a3ece8af1
--- /dev/null
+++ b/net/alcoholic_jwt/src/lib.rs
@@ -0,0 +1,509 @@
+// Copyright (C) 2019-2022 The TVL Community
+//
+// alcoholic_jwt is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Implements a library for for **validation** of **RS256** JWTs
+//! using keys from a JWKS. Nothing more, nothing less.
+//!
+//! The name of the library stems from the potential side-effects of
+//! trying to use the other Rust libraries that are made for similar
+//! purposes.
+//!
+//! This library is specifically aimed at developers that consume
+//! tokens from services which provide their RSA public keys in
+//! [JWKS][] format.
+//!
+//! ## Usage example (token with `kid`-claim)
+//!
+//! ```rust
+//! # extern crate serde_json;
+//! extern crate alcoholic_jwt;
+//!
+//! use alcoholic_jwt::{JWKS, Validation, validate, token_kid};
+//!
+//! # fn some_token_fetching_function() -> &'static str {
+//! #   "eyJraWQiOiI4ckRxOFB3MEZaY2FvWFdURVZRbzcrVGYyWXpTTDFmQnhOS1BDZWJhYWk0PSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoLnRlc3QuYXByaWxhLm5vIiwiaWF0IjoxNTM2MDUwNjkzLCJleHAiOjE1MzYwNTQyOTMsInN1YiI6IjQyIiwiZXh0Ijoic21va2V0ZXN0IiwicHJ2IjoiYXJpc3RpIiwic2NwIjoicHJvY2VzcyJ9.gOLsv98109qLkmRK6Dn7WWRHLW7o8W78WZcWvFZoxPLzVO0qvRXXRLYc9h5chpfvcWreLZ4f1cOdvxv31_qnCRSQQPOeQ7r7hj_sPEDzhKjk-q2aoNHaGGJg1vabI--9EFkFsGQfoS7UbMMssS44dgR68XEnKtjn0Vys-Vzbvz_CBSCH6yQhRLik2SU2jR2L7BoFvh4LGZ6EKoQWzm8Z-CHXLGLUs4Hp5aPhF46dGzgAzwlPFW4t9G4DciX1uB4vv1XnfTc5wqJch6ltjKMde1GZwLR757a8dJSBcmGWze3UNE2YH_VLD7NCwH2kkqr3gh8rn7lWKG4AUIYPxsw9CB"
+//! # }
+//!
+//! # fn jwks_fetching_function() -> JWKS {
+//! #   let jwks_json = "{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"kid\":\"8rDq8Pw0FZcaoXWTEVQo7+Tf2YzSL1fBxNKPCebaai4=\",\"n\":\"l4UTgk1zr-8C8utt0E57DtBV6qqAPWzVRrIuQS2j0_hp2CviaNl5XzGRDnB8gwk0Hx95YOhJupAe6RNq5ok3fDdxL7DLvppJNRLz3Ag9CsmDLcbXgNEQys33fBJaPw1v3GcaFC4tisU5p-o1f5RfWwvwdBtdBfGiwT1GRvbc5sFx6M4iYjg9uv1lNKW60PqSJW4iDYrfqzZmB0zF1SJ0BL_rnQZ1Wi_UkFmNe9arM8W9tI9T3Ie59HITFuyVSTCt6qQEtSfa1e5PiBaVuV3qoFI2jPBiVZQ6LPGBWEDyz4QtrHLdECPPoTF30NN6TSVwwlRbCuUUrdNdXdjYe2dMFQ\",\"e\":\"DhaD5zC7mzaDvHO192wKT_9sfsVmdy8w8T8C9VG17_b1jG2srd3cmc6Ycw-0blDf53Wrpi9-KGZXKHX6_uIuJK249WhkP7N1SHrTJxO0sUJ8AhK482PLF09Qtu6cUfJqY1X1y1S2vACJZItU4Vjr3YAfiVGQXeA8frAf7Sm4O1CBStCyg6yCcIbGojII0jfh2vSB-GD9ok1F69Nmk-R-bClyqMCV_Oq-5a0gqClVS8pDyGYMgKTww2RHgZaFSUcG13KeLMQsG2UOB2OjSC8FkOXK00NBlAjU3d0Vv-IamaLIszO7FQBY3Oh0uxNOvIE9ofQyCOpB-xIK6V9CTTphxw\"}]}";
+//! #   serde_json::from_str(jwks_json).unwrap()
+//! # }
+//! #
+//! // The function implied here would usually perform an HTTP-GET
+//! // on the JWKS-URL for an authentication provider and deserialize
+//! // the result into the `alcoholic_jwt::JWKS`-struct.
+//! let jwks: JWKS = jwks_fetching_function();
+//!
+//! let token = some_token_fetching_function();
+//!
+//! // Several types of built-in validations are provided:
+//! let validations = vec![
+//!   Validation::Issuer("auth.test.aprila.no".into()),
+//!   Validation::SubjectPresent,
+//! ];
+//!
+//! // If a JWKS contains multiple keys, the correct KID first
+//! // needs to be fetched from the token headers.
+//! let kid = token_kid(&token)
+//!     .expect("Failed to decode token headers")
+//!     .expect("No 'kid' claim present in token");
+//!
+//! let jwk = jwks.find(&kid).expect("Specified key not found in set");
+//!
+//! validate(token, jwk, validations).expect("Token validation has failed!");
+//! ```
+//!
+//! [JWKS]: https://tools.ietf.org/html/rfc7517
+
+#[macro_use]
+extern crate serde_derive;
+
+extern crate base64;
+extern crate openssl;
+extern crate serde;
+extern crate serde_json;
+
+use base64::{Config, DecodeError, URL_SAFE_NO_PAD};
+use openssl::bn::BigNum;
+use openssl::error::ErrorStack;
+use openssl::hash::MessageDigest;
+use openssl::pkey::{PKey, Public};
+use openssl::rsa::Rsa;
+use openssl::sign::Verifier;
+use serde::de::DeserializeOwned;
+use serde_json::Value;
+use std::error::Error;
+use std::fmt::{self, Display};
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+#[cfg(test)]
+mod tests;
+
+/// URL-safe character set without padding that allows trailing bits,
+/// which appear in some JWT implementations.
+///
+/// Note: The functions on `base64::Config` are not marked `const`,
+/// and the constructors are not exported, which is why this is
+/// implemented as a function.
+fn jwt_forgiving() -> Config {
+    URL_SAFE_NO_PAD.decode_allow_trailing_bits(true)
+}
+
+/// JWT algorithm used. The only supported algorithm is currently
+/// RS256.
+#[derive(Clone, Deserialize, Debug)]
+enum KeyAlgorithm {
+    RS256,
+}
+
+/// Type of key contained in a JWT. The only supported key type is
+/// currently RSA.
+#[derive(Clone, Deserialize, Debug)]
+enum KeyType {
+    RSA,
+}
+
+/// Representation of a single JSON Web Key. See [RFC
+/// 7517](https://tools.ietf.org/html/rfc7517#section-4).
+#[allow(dead_code)] // kty & alg only constrain deserialisation, but aren't used
+#[derive(Clone, Debug, Deserialize)]
+pub struct JWK {
+    kty: KeyType,
+    alg: Option<KeyAlgorithm>,
+    kid: Option<String>,
+
+    // Shared modulus
+    n: String,
+
+    // Public key exponent
+    e: String,
+}
+
+/// Representation of a set of JSON Web Keys. See [RFC
+/// 7517](https://tools.ietf.org/html/rfc7517#section-5).
+#[derive(Clone, Debug, Deserialize)]
+pub struct JWKS {
+    // This is a vector instead of some kind of map-like structure
+    // because key IDs are in fact optional.
+    //
+    // Technically having multiple keys with the same KID would not
+    // violate the JWKS-definition either, but behaviour in that case
+    // is unspecified.
+    keys: Vec<JWK>,
+}
+
+impl JWKS {
+    /// Attempt to find a JWK by its key ID.
+    pub fn find(&self, kid: &str) -> Option<&JWK> {
+        self.keys.iter().find(|jwk| jwk.kid == Some(kid.into()))
+    }
+}
+
+/// Representation of an undecoded JSON Web Token. See [RFC
+/// 7519](https://tools.ietf.org/html/rfc7519).
+struct JWT<'a>(&'a str);
+
+/// Representation of a decoded and validated JSON Web Token.
+///
+/// Specific claim fields are only decoded internally in the library
+/// for validation purposes, while it is generally up to the consumer
+/// of the validated JWT what structure they would like to impose.
+pub struct ValidJWT {
+    /// JOSE header of the JSON Web Token. Certain fields are
+    /// guaranteed to be present in this header, consult section 5 of
+    /// RFC7519 for more information.
+    pub headers: Value,
+
+    /// Claims (i.e. primary data) contained in the JSON Web Token.
+    /// While there are several registered and recommended headers
+    /// (consult section 4.1 of RFC7519), the presence of no field is
+    /// guaranteed in these.
+    pub claims: Value,
+}
+
+/// Possible token claim validations. This enumeration only covers
+/// common use-cases, for other types of validations the user is
+/// encouraged to inspect the claim set manually.
+pub enum Validation {
+    /// Validate that the issuer ("iss") claim matches a specified
+    /// value.
+    Issuer(String),
+
+    /// Validate that the audience ("aud") claim matches a specified
+    /// value.
+    Audience(String),
+
+    /// Validate that a subject value is present.
+    SubjectPresent,
+
+    /// Validate that the expiry time of the token ("exp"-claim) has
+    /// not yet been reached.
+    NotExpired,
+}
+
+/// Possible results of a token validation.
+#[derive(Debug)]
+pub enum ValidationError {
+    /// Invalid number of token components (not a JWT?)
+    InvalidComponents,
+
+    /// Token segments had invalid base64-encoding.
+    InvalidBase64(DecodeError),
+
+    /// Decoding of the provided JWK failed.
+    InvalidJWK,
+
+    /// Signature validation failed, i.e. because of a non-matching
+    /// public key.
+    InvalidSignature,
+
+    /// An OpenSSL operation failed along the way at a point at which
+    /// a more specific error variant could not be constructed.
+    OpenSSL(ErrorStack),
+
+    /// JSON decoding into a provided type failed.
+    JSON(serde_json::Error),
+
+    /// One or more claim validations failed. This variant contains
+    /// human-readable validation errors.
+    InvalidClaims(Vec<&'static str>),
+}
+
+impl Error for ValidationError {
+    fn source(&self) -> Option<&(dyn Error + 'static)> {
+        match self {
+            ValidationError::InvalidBase64(e) => Some(e),
+            ValidationError::OpenSSL(e) => Some(e),
+            ValidationError::JSON(e) => Some(e),
+            ValidationError::InvalidComponents
+            | ValidationError::InvalidJWK
+            | ValidationError::InvalidSignature
+            | ValidationError::InvalidClaims(_) => None,
+        }
+    }
+}
+
+impl Display for ValidationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ValidationError::InvalidComponents => {
+                f.write_str("Invalid number of token components in JWT")
+            }
+            ValidationError::InvalidBase64(_) => f.write_str("Invalid Base64 encoding in JWT"),
+            ValidationError::InvalidJWK => f.write_str("JWK decoding failed"),
+            ValidationError::InvalidSignature => f.write_str("JWT signature validation failed"),
+            ValidationError::OpenSSL(e) => write!(f, "SSL error: {}", e),
+            ValidationError::JSON(e) => write!(f, "JSON error: {}", e),
+            ValidationError::InvalidClaims(errs) => {
+                write!(f, "Invalid claims: {}", errs.join(", "))
+            }
+        }
+    }
+}
+
+type JWTResult<T> = Result<T, ValidationError>;
+
+impl From<ErrorStack> for ValidationError {
+    fn from(err: ErrorStack) -> Self {
+        ValidationError::OpenSSL(err)
+    }
+}
+
+impl From<serde_json::Error> for ValidationError {
+    fn from(err: serde_json::Error) -> Self {
+        ValidationError::JSON(err)
+    }
+}
+
+impl From<DecodeError> for ValidationError {
+    fn from(err: DecodeError) -> Self {
+        ValidationError::InvalidBase64(err)
+    }
+}
+
+/// Attempt to extract the `kid`-claim out of a JWT's header claims.
+///
+/// This function is normally used when a token provider has multiple
+/// public keys in rotation at the same time that could all still have
+/// valid tokens issued under them.
+///
+/// This is only safe if the key set containing the currently allowed
+/// key IDs is fetched from a trusted source.
+pub fn token_kid(token: &str) -> JWTResult<Option<String>> {
+    // Fetch the header component of the JWT by splitting it out and
+    // dismissing the rest.
+    let parts: Vec<&str> = token.splitn(2, '.').collect();
+    if parts.len() != 2 {
+        return Err(ValidationError::InvalidComponents);
+    }
+
+    // Decode only the first part of the token into a specialised
+    // representation:
+    #[derive(Deserialize)]
+    struct KidOnly {
+        kid: Option<String>,
+    }
+
+    let kid_only: KidOnly = deserialize_part(parts[0])?;
+
+    Ok(kid_only.kid)
+}
+
+/// Validate the signature of a JSON Web Token and optionally apply
+/// claim validations. Signatures are always verified before claims,
+/// and if a signature verification passes *all* claim validations are
+/// run and returned.
+///
+/// If validation succeeds a representation of the token is returned
+/// that contains the header and claims as simple JSON values.
+///
+/// It is the user's task to ensure that the correct JWK is passed in
+/// for validation.
+pub fn validate(token: &str, jwk: &JWK, validations: Vec<Validation>) -> JWTResult<ValidJWT> {
+    let jwt = JWT(token);
+    let public_key = public_key_from_jwk(&jwk)?;
+    validate_jwt_signature(&jwt, public_key)?;
+
+    // Split out all three parts of the JWT this time, deserialising
+    // the first and second as appropriate.
+    let parts: Vec<&str> = jwt.0.splitn(3, '.').collect();
+    if parts.len() != 3 {
+        // This is unlikely considering that validation has already
+        // been performed at this point, but better safe than sorry.
+        return Err(ValidationError::InvalidComponents);
+    }
+
+    // Perform claim validations before constructing the valid token:
+    let partial_claims = deserialize_part(parts[1])?;
+    validate_claims(partial_claims, validations)?;
+
+    let headers = deserialize_part(parts[0])?;
+    let claims = deserialize_part(parts[1])?;
+    let valid_jwt = ValidJWT { headers, claims };
+
+    Ok(valid_jwt)
+}
+
+// Internal implementation
+//
+// The functions in the following section are not part of the public
+// API of this library.
+
+/// Decode a single key fragment (base64-url encoded integer) to an
+/// OpenSSL BigNum.
+fn decode_fragment(fragment: &str) -> JWTResult<BigNum> {
+    let bytes = base64::decode_config(fragment, jwt_forgiving())
+        .map_err(|_| ValidationError::InvalidJWK)?;
+
+    BigNum::from_slice(&bytes).map_err(Into::into)
+}
+
+/// Decode an RSA public key from a JWK by constructing it directly
+/// from the public RSA key fragments.
+fn public_key_from_jwk(jwk: &JWK) -> JWTResult<Rsa<Public>> {
+    let jwk_n = decode_fragment(&jwk.n)?;
+    let jwk_e = decode_fragment(&jwk.e)?;
+    Rsa::from_public_components(jwk_n, jwk_e).map_err(Into::into)
+}
+
+/// Decode a base64-URL encoded string and deserialise the resulting
+/// JSON.
+fn deserialize_part<T: DeserializeOwned>(part: &str) -> JWTResult<T> {
+    let json = base64::decode_config(part, jwt_forgiving())?;
+    serde_json::from_slice(&json).map_err(Into::into)
+}
+
+/// Validate the signature on a JWT using a provided public key.
+///
+/// A JWT is made up of three components (headers, claims, signature)
+/// - only the first two are part of the signed data.
+fn validate_jwt_signature(jwt: &JWT, key: Rsa<Public>) -> JWTResult<()> {
+    let key = PKey::from_rsa(key)?;
+    let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
+
+    // Split the token from the back to a maximum of two elements.
+    // There are technically three components using the same separator
+    // ('.'), but we are interested in the first two together and
+    // splitting them is unnecessary.
+    let token_parts: Vec<&str> = jwt.0.rsplitn(2, '.').collect();
+    if token_parts.len() != 2 {
+        return Err(ValidationError::InvalidComponents);
+    }
+
+    // Second element of the vector will be the signed payload.
+    let data = token_parts[1];
+
+    // First element of the vector will be the (encoded) signature.
+    let sig_b64 = token_parts[0];
+    let sig = base64::decode_config(sig_b64, jwt_forgiving())?;
+
+    // Verify signature by inserting the payload data and checking it
+    // against the decoded signature.
+    verifier.update(data.as_bytes())?;
+
+    match verifier.verify(&sig)? {
+        true => Ok(()),
+        false => Err(ValidationError::InvalidSignature),
+    }
+}
+
+/// Internal helper enum for PartialClaims that supports single or
+/// multiple audiences
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum Audience {
+    Single(String),
+    Multi(Vec<String>),
+}
+
+/// Internal helper struct for claims that are relevant for claim
+/// validations.
+#[derive(Deserialize)]
+struct PartialClaims {
+    aud: Option<Audience>,
+    iss: Option<String>,
+    sub: Option<String>,
+    exp: Option<u64>,
+}
+
+/// Apply a single validation to the claim set of a token.
+fn apply_validation(claims: &PartialClaims, validation: Validation) -> Result<(), &'static str> {
+    match validation {
+        // Validate that an 'iss' claim is present and matches the
+        // supplied value.
+        Validation::Issuer(iss) => match claims.iss {
+            None => Err("'iss' claim is missing"),
+            Some(ref claim) => {
+                if *claim == iss {
+                    Ok(())
+                } else {
+                    Err("'iss' claim does not match")
+                }
+            }
+        },
+
+        // Validate that an 'aud' claim is present and matches the
+        // supplied value.
+        Validation::Audience(aud) => match claims.aud {
+            None => Err("'aud' claim is missing"),
+            Some(Audience::Single(ref claim)) => {
+                if *claim == aud {
+                    Ok(())
+                } else {
+                    Err("'aud' claim does not match")
+                }
+            }
+            Some(Audience::Multi(ref claims)) => {
+                if claims.contains(&aud) {
+                    Ok(())
+                } else {
+                    Err("'aud' claim does not match")
+                }
+            }
+        },
+
+        Validation::SubjectPresent => match claims.sub {
+            Some(_) => Ok(()),
+            None => Err("'sub' claim is missing"),
+        },
+
+        Validation::NotExpired => match claims.exp {
+            None => Err("'exp' claim is missing"),
+            Some(exp) => {
+                // Determine the current timestamp in seconds since
+                // the UNIX epoch.
+                let now = SystemTime::now()
+                    .duration_since(UNIX_EPOCH)
+                    // this is an unrecoverable, critical error. There
+                    // aren't many ways this can occur, other than
+                    // system time being set into the far future or
+                    // this library being used in some sort of future
+                    // museum.
+                    .expect("system time is likely incorrect");
+
+                // Convert the expiry time (which is also in epoch
+                // seconds) to a duration.
+                let exp_duration = Duration::from_secs(exp);
+
+                // The token has not expired if the expiry duration is
+                // larger than (i.e. in the future from) the current
+                // time.
+                if exp_duration > now {
+                    Ok(())
+                } else {
+                    Err("token has expired")
+                }
+            }
+        },
+    }
+}
+
+/// Apply all requested validations to a partial claim set.
+fn validate_claims(claims: PartialClaims, validations: Vec<Validation>) -> JWTResult<()> {
+    let validation_errors: Vec<_> = validations
+        .into_iter()
+        .map(|v| apply_validation(&claims, v))
+        .filter_map(|result| match result {
+            Ok(_) => None,
+            Err(err) => Some(err),
+        })
+        .collect();
+
+    if validation_errors.is_empty() {
+        Ok(())
+    } else {
+        Err(ValidationError::InvalidClaims(validation_errors))
+    }
+}
diff --git a/net/alcoholic_jwt/src/tests.rs b/net/alcoholic_jwt/src/tests.rs
new file mode 100644
index 0000000000..c264bb5a13
--- /dev/null
+++ b/net/alcoholic_jwt/src/tests.rs
@@ -0,0 +1,69 @@
+// Copyright (C) 2019-2022 The TVL Community
+//
+// alcoholic_jwt is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use super::*;
+
+#[test]
+fn test_fragment_decoding() {
+    let fragment = "ngRRjNbXgPW29oNtF0JgsyyfTwPyEL0u_X16s453X2AOc33XGFxVKLEQ7R_TiMenaKcr-tPifYqgps_deyi0XOr4I3SOdOMtAVKDZJCANe--CANOHZb-meIfjKhCHisvT90fm5Apd6qPRVsXsZ7A8pmClZHKM5fwZUkBv8NsPLm2Xy2sGOZIiwP_7z8m3j0abUzniPQsx2b3xcWimB9vRtshFHN1KgPUf1ALQ5xzLfJnlFkCxC7kmOxKC7_NpQ4kJR_DKzKFV_r3HxTqf-jddHcXIrrMcLQXCSyeLQtLaz7whQ4F-EfL42z4XgwPr4ji3sct2gWL13EqlbE5DDxLKQ";
+    let bignum = decode_fragment(fragment).expect("Failed to decode fragment");
+
+    let expected = "19947781743618558124649689124245117083485690334420160711273532766920651190711502679542723943527557680293732686428091794139998732541701457212387600480039297092835433997837314251024513773285252960725418984381935183495143908023024822433135775773958512751261112853383693442999603704969543668619221464654540065497665889289271044207667765128672709218996183649696030570183970367596949687544839066873508106034650634722970893169823917299050098551447676778961773465887890052852528696684907153295689693676910831376066659456592813140662563597179711588277621736656871685099184755908108451080261403193680966083938080206832839445289";
+    assert_eq!(
+        expected,
+        format!("{}", bignum),
+        "Decoded fragment should match "
+    );
+}
+
+#[test]
+fn test_decode_find_jwks() {
+    let json = "{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"kid\":\"mUjI\\/rIMLLtung35BKZfdbrqtlEAAYJ4JX\\/SKvnLxJc=\",\"n\":\"ngRRjNbXgPW29oNtF0JgsyyfTwPyEL0u_X16s453X2AOc33XGFxVKLEQ7R_TiMenaKcr-tPifYqgps_deyi0XOr4I3SOdOMtAVKDZJCANe--CANOHZb-meIfjKhCHisvT90fm5Apd6qPRVsXsZ7A8pmClZHKM5fwZUkBv8NsPLm2Xy2sGOZIiwP_7z8m3j0abUzniPQsx2b3xcWimB9vRtshFHN1KgPUf1ALQ5xzLfJnlFkCxC7kmOxKC7_NpQ4kJR_DKzKFV_r3HxTqf-jddHcXIrrMcLQXCSyeLQtLaz7whQ4F-EfL42z4XgwPr4ji3sct2gWL13EqlbE5DDxLKQ\",\"e\":\"GK7oLCDbNPAF59LhvyseqcG04hDnPs58qGYolr_HHmaR4lulWJ90ozx6e4Ut363yKG2p9vwvivR5UIC-aLPtqT2qr-OtjhBFzUFVaMGZ6mPCvMKk0AgMYdOHvWTgBSqQtNJTvl1yYLnhcWyoE2fLQhoEbY9qUyCBCEOScXOZRDpnmBtz5I8q5yYMV6a920J24T_IYbxHgkGcEU2SGg-b1cOMD7Rja7vCfV---CQ2pR4leQ0jufzudDoe7z3mziJm-Ihcdrz2Ujy5kPEMdz6R55prJ-ENKrkD_X4u5aSlSRaetwmHS3oAVkjr1JwUNbqnpM-kOqieqHEp8LUmez-Znw\"}]}";
+    let jwks: JWKS = serde_json::from_str(json).expect("Failed to decode JWKS");
+    let jwk = jwks
+        .find("mUjI/rIMLLtung35BKZfdbrqtlEAAYJ4JX/SKvnLxJc=")
+        .expect("Failed to find required JWK");
+
+    public_key_from_jwk(&jwk).expect("Failed to construct public key from JWK");
+}
+
+#[test]
+fn test_token_kid() {
+    let jwt = "eyJraWQiOiI4ckRxOFB3MEZaY2FvWFdURVZRbzcrVGYyWXpTTDFmQnhOS1BDZWJhYWk0PSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoLnRlc3QuYXByaWxhLm5vIiwiaWF0IjoxNTM2MDUwNjkzLCJleHAiOjE1MzYwNTQyOTMsInN1YiI6IjQyIiwiZXh0Ijoic21va2V0ZXN0IiwicHJ2IjoiYXJpc3RpIiwic2NwIjoicHJvY2VzcyJ9.gOLsv98109qLkmRK6Dn7WWRHLW7o8W78WZcWvFZoxPLzVO0qvRXXRLYc9h5chpfvcWreLZ4f1cOdvxv31_qnCRSQQPOeQ7r7hj_sPEDzhKjk-q2aoNHaGGJg1vabI--9EFkFsGQfoS7UbMMssS44dgR68XEnKtjn0Vys-Vzbvz_CBSCH6yQhRLik2SU2jR2L7BoFvh4LGZ6EKoQWzm8Z-CHXLGLUs4Hp5aPhF46dGzgAzwlPFW4t9G4DciX1uB4vv1XnfTc5wqJch6ltjKMde1GZwLR757a8dJSBcmGWze3UNE2YH_VLD7NCwH2kkqr3gh8rn7lWKG4AUIYPxsw9CB";
+
+    let kid = token_kid(&jwt).expect("Failed to extract token KID");
+    assert_eq!(
+        Some("8rDq8Pw0FZcaoXWTEVQo7+Tf2YzSL1fBxNKPCebaai4=".into()),
+        kid,
+        "Extracted KID did not match expected KID"
+    );
+}
+
+#[test]
+fn test_validate_jwt() {
+    let jwks_json = "{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"kid\":\"8rDq8Pw0FZcaoXWTEVQo7+Tf2YzSL1fBxNKPCebaai4=\",\"n\":\"l4UTgk1zr-8C8utt0E57DtBV6qqAPWzVRrIuQS2j0_hp2CviaNl5XzGRDnB8gwk0Hx95YOhJupAe6RNq5ok3fDdxL7DLvppJNRLz3Ag9CsmDLcbXgNEQys33fBJaPw1v3GcaFC4tisU5p-o1f5RfWwvwdBtdBfGiwT1GRvbc5sFx6M4iYjg9uv1lNKW60PqSJW4iDYrfqzZmB0zF1SJ0BL_rnQZ1Wi_UkFmNe9arM8W9tI9T3Ie59HITFuyVSTCt6qQEtSfa1e5PiBaVuV3qoFI2jPBiVZQ6LPGBWEDyz4QtrHLdECPPoTF30NN6TSVwwlRbCuUUrdNdXdjYe2dMFQ\",\"e\":\"DhaD5zC7mzaDvHO192wKT_9sfsVmdy8w8T8C9VG17_b1jG2srd3cmc6Ycw-0blDf53Wrpi9-KGZXKHX6_uIuJK249WhkP7N1SHrTJxO0sUJ8AhK482PLF09Qtu6cUfJqY1X1y1S2vACJZItU4Vjr3YAfiVGQXeA8frAf7Sm4O1CBStCyg6yCcIbGojII0jfh2vSB-GD9ok1F69Nmk-R-bClyqMCV_Oq-5a0gqClVS8pDyGYMgKTww2RHgZaFSUcG13KeLMQsG2UOB2OjSC8FkOXK00NBlAjU3d0Vv-IamaLIszO7FQBY3Oh0uxNOvIE9ofQyCOpB-xIK6V9CTTphxw\"}]}";
+
+    let jwks: JWKS = serde_json::from_str(jwks_json).expect("Failed to decode JWKS");
+
+    let jwk = jwks
+        .find("8rDq8Pw0FZcaoXWTEVQo7+Tf2YzSL1fBxNKPCebaai4=")
+        .expect("Failed to find required JWK");
+
+    let pkey = public_key_from_jwk(&jwk).expect("Failed to construct public key");
+
+    let jwt = JWT("eyJraWQiOiI4ckRxOFB3MEZaY2FvWFdURVZRbzcrVGYyWXpTTDFmQnhOS1BDZWJhYWk0PSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoLnRlc3QuYXByaWxhLm5vIiwiaWF0IjoxNTM2MDUwNjkzLCJleHAiOjE1MzYwNTQyOTMsInN1YiI6IjQyIiwiZXh0Ijoic21va2V0ZXN0IiwicHJ2IjoiYXJpc3RpIiwic2NwIjoicHJvY2VzcyJ9.gOLsv98109qLkmRK6Dn7WWRHLW7o8W78WZcWvFZoxPLzVO0qvRXXRLYc9h5chpfvcWreLZ4f1cOdvxv31_qnCRSQQPOeQ7r7hj_sPEDzhKjk-q2aoNHaGGJg1vabI--9EFkFsGQfoS7UbMMssS44dgR68XEnKtjn0Vys-Vzbvz_CBSCH6yQhRLik2SU2jR2L7BoFvh4LGZ6EKoQWzm8Z-CHXLGLUs4Hp5aPhF46dGzgAzwlPFW4t9G4DciX1uB4vv1XnfTc5wqJch6ltjKMde1GZwLR757a8dJSBcmGWze3UNE2YH_VLD7NCwH2kkqr3gh8rn7lWKG4AUIYPxsw9CB".into());
+
+    validate_jwt_signature(&jwt, pkey).expect("Validation failed unexpectedly");
+}
diff --git a/net/crimp/.gitignore b/net/crimp/.gitignore
new file mode 100644
index 0000000000..693699042b
--- /dev/null
+++ b/net/crimp/.gitignore
@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+Cargo.lock
diff --git a/net/crimp/Cargo.lock b/net/crimp/Cargo.lock
new file mode 100644
index 0000000000..05f33e85b8
--- /dev/null
+++ b/net/crimp/Cargo.lock
@@ -0,0 +1,180 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "crimp"
+version = "0.2.2"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.55+curl-7.83.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e7e15d7610cce1d9752e137625f14e61a28cd45929b6e12e47b50fe154ee2e"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/net/crimp/Cargo.toml b/net/crimp/Cargo.toml
new file mode 100644
index 0000000000..1e529cfadc
--- /dev/null
+++ b/net/crimp/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "crimp"
+description = "Higher-level Rust API for cURL bindings"
+version = "4087.0.0"
+authors = ["Vincent Ambo <tazjin@tvl.su>"]
+keywords = [ "http", "curl" ]
+categories = [ "api-bindings" ]
+license = "GPL-3.0-or-later"
+homepage = "https://code.tvl.fyi/about/net/crimp"
+repository = "https://code.tvl.fyi/depot.git:/net/crimp.git"
+
+
+[features]
+default = [ "json" ]
+json = [ "serde", "serde_json"]
+
+[dependencies]
+curl = "0.4"
+serde = { version = "1.0", optional = true }
+serde_json = { version = "1.0", optional = true }
diff --git a/net/crimp/LICENSE b/net/crimp/LICENSE
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/net/crimp/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/net/crimp/README.md b/net/crimp/README.md
new file mode 100644
index 0000000000..65a9d4293b
--- /dev/null
+++ b/net/crimp/README.md
@@ -0,0 +1,26 @@
+crimp
+=====
+
+[![](https://img.shields.io/crates/v/crimp.svg)](https://crates.io/crates/crimp)
+[![](https://docs.rs/crimp/badge.svg)](https://docs.rs/crimp)
+
+Crimp is an HTTP client interface on top of the [Rust bindings][] to
+cURL.
+
+The documentation for this crate is primarily in the [module
+documentation][]
+
+-------
+
+This project is developed in the [TVL monorepo][depot]. To work on it,
+you can either use a local clone of the entire repository or clone
+just the `crimp` subtree:
+
+    https://code.tvl.fyi/depot.git:/net/crimp.git
+
+Please follow the TVL [contribution guidelines][contributing].
+
+[Rust bindings]: https://docs.rs/curl
+[module documentation]: https://docs.rs/crimp
+[depot]: https://code.tvl.fyi/
+[contributing]: https://code.tvl.fyi/about/docs/CONTRIBUTING.md
diff --git a/net/crimp/default.nix b/net/crimp/default.nix
new file mode 100644
index 0000000000..c6b84fb7ab
--- /dev/null
+++ b/net/crimp/default.nix
@@ -0,0 +1,9 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  buildInputs = with pkgs; [
+    openssl
+    pkgconfig
+  ];
+}
diff --git a/net/crimp/src/lib.rs b/net/crimp/src/lib.rs
new file mode 100644
index 0000000000..4dd4d6c31b
--- /dev/null
+++ b/net/crimp/src/lib.rs
@@ -0,0 +1,537 @@
+// crimp - Higher-level Rust cURL API
+//
+// Copyright (C) 2019 Vincent Ambo
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+//! # crimp
+//!
+//! This library provides a simplified API over the [cURL Rust
+//! bindings][] that resemble that of higher-level libraries such as
+//! [reqwest][]. All calls are synchronous.
+//!
+//! `crimp` is intended to be used in situations where HTTP client
+//! functionality is desired without adding a significant number of
+//! dependencies or sacrificing too much usability.
+//!
+//! Using `crimp` to make HTTP requests is done using a simple
+//! builder-pattern style API. For example, to make a `GET`-request
+//! and print the result to `stdout`:
+//!
+//! ```rust
+//! use crimp::Request;
+//!
+//! let response = Request::get("http://httpbin.org/get")
+//!     .user_agent("crimp test suite")
+//!     .unwrap()
+//!     .send()
+//!     .unwrap()
+//!     .as_string()
+//!     .unwrap();
+//!
+//! println!("Status: {}\nBody: {}", response.status, response.body);
+//! # assert_eq!(response.status, 200);
+//! ```
+//!
+//! If a feature from the underlying cURL library is missing, the
+//! `Request::raw` method can be used as an escape hatch to deal with
+//! the handle directly. Should you find yourself doing this, please
+//! [file an issue][].
+//!
+//! `crimp` does not currently expose functionality for re-using a
+//! cURL Easy handle, meaning that keep-alive of HTTP connections and
+//! the like is not supported.
+//!
+//! ## Cargo features
+//!
+//! All optional features are enabled by default.
+//!
+//! * `json`: Adds `Request::json` and `Response::as_json` methods which can be used for convenient
+//!   serialisation of request/response bodies using `serde_json`. This feature adds a dependency on
+//!   the `serde` and `serde_json` crates.
+//!
+//! ## Initialisation
+//!
+//! It is recommended to call the underlying `curl::init` method
+//! (re-exported as `crimp::init`) when launching your application to
+//! initialise the cURL library. This is not required, but will
+//! otherwise occur the first time a request is made.
+//!
+//! [cURL Rust bindings]: https://docs.rs/curl
+//! [reqwest]: https://docs.rs/reqwest
+//! [file an issue]: https://github.com/tazjin/crimp/issues
+
+extern crate curl;
+
+#[cfg(feature = "json")]
+extern crate serde;
+#[cfg(feature = "json")]
+extern crate serde_json;
+
+pub use curl::init;
+
+use curl::easy::{Auth, Easy, Form, List, ReadError, Transfer, WriteError};
+use std::collections::HashMap;
+use std::io::Write;
+use std::path::Path;
+use std::string::{FromUtf8Error, ToString};
+use std::time::Duration;
+
+#[cfg(feature = "json")]
+use serde::de::DeserializeOwned;
+#[cfg(feature = "json")]
+use serde::Serialize;
+
+#[cfg(test)]
+mod tests;
+
+/// HTTP method to use for the request.
+enum Method {
+    Get,
+    Post,
+    Put,
+    Patch,
+    Delete,
+}
+
+/// Certificate types for client-certificate key pairs.
+pub enum CertType {
+    P12,
+    PEM,
+    DER,
+}
+
+/// Builder structure for an HTTP request.
+///
+/// This is the primary API-type in `crimp`. After creating a new
+/// request its parameters are modified using the various builder
+/// methods until it is consumed by `send()`.
+pub struct Request<'a> {
+    url: &'a str,
+    method: Method,
+    handle: Easy,
+    headers: List,
+    body: Body<'a>,
+}
+
+enum Body<'a> {
+    NoBody,
+    Form(Form),
+
+    Bytes {
+        content_type: &'a str,
+        data: &'a [u8],
+    },
+
+    #[cfg(feature = "json")]
+    Json(Vec<u8>),
+}
+
+/// HTTP responses structure containing response data and headers.
+///
+/// By default the `send()` function of the `Request` structure will
+/// return a `Response<Vec<u8>>`. Convenience helpers exist for
+/// decoding a string via `Response::as_string` or to a
+/// `serde`-compatible type with `Response::as_json` (if the
+/// `json`-feature is enabled).
+#[derive(Debug, PartialEq)]
+pub struct Response<T> {
+    /// HTTP status code of the response.
+    pub status: u32,
+
+    /// HTTP headers returned from the remote.
+    pub headers: HashMap<String, String>,
+
+    /// Body data from the HTTP response.
+    pub body: T,
+}
+
+impl<'a> Request<'a> {
+    /// Initiate an HTTP request with the given method and URL.
+    fn new(method: Method, url: &'a str) -> Self {
+        Request {
+            url,
+            method,
+            handle: Easy::new(),
+            headers: List::new(),
+            body: Body::NoBody,
+        }
+    }
+
+    /// Initiate a GET request with the given URL.
+    pub fn get(url: &'a str) -> Self {
+        Request::new(Method::Get, url)
+    }
+
+    /// Initiate a POST request with the given URL.
+    pub fn post(url: &'a str) -> Self {
+        Request::new(Method::Post, url)
+    }
+
+    /// Initiate a PUT request with the given URL.
+    pub fn put(url: &'a str) -> Self {
+        Request::new(Method::Put, url)
+    }
+
+    /// Initiate a PATCH request with the given URL.
+    pub fn patch(url: &'a str) -> Self {
+        Request::new(Method::Patch, url)
+    }
+
+    /// Initiate a DELETE request with the given URL.
+    pub fn delete(url: &'a str) -> Self {
+        Request::new(Method::Delete, url)
+    }
+
+    /// Add an HTTP header to a request.
+    pub fn header(mut self, k: &str, v: &str) -> Result<Self, curl::Error> {
+        self.headers.append(&format!("{}: {}", k, v))?;
+        Ok(self)
+    }
+
+    /// Set the `User-Agent` for this request. By default this will be
+    /// set to cURL's standard user agent.
+    pub fn user_agent(mut self, agent: &str) -> Result<Self, curl::Error> {
+        self.handle.useragent(agent)?;
+        Ok(self)
+    }
+
+    /// Set the `Authorization` header to a `Bearer` value with the
+    /// supplied token.
+    pub fn bearer_auth(mut self, token: &str) -> Result<Self, curl::Error> {
+        self.headers
+            .append(&format!("Authorization: Bearer {}", token))?;
+        Ok(self)
+    }
+
+    /// Set the `Authorization` header to a basic authentication value
+    /// from the supplied username and password.
+    pub fn basic_auth(mut self, username: &str, password: &str) -> Result<Self, curl::Error> {
+        let mut auth = Auth::new();
+        auth.basic(true);
+        self.handle.username(username)?;
+        self.handle.password(password)?;
+        self.handle.http_auth(&auth)?;
+        Ok(self)
+    }
+
+    /// Configure a TLS client certificate on the request.
+    ///
+    /// Depending on whether the certificate file contains the private
+    /// key or not, calling `tls_client_key` may be required in
+    /// addition.
+    ///
+    /// Consult the documentation for the `ssl_cert` and `ssl_key`
+    /// functions in `curl::easy::Easy2` for details on supported
+    /// formats and defaults.
+    pub fn tls_client_cert<P: AsRef<Path>>(
+        mut self,
+        cert_type: CertType,
+        cert: P,
+    ) -> Result<Self, curl::Error> {
+        self.handle.ssl_cert(cert)?;
+        self.handle.ssl_cert_type(match cert_type {
+            CertType::P12 => "P12",
+            CertType::PEM => "PEM",
+            CertType::DER => "DER",
+        })?;
+
+        Ok(self)
+    }
+
+    /// Configure a TLS client certificate key on the request.
+    ///
+    /// Note that this does **not** need to be called again for
+    /// PKCS12-encoded key pairs which are set via `tls_client_cert`.
+    ///
+    /// Currently only PEM-encoded key files are supported.
+    pub fn tls_client_key<P: AsRef<Path>>(mut self, key: P) -> Result<Self, curl::Error> {
+        self.handle.ssl_key(key)?;
+        Ok(self)
+    }
+
+    /// Configure an encryption password for a TLS client certificate
+    /// key on the request.
+    ///
+    /// This is required in case of an encrypted private key that
+    /// should be used.
+    pub fn tls_key_password(mut self, password: &str) -> Result<Self, curl::Error> {
+        self.handle.key_password(password)?;
+        Ok(self)
+    }
+
+    /// Configure a timeout for the request after which the request
+    /// will be aborted.
+    pub fn timeout(mut self, timeout: Duration) -> Result<Self, curl::Error> {
+        self.handle.timeout(timeout)?;
+        Ok(self)
+    }
+
+    /// Set custom configuration on the cURL `Easy` handle.
+    ///
+    /// This function can be considered an "escape-hatch" from the
+    /// high-level API which lets users access the internal
+    /// `curl::easy::Easy` handle and configure options on it
+    /// directly.
+    ///
+    /// ```
+    /// # use crimp::Request;
+    /// let response = Request::get("https://httpbin.org/get")
+    ///     .with_handle(|mut handle| handle.referer("Example-Referer"))
+    ///     .unwrap()
+    ///     .send()
+    ///     .unwrap();
+    /// #
+    /// # assert!(response.is_success());
+    /// ```
+    pub fn with_handle<F>(mut self, function: F) -> Result<Self, curl::Error>
+    where
+        F: FnOnce(&mut Easy) -> Result<(), curl::Error>,
+    {
+        function(&mut self.handle)?;
+        Ok(self)
+    }
+
+    /// Add a byte-array body to a request using the specified
+    /// `Content-Type`.
+    pub fn body(mut self, content_type: &'a str, data: &'a [u8]) -> Self {
+        self.body = Body::Bytes { data, content_type };
+        self
+    }
+
+    /// Add a form-encoded body to a request using the `curl::Form`
+    /// type.
+    ///
+    /// ```rust
+    /// # extern crate curl;
+    /// # extern crate serde_json;
+    /// # use crimp::*;
+    /// # use serde_json::{Value, json};
+    /// use curl::easy::Form;
+    ///
+    /// let mut form = Form::new();
+    /// form.part("some-name")
+    ///     .contents("some-data".as_bytes())
+    ///     .add()
+    ///     .unwrap();
+    ///
+    /// let response = Request::post("https://httpbin.org/post")
+    ///     .user_agent("crimp test suite")
+    ///     .unwrap()
+    ///     .form(form)
+    ///     .send()
+    ///     .unwrap();
+    /// #
+    /// # assert_eq!(200, response.status, "form POST should succeed");
+    /// # assert_eq!(
+    /// #     response.as_json::<Value>().unwrap().body.get("form").unwrap(),
+    /// #     &json!({"some-name": "some-data"}),
+    /// #     "posted form data should match",
+    /// # );
+    /// ```
+    ///
+    /// See the documentation of `curl::easy::Form` for details on how
+    /// to construct a form body.
+    pub fn form(mut self, form: Form) -> Self {
+        self.body = Body::Form(form);
+        self
+    }
+
+    /// Add a JSON-encoded body from a serializable type.
+    #[cfg(feature = "json")]
+    pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self, serde_json::Error> {
+        let json = serde_json::to_vec(body)?;
+        self.body = Body::Json(json);
+        Ok(self)
+    }
+
+    /// Send the HTTP request and return a response structure
+    /// containing the raw body.
+    pub fn send(mut self) -> Result<Response<Vec<u8>>, curl::Error> {
+        // Configure request basics:
+        self.handle.url(self.url)?;
+
+        match self.method {
+            Method::Get => self.handle.get(true)?,
+            Method::Post => self.handle.post(true)?,
+            Method::Put => self.handle.put(true)?,
+            Method::Patch => self.handle.custom_request("PATCH")?,
+            Method::Delete => self.handle.custom_request("DELETE")?,
+        }
+
+        // Create structures in which to store the response data:
+        let mut headers = HashMap::new();
+        let mut body = vec![];
+
+        // Submit a form value to cURL if it is set and proceed
+        // pretending that there is no body, as the handling of this
+        // type of body happens under-the-hood.
+        if let Body::Form(form) = self.body {
+            self.handle.httppost(form)?;
+            self.body = Body::NoBody;
+        }
+
+        // Optionally set content type if a body payload is configured
+        // and configure the expected body size (or form payload).
+        match self.body {
+            Body::Bytes { content_type, data } => {
+                self.handle.post_field_size(data.len() as u64)?;
+                self.headers
+                    .append(&format!("Content-Type: {}", content_type))?;
+            }
+
+            #[cfg(feature = "json")]
+            Body::Json(ref data) => {
+                self.handle.post_field_size(data.len() as u64)?;
+                self.headers.append("Content-Type: application/json")?;
+            }
+
+            // Do not set content-type header at all if there is no
+            // body, or if the form handler was invoked above.
+            _ => (),
+        };
+
+        // Configure headers on the request:
+        self.handle.http_headers(self.headers)?;
+
+        {
+            // Take a scoped transfer from the Easy handle. This makes it
+            // possible to write data into the above local buffers without
+            // fighting the borrow-checker:
+            let mut transfer = self.handle.transfer();
+
+            // Write the payload if it exists:
+            match self.body {
+                Body::Bytes { data, .. } => chunked_read_function(&mut transfer, data)?,
+
+                #[cfg(feature = "json")]
+                Body::Json(ref json) => chunked_read_function(&mut transfer, json)?,
+
+                // Do nothing if there is no body or if the body is a
+                // form.
+                _ => (),
+            };
+
+            // Read one header per invocation. Request processing is
+            // terminated if any header is malformed:
+            transfer.header_function(|header| {
+                // Headers are expected to be valid UTF-8 data. If they
+                // are not, the conversion is lossy.
+                //
+                // Technically it is legal for HTTP requests to use
+                // different encodings, but we don't interface with such
+                // services for hygienic reasons.
+                let header = String::from_utf8_lossy(header);
+                let split = header.splitn(2, ':').collect::<Vec<_>>();
+
+                // "Malformed" headers are skipped. In most cases this
+                // will only be the HTTP version statement.
+                if split.len() != 2 {
+                    return true;
+                }
+
+                headers.insert(split[0].trim().to_string(), split[1].trim().to_string());
+                true
+            })?;
+
+            // Read the body to the allocated buffer.
+            transfer.write_function(|data| {
+                let len = data.len();
+                body.write_all(data)
+                    .map(|_| len)
+                    .map_err(|_| WriteError::Pause)
+            })?;
+
+            transfer.perform()?;
+        }
+
+        Ok(Response {
+            status: self.handle.response_code()?,
+            headers,
+            body,
+        })
+    }
+}
+
+/// Provide a data chunk potentially larger than cURL's initial write
+/// buffer to the data reading callback by tracking the offset off
+/// already written data.
+///
+/// As we manually set the expected upload size, cURL will call the
+/// read callback repeatedly until it has all the data it needs.
+fn chunked_read_function<'easy, 'data>(
+    transfer: &mut Transfer<'easy, 'data>,
+    data: &'data [u8],
+) -> Result<(), curl::Error> {
+    let mut data = data;
+
+    transfer.read_function(move |mut into| {
+        let written = into.write(data).map_err(|_| ReadError::Abort)?;
+
+        data = &data[written..];
+
+        Ok(written)
+    })
+}
+
+impl<T> Response<T> {
+    /// Check whether the status code of this HTTP response is a
+    /// success (i.e. in the 200-299 range).
+    pub fn is_success(&self) -> bool {
+        self.status >= 200 && self.status < 300
+    }
+
+    /// Check whether a request succeeded using `Request::is_success`
+    /// and let users provide a closure that creates a custom error
+    /// from the request if it did not.
+    ///
+    /// This function exists for convenience to avoid having to write
+    /// repetitive `if !response.is_success() { ... }` blocks.
+    pub fn error_for_status<F, E>(self, closure: F) -> Result<Self, E>
+    where
+        F: FnOnce(Self) -> E,
+    {
+        if !self.is_success() {
+            return Err(closure(self));
+        }
+
+        Ok(self)
+    }
+}
+
+impl Response<Vec<u8>> {
+    /// Attempt to parse the HTTP response body as a UTF-8 encoded
+    /// string.
+    pub fn as_string(self) -> Result<Response<String>, FromUtf8Error> {
+        let body = String::from_utf8(self.body)?;
+
+        Ok(Response {
+            body,
+            status: self.status,
+            headers: self.headers,
+        })
+    }
+
+    /// Attempt to deserialize the HTTP response body from JSON.
+    #[cfg(feature = "json")]
+    pub fn as_json<T: DeserializeOwned>(self) -> Result<Response<T>, serde_json::Error> {
+        let deserialized = serde_json::from_slice(&self.body)?;
+
+        Ok(Response {
+            body: deserialized,
+            status: self.status,
+            headers: self.headers,
+        })
+    }
+}
diff --git a/net/crimp/src/tests.rs b/net/crimp/src/tests.rs
new file mode 100644
index 0000000000..e8e9223ce8
--- /dev/null
+++ b/net/crimp/src/tests.rs
@@ -0,0 +1,185 @@
+// All tests expect an httpbin instance to be available at
+// `http://localhost:4662`.
+//
+// This is easily spun up using Docker by running:
+//
+//    docker run --rm -p 4662:80 kennethreitz/httpbin
+
+use super::*;
+use serde_json::{json, Value};
+
+// These tests check whether the correct HTTP method is used in the
+// requests.
+
+#[test]
+fn test_http_get() {
+    let resp = Request::get("http://127.0.0.1:4662/get")
+        .send()
+        .expect("failed to send request");
+
+    assert!(resp.is_success(), "request should have succeeded");
+}
+
+#[test]
+fn test_http_delete() {
+    let resp = Request::delete("http://127.0.0.1:4662/delete")
+        .send()
+        .expect("failed to send request");
+
+    assert_eq!(200, resp.status, "response status should be 200 OK");
+}
+
+#[test]
+fn test_http_put() {
+    let resp = Request::put("http://127.0.0.1:4662/put")
+        .send()
+        .expect("failed to send request");
+
+    assert_eq!(200, resp.status, "response status should be 200 OK");
+}
+
+#[test]
+fn test_http_patch() {
+    let resp = Request::patch("http://127.0.0.1:4662/patch")
+        .send()
+        .expect("failed to send request");
+
+    assert_eq!(200, resp.status, "response status should be 200 OK");
+}
+
+// These tests perform various requests with different body payloads
+// and verify that those were received correctly by the remote side.
+
+#[test]
+fn test_http_post() {
+    let body = "test body";
+    let response = Request::post("http://127.0.0.1:4662/post")
+        .user_agent("crimp test suite")
+        .expect("failed to set user-agent")
+        .timeout(Duration::from_secs(5))
+        .expect("failed to set request timeout")
+        .body("text/plain", &body.as_bytes())
+        .send()
+        .expect("failed to send request")
+        .as_json::<Value>()
+        .expect("failed to deserialize response");
+
+    let data = response.body;
+
+    assert_eq!(200, response.status, "response status should be 200 OK");
+
+    assert_eq!(
+        data.get("data").unwrap(),
+        &json!("test body"),
+        "test body should have been POSTed"
+    );
+
+    assert_eq!(
+        data.get("headers").unwrap().get("Content-Type").unwrap(),
+        &json!("text/plain"),
+        "Content-Type should be `text/plain`",
+    );
+}
+
+#[cfg(feature = "json")]
+#[test]
+fn test_http_post_json() {
+    let body = json!({
+        "purpose": "testing!"
+    });
+
+    let response = Request::post("http://127.0.0.1:4662/post")
+        .user_agent("crimp test suite")
+        .expect("failed to set user-agent")
+        .timeout(Duration::from_secs(5))
+        .expect("failed to set request timeout")
+        .json(&body)
+        .expect("request serialization failed")
+        .send()
+        .expect("failed to send request")
+        .as_json::<Value>()
+        .expect("failed to deserialize response");
+
+    let data = response.body;
+
+    assert_eq!(200, response.status, "response status should be 200 OK");
+
+    assert_eq!(
+        data.get("json").unwrap(),
+        &body,
+        "test body should have been POSTed"
+    );
+
+    assert_eq!(
+        data.get("headers").unwrap().get("Content-Type").unwrap(),
+        &json!("application/json"),
+        "Content-Type should be `application/json`",
+    );
+}
+
+// Tests for different authentication methods that are supported
+// out-of-the-box:
+
+#[test]
+fn test_bearer_auth() {
+    let response = Request::get("http://127.0.0.1:4662/bearer")
+        .bearer_auth("some-token")
+        .expect("failed to set auth header")
+        .send()
+        .expect("failed to send request");
+
+    assert!(response.is_success(), "authorized request should succeed");
+}
+
+#[test]
+fn test_basic_auth() {
+    let request = Request::get("http://127.0.0.1:4662/basic-auth/alan_watts/oneness");
+
+    let response = request
+        .basic_auth("alan_watts", "oneness")
+        .expect("failed to set auth header")
+        .send()
+        .expect("failed to send request");
+
+    assert!(response.is_success(), "authorized request should succeed");
+}
+
+#[test]
+fn test_large_body() {
+    // By default cURL buffers seem to be 2^16 bytes in size. The test
+    // size is therefore 2^16+1.
+    const BODY_SIZE: usize = 65537;
+
+    let resp = Request::post("http://127.0.0.1:4662/post")
+        .body("application/octet-stream", &[0; BODY_SIZE])
+        .send()
+        .expect("sending request")
+        .as_json::<Value>()
+        .expect("JSON deserialisation");
+
+    // httpbin returns the uploaded data as a string in the `data`
+    // field.
+    let data = resp.body.get("data").unwrap().as_str().unwrap();
+
+    assert_eq!(
+        BODY_SIZE,
+        data.len(),
+        "uploaded data length should be correct"
+    );
+}
+
+// Tests for various other features.
+
+#[test]
+fn test_error_for_status() {
+    let response = Request::get("http://127.0.0.1:4662/patch")
+        .send()
+        .expect("failed to send request")
+        .error_for_status(|resp| format!("Response error code: {}", resp.status));
+
+    assert_eq!(
+        Err("Response error code: 405".into()),
+        response,
+        "returned error should be converted into Result::Err"
+    );
+}
diff --git a/net/stomp_erl/.gitignore b/net/stomp_erl/.gitignore
new file mode 100644
index 0000000000..8e46d5a07f
--- /dev/null
+++ b/net/stomp_erl/.gitignore
@@ -0,0 +1,10 @@
+.eunit
+deps
+*.o
+*.beam
+*.plt
+erl_crash.dump
+ebin
+rel/example_project
+.concrete/DEV_MODE
+.rebar
diff --git a/net/stomp_erl/Makefile b/net/stomp_erl/Makefile
new file mode 100644
index 0000000000..b3bc54673d
--- /dev/null
+++ b/net/stomp_erl/Makefile
@@ -0,0 +1,8 @@
+PROJECT = stomp
+PROJECT_DESCRIPTION = STOMP client for Erlang
+PROJECT_VERSION = 0.1.0
+
+# Whitespace to be used when creating files from templates.
+SP = 4
+
+include erlang.mk
diff --git a/net/stomp_erl/README.md b/net/stomp_erl/README.md
new file mode 100644
index 0000000000..21c95a11a2
--- /dev/null
+++ b/net/stomp_erl/README.md
@@ -0,0 +1,78 @@
+STOMP on Erlang
+===============
+
+`stomp.erl` is a simple Erlang client for the [STOMP protocol][] in version 1.2.
+
+Currently only subscribing to queues is supported.
+
+It provides an application called `stomp` which takes configuration of the form:
+
+```erlang
+[{stomp, #{host     => "stomp-server.somedomain.sexy", % required
+           port     => 61613,                          % optional
+           login    => <<"someuser">>,                 % optional
+           passcode => <<"hunter2>>,                   % optional
+ }}].
+```
+
+## Types
+
+The following types are used in `stomp.erl`, you can include them from
+`stomp.hrl`:
+
+```erlang
+%% Client ack modes, refer to the STOMP protocol documentation
+-type ack_mode() :: client | client_individual | auto.
+
+%% Subscriptions are enumerated from 0
+-type sub_id() :: integer().
+
+%% Message IDs (for acknowledgements) are simple strings. They are
+%% extracted from the 'ack' field of the header in client or client-individual
+%% mode, and from the 'message-id' field in auto mode.
+-type message_id() :: binary().
+
+%% A STOMP message as received from a queue subscription
+-record(stomp_msg, { headers :: #{ binary() => binary() },
+                     body    :: binary() }.
+-type stomp_msg() :: #stomp_msg{}.
+```
+
+Once the application starts it will register a process under the name
+`stomp_worker` and expose the following API:
+
+## Subscribing to a queue
+
+```erlang
+%% Subscribe to a destination, receive the subscription ID
+-spec subscribe(binary(),   % Destination (e.g. <<"/queue/lizards">>)
+                ack_mode(), % Client-acknowledgement mode
+                -> {ok, sub_id()}.
+```
+
+This synchronous call subscribes to a message queue. The `stomp_worker` will
+link itself to the caller and forward received messages as
+`{msg, sub_id(), stomp_msg()}`.
+
+Depending on the acknowledgement mode specified on connecting, the subscriber
+may have to acknowledge receival of messages.
+
+## Acknowledging messages
+
+```erlang
+%% Acknowledge a message ID.
+%% This is not required in auto mode. In client mode it will acknowledge the
+%% received messages up to the ID specified. In client-individual mode every
+%% single message has to be acknowledged.
+-spec ack(sub_id(), message_id()) -> ok.
+
+%% Explicitly "unacknowledge" a message
+-spec nack(sub_id(), message_id()) -> ok.
+```
+
+Both of these calls are asynchronous and will return immediately. Note that in
+the case of the `stomp_worker` crashing before a message acknowledgement is
+handled, the message *may* be delivered again. Your consumer needs to be able to
+handle this.
+
+[STOMP protocol]: https://stomp.github.io/stomp-specification-1.2.html
diff --git a/net/stomp_erl/include/stomp.hrl b/net/stomp_erl/include/stomp.hrl
new file mode 100644
index 0000000000..30c933b563
--- /dev/null
+++ b/net/stomp_erl/include/stomp.hrl
@@ -0,0 +1,22 @@
+%% Client ack modes, refer to the STOMP protocol documentation
+-type ack_mode() :: client | client_individual | auto.
+
+%% Subscriptions are enumerated from 0
+-type sub_id() :: integer().
+
+%% Message IDs (for acknowledgements) are simple strings. They are
+%% extracted from the 'ack' field of the header in client or client-individual
+%% mode, and from the 'message-id' field in auto mode.
+-type message_id() :: binary().
+
+%% A destination can be a queue, or something else.
+%% Example: <<"/queue/lizards">>
+-type destination() :: binary().
+
+%% A STOMP message as received from a queue subscription
+-record(stomp_msg, { headers :: #{ binary() => binary() },
+                     body    :: binary() }).
+-type stomp_msg() :: #stomp_msg{}.
+
+%% STOMP frame components
+-type headers() :: #{binary() => binary()}.
diff --git a/net/stomp_erl/src/stomp.app.src b/net/stomp_erl/src/stomp.app.src
new file mode 100644
index 0000000000..baf0e271d1
--- /dev/null
+++ b/net/stomp_erl/src/stomp.app.src
@@ -0,0 +1,7 @@
+{application, stomp, [{description, "STOMP client for Erlang"},
+                      {vsn, "0.1.0"},
+                      {modules, [stomp_app, stomp_sup, stomp_worker]},
+                      {registered, [stomp_worker]},
+                      {env, []},
+                      {applications, [kernel, stdlib]},
+                      {mod, {stomp_app, []}}]}.
diff --git a/net/stomp_erl/src/stomp_app.erl b/net/stomp_erl/src/stomp_app.erl
new file mode 100644
index 0000000000..2ba3e69f99
--- /dev/null
+++ b/net/stomp_erl/src/stomp_app.erl
@@ -0,0 +1,11 @@
+-module(stomp_app).
+-behaviour(application).
+
+-export([start/2]).
+-export([stop/1]).
+
+start(_Type, _Args) ->
+    stomp_sup:start_link().
+
+stop(_State) ->
+    ok.
diff --git a/net/stomp_erl/src/stomp_sup.erl b/net/stomp_erl/src/stomp_sup.erl
new file mode 100644
index 0000000000..3a298bc9bf
--- /dev/null
+++ b/net/stomp_erl/src/stomp_sup.erl
@@ -0,0 +1,22 @@
+-module(stomp_sup).
+-behaviour(supervisor).
+
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+    Procs = [stomp_spec()],
+    {ok, {{one_for_one, 1, 5}, Procs}}.
+
+%% Private
+
+stomp_spec() ->
+    #{id       => stomp_proc,
+      start    => {stomp_worker, start_link, []},
+      restart  => permanent,
+      shutdown => 5000,
+      type     => worker,
+      module   => [stomp_worker]}.
diff --git a/net/stomp_erl/src/stomp_worker.erl b/net/stomp_erl/src/stomp_worker.erl
new file mode 100644
index 0000000000..80981d37ab
--- /dev/null
+++ b/net/stomp_erl/src/stomp_worker.erl
@@ -0,0 +1,193 @@
+-module(stomp_worker).
+-behaviour(gen_server).
+
+%% API.
+-export([start_link/0]).
+
+%% gen_server.
+-export([init/1]).
+-export([handle_call/3]).
+-export([handle_cast/2]).
+-export([handle_info/2]).
+-export([terminate/2]).
+-export([code_change/3]).
+
+%% Testing
+-compile(export_all).
+
+-include("stomp.hrl").
+
+%% State of a stomp_worker
+-record(state, {connection    :: port(),
+                next_sub      :: sub_id(),
+                subscriptions :: #{ destination() => sub_id() },
+                subscribers   :: #{ destination() => pid() }
+               }).
+-type state() :: #state{}.
+
+%% API implementation
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+    {ok, Pid} = gen_server:start_link(?MODULE, [], []),
+    register(?MODULE, Pid),
+    {ok, Pid}.
+
+%% gen_server implementation
+
+-spec init(any()) -> {ok, state()}.
+init(_Args) ->
+    %% Fetch configuration from app config
+    {ok, Host} = application:get_env(stomp, host),
+    Port = application:get_env(stomp, port, 61613),
+    Login = application:get_env(stomp, login),
+    Pass = application:get_env(stomp, passcode),
+
+    %% Catch exit signals from linked processes (subscribers dying)
+    process_flag(trap_exit, true),
+
+    %% Establish connection
+    {ok, Conn} = connect(Host, Port, Login, Pass),
+
+    {ok, #state{connection = Conn,
+                next_sub   = 0,
+                subscriptions = #{},
+                subscribers = #{}}}.
+
+%% Handle subscription calls
+handle_call({subscribe, Dest, Ack}, From, State) ->
+    %% Subscribe to new destination
+    SubId = State#state.next_sub,
+    ok = subscribe(State#state.connection, SubId, Dest, Ack),
+
+    %% Add subscription and subscriber to state
+    Subscriptions = maps:put(SubId, Dest, State#state.subscriptions),
+    Subscribers = maps:put(SubId, From, State#state.subscribers),
+    NextSub = SubId + 1,
+    NewState = State#state{subscriptions = Subscriptions,
+                           subscribers = Subscribers,
+                           next_sub = NextSub },
+
+    {reply, {ok, SubId}, NewState};
+handle_call(_Req, _From, State) ->
+    {reply, ignored, State}.
+
+handle_info({tcp, Conn, Frame}, State) when Conn =:= State#state.connection ->
+    handle_frame(Frame, State);
+handle_info(_Msg, State) ->
+    {noreply, State}.
+
+%% Unused gen_server callbacks
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%% Private functions
+
+-spec connect(list(), integer(), any(), any()) -> {ok, port()}.
+connect(Host, Port, Login, Pass) ->
+    %% STOMP CONNECT frame
+    Connect = connect_frame(Host, Login, Pass),
+
+    %% TODO: Configurable buffer size
+    %% Frames larger than the user-level buffer will be truncated, so it should
+    %% never be smaller than the largest expected messages.
+    {ok, Socket} = gen_tcp:connect(Host, Port, [binary,
+                                                {packet, line},
+                                                {line_delimiter, $\0},
+                                                {buffer, 262144}]),
+
+    ok = gen_tcp:send(Socket, Connect),
+    {ok, Socket}.
+
+-spec subscribe(port(), sub_id(), destination(), ack_mode()) -> ok.
+subscribe(Socket, Id, Queue, Ack) ->
+    {ok, SubscribeFrame} = subscribe_frame(Id, Queue, Ack),
+    gen_tcp:send(Socket, SubscribeFrame).
+
+%%% Parsing STOMP frames
+
+handle_frame(<<"MESSAGE", "\n", _Frame/binary>>, State) ->
+    {noreply, State};
+handle_frame(Frame, State) ->
+    io:format("Received unknown frame ~p", [Frame]),
+    {noreply, State}.
+
+
+%% Parse out headers into a map
+-spec parse_headers(binary()) -> headers().
+parse_headers(HeadersBin) ->
+    Headers = binary:split(HeadersBin, <<"\n">>, [global]),
+    ToPairs = fun(H, M) -> [K,V | []] = binary:split(H, <<":">>),
+                           maps:put(K, V, M)
+              end,
+    {ok, lists:mapfoldl(ToPairs, #{}, Headers)}.
+
+%%% Making STOMP protocol frames
+
+%% Format a header
+-spec format_header({binary(), binary()}) -> binary().
+format_header({Key, Val}) ->
+    <<Key/binary, ":", Val/binary, "\n">>.
+
+%% Build a single STOMP frame
+-spec make_frame(binary(),
+                 headers(),
+                 binary())
+                -> {ok, iolist()}.
+make_frame(Command, HeaderMap, Body) ->
+    Headers = lists:map(fun format_header/1, maps:to_list(HeaderMap)),
+    Frame = [Command, <<"\n">>, Headers, <<"\n">>, Body, <<0>>],
+    {ok, Frame}.
+
+%%% Default frames
+
+-spec connect_frame(list(), any(), any()) -> iolist().
+connect_frame(Host, {ok, Login}, {ok, Pass}) ->
+    make_frame(<<"CONNECT">>,
+               #{<<"accept-version">> => <<"1.2">>,
+                 <<"host">>           => Host,
+                 <<"login">>          => Login,
+                 <<"passcode">>       => Pass,
+                 <<"heart-beat">>     => <<"0,5000">>},
+               []);
+connect_frame(Host, _Login, _Pass) ->
+    make_frame(<<"CONNECT">>,
+               #{<<"accept-version">> => <<"1.2">>,
+                 <<"host">>           => Host,
+                 %% Expect a server heartbeat every 5 seconds, let the server
+                 %% expect one every 10. We don't actually check this and just
+                 %% echo server heartbeats.
+                 %% TODO: For now the server is told not to expect replies due to
+                 %% a weird behaviour.
+                 <<"heart-beat">>     => <<"0,5000">>},
+               []).
+
+
+-spec subscribe_frame(sub_id(), destination(), ack_mode()) -> iolist().
+subscribe_frame(Id, Queue, Ack) ->
+    make_frame(<<"SUBSCRIBE">>,
+               #{<<"id">>          => integer_to_binary(Id),
+                 <<"destination">> => Queue,
+                 <<"ack">>         => ack_mode_to_binary(Ack)},
+               []).
+
+-spec ack_mode_to_binary(ack_mode()) -> binary().
+ack_mode_to_binary(AckMode) ->
+    case AckMode of
+        auto              -> <<"auto">>;
+        client            -> <<"client">>;
+        client_individual -> <<"client-individual">>
+    end.
+
+%% -spec ack_frame(binary()) -> iolist().
+%% ack_frame(MessageID) ->
+%%     make_frame(<<"ACK">>,
+%%                [{"id", MessageID}],
+%%                []).
diff --git a/nix/OWNERS b/nix/OWNERS
new file mode 100644
index 0000000000..a742d0d22b
--- /dev/null
+++ b/nix/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - Profpatsch
diff --git a/nix/binify/default.nix b/nix/binify/default.nix
new file mode 100644
index 0000000000..a9900caf43
--- /dev/null
+++ b/nix/binify/default.nix
@@ -0,0 +1,16 @@
+{ pkgs, lib, ... }:
+
+# Create a store path where the executable `exe`
+# is linked to $out/bin/${name}.
+# This is useful for e.g. including it as a “package”
+# in `buildInputs` of a shell.nix.
+#
+# For example, if I have the exeutable /nix/store/…-hello,
+# I can make it into /nix/store/…-binify-hello/bin/hello
+# with `binify { exe = …; name = "hello" }`.
+{ exe, name }:
+
+pkgs.runCommandLocal "${name}-bin" { } ''
+  mkdir -p $out/bin
+  ln -sT ${lib.escapeShellArg exe} $out/bin/${lib.escapeShellArg name}
+''
diff --git a/nix/bufCheck/default.nix b/nix/bufCheck/default.nix
new file mode 100644
index 0000000000..039303ba68
--- /dev/null
+++ b/nix/bufCheck/default.nix
@@ -0,0 +1,9 @@
+# Check protobuf syntax and breaking.
+#
+{ depot, pkgs, ... }:
+
+pkgs.writeShellScriptBin "ci-buf-check" ''
+  ${depot.third_party.bufbuild}/bin/buf check lint --input .
+  # Report-only
+  ${depot.third_party.bufbuild}/bin/buf check breaking --input "." --against-input "./.git#branch=canon" || true
+''
diff --git a/nix/buildGo/.skip-subtree b/nix/buildGo/.skip-subtree
new file mode 100644
index 0000000000..8db1f814f6
--- /dev/null
+++ b/nix/buildGo/.skip-subtree
@@ -0,0 +1,2 @@
+Subdirectories of this folder should not be imported since they are
+internal to buildGo.nix and incompatible with readTree.
diff --git a/nix/buildGo/README.md b/nix/buildGo/README.md
new file mode 100644
index 0000000000..37e0c06933
--- /dev/null
+++ b/nix/buildGo/README.md
@@ -0,0 +1,140 @@
+buildGo.nix
+===========
+
+This is an alternative [Nix][] build system for [Go][]. It supports building Go
+libraries and programs, and even automatically generating Protobuf & gRPC
+libraries.
+
+*Note:* This will probably end up being folded into [Nixery][].
+
+## Background
+
+Most language-specific Nix tooling outsources the build to existing
+language-specific build tooling, which essentially means that Nix ends up being
+a wrapper around all sorts of external build systems.
+
+However, systems like [Bazel][] take an alternative approach in which the
+compiler is invoked directly and the composition of programs and libraries stays
+within a single homogeneous build system.
+
+Users don't need to learn per-language build systems and especially for
+companies with large monorepo-setups ([like Google][]) this has huge
+productivity impact.
+
+This project is an attempt to prove that Nix can be used in a similar style to
+build software directly, rather than shelling out to other build systems.
+
+## Example
+
+Given a program layout like this:
+
+```
+.
+├── lib          <-- some library component
+│   ├── bar.go
+│   └── foo.go
+├── api.proto    <-- gRPC API definition
+├── main.go      <-- program implementation
+└── default.nix  <-- build instructions
+```
+
+The contents of `default.nix` could look like this:
+
+```nix
+{ buildGo }:
+
+let
+  api = buildGo.grpc {
+    name  = "someapi";
+    proto = ./api.proto;
+  };
+
+  lib = buildGo.package {
+    name = "somelib";
+    srcs = [
+      ./lib/bar.go
+      ./lib/foo.go
+    ];
+  };
+in buildGo.program {
+  name = "my-program";
+  deps = [ api lib ];
+
+  srcs = [
+    ./main.go
+  ];
+}
+```
+
+(If you don't know how to read Nix, check out [nix-1p][])
+
+## Usage
+
+`buildGo` exposes five different functions:
+
+* `buildGo.program`: Build a Go binary out of the specified source files.
+
+  | parameter | type                    | use                                            | required? |
+  |-----------|-------------------------|------------------------------------------------|-----------|
+  | `name`    | `string`                | Name of the program (and resulting executable) | yes       |
+  | `srcs`    | `list<path>`            | List of paths to source files                  | yes       |
+  | `deps`    | `list<drv>`             | List of dependencies (i.e. other Go libraries) | no        |
+  | `x_defs`  | `attrs<string, string>` | Attribute set of linker vars (i.e. `-X`-flags) | no        |
+
+* `buildGo.package`: Build a Go library out of the specified source files.
+
+  | parameter | type         | use                                            | required? |
+  |-----------|--------------|------------------------------------------------|-----------|
+  | `name`    | `string`     | Name of the library                            | yes       |
+  | `srcs`    | `list<path>` | List of paths to source files                  | yes       |
+  | `deps`    | `list<drv>`  | List of dependencies (i.e. other Go libraries) | no        |
+  | `path`    | `string`     | Go import path for the resulting library       | no        |
+
+* `buildGo.external`: Build an externally defined Go library or program.
+
+  This function performs analysis on the supplied source code (which
+  can use the standard Go tooling layout) and creates a tree of all
+  the packages contained within.
+
+  This exists for compatibility with external libraries that were not
+  defined using buildGo.
+
+  | parameter | type           | use                                           | required? |
+  |-----------|----------------|-----------------------------------------------|-----------|
+  | `path`    | `string`       | Go import path for the resulting package      | yes       |
+  | `src`     | `path`         | Path to the source **directory**              | yes       |
+  | `deps`    | `list<drv>`    | List of dependencies (i.e. other Go packages) | no        |
+
+  For some examples of how `buildGo.external` is used, check out
+  [`proto.nix`](./proto.nix).
+
+* `buildGo.proto`: Build a Go library out of the specified Protobuf definition.
+
+  | parameter   | type        | use                                              | required? |
+  |-------------|-------------|--------------------------------------------------|-----------|
+  | `name`      | `string`    | Name for the resulting library                   | yes       |
+  | `proto`     | `path`      | Path to the Protobuf definition file             | yes       |
+  | `path`      | `string`    | Import path for the resulting Go library         | no        |
+  | `extraDeps` | `list<drv>` | Additional Go dependencies to add to the library | no        |
+
+* `buildGo.grpc`: Build a Go library out of the specified gRPC definition.
+
+  The parameters are identical to `buildGo.proto`.
+
+## Current status
+
+This project is work-in-progress. Crucially it is lacking the following features:
+
+* feature flag parity with Bazel's Go rules
+* documentation building
+* test execution
+
+There are still some open questions around how to structure some of those
+features in Nix.
+
+[Nix]: https://nixos.org/nix/
+[Go]: https://golang.org/
+[Nixery]: https://github.com/google/nixery
+[Bazel]: https://bazel.build/
+[like Google]: https://ai.google/research/pubs/pub45424
+[nix-1p]: https://github.com/tazjin/nix-1p
diff --git a/nix/buildGo/default.nix b/nix/buildGo/default.nix
new file mode 100644
index 0000000000..92951b3cb2
--- /dev/null
+++ b/nix/buildGo/default.nix
@@ -0,0 +1,140 @@
+# Copyright 2019 Google LLC.
+# SPDX-License-Identifier: Apache-2.0
+#
+# buildGo provides Nix functions to build Go packages in the style of Bazel's
+# rules_go.
+
+{ pkgs ? import <nixpkgs> { }
+, ...
+}:
+
+let
+  inherit (builtins)
+    attrNames
+    baseNameOf
+    dirOf
+    elemAt
+    filter
+    listToAttrs
+    map
+    match
+    readDir
+    replaceStrings
+    toString;
+
+  inherit (pkgs) lib go runCommand fetchFromGitHub protobuf symlinkJoin;
+
+  # Helpers for low-level Go compiler invocations
+  spaceOut = lib.concatStringsSep " ";
+
+  includeDepSrc = dep: "-I ${dep}";
+  includeSources = deps: spaceOut (map includeDepSrc deps);
+
+  includeDepLib = dep: "-L ${dep}";
+  includeLibs = deps: spaceOut (map includeDepLib deps);
+
+  srcBasename = src: elemAt (match "([a-z0-9]{32}\-)?(.*\.go)" (baseNameOf src)) 1;
+  srcCopy = path: src: "cp ${src} $out/${path}/${srcBasename src}";
+  srcList = path: srcs: lib.concatStringsSep "\n" (map (srcCopy path) srcs);
+
+  allDeps = deps: lib.unique (lib.flatten (deps ++ (map (d: d.goDeps) deps)));
+
+  xFlags = x_defs: spaceOut (map (k: "-X ${k}=${x_defs."${k}"}") (attrNames x_defs));
+
+  pathToName = p: replaceStrings [ "/" ] [ "_" ] (toString p);
+
+  # Add an `overrideGo` attribute to a function result that works
+  # similar to `overrideAttrs`, but is used specifically for the
+  # arguments passed to Go builders.
+  makeOverridable = f: orig: (f orig) // {
+    overrideGo = new: makeOverridable f (orig // (new orig));
+  };
+
+  # High-level build functions
+
+  # Build a Go program out of the specified files and dependencies.
+  program = { name, srcs, deps ? [ ], x_defs ? { } }:
+    let uniqueDeps = allDeps (map (d: d.gopkg) deps);
+    in runCommand name { } ''
+      ${go}/bin/go tool compile -o ${name}.a -trimpath=$PWD -trimpath=${go} ${includeSources uniqueDeps} ${spaceOut srcs}
+      mkdir -p $out/bin
+      export GOROOT_FINAL=go
+      ${go}/bin/go tool link -o $out/bin/${name} -buildid nix ${xFlags x_defs} ${includeLibs uniqueDeps} ${name}.a
+    '';
+
+  # Build a Go library assembled out of the specified files.
+  #
+  # This outputs both the sources and compiled binary, as both are
+  # needed when downstream packages depend on it.
+  package = { name, srcs, deps ? [ ], path ? name, sfiles ? [ ] }:
+    let
+      uniqueDeps = allDeps (map (d: d.gopkg) deps);
+
+      # The build steps below need to be executed conditionally for Go
+      # assembly if the analyser detected any *.s files.
+      #
+      # This is required for several popular packages (e.g. x/sys).
+      ifAsm = do: lib.optionalString (sfiles != [ ]) do;
+      asmBuild = ifAsm ''
+        ${go}/bin/go tool asm -trimpath $PWD -I $PWD -I ${go}/share/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -gensymabis -o ./symabis ${spaceOut sfiles}
+        ${go}/bin/go tool asm -trimpath $PWD -I $PWD -I ${go}/share/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o ./asm.o ${spaceOut sfiles}
+      '';
+      asmLink = ifAsm "-symabis ./symabis -asmhdr $out/go_asm.h";
+      asmPack = ifAsm ''
+        ${go}/bin/go tool pack r $out/${path}.a ./asm.o
+      '';
+
+      gopkg = (runCommand "golib-${name}" { } ''
+        mkdir -p $out/${path}
+        ${srcList path (map (s: "${s}") srcs)}
+        ${asmBuild}
+        ${go}/bin/go tool compile -pack ${asmLink} -o $out/${path}.a -trimpath=$PWD -trimpath=${go} -p ${path} ${includeSources uniqueDeps} ${spaceOut srcs}
+        ${asmPack}
+      '').overrideAttrs (_: {
+        passthru = {
+          inherit gopkg;
+          goDeps = uniqueDeps;
+          goImportPath = path;
+        };
+      });
+    in
+    gopkg;
+
+  # Build a tree of Go libraries out of an external Go source
+  # directory that follows the standard Go layout and was not built
+  # with buildGo.nix.
+  #
+  # The derivation for each actual package will reside in an attribute
+  # named "gopkg", and an attribute named "gobin" for binaries.
+  external = import ./external { inherit pkgs program package; };
+
+  # Import support libraries needed for protobuf & gRPC support
+  protoLibs = import ./proto.nix {
+    inherit external;
+  };
+
+  # Build a Go library out of the specified protobuf definition.
+  proto = { name, proto, path ? name, goPackage ? name, extraDeps ? [ ] }: (makeOverridable package) {
+    inherit name path;
+    deps = [ protoLibs.goProto.proto.gopkg ] ++ extraDeps;
+    srcs = lib.singleton (runCommand "goproto-${name}.pb.go" { } ''
+      cp ${proto} ${baseNameOf proto}
+      ${protobuf}/bin/protoc --plugin=${protoLibs.goProto.protoc-gen-go.gopkg}/bin/protoc-gen-go \
+        --go_out=plugins=grpc,import_path=${baseNameOf path}:. ${baseNameOf proto}
+      mv ./${goPackage}/*.pb.go $out
+    '');
+  };
+
+  # Build a Go library out of the specified gRPC definition.
+  grpc = args: proto (args // { extraDeps = [ protoLibs.goGrpc.gopkg ]; });
+
+in
+{
+  # Only the high-level builder functions are exposed, but made
+  # overrideable.
+  program = makeOverridable program;
+  package = makeOverridable package;
+  proto = makeOverridable proto;
+  grpc = makeOverridable grpc;
+  external = makeOverridable external;
+}
diff --git a/nix/buildGo/example/default.nix b/nix/buildGo/example/default.nix
new file mode 100644
index 0000000000..08da075e18
--- /dev/null
+++ b/nix/buildGo/example/default.nix
@@ -0,0 +1,48 @@
+# Copyright 2019 Google LLC.
+# SPDX-License-Identifier: Apache-2.0
+
+# This file provides examples for how to use the various builder
+# functions provided by `buildGo`.
+#
+# The features used in the example are not exhaustive, but should give
+# users a quick introduction to how to use buildGo.
+
+let
+  buildGo = import ../default.nix { };
+
+  # Example use of buildGo.package, which creates an importable Go
+  # package from the specified source files.
+  examplePackage = buildGo.package {
+    name = "example";
+    srcs = [
+      ./lib.go
+    ];
+  };
+
+  # Example use of buildGo.proto, which generates a Go library from a
+  # Protobuf definition file.
+  exampleProto = buildGo.proto {
+    name = "exampleproto";
+    proto = ./thing.proto;
+  };
+
+  # Example use of buildGo.program, which builds an executable using
+  # the specified name and dependencies (which in turn must have been
+  # created via buildGo.package etc.)
+in
+buildGo.program {
+  name = "example";
+
+  srcs = [
+    ./main.go
+  ];
+
+  deps = [
+    examplePackage
+    exampleProto
+  ];
+
+  x_defs = {
+    "main.Flag" = "successfully";
+  };
+}
diff --git a/nix/buildGo/example/lib.go b/nix/buildGo/example/lib.go
new file mode 100644
index 0000000000..8a61370e99
--- /dev/null
+++ b/nix/buildGo/example/lib.go
@@ -0,0 +1,9 @@
+// Copyright 2019 Google LLC.
+// SPDX-License-Identifier: Apache-2.0
+
+package example
+
+// UUID returns a totally random, carefully chosen UUID
+func UUID() string {
+	return "3640932f-ad40-4bc9-b45d-f504a0f5910a"
+}
diff --git a/nix/buildGo/example/main.go b/nix/buildGo/example/main.go
new file mode 100644
index 0000000000..bbcedbff87
--- /dev/null
+++ b/nix/buildGo/example/main.go
@@ -0,0 +1,25 @@
+// Copyright 2019 Google LLC.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Package main provides a tiny example program for the Bazel-style
+// Nix build system for Go.
+
+package main
+
+import (
+	"example"
+	"exampleproto"
+	"fmt"
+)
+
+var Flag string = "unsuccessfully"
+
+func main() {
+	thing := exampleproto.Thing{
+		Id:          example.UUID(),
+		KindOfThing: "test thing",
+	}
+
+	fmt.Printf("The thing is a %s with ID %q\n", thing.Id, thing.KindOfThing)
+	fmt.Printf("The flag has been %s set\n", Flag)
+}
diff --git a/nix/buildGo/example/thing.proto b/nix/buildGo/example/thing.proto
new file mode 100644
index 0000000000..0f6d6575e0
--- /dev/null
+++ b/nix/buildGo/example/thing.proto
@@ -0,0 +1,10 @@
+// Copyright 2019 Google LLC.
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package example;
+
+message Thing {
+  string id = 1;
+  string kind_of_thing = 2;
+}
diff --git a/nix/buildGo/external/default.nix b/nix/buildGo/external/default.nix
new file mode 100644
index 0000000000..f713783a58
--- /dev/null
+++ b/nix/buildGo/external/default.nix
@@ -0,0 +1,109 @@
+# Copyright 2019 Google LLC.
+# SPDX-License-Identifier: Apache-2.0
+{ pkgs, program, package }:
+
+let
+  inherit (builtins)
+    elemAt
+    foldl'
+    fromJSON
+    head
+    length
+    listToAttrs
+    readFile
+    replaceStrings
+    tail
+    throw;
+
+  inherit (pkgs) lib runCommand go jq ripgrep;
+
+  pathToName = p: replaceStrings [ "/" ] [ "_" ] (toString p);
+
+  # Collect all non-vendored dependencies from the Go standard library
+  # into a file that can be used to filter them out when processing
+  # dependencies.
+  stdlibPackages = runCommand "stdlib-pkgs.json" { } ''
+    export HOME=$PWD
+    export GOPATH=/dev/null
+    ${go}/bin/go list std | \
+      ${ripgrep}/bin/rg -v 'vendor' | \
+      ${jq}/bin/jq -R '.' | \
+      ${jq}/bin/jq -c -s 'map({key: ., value: true}) | from_entries' \
+      > $out
+  '';
+
+  analyser = program {
+    name = "analyser";
+
+    srcs = [
+      ./main.go
+    ];
+
+    x_defs = {
+      "main.stdlibList" = "${stdlibPackages}";
+    };
+  };
+
+  mkset = path: value:
+    if path == [ ] then { gopkg = value; }
+    else { "${head path}" = mkset (tail path) value; };
+
+  last = l: elemAt l ((length l) - 1);
+
+  toPackage = self: src: path: depMap: entry:
+    let
+      localDeps = map
+        (d: lib.attrByPath (d ++ [ "gopkg" ])
+          (
+            throw "missing local dependency '${lib.concatStringsSep "." d}' in '${path}'"
+          )
+          self)
+        entry.localDeps;
+
+      foreignDeps = map
+        (d: lib.attrByPath [ d.path ]
+          (
+            throw "missing foreign dependency '${d.path}' in '${path}, imported at ${d.position}'"
+          )
+          depMap)
+        entry.foreignDeps;
+
+      args = {
+        srcs = map (f: src + ("/" + f)) entry.files;
+        deps = localDeps ++ foreignDeps;
+      };
+
+      libArgs = args // {
+        name = pathToName entry.name;
+        path = lib.concatStringsSep "/" ([ path ] ++ entry.locator);
+        sfiles = map (f: src + ("/" + f)) entry.sfiles;
+      };
+
+      binArgs = args // {
+        name = (last ((lib.splitString "/" path) ++ entry.locator));
+      };
+    in
+    if entry.isCommand then (program binArgs) else (package libArgs);
+
+in
+{ src, path, deps ? [ ] }:
+let
+  # Build a map of dependencies (from their import paths to their
+  # derivation) so that they can be conditionally imported only in
+  # sub-packages that require them.
+  depMap = listToAttrs (map
+    (d: {
+      name = d.goImportPath;
+      value = d;
+    })
+    (map (d: d.gopkg) deps));
+
+  name = pathToName path;
+  analysisOutput = runCommand "${name}-structure.json" { } ''
+    ${analyser}/bin/analyser -path ${path} -source ${src} > $out
+  '';
+  analysis = fromJSON (readFile analysisOutput);
+in
+lib.fix (self: foldl' lib.recursiveUpdate { } (
+  map (entry: mkset entry.locator (toPackage self src path depMap entry)) analysis
+))
diff --git a/nix/buildGo/external/main.go b/nix/buildGo/external/main.go
new file mode 100644
index 0000000000..a77c43b371
--- /dev/null
+++ b/nix/buildGo/external/main.go
@@ -0,0 +1,201 @@
+// Copyright 2019 Google LLC.
+// SPDX-License-Identifier: Apache-2.0
+
+// This tool analyses external (i.e. not built with `buildGo.nix`) Go
+// packages to determine a build plan that Nix can import.
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"go/build"
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+)
+
+// Path to a JSON file describing all standard library import paths.
+// This file is generated and set here by Nix during the build
+// process.
+var stdlibList string
+
+// pkg describes a single Go package within the specified source
+// directory.
+//
+// Return information includes the local (relative from project root)
+// and external (none-stdlib) dependencies of this package.
+type pkg struct {
+	Name        string       `json:"name"`
+	Locator     []string     `json:"locator"`
+	Files       []string     `json:"files"`
+	SFiles      []string     `json:"sfiles"`
+	LocalDeps   [][]string   `json:"localDeps"`
+	ForeignDeps []foreignDep `json:"foreignDeps"`
+	IsCommand   bool         `json:"isCommand"`
+}
+
+type foreignDep struct {
+	Path string `json:"path"`
+	// filename, column and line number of the import, if known
+	Position string `json:"position"`
+}
+
+// findGoDirs returns a filepath.WalkFunc that identifies all
+// directories that contain Go source code in a certain tree.
+func findGoDirs(at string) ([]string, error) {
+	dirSet := make(map[string]bool)
+
+	err := filepath.Walk(at, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		name := info.Name()
+		// Skip folders that are guaranteed to not be relevant
+		if info.IsDir() && (name == "testdata" || name == ".git") {
+			return filepath.SkipDir
+		}
+
+		// If the current file is a Go file, then the directory is popped
+		// (i.e. marked as a Go directory).
+		if !info.IsDir() && strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") {
+			dirSet[filepath.Dir(path)] = true
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	goDirs := []string{}
+	for k, _ := range dirSet {
+		goDirs = append(goDirs, k)
+	}
+
+	return goDirs, nil
+}
+
+// analysePackage loads and analyses the imports of a single Go
+// package, returning the data that is required by the Nix code to
+// generate a derivation for this package.
+func analysePackage(root, source, importpath string, stdlib map[string]bool) (pkg, error) {
+	ctx := build.Default
+	ctx.CgoEnabled = false
+
+	p, err := ctx.ImportDir(source, build.IgnoreVendor)
+	if err != nil {
+		return pkg{}, err
+	}
+
+	local := [][]string{}
+	foreign := []foreignDep{}
+
+	for _, i := range p.Imports {
+		if stdlib[i] {
+			continue
+		}
+
+		if i == importpath {
+			local = append(local, []string{})
+		} else if strings.HasPrefix(i, importpath+"/") {
+			local = append(local, strings.Split(strings.TrimPrefix(i, importpath+"/"), "/"))
+		} else {
+			// The import positions is a map keyed on the import name.
+			// The value is a list, presumably because an import can appear
+			// multiple times in a package. Let’s just take the first one,
+			// should be enough for a good error message.
+			firstPos := p.ImportPos[i][0].String()
+			foreign = append(foreign, foreignDep{Path: i, Position: firstPos})
+		}
+	}
+
+	prefix := strings.TrimPrefix(source, root+"/")
+
+	locator := []string{}
+	if len(prefix) != len(source) {
+		locator = strings.Split(prefix, "/")
+	} else {
+		// Otherwise, the locator is empty since its the root package and
+		// no prefix should be added to files.
+		prefix = ""
+	}
+
+	files := []string{}
+	for _, f := range p.GoFiles {
+		files = append(files, path.Join(prefix, f))
+	}
+
+	sfiles := []string{}
+	for _, f := range p.SFiles {
+		sfiles = append(sfiles, path.Join(prefix, f))
+	}
+
+	return pkg{
+		Name:        path.Join(importpath, prefix),
+		Locator:     locator,
+		Files:       files,
+		SFiles:      sfiles,
+		LocalDeps:   local,
+		ForeignDeps: foreign,
+		IsCommand:   p.IsCommand(),
+	}, nil
+}
+
+func loadStdlibPkgs(from string) (pkgs map[string]bool, err error) {
+	f, err := ioutil.ReadFile(from)
+	if err != nil {
+		return
+	}
+
+	err = json.Unmarshal(f, &pkgs)
+	return
+}
+
+func main() {
+	source := flag.String("source", "", "path to directory with sources to process")
+	path := flag.String("path", "", "import path for the package")
+
+	flag.Parse()
+
+	if *source == "" {
+		log.Fatalf("-source flag must be specified")
+	}
+
+	stdlibPkgs, err := loadStdlibPkgs(stdlibList)
+	if err != nil {
+		log.Fatalf("failed to load standard library index from %q: %s\n", stdlibList, err)
+	}
+
+	goDirs, err := findGoDirs(*source)
+	if err != nil {
+		log.Fatalf("failed to walk source directory '%s': %s", *source, err)
+	}
+
+	all := []pkg{}
+	for _, d := range goDirs {
+		analysed, err := analysePackage(*source, d, *path, stdlibPkgs)
+
+		// If the Go source analysis returned "no buildable Go files",
+		// that directory should be skipped.
+		//
+		// This might be due to `+build` flags on the platform and other
+		// reasons (such as test files).
+		if _, ok := err.(*build.NoGoError); ok {
+			continue
+		}
+
+		if err != nil {
+			log.Fatalf("failed to analyse package at %q: %s", d, err)
+		}
+		all = append(all, analysed)
+	}
+
+	j, _ := json.Marshal(all)
+	fmt.Println(string(j))
+}
diff --git a/nix/buildGo/proto.nix b/nix/buildGo/proto.nix
new file mode 100644
index 0000000000..6c37f758ce
--- /dev/null
+++ b/nix/buildGo/proto.nix
@@ -0,0 +1,87 @@
+# Copyright 2019 Google LLC.
+# SPDX-License-Identifier: Apache-2.0
+#
+# This file provides derivations for the dependencies of a gRPC
+# service in Go.
+
+{ external }:
+
+let
+  inherit (builtins) fetchGit map;
+in
+rec {
+  goProto = external {
+    path = "github.com/golang/protobuf";
+    src = fetchGit {
+      url = "https://github.com/golang/protobuf";
+      rev = "ed6926b37a637426117ccab59282c3839528a700";
+    };
+  };
+
+  xnet = external {
+    path = "golang.org/x/net";
+
+    src = fetchGit {
+      url = "https://go.googlesource.com/net";
+      rev = "ffdde105785063a81acd95bdf89ea53f6e0aac2d";
+    };
+
+    deps = [
+      xtext.secure.bidirule
+      xtext.unicode.bidi
+      xtext.unicode.norm
+    ];
+  };
+
+  xsys = external {
+    path = "golang.org/x/sys";
+    src = fetchGit {
+      url = "https://go.googlesource.com/sys";
+      rev = "bd437916bb0eb726b873ee8e9b2dcf212d32e2fd";
+    };
+  };
+
+  xtext = external {
+    path = "golang.org/x/text";
+    src = fetchGit {
+      url = "https://go.googlesource.com/text";
+      rev = "cbf43d21aaebfdfeb81d91a5f444d13a3046e686";
+    };
+  };
+
+  genproto = external {
+    path = "google.golang.org/genproto";
+    src = fetchGit {
+      url = "https://github.com/google/go-genproto";
+      # necessary because https://github.com/NixOS/nix/issues/1923
+      ref = "main";
+      rev = "83cc0476cb11ea0da33dacd4c6354ab192de6fe6";
+    };
+
+    deps = with goProto; [
+      proto
+      ptypes.any
+    ];
+  };
+
+  goGrpc = external {
+    path = "google.golang.org/grpc";
+    deps = ([
+      xnet.trace
+      xnet.http2
+      xsys.unix
+      xnet.http2.hpack
+      genproto.googleapis.rpc.status
+    ] ++ (with goProto; [
+      proto
+      ptypes
+      ptypes.duration
+      ptypes.timestamp
+    ]));
+
+    src = fetchGit {
+      url = "https://github.com/grpc/grpc-go";
+      rev = "d8e3da36ac481ef00e510ca119f6b68177713689";
+    };
+  };
+}
diff --git a/nix/buildLisp/README.md b/nix/buildLisp/README.md
new file mode 100644
index 0000000000..0d1e469834
--- /dev/null
+++ b/nix/buildLisp/README.md
@@ -0,0 +1,254 @@
+buildLisp.nix
+=============
+
+This is a build system for Common Lisp, written in Nix.
+
+It aims to offer an alternative to ASDF for users who live in a
+Nix-based ecosystem. This offers several advantages over ASDF:
+
+* Simpler (almost logic-less) package definitions
+* Easy linking of native dependencies (from Nix)
+* Composability with Nix tooling for other languages
+* Effective, per-system caching strategies
+* Easy overriding of dependencies and whatnot
+* Convenient support for multiple Common Lisp implementations
+* ... and more!
+
+The project is still in its early stages and some important
+restrictions should be highlighted:
+
+* Extending `buildLisp` with support for a custom implementation
+  currently requires some knowledge of internals and may not be
+  considered stable yet.
+* Parallel compilation is not possible: Since buildLisp doesn't encode
+  dependencies between components (i. e. source files) like ASDF,
+  it must compile source files in sequence to avoid errors due to
+  undefined symbols.
+
+## Usage
+
+`buildLisp` exposes four different functions:
+
+* `buildLisp.library`: Builds a collection of Lisp files into a library.
+
+  | parameter | type         | use                           | required? |
+  |-----------|--------------|-------------------------------|-----------|
+  | `name`    | `string`     | Name of the library           | yes       |
+  | `srcs`    | `list<path>` | List of paths to source files | yes       |
+  | `deps`    | `list<drv>`  | List of dependencies          | no        |
+  | `native`  | `list<drv>`  | List of native dependencies   | no        |
+  | `test`    | see "Tests"  | Specification for test suite  | no        |
+  | `implementation` | see "Implementations" | Common Lisp implementation to use | no |
+
+  The output of invoking this is a directory containing a FASL file
+  that is the concatenated result of all compiled sources.
+
+* `buildLisp.program`: Builds an executable program out of Lisp files.
+
+  | parameter | type         | use                           | required? |
+  |-----------|--------------|-------------------------------|-----------|
+  | `name`    | `string`     | Name of the program           | yes       |
+  | `srcs`    | `list<path>` | List of paths to source files | yes       |
+  | `deps`    | `list<drv>`  | List of dependencies          | no        |
+  | `native`  | `list<drv>`  | List of native dependencies   | no        |
+  | `main`    | `string`     | Entrypoint function           | no        |
+  | `test`    | see "Tests"  | Specification for test suite  | no        |
+  | `implementation` | see "Implementations" | Common Lisp implementation to use | no |
+
+  The `main` parameter should be the name of a function and defaults
+  to `${name}:main` (i.e. the *exported* `main` function of the
+  package named after the program).
+
+  The output of invoking this is a directory containing a
+  `bin/${name}`.
+
+* `buildLisp.bundled`: Creates a virtual dependency on a built-in library.
+
+  Certain libraries ship with Lisp implementations, for example
+  UIOP/ASDF are commonly included but many implementations also ship
+  internals (such as SBCLs various `sb-*` libraries).
+
+  This function takes a single string argument that is the name of a
+  built-in library and returns a "package" that simply requires this
+  library.
+
+## Tests
+
+Both `buildLisp.library` and `buildLisp.program` take an optional argument
+`tests`, which has the following supported fields:
+
+  | parameter    | type         | use                           | required? |
+  |--------------|--------------|-------------------------------|-----------|
+  | `name`       | `string`     | Name of the test suite        | no        |
+  | `expression` | `string`     | Lisp expression to run tests  | yes       |
+  | `srcs`       | `list<path>` | List of paths to source files | no        |
+  | `native`     | `list<drv>`  | List of native dependencies   | no        |
+
+the `expression` parameter should be a Lisp expression and will be evaluated
+after loading all sources and dependencies (including library/program
+dependencies). It must return a non-`NIL` value if the test suite has passed.
+
+## Example
+
+Using buildLisp could look like this:
+
+```nix
+{ buildLisp, lispPkgs }:
+
+let libExample = buildLisp.library {
+    name = "lib-example";
+    srcs = [ ./lib.lisp ];
+
+    deps = with lispPkgs; [
+      (buildLisp.bundled "sb-posix")
+      iterate
+      cl-ppcre
+    ];
+};
+in buildLisp.program {
+    name = "example";
+    deps = [ libExample ];
+    srcs = [ ./main.lisp ];
+    tests = {
+      deps = [ lispPkgs.fiveam ];
+      srcs = [ ./tests.lisp ];
+      expression = "(fiveam:run!)";
+    };
+}
+```
+
+## Development REPLs
+
+`buildLisp` builds loadable variants of both `program` and `library` derivations
+(usually FASL files). Therefore it can provide a convenient way to obtain an
+instance of any implementation preloaded with `buildLisp`-derivations. This
+is especially useful to use as a host for Sly or SLIME.
+
+* `buildLisp.sbcl.lispWith`, `buildLisp.ccl.lispWith`, ...:
+  Creates a wrapper script preloading a Lisp implementation with various dependencies.
+
+  This function takes a single argument which is a list of Lisp
+  libraries programs or programs. The desired Lisp implementation
+  will load all given derivations and all their dependencies on
+  startup.
+
+  The shortcut `buildLisp.sbclWith` for `buildLisp.sbcl.lispWith` is also provided.
+
+* `repl` passthru attribute: `derivation.repl` is provided as a shortcut
+  for `buildLisp.${implementationName}.lispWith [ derivation ]`.
+  `derivation.ccl.repl`, `derivation.sbcl.repl` etc. work as well, of course
+  (see also "Implementations" section).
+
+## Implementations
+
+Both `buildLisp.library` and `buildLisp.program` allow specifying a different
+Common Lisp implementation than the default one (which is SBCL). When an
+implementation is passed, `buildLisp` makes sure all dependencies are built
+with that implementation as well since build artifacts from different
+implementation will be incompatible with each other.
+
+The argument taken by `implementation` is a special attribute set which
+describes how to do certain tasks for a given implementation, like building
+or loading a library. In case you want to use a custom implementation
+description, the precise structure needed is documented in `buildLisp`'s
+source code for now. `buildLisp` also exposes the following already
+working implementation sets:
+
+* `buildLisp.sbcl`: [SBCL][sbcl], our default implementation
+
+* `buildLisp.ccl`: [CCL][ccl], similar to SBCL, but with very good macOS support
+
+* `buildLisp.ecl`: [ECL][ecl] setup to produce statically linked binaries and
+  libraries. Note that its runtime library is LGPL, so [extra conditions][lgpl-static]
+  must be fulfilled when distributing binaries produced this way.
+
+* Support for ABCL is planned.
+
+For every of these “known” implementations, `buildLisp` will create a `passthru`
+attribute named like the implementation which points to a variant of the derivation
+built with said implementation. Say we have a derivation, `myDrv`, built using SBCL:
+While `myDrv` and `myDrv.sbcl` are built using SBCL, `myDrv.ecl`, `myDrv.ccl` etc.
+build the derivation and all its dependencies using ECL and CCL respectively.
+
+This is useful to test portability of your derivation, but is also used internally
+to speed up the “normalization” of the dependency graph. Thus it is important to
+make sure that your custom implementation's name doesn't clash with one of the
+“known” ones.
+
+## Handling Implementation Specifics
+
+When targeting multiple Common Lisp implementation, it is often necessary to
+handle differing interfaces for OS interaction or to make use of special
+implementation features. For this reason, `buildLisp` allows specifying
+dependencies and source files for specific implementations only. This can
+be utilized by having an attribute set in the list for the `deps` or `srcs`
+argument: `buildLisp` will pick the value of the attribute named like the
+used implementation or `default` and ignore the set completely if both
+are missing.
+
+```nix
+{ buildLisp, lispPkgs }:
+
+buildLisp.library {
+  name = "mylib";
+
+  srcs = [
+    # These are included always of course
+    ./package.lisp
+    ./portable-lib.lisp
+
+    # Choose right impl-* file
+    {
+      sbcl = ./impl-sbcl.lisp;
+      ccl = ./impl-ccl.lisp;
+      ecl = ./impl-ecl.lisp;
+    }
+
+    # We can also use this to inject extra files
+    { ecl = ./extra-ecl-optimizations.lisp; }
+  ];
+
+  deps = [
+    # Use SBCL's special bundled package, flexi-streams otherwise
+    {
+      sbcl = buildLisp.bundled "sb-rotate-byte";
+      default = lispPkgs.flexi-streams;
+    }
+  ];
+}
+```
+
+Additionally a `brokenOn` parameter is accepted which takes a list of
+implementation names on which the derivation is not expected to work.
+This only influences `meta.ci.targets` which is read by depot's CI to
+check which variants (see "Implementations") of the derivation to
+build, so it may not be useful outside of depot.
+
+## Influencing the Lisp Runtime
+
+Lisp implementations which create an executable by dumping an image
+usually parse a few implementation-specific command line options on
+executable startup that influence runtime settings related to things
+like GC. `buildLisp` generates a wrapper which makes sure that this
+never interferes with the argument parsing implemented in the actual
+application, but sometimes it is useful to run an executable with
+special settings. To allow this, the content of `NIX_BUILDLISP_LISP_ARGS`
+is passed to the lisp implementation.
+
+For example, you can make the underlying SBCL print its version for
+any executable built with `buildLisp` (and SBCL) like this:
+
+```console
+$ env NIX_BUILDLISP_LISP_ARGS="--version" ./result/bin/🕰️
+SBCL 2.1.2.nixos
+```
+
+In practice you'd probably want to specify options like
+`--dynamic-space-size` or `--tls-limit` (try passing `--help` for a
+full list). Naturally, these options are completely different for
+different implementations.
+
+[sbcl]: http://www.sbcl.org/
+[ccl]: https://ccl.clozure.com/
+[ecl]: https://common-lisp.net/project/ecl/
+[lgpl-static]: https://www.gnu.org/licenses/gpl-faq.en.html#LGPLStaticVsDynamic
diff --git a/nix/buildLisp/default.nix b/nix/buildLisp/default.nix
new file mode 100644
index 0000000000..d9f9e70a37
--- /dev/null
+++ b/nix/buildLisp/default.nix
@@ -0,0 +1,780 @@
+# buildLisp provides Nix functions to build Common Lisp packages,
+# targeting SBCL.
+#
+# buildLisp is designed to enforce conventions and do away with the
+# free-for-all of existing Lisp build systems.
+
+{ pkgs ? import <nixpkgs> { }, ... }:
+
+let
+  inherit (builtins) map elemAt match filter;
+  inherit (pkgs) lib runCommandNoCC makeWrapper writeText writeShellScriptBin sbcl ecl-static ccl;
+  inherit (pkgs.stdenv) targetPlatform;
+
+  #
+  # Internal helper definitions
+  #
+
+  defaultImplementation = impls.sbcl;
+
+  # Many Common Lisp implementations (like ECL and CCL) will occasionally drop
+  # you into an interactive debugger even when executing something as a script.
+  # In nix builds we don't want such a situation: Any error should make the
+  # script exit non-zero. Luckily the ANSI standard specifies *debugger-hook*
+  # which is invoked before the debugger letting us just do that.
+  disableDebugger = writeText "disable-debugger.lisp" ''
+    (setf *debugger-hook*
+          (lambda (error hook)
+            (declare (ignore hook))
+            (format *error-output* "~%Unhandled error: ~a~%" error)
+            #+ccl (quit 1)
+            #+ecl (ext:quit 1)))
+  '';
+
+  # Process a list of arbitrary values which also contains “implementation
+  # filter sets” which describe conditonal inclusion of elements depending
+  # on the CL implementation used. Elements are processed in the following
+  # manner:
+  #
+  # * Paths, strings, derivations are left as is
+  # * A non-derivation attribute set is processed like this:
+  #   1. If it has an attribute equal to impl.name, replace with its value.
+  #   2. Alternatively use the value of the "default" attribute.
+  #   3. In all other cases delete the element from the list.
+  #
+  # This can be used to express dependencies or source files which are specific
+  # to certain implementations:
+  #
+  #  srcs = [
+  #    # mixable with unconditional entries
+  #    ./package.lisp
+  #
+  #    # implementation specific source files
+  #    {
+  #      ccl = ./impl-ccl.lisp;
+  #      sbcl = ./impl-sbcl.lisp;
+  #      ecl = ./impl-ecl.lisp;
+  #    }
+  #  ];
+  #
+  #  deps = [
+  #    # this dependency is ignored if impl.name != "sbcl"
+  #    { sbcl = buildLisp.bundled "sb-posix"; }
+  #
+  #    # only special casing for a single implementation
+  #    {
+  #      sbcl = buildLisp.bundled "uiop";
+  #      default = buildLisp.bundled "asdf";
+  #    }
+  #  ];
+  implFilter = impl: xs:
+    let
+      isFilterSet = x: builtins.isAttrs x && !(lib.isDerivation x);
+    in
+    builtins.map
+      (
+        x: if isFilterSet x then x.${impl.name} or x.default else x
+      )
+      (builtins.filter
+        (
+          x: !(isFilterSet x) || x ? ${impl.name} || x ? default
+        )
+        xs);
+
+  # Generates lisp code which instructs the given lisp implementation to load
+  # all the given dependencies.
+  genLoadLispGeneric = impl: deps:
+    lib.concatStringsSep "\n"
+      (map (lib: "(load \"${lib}/${lib.lispName}.${impl.faslExt}\")")
+        (allDeps impl deps));
+
+  # 'genTestLispGeneric' generates a Lisp file that loads all sources and deps
+  # and executes expression for a given implementation description.
+  genTestLispGeneric = impl: { name, srcs, deps, expression }: writeText "${name}.lisp" ''
+    ;; Dependencies
+    ${impl.genLoadLisp deps}
+
+    ;; Sources
+    ${lib.concatStringsSep "\n" (map (src: "(load \"${src}\")") srcs)}
+
+    ;; Test expression
+    (unless ${expression}
+      (exit :code 1))
+  '';
+
+  # 'dependsOn' determines whether Lisp library 'b' depends on 'a'.
+  dependsOn = a: b: builtins.elem a b.lispDeps;
+
+  # 'allDeps' flattens the list of dependencies (and their
+  # dependencies) into one ordered list of unique deps which
+  # all use the given implementation.
+  allDeps = impl: deps:
+    let
+      # The override _should_ propagate itself recursively, as every derivation
+      # would only expose its actually used dependencies. Use implementation
+      # attribute created by withExtras if present, override in all other cases
+      # (mainly bundled).
+      deps' = builtins.map
+        (dep: dep."${impl.name}" or (dep.overrideLisp (_: {
+          implementation = impl;
+        })))
+        deps;
+    in
+    (lib.toposort dependsOn (lib.unique (
+      lib.flatten (deps' ++ (map (d: d.lispDeps) deps'))
+    ))).result;
+
+  # 'allNative' extracts all native dependencies of a dependency list
+  # to ensure that library load paths are set correctly during all
+  # compilations and program assembly.
+  allNative = native: deps: lib.unique (
+    lib.flatten (native ++ (map (d: d.lispNativeDeps) deps))
+  );
+
+  # Add an `overrideLisp` attribute to a function result that works
+  # similar to `overrideAttrs`, but is used specifically for the
+  # arguments passed to Lisp builders.
+  makeOverridable = f: orig: (f orig) // {
+    overrideLisp = new: makeOverridable f (orig // (new orig));
+  };
+
+  # This is a wrapper arround 'makeOverridable' which performs its
+  # function, but also adds a the following additional attributes to the
+  # resulting derivation, namely a repl attribute which builds a `lispWith`
+  # derivation for the current implementation and additional attributes for
+  # every all implementations. So `drv.sbcl` would build the derivation
+  # with SBCL regardless of what was specified in the initial arguments.
+  withExtras = f: args:
+    let
+      drv = (makeOverridable f) args;
+    in
+    lib.fix (self:
+      drv.overrideLisp
+        (old:
+          let
+            implementation = old.implementation or defaultImplementation;
+            brokenOn = old.brokenOn or [ ];
+            # TODO(sterni): https://github.com/Clozure/ccl/issues/405
+            targets = lib.subtractLists (brokenOn ++ [ "ccl" implementation.name ])
+              (builtins.attrNames impls);
+          in
+          {
+            passthru = (old.passthru or { }) // {
+              repl = implementation.lispWith [ self ];
+
+              # meta is done via passthru to minimize rebuilds caused by overriding
+              meta = (old.passthru.meta or { }) // {
+                ci = (old.passthru.meta.ci or { }) // {
+                  inherit targets;
+                };
+              };
+            } // builtins.listToAttrs (builtins.map
+              (impl: {
+                inherit (impl) name;
+                value = self.overrideLisp (_: {
+                  implementation = impl;
+                });
+              })
+              (builtins.attrValues impls));
+          }) // {
+        overrideLisp = new: withExtras f (args // new args);
+      });
+
+  # 'testSuite' builds a Common Lisp test suite that loads all of srcs and deps,
+  # and then executes expression to check its result
+  testSuite = { name, expression, srcs, deps ? [ ], native ? [ ], implementation }:
+    let
+      lispDeps = allDeps implementation (implFilter implementation deps);
+      lispNativeDeps = allNative native lispDeps;
+      filteredSrcs = implFilter implementation srcs;
+    in
+    runCommandNoCC name
+      {
+        LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
+        LANG = "C.UTF-8";
+      } ''
+      echo "Running test suite ${name}"
+
+      ${implementation.runScript} ${
+        implementation.genTestLisp {
+          inherit name expression;
+          srcs = filteredSrcs;
+          deps = lispDeps;
+        }
+      } | tee $out
+
+      echo "Test suite ${name} succeeded"
+    '';
+
+  # 'impls' is an attribute set of attribute sets which describe how to do common
+  # tasks when building for different Common Lisp implementations. Each
+  # implementation set has the following members:
+  #
+  # Required members:
+  #
+  # - runScript :: string
+  #   Describes how to invoke the implementation from the shell, so it runs a
+  #   lisp file as a script and exits.
+  # - faslExt :: string
+  #   File extension of the implementations loadable (FASL) files.
+  #   Implementations are free to generate native object files, but with the way
+  #   buildLisp works it is required that we can also 'load' libraries, so
+  #   (additionally) building a FASL or equivalent is required.
+  # - genLoadLisp :: [ dependency ] -> string
+  #   Returns lisp code to 'load' the given dependencies. 'genLoadLispGeneric'
+  #   should work for most dependencies.
+  # - genCompileLisp :: { name, srcs, deps } -> file
+  #   Builds a lisp file which instructs the implementation to build a library
+  #   from the given source files when executed. After running at least
+  #   the file "$out/${name}.${impls.${implementation}.faslExt}" should have
+  #   been created.
+  # - genDumpLisp :: { name, main, deps } -> file
+  #   Builds a lisp file which instructs the implementation to build an
+  #   executable which runs 'main' (and exits) where 'main' is available from
+  #   'deps'. The executable should be created as "$out/bin/${name}", usually
+  #   by dumping the lisp image with the replaced toplevel function replaced.
+  # - wrapProgram :: boolean
+  #   Whether to wrap the resulting binary / image with a wrapper script setting
+  #   `LD_LIBRARY_PATH`.
+  # - genTestLisp :: { name, srcs, deps, expression } -> file
+  #   Builds a lisp file which loads the given 'deps' and 'srcs' files and
+  #   then evaluates 'expression'. Depending on whether 'expression' returns
+  #   true or false, the script must exit with a zero or non-zero exit code.
+  #   'genTestLispGeneric' will work for most implementations.
+  # - lispWith :: [ dependency ] -> drv
+  #   Builds a script (or dumped image) which when executed loads (or has
+  #   loaded) all given dependencies. When built this should create an executable
+  #   at "$out/bin/${implementation}".
+  #
+  # Optional members:
+  #
+  # - bundled :: string -> library
+  #   Allows giving an implementation specific builder for a bundled library.
+  #   This function is used as a replacement for the internal defaultBundled
+  #   function and only needs to support one implementation. The returned derivation
+  #   must behave like one built by 'library' (in particular have the same files
+  #   available in "$out" and the same 'passthru' attributes), but may be built
+  #   completely differently.
+  impls = lib.mapAttrs (name: v: { inherit name; } // v) {
+    sbcl = {
+      runScript = "${sbcl}/bin/sbcl --script";
+      faslExt = "fasl";
+
+      # 'genLoadLisp' generates Lisp code that instructs SBCL to load all
+      # the provided Lisp libraries.
+      genLoadLisp = genLoadLispGeneric impls.sbcl;
+
+      # 'genCompileLisp' generates a Lisp file that instructs SBCL to
+      # compile the provided list of Lisp source files to "$out/${name}.fasl".
+      genCompileLisp = { name, srcs, deps }: writeText "sbcl-compile.lisp" ''
+        ;; This file compiles the specified sources into the Nix build
+        ;; directory, creating one FASL file for each source.
+        (require 'sb-posix)
+
+        ${impls.sbcl.genLoadLisp deps}
+
+        (defun nix-compile-lisp (srcfile)
+          (let ((outfile (make-pathname :type "fasl"
+                                        :directory (or (sb-posix:getenv "NIX_BUILD_TOP")
+                                                       (error "not running in a Nix build"))
+                                        :name (substitute #\- #\/ srcfile))))
+            (multiple-value-bind (out-truename _warnings-p failure-p)
+                (compile-file srcfile :output-file outfile)
+              (if failure-p (sb-posix:exit 1)
+                  (progn
+                    ;; For the case of multiple files belonging to the same
+                    ;; library being compiled, load them in order:
+                    (load out-truename)
+
+                    ;; Return pathname as a string for cat-ting it later
+                    (namestring out-truename))))))
+
+        (let ((*compile-verbose* t)
+              (catted-fasl (make-pathname :type "fasl"
+                                          :directory (or (sb-posix:getenv "out")
+                                                         (error "not running in a Nix build"))
+                                          :name "${name}")))
+
+          (with-open-file (file catted-fasl
+                                :direction :output
+                                :if-does-not-exist :create)
+
+            ;; SBCL's FASL files can just be bundled together using cat
+            (sb-ext:run-program "cat"
+             (mapcar #'nix-compile-lisp
+              ;; These forms were inserted by the Nix build:
+              '(${
+                lib.concatMapStringsSep "\n" (src: "\"${src}\"") srcs
+              }))
+             :output file :search t)))
+      '';
+
+      # 'genDumpLisp' generates a Lisp file that instructs SBCL to dump
+      # the currently loaded image as an executable to $out/bin/$name.
+      #
+      # TODO(tazjin): Compression is currently unsupported because the
+      # SBCL in nixpkgs is, by default, not compiled with zlib support.
+      genDumpLisp = { name, main, deps }: writeText "sbcl-dump.lisp" ''
+        (require 'sb-posix)
+
+        ${impls.sbcl.genLoadLisp deps}
+
+        (let* ((bindir (concatenate 'string (sb-posix:getenv "out") "/bin"))
+               (outpath (make-pathname :name "${name}"
+                                       :directory bindir)))
+
+          ;; Tell UIOP that argv[0] will refer to running image, not the lisp impl
+          (when (find-package :uiop)
+            (eval `(setq ,(find-symbol "*IMAGE-DUMPED-P*" :uiop) :executable)))
+
+          (save-lisp-and-die outpath
+                             :executable t
+                             :toplevel
+                             (lambda ()
+                               ;; Filter out everything prior to the `--` we
+                               ;; insert in the wrapper to prevent SBCL from
+                               ;; parsing arguments at startup
+                               (setf sb-ext:*posix-argv*
+                                     (delete "--" sb-ext:*posix-argv*
+                                             :test #'string= :count 1))
+                               (${main}))
+                             :purify t))
+      '';
+
+      wrapProgram = true;
+
+      genTestLisp = genTestLispGeneric impls.sbcl;
+
+      lispWith = deps:
+        let lispDeps = filter (d: !d.lispBinary) (allDeps impls.sbcl deps);
+        in writeShellScriptBin "sbcl" ''
+          export LD_LIBRARY_PATH="${lib.makeLibraryPath (allNative [] lispDeps)}"
+          export LANG="C.UTF-8"
+          exec ${sbcl}/bin/sbcl ${
+            lib.optionalString (deps != [])
+              "--load ${writeText "load.lisp" (impls.sbcl.genLoadLisp lispDeps)}"
+          } $@
+        '';
+    };
+    ecl = {
+      runScript = "${ecl-static}/bin/ecl --load ${disableDebugger} --shell";
+      faslExt = "fasc";
+      genLoadLisp = genLoadLispGeneric impls.ecl;
+      genCompileLisp = { name, srcs, deps }: writeText "ecl-compile.lisp" ''
+        ;; This seems to be required to bring make the 'c' package available
+        ;; early, otherwise ECL tends to fail with a read failure…
+        (ext:install-c-compiler)
+
+        ;; Load dependencies
+        ${impls.ecl.genLoadLisp deps}
+
+        (defun getenv-or-fail (var)
+          (or (ext:getenv var)
+              (error (format nil "Missing expected environment variable ~A" var))))
+
+        (defun nix-compile-file (srcfile &key native)
+          "Compile the given srcfile into a compilation unit in :out-dir using
+          a unique name based on srcfile as the filename which is returned after
+          compilation. If :native is true, create an native object file,
+          otherwise a byte-compile fasc file is built and immediately loaded."
+
+          (let* ((unique-name (substitute #\_ #\/ srcfile))
+                 (out-file (make-pathname :type (if native "o" "fasc")
+                                          :directory (getenv-or-fail "NIX_BUILD_TOP")
+                                          :name unique-name)))
+            (multiple-value-bind (out-truename _warnings-p failure-p)
+                (compile-file srcfile :system-p native
+                                      :load (not native)
+                                      :output-file out-file
+                                      :verbose t :print t)
+              (if failure-p (ext:quit 1) out-truename))))
+
+        (let* ((out-dir (getenv-or-fail "out"))
+               (nix-build-dir (getenv-or-fail "NIX_BUILD_TOP"))
+               (srcs
+                ;; These forms are inserted by the Nix build
+                '(${lib.concatMapStringsSep "\n" (src: "\"${src}\"") srcs})))
+
+          ;; First, we'll byte compile loadable FASL files and load them
+          ;; immediately. Since we are using a statically linked ECL, there's
+          ;; no way to load native objects, so we rely on byte compilation
+          ;; for all our loading — which is crucial in compilation of course.
+          (ext:install-bytecodes-compiler)
+
+          ;; ECL's bytecode FASLs can just be concatenated to create a bundle
+          ;; at least since a recent bugfix which we apply as a patch.
+          ;; See also: https://gitlab.com/embeddable-common-lisp/ecl/-/issues/649
+          (let ((bundle-out (make-pathname :type "fasc" :name "${name}"
+                                           :directory out-dir)))
+
+            (with-open-file (fasc-stream bundle-out :direction :output)
+              (ext:run-program "cat"
+                               (mapcar (lambda (f)
+                                         (namestring
+                                          (nix-compile-file f :native nil)))
+                                       srcs)
+                               :output fasc-stream)))
+
+          (ext:install-c-compiler)
+
+          ;; Build a (natively compiled) static archive (.a) file. We want to
+          ;; use this for (statically) linking an executable later. The bytecode
+          ;; dance is only required because we can't load such archives.
+          (c:build-static-library
+           (make-pathname :type "a" :name "${name}" :directory out-dir)
+           :lisp-files (mapcar (lambda (x)
+                                 (nix-compile-file x :native t))
+                               srcs)))
+      '';
+      genDumpLisp = { name, main, deps }: writeText "ecl-dump.lisp" ''
+        (defun getenv-or-fail (var)
+          (or (ext:getenv var)
+              (error (format nil "Missing expected environment variable ~A" var))))
+
+        ${impls.ecl.genLoadLisp deps}
+
+        ;; makes a 'c' package available that can link executables
+        (ext:install-c-compiler)
+
+        (c:build-program
+         (merge-pathnames (make-pathname :directory '(:relative "bin")
+                                         :name "${name}")
+                          (truename (getenv-or-fail "out")))
+         :epilogue-code `(progn
+                          ;; UIOP doesn't understand ECL, so we need to make it
+                          ;; aware that we are a proper executable, causing it
+                          ;; to handle argument parsing and such properly. Since
+                          ;; this needs to work even when we're not using UIOP,
+                          ;; we need to do some compile-time acrobatics.
+                          ,(when (find-package :uiop)
+                            `(setf ,(find-symbol "*IMAGE-DUMPED-P*" :uiop) :executable))
+                          ;; Run the actual application…
+                          (${main})
+                          ;; … and exit.
+                          (ext:quit))
+         ;; ECL can't remember these from its own build…
+         :ld-flags '("-static")
+         :lisp-files
+         ;; The following forms are inserted by the Nix build
+         '(${
+             lib.concatMapStrings (dep: ''
+               "${dep}/${dep.lispName}.a"
+             '') (allDeps impls.ecl deps)
+           }))
+      '';
+
+      wrapProgram = false;
+
+      genTestLisp = genTestLispGeneric impls.ecl;
+
+      lispWith = deps:
+        let lispDeps = filter (d: !d.lispBinary) (allDeps impls.ecl deps);
+        in writeShellScriptBin "ecl" ''
+          exec ${ecl-static}/bin/ecl ${
+            lib.optionalString (deps != [])
+              "--load ${writeText "load.lisp" (impls.ecl.genLoadLisp lispDeps)}"
+          } $@
+        '';
+
+      bundled = name: runCommandNoCC "${name}-cllib"
+        {
+          passthru = {
+            lispName = name;
+            lispNativeDeps = [ ];
+            lispDeps = [ ];
+            lispBinary = false;
+            repl = impls.ecl.lispWith [ (impls.ecl.bundled name) ];
+          };
+        } ''
+        mkdir -p "$out"
+        ln -s "${ecl-static}/lib/ecl-${ecl-static.version}/${name}.${impls.ecl.faslExt}" -t "$out"
+        ln -s "${ecl-static}/lib/ecl-${ecl-static.version}/lib${name}.a" "$out/${name}.a"
+      '';
+    };
+    ccl = {
+      # Relatively bespoke wrapper script necessary to make CCL just™ execute
+      # a lisp file as a script.
+      runScript = pkgs.writers.writeBash "ccl" ''
+        # don't print intro message etc.
+        args=("--quiet")
+
+        # makes CCL crash on error instead of entering the debugger
+        args+=("--load" "${disableDebugger}")
+
+        # load files from command line in order
+        for f in "$@"; do
+          args+=("--load" "$f")
+        done
+
+        # Exit if everything was processed successfully
+        args+=("--eval" "(quit)")
+
+        exec ${ccl}/bin/ccl ''${args[@]}
+      '';
+
+      # See https://ccl.clozure.com/docs/ccl.html#building-definitions
+      faslExt =
+        /**/
+        if targetPlatform.isPowerPC && targetPlatform.is32bit then "pfsl"
+        else if targetPlatform.isPowerPC && targetPlatform.is64bit then "p64fsl"
+        else if targetPlatform.isx86_64 && targetPlatform.isLinux then "lx64fsl"
+        else if targetPlatform.isx86_32 && targetPlatform.isLinux then "lx32fsl"
+        else if targetPlatform.isAarch32 && targetPlatform.isLinux then "lafsl"
+        else if targetPlatform.isx86_32 && targetPlatform.isDarwin then "dx32fsl"
+        else if targetPlatform.isx86_64 && targetPlatform.isDarwin then "dx64fsl"
+        else if targetPlatform.isx86_64 && targetPlatform.isDarwin then "dx64fsl"
+        else if targetPlatform.isx86_32 && targetPlatform.isFreeBSD then "fx32fsl"
+        else if targetPlatform.isx86_64 && targetPlatform.isFreeBSD then "fx64fsl"
+        else if targetPlatform.isx86_32 && targetPlatform.isWindows then "wx32fsl"
+        else if targetPlatform.isx86_64 && targetPlatform.isWindows then "wx64fsl"
+        else builtins.throw "Don't know what FASLs are called for this platform: "
+          + pkgs.stdenv.targetPlatform.system;
+
+      genLoadLisp = genLoadLispGeneric impls.ccl;
+
+      genCompileLisp = { name, srcs, deps }: writeText "ccl-compile.lisp" ''
+        ${impls.ccl.genLoadLisp deps}
+
+        (defun getenv-or-fail (var)
+          (or (getenv var)
+              (error (format nil "Missing expected environment variable ~A" var))))
+
+        (defun nix-compile-file (srcfile)
+          "Trivial wrapper around COMPILE-FILE which causes CCL to exit if
+          compilation fails and LOADs the compiled file on success."
+          (let ((output (make-pathname :name (substitute #\_ #\/ srcfile)
+                                       :type "${impls.ccl.faslExt}"
+                                       :directory (getenv-or-fail "NIX_BUILD_TOP"))))
+            (multiple-value-bind (out-truename _warnings-p failure-p)
+                (compile-file srcfile :output-file output :print t :verbose t)
+                (declare (ignore _warnings-p))
+              (if failure-p (quit 1)
+                  (progn (load out-truename) out-truename)))))
+
+        (fasl-concatenate (make-pathname :name "${name}" :type "${impls.ccl.faslExt}"
+                                         :directory (getenv-or-fail "out"))
+                          (mapcar #'nix-compile-file
+                                  ;; These forms where inserted by the Nix build
+                                  '(${
+                                      lib.concatMapStrings (src: ''
+                                        "${src}"
+                                      '') srcs
+                                   })))
+      '';
+
+      genDumpLisp = { name, main, deps }: writeText "ccl-dump.lisp" ''
+        ${impls.ccl.genLoadLisp deps}
+
+        (let* ((out (or (getenv "out") (error "Not running in a Nix build")))
+               (bindir (concatenate 'string out "/bin/"))
+               (executable (make-pathname :directory bindir :name "${name}")))
+
+          ;; Tell UIOP that argv[0] will refer to running image, not the lisp impl
+          (when (find-package :uiop)
+            (eval `(setf ,(find-symbol "*IMAGE-DUMPED-P*" :uiop) :executable)))
+
+          (save-application executable
+                            :purify t
+                            :error-handler :quit
+                            :toplevel-function
+                            (lambda ()
+                              ;; Filter out everything prior to the `--` we
+                              ;; insert in the wrapper to prevent SBCL from
+                              ;; parsing arguments at startup
+                              (setf ccl:*command-line-argument-list*
+                                    (delete "--" ccl:*command-line-argument-list*
+                                                 :test #'string= :count 1))
+                              (${main}))
+                            :mode #o755
+                            ;; TODO(sterni): use :native t on macOS
+                            :prepend-kernel t))
+      '';
+
+      wrapProgram = true;
+
+      genTestLisp = genTestLispGeneric impls.ccl;
+
+      lispWith = deps:
+        let lispDeps = filter (d: !d.lispBinary) (allDeps impls.ccl deps);
+        in writeShellScriptBin "ccl" ''
+          export LD_LIBRARY_PATH="${lib.makeLibraryPath (allNative [] lispDeps)}"
+          exec ${ccl}/bin/ccl ${
+            lib.optionalString (deps != [])
+              "--load ${writeText "load.lisp" (impls.ccl.genLoadLisp lispDeps)}"
+          } "$@"
+        '';
+    };
+  };
+
+  #
+  # Public API functions
+  #
+
+  # 'library' builds a list of Common Lisp files into an implementation
+  # specific library format, usually a single FASL file, which can then be
+  # loaded and built into an executable via 'program'.
+  library =
+    { name
+    , implementation ? defaultImplementation
+    , brokenOn ? [ ] # TODO(sterni): make this a warning
+    , srcs
+    , deps ? [ ]
+    , native ? [ ]
+    , tests ? null
+    , passthru ? { }
+    }:
+    let
+      filteredDeps = implFilter implementation deps;
+      filteredSrcs = implFilter implementation srcs;
+      lispNativeDeps = (allNative native filteredDeps);
+      lispDeps = allDeps implementation filteredDeps;
+      testDrv =
+        if ! isNull tests
+        then
+          testSuite
+            {
+              name = tests.name or "${name}-test";
+              srcs = filteredSrcs ++ (tests.srcs or [ ]);
+              deps = filteredDeps ++ (tests.deps or [ ]);
+              expression = tests.expression;
+              inherit implementation;
+            }
+        else null;
+    in
+    lib.fix (self: runCommandNoCC "${name}-cllib"
+      {
+        LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
+        LANG = "C.UTF-8";
+        passthru = passthru // {
+          inherit lispNativeDeps lispDeps;
+          lispName = name;
+          lispBinary = false;
+          tests = testDrv;
+        };
+      } ''
+      ${if ! isNull testDrv
+        then "echo 'Test ${testDrv} succeeded'"
+        else "echo 'No tests run'"}
+
+      mkdir $out
+
+      ${implementation.runScript} ${
+        implementation.genCompileLisp {
+          srcs = filteredSrcs;
+          inherit name;
+          deps = lispDeps;
+        }
+      }
+    '');
+
+  # 'program' creates an executable, usually containing a dumped image of the
+  # specified sources and dependencies.
+  program =
+    { name
+    , implementation ? defaultImplementation
+    , brokenOn ? [ ] # TODO(sterni): make this a warning
+    , main ? "${name}:main"
+    , srcs
+    , deps ? [ ]
+    , native ? [ ]
+    , tests ? null
+    , passthru ? { }
+    }:
+    let
+      filteredSrcs = implFilter implementation srcs;
+      filteredDeps = implFilter implementation deps;
+      lispDeps = allDeps implementation filteredDeps;
+      libPath = lib.makeLibraryPath (allNative native lispDeps);
+      # overriding is used internally to propagate the implementation to use
+      selfLib = (makeOverridable library) {
+        inherit name native brokenOn;
+        deps = lispDeps;
+        srcs = filteredSrcs;
+      };
+      testDrv =
+        if ! isNull tests
+        then
+          testSuite
+            {
+              name = tests.name or "${name}-test";
+              srcs =
+                (
+                  # testSuite does run implFilter as well
+                  filteredSrcs ++ (tests.srcs or [ ])
+                );
+              deps = filteredDeps ++ (tests.deps or [ ]);
+              expression = tests.expression;
+              inherit implementation;
+            }
+        else null;
+    in
+    lib.fix (self: runCommandNoCC "${name}"
+      {
+        nativeBuildInputs = [ makeWrapper ];
+        LD_LIBRARY_PATH = libPath;
+        LANG = "C.UTF-8";
+        passthru = passthru // {
+          lispName = name;
+          lispDeps = [ selfLib ];
+          lispNativeDeps = native;
+          lispBinary = true;
+          tests = testDrv;
+        };
+      }
+      (''
+        ${if ! isNull testDrv
+          then "echo 'Test ${testDrv} succeeded'"
+          else ""}
+        mkdir -p $out/bin
+
+        ${implementation.runScript} ${
+          implementation.genDumpLisp {
+            inherit name main;
+            deps = ([ selfLib ] ++ lispDeps);
+          }
+        }
+      '' + lib.optionalString implementation.wrapProgram ''
+        wrapProgram $out/bin/${name} \
+          --prefix LD_LIBRARY_PATH : "${libPath}" \
+          --add-flags "\$NIX_BUILDLISP_LISP_ARGS --"
+      ''));
+
+  # 'bundled' creates a "library" which makes a built-in package available,
+  # such as any of SBCL's sb-* packages or ASDF. By default this is done
+  # by calling 'require', but implementations are free to provide their
+  # own specific bundled function.
+  bundled = name:
+    let
+      # TODO(sterni): allow overriding args to underlying 'library' (e. g. srcs)
+      defaultBundled = implementation: name: library {
+        inherit name implementation;
+        srcs = lib.singleton (builtins.toFile "${name}.lisp" "(require '${name})");
+      };
+
+      bundled' =
+        { implementation ? defaultImplementation
+        , name
+        }:
+        implementation.bundled or (defaultBundled implementation) name;
+
+    in
+    (makeOverridable bundled') {
+      inherit name;
+    };
+
+in
+{
+  library = withExtras library;
+  program = withExtras program;
+  inherit bundled;
+
+  # 'sbclWith' creates an image with the specified libraries /
+  # programs loaded in SBCL.
+  sbclWith = impls.sbcl.lispWith;
+
+  inherit (impls)
+    sbcl
+    ecl
+    ccl
+    ;
+}
diff --git a/nix/buildLisp/example/default.nix b/nix/buildLisp/example/default.nix
new file mode 100644
index 0000000000..6add2676f1
--- /dev/null
+++ b/nix/buildLisp/example/default.nix
@@ -0,0 +1,33 @@
+{ depot, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+
+  # Example Lisp library.
+  #
+  # Currently the `name` attribute is only used for the derivation
+  # itself, it has no practical implications.
+  libExample = buildLisp.library {
+    name = "lib-example";
+    srcs = [
+      ./lib.lisp
+    ];
+  };
+
+  # Example Lisp program.
+  #
+  # This builds & writes an executable for a program using the library
+  # above to disk.
+  #
+  # By default, buildLisp.program expects the entry point to be
+  # `$name:main`. This can be overridden by configuring the `main`
+  # attribute.
+in
+buildLisp.program {
+  name = "example";
+  deps = [ libExample ];
+
+  srcs = [
+    ./main.lisp
+  ];
+}
diff --git a/nix/buildLisp/example/lib.lisp b/nix/buildLisp/example/lib.lisp
new file mode 100644
index 0000000000..e557de4ae5
--- /dev/null
+++ b/nix/buildLisp/example/lib.lisp
@@ -0,0 +1,6 @@
+(defpackage lib-example
+  (:use :cl)
+  (:export :who))
+(in-package :lib-example)
+
+(defun who () "edef")
diff --git a/nix/buildLisp/example/main.lisp b/nix/buildLisp/example/main.lisp
new file mode 100644
index 0000000000..a29390cf4d
--- /dev/null
+++ b/nix/buildLisp/example/main.lisp
@@ -0,0 +1,7 @@
+(defpackage example
+  (:use :cl :lib-example)
+  (:export :main))
+(in-package :example)
+
+(defun main ()
+  (format t "i <3 ~A~%" (who)))
diff --git a/nix/buildLisp/tests/argv0.nix b/nix/buildLisp/tests/argv0.nix
new file mode 100644
index 0000000000..bc29337d06
--- /dev/null
+++ b/nix/buildLisp/tests/argv0.nix
@@ -0,0 +1,36 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildLisp.program {
+  name = "argv0-test";
+
+  srcs = [
+    (pkgs.writeText "argv0-test.lisp" ''
+      (defpackage :argv0-test (:use :common-lisp :uiop) (:export :main))
+      (in-package :argv0-test)
+
+      (defun main ()
+        (format t "~A~%" (uiop:argv0)))
+    '')
+  ];
+
+  deps = [
+    {
+      sbcl = depot.nix.buildLisp.bundled "uiop";
+      default = depot.nix.buildLisp.bundled "asdf";
+    }
+  ];
+
+  passthru.meta.ci = {
+    extraSteps.verify = {
+      label = "verify argv[0] output";
+      needsOutput = true;
+      command = pkgs.writeShellScript "check-argv0" ''
+        set -eux
+
+        for invocation in "$(pwd)/result/bin/argv0-test" "./result/bin/argv0-test"; do
+          test "$invocation" = "$("$invocation")"
+        done
+      '';
+    };
+  };
+}
diff --git a/nix/buildManPages/OWNERS b/nix/buildManPages/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/nix/buildManPages/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/nix/buildManPages/default.nix b/nix/buildManPages/default.nix
new file mode 100644
index 0000000000..746ed25182
--- /dev/null
+++ b/nix/buildManPages/default.nix
@@ -0,0 +1,103 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  inherit (pkgs)
+    gzip
+    mandoc
+    coreutils
+    ;
+
+  inherit (depot.nix)
+    runExecline
+    getBins
+    ;
+
+  bins = getBins mandoc [ "mandoc" ]
+    // getBins gzip [ "gzip" ]
+    // getBins coreutils [ "mkdir" "ln" "cp" ]
+  ;
+
+  defaultGzip = true;
+
+  basename = gzip: { name, section, ... }:
+    "${name}.${toString section}${lib.optionalString gzip ".gz"}";
+
+  manDir = { section, ... }:
+    "\${out}/share/man/man${toString section}";
+
+  target = gzip: args:
+    "${manDir args}/${basename gzip args}";
+
+  buildManPage =
+    { requireLint ? false
+    , gzip ? defaultGzip
+    , ...
+    }:
+    { content
+    , ...
+    }@page:
+    let
+      source = builtins.toFile (basename false page) content;
+    in
+    runExecline (basename gzip page) { } ([
+      (if requireLint then "if" else "foreground")
+      [
+        bins.mandoc
+        "-mdoc"
+        "-T"
+        "lint"
+        source
+      ]
+      "importas"
+      "out"
+      "out"
+    ] ++ (if gzip then [
+      "redirfd"
+      "-w"
+      "1"
+      "$out"
+      bins.gzip
+      "-c"
+      source
+    ] else [
+      bins.cp
+      "--reflink=auto"
+      source
+      "$out"
+    ]));
+
+  buildManPages =
+    name:
+    { derivationArgs ? { }
+    , gzip ? defaultGzip
+    , ...
+    }@args:
+    pages:
+    runExecline "${name}-man-pages"
+      {
+        inherit derivationArgs;
+      }
+      ([
+        "importas"
+        "out"
+        "out"
+      ] ++ lib.concatMap
+        ({ name, section, content }@page: [
+          "if"
+          [ bins.mkdir "-p" (manDir page) ]
+          "if"
+          [
+            bins.ln
+            "-s"
+            (buildManPage args page)
+            (target gzip page)
+          ]
+        ])
+        pages);
+
+in
+{
+  __functor = _: buildManPages;
+
+  single = buildManPage;
+}
diff --git a/nix/buildkite/default.nix b/nix/buildkite/default.nix
new file mode 100644
index 0000000000..6a0e9d246d
--- /dev/null
+++ b/nix/buildkite/default.nix
@@ -0,0 +1,335 @@
+# Logic for generating Buildkite pipelines from Nix build targets read
+# by //nix/readTree.
+#
+# It outputs a "YAML" (actually JSON) file which is evaluated and
+# submitted to Buildkite at the start of each build.
+#
+# The structure of the file that is being created is documented here:
+#   https://buildkite.com/docs/pipelines/defining-steps
+{ depot, pkgs, ... }:
+
+let
+  inherit (builtins)
+    attrValues
+    concatMap
+    concatStringsSep
+    filter
+    foldl'
+    getEnv
+    hasAttr
+    hashString
+    isNull
+    isString
+    length
+    listToAttrs
+    mapAttrs
+    partition
+    pathExists
+    toJSON
+    unsafeDiscardStringContext;
+
+  inherit (pkgs) lib runCommandNoCC writeText;
+  inherit (depot.nix.readTree) mkLabel;
+in
+rec {
+  # Creates a Nix expression that yields the target at the specified
+  # location in the repository.
+  #
+  # This makes a distinction between normal targets (which physically
+  # exist in the repository) and subtargets (which are "virtual"
+  # targets exposed by a physical one) to make it clear in the build
+  # output which is which.
+  mkBuildExpr = target:
+    let
+      descend = expr: attr: "builtins.getAttr \"${attr}\" (${expr})";
+      targetExpr = foldl' descend "import ./. {}" target.__readTree;
+      subtargetExpr = descend targetExpr target.__subtarget;
+    in
+    if target ? __subtarget then subtargetExpr else targetExpr;
+
+  # Determine whether to skip a target if it has not diverged from the
+  # HEAD branch.
+  shouldSkip = parentTargetMap: label: drvPath:
+    if (hasAttr label parentTargetMap) && parentTargetMap."${label}".drvPath == drvPath
+    then "Target has not changed."
+    else false;
+
+  # Create build command for a derivation target.
+  mkBuildCommand = target: drvPath: concatStringsSep " " [
+    # First try to realise the drvPath of the target so we don't evaluate twice.
+    # Nix has no concept of depending on a derivation file without depending on
+    # at least one of its `outPath`s, so we need to discard the string context
+    # if we don't want to build everything during pipeline construction.
+    "(nix-store --realise '${drvPath}' --add-root result --indirect && readlink result)"
+
+    # Since we don't gcroot the derivation files, they may be deleted by the
+    # garbage collector. In that case we can reevaluate and build the attribute
+    # using nix-build.
+    "|| (test ! -f '${drvPath}' && nix-build -E '${mkBuildExpr target}' --show-trace)"
+  ];
+
+  # Create a pipeline step from a single target.
+  mkStep = headBranch: parentTargetMap: target:
+    let
+      label = mkLabel target;
+      drvPath = unsafeDiscardStringContext target.drvPath;
+      shouldSkip' = shouldSkip parentTargetMap;
+    in
+    {
+      label = ":nix: " + label;
+      key = hashString "sha1" label;
+      skip = shouldSkip' label drvPath;
+      command = mkBuildCommand target drvPath;
+      env.READTREE_TARGET = label;
+
+      # Add a dependency on the initial static pipeline step which
+      # always runs. This allows build steps uploaded in batches to
+      # start running before all batches have been uploaded.
+      depends_on = ":init:";
+    };
+
+  # Helper function to inelegantly divide a list into chunks of at
+  # most n elements.
+  #
+  # This works by assigning each element a chunk ID based on its
+  # index, and then grouping all elements by their chunk ID.
+  chunksOf = n: list:
+    let
+      chunkId = idx: toString (idx / n + 1);
+      assigned = lib.imap1 (idx: value: { inherit value; chunk = chunkId idx; }) list;
+      unchunk = mapAttrs (_: elements: map (e: e.value) elements);
+    in
+    unchunk (lib.groupBy (e: e.chunk) assigned);
+
+  # Define a build pipeline chunk as a JSON file, using the pipeline
+  # format documented on
+  # https://buildkite.com/docs/pipelines/defining-steps.
+  makePipelineChunk = name: chunkId: chunk: rec {
+    filename = "${name}-chunk-${chunkId}.json";
+    path = writeText filename (toJSON {
+      steps = chunk;
+    });
+  };
+
+  # Split the pipeline into chunks of at most 256 steps at once, which
+  # are uploaded sequentially. This is because of a limitation in the
+  # Buildkite backend which struggles to process more than a specific
+  # number of chunks at once.
+  pipelineChunks = name: steps:
+    attrValues (mapAttrs (makePipelineChunk name) (chunksOf 192 steps));
+
+  # Create a pipeline structure for the given targets.
+  mkPipeline =
+    {
+      # HEAD branch of the repository on which release steps, GC
+      # anchoring and other "mainline only" steps should run.
+      headBranch
+    , # List of derivations as read by readTree (in most cases just the
+      # output of readTree.gather) that should be built in Buildkite.
+      #
+      # These are scheduled as the first build steps and run as fast as
+      # possible, in order, without any concurrency restrictions.
+      drvTargets
+    , # Derivation map of a parent commit. Only targets which no longer
+      # correspond to the content of this map will be built. Passing an
+      # empty map will always build all targets.
+      parentTargetMap ? { }
+    , # A list of plain Buildkite step structures to run alongside the
+      # build for all drvTargets, but before proceeding with any
+      # post-build actions such as status reporting.
+      #
+      # Can be used for things like code formatting checks.
+      additionalSteps ? [ ]
+    , # A list of plain Buildkite step structures to run after all
+      # previous steps succeeded.
+      #
+      # Can be used for status reporting steps and the like.
+      postBuildSteps ? [ ]
+    }:
+    let
+      # Convert a target into all of its build and post-build steps,
+      # treated separately as they need to be in different chunks.
+      targetToSteps = target:
+        let
+          step = mkStep headBranch parentTargetMap target;
+
+          # Same step, but with an override function applied. This is
+          # used in mkExtraStep if the extra step needs to modify the
+          # parent derivation somehow.
+          #
+          # Note that this will never affect the label.
+          overridable = f: mkStep headBranch parentTargetMap (f target);
+
+          # Split build/post-build steps
+          splitExtraSteps = partition ({ postStep, ... }: postStep)
+            (attrValues (mapAttrs
+              (name: value: {
+                inherit name value;
+                postStep = (value ? prompt) || (value.postBuild or false);
+              })
+              (target.meta.ci.extraSteps or { })));
+
+          mkExtraStep' = { name, value, ... }: mkExtraStep overridable name value;
+          extraBuildSteps = map mkExtraStep' splitExtraSteps.wrong; # 'wrong' -> no prompt
+          extraPostSteps = map mkExtraStep' splitExtraSteps.right; # 'right' -> has prompt
+        in
+        {
+          buildSteps = [ step ] ++ extraBuildSteps;
+          postSteps = extraPostSteps;
+        };
+
+      # Combine all target steps into separate build and post-build step lists.
+      steps = foldl'
+        (acc: t: {
+          buildSteps = acc.buildSteps ++ t.buildSteps;
+          postSteps = acc.postSteps ++ t.postSteps;
+        })
+        { buildSteps = [ ]; postSteps = [ ]; }
+        (map targetToSteps drvTargets);
+
+      buildSteps =
+        # Add build steps for each derivation target and their extra
+        # steps.
+        steps.buildSteps
+
+        # Add additional steps (if set).
+        ++ additionalSteps;
+
+      postSteps =
+        # Add post-build steps for each derivation target.
+        steps.postSteps
+
+        # Add any globally defined post-build steps.
+        ++ postBuildSteps;
+
+      buildChunks = pipelineChunks "build" buildSteps;
+      postBuildChunks = pipelineChunks "post" postSteps;
+      chunks = buildChunks ++ postBuildChunks;
+    in
+    runCommandNoCC "buildkite-pipeline" { } ''
+      mkdir $out
+      echo "Generated ${toString (length chunks)} pipeline chunks"
+      ${
+        lib.concatMapStringsSep "\n"
+          (chunk: "cp ${chunk.path} $out/${chunk.filename}") chunks
+      }
+    '';
+
+  # Create a drvmap structure for the given targets, containing the
+  # mapping of all target paths to their derivations. The mapping can
+  # be persisted for future use.
+  mkDrvmap = drvTargets: writeText "drvmap.json" (toJSON (listToAttrs (map
+    (target: {
+      name = mkLabel target;
+      value = {
+        drvPath = unsafeDiscardStringContext target.drvPath;
+
+        # Include the attrPath in the output to reconstruct the drv
+        # without parsing the human-readable label.
+        attrPath = target.__readTree ++ lib.optionals (target ? __subtarget) [
+          target.__subtarget
+        ];
+      };
+    })
+    drvTargets)));
+
+  # Implementation of extra step logic.
+  #
+  # Each target extra step is an attribute specified in
+  # `meta.ci.extraSteps`. Its attribute name will be used as the step
+  # name on Buildkite.
+  #
+  #   command (required): A command that will be run in the depot
+  #     checkout when this step is executed. Should be a derivation
+  #     resulting in a single executable file, e.g. through
+  #     pkgs.writeShellScript.
+  #
+  #   label (optional): Human-readable label for this step to display
+  #     in the Buildkite UI instead of the attribute name.
+  #
+  #   prompt (optional): Setting this blocks the step until confirmed
+  #     by a human. Should be a string which is displayed for
+  #     confirmation. These steps always run after the main build is
+  #     done and have no influence on CI status.
+  #
+  #   postBuild (optional): If set to true, this step will run after
+  #     all primary build steps (that is, after status has been reported
+  #     back to CI).
+  #
+  #   needsOutput (optional): If set to true, the parent derivation
+  #     will be built in the working directory before running the
+  #     command. Output will be available as 'result'.
+  #     TODO: Figure out multiple-output derivations.
+  #
+  #   parentOverride (optional): A function (drv -> drv) to override
+  #     the parent's target definition when preparing its output. Only
+  #     used in extra steps that use needsOutput.
+  #
+  #   branches (optional): Git references (branches, tags ... ) on
+  #     which this step should be allowed to run. List of strings.
+  #
+  #   alwaysRun (optional): If set to true, this step will always run,
+  #     even if its parent has not been rebuilt.
+  #
+  # Note that gated steps are independent of each other.
+
+  # Create a gated step in a step group, independent from any other
+  # steps.
+  mkGatedStep = { step, label, parent, prompt }: {
+    inherit (step) depends_on;
+    group = label;
+    skip = parent.skip or false;
+
+    steps = [
+      {
+        inherit (step) branches;
+        inherit prompt;
+        block = ":radio_button: Run ${label}? (from ${parent.env.READTREE_TARGET})";
+      }
+
+      # The explicit depends_on of the wrapped step must be removed,
+      # otherwise its dependency relationship with the gate step will
+      # break.
+      (builtins.removeAttrs step [ "depends_on" ])
+    ];
+  };
+
+  # Create the Buildkite configuration for an extra step, optionally
+  # wrapping it in a gate group.
+  mkExtraStep = overridableParent: key:
+    { command
+    , label ? key
+    , prompt ? false
+    , needsOutput ? false
+    , parentOverride ? (x: x)
+    , branches ? null
+    , alwaysRun ? false
+    , postBuild ? false
+    }@cfg:
+    let
+      parent = overridableParent parentOverride;
+      parentLabel = parent.env.READTREE_TARGET;
+
+      step = {
+        label = ":gear: ${label} (from ${parentLabel})";
+        skip = if alwaysRun then false else parent.skip or false;
+        depends_on = lib.optional (!alwaysRun && !needsOutput) parent.key;
+        branches = if branches != null then lib.concatStringsSep " " branches else null;
+
+        command = pkgs.writeShellScript "${key}-script" ''
+          set -ueo pipefail
+          ${lib.optionalString needsOutput "echo '~~~ Preparing build output of ${parentLabel}'"}
+          ${lib.optionalString needsOutput parent.command}
+          echo '+++ Running extra step command'
+          exec ${command}
+        '';
+      };
+    in
+    if (isString prompt)
+    then
+      mkGatedStep
+        {
+          inherit step label parent prompt;
+        }
+    else step;
+}
diff --git a/nix/buildkite/fetch-parent-targets.sh b/nix/buildkite/fetch-parent-targets.sh
new file mode 100755
index 0000000000..8afac1e5ec
--- /dev/null
+++ b/nix/buildkite/fetch-parent-targets.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+set -ueo pipefail
+
+# Each Buildkite build stores the derivation target map as a pipeline
+# artifact. This script determines the most appropriate commit (the
+# fork point of the current chain from HEAD) and fetches the artifact.
+#
+# New builds can be based on HEAD before the pipeline for the last
+# commit has finished, in which case it is possible that the fork
+# point has no derivation map. To account for this, up to 3 commits
+# prior to HEAD are also queried to find a map.
+#
+# If no map is found, the failure mode is not critical: We simply
+# build all targets.
+
+: ${DRVMAP_PATH:=pipeline/drvmap.json}
+: ${BUILDKITE_TOKEN_PATH:=~/buildkite-token}
+
+git fetch -v origin "${BUILDKITE_PIPELINE_DEFAULT_BRANCH}"
+
+FIRST=$(git merge-base FETCH_HEAD "${BUILDKITE_COMMIT}")
+SECOND=$(git rev-parse "$FIRST~1")
+THIRD=$(git rev-parse "$FIRST~2")
+
+function most_relevant_builds {
+    set -u
+    curl 'https://graphql.buildkite.com/v1' \
+         --silent \
+         -H "Authorization: Bearer $(cat ${BUILDKITE_TOKEN_PATH})" \
+         -d "{\"query\": \"query { pipeline(slug: \\\"$BUILDKITE_ORGANIZATION_SLUG/$BUILDKITE_PIPELINE_SLUG\\\") { builds(commit: [\\\"$FIRST\\\",\\\"$SECOND\\\",\\\"$THIRD\\\"]) { edges { node { uuid }}}}}\"}" | \
+         jq -r '.data.pipeline.builds.edges[] | .node.uuid'
+}
+
+mkdir -p tmp
+for build in $(most_relevant_builds); do
+    echo "Checking artifacts for build $build"
+    buildkite-agent artifact download --build "${build}" "${DRVMAP_PATH}" 'tmp/' || true
+
+    if [[ -f "tmp/${DRVMAP_PATH}" ]]; then
+        echo "Fetched target map from build ${build}"
+        mv "tmp/${DRVMAP_PATH}" tmp/parent-target-map.json
+        break
+    fi
+done
diff --git a/nix/drvSeqL/default.nix b/nix/drvSeqL/default.nix
new file mode 100644
index 0000000000..6437e1a043
--- /dev/null
+++ b/nix/drvSeqL/default.nix
@@ -0,0 +1,47 @@
+{ depot, lib, pkgs, ... }:
+
+let
+
+  inherit (depot.nix.yants)
+    defun
+    list
+    drv
+    ;
+
+  /* Realize drvDeps, then return drvOut if that succeds.
+   * This can be used to make drvOut depend on the
+   * build success of all drvDeps without making each drvDep
+   * a dependency of drvOut.
+   * => drvOut is not rebuilt if drvDep changes
+   */
+  drvSeqL = defun [ (list drv) drv drv ]
+    (drvDeps: drvOut:
+      let
+        drvOutOutputs = drvOut.outputs or [ "out" ];
+      in
+      pkgs.runCommandLocal drvOut.name
+        {
+          # we inherit all attributes in order to replicate
+          # the original derivation as much as possible
+          outputs = drvOutOutputs;
+          passthru = drvOut.drvAttrs;
+          # depend on drvDeps (by putting it in builder context)
+          inherit drvDeps;
+        }
+        # the outputs of the original derivation are replicated
+        # by creating a symlink to the old output path
+        (lib.concatMapStrings
+          (output: ''
+            target=${lib.escapeShellArg drvOut.${output}}
+            # if the target is already a symlink, follow it until it’s not;
+            # this is done to prevent too many dereferences
+            target=$(readlink -e "$target")
+            # link to the output
+            ln -s "$target" "${"$"}${output}"
+          '')
+          drvOutOutputs));
+
+in
+{
+  __functor = _: drvSeqL;
+}
diff --git a/nix/emptyDerivation/OWNERS b/nix/emptyDerivation/OWNERS
new file mode 100644
index 0000000000..a742d0d22b
--- /dev/null
+++ b/nix/emptyDerivation/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - Profpatsch
diff --git a/nix/emptyDerivation/default.nix b/nix/emptyDerivation/default.nix
new file mode 100644
index 0000000000..8433984012
--- /dev/null
+++ b/nix/emptyDerivation/default.nix
@@ -0,0 +1,21 @@
+{ depot, pkgs, ... }:
+
+let
+  emptyDerivation = import ./emptyDerivation.nix {
+    inherit pkgs;
+    inherit (pkgs) stdenv;
+    inherit (depot.nix) getBins;
+  };
+
+  tests = import ./tests.nix {
+    inherit emptyDerivation;
+    inherit pkgs;
+    inherit (depot.nix) writeExecline getBins;
+    inherit (depot.nix.runTestsuite) runTestsuite it assertEq;
+  };
+
+in
+{
+  __functor = _: emptyDerivation;
+  inherit tests;
+}
diff --git a/nix/emptyDerivation/emptyDerivation.nix b/nix/emptyDerivation/emptyDerivation.nix
new file mode 100644
index 0000000000..772df96352
--- /dev/null
+++ b/nix/emptyDerivation/emptyDerivation.nix
@@ -0,0 +1,38 @@
+{ stdenv, pkgs, getBins }:
+
+# The empty derivation. All it does is touch $out.
+# Basically the unit value for derivations.
+#
+# In addition to simple test situations which require
+# a derivation, we set __functor, so you can call it
+# as a function and pass an attrset. The set you pass
+# is `//`-merged with the attrset before calling derivation,
+# so you can use this to add more fields.
+
+let
+  bins = getBins pkgs.s6-portable-utils [ "s6-touch" ]
+    // getBins pkgs.execline [ "importas" "exec" ];
+
+  emptiness = {
+    name = "empty-derivation";
+
+    # TODO(Profpatsch): can we get system from tvl?
+    inherit (stdenv) system;
+
+    builder = bins.exec;
+    args = [
+      bins.importas
+      "out"
+      "out"
+      bins.s6-touch
+      "$out"
+    ];
+  };
+
+in
+(derivation emptiness) // {
+  # This allows us to call the empty derivation
+  # like a function and override fields/add new fields.
+  __functor = _: overrides:
+    derivation (emptiness // overrides);
+}
diff --git a/nix/emptyDerivation/tests.nix b/nix/emptyDerivation/tests.nix
new file mode 100644
index 0000000000..a738428824
--- /dev/null
+++ b/nix/emptyDerivation/tests.nix
@@ -0,0 +1,40 @@
+{ emptyDerivation, getBins, pkgs, writeExecline, runTestsuite, it, assertEq }:
+
+let
+  bins = getBins pkgs.s6-portable-utils [ "s6-echo" ];
+
+  empty = it "is just an empty path" [
+    (assertEq "path empty"
+      (builtins.readFile emptyDerivation)
+      "")
+  ];
+
+  fooOut = emptyDerivation {
+    builder = writeExecline "foo-builder" { } [
+      "importas"
+      "out"
+      "out"
+      "redirfd"
+      "-w"
+      "1"
+      "$out"
+      bins.s6-echo
+      "-n"
+      "foo"
+    ];
+  };
+
+  overrideBuilder = it "can override the builder" [
+    (assertEq "output is foo"
+      (builtins.readFile fooOut)
+      "foo")
+    (assertEq "can add new drv variables"
+      (emptyDerivation { foo = "bar"; }).foo
+      "bar")
+  ];
+
+in
+runTestsuite "emptyDerivation" [
+  empty
+  overrideBuilder
+]
diff --git a/nix/escapeExecline/default.nix b/nix/escapeExecline/default.nix
new file mode 100644
index 0000000000..d2c39dd398
--- /dev/null
+++ b/nix/escapeExecline/default.nix
@@ -0,0 +1,32 @@
+{ lib, ... }:
+let
+  # replaces " and \ to \" and \\ respectively and quote with "
+  # e.g.
+  #   a"b\c -> "a\"b\\c"
+  #   a\"bc -> "a\\\"bc"
+  escapeExeclineArg = arg:
+    ''"${builtins.replaceStrings [ ''"'' ''\'' ] [ ''\"'' ''\\'' ] (toString arg)}"'';
+
+  # Escapes an execline (list of execline strings) to be passed to execlineb
+  # Give it a nested list of strings. Nested lists are interpolated as execline
+  # blocks ({}).
+  # Everything is quoted correctly.
+  #
+  # Example:
+  #   escapeExecline [ "if" [ "somecommand" ] "true" ]
+  #   == ''"if" { "somecommand" } "true"''
+  escapeExecline = execlineList: lib.concatStringsSep " "
+    (
+      let
+        go = arg:
+          if builtins.isString arg then [ (escapeExeclineArg arg) ]
+          else if builtins.isPath arg then [ (escapeExeclineArg "${arg}") ]
+          else if lib.isDerivation arg then [ (escapeExeclineArg arg) ]
+          else if builtins.isList arg then [ "{" ] ++ builtins.concatMap go arg ++ [ "}" ]
+          else abort "escapeExecline can only hande nested lists of strings, was ${lib.generators.toPretty {} arg}";
+      in
+      builtins.concatMap go execlineList
+    );
+
+in
+escapeExecline
diff --git a/nix/getBins/default.nix b/nix/getBins/default.nix
new file mode 100644
index 0000000000..e354b176c8
--- /dev/null
+++ b/nix/getBins/default.nix
@@ -0,0 +1,51 @@
+{ lib, pkgs, depot, ... }:
+
+# Takes a derivation and a list of binary names
+# and returns an attribute set of `name -> path`.
+# The list can also contain renames in the form of
+# `{ use, as }`, which goes `as -> usePath`.
+#
+# It is usually used to construct an attrset `bins`
+# containing all the binaries required in a file,
+# similar to a simple import system.
+#
+# Example:
+#
+#   bins = getBins pkgs.hello [ "hello" ]
+#       // getBins pkgs.coreutils [ "printf" "ln" "echo" ]
+#       // getBins pkgs.execline
+#            [ { use = "if"; as = "execlineIf" } ]
+#       // getBins pkgs.s6-portable-utils
+#            [ { use = "s6-test"; as = "test" }
+#              { use = "s6-cat"; as = "cat" }
+#            ];
+#
+#   provides
+#     bins.{hello,printf,ln,echo,execlineIf,test,cat}
+#
+
+let
+  getBins = drv: xs:
+    let
+      f = x:
+        # TODO(Profpatsch): typecheck
+        let x' = if builtins.isString x then { use = x; as = x; } else x;
+        in {
+          name = x'.as;
+          value = "${lib.getBin drv}/bin/${x'.use}";
+        };
+    in
+    builtins.listToAttrs (builtins.map f xs);
+
+
+  tests = import ./tests.nix {
+    inherit getBins;
+    inherit (depot.nix) writeScriptBin;
+    inherit (depot.nix.runTestsuite) assertEq it runTestsuite;
+  };
+
+in
+{
+  __functor = _: getBins;
+  inherit tests;
+}
diff --git a/nix/getBins/tests.nix b/nix/getBins/tests.nix
new file mode 100644
index 0000000000..e0f5ab4263
--- /dev/null
+++ b/nix/getBins/tests.nix
@@ -0,0 +1,40 @@
+{ writeScriptBin, assertEq, it, runTestsuite, getBins }:
+
+let
+  drv = writeScriptBin "hello" "it’s me";
+  drv2 = writeScriptBin "goodbye" "tschau";
+
+  bins = getBins drv [
+    "hello"
+    { use = "hello"; as = "also-hello"; }
+  ]
+  // getBins drv2 [ "goodbye" ]
+  ;
+
+  simple = it "path is equal to the executable name" [
+    (assertEq "path"
+      bins.hello
+      "${drv}/bin/hello")
+    (assertEq "content"
+      (builtins.readFile bins.hello)
+      "it’s me")
+  ];
+
+  useAs = it "use/as can be used to rename attributes" [
+    (assertEq "path"
+      bins.also-hello
+      "${drv}/bin/hello")
+  ];
+
+  secondDrv = it "by merging attrsets you can build up bins" [
+    (assertEq "path"
+      bins.goodbye
+      "${drv2}/bin/goodbye")
+  ];
+
+in
+runTestsuite "getBins" [
+  simple
+  useAs
+  secondDrv
+]
diff --git a/nix/lazy-deps/default.nix b/nix/lazy-deps/default.nix
new file mode 100644
index 0000000000..3cce48d8a5
--- /dev/null
+++ b/nix/lazy-deps/default.nix
@@ -0,0 +1,75 @@
+# Helper function to synthesize a directory of "lazy-built" binaries
+# that can be added to $PATH inside of a repository.
+#
+# Using this, a Nix shell environment in some repository can contain
+# several slow-to-build commands without blowing up evaluation and
+# build time whenever the shell is loaded.
+#
+# Note that the generated script is deliberately impure to speed up
+# evaluation, and expects both `git` and `nix-build` to exist in the
+# user's $PATH. If required, this can be done in the shell
+# configuration invoking this function.
+{ pkgs, ... }:
+
+let
+  inherit (builtins) attrNames attrValues mapAttrs;
+  inherit (pkgs.lib) concatStringsSep;
+
+  # Create the case statement for a command invocations, optionally
+  # overriding the `TARGET_TOOL` variable.
+  invoke = name: { attr, cmd ? null }: ''
+    ${name})
+      attr="${attr}"
+      ${if cmd != null then "TARGET_TOOL=\"${cmd}\"\n;;" else ";;"}
+  '';
+
+  # Create command to symlink to the dispatch script for each tool.
+  link = name: "ln -s $target $out/bin/${name}";
+
+  invocations = tools: concatStringsSep "\n" (attrValues (mapAttrs invoke tools));
+in
+
+# Attribute set of tools that should be lazily-added to the $PATH.
+
+  # The name of each attribute is used as the command name (on $PATH).
+  # It must contain the keys 'attr' (containing the Nix attribute path
+  # to the tool's derivation from the top-level), and may optionally
+  # contain the key 'cmd' to override the name of the binary inside the
+  # derivation.
+tools:
+
+pkgs.writeTextFile {
+  name = "lazy-dispatch";
+  executable = true;
+  destination = "/bin/__dispatch";
+
+  text = ''
+    #!${pkgs.runtimeShell}
+    set -ue
+
+    if ! type git>/dev/null || ! type nix-build>/dev/null; then
+      echo "The 'git' and 'nix-build' commands must be available." >&2
+      exit 127
+    fi
+
+    readonly REPO_ROOT=$(git rev-parse --show-toplevel)
+    TARGET_TOOL=$(basename "$0")
+
+    case "''${TARGET_TOOL}" in
+    ${invocations tools}
+    *)
+      echo "''${TARGET_TOOL} is currently not installed in this repository." >&2
+      exit 127
+      ;;
+    esac
+
+    result=$(nix-build --no-out-link --attr "''${attr}" "''${REPO_ROOT}")
+    PATH="''${result}/bin:$PATH"
+    exec "''${TARGET_TOOL}" "''${@}"
+  '';
+
+  checkPhase = ''
+    ${pkgs.stdenv.shellDryRun} "$target"
+    ${concatStringsSep "\n" (map link (attrNames tools))}
+  '';
+}
diff --git a/nix/mergePatch/default.nix b/nix/mergePatch/default.nix
new file mode 100644
index 0000000000..d56106925a
--- /dev/null
+++ b/nix/mergePatch/default.nix
@@ -0,0 +1,192 @@
+{ lib, depot, ... }:
+/*
+  JSON Merge-Patch for nix
+  Spec: https://tools.ietf.org/html/rfc7396
+
+  An algorithm for changing and removing fields in nested objects.
+
+  For example, given the following original document:
+
+  {
+  a = "b";
+  c = {
+      d = "e";
+      f = "g";
+  }
+  }
+
+  Changing the value of `a` and removing `f` can be achieved by merging the patch
+
+  {
+  a = "z";
+  c.f = null;
+  }
+
+  which results in
+
+  {
+  a = "z";
+  c = {
+      d = "e";
+  };
+  }
+
+  Pseudo-code:
+  define MergePatch(Target, Patch):
+      if Patch is an Object:
+        if Target is not an Object:
+          Target = {} # Ignore the contents and set it to an empty Object
+        for each Name/Value pair in Patch:
+          if Value is null:
+            if Name exists in Target:
+              remove the Name/Value pair from Target
+          else:
+            Target[Name] = MergePatch(Target[Name], Value)
+        return Target
+      else:
+        return Patch
+*/
+
+let
+  foldlAttrs = op: init: attrs:
+    lib.foldl' op init
+      (lib.mapAttrsToList lib.nameValuePair attrs);
+
+  mergePatch = target: patch:
+    if lib.isAttrs patch
+    then
+      let target' = if lib.isAttrs target then target else { };
+      in foldlAttrs
+        (acc: patchEl:
+          if patchEl.value == null
+          then removeAttrs acc [ patchEl.name ]
+          else acc // {
+            ${patchEl.name} =
+              mergePatch
+                (acc.${patchEl.name} or "unnused")
+                patchEl.value;
+          })
+        target'
+        patch
+    else patch;
+
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    ;
+
+  tests =
+    let
+      # example target from the RFC
+      testTarget = {
+        a = "b";
+        c = {
+          d = "e";
+          f = "g";
+        };
+      };
+      # example patch from the RFC
+      testPatch = {
+        a = "z";
+        c.f = null;
+      };
+      emptyPatch = it "the empty patch returns the original target" [
+        (assertEq "id"
+          (mergePatch testTarget { })
+          testTarget)
+      ];
+      nonAttrs = it "one side is a non-attrset value" [
+        (assertEq "target is a value means the value is replaced by the patch"
+          (mergePatch 42 testPatch)
+          (mergePatch { } testPatch))
+        (assertEq "patch is a value means it replaces target alltogether"
+          (mergePatch testTarget 42)
+          42)
+      ];
+      rfcExamples = it "the examples from the RFC" [
+        (assertEq "a subset is deleted and overwritten"
+          (mergePatch testTarget testPatch)
+          {
+            a = "z";
+            c = {
+              d = "e";
+            };
+          })
+        (assertEq "a more complicated example from the example section"
+          (mergePatch
+            {
+              title = "Goodbye!";
+              author = {
+                givenName = "John";
+                familyName = "Doe";
+              };
+              tags = [ "example" "sample" ];
+              content = "This will be unchanged";
+            }
+            {
+              title = "Hello!";
+              phoneNumber = "+01-123-456-7890";
+              author.familyName = null;
+              tags = [ "example" ];
+            })
+          {
+            title = "Hello!";
+            phoneNumber = "+01-123-456-7890";
+            author = {
+              givenName = "John";
+            };
+            tags = [ "example" ];
+            content = "This will be unchanged";
+          })
+      ];
+
+      rfcTests =
+        let
+          r = index: target: patch: res:
+            (assertEq "test number ${toString index}"
+              (mergePatch target patch)
+              res);
+        in
+        it "the test suite from the RFC" [
+          (r 1 { "a" = "b"; } { "a" = "c"; } { "a" = "c"; })
+          (r 2 { "a" = "b"; } { "b" = "c"; } { "a" = "b"; "b" = "c"; })
+          (r 3 { "a" = "b"; } { "a" = null; } { })
+          (r 4 { "a" = "b"; "b" = "c"; }
+            { "a" = null; }
+            { "b" = "c"; })
+          (r 5 { "a" = [ "b" ]; } { "a" = "c"; } { "a" = "c"; })
+          (r 6 { "a" = "c"; } { "a" = [ "b" ]; } { "a" = [ "b" ]; })
+          (r 7 { "a" = { "b" = "c"; }; }
+            { "a" = { "b" = "d"; "c" = null; }; }
+            { "a" = { "b" = "d"; }; })
+          (r 8 { "a" = [{ "b" = "c"; }]; }
+            { "a" = [ 1 ]; }
+            { "a" = [ 1 ]; })
+          (r 9 [ "a" "b" ] [ "c" "d" ] [ "c" "d" ])
+          (r 10 { "a" = "b"; } [ "c" ] [ "c" ])
+          (r 11 { "a" = "foo"; } null null)
+          (r 12 { "a" = "foo"; } "bar" "bar")
+          (r 13 { "e" = null; } { "a" = 1; } { "e" = null; "a" = 1; })
+          (r 14 [ 1 2 ]
+            { "a" = "b"; "c" = null; }
+            { "a" = "b"; })
+          (r 15 { }
+            { "a" = { "bb" = { "ccc" = null; }; }; }
+            { "a" = { "bb" = { }; }; })
+        ];
+
+    in
+    runTestsuite "mergePatch" [
+      emptyPatch
+      nonAttrs
+      rfcExamples
+      rfcTests
+    ];
+
+in
+{
+  __functor = _: mergePatch;
+
+  inherit tests;
+}
diff --git a/nix/netstring/attrsToKeyValList.nix b/nix/netstring/attrsToKeyValList.nix
new file mode 100644
index 0000000000..c854b56955
--- /dev/null
+++ b/nix/netstring/attrsToKeyValList.nix
@@ -0,0 +1,33 @@
+{ depot, lib, ... }:
+
+# Convert an attrset of strings to a list of key/value netstring pairs.
+# A good minimally viable json replacement if all you need is to iterate.
+# You can use e.g. `forstdin -Ed '' item` in execline to split the items
+# and then get the key and value via `multidefine -d '' $item { key value }`
+#
+# Example:
+#   { foo = "bar"; x = "abc"; }
+#   => "12:3:foo,3:bar,,10:1:x,3:abc,,"
+#
+# Example with runExecline:
+#   nix.runExecline "test" {
+#     stdin = nix.netstring.attrsToKeyValList {
+#       foo = "bar";
+#       x = "abc";
+#     };
+#   } [
+#     "forstdin" "-Ed" "" "item"
+#     "multidefine" "-d" "" "$item" [ "key" "value" ]
+#     "${pkgs.coreutils}/bin/echo" "\${key} -> \${value}"
+#   ]
+
+#   will print:
+#     foo -> bar
+#     x -> abc
+attrs:
+lib.concatStrings
+  (lib.mapAttrsToList
+    (k: v: depot.nix.netstring.fromString
+      (depot.nix.netstring.fromString k
+        + depot.nix.netstring.fromString v))
+    attrs)
diff --git a/nix/netstring/fromString.nix b/nix/netstring/fromString.nix
new file mode 100644
index 0000000000..dcd688a8b0
--- /dev/null
+++ b/nix/netstring/fromString.nix
@@ -0,0 +1,10 @@
+{ ... }:
+# convert any nix string into a netstring
+# (prefixed by its length) according to https://en.wikipedia.org/wiki/Netstring
+#
+# Examples:
+#   netstring.fromString "foo"
+#   => "3:foo,"
+#   netstring.fromString ""
+#   => "0:,"
+s: "${toString (builtins.stringLength s)}:${s},"
diff --git a/nix/nint/OWNERS b/nix/nint/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/nix/nint/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/nix/nint/README.md b/nix/nint/README.md
new file mode 100644
index 0000000000..369a827619
--- /dev/null
+++ b/nix/nint/README.md
@@ -0,0 +1,93 @@
+# nint — Nix INTerpreter
+
+`nint` is a shebang compatible interpreter for nix. It is currently
+implemented as a fairly trivial wrapper around `nix-instantiate --eval`.
+It allows to run nix expressions as command line tools if they conform
+to the following calling convention:
+
+* Every nix script needs to evaluate to a function which takes an
+  attribute set as its single argument. Ideally a set pattern with
+  an ellipsis should be used. By default `nint` passes the following
+  arguments:
+
+  * `currentDir`: the current working directory as a nix path
+  * `argv`: a list of arguments to the invokation including the
+    program name at `builtins.head argv`.
+  * Extra arguments can be manually passed as described below.
+
+* The return value must either be
+
+  * A string which is rendered to `stdout`.
+
+  * An attribute set with the following optional attributes:
+
+    * `stdout`: A string that's rendered to `stdout`
+    * `stderr`: A string that's rendered to `stderr`
+    * `exit`: A number which is used as an exit code.
+      If missing, nint always exits with 0 (or equivalent).
+
+## Usage
+
+```
+nint [ --arg ARG VALUE … ] script.nix [ ARGS … ]
+```
+
+Instead of `--arg`, `--argstr` can also be used. They both work
+like the flags of the same name for `nix-instantiate` and may
+be specified any number of times as long as they are passed
+*before* the nix expression to run.
+
+Below is a shebang which also passes `depot` as an argument
+(note the usage of `env -S` to get around the shebang limitation
+to two arguments).
+
+```nix
+#!/usr/bin/env -S nint --arg depot /path/to/depot
+```
+
+## Limitations
+
+* No side effects except for writing to `stdout`.
+
+* Output is not streaming, i. e. even if the output is incrementally
+  calculated, nothing will be printed until the full output is available.
+  With plain nix strings we can't do better anyways.
+
+* Limited error handling for the script, no way to set the exit code etc.
+
+Some of these limitations may be possible to address in the future by using
+an alternative nix interpreter and a more elaborate calling convention.
+
+## Example
+
+Below is a (very simple) implementation of a `ls(1)`-like program in nix:
+
+```nix
+#!/usr/bin/env nint
+{ currentDir, argv, ... }:
+
+let
+  lib = import <nixpkgs/lib>;
+
+  dirs =
+    let
+      args = builtins.tail argv;
+    in
+      if args == []
+      then [ currentDir ]
+      else args;
+
+  makeAbsolute = p:
+    if builtins.isPath p
+    then p
+    else if builtins.match "^/.*" p != null
+    then p
+    else "${toString currentDir}/${p}";
+in
+
+  lib.concatStringsSep "\n"
+    (lib.flatten
+      (builtins.map
+        (d: (builtins.attrNames (builtins.readDir (makeAbsolute d))))
+        dirs)) + "\n"
+```
diff --git a/nix/nint/default.nix b/nix/nint/default.nix
new file mode 100644
index 0000000000..0087fc0416
--- /dev/null
+++ b/nix/nint/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix.writers)
+    rustSimpleBin
+    ;
+in
+
+rustSimpleBin
+{
+  name = "nint";
+  dependencies = [
+    depot.third_party.rust-crates.serde_json
+  ];
+}
+  (builtins.readFile ./nint.rs)
diff --git a/nix/nint/nint.rs b/nix/nint/nint.rs
new file mode 100644
index 0000000000..abb0153c3a
--- /dev/null
+++ b/nix/nint/nint.rs
@@ -0,0 +1,149 @@
+extern crate serde_json;
+
+use serde_json::Value;
+use std::convert::TryFrom;
+use std::ffi::OsString;
+use std::io::{stderr, stdout, Error, ErrorKind, Write};
+use std::os::unix::ffi::{OsStrExt, OsStringExt};
+use std::process::Command;
+
+fn render_nix_string(s: &OsString) -> OsString {
+    let mut rendered = Vec::new();
+
+    rendered.extend(b"\"");
+
+    for b in s.as_os_str().as_bytes() {
+        match char::from(*b) {
+            '\"' => rendered.extend(b"\\\""),
+            '\\' => rendered.extend(b"\\\\"),
+            '$' => rendered.extend(b"\\$"),
+            _ => rendered.push(*b),
+        }
+    }
+
+    rendered.extend(b"\"");
+
+    OsString::from_vec(rendered)
+}
+
+fn render_nix_list(arr: &[OsString]) -> OsString {
+    let mut rendered = Vec::new();
+
+    rendered.extend(b"[ ");
+
+    for el in arr {
+        rendered.extend(render_nix_string(el).as_os_str().as_bytes());
+        rendered.extend(b" ");
+    }
+
+    rendered.extend(b"]");
+
+    OsString::from_vec(rendered)
+}
+
+/// Slightly overkill helper macro which takes a `Map<String, Value>` obtained
+/// from `Value::Object` and an output name (`stderr` or `stdout`) as an
+/// identifier. If a value exists for the given output in the object it gets
+/// written to the appropriate output.
+macro_rules! handle_set_output {
+    ($map_name:ident, $output_name:ident) => {
+        match $map_name.get(stringify!($output_name)) {
+            Some(Value::String(s)) => $output_name().write_all(s.as_bytes()),
+            Some(_) => Err(Error::new(
+                ErrorKind::Other,
+                format!("Attribute {} must be a string!", stringify!($output_name)),
+            )),
+            None => Ok(()),
+        }
+    };
+}
+
+fn main() -> std::io::Result<()> {
+    let mut nix_args = Vec::new();
+
+    let mut args = std::env::args_os().into_iter();
+    let mut in_args = true;
+
+    let mut argv: Vec<OsString> = Vec::new();
+
+    // skip argv[0]
+    args.next();
+
+    loop {
+        let arg = match args.next() {
+            Some(a) => a,
+            None => break,
+        };
+
+        if !arg.to_str().map(|s| s.starts_with("-")).unwrap_or(false) {
+            in_args = false;
+        }
+
+        if in_args {
+            match (arg.to_str()) {
+                Some("--arg") | Some("--argstr") => {
+                    nix_args.push(arg);
+                    nix_args.push(args.next().unwrap());
+                    nix_args.push(args.next().unwrap());
+                    Ok(())
+                }
+                _ => Err(Error::new(ErrorKind::Other, "unknown argument")),
+            }?
+        } else {
+            argv.push(arg);
+        }
+    }
+
+    if argv.len() < 1 {
+        Err(Error::new(ErrorKind::Other, "missing argv"))
+    } else {
+        let cd = std::env::current_dir()?.into_os_string();
+
+        nix_args.push(OsString::from("--arg"));
+        nix_args.push(OsString::from("currentDir"));
+        nix_args.push(cd);
+
+        nix_args.push(OsString::from("--arg"));
+        nix_args.push(OsString::from("argv"));
+        nix_args.push(render_nix_list(&argv[..]));
+
+        nix_args.push(OsString::from("--eval"));
+        nix_args.push(OsString::from("--strict"));
+        nix_args.push(OsString::from("--json"));
+
+        nix_args.push(argv[0].clone());
+
+        let run = Command::new("nix-instantiate").args(nix_args).output()?;
+
+        match serde_json::from_slice(&run.stdout[..]) {
+            Ok(Value::String(s)) => stdout().write_all(s.as_bytes()),
+            Ok(Value::Object(m)) => {
+                handle_set_output!(m, stdout)?;
+                handle_set_output!(m, stderr)?;
+
+                match m.get("exit") {
+                    Some(Value::Number(n)) => {
+                        let code = n.as_i64().and_then(|v| i32::try_from(v).ok());
+
+                        match code {
+                            Some(i) => std::process::exit(i),
+                            None => {
+                                Err(Error::new(ErrorKind::Other, "Attribute exit is not an i32"))
+                            }
+                        }
+                    }
+                    Some(_) => Err(Error::new(ErrorKind::Other, "exit must be a number")),
+                    None => Ok(()),
+                }
+            }
+            Ok(_) => Err(Error::new(
+                ErrorKind::Other,
+                "output must be a string or an object",
+            )),
+            _ => {
+                stderr().write_all(&run.stderr[..]);
+                Err(Error::new(ErrorKind::Other, "internal nix error"))
+            }
+        }
+    }
+}
diff --git a/nix/readTree/README.md b/nix/readTree/README.md
new file mode 100644
index 0000000000..f8bbe2255e
--- /dev/null
+++ b/nix/readTree/README.md
@@ -0,0 +1,92 @@
+readTree
+========
+
+This is a Nix program that builds up an attribute set tree for a large
+repository based on the filesystem layout.
+
+It is in fact the tool that lays out the attribute set of this repository.
+
+As an example, consider a root (`.`) of a repository and a layout such as:
+
+```
+.
+├── third_party
+│   ├── default.nix
+│   └── rustpkgs
+│       ├── aho-corasick.nix
+│       └── serde.nix
+└── tools
+    ├── cheddar
+    │   └── default.nix
+    └── roquefort.nix
+```
+
+When `readTree` is called on that tree, it will construct an attribute set with
+this shape:
+
+```nix
+{
+    tools = {
+        cheddar = ...;
+        roquefort = ...;
+    };
+
+    third_party = {
+        # the `default.nix` of this folder might have had arbitrary other
+        # attributes here, such as this:
+        favouriteColour = "orange";
+
+        rustpkgs = {
+            aho-corasick = ...;
+            serde = ...;
+        };
+    };
+}
+```
+
+Every imported Nix file that yields an attribute set will have a `__readTree =
+true;` attribute merged into it.
+
+## Traversal logic
+
+`readTree` will follow any subdirectories of a tree and import all Nix files,
+with some exceptions:
+
+* A folder can declare that its children are off-limit by containing a
+  `.skip-subtree` file. Since the content of the file is not checked, it can be
+  useful to leave a note for a human in the file.
+* If a folder contains a `default.nix` file, no *sibling* Nix files will be
+  imported - however children are traversed as normal.
+* If a folder contains a `default.nix` it is loaded and, if it evaluates to a
+  set, *merged* with the children. If it evaluates to anything else the children
+  are *not traversed*.
+* The `default.nix` of the top-level folder on which readTree is
+  called is **not** read to avoid infinite recursion (as, presumably,
+  this file is where readTree itself is called).
+
+Traversal is lazy, `readTree` will only build up the tree as requested. This
+currently has the downside that directories with no importable files end up in
+the tree as empty nodes (`{}`).
+
+## Import structure
+
+`readTree` is called with an argument set containing a few parameters:
+
+* `path`: Initial path at which to start the traversal.
+* `args`: Arguments to pass to all imports.
+* `filter`: (optional) A function to filter the argument set on each
+  import based on the location in the tree. This can be used to, for
+  example, implement a "visibility" system inside of a tree.
+* `scopedArgs`: (optional) An argument set that is passed to all
+  imported files via `builtins.scopedImport`. This will forcefully
+  override the given values in the import scope, use with care!
+
+The package headers in this repository follow the form `{ pkgs, ... }:` where
+`pkgs` is a fixed-point of the entire package tree (see the `default.nix` at the
+root of the depot).
+
+In theory `readTree` can pass arguments of different shapes, but I have found
+this to be a good solution for the most part.
+
+Note that `readTree` does not currently make functions overridable, though it is
+feasible that it could do that in the future.
diff --git a/nix/readTree/default.nix b/nix/readTree/default.nix
new file mode 100644
index 0000000000..ba3363d8d6
--- /dev/null
+++ b/nix/readTree/default.nix
@@ -0,0 +1,284 @@
+# Copyright (c) 2019 Vincent Ambo
+# Copyright (c) 2020-2021 The TVL Authors
+# SPDX-License-Identifier: MIT
+#
+# Provides a function to automatically read a a filesystem structure
+# into a Nix attribute set.
+#
+# Called with an attribute set taking the following arguments:
+#
+#   path: Path to a directory from which to start reading the tree.
+#
+#   args: Argument set to pass to each imported file.
+#
+#   filter: Function to filter `args` based on the tree location. This should
+#           be a function of the form `args -> location -> args`, where the
+#           location is a list of strings representing the path components of
+#           the current readTree target. Optional.
+{ ... }:
+
+let
+  inherit (builtins)
+    attrNames
+    concatMap
+    concatStringsSep
+    elem
+    elemAt
+    filter
+    hasAttr
+    head
+    isAttrs
+    listToAttrs
+    map
+    match
+    readDir
+    substring;
+
+  argsWithPath = args: parts:
+    let meta.locatedAt = parts;
+    in meta // (if isAttrs args then args else args meta);
+
+  readDirVisible = path:
+    let
+      children = readDir path;
+      isVisible = f: f == ".skip-subtree" || (substring 0 1 f) != ".";
+      names = filter isVisible (attrNames children);
+    in
+    listToAttrs (map
+      (name: {
+        inherit name;
+        value = children.${name};
+      })
+      names);
+
+  # Create a mark containing the location of this attribute and
+  # a list of all child attribute names added by readTree.
+  marker = parts: children: {
+    __readTree = parts;
+    __readTreeChildren = builtins.attrNames children;
+  };
+
+  # Create a label from a target's tree location.
+  mkLabel = target:
+    let label = concatStringsSep "/" target.__readTree;
+    in if target ? __subtarget
+    then "${label}:${target.__subtarget}"
+    else label;
+
+  # Merge two attribute sets, but place attributes in `passthru` via
+  # `overrideAttrs` for derivation targets that support it.
+  merge = a: b:
+    if a ? overrideAttrs
+    then
+      a.overrideAttrs
+        (prev: {
+          passthru = (prev.passthru or { }) // b;
+        })
+    else a // b;
+
+  # Import a file and enforce our calling convention
+  importFile = args: scopedArgs: path: parts: filter:
+    let
+      importedFile =
+        if scopedArgs != { }
+        then builtins.scopedImport scopedArgs path
+        else import path;
+      pathType = builtins.typeOf importedFile;
+    in
+    if pathType != "lambda"
+    then builtins.throw "readTree: trying to import ${toString path}, but it’s a ${pathType}, you need to make it a function like { depot, pkgs, ... }"
+    else importedFile (filter parts (argsWithPath args parts));
+
+  nixFileName = file:
+    let res = match "(.*)\\.nix" file;
+    in if res == null then null else head res;
+
+  readTree = { args, initPath, rootDir, parts, argsFilter, scopedArgs }:
+    let
+      dir = readDirVisible initPath;
+      joinChild = c: initPath + ("/" + c);
+
+      self =
+        if rootDir
+        then { __readTree = [ ]; }
+        else importFile args scopedArgs initPath parts argsFilter;
+
+      # Import subdirectories of the current one, unless the special
+      # `.skip-subtree` file exists which makes readTree ignore the
+      # children.
+      #
+      # This file can optionally contain information on why the tree
+      # should be ignored, but its content is not inspected by
+      # readTree
+      filterDir = f: dir."${f}" == "directory";
+      children = if hasAttr ".skip-subtree" dir then [ ] else
+      map
+        (c: {
+          name = c;
+          value = readTree {
+            inherit argsFilter scopedArgs;
+            args = args;
+            initPath = (joinChild c);
+            rootDir = false;
+            parts = (parts ++ [ c ]);
+          };
+        })
+        (filter filterDir (attrNames dir));
+
+      # Import Nix files
+      nixFiles =
+        if hasAttr ".skip-subtree" dir then [ ]
+        else filter (f: f != null) (map nixFileName (attrNames dir));
+      nixChildren = map
+        (c:
+          let
+            p = joinChild (c + ".nix");
+            childParts = parts ++ [ c ];
+            imported = importFile args scopedArgs p childParts argsFilter;
+          in
+          {
+            name = c;
+            value =
+              if isAttrs imported
+              then merge imported (marker childParts { })
+              else imported;
+          })
+        nixFiles;
+
+      nodeValue = if dir ? "default.nix" then self else { };
+
+      allChildren = listToAttrs (
+        if dir ? "default.nix"
+        then children
+        else nixChildren ++ children
+      );
+
+    in
+    if isAttrs nodeValue
+    then merge nodeValue (allChildren // (marker parts allChildren))
+    else nodeValue;
+
+  # Helper function to fetch subtargets from a target. This is a
+  # temporary helper to warn on the use of the `meta.targets`
+  # attribute, which is deprecated in favour of `meta.ci.targets`.
+  subtargets = node:
+    let targets = (node.meta.targets or [ ]) ++ (node.meta.ci.targets or [ ]);
+    in if node ? meta.targets then
+      builtins.trace ''
+        Warning: The meta.targets attribute is deprecated.
+
+        Please move the subtargets of //${mkLabel node} to the
+        meta.ci.targets attribute.
+        
+      ''
+        targets else targets;
+
+  # Function which can be used to find all readTree targets within an
+  # attribute set.
+  #
+  # This function will gather physical targets, that is targets which
+  # correspond directly to a location in the repository, as well as
+  # subtargets (specified in the meta.ci.targets attribute of a node).
+  #
+  # This can be used to discover targets for inclusion in CI
+  # pipelines.
+  #
+  # Called with the arguments:
+  #
+  #   eligible: Function to determine whether the given derivation
+  #             should be included in the build.
+  gather = eligible: node:
+    if node ? __readTree then
+    # Include the node itself if it is eligible.
+      (if eligible node then [ node ] else [ ])
+      # Include eligible children of the node
+      ++ concatMap (gather eligible) (map (attr: node."${attr}") node.__readTreeChildren)
+      # Include specified sub-targets of the node
+      ++ filter eligible (map
+        (k: (node."${k}" or { }) // {
+          # Keep the same tree location, but explicitly mark this
+          # node as a subtarget.
+          __readTree = node.__readTree;
+          __readTreeChildren = [ ];
+          __subtarget = k;
+        })
+        (subtargets node))
+    else [ ];
+
+  # Determine whether a given value is a derivation.
+  # Copied from nixpkgs/lib for cases where lib is not available yet.
+  isDerivation = x: isAttrs x && x ? type && x.type == "derivation";
+in
+{
+  inherit gather mkLabel;
+
+  __functor = _:
+    { path
+    , args
+    , filter ? (_parts: x: x)
+    , scopedArgs ? { }
+    }:
+    readTree {
+      inherit args scopedArgs;
+      argsFilter = filter;
+      initPath = path;
+      rootDir = true;
+      parts = [ ];
+    };
+
+  # In addition to readTree itself, some functionality is exposed that
+  # is useful for users of readTree.
+
+  # Create a readTree filter disallowing access to the specified
+  # top-level folder in the repository, except for specific exceptions
+  # specified by their (full) paths.
+  #
+  # Called with the arguments:
+  #
+  #   folder: Name of the restricted top-level folder (e.g. 'experimental')
+  #
+  #   exceptions: List of readTree parts (e.g. [ [ "services" "some-app" ] ]),
+  #               which should be able to access the restricted folder.
+  #
+  #   reason: Textual explanation for the restriction (included in errors)
+  restrictFolder = { folder, exceptions ? [ ], reason }: parts: args:
+    if (elemAt parts 0) == folder || elem parts exceptions
+    then args
+    else args // {
+      depot = args.depot // {
+        "${folder}" = throw ''
+          Access to targets under //${folder} is not permitted from
+          other repository paths. Specific exceptions are configured
+          at the top-level.
+
+          ${reason}
+          At location: ${builtins.concatStringsSep "." parts}
+        '';
+      };
+    };
+
+  # This definition of fix is identical to <nixpkgs>.lib.fix, but is
+  # provided here for cases where readTree is used before nixpkgs can
+  # be imported.
+  #
+  # It is often required to create the args attribute set.
+  fix = f: let x = f x; in x;
+
+  # Takes an attribute set and adds a meta.ci.targets attribute to it
+  # which contains all direct children of the attribute set which are
+  # derivations.
+  #
+  # Type: attrs -> attrs
+  drvTargets = attrs:
+    attrs // {
+      # preserve .meta from original attrs
+      meta = (attrs.meta or { }) // {
+        # preserve .meta.ci (except .targets) from original attrs
+        ci = (attrs.meta.ci or { }) // {
+          targets = builtins.filter
+            (x: isDerivation attrs."${x}")
+            (builtins.attrNames attrs);
+        };
+      };
+    };
+}
diff --git a/nix/readTree/tests/.skip-subtree b/nix/readTree/tests/.skip-subtree
new file mode 100644
index 0000000000..46952a4ca6
--- /dev/null
+++ b/nix/readTree/tests/.skip-subtree
@@ -0,0 +1 @@
+These tests call their own readTree, so the toplevel one shouldn’t bother
diff --git a/nix/readTree/tests/default.nix b/nix/readTree/tests/default.nix
new file mode 100644
index 0000000000..fcca141714
--- /dev/null
+++ b/nix/readTree/tests/default.nix
@@ -0,0 +1,129 @@
+{ depot, lib, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    ;
+
+  tree-ex = depot.nix.readTree {
+    path = ./test-example;
+    args = { };
+  };
+
+  example = it "corresponds to the README example" [
+    (assertEq "third_party attrset"
+      (lib.isAttrs tree-ex.third_party
+        && (! lib.isDerivation tree-ex.third_party))
+      true)
+    (assertEq "third_party attrset other attribute"
+      tree-ex.third_party.favouriteColour
+      "orange")
+    (assertEq "rustpkgs attrset aho-corasick"
+      tree-ex.third_party.rustpkgs.aho-corasick
+      "aho-corasick")
+    (assertEq "rustpkgs attrset serde"
+      tree-ex.third_party.rustpkgs.serde
+      "serde")
+    (assertEq "tools cheddear"
+      "cheddar"
+      tree-ex.tools.cheddar)
+    (assertEq "tools roquefort"
+      tree-ex.tools.roquefort
+      "roquefort")
+  ];
+
+  tree-tl = depot.nix.readTree {
+    path = ./test-tree-traversal;
+    args = { };
+  };
+
+  traversal-logic = it "corresponds to the traversal logic in the README" [
+    (assertEq "skip subtree default.nix is read"
+      tree-tl.skip-subtree.but
+      "the default.nix is still read")
+    (assertEq "skip subtree a/default.nix is skipped"
+      (tree-tl.skip-subtree ? a)
+      false)
+    (assertEq "skip subtree b/c.nix is skipped"
+      (tree-tl.skip-subtree ? b)
+      false)
+    (assertEq "skip subtree a/default.nix would be read without .skip-subtree"
+      (tree-tl.no-skip-subtree.a)
+      "am I subtree yet?")
+    (assertEq "skip subtree b/c.nix would be read without .skip-subtree"
+      (tree-tl.no-skip-subtree.b.c)
+      "cool")
+
+    (assertEq "default.nix attrset is merged with siblings"
+      tree-tl.default-nix.no
+      "siblings should be read")
+    (assertEq "default.nix means sibling isn’t read"
+      (tree-tl.default-nix ? sibling)
+      false)
+    (assertEq "default.nix means subdirs are still read and merged into default.nix"
+      (tree-tl.default-nix.subdir.a)
+      "but I’m picked up")
+
+    (assertEq "default.nix can be not an attrset"
+      tree-tl.default-nix.no-merge
+      "I’m not merged with any children")
+    (assertEq "default.nix is not an attrset -> children are not merged"
+      (tree-tl.default-nix.no-merge ? subdir)
+      false)
+
+    (assertEq "default.nix can contain a derivation"
+      (lib.isDerivation tree-tl.default-nix.can-be-drv)
+      true)
+    (assertEq "Even if default.nix is a derivation, children are traversed and merged"
+      tree-tl.default-nix.can-be-drv.subdir.a
+      "Picked up through the drv")
+    (assertEq "default.nix drv is not changed by readTree"
+      tree-tl.default-nix.can-be-drv
+      (import ./test-tree-traversal/default-nix/can-be-drv/default.nix { }))
+  ];
+
+  # these each call readTree themselves because the throws have to happen inside assertThrows
+  wrong = it "cannot read these files and will complain" [
+    (assertThrows "this file is not a function"
+      (depot.nix.readTree {
+        path = ./test-wrong-not-a-function;
+        args = { };
+      }).not-a-function)
+    # can’t test for that, assertThrows can’t catch this error
+    # (assertThrows "this file is a function but doesn’t have dots"
+    #   (depot.nix.readTree {} ./test-wrong-no-dots).no-dots-in-function)
+  ];
+
+  read-markers = depot.nix.readTree {
+    path = ./test-marker;
+    args = { };
+  };
+
+  assertMarkerByPath = path:
+    assertEq "${lib.concatStringsSep "." path} is marked correctly"
+      (lib.getAttrFromPath path read-markers).__readTree
+      path;
+
+  markers = it "marks nodes correctly" [
+    (assertMarkerByPath [ "directory-marked" ])
+    (assertMarkerByPath [ "directory-marked" "nested" ])
+    (assertMarkerByPath [ "file-children" "one" ])
+    (assertMarkerByPath [ "file-children" "two" ])
+    (assertEq "nix file children are marked correctly"
+      read-markers.file-children.__readTreeChildren [ "one" "two" ])
+    (assertEq "directory children are marked correctly"
+      read-markers.directory-marked.__readTreeChildren [ "nested" ])
+    (assertEq "absence of children is marked"
+      read-markers.directory-marked.nested.__readTreeChildren [ ])
+  ];
+
+in
+runTestsuite "readTree" [
+  example
+  traversal-logic
+  wrong
+  markers
+]
diff --git a/nix/readTree/tests/test-example/third_party/default.nix b/nix/readTree/tests/test-example/third_party/default.nix
new file mode 100644
index 0000000000..27d8724218
--- /dev/null
+++ b/nix/readTree/tests/test-example/third_party/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  favouriteColour = "orange";
+}
diff --git a/nix/readTree/tests/test-example/third_party/rustpkgs/aho-corasick.nix b/nix/readTree/tests/test-example/third_party/rustpkgs/aho-corasick.nix
new file mode 100644
index 0000000000..f964fa583c
--- /dev/null
+++ b/nix/readTree/tests/test-example/third_party/rustpkgs/aho-corasick.nix
@@ -0,0 +1 @@
+{ ... }: "aho-corasick"
diff --git a/nix/readTree/tests/test-example/third_party/rustpkgs/serde.nix b/nix/readTree/tests/test-example/third_party/rustpkgs/serde.nix
new file mode 100644
index 0000000000..54b3c0503f
--- /dev/null
+++ b/nix/readTree/tests/test-example/third_party/rustpkgs/serde.nix
@@ -0,0 +1 @@
+{ ... }: "serde"
diff --git a/nix/readTree/tests/test-example/tools/cheddar/default.nix b/nix/readTree/tests/test-example/tools/cheddar/default.nix
new file mode 100644
index 0000000000..a1919442d8
--- /dev/null
+++ b/nix/readTree/tests/test-example/tools/cheddar/default.nix
@@ -0,0 +1 @@
+{ ... }: "cheddar"
diff --git a/nix/readTree/tests/test-example/tools/roquefort.nix b/nix/readTree/tests/test-example/tools/roquefort.nix
new file mode 100644
index 0000000000..03c7492bb6
--- /dev/null
+++ b/nix/readTree/tests/test-example/tools/roquefort.nix
@@ -0,0 +1 @@
+{ ... }: "roquefort"
diff --git a/nix/readTree/tests/test-marker/directory-marked/default.nix b/nix/readTree/tests/test-marker/directory-marked/default.nix
new file mode 100644
index 0000000000..5bd3e36b53
--- /dev/null
+++ b/nix/readTree/tests/test-marker/directory-marked/default.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+{ }
diff --git a/nix/readTree/tests/test-marker/directory-marked/nested/default.nix b/nix/readTree/tests/test-marker/directory-marked/nested/default.nix
new file mode 100644
index 0000000000..5bd3e36b53
--- /dev/null
+++ b/nix/readTree/tests/test-marker/directory-marked/nested/default.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+{ }
diff --git a/nix/readTree/tests/test-marker/file-children/one.nix b/nix/readTree/tests/test-marker/file-children/one.nix
new file mode 100644
index 0000000000..5bd3e36b53
--- /dev/null
+++ b/nix/readTree/tests/test-marker/file-children/one.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+{ }
diff --git a/nix/readTree/tests/test-marker/file-children/two.nix b/nix/readTree/tests/test-marker/file-children/two.nix
new file mode 100644
index 0000000000..5bd3e36b53
--- /dev/null
+++ b/nix/readTree/tests/test-marker/file-children/two.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+{ }
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/default.nix b/nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/default.nix
new file mode 100644
index 0000000000..95d13d3c27
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/default.nix
@@ -0,0 +1,7 @@
+{ ... }:
+derivation {
+  name = "im-a-drv";
+  system = builtins.currentSystem;
+  builder = "/bin/sh";
+  args = [ "-c" ''echo "" > $out'' ];
+}
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/subdir/a.nix b/nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/subdir/a.nix
new file mode 100644
index 0000000000..2ee2d648f0
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/can-be-drv/subdir/a.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"Picked up through the drv"
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/default.nix b/nix/readTree/tests/test-tree-traversal/default-nix/default.nix
new file mode 100644
index 0000000000..6003b53983
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  no = "siblings should be read";
+}
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/no-merge/default.nix b/nix/readTree/tests/test-tree-traversal/default-nix/no-merge/default.nix
new file mode 100644
index 0000000000..c469533fbe
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/no-merge/default.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"I’m not merged with any children"
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/no-merge/subdir/a.nix b/nix/readTree/tests/test-tree-traversal/default-nix/no-merge/subdir/a.nix
new file mode 100644
index 0000000000..2008c2d241
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/no-merge/subdir/a.nix
@@ -0,0 +1 @@
+"not accessible since parent default.nix is not an attrset"
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/sibling.nix b/nix/readTree/tests/test-tree-traversal/default-nix/sibling.nix
new file mode 100644
index 0000000000..8c57f2c161
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/sibling.nix
@@ -0,0 +1 @@
+"I’m left alone"
diff --git a/nix/readTree/tests/test-tree-traversal/default-nix/subdir/a.nix b/nix/readTree/tests/test-tree-traversal/default-nix/subdir/a.nix
new file mode 100644
index 0000000000..cf0ac2c8f3
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/default-nix/subdir/a.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"but I’m picked up"
diff --git a/nix/readTree/tests/test-tree-traversal/no-skip-subtree/a/default.nix b/nix/readTree/tests/test-tree-traversal/no-skip-subtree/a/default.nix
new file mode 100644
index 0000000000..a586ce534c
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/no-skip-subtree/a/default.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"am I subtree yet?"
diff --git a/nix/readTree/tests/test-tree-traversal/no-skip-subtree/b/c.nix b/nix/readTree/tests/test-tree-traversal/no-skip-subtree/b/c.nix
new file mode 100644
index 0000000000..06216c417b
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/no-skip-subtree/b/c.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"cool"
diff --git a/nix/readTree/tests/test-tree-traversal/no-skip-subtree/default.nix b/nix/readTree/tests/test-tree-traversal/no-skip-subtree/default.nix
new file mode 100644
index 0000000000..3d9f241cdd
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/no-skip-subtree/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  but = "the default.nix is still read";
+}
diff --git a/nix/readTree/tests/test-tree-traversal/skip-subtree/.skip-subtree b/nix/readTree/tests/test-tree-traversal/skip-subtree/.skip-subtree
new file mode 100644
index 0000000000..87271ba5e1
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-subtree/.skip-subtree
@@ -0,0 +1 @@
+this file makes subdirs be skipped, I hope
diff --git a/nix/readTree/tests/test-tree-traversal/skip-subtree/a/default.nix b/nix/readTree/tests/test-tree-traversal/skip-subtree/a/default.nix
new file mode 100644
index 0000000000..a586ce534c
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-subtree/a/default.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"am I subtree yet?"
diff --git a/nix/readTree/tests/test-tree-traversal/skip-subtree/b/c.nix b/nix/readTree/tests/test-tree-traversal/skip-subtree/b/c.nix
new file mode 100644
index 0000000000..06216c417b
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-subtree/b/c.nix
@@ -0,0 +1,3 @@
+{ ... }:
+
+"cool"
diff --git a/nix/readTree/tests/test-tree-traversal/skip-subtree/default.nix b/nix/readTree/tests/test-tree-traversal/skip-subtree/default.nix
new file mode 100644
index 0000000000..3d9f241cdd
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-subtree/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  but = "the default.nix is still read";
+}
diff --git a/nix/readTree/tests/test-wrong-no-dots/no-dots-in-function.nix b/nix/readTree/tests/test-wrong-no-dots/no-dots-in-function.nix
new file mode 100644
index 0000000000..4681253af8
--- /dev/null
+++ b/nix/readTree/tests/test-wrong-no-dots/no-dots-in-function.nix
@@ -0,0 +1,3 @@
+{}:
+
+"This is a function, but readTree wants to pass a bunch of arguments, and not having dots means we depend on exactly which arguments."
diff --git a/nix/readTree/tests/test-wrong-not-a-function/not-a-function.nix b/nix/readTree/tests/test-wrong-not-a-function/not-a-function.nix
new file mode 100644
index 0000000000..f46ee2a355
--- /dev/null
+++ b/nix/readTree/tests/test-wrong-not-a-function/not-a-function.nix
@@ -0,0 +1 @@
+"This file needs to be a function, otherwise readTree doesn’t like it!"
diff --git a/nix/renderMarkdown/default.nix b/nix/renderMarkdown/default.nix
new file mode 100644
index 0000000000..8d6b31cfcc
--- /dev/null
+++ b/nix/renderMarkdown/default.nix
@@ -0,0 +1,8 @@
+# Render a Markdown file to HTML.
+{ depot, pkgs, ... }:
+
+with depot.nix.yants;
+
+defun [ path drv ] (file: pkgs.runCommandNoCC "${file}.rendered.html" { } ''
+  cat ${file} | ${depot.tools.cheddar}/bin/cheddar --about-filter ${file} > $out
+'')
diff --git a/nix/runExecline/default.nix b/nix/runExecline/default.nix
new file mode 100644
index 0000000000..76fffdce7b
--- /dev/null
+++ b/nix/runExecline/default.nix
@@ -0,0 +1,31 @@
+{ depot, pkgs, lib, ... }:
+let
+  runExecline = import ./runExecline.nix {
+    inherit (pkgs) stdenv;
+    inherit (depot.nix) escapeExecline getBins;
+    inherit pkgs lib;
+  };
+
+  runExeclineLocal = name: args: execline:
+    runExecline name
+      (args // {
+        derivationArgs = args.derivationArgs or { } // {
+          preferLocalBuild = true;
+          allowSubstitutes = false;
+        };
+      })
+      execline;
+
+  tests = import ./tests.nix {
+    inherit runExecline runExeclineLocal;
+    inherit (depot.nix) getBins writeScript;
+    inherit (pkgs) stdenv coreutils;
+    inherit pkgs;
+  };
+
+in
+{
+  __functor = _: runExecline;
+  local = runExeclineLocal;
+  inherit tests;
+}
diff --git a/nix/runExecline/runExecline.nix b/nix/runExecline/runExecline.nix
new file mode 100644
index 0000000000..23b9a63303
--- /dev/null
+++ b/nix/runExecline/runExecline.nix
@@ -0,0 +1,122 @@
+{ pkgs, stdenv, lib, getBins, escapeExecline }:
+
+# runExecline is a primitive building block
+# for writing non-kitchen sink builders.
+#
+# It’s conceptually similar to `runCommand`,
+# but instead of concatenating bash scripts left
+# and right, it actually *uses* the features of
+# `derivation`, passing things to `args`
+# and making it possible to overwrite the `builder`
+# in a sensible manner.
+#
+# Additionally, it provides a way to pass a nix string
+# to `stdin` of the build script.
+#
+# Similar to //nix/writeExecline, the passed script is
+# not a string, but a nested list of nix lists
+# representing execline blocks. Escaping is
+# done by the implementation, the user can just use
+# normal nix strings.
+#
+# Example:
+#
+#  runExecline "my-drv" { stdin = "hi!"; } [
+#    "importas" "out" "out"
+#    # this pipes stdout of s6-cat to $out
+#    # and s6-cat redirects from stdin to stdout
+#    "redirfd" "-w" "1" "$out" bins.s6-cat
+#  ]
+#
+# which creates a derivation with "hi!" in $out.
+#
+# See ./tests.nix for more examples.
+
+
+let
+  bins = getBins pkgs.execline [
+    "execlineb"
+    { use = "if"; as = "execlineIf"; }
+    "redirfd"
+    "importas"
+    "exec"
+  ]
+  // getBins pkgs.s6-portable-utils [
+    "s6-cat"
+    "s6-grep"
+    "s6-touch"
+    "s6-test"
+    "s6-chmod"
+  ];
+
+in
+
+# TODO: move name into the attrset
+name:
+{
+  # a string to pass as stdin to the execline script
+  stdin ? ""
+  # a program wrapping the acutal execline invocation;
+  # should be in Bernstein-chaining style
+, builderWrapper ? bins.exec
+  # additional arguments to pass to the derivation
+, derivationArgs ? { }
+}:
+# the execline script as a nested list of string,
+# representing the blocks;
+# see docs of `escapeExecline`.
+execline:
+
+# those arguments can’t be overwritten
+assert !derivationArgs ? system;
+assert !derivationArgs ? name;
+assert !derivationArgs ? builder;
+assert !derivationArgs ? args;
+
+derivation (derivationArgs // {
+  # TODO(Profpatsch): what about cross?
+  inherit (stdenv) system;
+  inherit name;
+
+  # okay, `builtins.toFile` does not accept strings
+  # that reference drv outputs. This means we need
+  # to pass the script and stdin as envvar;
+  # this might clash with another passed envar,
+  # so we give it a long & unique name
+  _runExeclineScript =
+    let
+    in escapeExecline execline;
+  _runExeclineStdin = stdin;
+  passAsFile = [
+    "_runExeclineScript"
+    "_runExeclineStdin"
+  ] ++ derivationArgs.passAsFile or [ ];
+
+  # the default, exec acts as identity executable
+  builder = builderWrapper;
+
+  args = [
+    bins.importas # import script file as $script
+    "-ui" # drop the envvar afterwards
+    "script" # substitution name
+    "_runExeclineScriptPath" # passed script file
+
+    bins.importas # do the same for $stdin
+    "-ui"
+    "stdin"
+    "_runExeclineStdinPath"
+
+    bins.redirfd # now we
+    "-r" # read the file
+    "0" # into the stdin of execlineb
+    "$stdin" # that was given via stdin
+
+    bins.execlineb # the actual invocation
+    # TODO(Profpatsch): depending on the use-case, -S0 might not be enough
+    # in all use-cases, then a wrapper for execlineb arguments
+    # should be added (-P, -S, -s).
+    "-S0" # set $@ inside the execline script
+    "-W" # die on syntax error
+    "$script" # substituted by importas
+  ];
+})
diff --git a/nix/runExecline/tests.nix b/nix/runExecline/tests.nix
new file mode 100644
index 0000000000..f82b544224
--- /dev/null
+++ b/nix/runExecline/tests.nix
@@ -0,0 +1,117 @@
+{ stdenv
+, pkgs
+, runExecline
+, runExeclineLocal
+, getBins
+, writeScript
+  # https://www.mail-archive.com/skaware@list.skarnet.org/msg01256.html
+, coreutils
+}:
+
+let
+
+  bins = getBins coreutils [ "mv" ]
+    // getBins pkgs.execline [
+    "execlineb"
+    { use = "if"; as = "execlineIf"; }
+    "redirfd"
+    "importas"
+  ]
+    // getBins pkgs.s6-portable-utils [
+    "s6-chmod"
+    "s6-grep"
+    "s6-touch"
+    "s6-cat"
+    "s6-test"
+  ];
+
+  # execline block of depth 1
+  block = args: builtins.map (arg: " ${arg}") args ++ [ "" ];
+
+  # derivation that tests whether a given line exists
+  # in the given file. Does not use runExecline, because
+  # that should be tested after all.
+  fileHasLine = line: file: derivation {
+    name = "run-execline-test-file-${file.name}-has-line";
+    inherit (stdenv) system;
+    builder = bins.execlineIf;
+    args =
+      (block [
+        bins.redirfd
+        "-r"
+        "0"
+        file # read file to stdin
+        bins.s6-grep
+        "-F"
+        "-q"
+        line # and grep for the line
+      ])
+      ++ [
+        # if the block succeeded, touch $out
+        bins.importas
+        "-ui"
+        "out"
+        "out"
+        bins.s6-touch
+        "$out"
+      ];
+    preferLocalBuild = true;
+    allowSubstitutes = false;
+  };
+
+  # basic test that touches out
+  basic = runExeclineLocal "run-execline-test-basic"
+    { } [
+    "importas"
+    "-ui"
+    "out"
+    "out"
+    "${bins.s6-touch}"
+    "$out"
+  ];
+
+  # whether the stdin argument works as intended
+  stdin = fileHasLine "foo" (runExeclineLocal "run-execline-test-stdin"
+    {
+      stdin = "foo\nbar\nfoo";
+    } [
+    "importas"
+    "-ui"
+    "out"
+    "out"
+    # this pipes stdout of s6-cat to $out
+    # and s6-cat redirects from stdin to stdout
+    "redirfd"
+    "-w"
+    "1"
+    "$out"
+    bins.s6-cat
+  ]);
+
+
+  wrapWithVar = runExeclineLocal "run-execline-test-wrap-with-var"
+    {
+      builderWrapper = writeScript "var-wrapper" ''
+        #!${bins.execlineb} -S0
+        export myvar myvalue $@
+      '';
+    } [
+    "importas"
+    "-ui"
+    "v"
+    "myvar"
+    "if"
+    [ bins.s6-test "myvalue" "=" "$v" ]
+    "importas"
+    "out"
+    "out"
+    bins.s6-touch
+    "$out"
+  ];
+
+in
+[
+  basic
+  stdin
+  wrapWithVar
+]
diff --git a/nix/runTestsuite/default.nix b/nix/runTestsuite/default.nix
new file mode 100644
index 0000000000..8b02ed86d8
--- /dev/null
+++ b/nix/runTestsuite/default.nix
@@ -0,0 +1,199 @@
+{ lib, pkgs, depot, ... }:
+
+# Run a nix testsuite.
+#
+# The tests are simple assertions on the nix level,
+# and can use derivation outputs if IfD is enabled.
+#
+# You build a testsuite by bundling assertions into
+# “it”s and then bundling the “it”s into a testsuite.
+#
+# Running the testsuite will abort evaluation if
+# any assertion fails.
+#
+# Example:
+#
+#   runTestsuite "myFancyTestsuite" [
+#     (it "does an assertion" [
+#       (assertEq "42 is equal to 42" "42" "42")
+#       (assertEq "also 23" 23 23)
+#     ])
+#     (it "frmbls the brlbr" [
+#       (assertEq true false)
+#     ])
+#   ]
+#
+# will fail the second it group because true is not false.
+
+let
+  inherit (depot.nix.yants)
+    sum
+    struct
+    string
+    any
+    defun
+    list
+    drv
+    bool
+    ;
+
+  bins = depot.nix.getBins pkgs.coreutils [ "printf" ]
+    // depot.nix.getBins pkgs.s6-portable-utils [ "s6-touch" "s6-false" "s6-cat" ];
+
+  # Returns true if the given expression throws when `deepSeq`-ed
+  throws = expr:
+    !(builtins.tryEval (builtins.deepSeq expr { })).success;
+
+  # rewrite the builtins.partition result
+  # to use `ok` and `err` instead of `right` and `wrong`.
+  partitionTests = pred: xs:
+    let res = builtins.partition pred xs;
+    in {
+      ok = res.right;
+      err = res.wrong;
+    };
+
+  AssertErrorContext =
+    sum "AssertErrorContext" {
+      not-equal = struct "not-equal" {
+        left = any;
+        right = any;
+      };
+      should-throw = struct "should-throw" {
+        expr = any;
+      };
+      unexpected-throw = struct "unexpected-throw" { };
+    };
+
+  # The result of an assert,
+  # either it’s true (yep) or false (nope).
+  # If it's nope we return an additional context
+  # attribute which gives details on the failure
+  # depending on the type of assert performed.
+  AssertResult =
+    sum "AssertResult" {
+      yep = struct "yep" {
+        test = string;
+      };
+      nope = struct "nope" {
+        test = string;
+        context = AssertErrorContext;
+      };
+    };
+
+  # Result of an it. An it is a bunch of asserts
+  # bundled up with a good description of what is tested.
+  ItResult =
+    struct "ItResult" {
+      it-desc = string;
+      asserts = list AssertResult;
+    };
+
+  # If the given boolean is true return a positive AssertResult.
+  # If the given boolean is false return a negative AssertResult
+  # with the provided AssertErrorContext describing the failure.
+  #
+  # This function is intended as a generic assert to implement
+  # more assert types and is not exposed to the user.
+  assertBoolContext = defun [ AssertErrorContext string bool AssertResult ]
+    (context: desc: res:
+      if res
+      then { yep = { test = desc; }; }
+      else {
+        nope = {
+          test = desc;
+          inherit context;
+        };
+      });
+
+  # assert that left and right values are equal
+  assertEq = defun [ string any any AssertResult ]
+    (desc: left: right:
+      let
+        context = { not-equal = { inherit left right; }; };
+      in
+      assertBoolContext context desc (left == right));
+
+  # assert that the expression throws when `deepSeq`-ed
+  assertThrows = defun [ string any AssertResult ]
+    (desc: expr:
+      let
+        context = { should-throw = { inherit expr; }; };
+      in
+      assertBoolContext context desc (throws expr));
+
+  # assert that the expression does not throw when `deepSeq`-ed
+  assertDoesNotThrow = defun [ string any AssertResult ]
+    (desc: expr:
+      assertBoolContext { unexpected-throw = { }; } desc (!(throws expr)));
+
+  # Annotate a bunch of asserts with a descriptive name
+  it = desc: asserts: {
+    it-desc = desc;
+    inherit asserts;
+  };
+
+  # Run a bunch of its and check whether all asserts are yep.
+  # If not, abort evaluation with `throw`
+  # and print the result of the test suite.
+  #
+  # Takes a test suite name as first argument.
+  runTestsuite = defun [ string (list ItResult) drv ]
+    (name: itResults:
+      let
+        goodAss = ass: AssertResult.match ass {
+          yep = _: true;
+          nope = _: false;
+        };
+        res = partitionTests
+          (it:
+            (partitionTests goodAss it.asserts).err == [ ]
+          )
+          itResults;
+        prettyRes = lib.generators.toPretty { } res;
+      in
+      if res.err == [ ]
+      then
+        depot.nix.runExecline.local "testsuite-${name}-successful" { } [
+          "importas"
+          "out"
+          "out"
+          # force derivation to rebuild if test case list changes
+          "ifelse"
+          [ bins.s6-false ]
+          [
+            bins.printf
+            ""
+            (builtins.hashString "sha512" prettyRes)
+          ]
+          "if"
+          [ bins.printf "%s\n" "testsuite ${name} successful!" ]
+          bins.s6-touch
+          "$out"
+        ]
+      else
+        depot.nix.runExecline.local "testsuite-${name}-failed"
+          {
+            stdin = prettyRes + "\n";
+          } [
+          "importas"
+          "out"
+          "out"
+          "if"
+          [ bins.printf "%s\n" "testsuite ${name} failed!" ]
+          "if"
+          [ bins.s6-cat ]
+          "exit"
+          "1"
+        ]);
+
+in
+{
+  inherit
+    assertEq
+    assertThrows
+    assertDoesNotThrow
+    it
+    runTestsuite
+    ;
+}
diff --git a/nix/sparseTree/OWNERS b/nix/sparseTree/OWNERS
new file mode 100644
index 0000000000..fdf6d72040
--- /dev/null
+++ b/nix/sparseTree/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
\ No newline at end of file
diff --git a/nix/sparseTree/default.nix b/nix/sparseTree/default.nix
new file mode 100644
index 0000000000..16fc9b6103
--- /dev/null
+++ b/nix/sparseTree/default.nix
@@ -0,0 +1,74 @@
+# Build a “sparse” version of a given directory, only including contained files
+# and directories if they are listed in a supplied list:
+#
+# # A very minimal depot
+# sparseTree ./depot [
+#   ./default.nix
+#   ./depot/nix/readTree/default.nix
+#   ./third_party/nixpkgs
+#   ./third_party/overlays
+# ]
+{ pkgs, lib, ... }:
+
+# root path to use as a reference point
+root:
+# list of paths below `root` that should be
+# included in the resulting directory
+#
+# If path, need to refer to the actual file / directory to be included.
+# If a string, it is treated as a string relative to the root.
+paths:
+
+let
+  rootLength = builtins.stringLength (toString root);
+
+  # Count slashes in a path.
+  #
+  # Type: path -> int
+  depth = path: lib.pipe path [
+    toString
+    (builtins.split "/")
+    (builtins.filter builtins.isList)
+    builtins.length
+  ];
+
+  # (Parent) directories will be created from deepest to shallowest
+  # which should mean no conflicts are caused unless both a child
+  # and its parent directory are in the list of paths.
+  # TODO(sterni): improve error messages in such cases
+  fromDeepest = lib.sort (a: b: depth a < depth b) paths;
+
+  # Create a set which contains the source path to copy / symlink and
+  # it's destination, so the path below the destination root including
+  # a leading slash. Additionally some sanity checking is done.
+  makeSymlink = path:
+    let
+      withLeading = p: if builtins.substring 0 1 p == "/" then p else "/" + p;
+      fullPath =
+        /**/
+        if builtins.isPath path then path
+        else if builtins.isString path then (root + withLeading path)
+        else builtins.throw "Unsupported path type ${builtins.typeOf path}";
+      strPath = toString fullPath;
+      contextPath = "${fullPath}";
+      belowRoot = builtins.substring rootLength (-1) strPath;
+      prefix = builtins.substring 0 rootLength strPath;
+    in
+    assert toString root == prefix; {
+      src = contextPath;
+      dst = belowRoot;
+    };
+
+  symlinks = builtins.map makeSymlink fromDeepest;
+in
+
+# TODO(sterni): teach readTree to also read symlinked directories,
+  # so we ln -sT instead of cp -aT.
+pkgs.runCommandNoCC "sparse-${builtins.baseNameOf root}" { } (
+  lib.concatMapStrings
+    ({ src, dst }: ''
+      mkdir -p "$(dirname "$out${dst}")"
+      cp -aT --reflink=auto "${src}" "$out${dst}"
+    '')
+    symlinks
+)
diff --git a/nix/tag/default.nix b/nix/tag/default.nix
new file mode 100644
index 0000000000..0038404460
--- /dev/null
+++ b/nix/tag/default.nix
@@ -0,0 +1,166 @@
+{ depot, lib, ... }:
+let
+  # Takes a tag, checks whether it is an attrset with one element,
+  # if so sets `isTag` to `true` and sets the name and value.
+  # If not, sets `isTag` to `false` and sets `errmsg`.
+  verifyTag = tag:
+    let
+      cases = builtins.attrNames tag;
+      len = builtins.length cases;
+    in
+    if builtins.length cases == 1
+    then
+      let name = builtins.head cases; in {
+        isTag = true;
+        name = name;
+        val = tag.${name};
+        errmsg = null;
+      }
+    else {
+      isTag = false;
+      errmsg =
+        ("match: an instance of a sum is an attrset "
+          + "with exactly one element, yours had ${toString len}"
+          + ", namely: ${lib.generators.toPretty {} cases}");
+      name = null;
+      val = null;
+    };
+
+  # Returns the tag name of a given tag attribute set.
+  # Throws if the tag is invalid.
+  #
+  # Type: tag -> string
+  tagName = tag: (assertIsTag tag).name;
+
+  # Returns the tagged value of a given tag attribute set.
+  # Throws if the tag is invalid.
+  #
+  # Type: tag -> any
+  tagValue = tag: (assertIsTag tag).val;
+
+  # like `verifyTag`, but throws the error message if it is not a tag.
+  assertIsTag = tag:
+    let res = verifyTag tag; in
+    assert res.isTag || throw res.errmsg;
+    { inherit (res) name val; };
+
+
+  # Discriminator for values.
+  # Goes through a list of tagged predicates `{ <tag> = <pred>; }`
+  # and returns the value inside the tag
+  # for which the first predicate applies, `{ <tag> = v; }`.
+  # They can then later be matched on with `match`.
+  #
+  # `defTag` is the tag that is assigned if there is no match.
+  #
+  # Examples:
+  #   discrDef "smol" [
+  #     { biggerFive = i: i > 5; }
+  #     { negative = i: i < 0; }
+  #   ] (-100)
+  #   => { negative = -100; }
+  #   discrDef "smol" [
+  #     { biggerFive = i: i > 5; }
+  #     { negative = i: i < 0; }
+  #   ] 1
+  #   => { smol = 1; }
+  discrDef = defTag: fs: v:
+    let
+      res = lib.findFirst
+        (t: t.val v)
+        null
+        (map assertIsTag fs);
+    in
+    if res == null
+    then { ${defTag} = v; }
+    else { ${res.name} = v; };
+
+  # Like `discrDef`, but fail if there is no match.
+  discr = fs: v:
+    let res = discrDef null fs v; in
+    assert lib.assertMsg (res != null)
+      "tag.discr: No predicate found that matches ${lib.generators.toPretty {} v}";
+    res;
+
+  # The canonical pattern matching primitive.
+  # A sum value is an attribute set with one element,
+  # whose key is the name of the variant and
+  # whose value is the content of the variant.
+  # `matcher` is an attribute set which enumerates
+  # all possible variants as keys and provides a function
+  # which handles each variant’s content.
+  # You should make an effort to return values of the same
+  # type in your matcher, or new sums.
+  #
+  # Example:
+  #   let
+  #      success = { res = 42; };
+  #      failure = { err = "no answer"; };
+  #      matcher = {
+  #        res = i: i + 1;
+  #        err = _: 0;
+  #      };
+  #    in
+  #       match success matcher == 43
+  #    && match failure matcher == 0;
+  #
+  match = sum: matcher:
+    let cases = builtins.attrNames sum;
+    in assert
+    let len = builtins.length cases; in
+    lib.assertMsg (len == 1)
+      ("match: an instance of a sum is an attrset "
+        + "with exactly one element, yours had ${toString len}"
+        + ", namely: ${lib.generators.toPretty {} cases}");
+    let case = builtins.head cases;
+    in assert
+    lib.assertMsg (matcher ? ${case})
+      ("match: \"${case}\" is not a valid case of this sum, "
+        + "the matcher accepts: ${lib.generators.toPretty {}
+            (builtins.attrNames matcher)}");
+    matcher.${case} sum.${case};
+
+  # A `match` with the arguments flipped.
+  # “Lam” stands for “lambda”, because it can be used like the
+  # `\case` LambdaCase statement in Haskell, to create a curried
+  # “matcher” function ready to take a value.
+  #
+  # Example:
+  #   lib.pipe { foo = 42; } [
+  #     (matchLam {
+  #       foo = i: if i < 23 then { small = i; } else { big = i; };
+  #       bar = _: { small = 5; };
+  #     })
+  #     (matchLam {
+  #       small = i: "yay it was small";
+  #       big = i: "whoo it was big!";
+  #     })
+  #   ]
+  #   => "whoo it was big!";
+  matchLam = matcher: sum: match sum matcher;
+
+  tests = import ./tests.nix {
+    inherit
+      depot
+      lib
+      verifyTag
+      discr
+      discrDef
+      match
+      matchLam
+      ;
+  };
+
+in
+{
+  inherit
+    verifyTag
+    tagName
+    tagValue
+    discr
+    discrDef
+    match
+    matchLam
+    tests
+    ;
+}
diff --git a/nix/tag/tests.nix b/nix/tag/tests.nix
new file mode 100644
index 0000000000..bcc42c758a
--- /dev/null
+++ b/nix/tag/tests.nix
@@ -0,0 +1,94 @@
+{ depot, lib, verifyTag, discr, discrDef, match, matchLam }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    assertEq
+    it
+    ;
+
+  isTag-test = it "checks whether something is a tag" [
+    (assertEq "is Tag"
+      (verifyTag { foo = "bar"; })
+      {
+        isTag = true;
+        name = "foo";
+        val = "bar";
+        errmsg = null;
+      })
+    (assertEq "is not Tag"
+      (removeAttrs (verifyTag { foo = "bar"; baz = 42; }) [ "errmsg" ])
+      {
+        isTag = false;
+        name = null;
+        val = null;
+      })
+  ];
+
+  discr-test = it "can discr things" [
+    (assertEq "id"
+      (discr [
+        { a = lib.const true; }
+      ] "x")
+      { a = "x"; })
+    (assertEq "bools here, ints there"
+      (discr [
+        { bool = lib.isBool; }
+        { int = lib.isInt; }
+      ] 25)
+      { int = 25; })
+    (assertEq "bools here, ints there 2"
+      (discr [
+        { bool = lib.isBool; }
+        { int = lib.isInt; }
+      ]
+        true)
+      { bool = true; })
+    (assertEq "fallback to default"
+      (discrDef "def" [
+        { bool = lib.isBool; }
+        { int = lib.isInt; }
+      ] "foo")
+      { def = "foo"; })
+  ];
+
+  match-test = it "can match things" [
+    (assertEq "match example"
+      (
+        let
+          success = { res = 42; };
+          failure = { err = "no answer"; };
+          matcher = {
+            res = i: i + 1;
+            err = _: 0;
+          };
+        in
+        {
+          one = match success matcher;
+          two = match failure matcher;
+        }
+      )
+      {
+        one = 43;
+        two = 0;
+      })
+    (assertEq "matchLam & pipe"
+      (lib.pipe { foo = 42; } [
+        (matchLam {
+          foo = i: if i < 23 then { small = i; } else { big = i; };
+          bar = _: { small = 5; };
+        })
+        (matchLam {
+          small = i: "yay it was small";
+          big = i: "whoo it was big!";
+        })
+      ])
+      "whoo it was big!")
+  ];
+
+in
+runTestsuite "tag" [
+  isTag-test
+  discr-test
+  match-test
+]
diff --git a/nix/tailscale/default.nix b/nix/tailscale/default.nix
new file mode 100644
index 0000000000..363f717db6
--- /dev/null
+++ b/nix/tailscale/default.nix
@@ -0,0 +1,31 @@
+# This file defines a Nix helper function to create Tailscale ACL files.
+#
+# https://tailscale.com/kb/1018/install-acls
+
+{ depot, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) toFile toJSON;
+
+  acl = struct "acl" {
+    Action = enum [ "accept" "reject" ];
+    Users = list string;
+    Ports = list string;
+  };
+
+  acls = list entry;
+
+  aclConfig = struct "aclConfig" {
+    # Static group mappings from group names to lists of users
+    Groups = option (attrs (list string));
+
+    # Hostname aliases to use in place of IPs
+    Hosts = option (attrs string);
+
+    # Actual ACL entries
+    ACLs = list acl;
+  };
+in
+config: pkgs.writeText "tailscale-acl.json" (toJSON (aclConfig config))
diff --git a/nix/utils/OWNERS b/nix/utils/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/nix/utils/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/nix/utils/default.nix b/nix/utils/default.nix
new file mode 100644
index 0000000000..a29f346519
--- /dev/null
+++ b/nix/utils/default.nix
@@ -0,0 +1,176 @@
+{ depot, lib, ... }:
+
+let
+  /* Get the basename of a store path without
+     the leading hash.
+
+     Type: (path | drv | string) -> string
+
+     Example:
+       storePathName ./foo.c
+       => "foo.c"
+
+       storePathName (writeText "foo.c" "int main() { return 0; }")
+       => "foo.c"
+
+       storePathName "${hello}/bin/hello"
+       => "hello"
+  */
+  storePathName = p:
+    if lib.isDerivation p
+    then p.name
+    else if builtins.isPath p
+    then builtins.baseNameOf p
+    else if builtins.isString p || (builtins.isAttrs p && (p ? outPath || p ? __toString))
+    then
+      let
+        strPath = toString p;
+        # strip leading storeDir and trailing slashes
+        noStoreDir = lib.removeSuffix "/"
+          (lib.removePrefix "${builtins.storeDir}/" strPath);
+        # a basename of a child of a store path isn't really
+        # referring to a store path, so removing the string
+        # context is safe (e. g. "hello" for "${hello}/bin/hello").
+        basename = builtins.unsafeDiscardStringContext
+          (builtins.baseNameOf strPath);
+      in
+      # If p is a direct child of storeDir, we need to remove
+        # the leading hash as well to make sure that:
+        # `storePathName drv == storePathName (toString drv)`.
+      if noStoreDir == basename
+      then builtins.substring 33 (-1) basename
+      else basename
+    else builtins.throw "Don't know how to get (base)name of "
+      + lib.generators.toPretty { } p;
+
+  /* Retrieves the drvPath attribute from a given derivation, but ensures that
+     the resulting string only depends on the `.drv` file in the nix store and
+     not on its realised outputs as well.
+
+     Type: drv -> string
+  */
+  onlyDrvPath = drv:
+    let
+      inherit (drv) drvPath;
+      unsafeDrvPath = builtins.unsafeDiscardStringContext drvPath;
+    in
+    builtins.appendContext unsafeDrvPath {
+      ${unsafeDrvPath} = { path = true; };
+    };
+
+  /* Query the type of a path exposing the same information as would be by
+     `builtins.readDir`, but for a single, specific target path.
+
+     The information is returned as a tagged value, i. e. an attribute set with
+     exactly one attribute where the type of the path is encoded in the name
+     of the single attribute. The allowed tags and values are as follows:
+
+     * `regular`: is a regular file, always `true` if returned
+     * `directory`: is a directory, always `true` if returned
+     * `missing`: path does not exist, always `true` if returned
+     * `symlink`: path is a symlink, always `true` if returned
+
+     Type: path(-like) -> tag
+
+     `tag` refers to the attribute set format of `//nix/tag`.
+
+     Example:
+       pathType ./foo.c
+       => { regular = true; }
+
+       pathType /home/lukas
+       => { directory = true; }
+
+       pathType ./result
+       => { symlink = true; }
+
+       pathType ./link-to-file
+       => { symlink = true; }
+
+       pathType /does/not/exist
+       => { missing = true; }
+
+       # Check if a path exists
+       !(pathType /file ? missing)
+
+       # Check if a path is a directory or a symlink to a directory
+       # A handy shorthand for this is provided as `realPathIsDirectory`.
+       pathType /path ? directory || (pathType /path).symlink or null == "directory"
+
+       # Match on the result using //nix/tag
+       nix.tag.match (nix.utils.pathType ./result) {
+         symlink = _: "symlink";
+         directory  = _: "directory";
+         regular = _: "regular";
+         missing = _: "path does not exist";
+       }
+       => "symlink"
+
+       # Query path type
+       nix.tag.tagName (pathType /path)
+  */
+  pathType = path:
+    let
+      # baseNameOf is very annoyed if we proceed with string context.
+      # We need to call toString to prevent unsafeDiscardStringContext
+      # from importing a path into store which messes with base- and
+      # dirname of course.
+      path' = builtins.unsafeDiscardStringContext (toString path);
+      # To read the containing directory we absolutely need
+      # to keep the string context, otherwise a derivation
+      # would not be realized before our check (at eval time)
+      containingDir = builtins.readDir (builtins.dirOf path);
+      # Construct tag to use for the value
+      thisPathType = containingDir.${builtins.baseNameOf path'} or "missing";
+      # Trick to check if the symlink target exists and is a directory:
+      # if we append a "/." to the string version of the path, Nix won't
+      # canocalize it (which would strip any "/." in the path), so if
+      # path' + "/." exists, we know that the symlink points to an existing
+      # directory. If not, either the target doesn't exist or is a regular file.
+      # TODO(sterni): is there a way to check reliably if the symlink target exists?
+      isSymlinkDir = builtins.pathExists (path' + "/.");
+    in
+    {
+      ${thisPathType} = true;
+    };
+
+  pathType' = path:
+    let
+      p = pathType path;
+    in
+    if p ? missing
+    then builtins.throw "${lib.generators.toPretty {} path} does not exist"
+    else p;
+
+  /* Check whether the given path is a directory.
+     Throws if the path in question doesn't exist.
+
+     Type: path(-like) -> bool
+  */
+  isDirectory = path: pathType' path ? directory;
+
+  /* Check whether the given path is a regular file.
+     Throws if the path in question doesn't exist.
+
+     Type: path(-like) -> bool
+  */
+  isRegularFile = path: pathType' path ? regular;
+
+  /* Check whether the given path is a symbolic link.
+     Throws if the path in question doesn't exist.
+
+     Type: path(-like) -> bool
+  */
+  isSymlink = path: pathType' path ? symlink;
+
+in
+{
+  inherit
+    storePathName
+    onlyDrvPath
+    pathType
+    isDirectory
+    isRegularFile
+    isSymlink
+    ;
+}
diff --git a/nix/utils/tests/.skip-subtree b/nix/utils/tests/.skip-subtree
new file mode 100644
index 0000000000..1efb1b9476
--- /dev/null
+++ b/nix/utils/tests/.skip-subtree
@@ -0,0 +1 @@
+subdirectories are just test cases
diff --git a/nix/utils/tests/default.nix b/nix/utils/tests/default.nix
new file mode 100644
index 0000000000..d5159a8433
--- /dev/null
+++ b/nix/utils/tests/default.nix
@@ -0,0 +1,110 @@
+{ depot, lib, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    assertDoesNotThrow
+    ;
+
+  inherit (depot.nix.utils)
+    isDirectory
+    isRegularFile
+    isSymlink
+    pathType
+    storePathName
+    onlyDrvPath
+    ;
+
+  assertUtilsPred = msg: act: exp: [
+    (assertDoesNotThrow "${msg} does not throw" act)
+    (assertEq msg (builtins.tryEval act).value exp)
+  ];
+
+  pathPredicates = it "judges paths correctly" (lib.flatten [
+    # isDirectory
+    (assertUtilsPred "directory isDirectory"
+      (isDirectory ./directory)
+      true)
+    (assertUtilsPred "symlink not isDirectory"
+      (isDirectory ./symlink-directory)
+      false)
+    (assertUtilsPred "file not isDirectory"
+      (isDirectory ./directory/file)
+      false)
+    # isRegularFile
+    (assertUtilsPred "file isRegularFile"
+      (isRegularFile ./directory/file)
+      true)
+    (assertUtilsPred "symlink not isRegularFile"
+      (isRegularFile ./symlink-file)
+      false)
+    (assertUtilsPred "directory not isRegularFile"
+      (isRegularFile ./directory)
+      false)
+    # isSymlink
+    (assertUtilsPred "symlink to file isSymlink"
+      (isSymlink ./symlink-file)
+      true)
+    (assertUtilsPred "symlink to directory isSymlink"
+      (isSymlink ./symlink-directory)
+      true)
+    (assertUtilsPred "symlink to symlink isSymlink"
+      (isSymlink ./symlink-symlink-file)
+      true)
+    (assertUtilsPred "symlink to missing file isSymlink"
+      (isSymlink ./missing)
+      true)
+    (assertUtilsPred "directory not isSymlink"
+      (isSymlink ./directory)
+      false)
+    (assertUtilsPred "file not isSymlink"
+      (isSymlink ./directory/file)
+      false)
+    # missing files throw
+    (assertThrows "isDirectory throws on missing file"
+      (isDirectory ./does-not-exist))
+    (assertThrows "isRegularFile throws on missing file"
+      (isRegularFile ./does-not-exist))
+    (assertThrows "isSymlink throws on missing file"
+      (isSymlink ./does-not-exist))
+  ]);
+
+  cheddarStorePath =
+    builtins.unsafeDiscardStringContext depot.tools.cheddar.outPath;
+
+  cleanedSource = lib.cleanSource ./.;
+
+  storePathNameTests = it "correctly gets the basename of a store path" [
+    (assertEq "base name of a derivation"
+      (storePathName depot.tools.cheddar)
+      depot.tools.cheddar.name)
+    (assertEq "base name of a store path string"
+      (storePathName cheddarStorePath)
+      depot.tools.cheddar.name)
+    (assertEq "base name of a path within a store path"
+      (storePathName "${cheddarStorePath}/bin/cheddar") "cheddar")
+    (assertEq "base name of a path"
+      (storePathName ../default.nix) "default.nix")
+    (assertEq "base name of a cleanSourced path"
+      (storePathName cleanedSource)
+      cleanedSource.name)
+  ];
+
+  onlyDrvPathTests = it "correctly updates the string context of drvPath" [
+    (assertEq "onlyDrvPath only produces path dependencies"
+      (builtins.all
+        (dep: dep.path or false)
+        (builtins.attrValues
+          (builtins.getContext (onlyDrvPath depot.tools.cheddar))))
+      true)
+  ];
+in
+
+runTestsuite "nix.utils" [
+  pathPredicates
+  storePathNameTests
+  onlyDrvPathTests
+]
diff --git a/nix/utils/tests/directory/file b/nix/utils/tests/directory/file
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nix/utils/tests/directory/file
diff --git a/nix/utils/tests/missing b/nix/utils/tests/missing
new file mode 120000
index 0000000000..cfa0a46515
--- /dev/null
+++ b/nix/utils/tests/missing
@@ -0,0 +1 @@
+does-not-exist
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-directory b/nix/utils/tests/symlink-directory
new file mode 120000
index 0000000000..6d0450cc24
--- /dev/null
+++ b/nix/utils/tests/symlink-directory
@@ -0,0 +1 @@
+directory
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-file b/nix/utils/tests/symlink-file
new file mode 120000
index 0000000000..cd5d01792a
--- /dev/null
+++ b/nix/utils/tests/symlink-file
@@ -0,0 +1 @@
+directory/file
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-symlink-directory b/nix/utils/tests/symlink-symlink-directory
new file mode 120000
index 0000000000..5d6ba5247e
--- /dev/null
+++ b/nix/utils/tests/symlink-symlink-directory
@@ -0,0 +1 @@
+symlink-directory
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-symlink-file b/nix/utils/tests/symlink-symlink-file
new file mode 120000
index 0000000000..f8d9ce3659
--- /dev/null
+++ b/nix/utils/tests/symlink-symlink-file
@@ -0,0 +1 @@
+symlink-file
\ No newline at end of file
diff --git a/nix/writeElispBin/default.nix b/nix/writeElispBin/default.nix
new file mode 100644
index 0000000000..ee3dc7a3ed
--- /dev/null
+++ b/nix/writeElispBin/default.nix
@@ -0,0 +1,20 @@
+{ depot, pkgs, ... }:
+
+{ name, src, deps ? (_: [ ]), emacs ? pkgs.emacs28-nox }:
+
+let
+  inherit (pkgs) emacsPackages emacsPackagesFor;
+  inherit (builtins) isString toFile;
+
+  finalEmacs = (emacsPackagesFor emacs).emacsWithPackages deps;
+
+  srcFile =
+    if isString src
+    then toFile "${name}.el" src
+    else src;
+
+in
+depot.nix.writeScriptBin name ''
+  #!/bin/sh
+  ${finalEmacs}/bin/emacs --batch --no-site-file --script ${srcFile} $@
+''
diff --git a/nix/writeExecline/default.nix b/nix/writeExecline/default.nix
new file mode 100644
index 0000000000..5169b01386
--- /dev/null
+++ b/nix/writeExecline/default.nix
@@ -0,0 +1,39 @@
+{ pkgs, depot, ... }:
+
+# Write an execline script, represented as nested nix lists.
+# Everything is escaped correctly.
+# https://skarnet.org/software/execline/
+
+# TODO(Profpatsch) upstream into nixpkgs
+
+name:
+{
+  # "var": substitute readNArgs variables and start $@
+  # from the (readNArgs+1)th argument
+  # "var-full": substitute readNArgs variables and start $@ from $0
+  # "env": don’t substitute, set # and 0…n environment vaariables, where n=$#
+  # "none": don’t substitute or set any positional arguments
+  # "env-no-push": like "env", but bypass the push-phase. Not recommended.
+  argMode ? "var"
+, # Number of arguments to be substituted as variables (passed to "var"/"-s" or "var-full"/"-S"
+  readNArgs ? 0
+,
+}:
+# Nested list of lists of commands.
+# Inner lists are translated to execline blocks.
+argList:
+
+let
+  env =
+    if argMode == "var" then "s${toString readNArgs}"
+    else if argMode == "var-full" then "S${toString readNArgs}"
+    else if argMode == "env" then ""
+    else if argMode == "none" then "P"
+    else if argMode == "env-no-push" then "p"
+    else abort ''"${toString argMode}" is not a valid argMode, use one of "var", "var-full", "env", "none", "env-no-push".'';
+
+in
+depot.nix.writeScript name ''
+  #!${pkgs.execline}/bin/execlineb -W${env}
+  ${depot.nix.escapeExecline argList}
+''
diff --git a/nix/writeScript/default.nix b/nix/writeScript/default.nix
new file mode 100644
index 0000000000..1f53b4e4ff
--- /dev/null
+++ b/nix/writeScript/default.nix
@@ -0,0 +1,35 @@
+{ pkgs, depot, ... }:
+
+# Write the given string to $out
+# and make it executable.
+
+let
+  bins = depot.nix.getBins pkgs.s6-portable-utils [
+    "s6-cat"
+    "s6-chmod"
+  ];
+
+in
+name:
+# string of the executable script that is put in $out
+script:
+
+depot.nix.runExecline name
+{
+  stdin = script;
+  derivationArgs = {
+    preferLocalBuild = true;
+    allowSubstitutes = false;
+  };
+} [
+  "importas"
+  "out"
+  "out"
+  # this pipes stdout of s6-cat to $out
+  # and s6-cat redirects from stdin to stdout
+  "if"
+  [ "redirfd" "-w" "1" "$out" bins.s6-cat ]
+  bins.s6-chmod
+  "0755"
+  "$out"
+]
diff --git a/nix/writeScriptBin/default.nix b/nix/writeScriptBin/default.nix
new file mode 100644
index 0000000000..ed26cf197e
--- /dev/null
+++ b/nix/writeScriptBin/default.nix
@@ -0,0 +1,12 @@
+{ depot, ... }:
+
+# Like writeScript,
+# but put the script into `$out/bin/${name}`.
+
+name:
+script:
+
+depot.nix.binify {
+  exe = (depot.nix.writeScript name script);
+  inherit name;
+}
diff --git a/nix/writers/default.nix b/nix/writers/default.nix
new file mode 100644
index 0000000000..55355913a9
--- /dev/null
+++ b/nix/writers/default.nix
@@ -0,0 +1,113 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.s6-portable-utils [ "s6-ln" "s6-ls" "s6-touch" ]
+  ;
+
+  linkTo = name: path: depot.nix.runExecline.local name { } [
+    "importas"
+    "out"
+    "out"
+    bins.s6-ln
+    "-s"
+    path
+    "$out"
+  ];
+
+  # Build a rust executable, $out is the executable.
+  rustSimple = args@{ name, ... }: src:
+    linkTo name "${rustSimpleBin args src}/bin/${name}";
+
+  # Like `rustSimple`, but put the binary in `$out/bin/`.
+  rustSimpleBin =
+    { name
+    , dependencies ? [ ]
+    , doCheck ? true
+    ,
+    }: src:
+    (if doCheck then testRustSimple else pkgs.lib.id)
+      (pkgs.buildRustCrate ({
+        pname = name;
+        version = "1.0.0";
+        crateName = name;
+        crateBin = [ name ];
+        dependencies = dependencies;
+        src = pkgs.runCommandLocal "write-main.rs"
+          {
+            src = src;
+            passAsFile = [ "src" ];
+          } ''
+          mkdir -p $out/src/bin
+          cp "$srcPath" $out/src/bin/${name}.rs
+          find $out
+        '';
+      }));
+
+  # Build a rust library, that can be used as dependency to `rustSimple`.
+  # Wrapper around `pkgs.buildRustCrate`, takes all its arguments.
+  rustSimpleLib =
+    { name
+    , dependencies ? [ ]
+    , doCheck ? true
+    ,
+    }: src:
+    (if doCheck then testRustSimple else pkgs.lib.id)
+      (pkgs.buildRustCrate ({
+        pname = name;
+        version = "1.0.0";
+        crateName = name;
+        dependencies = dependencies;
+        src = pkgs.runCommandLocal "write-lib.rs"
+          {
+            src = src;
+            passAsFile = [ "src" ];
+          } ''
+          mkdir -p $out/src
+          cp "$srcPath" $out/src/lib.rs
+          find $out
+        '';
+      }));
+
+  /* Takes a `buildRustCrate` derivation as an input,
+    * builds it with `{ buildTests = true; }` and runs
+    * all tests found in its `tests` dir. If they are
+    * all successful, `$out` will point to the crate
+    * built with `{ buildTests = false; }`, otherwise
+    * it will fail to build.
+    *
+    * See also `nix.drvSeqL` which is used to implement
+    * this behavior.
+    */
+  testRustSimple = rustDrv:
+    let
+      crate = buildTests: rustDrv.override { inherit buildTests; };
+      tests = depot.nix.runExecline.local "${rustDrv.name}-tests-run" { } [
+        "importas"
+        "out"
+        "out"
+        "if"
+        [
+          "pipeline"
+          [ bins.s6-ls "${crate true}/tests" ]
+          "forstdin"
+          "-o0"
+          "test"
+          "importas"
+          "test"
+          "test"
+          "${crate true}/tests/$test"
+        ]
+        bins.s6-touch
+        "$out"
+      ];
+    in
+    depot.nix.drvSeqL [ tests ] (crate false);
+
+in
+{
+  inherit
+    rustSimple
+    rustSimpleBin
+    rustSimpleLib
+    ;
+}
diff --git a/nix/writers/tests/rust.nix b/nix/writers/tests/rust.nix
new file mode 100644
index 0000000000..232a2dc608
--- /dev/null
+++ b/nix/writers/tests/rust.nix
@@ -0,0 +1,76 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix.writers)
+    rustSimple
+    rustSimpleLib
+    rustSimpleBin
+    ;
+
+  inherit (pkgs)
+    coreutils
+    ;
+
+  run = drv: depot.nix.runExecline.local "run-${drv.name}" { } [
+    "if"
+    [ drv ]
+    "importas"
+    "out"
+    "out"
+    "${coreutils}/bin/touch"
+    "$out"
+  ];
+
+  rustTransitiveLib = rustSimpleLib
+    {
+      name = "transitive";
+    } ''
+    pub fn transitive(s: &str) -> String {
+      let mut new = s.to_string();
+      new.push_str(" 1 2 3");
+      new
+    }
+
+    #[cfg(test)]
+    mod tests {
+      use super::*;
+
+      #[test]
+      fn test_transitive() {
+        assert_eq!(transitive("foo").as_str(), "foo 1 2 3")
+      }
+    }
+  '';
+
+  rustTestLib = rustSimpleLib
+    {
+      name = "test_lib";
+      dependencies = [ rustTransitiveLib ];
+    } ''
+    extern crate transitive;
+    use transitive::{transitive};
+    pub fn test() -> String {
+      transitive("test")
+    }
+  '';
+
+  rustWithLib = run (rustSimple
+    {
+      name = "rust-with-lib";
+      dependencies = [ rustTestLib ];
+    } ''
+    extern crate test_lib;
+
+    fn main() {
+      assert_eq!(test_lib::test(), String::from("test 1 2 3"));
+    }
+  '');
+
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    rustTransitiveLib
+    rustWithLib
+    ;
+}
diff --git a/nix/yants/README.md b/nix/yants/README.md
new file mode 100644
index 0000000000..98e6642e2d
--- /dev/null
+++ b/nix/yants/README.md
@@ -0,0 +1,88 @@
+yants
+=====
+
+This is a tiny type-checker for data in Nix, written in Nix.
+
+# Features
+
+* Checking of primitive types (`int`, `string` etc.)
+* Checking polymorphic types (`option`, `list`, `either`)
+* Defining & checking struct/record types
+* Defining & matching enum types
+* Defining & matching sum types
+* Defining function signatures (including curried functions)
+* Types are composable! `option string`! `list (either int (option float))`!
+* Type errors also compose!
+
+Currently lacking:
+
+* Any kind of inference
+* Convenient syntax for attribute-set function signatures
+
+## Primitives & simple polymorphism
+
+![simple](/about/nix/yants/screenshots/simple.png)
+
+## Structs
+
+![structs](/about/nix/yants/screenshots/structs.png)
+
+## Nested structs!
+
+![nested structs](/about/nix/yants/screenshots/nested-structs.png)
+
+## Enums!
+
+![enums](/about/nix/yants/screenshots/enums.png)
+
+## Functions!
+
+![functions](/about/nix/yants/screenshots/functions.png)
+
+# Usage
+
+Yants can be imported from its `default.nix`. A single attribute (`lib`) can be
+passed, which will otherwise be imported from `<nixpkgs>`.
+
+TIP: You do not need to clone the entire TVL repository to use Yants!
+You can clone just this project through josh: `git clone
+https://code.tvl.fyi/depot.git:/nix/yants.git`
+
+Examples for the most common import methods would be:
+
+1. Import into scope with `with`:
+    ```nix
+    with (import ./default.nix {});
+    # ... Nix code that uses yants ...
+    ```
+
+2. Import as a named variable:
+    ```nix
+    let yants = import ./default.nix {};
+    in yants.string "foo" # or other uses ...
+    ````
+
+3. Overlay into `pkgs.lib`:
+    ```nix
+    # wherever you import your package set (e.g. from <nixpkgs>):
+    import <nixpkgs> {
+      overlays = [
+        (self: super: {
+          lib = super.lib // { yants = import ./default.nix { inherit (super) lib; }; };
+        })
+      ];
+    }
+
+    # yants now lives at lib.yants, besides the other library functions!
+    ```
+
+Please see my [Nix one-pager](https://github.com/tazjin/nix-1p) for more generic
+information about the Nix language and what the above constructs mean.
+
+# Stability
+
+The current API of Yants is **not yet** considered stable, but it works fine and
+should continue to do so even if used at an older version.
+
+Yants' tests use Nix versions above 2.2 - compatibility with older versions is
+not guaranteed.
diff --git a/nix/yants/default.nix b/nix/yants/default.nix
new file mode 100644
index 0000000000..cb9fc08287
--- /dev/null
+++ b/nix/yants/default.nix
@@ -0,0 +1,368 @@
+# Copyright 2019 Google LLC
+# SPDX-License-Identifier: Apache-2.0
+#
+# Provides a "type-system" for Nix that provides various primitive &
+# polymorphic types as well as the ability to define & check records.
+#
+# All types (should) compose as expected.
+
+{ lib ? (import <nixpkgs> { }).lib, ... }:
+
+with builtins; let
+  prettyPrint = lib.generators.toPretty { };
+
+  # typedef' :: struct {
+  #   name = string;
+  #   checkType = function; (a -> result)
+  #   checkToBool = option function; (result -> bool)
+  #   toError = option function; (a -> result -> string)
+  #   def = option any;
+  #   match = option function;
+  # } -> type
+  #           -> (a -> b)
+  #           -> (b -> bool)
+  #           -> (a -> b -> string)
+  #           -> type
+  #
+  # This function creates an attribute set that acts as a type.
+  #
+  # It receives a type name, a function that is used to perform a
+  # check on an arbitrary value, a function that can translate the
+  # return of that check to a boolean that informs whether the value
+  # is type-conformant, and a function that can construct error
+  # messages from the check result.
+  #
+  # This function is the low-level primitive used to create types. For
+  # many cases the higher-level 'typedef' function is more appropriate.
+  typedef' =
+    { name
+    , checkType
+    , checkToBool ? (result: result.ok)
+    , toError ? (_: result: result.err)
+    , def ? null
+    , match ? null
+    }: {
+      inherit name checkToBool toError;
+
+      # check :: a -> bool
+      #
+      # This function is used to determine whether a given type is
+      # conformant.
+      check = value: checkToBool (checkType value);
+
+      # checkType :: a -> struct { ok = bool; err = option string; }
+      #
+      # This function checks whether the passed value is type conformant
+      # and returns an optional type error string otherwise.
+      inherit checkType;
+
+      # __functor :: a -> a
+      #
+      # This function checks whether the passed value is type conformant
+      # and throws an error if it is not.
+      #
+      # The name of this function is a special attribute in Nix that
+      # makes it possible to execute a type attribute set like a normal
+      # function.
+      __functor = self: value:
+        let result = self.checkType value;
+        in if checkToBool result then value
+        else throw (toError value result);
+    };
+
+  typeError = type: val:
+    "expected type '${type}', but value '${prettyPrint val}' is of type '${typeOf val}'";
+
+  # typedef :: string -> (a -> bool) -> type
+  #
+  # typedef is the simplified version of typedef' which uses a default
+  # error message constructor.
+  typedef = name: check: typedef' {
+    inherit name;
+    checkType = v:
+      let res = check v;
+      in {
+        ok = res;
+      } // (lib.optionalAttrs (!res) {
+        err = typeError name v;
+      });
+  };
+
+  checkEach = name: t: l: foldl'
+    (acc: e:
+      let
+        res = t.checkType e;
+        isT = t.checkToBool res;
+      in
+      {
+        ok = acc.ok && isT;
+        err =
+          if isT
+          then acc.err
+          else acc.err + "${prettyPrint e}: ${t.toError e res}\n";
+      })
+    { ok = true; err = "expected type ${name}, but found:\n"; }
+    l;
+in
+lib.fix (self: {
+  # Primitive types
+  any = typedef "any" (_: true);
+  unit = typedef "unit" (v: v == { });
+  int = typedef "int" isInt;
+  bool = typedef "bool" isBool;
+  float = typedef "float" isFloat;
+  string = typedef "string" isString;
+  path = typedef "path" (x: typeOf x == "path");
+  drv = typedef "derivation" (x: isAttrs x && x ? "type" && x.type == "derivation");
+  function = typedef "function" (x: isFunction x || (isAttrs x && x ? "__functor"
+    && isFunction x.__functor));
+
+  # Type for types themselves. Useful when defining polymorphic types.
+  type = typedef "type" (x:
+    isAttrs x
+    && hasAttr "name" x && self.string.check x.name
+    && hasAttr "checkType" x && self.function.check x.checkType
+    && hasAttr "checkToBool" x && self.function.check x.checkToBool
+    && hasAttr "toError" x && self.function.check x.toError
+  );
+
+  # Polymorphic types
+  option = t: typedef' rec {
+    name = "option<${t.name}>";
+    checkType = v:
+      let res = t.checkType v;
+      in {
+        ok = isNull v || (self.type t).checkToBool res;
+        err = "expected type ${name}, but value does not conform to '${t.name}': "
+          + t.toError v res;
+      };
+  };
+
+  eitherN = tn: typedef "either<${concatStringsSep ", " (map (x: x.name) tn)}>"
+    (x: any (t: (self.type t).check x) tn);
+
+  either = t1: t2: self.eitherN [ t1 t2 ];
+
+  list = t: typedef' rec {
+    name = "list<${t.name}>";
+
+    checkType = v:
+      if isList v
+      then checkEach name (self.type t) v
+      else {
+        ok = false;
+        err = typeError name v;
+      };
+  };
+
+  attrs = t: typedef' rec {
+    name = "attrs<${t.name}>";
+
+    checkType = v:
+      if isAttrs v
+      then checkEach name (self.type t) (attrValues v)
+      else {
+        ok = false;
+        err = typeError name v;
+      };
+  };
+
+  # Structs / record types
+  #
+  # Checks that all fields match their declared types, no optional
+  # fields are missing and no unexpected fields occur in the struct.
+  #
+  # Anonymous structs are supported (e.g. for nesting) by omitting the
+  # name.
+  #
+  # TODO: Support open records?
+  struct =
+    # Struct checking is more involved than the simpler types above.
+    # To make the actual type definition more readable, several
+    # helpers are defined below.
+    let
+      # checkField checks an individual field of the struct against
+      # its definition and creates a typecheck result. These results
+      # are aggregated during the actual checking.
+      checkField = def: name: value:
+        let result = def.checkType value; in rec {
+          ok = def.checkToBool result;
+          err =
+            if !ok && isNull value
+            then "missing required ${def.name} field '${name}'\n"
+            else "field '${name}': ${def.toError value result}\n";
+        };
+
+      # checkExtraneous determines whether a (closed) struct contains
+      # any fields that are not part of the definition.
+      checkExtraneous = def: has: acc:
+        if (length has) == 0 then acc
+        else if (hasAttr (head has) def)
+        then checkExtraneous def (tail has) acc
+        else
+          checkExtraneous def (tail has) {
+            ok = false;
+            err = acc.err + "unexpected struct field '${head has}'\n";
+          };
+
+      # checkStruct combines all structure checks and creates one
+      # typecheck result from them
+      checkStruct = def: value:
+        let
+          init = { ok = true; err = ""; };
+          extraneous = checkExtraneous def (attrNames value) init;
+
+          checkedFields = map
+            (n:
+              let v = if hasAttr n value then value."${n}" else null;
+              in checkField def."${n}" n v)
+            (attrNames def);
+
+          combined = foldl'
+            (acc: res: {
+              ok = acc.ok && res.ok;
+              err = if !res.ok then acc.err + res.err else acc.err;
+            })
+            init
+            checkedFields;
+        in
+        {
+          ok = combined.ok && extraneous.ok;
+          err = combined.err + extraneous.err;
+        };
+
+      struct' = name: def: typedef' {
+        inherit name def;
+        checkType = value:
+          if isAttrs value
+          then (checkStruct (self.attrs self.type def) value)
+          else { ok = false; err = typeError name value; };
+
+        toError = _: result: "expected '${name}'-struct, but found:\n" + result.err;
+      };
+    in
+    arg: if isString arg then (struct' arg) else (struct' "anon" arg);
+
+  # Enums & pattern matching
+  enum =
+    let
+      plain = name: def: typedef' {
+        inherit name def;
+
+        checkType = (x: isString x && elem x def);
+        checkToBool = x: x;
+        toError = value: _: "'${prettyPrint value} is not a member of enum ${name}";
+      };
+      enum' = name: def: lib.fix (e: (plain name def) // {
+        match = x: actions: deepSeq (map e (attrNames actions)) (
+          let
+            actionKeys = attrNames actions;
+            missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] def;
+          in
+          if (length missing) > 0
+          then throw "Missing match action for members: ${prettyPrint missing}"
+          else actions."${e x}"
+        );
+      });
+    in
+    arg: if isString arg then (enum' arg) else (enum' "anon" arg);
+
+  # Sum types
+  #
+  # The representation of a sum type is an attribute set with only one
+  # value, where the key of the value denotes the variant of the type.
+  sum =
+    let
+      plain = name: def: typedef' {
+        inherit name def;
+        checkType = (x:
+          let variant = elemAt (attrNames x) 0;
+          in if isAttrs x && length (attrNames x) == 1 && hasAttr variant def
+          then
+            let
+              t = def."${variant}";
+              v = x."${variant}";
+              res = t.checkType v;
+            in
+            if t.checkToBool res
+            then { ok = true; }
+            else {
+              ok = false;
+              err = "while checking '${name}' variant '${variant}': "
+                + t.toError v res;
+            }
+          else { ok = false; err = typeError name x; }
+        );
+      };
+      sum' = name: def: lib.fix (s: (plain name def) // {
+        match = x: actions:
+          let
+            variant = deepSeq (s x) (elemAt (attrNames x) 0);
+            actionKeys = attrNames actions;
+            defKeys = attrNames def;
+            missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] defKeys;
+          in
+          if (length missing) > 0
+          then throw "Missing match action for variants: ${prettyPrint missing}"
+          else actions."${variant}" x."${variant}";
+      });
+    in
+    arg: if isString arg then (sum' arg) else (sum' "anon" arg);
+
+  # Typed function definitions
+  #
+  # These definitions wrap the supplied function in type-checking
+  # forms that are evaluated when the function is called.
+  #
+  # Note that typed functions themselves are not types and can not be
+  # used to check values for conformity.
+  defun =
+    let
+      mkFunc = sig: f: {
+        inherit sig;
+        __toString = self: foldl' (s: t: "${s} -> ${t.name}")
+          "λ :: ${(head self.sig).name}"
+          (tail self.sig);
+        __functor = _: f;
+      };
+
+      defun' = sig: func:
+        if length sig > 2
+        then mkFunc sig (x: defun' (tail sig) (func ((head sig) x)))
+        else mkFunc sig (x: ((head (tail sig)) (func ((head sig) x))));
+
+    in
+    sig: func:
+      if length sig < 2
+      then (throw "Signature must at least have two types (a -> b)")
+      else defun' sig func;
+
+  # Restricting types
+  #
+  # `restrict` wraps a type `t`, and uses a predicate `pred` to further
+  # restrict the values, giving the restriction a descriptive `name`.
+  #
+  # First, the wrapped type definition is checked (e.g. int) and then the
+  # value is checked with the predicate, so the predicate can already
+  # depend on the value being of the wrapped type.
+  restrict = name: pred: t:
+    let restriction = "${t.name}[${name}]"; in typedef' {
+      name = restriction;
+      checkType = v:
+        let res = t.checkType v;
+        in
+        if !(t.checkToBool res)
+        then res
+        else
+          let
+            iok = pred v;
+          in
+          if isBool iok then {
+            ok = iok;
+            err = "${prettyPrint v} does not conform to restriction '${restriction}'";
+          } else
+          # use throw here to avoid spamming the build log
+            throw "restriction '${restriction}' predicate returned unexpected value '${prettyPrint iok}' instead of boolean";
+    };
+
+})
diff --git a/nix/yants/screenshots/enums.png b/nix/yants/screenshots/enums.png
new file mode 100644
index 0000000000..71673e7ab6
--- /dev/null
+++ b/nix/yants/screenshots/enums.png
Binary files differdiff --git a/nix/yants/screenshots/functions.png b/nix/yants/screenshots/functions.png
new file mode 100644
index 0000000000..30ed50f832
--- /dev/null
+++ b/nix/yants/screenshots/functions.png
Binary files differdiff --git a/nix/yants/screenshots/nested-structs.png b/nix/yants/screenshots/nested-structs.png
new file mode 100644
index 0000000000..6b03ed65ce
--- /dev/null
+++ b/nix/yants/screenshots/nested-structs.png
Binary files differdiff --git a/nix/yants/screenshots/simple.png b/nix/yants/screenshots/simple.png
new file mode 100644
index 0000000000..05a302cc6b
--- /dev/null
+++ b/nix/yants/screenshots/simple.png
Binary files differdiff --git a/nix/yants/screenshots/structs.png b/nix/yants/screenshots/structs.png
new file mode 100644
index 0000000000..fcbcf6415f
--- /dev/null
+++ b/nix/yants/screenshots/structs.png
Binary files differdiff --git a/nix/yants/tests/default.nix b/nix/yants/tests/default.nix
new file mode 100644
index 0000000000..0c7ec24188
--- /dev/null
+++ b/nix/yants/tests/default.nix
@@ -0,0 +1,158 @@
+{ depot, pkgs, ... }:
+
+with depot.nix.yants;
+
+# Note: Derivations are not included in the tests below as they cause
+# issues with deepSeq.
+
+let
+
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    assertDoesNotThrow
+    ;
+
+  # this derivation won't throw if evaluated with deepSeq
+  # unlike most things even remotely related with nixpkgs
+  trivialDerivation = derivation {
+    name = "trivial-derivation";
+    inherit (pkgs.stdenv) system;
+    builder = "/bin/sh";
+    args = [ "-c" "echo hello > $out" ];
+  };
+
+  testPrimitives = it "checks that all primitive types match" [
+    (assertDoesNotThrow "unit type" (unit { }))
+    (assertDoesNotThrow "int type" (int 15))
+    (assertDoesNotThrow "bool type" (bool false))
+    (assertDoesNotThrow "float type" (float 13.37))
+    (assertDoesNotThrow "string type" (string "Hello!"))
+    (assertDoesNotThrow "function type" (function (x: x * 2)))
+    (assertDoesNotThrow "path type" (path /nix))
+    (assertDoesNotThrow "derivation type" (drv trivialDerivation))
+  ];
+
+  testPoly = it "checks that polymorphic types work as intended" [
+    (assertDoesNotThrow "option type" (option int null))
+    (assertDoesNotThrow "list type" (list string [ "foo" "bar" ]))
+    (assertDoesNotThrow "either type" (either int float 42))
+  ];
+
+  # Test that structures work as planned.
+  person = struct "person" {
+    name = string;
+    age = int;
+
+    contact = option (struct {
+      email = string;
+      phone = option string;
+    });
+  };
+
+  testStruct = it "checks that structures work as intended" [
+    (assertDoesNotThrow "person struct" (person {
+      name = "Brynhjulf";
+      age = 42;
+      contact.email = "brynhjulf@yants.nix";
+    }))
+  ];
+
+  # Test enum definitions & matching
+  colour = enum "colour" [ "red" "blue" "green" ];
+  colourMatcher = {
+    red = "It is in fact red!";
+    blue = "It should not be blue!";
+    green = "It should not be green!";
+  };
+
+  testEnum = it "checks enum definitions and matching" [
+    (assertEq "enum is matched correctly"
+      "It is in fact red!"
+      (colour.match "red" colourMatcher))
+    (assertThrows "out of bounds enum fails"
+      (colour.match "alpha" (colourMatcher // {
+        alpha = "This should never happen";
+      }))
+    )
+  ];
+
+  # Test sum type definitions
+  creature = sum "creature" {
+    human = struct {
+      name = string;
+      age = option int;
+    };
+
+    pet = enum "pet" [ "dog" "lizard" "cat" ];
+  };
+  some-human = creature {
+    human = {
+      name = "Brynhjulf";
+      age = 42;
+    };
+  };
+
+  testSum = it "checks sum types definitions and matching" [
+    (assertDoesNotThrow "creature sum type" some-human)
+    (assertEq "sum type is matched correctly"
+      "It's a human named Brynhjulf"
+      (creature.match some-human {
+        human = v: "It's a human named ${v.name}";
+        pet = v: "It's not supposed to be a pet!";
+      })
+    )
+  ];
+
+  # Test curried function definitions
+  func = defun [ string int string ]
+    (name: age: "${name} is ${toString age} years old");
+
+  testFunctions = it "checks function definitions" [
+    (assertDoesNotThrow "function application" (func "Brynhjulf" 42))
+  ];
+
+  # Test that all types are types.
+  assertIsType = name: t:
+    assertDoesNotThrow "${name} is a type" (type t);
+  testTypes = it "checks that all types are types" [
+    (assertIsType "any" any)
+    (assertIsType "bool" bool)
+    (assertIsType "drv" drv)
+    (assertIsType "float" float)
+    (assertIsType "int" int)
+    (assertIsType "string" string)
+    (assertIsType "path" path)
+
+    (assertIsType "attrs int" (attrs int))
+    (assertIsType "eitherN [ ... ]" (eitherN [ int string bool ]))
+    (assertIsType "either int string" (either int string))
+    (assertIsType "enum [ ... ]" (enum [ "foo" "bar" ]))
+    (assertIsType "list string" (list string))
+    (assertIsType "option int" (option int))
+    (assertIsType "option (list string)" (option (list string)))
+    (assertIsType "struct { ... }" (struct { a = int; b = option string; }))
+    (assertIsType "sum { ... }" (sum { a = int; b = option string; }))
+  ];
+
+  testRestrict = it "checks restrict types" [
+    (assertDoesNotThrow "< 42" ((restrict "< 42" (i: i < 42) int) 25))
+    (assertDoesNotThrow "list length < 3"
+      ((restrict "not too long" (l: builtins.length l < 3) (list int)) [ 1 2 ]))
+    (assertDoesNotThrow "list eq 5"
+      (list (restrict "eq 5" (v: v == 5) any) [ 5 5 5 ]))
+  ];
+
+in
+runTestsuite "yants" [
+  testPrimitives
+  testPoly
+  testStruct
+  testEnum
+  testSum
+  testFunctions
+  testTypes
+  testRestrict
+]
diff --git a/ops/besadii/default.nix b/ops/besadii/default.nix
new file mode 100644
index 0000000000..1199c56cfb
--- /dev/null
+++ b/ops/besadii/default.nix
@@ -0,0 +1,8 @@
+# This program is used as a Gerrit hook to trigger builds on
+# Buildkite, Sourcegraph reindexing and other maintenance tasks.
+{ depot, ... }:
+
+depot.nix.buildGo.program {
+  name = "besadii";
+  srcs = [ ./main.go ];
+}
diff --git a/ops/besadii/main.go b/ops/besadii/main.go
new file mode 100644
index 0000000000..412ae3a73b
--- /dev/null
+++ b/ops/besadii/main.go
@@ -0,0 +1,558 @@
+// Copyright 2019-2020 Google LLC.
+// SPDX-License-Identifier: Apache-2.0
+//
+// besadii is a small CLI tool that is invoked as a hook by various
+// programs to cause CI-related actions.
+//
+// It supports the following modes & operations:
+//
+// Gerrit (ref-updated) hook:
+// - Trigger Buildkite CI builds
+// - Trigger SourceGraph repository index updates
+//
+// Buildkite (post-command) hook:
+// - Submit CL verification status back to Gerrit
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log/syslog"
+	"net/http"
+	"net/mail"
+	"os"
+	"os/user"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// Regular expression to extract change ID out of a URL
+var changeIdRegexp = regexp.MustCompile(`^.*/(\d+)$`)
+
+// Regular expression to check if gerritChangeName valid. The
+// limitation could be what is allowed for a git branch name. For now
+// we want to have a stricter limitation for readability and ease of
+// use.
+var gerritChangeNameRegexp = `^[a-z0-9]+$`
+var gerritChangeNameCheck = regexp.MustCompile(gerritChangeNameRegexp)
+
+// besadii configuration file structure
+type config struct {
+	// Required configuration for Buildkite<>Gerrit monorepo
+	// integration.
+	Repository       string `json:"repository"`
+	Branch           string `json:"branch"`
+	GerritUrl        string `json:"gerritUrl"`
+	GerritUser       string `json:"gerritUser"`
+	GerritPassword   string `json:"gerritPassword"`
+	GerritLabel      string `json:"gerritLabel"`
+	BuildkiteOrg     string `json:"buildkiteOrg"`
+	BuildkiteProject string `json:"buildkiteProject"`
+	BuildkiteToken   string `json:"buildkiteToken"`
+	GerritChangeName string `json:"gerritChangeName"`
+
+	// Optional configuration for Sourcegraph trigger updates.
+	SourcegraphUrl   string `json:"sourcegraphUrl"`
+	SourcegraphToken string `json:"sourcegraphToken"`
+}
+
+// buildTrigger represents the information passed to besadii when it
+// is invoked as a Gerrit hook.
+//
+// https://gerrit.googlesource.com/plugins/hooks/+/HEAD/src/main/resources/Documentation/hooks.md
+type buildTrigger struct {
+	project string
+	ref     string
+	commit  string
+	author  string
+	email   string
+
+	changeId string
+	patchset string
+}
+
+type Author struct {
+	Name  string `json:"name"`
+	Email string `json:"email"`
+}
+
+// Build is the representation of a Buildkite build as described on
+// https://buildkite.com/docs/apis/rest-api/builds#create-a-build
+type Build struct {
+	Commit string            `json:"commit"`
+	Branch string            `json:"branch"`
+	Author Author            `json:"author"`
+	Env    map[string]string `json:"env"`
+}
+
+// BuildResponse is the representation of Buildkite's success response
+// after triggering a build. This has many fields, but we only need
+// one of them.
+type buildResponse struct {
+	WebUrl string `json:"web_url"`
+}
+
+// reviewInput is a struct representing the data submitted to Gerrit
+// to post a review on a CL.
+//
+// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input
+type reviewInput struct {
+	Message                        string         `json:"message"`
+	Labels                         map[string]int `json:"labels,omitempty"`
+	OmitDuplicateComments          bool           `json:"omit_duplicate_comments"`
+	IgnoreDefaultAttentionSetRules bool           `json:"ignore_default_attention_set_rules"`
+	Tag                            string         `json:"tag"`
+	Notify                         string         `json:"notify,omitempty"`
+}
+
+func defaultConfigLocation() (string, error) {
+	usr, err := user.Current()
+	if err != nil {
+		return "", fmt.Errorf("failed to get current user: %w", err)
+	}
+
+	return path.Join(usr.HomeDir, "besadii.json"), nil
+}
+
+func loadConfig() (*config, error) {
+	configPath := os.Getenv("BESADII_CONFIG")
+
+	if configPath == "" {
+		var err error
+		configPath, err = defaultConfigLocation()
+		if err != nil {
+			return nil, fmt.Errorf("failed to get config location: %w", err)
+		}
+	}
+
+	configJson, err := ioutil.ReadFile(configPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load besadii config: %w", err)
+	}
+
+	var cfg config
+	err = json.Unmarshal(configJson, &cfg)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshal besadii config: %w", err)
+	}
+
+	// The default Gerrit label to set is 'Verified', unless specified otherwise.
+	if cfg.GerritLabel == "" {
+		cfg.GerritLabel = "Verified"
+	}
+
+	// The default text referring to a Gerrit Change in BuildKite.
+	if cfg.GerritChangeName == "" {
+		cfg.GerritChangeName = "cl"
+	}
+	if !gerritChangeNameCheck.MatchString(cfg.GerritChangeName) {
+		return nil, fmt.Errorf("invalid 'gerritChangeName': %s", cfg.GerritChangeName)
+	}
+
+	// Rudimentary config validation logic
+	if cfg.SourcegraphUrl != "" && cfg.SourcegraphToken == "" {
+		return nil, fmt.Errorf("'SourcegraphToken' must be set if 'SourcegraphUrl' is set")
+	}
+
+	if cfg.Repository == "" || cfg.Branch == "" {
+		return nil, fmt.Errorf("missing repository configuration (required: repository, branch)")
+	}
+
+	if cfg.GerritUrl == "" || cfg.GerritUser == "" || cfg.GerritPassword == "" {
+		return nil, fmt.Errorf("missing Gerrit configuration (required: gerritUrl, gerritUser, gerritPassword)")
+	}
+
+	if cfg.BuildkiteOrg == "" || cfg.BuildkiteProject == "" || cfg.BuildkiteToken == "" {
+		return nil, fmt.Errorf("mising Buildkite configuration (required: buildkiteOrg, buildkiteProject, buildkiteToken)")
+	}
+
+	return &cfg, nil
+}
+
+// linkToChange creates the full link to a change's patchset in Gerrit
+func linkToChange(cfg *config, changeId, patchset string) string {
+	return fmt.Sprintf("%s/c/%s/+/%s/%s", cfg.GerritUrl, cfg.Repository, changeId, patchset)
+}
+
+// updateGerrit posts a comment on a Gerrit CL to indicate the current build status.
+func updateGerrit(cfg *config, review reviewInput, changeId, patchset string) {
+	body, _ := json.Marshal(review)
+	reader := ioutil.NopCloser(bytes.NewReader(body))
+
+	url := fmt.Sprintf("%s/a/changes/%s/revisions/%s/review", cfg.GerritUrl, changeId, patchset)
+	req, err := http.NewRequest("POST", url, reader)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to create an HTTP request: %w", err)
+		os.Exit(1)
+	}
+
+	req.SetBasicAuth(cfg.GerritUser, cfg.GerritPassword)
+	req.Header.Add("Content-Type", "application/json")
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		fmt.Errorf("failed to update %s on %s: %w", cfg.GerritChangeName, cfg.GerritUrl, err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		respBody, _ := ioutil.ReadAll(resp.Body)
+		fmt.Fprintf(os.Stderr, "received non-success response from Gerrit: %s (%v)", respBody, resp.Status)
+	} else {
+		fmt.Printf("Added CI status comment on %s", linkToChange(cfg, changeId, patchset))
+	}
+}
+
+// Trigger a build of a given branch & commit on Buildkite
+func triggerBuild(cfg *config, log *syslog.Writer, trigger *buildTrigger) error {
+	env := make(map[string]string)
+	branch := trigger.ref
+
+	// Pass information about the originating Gerrit change to the
+	// build, if it is for a patchset.
+	//
+	// This information is later used by besadii when invoked by Gerrit
+	// to communicate the build status back to Gerrit.
+	headBuild := true
+	if trigger.changeId != "" && trigger.patchset != "" {
+		env["GERRIT_CHANGE_URL"] = linkToChange(cfg, trigger.changeId, trigger.patchset)
+		env["GERRIT_CHANGE_ID"] = trigger.changeId
+		env["GERRIT_PATCHSET"] = trigger.patchset
+		headBuild = false
+
+		// The branch doesn't have to be a real ref (it's just used to
+		// group builds), so make it the identifier for the CL.
+		branch = fmt.Sprintf("%s/%v", cfg.GerritChangeName, strings.Split(trigger.ref, "/")[3])
+	}
+
+	build := Build{
+		Commit: trigger.commit,
+		Branch: branch,
+		Env:    env,
+		Author: Author{
+			Name:  trigger.author,
+			Email: trigger.email,
+		},
+	}
+
+	body, _ := json.Marshal(build)
+	reader := ioutil.NopCloser(bytes.NewReader(body))
+
+	bkUrl := fmt.Sprintf("https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds", cfg.BuildkiteOrg, cfg.BuildkiteProject)
+	req, err := http.NewRequest("POST", bkUrl, reader)
+	if err != nil {
+		return fmt.Errorf("failed to create an HTTP request: %w", err)
+	}
+
+	req.Header.Add("Authorization", "Bearer "+cfg.BuildkiteToken)
+	req.Header.Add("Content-Type", "application/json")
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		// This might indicate a temporary error on the Buildkite side.
+		return fmt.Errorf("failed to send Buildkite request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("failed to read Buildkite response body: %w", err)
+	}
+
+	if resp.StatusCode != http.StatusCreated {
+		return fmt.Errorf("received non-success response from Buildkite: %s (%v)", respBody, resp.Status)
+	}
+
+	var buildResp buildResponse
+	err = json.Unmarshal(respBody, &buildResp)
+	if err != nil {
+		return fmt.Errorf("failed to unmarshal build response: %w", err)
+	}
+
+	fmt.Fprintf(log, "triggered build for ref %q at commit %q: %s", trigger.ref, trigger.commit, buildResp.WebUrl)
+
+	// For builds of the HEAD branch there is nothing else to do
+	if headBuild {
+		return nil
+	}
+
+	// Report the status back to the Gerrit CL so that users can click
+	// through to the running build.
+	msg := fmt.Sprintf("Started build for patchset #%s on: %s", trigger.patchset, buildResp.WebUrl)
+	review := reviewInput{
+		Message:               msg,
+		OmitDuplicateComments: true,
+		Tag:                   "autogenerated:buildkite~trigger",
+
+		// Do not update the attention set for this comment.
+		IgnoreDefaultAttentionSetRules: true,
+
+		Notify: "NONE",
+	}
+	updateGerrit(cfg, review, trigger.changeId, trigger.patchset)
+
+	return nil
+}
+
+// Trigger a Sourcegraph repository index update.
+//
+// https://docs.sourcegraph.com/admin/repo/webhooks
+func triggerIndexUpdate(cfg *config, log *syslog.Writer) error {
+	req, err := http.NewRequest("POST", cfg.SourcegraphUrl, nil)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Add("Authorization", "token "+cfg.SourcegraphToken)
+
+	_, err = http.DefaultClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("failed to trigger Sourcegraph index update: %w", err)
+	}
+
+	log.Info("triggered sourcegraph index update")
+	return nil
+}
+
+// Gerrit passes more flags than we want, but Rob Pike decided[0] in
+// 2013 that the Go art project will not allow users to ignore flags
+// because he "doesn't like it". This function allows users to ignore
+// flags.
+//
+// [0]: https://github.com/golang/go/issues/6112#issuecomment-66083768
+func ignoreFlags(ignore []string) {
+	for _, f := range ignore {
+		flag.String(f, "", "flag to ignore")
+	}
+}
+
+// Extract the username & email from Gerrit's uploader flag and set it
+// on the trigger struct, for display in Buildkite.
+func extractChangeUploader(uploader string, trigger *buildTrigger) error {
+	// Gerrit passes the uploader in another extra layer of quotes.
+	uploader, err := strconv.Unquote(uploader)
+	if err != nil {
+		return fmt.Errorf("failed to unquote email - forgot quotes on manual invocation?: %w", err)
+	}
+
+	// Extract the uploader username & email from the input passed by
+	// Gerrit (in RFC 5322 format).
+	addr, err := mail.ParseAddress(uploader)
+	if err != nil {
+		return fmt.Errorf("invalid change uploader (%s): %w", uploader, err)
+	}
+
+	trigger.author = addr.Name
+	trigger.email = addr.Address
+
+	return nil
+}
+
+// Extract the buildtrigger struct out of the flags passed to besadii
+// when invoked as Gerrit's 'patchset-created' hook. This hook is used
+// for triggering CI on in-progress CLs.
+func buildTriggerFromPatchsetCreated(cfg *config) (*buildTrigger, error) {
+	// Information that needs to be returned
+	var trigger buildTrigger
+
+	// Information that is only needed for parsing
+	var targetBranch, changeUrl, uploader, kind string
+
+	flag.StringVar(&trigger.project, "project", "", "Gerrit project")
+	flag.StringVar(&trigger.commit, "commit", "", "commit hash")
+	flag.StringVar(&trigger.patchset, "patchset", "", "patchset ID")
+
+	flag.StringVar(&targetBranch, "branch", "", "CL target branch")
+	flag.StringVar(&changeUrl, "change-url", "", "HTTPS URL of change")
+	flag.StringVar(&uploader, "uploader", "", "Change uploader name & email")
+	flag.StringVar(&kind, "kind", "", "Kind of patchset")
+
+	// patchset-created also passes various flags which we don't need.
+	ignoreFlags([]string{"topic", "change", "uploader-username", "change-owner", "change-owner-username"})
+
+	flag.Parse()
+
+	// Ignore patchsets which do not contain code changes
+	if kind == "NO_CODE_CHANGE" || kind == "NO_CHANGE" {
+		return nil, nil
+	}
+
+	// Parse username & email
+	err := extractChangeUploader(uploader, &trigger)
+	if err != nil {
+		return nil, err
+	}
+
+	// If the patchset is not for the HEAD branch of the monorepo, then
+	// we can ignore it. It might be some other kind of change
+	// (refs/meta/config or Gerrit-internal), but it is not an error.
+	if trigger.project != cfg.Repository || targetBranch != cfg.Branch {
+		return nil, nil
+	}
+
+	// Change ID is not directly passed in the numeric format, so we
+	// need to extract it out of the URL
+	matches := changeIdRegexp.FindStringSubmatch(changeUrl)
+	trigger.changeId = matches[1]
+
+	// Construct the CL ref from which the build should happen.
+	changeId, _ := strconv.Atoi(trigger.changeId)
+	trigger.ref = fmt.Sprintf(
+		"refs/changes/%02d/%s/%s",
+		changeId%100, trigger.changeId, trigger.patchset,
+	)
+
+	return &trigger, nil
+}
+
+// Extract the buildtrigger struct out of the flags passed to besadii
+// when invoked as Gerrit's 'change-merged' hook. This hook is used
+// for triggering HEAD builds after change submission.
+func buildTriggerFromChangeMerged(cfg *config) (*buildTrigger, error) {
+	// Information that needs to be returned
+	var trigger buildTrigger
+
+	// Information that is only needed for parsing
+	var targetBranch, submitter string
+
+	flag.StringVar(&trigger.project, "project", "", "Gerrit project")
+	flag.StringVar(&trigger.commit, "commit", "", "Commit hash")
+	flag.StringVar(&submitter, "submitter", "", "Submitter email & username")
+	flag.StringVar(&targetBranch, "branch", "", "CL target branch")
+
+	// Ignore extra flags passed by change-merged
+	ignoreFlags([]string{"change", "topic", "change-url", "submitter-username", "newrev", "change-owner", "change-owner-username"})
+
+	flag.Parse()
+
+	// Parse username & email
+	err := extractChangeUploader(submitter, &trigger)
+	if err != nil {
+		return nil, err
+	}
+
+	// If the patchset is not for the HEAD branch of the monorepo, then
+	// we can ignore it.
+	if trigger.project != cfg.Repository || targetBranch != cfg.Branch {
+		return nil, nil
+	}
+
+	trigger.ref = "refs/heads/" + targetBranch
+
+	return &trigger, nil
+}
+
+func gerritHookMain(cfg *config, log *syslog.Writer, trigger *buildTrigger) {
+	if trigger == nil {
+		// The hook was not for something we care about.
+		os.Exit(0)
+	}
+
+	err := triggerBuild(cfg, log, trigger)
+
+	if err != nil {
+		log.Err(fmt.Sprintf("failed to trigger Buildkite build: %s", err))
+	}
+
+	if cfg.SourcegraphUrl != "" && trigger.ref == "refs/heads/canon" {
+		err = triggerIndexUpdate(cfg, log)
+		if err != nil {
+			log.Err(fmt.Sprintf("failed to trigger sourcegraph index update: %s", err))
+		}
+	}
+}
+
+func postCommandMain(cfg *config) {
+	changeId := os.Getenv("GERRIT_CHANGE_ID")
+	patchset := os.Getenv("GERRIT_PATCHSET")
+
+	if changeId == "" || patchset == "" {
+		// If these variables are unset, but the hook was invoked, the
+		// build was most likely for a branch and not for a CL - no status
+		// needs to be reported back to Gerrit!
+		fmt.Printf("This isn't a %s build, nothing to do. Have a nice day!\n", cfg.GerritChangeName)
+		return
+	}
+
+	if os.Getenv("BUILDKITE_LABEL") != ":duck:" {
+		// this is not the build stage, don't do anything.
+		return
+	}
+
+	var vote int
+	var verb string
+	var notify string
+
+	if os.Getenv("BUILDKITE_COMMAND_EXIT_STATUS") == "0" {
+		vote = 1 // automation passed: +1 in Gerrit
+		verb = "passed"
+		notify = "NONE"
+	} else {
+		vote = -1
+		verb = "failed"
+		notify = "OWNER"
+	}
+
+	msg := fmt.Sprintf("Build of patchset %s %s: %s", patchset, verb, os.Getenv("BUILDKITE_BUILD_URL"))
+	review := reviewInput{
+		Message:               msg,
+		OmitDuplicateComments: true,
+		Labels: map[string]int{
+			cfg.GerritLabel: vote,
+		},
+
+		// Update the attention set if we are failing this patchset.
+		IgnoreDefaultAttentionSetRules: vote == 1,
+
+		Tag: "autogenerated:buildkite~result",
+
+		Notify: notify,
+	}
+	updateGerrit(cfg, review, changeId, patchset)
+}
+
+func main() {
+	// Logging happens in syslog because it's almost impossible to get
+	// output out of Gerrit hooks otherwise.
+	log, err := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "besadii")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to open syslog: %s\n", err)
+		os.Exit(1)
+	}
+
+	log.Info(fmt.Sprintf("besadii called with arguments: %v", os.Args))
+
+	bin := path.Base(os.Args[0])
+	cfg, err := loadConfig()
+
+	if err != nil {
+		log.Crit(fmt.Sprintf("besadii configuration error: %v", err))
+		os.Exit(4)
+	}
+
+	if bin == "patchset-created" {
+		trigger, err := buildTriggerFromPatchsetCreated(cfg)
+		if err != nil {
+			log.Crit(fmt.Sprintf("failed to parse 'patchset-created' invocation from args: %v", err))
+			os.Exit(1)
+		}
+		gerritHookMain(cfg, log, trigger)
+	} else if bin == "change-merged" {
+		trigger, err := buildTriggerFromChangeMerged(cfg)
+		if err != nil {
+			log.Crit(fmt.Sprintf("failed to parse 'change-merged' invocation from args: %v", err))
+			os.Exit(1)
+		}
+		gerritHookMain(cfg, log, trigger)
+	} else if bin == "post-command" {
+		postCommandMain(cfg)
+	} else {
+		fmt.Fprintf(os.Stderr, "besadii does not know how to be invoked as %q, sorry!", bin)
+		os.Exit(1)
+	}
+}
diff --git a/ops/deploy-whitby/default.nix b/ops/deploy-whitby/default.nix
new file mode 100644
index 0000000000..aafe798cbf
--- /dev/null
+++ b/ops/deploy-whitby/default.nix
@@ -0,0 +1,31 @@
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "deploy-whitby";
+
+  phases = [ "installPhase" "installCheckPhase" ];
+
+  nativeBuildInputs = with pkgs; [
+    makeWrapper
+  ];
+
+  installPhase = ''
+    mkdir -p $out/bin
+    makeWrapper ${./deploy-whitby.sh} $out/bin/deploy-whitby.sh \
+      --prefix PATH : ${with pkgs; lib.makeBinPath [
+        ansi2html
+        git
+        jq
+        nvd
+      ]}
+  '';
+
+  installCheckInputs = with pkgs; [
+    shellcheck
+  ];
+
+  doInstallCheck = true;
+  installCheckPhase = ''
+    shellcheck $out/bin/deploy-whitby.sh
+  '';
+}
diff --git a/ops/deploy-whitby/deploy-whitby.sh b/ops/deploy-whitby/deploy-whitby.sh
new file mode 100755
index 0000000000..756aa7ae08
--- /dev/null
+++ b/ops/deploy-whitby/deploy-whitby.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+set -Ceuo pipefail
+
+HTML_ROOT="${HTML_ROOT:-/var/html/deploys.tvl.fyi}"
+URL_BASE="${URL_BASE:-https://deploys.tvl.fyi/diff}"
+IRCCAT_PORT="${IRCCAT_PORT:-4722}"
+
+drv_hash() {
+    basename "$1" | sed 's/-.*//'
+}
+
+new_rev="$1"
+
+if [ -z "$new_rev" ]; then
+    >&2 echo "Usage: $0 <new_rev>"
+    exit 1
+fi
+
+if [ -d "/tmp/deploy.worktree" ]; then
+    >&2 echo "/tmp/deploy.worktree exists - exiting in case another deploy is currently running"
+    exit 1
+fi
+
+worktree_dir=/tmp/worktree_dir
+
+cleanup() {
+    rm -rf "$worktree_dir"
+}
+trap cleanup EXIT
+
+git clone https://cl.tvl.fyi/depot "$worktree_dir" --reference /depot
+git -C "$worktree_dir" checkout "$new_rev"
+
+current=$(nix show-derivation /run/current-system | jq -r 'keys | .[0]')
+new=$(nix-instantiate -A ops.nixos.whitbySystem "$worktree_dir")
+
+diff_filename="$(drv_hash "$current")..$(drv_hash "$new").html"
+nvd --color always diff "$current" "$new" \
+    | ansi2html \
+    >| "$HTML_ROOT/diff/$diff_filename"
+chmod a+r "$HTML_ROOT/diff/$diff_filename"
+
+echo "#tvl whitby is being deployed! system diff: $URL_BASE/$diff_filename" \
+    | nc -w 5 -N localhost "$IRCCAT_PORT"
+
+# TODO(grfn): Actually do the deploy
diff --git a/ops/dns/README.md b/ops/dns/README.md
new file mode 100644
index 0000000000..2290299fe4
--- /dev/null
+++ b/ops/dns/README.md
@@ -0,0 +1,11 @@
+DNS configuration
+=================
+
+This folder contains configuration for our DNS zones. The zones are hosted with
+Google Cloud DNS, which supports zone-file based import/export.
+
+Currently there is no automation to deploy these zones, but CI will check their
+integrity.
+
+*Note: While each zone file specifies an SOA record, it only exists to satisfy
+`named-checkzone`. Cloud DNS manages this record for us.*
diff --git a/ops/dns/default.nix b/ops/dns/default.nix
new file mode 100644
index 0000000000..ad6e136f27
--- /dev/null
+++ b/ops/dns/default.nix
@@ -0,0 +1,14 @@
+# Performs simple (local-only) validity checks on DNS zones.
+{ depot, pkgs, ... }:
+
+let
+  checkZone = zone: file: pkgs.runCommandNoCC "${zone}-check" { } ''
+    ${pkgs.bind}/bin/named-checkzone -i local ${zone} ${file} | tee $out
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  nixery-dev = checkZone "nixery.dev" ./nixery.dev.zone;
+  tvl-fyi = checkZone "tvl.fyi" ./tvl.fyi.zone;
+  tvl-su = checkZone "tvl.su" ./tvl.su.zone;
+}
diff --git a/ops/dns/nixery.dev.zone b/ops/dns/nixery.dev.zone
new file mode 100644
index 0000000000..44cabab29b
--- /dev/null
+++ b/ops/dns/nixery.dev.zone
@@ -0,0 +1,10 @@
+;; Google Cloud DNS zone for nixery.dev
+nixery.dev. 21600 IN SOA ns-cloud-b1.googledomains.com. cloud-dns-hostmaster.google.com. 5 21600 3600 259200 300
+nixery.dev. 21600 IN NS ns-cloud-b1.googledomains.com.
+nixery.dev. 21600 IN NS ns-cloud-b2.googledomains.com.
+nixery.dev. 21600 IN NS ns-cloud-b3.googledomains.com.
+nixery.dev. 21600 IN NS ns-cloud-b4.googledomains.com.
+
+;; Records for pointing nixery.dev to whitby
+nixery.dev. 300 IN A 49.12.129.211
+nixery.dev. 300 IN AAAA 2a01:4f8:242:5b21:0:feed:edef:beef
diff --git a/ops/dns/tvl.fyi.zone b/ops/dns/tvl.fyi.zone
new file mode 100644
index 0000000000..d1961c6a7a
--- /dev/null
+++ b/ops/dns/tvl.fyi.zone
@@ -0,0 +1,39 @@
+;; Google Cloud DNS zone for tvl.fyi.
+;;
+;; This zone is hosted in the project 'tvl-fyi', and registered via
+;; Google Domains.
+tvl.fyi. 21600 IN SOA ns-cloud-b1.googledomains.com. cloud-dns-hostmaster.google.com. 20 21600 3600 259200 300
+tvl.fyi. 21600 IN NS ns-cloud-b1.googledomains.com.
+tvl.fyi. 21600 IN NS ns-cloud-b2.googledomains.com.
+tvl.fyi. 21600 IN NS ns-cloud-b3.googledomains.com.
+tvl.fyi. 21600 IN NS ns-cloud-b4.googledomains.com.
+
+;; Mail forwarding (via domains.google)
+tvl.fyi. 3600 IN MX 5 gmr-smtp-in.l.google.com.
+tvl.fyi. 3600 IN MX 10 alt1.gmr-smtp-in.l.google.com.
+tvl.fyi. 3600 IN MX 20 alt2.gmr-smtp-in.l.google.com.
+tvl.fyi. 3600 IN MX 30 alt3.gmr-smtp-in.l.google.com.
+tvl.fyi. 3600 IN MX 40 alt4.gmr-smtp-in.l.google.com.
+
+;; Landing website is hosted on whitby on the apex.
+tvl.fyi. 21600 IN A 49.12.129.211
+tvl.fyi. 21600 IN AAAA 2a01:4f8:242:5b21:0:feed:edef:beef
+
+;; TVL infrastructure
+whitby.tvl.fyi. 21600 IN A 49.12.129.211
+whitby.tvl.fyi. 21600 IN AAAA 2a01:4f8:242:5b21:0:feed:edef:beef
+
+;; TVL services
+at.tvl.fyi.      21600 IN CNAME whitby.tvl.fyi.
+atward.tvl.fyi.  21600 IN CNAME whitby.tvl.fyi.
+b.tvl.fyi.       21600 IN CNAME whitby.tvl.fyi.
+cache.tvl.fyi.   21600 IN CNAME whitby.tvl.fyi.
+cl.tvl.fyi.      21600 IN CNAME whitby.tvl.fyi.
+code.tvl.fyi.    21600 IN CNAME whitby.tvl.fyi.
+cs.tvl.fyi.      21600 IN CNAME whitby.tvl.fyi.
+deploys.tvl.fyi. 21600 IN CNAME whitby.tvl.fyi.
+images.tvl.fyi.  21600 IN CNAME whitby.tvl.fyi.
+login.tvl.fyi.   21600 IN CNAME whitby.tvl.fyi.
+static.tvl.fyi.  21600 IN CNAME whitby.tvl.fyi.
+status.tvl.fyi.  21600 IN CNAME whitby.tvl.fyi.
+todo.tvl.fyi.    21600 IN CNAME whitby.tvl.fyi.
diff --git a/ops/dns/tvl.su.zone b/ops/dns/tvl.su.zone
new file mode 100644
index 0000000000..da46752f13
--- /dev/null
+++ b/ops/dns/tvl.su.zone
@@ -0,0 +1,51 @@
+;; Google Cloud DNS for tvl.su.
+;;
+;; This zone is hosted in the project 'tvl-fyi', and registered via
+;; NIC.RU.
+;;
+;; This zone is mostly identical to tvl.fyi and will eventually become
+;; the primary zone.
+tvl.su. 21600 IN SOA ns-cloud-b1.googledomains.com. cloud-dns-hostmaster.google.com. 33 21600 3600 259200 300
+tvl.su. 21600 IN NS ns-cloud-b1.googledomains.com.
+tvl.su. 21600 IN NS ns-cloud-b2.googledomains.com.
+tvl.su. 21600 IN NS ns-cloud-b3.googledomains.com.
+tvl.su. 21600 IN NS ns-cloud-b4.googledomains.com.
+
+;; Landing website is hosted on whitby on the apex.
+tvl.su. 21600 IN A 49.12.129.211
+tvl.su. 21600 IN AAAA 2a01:4f8:242:5b21:0:feed:edef:beef
+
+;; TVL infrastructure
+whitby.tvl.su. 21600 IN A 49.12.129.211
+whitby.tvl.su. 21600 IN AAAA 2a01:4f8:242:5b21:0:feed:edef:beef
+
+;; TVL services
+at.tvl.su.     21600 IN CNAME whitby.tvl.su.
+atward.tvl.su. 21600 IN CNAME whitby.tvl.su.
+b.tvl.su.      21600 IN CNAME whitby.tvl.su.
+cache.tvl.su.  21600 IN CNAME whitby.tvl.su.
+cl.tvl.su.     21600 IN CNAME whitby.tvl.su.
+code.tvl.su.   21600 IN CNAME whitby.tvl.su.
+cs.tvl.su.     21600 IN CNAME whitby.tvl.su.
+images.tvl.su. 21600 IN CNAME whitby.tvl.su.
+login.tvl.su.  21600 IN CNAME whitby.tvl.su.
+static.tvl.su. 21600 IN CNAME whitby.tvl.su.
+status.tvl.su. 21600 IN CNAME whitby.tvl.su.
+todo.tvl.su.   21600 IN CNAME whitby.tvl.su.
+
+;; Google Workspaces domain verification
+tvl.su. 21600 IN TXT "google-site-verification=3ksTBzFK3lZlzD3ddBfpaHs9qasfAiYBmvbW2T_ejH4"
+
+;; Google Workspaces email configuration
+tvl.su. 21600 IN MX 1 aspmx.l.google.com.
+tvl.su. 21600 IN MX 5 alt1.aspmx.l.google.com.
+tvl.su. 21600 IN MX 5 alt2.aspmx.l.google.com.
+tvl.su. 21600 IN MX 10 alt3.aspmx.l.google.com.
+tvl.su. 21600 IN MX 10 alt4.aspmx.l.google.com.
+tvl.su. 21600 IN TXT "v=spf1 include:_spf.google.com ~all"
+google._domainkey.tvl.su. 21600 IN TXT ("v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlqCbnGa8oPwrudJK60l6MJj3NBnwj8wAPXNGtYy2SXrOBi7FT+ySwW7ATpfv6Xq9zGDUWJsENPUlFmvDiUs7Qi4scnNvSO1L+sDseB9/q1m3gMFVnTuieDO/" "T+KKkg0+uYgMM7YX5PahsAAJJ+EMb/r4afl3tcBMPR64VveKQ0hiSHA4zIYPsB9FB+b8S5C46uyY0r6WR7IzGjq2Gzb1do0kxvaKItTITWLSImcUu5ZZuXOUKJb441frVBWur5lXaYuedkxb1IRTTK0V/mBODE1D7k73MxGrqlzaMPdCqz+c3hRE18WVUkBTYjANVXDrs3yzBBVxaIAeu++vkO6BvQIDAQAB")
+
+;; Google Workspaces site aliases
+docs.tvl.su. 21600 IN CNAME ghs.googlehosted.com.
+groups.tvl.su. 21600 IN CNAME ghs.googlehosted.com.
+mail.tvl.su. 21600 IN CNAME ghs.googlehosted.com.
diff --git a/ops/gerrit-tvl/HttpModule.java b/ops/gerrit-tvl/HttpModule.java
new file mode 100644
index 0000000000..6d785c0817
--- /dev/null
+++ b/ops/gerrit-tvl/HttpModule.java
@@ -0,0 +1,14 @@
+package su.tvl.gerrit;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.inject.servlet.ServletModule;
+
+public final class HttpModule extends ServletModule {
+
+  @Override
+  protected void configureServlets() {
+    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new JavaScriptPlugin("tvl.js"));
+  }
+}
diff --git a/ops/gerrit-tvl/MANIFEST.MF b/ops/gerrit-tvl/MANIFEST.MF
new file mode 100644
index 0000000000..bfe4eedeb6
--- /dev/null
+++ b/ops/gerrit-tvl/MANIFEST.MF
@@ -0,0 +1,2 @@
+Gerrit-HttpModule: su.tvl.gerrit.HttpModule
+Gerrit-PluginName: tvl
diff --git a/ops/gerrit-tvl/README.md b/ops/gerrit-tvl/README.md
new file mode 100644
index 0000000000..1b88600f19
--- /dev/null
+++ b/ops/gerrit-tvl/README.md
@@ -0,0 +1,6 @@
+# gerrit-tvl
+
+A Gerrit plugin that does TVL-specific things.
+
+You probably want to take inspiration from this rather than using it directly,
+as it has a variety of TVL-ish assumptions baked into it.
diff --git a/ops/gerrit-tvl/default.nix b/ops/gerrit-tvl/default.nix
new file mode 100644
index 0000000000..f3bec7a3a2
--- /dev/null
+++ b/ops/gerrit-tvl/default.nix
@@ -0,0 +1,33 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  classPath = lib.concatStringsSep ":" [
+    "${depot.third_party.gerrit}/share/api/extension-api_deploy.jar"
+  ];
+in
+pkgs.stdenvNoCC.mkDerivation rec {
+  name = "${pname}-${version}.jar";
+  pname = "gerrit-tvl";
+  version = "0.0.1";
+
+  src = ./.;
+
+  nativeBuildInputs = with pkgs; [
+    jdk
+  ];
+
+  buildPhase = ''
+    mkdir $NIX_BUILD_TOP/build
+
+    # Build Java components.
+    export JAVAC="javac -cp ${classPath} -d $NIX_BUILD_TOP/build --release 11"
+    $JAVAC ./HttpModule.java
+
+    # Install static files.
+    cp -R static $NIX_BUILD_TOP/build/static
+  '';
+
+  installPhase = ''
+    jar --create --file $out --manifest $src/MANIFEST.MF -C $NIX_BUILD_TOP/build .
+  '';
+}
diff --git a/ops/gerrit-tvl/static/tvl.js b/ops/gerrit-tvl/static/tvl.js
new file mode 100644
index 0000000000..684636de30
--- /dev/null
+++ b/ops/gerrit-tvl/static/tvl.js
@@ -0,0 +1,189 @@
+// vim: set noai ts=2 sw=2 et: */
+
+// This is a read-only Buildkite token: it was generated by lukegb@, and has
+// read_builds, read_build_logs, and read_pipelines permissions.
+const BUILDKITE_TOKEN = 'a150658fb61062e432f13a032962d70fa9352088';
+
+function encodeParams(p) {
+  const pieces = [];
+  for (let k of Object.getOwnPropertyNames(p)) {
+    pieces.push(`${encodeURIComponent(k)}=${encodeURIComponent(p[k])}`);
+  }
+  return pieces.join('&');
+}
+
+function formatDuration(from, to) {
+  const millisecondsTook = Math.floor(to.valueOf() - from.valueOf());
+  if (millisecondsTook < 2000) return `${millisecondsTook} ms`;
+  const secondsTook = Math.floor(millisecondsTook / 1000);
+  if (secondsTook < 100) return `${secondsTook} seconds`;
+  const minutesTook = Math.floor(secondsTook / 60);
+  if (minutesTook < 60) return `${minutesTook} minutes`;
+  const hoursTook = Math.floor(minutesTook / 60);
+  const minutesRemainder = minutesTook - (hoursTook * 60);
+  return `${hoursTook}hr ${minutesRemainder}min`;
+}
+
+// Maps the status of a Buildkite *job* to the statuses available for
+// a Gerrit check.
+//
+// Note that jobs can have statuses that, according to the Buildkite
+// documentation, are only available for builds, and maybe vice-versa.
+// To deal with this we simply cover all statuses for all types here.
+//
+// Buildkite job statuses: https://buildkite.com/docs/pipelines/notifications#job-states
+//
+// Gerrit check statuses: https://gerrit.googlesource.com/gerrit/+/v3.4.0/polygerrit-ui/app/api/checks.ts#167
+//
+// TODO(tazjin): Use SCHEDULED status once we have upgraded Gerrit
+// past 3.4
+function jobStateToCheckRunStatus(state) {
+  const status = {
+    // Statuses documented for both types
+    'blocked': 'RUNNABLE',
+    'canceled': 'COMPLETED',
+    'canceling': 'RUNNING',
+    'running': 'RUNNING',
+    'scheduled': 'RUNNABLE',
+    'skipped': 'COMPLETED',
+
+    // Statuses only documented for builds
+    'creating': 'RUNNABLE',
+    'failed': 'COMPLETED',
+    'not_run': 'COMPLETED',
+    'passed': 'COMPLETED',
+
+    // Statuses only documented for jobs
+    'accepted': 'RUNNABLE',
+    'assigned': 'RUNNABLE',
+    'blocked_failed': 'COMPLETED',
+    'broken': 'COMPLETED',
+    'finished': 'COMPLETED',
+    'limited': 'RUNNABLE',
+    'limiting': 'RUNNABLE',
+    'pending': 'RUNNABLE',
+    'timed_out': 'COMPLETED',
+    'timing_out': 'RUNNING',
+    'unblocked': 'RUNNABLE',
+    'unblocked_failed': 'COMPLETED',
+    'waiting': 'RUNNABLE',
+    'waiting_failed': 'COMPLETED',
+  }[state];
+
+  if (!status) {
+    console.log(`unknown Buildkite job state: ${state}`);
+  }
+
+  return status;
+}
+
+const tvlChecksProvider = {
+  async fetch(change) {
+    let {patchsetSha, repo} = change;
+
+    const experiments = window.ENABLED_EXPERIMENTS || [];
+    if (experiments.includes("UiFeature__tvl_check_debug")) {
+      patchsetSha = '76692104f58b849b1503a8d8a700298003fa7b5f';
+      repo = 'depot';
+    }
+
+    if (repo !== 'depot') {
+      // We only handle TVL's depot at the moment.
+      return {responseCode: 'OK'};
+    }
+
+    const params = {
+      commit: patchsetSha,
+    };
+    const url = `https://api.buildkite.com/v2/organizations/tvl/pipelines/depot/builds?${encodeParams(params)}`;
+    const resp = await fetch(url, {
+      headers: {
+        Authorization: `Bearer ${BUILDKITE_TOKEN}`,
+      },
+    });
+    const respJSON = await resp.json();
+
+    const runs = [];
+    for (let i = 0; i < respJSON.length; i++) {
+      const attempt = respJSON.length - i;
+      const build = respJSON[i];
+
+      for (let job of build.jobs) {
+        // Skip non-command jobs (e.g. waiting/grouping jobs)
+        if (job.type !== 'script') {
+          continue;
+        }
+
+        // Skip jobs marked as 'broken' (this means they were skipped
+        // intentionally)
+        if (job.state === 'broken') {
+          continue;
+        }
+
+        // TODO(lukegb): add the ability to retry these
+        const checkRun = {
+          patchset: parseInt(build.env.GERRIT_PATCHSET, 10),
+          attempt: attempt,
+          externalId: job.id,
+          checkName: job.name,
+          checkDescription: job.command,
+          checkLink: job.web_url,
+          status: jobStateToCheckRunStatus(job.state),
+          labelName: 'Verified',
+        };
+
+        if (job.scheduled_at) {
+          checkRun.scheduledTimestamp = new Date(job.scheduled_at);
+        }
+
+        if (job.started_at) {
+          checkRun.startedTimestamp = new Date(job.started_at);
+        }
+
+        if (job.finished_at) {
+          checkRun.finishedTimestamp = new Date(job.finished_at);
+        }
+
+        let statusDescription = job.state;
+        if (checkRun.startedTimestamp && checkRun.finishedTimestamp) {
+          statusDescription = `${statusDescription} in ${formatDuration(checkRun.startedTimestamp, checkRun.finishedTimestamp)}`;
+        } else if (checkRun.startedTimestamp) {
+          statusDescription = `${statusDescription} for ${formatDuration(checkRun.startedTimestamp, new Date())}`;
+        } else if (checkRun.scheduledTimestamp) {
+          statusDescription = `${statusDescription} for ${formatDuration(checkRun.scheduledTimestamp, new Date())}`;
+        }
+        checkRun.statusDescription = statusDescription;
+
+        if (['failed', 'timed_out'].includes(job.state)) {
+          const result = {
+            // TODO(lukegb): get the log as the message here (the Gerrit
+            // implementation doesn't yet seem to support newlines in message
+            // strings...)
+            links: [{
+              url: job.web_url,
+              tooltip: "Buildkite",
+              primary: true,
+              icon: 'EXTERNAL',
+            }],
+            category: 'ERROR',
+            summary: `${job.command} failed`,
+          };
+          checkRun.results = [result];
+        }
+
+        runs.push(checkRun);
+      }
+    }
+
+    return {
+      responseCode: 'OK',
+      runs: runs,
+    };
+  },
+};
+
+Gerrit.install(plugin => {
+  console.log('TVL plugin initialising');
+
+  plugin.checks().register(tvlChecksProvider);
+});
diff --git a/ops/glesys/.gitignore b/ops/glesys/.gitignore
new file mode 100644
index 0000000000..de8e8f12ee
--- /dev/null
+++ b/ops/glesys/.gitignore
@@ -0,0 +1,3 @@
+.terraform*
+terraform.tfstate*
+.envrc
diff --git a/ops/glesys/README.md b/ops/glesys/README.md
new file mode 100644
index 0000000000..00f61a9360
--- /dev/null
+++ b/ops/glesys/README.md
@@ -0,0 +1,20 @@
+Terraform for GleSYS
+======================
+
+This contains the Terraform configuration for deploying TVL's
+infrastructure at [GleSYS](https://glesys.com). This includes object
+storage (e.g. for backups and Terraform state) and DNS.
+
+Secrets are needed for applying this. The encrypted file
+`//ops/secrets/tf-glesys.age` contains `export` calls which should be
+sourced, for example via `direnv`, by users with the appropriate
+credentials.
+
+An example `direnv` configuration used by tazjin is this:
+
+```
+# //ops/secrets/.envrc
+source_up
+eval $(age --decrypt -i ~/.ssh/id_ed25519 $(git rev-parse --show-toplevel)/ops/secrets/tf-glesys.age)
+watch_file $(git rev-parse --show-toplevel)/secrets/tf-glesys.age
+```
diff --git a/ops/glesys/default.nix b/ops/glesys/default.nix
new file mode 100644
index 0000000000..2dfb505fb4
--- /dev/null
+++ b/ops/glesys/default.nix
@@ -0,0 +1,8 @@
+{ depot, pkgs, ... }:
+
+depot.nix.readTree.drvTargets {
+  # Provide a Terraform wrapper with the right provider installed.
+  terraform = pkgs.terraform.withPlugins (_: [
+    depot.third_party.terraform-provider-glesys
+  ]);
+}
diff --git a/ops/glesys/dns-nixery-dev.tf b/ops/glesys/dns-nixery-dev.tf
new file mode 100644
index 0000000000..53a421d20e
--- /dev/null
+++ b/ops/glesys/dns-nixery-dev.tf
@@ -0,0 +1,44 @@
+# DNS configuration for nixery.dev
+#
+# TODO(tazjin): Figure out what to do with //ops/dns for this. I'd
+# like to keep zonefiles in case we move providers again, but maybe
+# generate something from them. Not sure yet.
+
+resource "glesys_dnsdomain" "nixery_dev" {
+  name = "nixery.dev"
+}
+
+resource "glesys_dnsdomain_record" "nixery_dev_apex_A" {
+  domain = glesys_dnsdomain.nixery_dev.id
+  host   = "@"
+  type   = "A"
+  data   = var.whitby_ipv4
+}
+
+resource "glesys_dnsdomain_record" "nixery_dev_apex_AAAA" {
+  domain = glesys_dnsdomain.nixery_dev.id
+  host   = "@"
+  type   = "AAAA"
+  data   = var.whitby_ipv6
+}
+
+resource "glesys_dnsdomain_record" "nixery_dev_NS1" {
+  domain = glesys_dnsdomain.nixery_dev.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns1.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "nixery_dev_NS2" {
+  domain = glesys_dnsdomain.nixery_dev.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns2.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "nixery_dev_NS3" {
+  domain = glesys_dnsdomain.nixery_dev.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns3.namesystem.se."
+}
diff --git a/ops/glesys/dns-tvl-fyi.tf b/ops/glesys/dns-tvl-fyi.tf
new file mode 100644
index 0000000000..803bfeae08
--- /dev/null
+++ b/ops/glesys/dns-tvl-fyi.tf
@@ -0,0 +1,99 @@
+# DNS configuration for tvl.fyi
+
+resource "glesys_dnsdomain" "tvl_fyi" {
+  name = "tvl.fyi"
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_NS1" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns1.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_NS2" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns2.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_NS3" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns3.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_apex_A" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "A"
+  data   = var.whitby_ipv4
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_apex_AAAA" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "AAAA"
+  data   = var.whitby_ipv6
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_whitby_A" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "whitby"
+  type   = "A"
+  data   = var.whitby_ipv4
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_whitby_AAAA" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "whitby"
+  type   = "AAAA"
+  data   = var.whitby_ipv6
+}
+
+# This record is responsible for hosting ~all TVL services. Be
+# mindful!
+resource "glesys_dnsdomain_record" "tvl_fyi_wildcard" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "*"
+  type   = "CNAME"
+  data   = "whitby.tvl.fyi."
+}
+
+# Google Domains mail forwarding configuration (no sending)
+resource "glesys_dnsdomain_record" "tvl_fyi_MX_5" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "MX"
+  data   = "5 gmr-smtp-in.l.google.com."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_MX_10" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "MX"
+  data   = "10 alt1.gmr-smtp-in.l.google.com."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_MX_20" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "MX"
+  data   = "20 alt2.gmr-smtp-in.l.google.com."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_MX_30" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "MX"
+  data   = "30 alt3.aspmx.l.google.com."
+}
+
+resource "glesys_dnsdomain_record" "tvl_fyi_MX_40" {
+  domain = glesys_dnsdomain.tvl_fyi.id
+  host   = "@"
+  type   = "MX"
+  data   = "40 alt4.gmr-smtp-in.l.google.com."
+}
diff --git a/ops/glesys/dns-tvl-su.tf b/ops/glesys/dns-tvl-su.tf
new file mode 100644
index 0000000000..39fd054e01
--- /dev/null
+++ b/ops/glesys/dns-tvl-su.tf
@@ -0,0 +1,122 @@
+# DNS configuration for tvl.su
+
+resource "glesys_dnsdomain" "tvl_su" {
+  name = "tvl.su"
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_NS1" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns1.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_NS2" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns2.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_NS3" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns3.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_apex_A" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "A"
+  data   = var.whitby_ipv4
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_apex_AAAA" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "AAAA"
+  data   = var.whitby_ipv6
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_whitby_A" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "whitby"
+  type   = "A"
+  data   = var.whitby_ipv4
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_whitby_AAAA" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "whitby"
+  type   = "AAAA"
+  data   = var.whitby_ipv6
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_sanduny_A" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "sanduny"
+  type   = "A"
+  data   = var.sanduny_ipv4
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_sanduny_AAAA" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "sanduny"
+  type   = "AAAA"
+  data   = var.sanduny_ipv6
+}
+
+# This record is responsible for hosting ~all TVL services. Be
+# mindful!
+resource "glesys_dnsdomain_record" "tvl_su_wildcard" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "*"
+  type   = "CNAME"
+  data   = "whitby.tvl.su."
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_TXT_google_site" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "TXT"
+  data   = "google-site-verification=3ksTBzFK3lZlzD3ddBfpaHs9qasfAiYBmvbW2T_ejH4"
+}
+
+# Yandex 360 setup
+
+resource "glesys_dnsdomain_record" "tvl_su_TXT_yandex" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "TXT"
+  data   = "yandex-verification: b99c43b7838949dc"
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_MX_yandex" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "MX"
+  data   = "10 mx.yandex.net."
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_TXT_yandex_spf" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "@"
+  type   = "TXT"
+  data   = "v=spf1 redirect=_spf.yandex.net"
+
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_TXT_yandex_dkim" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "mail._domainkey"
+  type   = "TXT"
+  data   = "v=DKIM1; k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDaRdWF8BtCHlTTQN8O+E5Qn27FVIpUEAdk1uq2vdIKh1Un/3NfdWtxStcS1Mf0iEprt1Fb4zgWOkDlPi+hH/UZqiC9QNeNqEBGMB9kgJyfsUt6cDCIVGvn8PT9JcZW1jxSziOj8nUWB4noqbaVcQNqNbwtaHPm3aifwKwScxVO7wIDAQAB"
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_CNAME_yandex_mail" {
+  domain = glesys_dnsdomain.tvl_su.id
+  host   = "mail"
+  type   = "CNAME"
+  data   = "domain.mail.yandex.net."
+}
diff --git a/ops/glesys/main.tf b/ops/glesys/main.tf
new file mode 100644
index 0000000000..9032d501a5
--- /dev/null
+++ b/ops/glesys/main.tf
@@ -0,0 +1,72 @@
+# Configure TVL resources hosted with GleSYS.
+#
+# Most importantly:
+#  - all of our DNS
+#  - object storage (e.g. backups)
+
+terraform {
+  required_providers {
+    glesys = {
+      source = "depot/glesys"
+    }
+  }
+
+  backend "s3" {
+    endpoint = "https://objects.dc-sto1.glesys.net"
+    bucket   = "tvl-state"
+    key      = "terraform/tvl-glesys"
+    region   = "glesys"
+
+    skip_credentials_validation = true
+    skip_region_validation      = true
+    skip_metadata_api_check     = true
+  }
+}
+
+provider "glesys" {
+  userid = "cl26117" # generated by GleSYS
+}
+
+resource "glesys_objectstorage_instance" "tvl-backups" {
+  description = "tvl-backups"
+  datacenter  = "dc-sto1"
+}
+
+resource "glesys_objectstorage_instance" "tvl-state" {
+  description = "tvl-state"
+  datacenter  = "dc-sto1"
+
+  lifecycle {
+    ignore_changes = [accesskey]
+  }
+}
+
+resource "glesys_objectstorage_credential" "terraform-state" {
+  instanceid  = glesys_objectstorage_instance.tvl-state.id
+  description = "key for terraform state"
+}
+
+resource "glesys_objectstorage_credential" "litestream" {
+  instanceid  = glesys_objectstorage_instance.tvl-state.id
+  description = "key for litestream"
+}
+
+variable "whitby_ipv4" {
+  type    = string
+  default = "49.12.129.211"
+}
+
+variable "whitby_ipv6" {
+  type    = string
+  default = "2a01:4f8:242:5b21:0:feed:edef:beef"
+}
+
+variable "sanduny_ipv4" {
+  type    = string
+  default = "85.119.82.231"
+}
+
+variable "sanduny_ipv6" {
+  type    = string
+  default = "2001:ba8:1f1:f109::feed:edef:beef"
+}
diff --git a/ops/journaldriver/.gitignore b/ops/journaldriver/.gitignore
new file mode 100644
index 0000000000..29e65519ba
--- /dev/null
+++ b/ops/journaldriver/.gitignore
@@ -0,0 +1,3 @@
+result
+/target
+**/*.rs.bk
diff --git a/ops/journaldriver/Cargo.lock b/ops/journaldriver/Cargo.lock
new file mode 100644
index 0000000000..0b7afd9932
--- /dev/null
+++ b/ops/journaldriver/Cargo.lock
@@ -0,0 +1,511 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr 2.4.1",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "crimp"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe8f9a320ad9c1a2e3bacedaa281587bd297fb10a10179fd39f777049d04794"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "cstr-argument"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514570a4b719329df37f93448a70df2baac553020d0eb43a8dfa9c1f5ba7b658"
+dependencies = [
+ "cfg-if 0.1.10",
+ "memchr 1.0.2",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.53+curl-7.82.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8092905a5a9502c312f223b2775f57ec5c5b715f9a15ee9d2a8591d1364a0352"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+dependencies = [
+ "quick-error",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "journaldriver"
+version = "1.1.0"
+dependencies = [
+ "anyhow",
+ "crimp",
+ "env_logger",
+ "lazy_static",
+ "log",
+ "medallion",
+ "pkg-config",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "systemd",
+ "time",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
+
+[[package]]
+name = "libsystemd-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7b98458cd04a5c3aacba6f1a3a3c4b9abcb0ae4d66a055eee502e0d52dc226b"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "log"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "medallion"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35b83c0c3277d722b53a6eb24e3c1321172f85b715cc7405add8ffd1f2f06288"
+dependencies = [
+ "anyhow",
+ "base64",
+ "openssl",
+ "serde",
+ "serde_json",
+ "time",
+]
+
+[[package]]
+name = "memchr"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "num_threads"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "openssl"
+version = "0.10.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr 2.4.1",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "systemd"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b62a732355787f960c25536210ae0a981aca2e5dae9dab8491bdae39613ce48"
+dependencies = [
+ "cstr-argument",
+ "libc",
+ "libsystemd-sys",
+ "log",
+ "utf8-cstr",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "time"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+ "serde",
+ "time-macros",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "utf8-cstr"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55bcbb425141152b10d5693095950b51c3745d019363fc2929ffd8f61449b628"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/ops/journaldriver/Cargo.toml b/ops/journaldriver/Cargo.toml
new file mode 100644
index 0000000000..4c32b893f7
--- /dev/null
+++ b/ops/journaldriver/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "journaldriver"
+version = "1.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+license = "GPL-3.0-or-later"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0"
+crimp = "0.2"
+env_logger = "0.5"
+lazy_static = "1.0"
+log = "0.4"
+medallion = "2.5"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
+systemd = "0.3"
+time = { version = "0.3", features = [ "serde-well-known", "macros" ]}
+
+[build-dependencies]
+pkg-config = "0.3"
diff --git a/ops/journaldriver/README.md b/ops/journaldriver/README.md
new file mode 100644
index 0000000000..4dc9de0f61
--- /dev/null
+++ b/ops/journaldriver/README.md
@@ -0,0 +1,152 @@
+journaldriver
+=============
+
+This is a small daemon used to forward logs from `journald` (systemd's
+logging service) to [Stackdriver Logging][].
+
+Many existing log services are written in inefficient dynamic
+languages with error-prone "cover every possible use-case"
+configuration. `journaldriver` instead aims to fit a specific use-case
+very well, instead of covering every possible logging setup.
+
+`journaldriver` can be run on GCP-instances with no additional
+configuration as authentication tokens are retrieved from the
+[metadata server][].
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Features](#features)
+- [Usage on Google Cloud Platform](#usage-on-google-cloud-platform)
+- [Usage outside of Google Cloud Platform](#usage-outside-of-google-cloud-platform)
+- [Log levels / severities / priorities](#log-levels--severities--priorities)
+- [NixOS module](#nixos-module)
+- [Stackdriver Error Reporting](#stackdriver-error-reporting)
+
+<!-- markdown-toc end -->
+
+# Features
+
+* `journaldriver` persists the last forwarded position in the journal
+  and will resume forwarding at the same position after a restart
+* `journaldriver` will recognise log entries in JSON format and
+  forward them appropriately to make structured log entries available
+  in Stackdriver
+* `journaldriver` can be used outside of GCP by configuring static
+  credentials
+* `journaldriver` will recognise journald's log priority levels and
+  convert them into equivalent Stackdriver log severity levels
+
+# Usage on Google Cloud Platform
+
+`journaldriver` does not require any configuration when running on GCP
+instances.
+
+1. Install `journaldriver` on the instance from which you wish to
+   forward logs.
+
+2. Ensure that the instance has the appropriate permissions to write
+   to Stackdriver. Google continously changes how IAM is implemented
+   on GCP, so you will have to refer to [Google's documentation][].
+
+   By default instances have the required permissions if Stackdriver
+   Logging support is enabled in the project.
+
+3. Start `journaldriver`, for example via `systemd`.
+
+# Usage outside of Google Cloud Platform
+
+When running outside of GCP, the following extra steps need to be
+performed:
+
+1. Create a Google Cloud Platform service account with the "Log
+   Writer" role and download its private key in JSON-format.
+2. When starting `journaldriver`, configure the following environment
+   variables:
+
+   * `GOOGLE_CLOUD_PROJECT`: Name of the GCP project to which logs
+     should be written.
+   * `GOOGLE_APPLICATION_CREDENTIALS`: Filesystem path to the
+     JSON-file containing the service account's private key.
+   * `LOG_STREAM`: Name of the target log stream in Stackdriver Logging.
+     This will be automatically created if it does not yet exist.
+   * `LOG_NAME`: Name of the target log to write to. This defaults to
+     `journaldriver` if unset, but it is recommended to - for
+     example - set it to the machine hostname.
+
+# Log levels / severities / priorities
+
+`journaldriver` recognises [journald's priorities][] and converts them
+into [equivalent severities][] in Stackdriver. Both sets of values
+correspond to standard `syslog` priorities.
+
+The easiest way to emit log messages with priorites from an
+application is to use [priority prefixes][], which are compatible with
+structured log messages.
+
+For example, to emit a simple warning message (structured and
+unstructured):
+
+```
+$ echo '<4>{"fnord":true, "msg":"structured log (warning)"}' | systemd-cat
+$ echo '<4>unstructured log (warning)' | systemd-cat
+```
+
+# NixOS module
+
+The NixOS package repository [contains a module][] for setting up
+`journaldriver` on NixOS machines. NixOS by default uses `systemd` for
+service management and `journald` for logging, which means that log
+output from most services will be captured automatically.
+
+On a GCP instance the only required option is this:
+
+```nix
+services.journaldriver.enable = true;
+```
+
+When running outside of GCP, the configuration looks as follows:
+
+```nix
+services.journaldriver = {
+  enable                 = true;
+  logStream              = "prod-environment";
+  logName                = "hostname";
+  googleCloudProject     = "gcp-project-name";
+  applicationCredentials = keyFile;
+};
+```
+
+**Note**: The `journaldriver`-module is included in stable releases of
+NixOS since NixOS 18.09.
+
+# Stackdriver Error Reporting
+
+The [Stackdriver Error Reporting][] service of Google's monitoring
+toolbox supports automatically detecting and correlating errors from
+log entries.
+
+To use this functionality log messages must be logged in the expected
+[log format][].
+
+*Note*: Reporting errors from non-GCP instances requires that the
+`LOG_STREAM` environment variable is set to the special value
+`global`.
+
+This value changes the monitored resource descriptor from a log stream
+to the project-global stream. Due to a limitation in Stackdriver Error
+Reporting, this is the only way to correctly ingest errors from
+non-GCP machines. Please see [issue #4][] for more information about
+this.
+
+[Stackdriver Logging]: https://cloud.google.com/logging/
+[metadata server]: https://cloud.google.com/compute/docs/storing-retrieving-metadata
+[Google's documentation]: https://cloud.google.com/logging/docs/access-control
+[NixOS]: https://nixos.org/
+[contains a module]: https://github.com/NixOS/nixpkgs/pull/42134
+[journald's priorities]: http://0pointer.de/public/systemd-man/sd-daemon.html
+[equivalent severities]: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
+[priority prefixes]: http://0pointer.de/public/systemd-man/sd-daemon.html
+[Stackdriver Error Reporting]: https://cloud.google.com/error-reporting/
+[log format]: https://cloud.google.com/error-reporting/docs/formatting-error-messages
+[issue #4]: https://github.com/tazjin/journaldriver/issues/4
diff --git a/ops/journaldriver/build.rs b/ops/journaldriver/build.rs
new file mode 100644
index 0000000000..79eb1001bf
--- /dev/null
+++ b/ops/journaldriver/build.rs
@@ -0,0 +1,5 @@
+extern crate pkg_config;
+
+fn main() {
+    pkg_config::probe_library("libsystemd").expect("Could not probe libsystemd");
+}
diff --git a/ops/journaldriver/default.nix b/ops/journaldriver/default.nix
new file mode 100644
index 0000000000..a06a858fa1
--- /dev/null
+++ b/ops/journaldriver/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+
+  buildInputs = with pkgs; [
+    pkgconfig
+    openssl
+    systemd.dev
+  ];
+}
diff --git a/ops/journaldriver/src/main.rs b/ops/journaldriver/src/main.rs
new file mode 100644
index 0000000000..4c404e607e
--- /dev/null
+++ b/ops/journaldriver/src/main.rs
@@ -0,0 +1,638 @@
+// Copyright (C) 2018 Vincent Ambo <mail@tazj.in>
+//
+// journaldriver is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+//! This file implements journaldriver, a small application that
+//! forwards logs from journald (systemd's log facility) to
+//! Stackdriver Logging.
+//!
+//! Log entries are read continously from journald and are forwarded
+//! to Stackdriver in batches.
+//!
+//! Stackdriver Logging has a concept of monitored resources. In the
+//! simplest case this monitored resource will be the GCE instance on
+//! which journaldriver is running.
+//!
+//! Information about the instance, the project and required security
+//! credentials are retrieved from Google's metadata instance on GCP.
+//!
+//! To run journaldriver on non-GCP machines, users must specify the
+//! `GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_CLOUD_PROJECT` and
+//! `LOG_NAME` environment variables.
+
+use anyhow::{bail, Context, Result};
+use lazy_static::lazy_static;
+use log::{debug, error, info, trace};
+use serde::{Deserialize, Serialize};
+use serde_json::{from_str, json, Value};
+use std::convert::TryInto;
+use std::fs::{self, rename, File};
+use std::io::{self, ErrorKind, Read, Write};
+use std::path::PathBuf;
+use std::time::{Duration, Instant};
+use std::{env, mem, process};
+use systemd::journal::{Journal, JournalFiles, JournalRecord, JournalSeek};
+
+#[cfg(test)]
+mod tests;
+
+const LOGGING_SERVICE: &str = "https://logging.googleapis.com/google.logging.v2.LoggingServiceV2";
+const ENTRIES_WRITE_URL: &str = "https://logging.googleapis.com/v2/entries:write";
+const METADATA_TOKEN_URL: &str =
+    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
+const METADATA_ID_URL: &str = "http://metadata.google.internal/computeMetadata/v1/instance/id";
+const METADATA_ZONE_URL: &str = "http://metadata.google.internal/computeMetadata/v1/instance/zone";
+const METADATA_PROJECT_URL: &str =
+    "http://metadata.google.internal/computeMetadata/v1/project/project-id";
+
+/// Representation of static service account credentials for GCP.
+#[derive(Debug, Deserialize)]
+struct Credentials {
+    /// PEM encoded private key
+    private_key: String,
+
+    /// `kid` of this private key
+    private_key_id: String,
+
+    /// "email" address of the service account
+    client_email: String,
+}
+
+lazy_static! {
+    /// ID of the GCP project to which to send logs.
+    static ref PROJECT_ID: String = get_project_id();
+
+    /// Name of the log to write to (this should only be manually
+    /// configured if not running on GCP):
+    static ref LOG_NAME: String = env::var("LOG_NAME")
+        .unwrap_or("journaldriver".into());
+
+    /// Service account credentials (if configured)
+    static ref SERVICE_ACCOUNT_CREDENTIALS: Option<Credentials> =
+        env::var("GOOGLE_APPLICATION_CREDENTIALS").ok()
+        .and_then(|path| File::open(path).ok())
+        .and_then(|file| serde_json::from_reader(file).ok());
+
+    /// Descriptor of the currently monitored instance. Refer to the
+    /// documentation of `determine_monitored_resource` for more
+    /// information.
+    static ref MONITORED_RESOURCE: Value = determine_monitored_resource();
+
+    /// Path to the directory in which journaldriver should persist
+    /// its cursor state.
+    static ref CURSOR_DIR: PathBuf = env::var("CURSOR_POSITION_DIR")
+        .unwrap_or("/var/lib/journaldriver".into())
+        .into();
+
+    /// Path to the cursor position file itself.
+    static ref CURSOR_FILE: PathBuf = {
+        let mut path = CURSOR_DIR.clone();
+        path.push("cursor.pos");
+        path
+    };
+
+    /// Path to the temporary file used for cursor position writes.
+    static ref CURSOR_TMP_FILE: PathBuf = {
+        let mut path = CURSOR_DIR.clone();
+        path.push("cursor.tmp");
+        path
+    };
+}
+
+/// Convenience helper for retrieving values from the metadata server.
+fn get_metadata(url: &str) -> Result<String> {
+    let response = crimp::Request::get(url)
+        .header("Metadata-Flavor", "Google")?
+        .timeout(std::time::Duration::from_secs(5))?
+        .send()?
+        .as_string()?;
+
+    if !response.is_success() {
+        bail!(
+            "Error response ({}) from metadata server: {}",
+            response.status,
+            response.body
+        );
+    }
+
+    Ok(response.body.trim().to_owned())
+}
+
+/// Convenience helper for determining the project ID.
+fn get_project_id() -> String {
+    env::var("GOOGLE_CLOUD_PROJECT")
+        .or_else(|_| get_metadata(METADATA_PROJECT_URL))
+        .expect("Could not determine project ID")
+}
+
+/// Determines the monitored resource descriptor used in Stackdriver
+/// logs. On GCP this will be set to the instance ID as returned by
+/// the metadata server.
+///
+/// On non-GCP machines the value is determined by using the
+/// `GOOGLE_CLOUD_PROJECT` and `LOG_STREAM` environment variables.
+///
+/// [issue #4]: https://github.com/tazjin/journaldriver/issues/4
+fn determine_monitored_resource() -> Value {
+    if let Ok(log) = env::var("LOG_STREAM") {
+        // The special value `global` is recognised as a log stream name that
+        // results in a `global`-type resource descriptor. This is useful in
+        // cases where Stackdriver Error Reporting is intended to be used on
+        // a non-GCE instance. See [issue #4][] for details.
+        if log == "global" {
+            return json!({
+                "type": "global",
+                "labels": {
+                    "project_id": PROJECT_ID.as_str(),
+                }
+            });
+        }
+
+        json!({
+            "type": "logging_log",
+            "labels": {
+                "project_id": PROJECT_ID.as_str(),
+                "name": log,
+            }
+        })
+    } else {
+        let instance_id = get_metadata(METADATA_ID_URL).expect("Could not determine instance ID");
+
+        let zone = get_metadata(METADATA_ZONE_URL).expect("Could not determine instance zone");
+
+        json!({
+            "type": "gce_instance",
+            "labels": {
+                "project_id": PROJECT_ID.as_str(),
+                "instance_id": instance_id,
+                "zone": zone,
+            }
+        })
+    }
+}
+
+/// Represents the response returned by the metadata server's token
+/// endpoint. The token is normally valid for an hour.
+#[derive(Deserialize)]
+struct TokenResponse {
+    expires_in: u64,
+    access_token: String,
+}
+
+/// Struct used to store a token together with a sensible
+/// representation of when it expires.
+struct Token {
+    token: String,
+    fetched_at: Instant,
+    expires: Duration,
+}
+
+impl Token {
+    /// Does this token need to be renewed?
+    fn is_expired(&self) -> bool {
+        self.fetched_at.elapsed() > self.expires
+    }
+}
+
+/// Retrieves a token from the GCP metadata service. Retrieving these
+/// tokens requires no additional authentication.
+fn get_metadata_token() -> Result<Token> {
+    let body = get_metadata(METADATA_TOKEN_URL)?;
+    let token: TokenResponse = from_str(&body)?;
+
+    debug!("Fetched new token from metadata service");
+
+    Ok(Token {
+        fetched_at: Instant::now(),
+        expires: Duration::from_secs(token.expires_in / 2),
+        token: token.access_token,
+    })
+}
+
+/// Signs a token using static client credentials configured for a
+/// service account. This service account must have been given the
+/// `Log Writer` role in Google Cloud IAM.
+///
+/// The process for creating and signing these tokens is described
+/// here:
+///
+/// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#jwt-auth
+fn sign_service_account_token(credentials: &Credentials) -> Result<Token> {
+    use medallion::{Algorithm, Header, Payload};
+
+    let iat = time::OffsetDateTime::now_utc();
+    let exp = iat + time::Duration::seconds(3600);
+
+    let header = Header {
+        alg: Algorithm::RS256,
+        headers: Some(json!({
+            "kid": credentials.private_key_id,
+        })),
+    };
+
+    let payload: Payload<()> = Payload {
+        iss: Some(credentials.client_email.clone()),
+        sub: Some(credentials.client_email.clone()),
+        aud: Some(LOGGING_SERVICE.to_string()),
+        iat: Some(iat.unix_timestamp().try_into().unwrap()),
+        exp: Some(exp.unix_timestamp().try_into().unwrap()),
+        ..Default::default()
+    };
+
+    let token = medallion::Token::new(header, payload)
+        .sign(credentials.private_key.as_bytes())
+        .context("Signing service account token failed")?;
+
+    debug!("Signed new service account token");
+
+    Ok(Token {
+        token,
+        fetched_at: Instant::now(),
+        expires: Duration::from_secs(3000),
+    })
+}
+
+/// Retrieve the authentication token either by using static client
+/// credentials, or by talking to the metadata server.
+///
+/// Which behaviour is used is controlled by the environment variable
+/// `GOOGLE_APPLICATION_CREDENTIALS`, which should be configured to
+/// point at a JSON private key file if service account authentication
+/// is to be used.
+fn get_token() -> Result<Token> {
+    if let Some(credentials) = SERVICE_ACCOUNT_CREDENTIALS.as_ref() {
+        sign_service_account_token(credentials)
+    } else {
+        get_metadata_token()
+    }
+}
+
+/// This structure represents the different types of payloads
+/// supported by journaldriver.
+///
+/// Currently log entries can either contain plain text messages or
+/// structured payloads in JSON-format.
+#[derive(Debug, Serialize, PartialEq)]
+#[serde(untagged)]
+enum Payload {
+    TextPayload {
+        #[serde(rename = "textPayload")]
+        text_payload: String,
+    },
+    JsonPayload {
+        #[serde(rename = "jsonPayload")]
+        json_payload: Value,
+    },
+}
+
+/// Attempt to parse a log message as JSON and return it as a
+/// structured payload. If parsing fails, return the entry in plain
+/// text format.
+fn message_to_payload(message: Option<String>) -> Payload {
+    match message {
+        None => Payload::TextPayload {
+            text_payload: "empty log entry".into(),
+        },
+        Some(text_payload) => {
+            // Attempt to deserialize the text payload as a generic
+            // JSON value.
+            if let Ok(json_payload) = serde_json::from_str::<Value>(&text_payload) {
+                // If JSON-parsing succeeded on the payload, check
+                // whether we parsed an object (Stackdriver does not
+                // expect other types of JSON payload) and return it
+                // in that case.
+                if json_payload.is_object() {
+                    return Payload::JsonPayload { json_payload };
+                }
+            }
+
+            Payload::TextPayload { text_payload }
+        }
+    }
+}
+
+/// Attempt to parse journald's microsecond timestamps into a UTC
+/// timestamp.
+///
+/// Parse errors are dismissed and returned as empty options: There
+/// simply aren't any useful fallback mechanisms other than defaulting
+/// to ingestion time for journaldriver's use-case.
+fn parse_microseconds(input: String) -> Option<time::OffsetDateTime> {
+    if input.len() != 16 {
+        return None;
+    }
+
+    let micros: i128 = input.parse().ok()?;
+    let nanos: i128 = micros * 1000;
+
+    time::OffsetDateTime::from_unix_timestamp_nanos(nanos).ok()
+}
+
+/// Converts a journald log message priority to a
+/// Stackdriver-compatible severity number.
+///
+/// Both Stackdriver and journald specify equivalent
+/// severities/priorities. Conveniently, the names are the same.
+/// Inconveniently, the numbers are not.
+///
+/// For more information on the journald priorities, consult these
+/// man-pages:
+///
+/// * systemd.journal-fields(7) (section 'PRIORITY')
+/// * sd-daemon(3)
+/// * systemd.exec(5) (section 'SyslogLevelPrefix')
+///
+/// Note that priorities can be logged by applications via the prefix
+/// concept described in these man pages, without interfering with
+/// structured JSON-payloads.
+///
+/// For more information on the Stackdriver severity levels, please
+/// consult Google's documentation:
+///
+/// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
+///
+/// Any unknown priority values result in no severity being set.
+fn priority_to_severity(priority: String) -> Option<u32> {
+    match priority.as_ref() {
+        "0" => Some(800), // emerg
+        "1" => Some(700), // alert
+        "2" => Some(600), // crit
+        "3" => Some(500), // err
+        "4" => Some(400), // warning
+        "5" => Some(300), // notice
+        "6" => Some(200), // info
+        "7" => Some(100), // debug
+        _ => None,
+    }
+}
+
+/// This structure represents a log entry in the format expected by
+/// the Stackdriver API.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct LogEntry {
+    labels: Value,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(with = "time::serde::rfc3339::option")]
+    timestamp: Option<time::OffsetDateTime>,
+
+    #[serde(flatten)]
+    payload: Payload,
+
+    // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
+    #[serde(skip_serializing_if = "Option::is_none")]
+    severity: Option<u32>,
+}
+
+impl From<JournalRecord> for LogEntry {
+    // Converts from the fields contained in a journald record to the
+    // representation required by Stackdriver Logging.
+    //
+    // The fields are documented in systemd.journal-fields(7).
+    fn from(mut record: JournalRecord) -> LogEntry {
+        // The message field is technically just a convention, but
+        // journald seems to default to it when ingesting unit
+        // output.
+        let payload = message_to_payload(record.remove("MESSAGE"));
+
+        // Presumably this is always set, but who can be sure
+        // about anything in this world.
+        let hostname = record.remove("_HOSTNAME");
+
+        // The unit is seemingly missing on kernel entries, but
+        // present on all others.
+        let unit = record.remove("_SYSTEMD_UNIT");
+
+        // The source timestamp (if present) is specified in
+        // microseconds since epoch.
+        //
+        // If it is not present or can not be parsed, journaldriver
+        // will not send a timestamp for the log entry and it will
+        // default to the ingestion time.
+        let timestamp = record
+            .remove("_SOURCE_REALTIME_TIMESTAMP")
+            .and_then(parse_microseconds);
+
+        // Journald uses syslogd's concept of priority. No idea if this is
+        // always present, but it's optional in the Stackdriver API, so we just
+        // omit it if we can't find or parse it.
+        let severity = record.remove("PRIORITY").and_then(priority_to_severity);
+
+        LogEntry {
+            payload,
+            timestamp,
+            labels: json!({
+                "host": hostname,
+                "unit": unit.unwrap_or_else(|| "syslog".into()),
+            }),
+            severity,
+        }
+    }
+}
+
+/// Attempt to read from the journal. If no new entry is present,
+/// await the next one up to the specified timeout.
+fn receive_next_record(timeout: Duration, journal: &mut Journal) -> Result<Option<JournalRecord>> {
+    let next_record = journal.next_record()?;
+    if next_record.is_some() {
+        return Ok(next_record);
+    }
+
+    Ok(journal.await_next_record(Some(timeout))?)
+}
+
+/// This function starts a double-looped, blocking receiver. It will
+/// buffer messages for half a second before flushing them to
+/// Stackdriver.
+fn receiver_loop(mut journal: Journal) -> Result<()> {
+    let mut token = get_token()?;
+
+    let mut buf: Vec<LogEntry> = Vec::new();
+    let iteration = Duration::from_millis(500);
+
+    loop {
+        trace!("Beginning outer iteration");
+        let now = Instant::now();
+
+        loop {
+            if now.elapsed() > iteration {
+                break;
+            }
+
+            if let Ok(Some(entry)) = receive_next_record(iteration, &mut journal) {
+                trace!("Received a new entry");
+                buf.push(entry.into());
+            }
+        }
+
+        if !buf.is_empty() {
+            let to_flush = mem::replace(&mut buf, Vec::new());
+            flush(&mut token, to_flush, journal.cursor()?)?;
+        }
+
+        trace!("Done outer iteration");
+    }
+}
+
+/// Writes the current cursor into `/var/journaldriver/cursor.pos`. To
+/// avoid issues with journaldriver being terminated while the cursor
+/// is still being written, this will first write the cursor into a
+/// temporary file and then move it.
+fn persist_cursor(cursor: String) -> Result<()> {
+    // This code exists to aid in tracking down if there are other
+    // causes of issue #2 than what has already been taken care of.
+    //
+    // One theory is that journald (or the Rust library to interface
+    // with it) may occasionally return empty cursor strings. If this
+    // is ever the case, we would like to know about it.
+    if cursor.is_empty() {
+        error!("Received empty journald cursor position, refusing to persist!");
+        error!("Please report this message at https://github.com/tazjin/journaldriver/issues/2");
+        return Ok(());
+    }
+
+    let mut file = File::create(&*CURSOR_TMP_FILE).context("Failed to create cursor file")?;
+
+    write!(file, "{}", cursor).context("Failed to write cursor file")?;
+
+    rename(&*CURSOR_TMP_FILE, &*CURSOR_FILE)
+        .context("Failed to move cursor file")
+        .map_err(Into::into)
+}
+
+/// Flushes all drained records to Stackdriver. Any Stackdriver
+/// message can at most contain 1000 log entries which means they are
+/// chunked up here.
+///
+/// In some cases large payloads seem to cause errors in Stackdriver -
+/// the chunks are therefore made smaller here.
+///
+/// If flushing is successful the last cursor position will be
+/// persisted to disk.
+fn flush(token: &mut Token, entries: Vec<LogEntry>, cursor: String) -> Result<()> {
+    if token.is_expired() {
+        debug!("Refreshing Google metadata access token");
+        let new_token = get_token()?;
+        *token = new_token;
+    }
+
+    for chunk in entries.chunks(750) {
+        let request = prepare_request(chunk);
+        if let Err(write_error) = write_entries(token, request) {
+            error!("Failed to write {} entries: {}", chunk.len(), write_error)
+        } else {
+            debug!("Wrote {} entries to Stackdriver", chunk.len())
+        }
+    }
+
+    persist_cursor(cursor)
+}
+
+/// Convert a slice of log entries into the format expected by
+/// Stackdriver. This format is documented here:
+///
+/// https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write
+fn prepare_request(entries: &[LogEntry]) -> Value {
+    json!({
+        "logName": format!("projects/{}/logs/{}", PROJECT_ID.as_str(), LOG_NAME.as_str()),
+        "resource": &*MONITORED_RESOURCE,
+        "entries": entries,
+        "partialSuccess": true
+    })
+}
+
+/// Perform the log entry insertion in Stackdriver Logging.
+fn write_entries(token: &Token, request: Value) -> Result<()> {
+    let response = crimp::Request::post(ENTRIES_WRITE_URL)
+        .json(&request)?
+        .header("Authorization", format!("Bearer {}", token.token).as_str())?
+        // The timeout values are set relatively high, not because of
+        // an expectation of Stackdriver being slow but just to
+        // eventually force an error in case of network troubles.
+        // Presumably no request in a functioning environment will
+        // ever hit these limits.
+        .timeout(std::time::Duration::from_secs(5))?
+        .send()?;
+
+    if !response.is_success() {
+        let status = response.status;
+        let body = response
+            .as_string()
+            .map(|r| r.body)
+            .unwrap_or_else(|_| "no valid response body".to_owned());
+
+        bail!("Writing to Stackdriver failed({}): {}", status, body);
+    }
+
+    Ok(())
+}
+
+/// Attempt to read the initial cursor position from the configured
+/// file. If there is no initial cursor position set, read from the
+/// tail of the log.
+///
+/// The only "acceptable" error when reading the cursor position is
+/// the cursor position file not existing, other errors are fatal
+/// because they indicate a misconfiguration of journaldriver.
+fn initial_cursor() -> Result<JournalSeek> {
+    let read_result: io::Result<String> = (|| {
+        let mut contents = String::new();
+        let mut file = File::open(&*CURSOR_FILE)?;
+        file.read_to_string(&mut contents)?;
+        Ok(contents.trim().into())
+    })();
+
+    match read_result {
+        Ok(cursor) => Ok(JournalSeek::Cursor { cursor }),
+        Err(ref err) if err.kind() == ErrorKind::NotFound => {
+            info!("No previous cursor position, reading from journal tail");
+            Ok(JournalSeek::Tail)
+        }
+        Err(err) => (Err(err).context("Could not read cursor position"))?,
+    }
+}
+
+fn main() {
+    env_logger::init();
+
+    // The directory in which cursor positions are persisted should
+    // have been created:
+    if !CURSOR_DIR.exists() {
+        error!("Cursor directory at '{:?}' does not exist", *CURSOR_DIR);
+        process::exit(1);
+    }
+
+    let cursor_position_dir = CURSOR_FILE
+        .parent()
+        .expect("Invalid cursor position file path");
+
+    fs::create_dir_all(cursor_position_dir)
+        .expect("Could not create directory to store cursor position in");
+
+    let mut journal =
+        Journal::open(JournalFiles::All, false, true).expect("Failed to open systemd journal");
+
+    let seek_position = initial_cursor().expect("Failed to determine initial cursor position");
+
+    match journal.seek(seek_position) {
+        Ok(cursor) => info!("Opened journal at cursor '{}'", cursor),
+        Err(err) => {
+            error!("Failed to set initial journal position: {}", err);
+            process::exit(1)
+        }
+    }
+
+    receiver_loop(journal).expect("log receiver encountered an unexpected error");
+}
diff --git a/ops/journaldriver/src/tests.rs b/ops/journaldriver/src/tests.rs
new file mode 100644
index 0000000000..6f5045d6a5
--- /dev/null
+++ b/ops/journaldriver/src/tests.rs
@@ -0,0 +1,131 @@
+use super::*;
+use serde_json::to_string;
+use time::macros::datetime;
+
+#[test]
+fn test_text_entry_serialization() {
+    let entry = LogEntry {
+        labels: Value::Null,
+        timestamp: None,
+        payload: Payload::TextPayload {
+            text_payload: "test entry".into(),
+        },
+        severity: None,
+    };
+
+    let expected = "{\"labels\":null,\"textPayload\":\"test entry\"}";
+    let result = to_string(&entry).expect("serialization failed");
+
+    assert_eq!(
+        expected, result,
+        "Plain text payload should serialize correctly"
+    )
+}
+
+#[test]
+fn test_timestamped_entry_serialization() {
+    let entry = LogEntry {
+        labels: Value::Null,
+        timestamp: Some(datetime!(1952-10-07 12:00:00 UTC)),
+        payload: Payload::TextPayload {
+            text_payload: "test entry".into(),
+        },
+        severity: None,
+    };
+
+    let expected =
+        "{\"labels\":null,\"timestamp\":\"1952-10-07T12:00:00Z\",\"textPayload\":\"test entry\"}";
+    let result = to_string(&entry).expect("serialization failed");
+
+    assert_eq!(
+        expected, result,
+        "Plain text payload should serialize correctly"
+    )
+}
+
+#[test]
+fn test_json_entry_serialization() {
+    let entry = LogEntry {
+        labels: Value::Null,
+        timestamp: None,
+        payload: Payload::JsonPayload {
+            json_payload: json!({
+                "message": "JSON test"
+            }),
+        },
+        severity: None,
+    };
+
+    let expected = "{\"labels\":null,\"jsonPayload\":{\"message\":\"JSON test\"}}";
+    let result = to_string(&entry).expect("serialization failed");
+
+    assert_eq!(expected, result, "JSON payload should serialize correctly")
+}
+
+#[test]
+fn test_plain_text_payload() {
+    let message = "plain text payload".into();
+    let payload = message_to_payload(Some(message));
+    let expected = Payload::TextPayload {
+        text_payload: "plain text payload".into(),
+    };
+
+    assert_eq!(
+        expected, payload,
+        "Plain text payload should be detected correctly"
+    );
+}
+
+#[test]
+fn test_empty_payload() {
+    let payload = message_to_payload(None);
+    let expected = Payload::TextPayload {
+        text_payload: "empty log entry".into(),
+    };
+
+    assert_eq!(
+        expected, payload,
+        "Empty payload should be handled correctly"
+    );
+}
+
+#[test]
+fn test_json_payload() {
+    let message = "{\"someKey\":\"someValue\", \"otherKey\": 42}".into();
+    let payload = message_to_payload(Some(message));
+    let expected = Payload::JsonPayload {
+        json_payload: json!({
+            "someKey": "someValue",
+            "otherKey": 42
+        }),
+    };
+
+    assert_eq!(
+        expected, payload,
+        "JSON payload should be detected correctly"
+    );
+}
+
+#[test]
+fn test_json_no_object() {
+    // This message can be parsed as valid JSON, but it is not an
+    // object - it should be returned as a plain-text payload.
+    let message = "42".into();
+    let payload = message_to_payload(Some(message));
+    let expected = Payload::TextPayload {
+        text_payload: "42".into(),
+    };
+
+    assert_eq!(
+        expected, payload,
+        "Non-object JSON payload should be plain text"
+    );
+}
+
+#[test]
+fn test_parse_microseconds() {
+    let input: String = "1529175149291187".into();
+    let expected: time::OffsetDateTime = datetime!(2018-06-16 18:52:29.291187 UTC);
+
+    assert_eq!(Some(expected), parse_microseconds(input));
+}
diff --git a/ops/keycloak/.gitignore b/ops/keycloak/.gitignore
new file mode 100644
index 0000000000..017878c614
--- /dev/null
+++ b/ops/keycloak/.gitignore
@@ -0,0 +1,3 @@
+.terraform*
+*.tfstate*
+.envrc
diff --git a/ops/keycloak/README.md b/ops/keycloak/README.md
new file mode 100644
index 0000000000..e8ffd700b5
--- /dev/null
+++ b/ops/keycloak/README.md
@@ -0,0 +1,18 @@
+Terraform for Keycloak
+======================
+
+This contains the Terraform configuration for deploying TVL's Keycloak
+instance (which lives at `auth.tvl.fyi`).
+
+Secrets are needed for applying this. The encrypted file
+`//ops/secrets/tf-keycloak.age` contains `export` calls which should
+be sourced, for example via `direnv`, by users with the appropriate
+credentials.
+
+An example `direnv` configuration used by tazjin is this:
+
+```
+# //ops/secrets/.envrc
+source_up
+eval $(age --decrypt -i ~/.ssh/id_ed25519 $(git rev-parse --show-toplevel)/ops/secrets/tf-keycloak.age)
+```
diff --git a/ops/keycloak/clients.tf b/ops/keycloak/clients.tf
new file mode 100644
index 0000000000..9506bd4aa0
--- /dev/null
+++ b/ops/keycloak/clients.tf
@@ -0,0 +1,106 @@
+# All Keycloak clients, that is applications which authenticate
+# through Keycloak.
+#
+# Includes first-party (i.e. TVL-hosted) and third-party clients.
+
+resource "keycloak_openid_client" "grafana" {
+  realm_id              = keycloak_realm.tvl.id
+  client_id             = "grafana"
+  name                  = "Grafana"
+  enabled               = true
+  access_type           = "CONFIDENTIAL"
+  standard_flow_enabled = true
+  base_url              = "https://status.tvl.su"
+
+  valid_redirect_uris = [
+    "https://status.tvl.su/*",
+  ]
+}
+
+resource "keycloak_openid_client" "gerrit" {
+  realm_id                                 = keycloak_realm.tvl.id
+  client_id                                = "gerrit"
+  name                                     = "TVL Gerrit"
+  enabled                                  = true
+  access_type                              = "CONFIDENTIAL"
+  standard_flow_enabled                    = true
+  base_url                                 = "https://cl.tvl.fyi"
+  description                              = "TVL's code review tool"
+  direct_access_grants_enabled             = true
+  exclude_session_state_from_auth_response = false
+
+  valid_redirect_uris = [
+    "https://cl.tvl.fyi/*",
+  ]
+
+  web_origins = [
+    "https://cl.tvl.fyi",
+  ]
+}
+
+resource "keycloak_saml_client" "buildkite" {
+  realm_id  = keycloak_realm.tvl.id
+  client_id = "https://buildkite.com"
+  name      = "Buildkite"
+  base_url  = "https://buildkite.com/sso/tvl"
+
+  client_signature_required   = false
+  assertion_consumer_post_url = "https://buildkite.com/sso/~/1531aca5-f49c-4151-8832-a451e758af4c/saml/consume"
+
+  valid_redirect_uris = [
+    "https://buildkite.com/sso/~/1531aca5-f49c-4151-8832-a451e758af4c/saml/consume"
+  ]
+}
+
+resource "keycloak_saml_user_attribute_protocol_mapper" "buildkite_email" {
+  realm_id                   = keycloak_realm.tvl.id
+  client_id                  = keycloak_saml_client.buildkite.id
+  name                       = "buildkite-email-mapper"
+  user_attribute             = "email"
+  saml_attribute_name        = "email"
+  saml_attribute_name_format = "Unspecified"
+}
+
+resource "keycloak_saml_user_attribute_protocol_mapper" "buildkite_name" {
+  realm_id                   = keycloak_realm.tvl.id
+  client_id                  = keycloak_saml_client.buildkite.id
+  name                       = "buildkite-name-mapper"
+  user_attribute             = "displayName"
+  saml_attribute_name        = "name"
+  saml_attribute_name_format = "Unspecified"
+}
+
+resource "keycloak_openid_client" "oauth2_proxy" {
+  realm_id              = keycloak_realm.tvl.id
+  client_id             = "oauth2-proxy"
+  name                  = "TVL OAuth2 Proxy"
+  enabled               = true
+  access_type           = "CONFIDENTIAL"
+  standard_flow_enabled = true
+
+  valid_redirect_uris = [
+    "https://login.tvl.fyi/oauth2/callback",
+    "http://localhost:4774/oauth2/callback",
+  ]
+}
+
+resource "keycloak_openid_audience_protocol_mapper" "oauth2_proxy_audience" {
+  realm_id                 = keycloak_realm.tvl.id
+  client_id                = keycloak_openid_client.oauth2_proxy.id
+  name                     = "oauth2-proxy-audience"
+  included_custom_audience = keycloak_openid_client.oauth2_proxy.client_id
+}
+
+resource "keycloak_openid_client" "panettone" {
+  realm_id              = keycloak_realm.tvl.id
+  client_id             = "panettone"
+  name                  = "Panettone"
+  enabled               = true
+  access_type           = "CONFIDENTIAL"
+  standard_flow_enabled = true
+
+  valid_redirect_uris = [
+    "https://b.tvl.fyi/auth",
+    "http://localhost:6161/auth",
+  ]
+}
diff --git a/ops/keycloak/default.nix b/ops/keycloak/default.nix
new file mode 100644
index 0000000000..5757debd1a
--- /dev/null
+++ b/ops/keycloak/default.nix
@@ -0,0 +1,8 @@
+{ depot, pkgs, ... }:
+
+depot.nix.readTree.drvTargets {
+  # Provide a Terraform wrapper with the right provider installed.
+  terraform = pkgs.terraform.withPlugins (p: [
+    p.keycloak
+  ]);
+}
diff --git a/ops/keycloak/main.tf b/ops/keycloak/main.tf
new file mode 100644
index 0000000000..819267ff96
--- /dev/null
+++ b/ops/keycloak/main.tf
@@ -0,0 +1,34 @@
+# Configure TVL Keycloak instance.
+#
+# TODO(tazjin): Configure GitHub/GitLab IDP
+
+terraform {
+  required_providers {
+    keycloak = {
+      source = "mrparkers/keycloak"
+    }
+  }
+
+  backend "s3" {
+    endpoint = "https://objects.dc-sto1.glesys.net"
+    bucket   = "tvl-state"
+    key      = "terraform/tvl-keycloak"
+    region   = "glesys"
+
+    skip_credentials_validation = true
+    skip_region_validation      = true
+    skip_metadata_api_check     = true
+  }
+}
+
+provider "keycloak" {
+  client_id = "terraform"
+  url       = "https://auth.tvl.fyi"
+}
+
+resource "keycloak_realm" "tvl" {
+  realm                       = "TVL"
+  enabled                     = true
+  display_name                = "The Virus Lounge"
+  default_signature_algorithm = "RS256"
+}
diff --git a/ops/keycloak/user_sources.tf b/ops/keycloak/user_sources.tf
new file mode 100644
index 0000000000..3fde6e07cc
--- /dev/null
+++ b/ops/keycloak/user_sources.tf
@@ -0,0 +1,21 @@
+# All user sources, that is services from which Keycloak gets user
+# information (either by accessing a system like LDAP or integration
+# through protocols like OIDC).
+
+resource "keycloak_ldap_user_federation" "tvl_ldap" {
+  name                    = "tvl-ldap"
+  realm_id                = keycloak_realm.tvl.id
+  enabled                 = true
+  connection_url          = "ldap://localhost"
+  users_dn                = "ou=users,dc=tvl,dc=fyi"
+  username_ldap_attribute = "cn"
+  uuid_ldap_attribute     = "cn"
+  rdn_ldap_attribute      = "cn"
+  full_sync_period        = 86400
+  trust_email             = true
+
+  user_object_classes = [
+    "inetOrgPerson",
+    "organizationalPerson",
+  ]
+}
diff --git a/ops/kontemplate/.gitignore b/ops/kontemplate/.gitignore
new file mode 100644
index 0000000000..53a04aab3a
--- /dev/null
+++ b/ops/kontemplate/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+release/
diff --git a/ops/kontemplate/LICENSE b/ops/kontemplate/LICENSE
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/ops/kontemplate/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/ops/kontemplate/README.md b/ops/kontemplate/README.md
new file mode 100644
index 0000000000..803a1c4f16
--- /dev/null
+++ b/ops/kontemplate/README.md
@@ -0,0 +1,185 @@
+Kontemplate - A simple Kubernetes templater
+===========================================
+
+Kontemplate is a simple CLI tool that can take sets of Kubernetes resource files
+with placeholders and insert values per environment.
+
+This tool was made because in many cases all I want in terms of Kubernetes
+configuration is simple value interpolation per environment (i.e. Kubernetes
+cluster), but with the same deployment files.
+
+In my experience this is often enough and more complex solutions such as
+[Helm][] are not required.
+
+Check out a Kontemplate setup example and the feature list below!
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Kontemplate - A simple Kubernetes templater](#kontemplate---a-simple-kubernetes-templater)
+    - [Features](#features)
+    - [Example](#example)
+    - [Installation](#installation)
+        - [Homebrew](#homebrew)
+        - [Arch Linux](#arch-linux)
+        - [Building repeatably from source](#building-repeatably-from-source)
+        - [Building from source](#building-from-source)
+    - [Usage](#usage)
+    - [Contributing](#contributing)
+
+<!-- markdown-toc end -->
+
+## Features
+
+* [Simple, yet powerful templates](docs/templates.md)
+* [Clean cluster configuration files](docs/cluster-config.md)
+* [Resources organised as simple resource sets](docs/resource-sets.md)
+* Integration with pass
+* Integration with kubectl
+
+## Example
+
+Kontemplate lets you describe resources as you normally would in a simple folder structure:
+
+```
+.
+├── prod-cluster.yaml
+└── some-api
+    ├── deployment.yaml
+    └── service.yaml
+```
+
+This example has all resources belonging to `some-api` (no file naming conventions enforced at all!) in the `some-api`
+folder and the configuration for the cluster `prod-cluster` in the corresponding file.
+
+Lets take a short look at `prod-cluster.yaml`:
+
+```yaml
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
+```
+
+Those values are then templated into the resource files of `some-api`. That's it!
+
+You can also set up more complicated folder structures for organisation, for example:
+
+```
+.
+├── api
+│   ├── image-api
+│   │   └── deployment.yaml
+│   └── music-api
+│       └── deployment.yaml
+│   │   └── default.json
+├── frontend
+│   ├── main-app
+│   │   ├── deployment.yaml
+│   │   └── service.yaml
+│   └── user-page
+│       ├── deployment.yaml
+│       └── service.yaml
+├── prod-cluster.yaml
+└── test-cluster.yaml
+```
+
+And selectively template or apply resources with a command such as
+`kontemplate apply test-cluster.yaml --include api --include frontend/user-page`
+to only update the `api` resource sets and the `frontend/user-page` resource set.
+
+## Installation
+
+It is recommended to install Kontemplate from the [Nix](https://nixos.org/) package set,
+where it is available since NixOS 17.09 as `kontemplate`.
+
+If using Nix is not an option for you, several other methods of installation are
+available:
+
+### Binary releases
+
+Signed binary releases are available on the [releases page][] for Linux, OS X, FreeBSD and
+Windows.
+
+Releases are signed with the GPG key `DCF34CFAC1AC44B87E26333136EE34814F6D294A`.
+
+### Building from source
+
+You can clone Kontemplate either by cloning the full TVL
+[depot][https://code.tvl.fyi] or by just cloning the kontemplate
+subtree like so:
+
+    git clone https://code.tvl.fyi/depot.git:/ops/kontemplate.git
+
+The `go` tooling can be used as normal with this cloned repository. In
+a full clone of the depot, Nix can be used to build Kontemplate:
+
+    nix-build -A ops.kontemplate
+
+## Usage
+
+You must have `kubectl` installed to use Kontemplate effectively.
+
+```
+usage: kontemplate [<flags>] <command> [<args> ...]
+
+simple Kubernetes resource templating
+
+Flags:
+  -h, --help                 Show context-sensitive help (also try --help-long and --help-man).
+  -i, --include=INCLUDE ...  Resource sets to include explicitly
+  -e, --exclude=EXCLUDE ...  Resource sets to exclude explicitly
+
+Commands:
+  help [<command>...]
+    Show help.
+
+  template <file>
+    Template resource sets and print them
+
+  apply [<flags>] <file>
+    Template resources and pass to 'kubectl apply'
+
+  replace <file>
+    Template resources and pass to 'kubectl replace'
+
+  delete <file>
+    Template resources and pass to 'kubectl delete'
+
+  create <file>
+    Template resources and pass to 'kubectl create'
+
+```
+
+Examples:
+
+```
+# Look at output for a specific resource set and check to see if it's correct ...
+kontemplate template example/prod-cluster.yaml -i some-api
+
+# ... maybe do a dry-run to see what kubectl would do:
+kontemplate apply example/prod-cluster.yaml --dry-run
+
+# And actually apply it if you like what you see:
+kontemplate apply example/prod-cluster.yaml
+```
+
+Check out the feature list and the individual feature documentation above. Then you should be good to go!
+
+## Contributing
+
+Feel free to contribute pull requests, file bugs and open issues with feature suggestions!
+
+Kontemplate is licensed under the GPLv3, a copy of the license and its terms can be found
+in the `LICENSE` file.
+
+Please follow the [code of conduct](CODE_OF_CONDUCT.md).
+
+[Helm]: https://helm.sh/
+[releases page]: https://github.com/tazjin/kontemplate/releases
diff --git a/ops/kontemplate/build-release.sh b/ops/kontemplate/build-release.sh
new file mode 100755
index 0000000000..e4258c53dd
--- /dev/null
+++ b/ops/kontemplate/build-release.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -ueo pipefail
+
+# Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+#
+# This file is part of Kontemplate.
+#
+# Kontemplate is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+readonly GIT_HASH="$(git rev-parse --short HEAD)"
+readonly LDFLAGS="-X main.gitHash=${GIT_HASH} -w -s"
+readonly VERSION="1.8.0-${GIT_HASH}"
+
+function binary-name() {
+    local os="${1}"
+    local target="${2}"
+    if [ "${os}" = "windows" ]; then
+        echo -n "${target}/kontemplate.exe"
+    else
+        echo -n "${target}/kontemplate"
+    fi
+}
+
+function build-for() {
+    local os="${1}"
+    local arch="${2}"
+    local target="release/${os}/${arch}"
+    local bin=$(binary-name "${os}" "${target}")
+
+    echo "Building kontemplate for ${os}-${arch} in ${target}"
+
+    mkdir -p "${target}"
+
+    env GOOS="${os}" GOARCH="${arch}" go build \
+        -ldflags "${LDFLAGS}" \
+        -o "${bin}" \
+        -tags netgo
+}
+
+function sign-for() {
+    local os="${1}"
+    local arch="${2}"
+    local target="release/${os}/${arch}"
+    local bin=$(binary-name "${os}" "${target}")
+    local tar="release/kontemplate-${VERSION}-${os}-${arch}.tar.gz"
+
+    echo "Packing release into ${tar}"
+    tar czvf "${tar}" -C "${target}" $(basename "${bin}")
+
+    local hash=$(sha256sum "${tar}")
+    echo "Signing kontemplate release tarball for ${os}-${arch} with SHA256 ${hash}"
+    gpg --armor --detach-sig --sign "${tar}"
+}
+
+case "${1}" in
+    "build")
+        # Build releases for various operating systems:
+        build-for "linux" "amd64"
+        build-for "darwin" "amd64"
+        build-for "windows" "amd64"
+        build-for "freebsd" "amd64"
+        exit 0
+        ;;
+    "sign")
+        # Bundle and sign releases:
+        sign-for "linux" "amd64"
+        sign-for "darwin" "amd64"
+        sign-for "windows" "amd64"
+        sign-for "freebsd" "amd64"
+        exit 0
+        ;;
+esac
diff --git a/ops/kontemplate/context/context.go b/ops/kontemplate/context/context.go
new file mode 100644
index 0000000000..2d0378a0ec
--- /dev/null
+++ b/ops/kontemplate/context/context.go
@@ -0,0 +1,266 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package context
+
+import (
+	"fmt"
+	"path"
+	"strings"
+
+	"github.com/tazjin/kontemplate/util"
+)
+
+type ResourceSet struct {
+	// Name of the resource set. This can be used in include/exclude statements during kontemplate runs.
+	Name string `json:"name"`
+
+	// Path to the folder containing the files for this resource set. This defaults to the value of the 'name' field
+	// if unset.
+	Path string `json:"path"`
+
+	// Values to include when interpolating resources from this resource set.
+	Values map[string]interface{} `json:"values"`
+
+	// Args to pass on to kubectl for this resource set.
+	Args []string `json:"args"`
+
+	// Nested resource sets to include
+	Include []ResourceSet `json:"include"`
+
+	// Parent resource set for flattened resource sets. Should not be manually specified.
+	Parent string
+}
+
+type Context struct {
+	// The name of the kubectl context
+	Name string `json:"context"`
+
+	// Global variables that should be accessible by all resource sets
+	Global map[string]interface{} `json:"global"`
+
+	// File names of YAML or JSON files including extra variables that should be globally accessible
+	VariableImportFiles []string `json:"import"`
+
+	// The resource sets to include in this context
+	ResourceSets []ResourceSet `json:"include"`
+
+	// Variables imported from additional files
+	ImportedVars map[string]interface{}
+
+	// Explicitly set variables (via `--var`) that should override all others
+	ExplicitVars map[string]interface{}
+
+	// This field represents the absolute path to the context base directory and should not be manually specified.
+	BaseDir string
+}
+
+func contextLoadingError(filename string, cause error) error {
+	return fmt.Errorf("Context loading failed on file %s due to: \n%v", filename, cause)
+}
+
+// Attempt to load and deserialise a Context from the specified file.
+func LoadContext(filename string, explicitVars *[]string) (*Context, error) {
+	var ctx Context
+	err := util.LoadData(filename, &ctx)
+
+	if err != nil {
+		return nil, contextLoadingError(filename, err)
+	}
+
+	ctx.BaseDir = path.Dir(filename)
+
+	// Prepare the resource sets by resolving parents etc.
+	ctx.ResourceSets = flattenPrepareResourceSetPaths(&ctx.BaseDir, &ctx.ResourceSets)
+
+	// Add variables explicitly specified on the command line
+	ctx.ExplicitVars, err = loadExplicitVars(explicitVars)
+	if err != nil {
+		return nil, fmt.Errorf("Error setting explicit variables: %v\n", err)
+	}
+
+	// Add variables loaded from import files
+	ctx.ImportedVars, err = ctx.loadImportedVariables()
+	if err != nil {
+		return nil, contextLoadingError(filename, err)
+	}
+
+	// Merge variables defined at different levels. The
+	// `mergeContextValues` function is documented with the merge
+	// hierarchy.
+	ctx.ResourceSets = ctx.mergeContextValues()
+
+	if err != nil {
+		return nil, contextLoadingError(filename, err)
+	}
+
+	return &ctx, nil
+}
+
+// Kontemplate supports specifying additional variable files with the
+// `import` keyword. This function loads those variable files and
+// merges them together with the context's other global variables.
+func (ctx *Context) loadImportedVariables() (map[string]interface{}, error) {
+	allImportedVars := make(map[string]interface{})
+
+	for _, file := range ctx.VariableImportFiles {
+		// Ensure that the filename is not merged with the baseDir if
+		// it is set to an absolute path.
+		var filePath string
+		if path.IsAbs(file) {
+			filePath = file
+		} else {
+			filePath = path.Join(ctx.BaseDir, file)
+		}
+
+		var importedVars map[string]interface{}
+		err := util.LoadData(filePath, &importedVars)
+
+		if err != nil {
+			return nil, err
+		}
+
+		allImportedVars = *util.Merge(&allImportedVars, &importedVars)
+	}
+
+	return allImportedVars, nil
+}
+
+// Correctly prepares the file paths for resource sets by inferring implicit paths and flattening resource set
+// collections, i.e. resource sets that themselves have an additional 'include' field set.
+// Those will be regarded as a short-hand for including multiple resource sets from a subfolder.
+// See https://github.com/tazjin/kontemplate/issues/9 for more information.
+func flattenPrepareResourceSetPaths(baseDir *string, rs *[]ResourceSet) []ResourceSet {
+	flattened := make([]ResourceSet, 0)
+
+	for _, r := range *rs {
+		// If a path is not explicitly specified it should default to the resource set name.
+		// This is also the classic behaviour prior to kontemplate 1.2
+		if r.Path == "" {
+			r.Path = r.Name
+		}
+
+		// Paths are made absolute by resolving them relative to the context base,
+		// unless absolute paths were specified.
+		if !path.IsAbs(r.Path) {
+			r.Path = path.Join(*baseDir, r.Path)
+		}
+
+		if len(r.Include) == 0 {
+			flattened = append(flattened, r)
+		} else {
+			for _, subResourceSet := range r.Include {
+				if subResourceSet.Path == "" {
+					subResourceSet.Path = subResourceSet.Name
+				}
+
+				subResourceSet.Parent = r.Name
+				subResourceSet.Name = path.Join(r.Name, subResourceSet.Name)
+				subResourceSet.Path = path.Join(r.Path, subResourceSet.Path)
+				subResourceSet.Values = *util.Merge(&r.Values, &subResourceSet.Values)
+				flattened = append(flattened, subResourceSet)
+			}
+		}
+	}
+
+	return flattened
+}
+
+// Merges the context and resource set variables according in the
+// desired precedence order.
+//
+// For now the reasoning behind the merge order is from least specific
+// in relation to the cluster configuration, which means that the
+// precedence is (in ascending order):
+//
+// 1. Default values in resource sets.
+// 2. Values imported from files (via `import:`)
+// 3. Global values in a cluster configuration
+// 4. Values set in a resource set's `include`-section
+// 5. Explicit values set on the CLI (`--var`)
+//
+// For a discussion on the reasoning behind this order, please consult
+// https://github.com/tazjin/kontemplate/issues/142
+func (ctx *Context) mergeContextValues() []ResourceSet {
+	updated := make([]ResourceSet, len(ctx.ResourceSets))
+
+	// Merging has to happen separately for every individual
+	// resource set to make use of the default values:
+	for i, rs := range ctx.ResourceSets {
+		// Begin by loading default values from the resource
+		// sets configuration.
+		//
+		// Resource sets are used across different cluster
+		// contexts and the default values in them have the
+		// lowest precedence.
+		defaultValues := loadDefaultValues(&rs, ctx)
+
+		// Continue by merging default values with values
+		// imported from external files. Those values are also
+		// used across cluster contexts, but have higher
+		// precedence than defaults.
+		merged := util.Merge(defaultValues, &ctx.ImportedVars)
+
+		// Merge global values defined in the cluster context:
+		merged = util.Merge(merged, &ctx.Global)
+
+		// Merge values configured in the resource set's
+		// `include` section:
+		merged = util.Merge(merged, &rs.Values)
+
+		// Merge values defined explicitly on the CLI:
+		merged = util.Merge(merged, &ctx.ExplicitVars)
+
+		// Continue with the newly merged resource set:
+		rs.Values = *merged
+		updated[i] = rs
+	}
+
+	return updated
+}
+
+// Loads default values for a resource set collection from
+// path/to/set/default.{json|yaml}.
+func loadDefaultValues(rs *ResourceSet, c *Context) *map[string]interface{} {
+	var defaultVars map[string]interface{}
+
+	for _, filename := range util.DefaultFilenames {
+		err := util.LoadData(path.Join(rs.Path, filename), &defaultVars)
+		if err == nil {
+			return &defaultVars
+		}
+	}
+
+	// The actual error is not inspected here. The reasoning for
+	// this is that in case of serious problems (e.g. permission
+	// issues with the folder / folder not existing) failure will
+	// occur a bit later anyways.
+	//
+	// Otherwise we'd have to differentiate between
+	// file-not-found-errors (no default values specified) and
+	// other errors here.
+	return &rs.Values
+}
+
+// Prepares the variables specified explicitly via `--var` when
+// executing kontemplate for adding to the context.
+func loadExplicitVars(vars *[]string) (map[string]interface{}, error) {
+	explicitVars := make(map[string]interface{}, len(*vars))
+
+	for _, v := range *vars {
+		varParts := strings.SplitN(v, "=", 2)
+		if len(varParts) != 2 {
+			return nil, fmt.Errorf(`invalid explicit variable provided (%s), name and value should be separated with "="`, v)
+		}
+
+		explicitVars[varParts[0]] = varParts[1]
+	}
+
+	return explicitVars, nil
+}
diff --git a/ops/kontemplate/context/context_test.go b/ops/kontemplate/context/context_test.go
new file mode 100644
index 0000000000..471eb246cf
--- /dev/null
+++ b/ops/kontemplate/context/context_test.go
@@ -0,0 +1,353 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package context
+
+import (
+	"reflect"
+	"testing"
+)
+
+var noExplicitVars []string = make([]string, 0)
+
+func TestLoadFlatContextFromFile(t *testing.T) {
+	ctx, err := LoadContext("testdata/flat-test.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		Global: map[string]interface{}{
+			"globalVar": "lizards",
+		},
+		ResourceSets: []ResourceSet{
+			{
+				Name: "some-api",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"apiPort":          float64(4567), // yep!
+					"importantFeature": true,
+					"version":          "1.0-0e6884d",
+					"globalVar":        "lizards",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestLoadContextWithArgs(t *testing.T) {
+	ctx, err := LoadContext("testdata/flat-with-args-test.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name:   "some-api",
+				Path:   "testdata/some-api",
+				Values: make(map[string]interface{}, 0),
+				Args: []string{
+					"--as=some-user",
+					"--as-group=hello:world",
+					"--as-banana",
+					"true",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestLoadContextWithResourceSetCollections(t *testing.T) {
+	ctx, err := LoadContext("testdata/collections-test.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		Global: map[string]interface{}{
+			"globalVar": "lizards",
+		},
+		ResourceSets: []ResourceSet{
+			{
+				Name: "some-api",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"apiPort":          float64(4567), // yep!
+					"importantFeature": true,
+					"version":          "1.0-0e6884d",
+					"globalVar":        "lizards",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+			{
+				Name: "collection/nested",
+				Path: "testdata/collection/nested",
+				Values: map[string]interface{}{
+					"lizards":   "good",
+					"globalVar": "lizards",
+				},
+				Include: nil,
+				Parent:  "collection",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+
+}
+
+func TestSubresourceVariableInheritance(t *testing.T) {
+	ctx, err := LoadContext("testdata/parent-variables.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name: "parent/child",
+				Path: "testdata/parent/child",
+				Values: map[string]interface{}{
+					"foo": "bar",
+					"bar": "baz",
+				},
+				Include: nil,
+				Parent:  "parent",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestSubresourceVariableInheritanceOverride(t *testing.T) {
+	ctx, err := LoadContext("testdata/parent-variable-override.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name: "parent/child",
+				Path: "testdata/parent/child",
+				Values: map[string]interface{}{
+					"foo": "newvalue",
+				},
+				Include: nil,
+				Parent:  "parent",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestDefaultValuesLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/default-loading.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	rs := ctx.ResourceSets[0]
+	if rs.Values["defaultValues"] != "loaded" {
+		t.Errorf("Default values not loaded from YAML file")
+		t.Fail()
+	}
+
+	if rs.Values["override"] != "notAtAll" {
+		t.Error("Default values should not override other values")
+		t.Fail()
+	}
+}
+
+func TestImportValuesLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/import-vars-simple.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := map[string]interface{}{
+		"override": "true",
+		"music": map[string]interface{}{
+			"artist": "Pallida",
+			"track":  "Tractor Beam",
+		},
+	}
+
+	if !reflect.DeepEqual(ctx.ImportedVars, expected) {
+		t.Error("Expected imported values after loading imports did not match!")
+		t.Fail()
+	}
+}
+
+func TestExplicitPathLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/explicit-path.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name: "some-api-europe",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"location": "europe",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+			{
+				Name: "some-api-asia",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"location": "asia",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestExplicitSubresourcePathLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/explicit-subresource-path.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name:   "parent/child",
+				Path:   "testdata/parent-path/child-path",
+				Parent: "parent",
+				Values: make(map[string]interface{}, 0),
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestSetVariablesFromArguments(t *testing.T) {
+	vars := []string{"version=some-service-version"}
+	ctx, _ := LoadContext("testdata/default-loading.yaml", &vars)
+
+	if version := ctx.ExplicitVars["version"]; version != "some-service-version" {
+		t.Errorf(`Expected variable "version" to have value "some-service-version" but was "%s"`, version)
+	}
+}
+
+func TestSetInvalidVariablesFromArguments(t *testing.T) {
+	vars := []string{"version: some-service-version"}
+	_, err := LoadContext("testdata/default-loading.yaml", &vars)
+
+	if err == nil {
+		t.Error("Expected invalid variable to return an error")
+	}
+}
+
+// This test ensures that variables are merged in the correct order.
+// Please consult the test data in `testdata/merging`.
+func TestValueMergePrecedence(t *testing.T) {
+	cliVars := []string{"cliVar=cliVar"}
+	ctx, _ := LoadContext("testdata/merging/context.yaml", &cliVars)
+
+	expected := map[string]interface{}{
+		"defaultVar": "defaultVar",
+		"importVar":  "importVar",
+		"globalVar":  "globalVar",
+		"includeVar": "includeVar",
+		"cliVar":     "cliVar",
+	}
+
+	result := ctx.ResourceSets[0].Values
+
+	if !reflect.DeepEqual(expected, result) {
+		t.Errorf("Merged values did not match expected result: \n%v", result)
+		t.Fail()
+	}
+}
diff --git a/ops/kontemplate/context/testdata/collections-test.yaml b/ops/kontemplate/context/testdata/collections-test.yaml
new file mode 100644
index 0000000000..a619c8cfdd
--- /dev/null
+++ b/ops/kontemplate/context/testdata/collections-test.yaml
@@ -0,0 +1,15 @@
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
+  - name: collection
+    include:
+      - name: nested
+        values:
+          lizards: good
diff --git a/ops/kontemplate/context/testdata/default-loading.yaml b/ops/kontemplate/context/testdata/default-loading.yaml
new file mode 100644
index 0000000000..d589c99b4e
--- /dev/null
+++ b/ops/kontemplate/context/testdata/default-loading.yaml
@@ -0,0 +1,6 @@
+---
+context: default-loading
+include:
+  - name: default
+    values:
+      override: notAtAll
\ No newline at end of file
diff --git a/ops/kontemplate/context/testdata/default/default.yaml b/ops/kontemplate/context/testdata/default/default.yaml
new file mode 100644
index 0000000000..0ffa3cd81f
--- /dev/null
+++ b/ops/kontemplate/context/testdata/default/default.yaml
@@ -0,0 +1,2 @@
+defaultValues: loaded
+override: noop
\ No newline at end of file
diff --git a/ops/kontemplate/context/testdata/explicit-path.yaml b/ops/kontemplate/context/testdata/explicit-path.yaml
new file mode 100644
index 0000000000..2c81f83c09
--- /dev/null
+++ b/ops/kontemplate/context/testdata/explicit-path.yaml
@@ -0,0 +1,11 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: some-api-europe
+    path: some-api
+    values:
+      location: europe
+  - name: some-api-asia
+    path: some-api
+    values:
+      location: asia
diff --git a/ops/kontemplate/context/testdata/explicit-subresource-path.yaml b/ops/kontemplate/context/testdata/explicit-subresource-path.yaml
new file mode 100644
index 0000000000..6cf8618322
--- /dev/null
+++ b/ops/kontemplate/context/testdata/explicit-subresource-path.yaml
@@ -0,0 +1,8 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: parent
+    path: parent-path
+    include:
+      - name: child
+        path: child-path
diff --git a/ops/kontemplate/context/testdata/flat-test.yaml b/ops/kontemplate/context/testdata/flat-test.yaml
new file mode 100644
index 0000000000..dd7804f719
--- /dev/null
+++ b/ops/kontemplate/context/testdata/flat-test.yaml
@@ -0,0 +1,10 @@
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
diff --git a/ops/kontemplate/context/testdata/flat-with-args-test.yaml b/ops/kontemplate/context/testdata/flat-with-args-test.yaml
new file mode 100644
index 0000000000..29d3334fb5
--- /dev/null
+++ b/ops/kontemplate/context/testdata/flat-with-args-test.yaml
@@ -0,0 +1,9 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: some-api
+    args:
+      - --as=some-user
+      - --as-group=hello:world
+      - --as-banana
+      - "true"
diff --git a/ops/kontemplate/context/testdata/import-vars-simple.yaml b/ops/kontemplate/context/testdata/import-vars-simple.yaml
new file mode 100644
index 0000000000..12244e1ab1
--- /dev/null
+++ b/ops/kontemplate/context/testdata/import-vars-simple.yaml
@@ -0,0 +1,5 @@
+---
+context: k8s.prod.mydomain.com
+import:
+  - test-vars.yaml
+include: []
diff --git a/ops/kontemplate/context/testdata/merging/context.yaml b/ops/kontemplate/context/testdata/merging/context.yaml
new file mode 100644
index 0000000000..df30d3d8cb
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/context.yaml
@@ -0,0 +1,15 @@
+# This context file is intended to test the merge hierarchy of
+# variables defined at different levels.
+---
+context: merging.in.kontemplate.works
+global:
+  globalVar: globalVar
+  includeVar: should be overridden (global)
+  cliVar: should be overridden (global)
+import:
+  - import-vars.yaml
+include:
+  - name: resource
+    values:
+      includeVar: includeVar
+      cliVar: should be overridden (include)
diff --git a/ops/kontemplate/context/testdata/merging/import-vars.yaml b/ops/kontemplate/context/testdata/merging/import-vars.yaml
new file mode 100644
index 0000000000..2a51352571
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/import-vars.yaml
@@ -0,0 +1,4 @@
+importVar: importVar
+globalVar: should be overridden (import)
+includeVar: should be overridden (import)
+cliVar: should be overridden (import)
diff --git a/ops/kontemplate/context/testdata/merging/resource/default.yaml b/ops/kontemplate/context/testdata/merging/resource/default.yaml
new file mode 100644
index 0000000000..040a19aaba
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/resource/default.yaml
@@ -0,0 +1,5 @@
+defaultVar: defaultVar
+importVar: should be overridden (default)
+globalVar: should be overridden (default)
+includeVar: should be overridden (default)
+cliVar: should be overridden (default)
diff --git a/ops/kontemplate/context/testdata/merging/resource/output.yaml b/ops/kontemplate/context/testdata/merging/resource/output.yaml
new file mode 100644
index 0000000000..5920b27207
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/resource/output.yaml
@@ -0,0 +1,5 @@
+defaultVar: {{ .defaultVar }}
+importVar: {{ .importVar }}
+globalVar: {{ .globalVar }}
+includeVar: {{ .includeVar }}
+cliVar: {{ .cliVar }}
diff --git a/ops/kontemplate/context/testdata/parent-variable-override.yaml b/ops/kontemplate/context/testdata/parent-variable-override.yaml
new file mode 100644
index 0000000000..42676c3028
--- /dev/null
+++ b/ops/kontemplate/context/testdata/parent-variable-override.yaml
@@ -0,0 +1,10 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: parent
+    values:
+      foo: bar
+    include:
+      - name: child
+        values:
+          foo: newvalue
diff --git a/ops/kontemplate/context/testdata/parent-variables.yaml b/ops/kontemplate/context/testdata/parent-variables.yaml
new file mode 100644
index 0000000000..8459fd3040
--- /dev/null
+++ b/ops/kontemplate/context/testdata/parent-variables.yaml
@@ -0,0 +1,10 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: parent
+    values:
+      foo: bar
+    include:
+      - name: child
+        values:
+          bar: baz
diff --git a/ops/kontemplate/context/testdata/test-vars-override.yaml b/ops/kontemplate/context/testdata/test-vars-override.yaml
new file mode 100644
index 0000000000..5215c559c1
--- /dev/null
+++ b/ops/kontemplate/context/testdata/test-vars-override.yaml
@@ -0,0 +1,3 @@
+---
+override: 3
+place: Oslo
diff --git a/ops/kontemplate/context/testdata/test-vars.yaml b/ops/kontemplate/context/testdata/test-vars.yaml
new file mode 100644
index 0000000000..af27bdc455
--- /dev/null
+++ b/ops/kontemplate/context/testdata/test-vars.yaml
@@ -0,0 +1,5 @@
+---
+override: 'true'
+music:
+  artist: Pallida
+  track: Tractor Beam
diff --git a/ops/kontemplate/default.nix b/ops/kontemplate/default.nix
new file mode 100644
index 0000000000..1190869c3f
--- /dev/null
+++ b/ops/kontemplate/default.nix
@@ -0,0 +1,36 @@
+# Copyright (C) 2016-2021  Vincent Ambo <mail@tazj.in>
+#
+# This file is part of Kontemplate.
+#
+# Kontemplate is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This file is the Nix derivation used to install Kontemplate on
+# Nix-based systems.
+
+{ lib, pkgs, ... }:
+
+pkgs.buildGoPackage rec {
+  name = "kontemplate-${version}";
+  version = "canon";
+  src = ./.;
+  goPackagePath = "github.com/tazjin/kontemplate";
+  goDeps = ./deps.nix;
+  buildInputs = [ pkgs.parallel ];
+
+  # Enable checks and configure check-phase to include vet:
+  doCheck = true;
+  preCheck = ''
+    for pkg in $(getGoDirs ""); do
+      buildGoDir vet "$pkg"
+    done
+  '';
+
+  meta = with lib; {
+    description = "A resource templating helper for Kubernetes";
+    homepage = "http://kontemplate.works/";
+    license = licenses.gpl3;
+  };
+}
diff --git a/ops/kontemplate/deps.nix b/ops/kontemplate/deps.nix
new file mode 100644
index 0000000000..7693968bd5
--- /dev/null
+++ b/ops/kontemplate/deps.nix
@@ -0,0 +1,111 @@
+# This file was generated by https://github.com/kamilchm/go2nix v1.3.0
+[
+  {
+    goPackagePath = "github.com/Masterminds/goutils";
+    fetch = {
+      type = "git";
+      url = "https://github.com/Masterminds/goutils";
+      rev = "41ac8693c5c10a92ea1ff5ac3a7f95646f6123b0";
+      sha256 = "180px47gj936qyk5bkv5mbbgiil9abdjq6kwkf7sq70vyi9mcfiq";
+    };
+  }
+  {
+    goPackagePath = "github.com/Masterminds/semver";
+    fetch = {
+      type = "git";
+      url = "https://github.com/Masterminds/semver";
+      rev = "5bc3b9184d48f1412b300b87a200cf020d9254cf";
+      sha256 = "1vdfm653v50jf63cw0kg2hslx50cn4mk6lj3p51bi11jrg48kfng";
+    };
+  }
+  {
+    goPackagePath = "github.com/Masterminds/sprig";
+    fetch = {
+      type = "git";
+      url = "https://github.com/Masterminds/sprig";
+      rev = "6f509977777c33eae63b2136d97f7b976cb971cc";
+      sha256 = "05h9k6fhjxnpwlihj3z02q9kvqvnq53jix0ab84sx0666bci3cdh";
+    };
+  }
+  {
+    goPackagePath = "github.com/alecthomas/template";
+    fetch = {
+      type = "git";
+      url = "https://github.com/alecthomas/template";
+      rev = "fb15b899a75114aa79cc930e33c46b577cc664b1";
+      sha256 = "1vlasv4dgycydh5wx6jdcvz40zdv90zz1h7836z7lhsi2ymvii26";
+    };
+  }
+  {
+    goPackagePath = "github.com/alecthomas/units";
+    fetch = {
+      type = "git";
+      url = "https://github.com/alecthomas/units";
+      rev = "c3de453c63f4bdb4dadffab9805ec00426c505f7";
+      sha256 = "0js37zlgv37y61j4a2d46jh72xm5kxmpaiw0ya9v944bjpc386my";
+    };
+  }
+  {
+    goPackagePath = "github.com/ghodss/yaml";
+    fetch = {
+      type = "git";
+      url = "https://github.com/ghodss/yaml";
+      rev = "25d852aebe32c875e9c044af3eef9c7dc6bc777f";
+      sha256 = "1w9yq0bxzygc4qwkwwiy7k1k1yviaspcqqv18255k2xkjv5ipccz";
+    };
+  }
+  {
+    goPackagePath = "github.com/google/uuid";
+    fetch = {
+      type = "git";
+      url = "https://github.com/google/uuid";
+      rev = "c2e93f3ae59f2904160ceaab466009f965df46d6";
+      sha256 = "0zw8fvl6jqg0fmv6kmvhss0g4gkrbvgyvl2zgy5wdbdlgp4fja0h";
+    };
+  }
+  {
+    goPackagePath = "github.com/huandu/xstrings";
+    fetch = {
+      type = "git";
+      url = "https://github.com/huandu/xstrings";
+      rev = "8bbcf2f9ccb55755e748b7644164cd4bdce94c1d";
+      sha256 = "1ivvc95514z63k7cpz71l0dwlanffmsh1pijhaqmp41kfiby8rsx";
+    };
+  }
+  {
+    goPackagePath = "github.com/imdario/mergo";
+    fetch = {
+      type = "git";
+      url = "https://github.com/imdario/mergo";
+      rev = "4c317f2286be3bd0c4f1a0e622edc6398ec4656d";
+      sha256 = "0bihha1qsgfjk14yv1hwddv3d8dzxpbjlaxwwyys6lhgxz1cr9h9";
+    };
+  }
+  {
+    goPackagePath = "golang.org/x/crypto";
+    fetch = {
+      type = "git";
+      url = "https://go.googlesource.com/crypto";
+      rev = "9756ffdc24725223350eb3266ffb92590d28f278";
+      sha256 = "0q7hxaaq6lp0v8qqzifvysl47z5rfdlrxkh3d29vsl3wyby3dxl8";
+    };
+  }
+  {
+    goPackagePath = "gopkg.in/alecthomas/kingpin.v2";
+    fetch = {
+      type = "git";
+      url = "https://gopkg.in/alecthomas/kingpin.v2";
+      rev = "947dcec5ba9c011838740e680966fd7087a71d0d";
+      sha256 = "0mndnv3hdngr3bxp7yxfd47cas4prv98sqw534mx7vp38gd88n5r";
+    };
+  }
+  {
+    goPackagePath = "gopkg.in/yaml.v2";
+    fetch = {
+      type = "git";
+      url = "https://gopkg.in/yaml.v2";
+      rev = "51d6538a90f86fe93ac480b35f37b2be17fef232";
+      sha256 = "01wj12jzsdqlnidpyjssmj0r4yavlqy7dwrg7adqd8dicjc4ncsa";
+    };
+  }
+]
diff --git a/ops/kontemplate/docs/cluster-config.md b/ops/kontemplate/docs/cluster-config.md
new file mode 100644
index 0000000000..4e87016179
--- /dev/null
+++ b/ops/kontemplate/docs/cluster-config.md
@@ -0,0 +1,106 @@
+Cluster configuration
+==========================
+
+Every cluster (or "environment") that requires individual configuration is specified in
+a very simple YAML file in Kontemplate.
+
+An example file for a hypothetical test environment could look like this:
+
+```yaml
+---
+context: k8s.test.mydomain.com
+global:
+  clusterName: test-cluster
+  defaultReplicas: 2
+import:
+  - test-secrets.yaml
+include:
+  - name: gateway
+    path: tools/nginx
+    values:
+      tlsDomains:
+        - test.oslo.pub
+        - test.tazj.in
+  - path: backend
+    values:
+      env: test
+    include:
+      - name: blog
+        values:
+          url: test.tazj.in
+      - name: pub-service
+```
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Cluster configuration](#cluster-configuration)
+    - [Fields](#fields)
+        - [`context`](#context)
+        - [`global`](#global)
+        - [`import`](#import)
+        - [`include`](#include)
+    - [External variables](#external-variables)
+
+<!-- markdown-toc end -->
+
+## Fields
+
+This is documentation for the individual fields in a cluster context file.
+
+### `context`
+
+The `context` field contains the name of the kubectl-context. You can list context names with
+'kubectl config get-contexts'.
+
+This must be set here so that Kontemplate can use the correct context when calling kubectl.
+
+This field is **required** for `kubectl`-wrapping commands. It can be left out if only the `template`-command is used.
+
+### `global`
+
+The `global` field contains a key/value map of variables that should be available to all resource
+sets in the cluster.
+
+This field is **optional**.
+
+### `import`
+
+The `import` field contains the file names of additional YAML or JSON files from which global
+variables should be loaded. Using this field makes it possible to keep certain configuration that
+is the same for some, but not all, clusters in a common place.
+
+This field is **optional**.
+
+### `include`
+
+The `include` field contains the actual resource sets to be included in the cluster.
+
+Information about the structure of resource sets can be found in the [resource set documentation][].
+
+This field is **required**.
+
+## External variables
+
+As mentioned above, extra variables can be loaded from additional YAML or JSON files. Assuming you
+have a file called `test-secrets.yaml` which contains variables that should be shared between a `test`
+and `dev` cluster, you could import it in your context as such:
+
+```yaml
+# test-secrets.yaml:
+mySecretVar: foo-bar-12345
+
+# test-cluster.yaml:
+context: k8s.test.mydomain.com
+import:
+  - test-secrets.yaml
+
+# dev-cluster.yaml:
+context: k8s.dev.mydomain.com
+import:
+  - test-secrets.yaml
+```
+
+The variable `mySecretVar` is then available as a global variable.
+
+[resource set documentation]: resource-sets.md
diff --git a/ops/kontemplate/docs/resource-sets.md b/ops/kontemplate/docs/resource-sets.md
new file mode 100644
index 0000000000..1444dd4912
--- /dev/null
+++ b/ops/kontemplate/docs/resource-sets.md
@@ -0,0 +1,170 @@
+Resource Sets
+================
+
+Resource sets are collections of Kubernetes resources that should be passed to `kubectl` together.
+
+Technically a resource set is simply a folder with a few YAML and/or JSON templates in it.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Resource Sets](#resource-sets)
+- [Creating resource sets](#creating-resource-sets)
+    - [Default variables](#default-variables)
+- [Including resource sets](#including-resource-sets)
+    - [Fields](#fields)
+        - [`name`](#name)
+        - [`path`](#path)
+        - [`values`](#values)
+        - [`args`](#args)
+        - [`include`](#include)
+    - [Multiple includes](#multiple-includes)
+    - [Nesting resource sets](#nesting-resource-sets)
+        - [Caveats](#caveats)
+
+<!-- markdown-toc end -->
+
+# Creating resource sets
+
+Simply create a folder in your Kontemplate repository and place a YAML or JSON file in it. These
+files get interpreted as [templates][] during Kontemplate runs and variables (as well as template
+logic or functions) will be interpolated.
+
+Refer to the template documentation for information on how to write templates.
+
+## Default variables
+
+Sometimes it is useful to specify default values for variables that should be interpolated during
+a run if the [cluster configuration][] does not specify a variable explicitly.
+
+This can be done simply by placing a `default.yaml` or `default.json` file in the resource set
+folder and filling it with key/value pairs of the intended default variables.
+
+Kontemplate will error during interpolation if any variables are left unspecified.
+
+# Including resource sets
+
+Under the cluster configuration `include` key resource sets are included and required variables
+are specified. For example:
+
+```yaml
+include:
+  - name: some-api
+    values:
+      version: 1.2-SNAPSHOT
+```
+
+This will include a resource set from a folder called `some-api` and set the specified `version` variable.
+
+## Fields
+
+The available fields when including a resource set are these:
+
+### `name`
+
+The `name` field contains the name of the resource set. This name can be used to refer to the resource set
+when specifying explicit includes or excludes during a run.
+
+By default it is assumed that the `name` is the path to the resource set folder, but this can be overridden.
+
+This field is **required**.
+
+### `path`
+
+The `path` field specifies an explicit path to a resource set folder in the case that it should differ from
+the resource set's `name`.
+
+This field is **optional**.
+
+### `values`
+
+The `values` field specifies key/values pairs of variables that should be available during templating.
+
+This field is **optional**.
+
+### `args`
+
+The `args` field specifies a list of arguments that should be passed to `kubectl`.
+
+This field is **optional**.
+
+### `include`
+
+The `include` field specifies additional resource sets that should be included and that should inherit the
+variables of this resource set.
+
+The fully qualified names of "nested" resource sets are set to `${PARENT_NAME}/${CHILD_NAME}` and paths are
+merged in the same way.
+
+This makes it easy to organise different resource sets as "groups" to include / exclude them collectively
+during runs.
+
+This field is **optional**.
+
+## Multiple includes
+
+Resource sets can be included multiple times with different configurations. In this case it is recommended
+to set the `path` and `name` fields explicitly. For example:
+
+```yaml
+include:
+  - name: forwarder-europe
+    path: tools/forwarder
+    values:
+      source: europe
+  - name: forwarder-asia
+    path: tools/forwarder
+    values:
+      source: asia
+```
+
+The two different configurations can be referred to by their set names, but will use the same resource
+templates with different configurations.
+
+## Nesting resource sets
+
+As mentioned above for the `include` field, resource sets can be nested. This lets users group resource
+sets in logical ways using simple folder structures.
+
+Assuming a folder structure like:
+
+```
+├── backend
+│   ├── auth-api
+│   ├── message-api
+│   └── order-api
+└── frontend
+    ├── app-page
+    └── login-page
+```
+
+With each of these folders being a resource set, they could be included in a cluster configuration like so:
+
+```yaml
+include:
+  - name: backend
+    include:
+      - name: auth-api
+      - name: message-api
+      - name: order-api
+  - name: frontend:
+    include:
+      - name: app-page
+      - name: login-page
+```
+
+Kontemplate could then be run with, for example, `--include backend` to only include the resource sets nested
+in the backend group. Specific resource sets can also be targeted, for example as `--include backend/order-api`.
+
+Variables specified in the parent resource set are inherited by the children.
+
+### Caveats
+
+Two caveats apply that users should be aware of:
+
+1. The parent resource set can not contain any resource templates itself.
+
+2. Only one level of nesting is supported. Specifying `include` again on a nested resource set will be ignored.
+
+[templates]: templates.md
+[cluster configuration]: cluster-config.md
diff --git a/ops/kontemplate/docs/templates.md b/ops/kontemplate/docs/templates.md
new file mode 100644
index 0000000000..32da205108
--- /dev/null
+++ b/ops/kontemplate/docs/templates.md
@@ -0,0 +1,153 @@
+Kontemplate templates
+=====================
+
+The template file format is based on Go's [templating engine][] in combination
+with a small extension library called [sprig][] that adds additional template
+functions.
+
+Go templates can either simply display variables or build more complicated
+*pipelines* in which variables are passed to functions for further processing,
+or in which conditionals are evaluated for more complex template logic.
+
+It is recommended that you check out the Golang [documentation][] for the templating
+engine in addition to the cherry-picked features listed here.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Kontemplate templates](#kontemplate-templates)
+    - [Basic variable interpolation](#basic-variable-interpolation)
+        - [Example:](#example)
+    - [Template functions](#template-functions)
+    - [Examples:](#examples)
+    - [Conditionals & ranges](#conditionals--ranges)
+    - [Caveats](#caveats)
+
+<!-- markdown-toc end -->
+
+## Basic variable interpolation
+
+The basic template format uses `{{ .variableName }}` as the interpolation format.
+
+### Example:
+
+Assuming that you include a resource set as such:
+
+```
+- name: api-gateway
+  values:
+    internalHost: http://my-internal-host/
+```
+
+And the api-gateway resource set includes a ConfigMap (some fields left out for
+the example):
+
+```
+# api-gateway/configmap.yaml:
+---
+kind: ConfigMap
+metadata:
+  name: api-gateway-config
+data:
+  internalHost: {{ .internalHost }}
+```
+
+The resulting output will be:
+
+```
+
+---
+kind: ConfigMap
+metadata:
+  name: api-gateway-config
+data:
+  internalHost: http://my-internal-host/
+```
+
+## Template functions
+
+Go templates support template functions which you can think of as a sort of
+shell-like pipeline where text flows through transformations from left to
+right.
+
+Some template functions come from Go's standard library and are listed in the
+[Go documentation][]. In addition the functions declared by [sprig][] are
+available in kontemplate, as well as five custom functions:
+
+* `json`: Encodes any supplied data structure as JSON.
+* `gitHEAD`: Retrieves the commit hash at Git `HEAD`.
+* `passLookup`: Looks up the supplied key in [pass][].
+* `insertFile`: Insert the contents of the given file in the resource
+  set folder as a string.
+* `insertTemplate`: Insert the contents of the given template in the resource
+  set folder as a string.
+
+## Examples:
+
+```
+# With the following values:
+name: Donald
+certKeyPath: my-website/cert-key
+
+# The following interpolations are possible:
+
+{{ .name | upper }}
+-> DONALD
+
+{{ .name | upper | repeat 2 }}
+-> DONALD DONALD
+
+{{ .certKeyPath | passLookup }}
+-> Returns content of 'my-website/cert-key' from pass
+
+{{ gitHEAD }}
+-> Returns the Git commit hash at HEAD.
+```
+
+## Conditionals & ranges
+
+Some logic is supported in Golang templates and can be used in Kontemplate, too.
+
+With the following values:
+
+```
+useKube2IAM: true
+servicePorts:
+  - 8080
+  - 9090
+```
+
+The following interpolations are possible:
+
+```
+# Conditionally insert something in the template:
+metadata:
+  annotations:
+    foo: bar
+    {{ if .useKube2IAM -}} iam.amazonaws.com/role: my-api {{- end }}
+```
+
+```
+# Iterate over a list of values
+ports:
+  {{ range .servicePorts }}
+  - port: {{ . }}
+  {{ end }}
+```
+
+Check out the Golang documentation (linked above) for more information about template logic.
+
+## Caveats
+
+Kontemplate does not by itself parse any of the content of the templates, which
+means that it does not validate whether the resources you supply are valid YAML
+or JSON.
+
+You can perform some validation by using `kontemplate apply --dry-run` which
+will make use of the Dry-Run functionality in `kubectl`.
+
+[templating engine]: https://golang.org/pkg/text/template/
+[documentation]: https://golang.org/pkg/text/template/
+[sprig]: http://masterminds.github.io/sprig/
+[Go documentation]: https://golang.org/pkg/text/template/#hdr-Functions
+[pass]: https://www.passwordstore.org/
diff --git a/ops/kontemplate/docs/tips-and-tricks.md b/ops/kontemplate/docs/tips-and-tricks.md
new file mode 100644
index 0000000000..5401ac91e5
--- /dev/null
+++ b/ops/kontemplate/docs/tips-and-tricks.md
@@ -0,0 +1,77 @@
+Kontemplate tips & tricks
+=========================
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Kontemplate tips & tricks](#kontemplate-tips--tricks)
+    - [Update Deployments when ConfigMaps change](#update-deployments-when-configmaps-change)
+    - [direnv & pass](#direnv--pass)
+
+<!-- markdown-toc end -->
+
+## Update Deployments when ConfigMaps change
+
+Kubernetes does [not currently][] have the ability to perform rolling updates
+of Deployments and other resource types when `ConfigMap` or `Secret` objects
+are updated.
+
+It is possible to make use of annotations and templating functions in
+Kontemplate to force updates to these resources anyways.
+ 
+For example:
+
+```yaml
+# A ConfigMap that contains some configuration for your app
+---
+kind: ConfigMap
+metadata:
+  name: app-config
+data:
+  app.conf: |
+    name: {{ .appName }}
+    foo: bar
+```
+
+Now whenever the `appName` variable changes or we make an edit to the
+`ConfigMap` we would like to update the `Deployment` making use of it, too. We
+can do this by adding a hash of the parsed template to the annotations of the
+created `Pod` objects:
+
+```yaml
+
+---
+kind: Deployment
+metadata:
+  name: app
+spec:
+  template:
+    metadata:
+      annotations:
+        configHash: {{ insertTemplate "app-config.yaml" | sha256sum }}
+    spec:
+      containers:
+        - name: app
+          # Some details omitted ... 
+          volumeMounts:
+            - name: config
+              mountPath: /etc/app/
+      volumes:
+        - name: config
+          configMap:
+            name: app-config
+```
+
+Now any change to the `ConfigMap` - either by directly editing the yaml file or
+via a changed template variable - will cause the annotation to change,
+triggering a rolling update of all relevant pods.
+
+## direnv & pass
+
+Users of `pass` may have multiple different password stores on their machines.
+Assuming that `kontemplate` configuration exists somewhere on the filesystem
+per project, it is easy to use [direnv][] to switch to the correct
+`PASSWORD_STORE_DIR` variable when entering the folder.
+
+[not currently]: https://github.com/kubernetes/kubernetes/issues/22368
+[direnv]: https://direnv.net/
diff --git a/ops/kontemplate/example/other-config.yaml b/ops/kontemplate/example/other-config.yaml
new file mode 100644
index 0000000000..87370569c4
--- /dev/null
+++ b/ops/kontemplate/example/other-config.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: extensions/v1beta1
+kind: ConfigMap
+metadata:
+  name: other-config
+data:
+  globalData: {{ .globalVar }}
diff --git a/ops/kontemplate/example/prod-cluster.json b/ops/kontemplate/example/prod-cluster.json
new file mode 100644
index 0000000000..70e2365f17
--- /dev/null
+++ b/ops/kontemplate/example/prod-cluster.json
@@ -0,0 +1,16 @@
+{
+  "context": "k8s.prod.mydomain.com",
+  "global": {
+    "globalVar": "lizards"
+  },
+  "include": [
+    {
+      "name": "some-api",
+      "values": {
+        "version": "1.0-SNAPSHOT-0e6884d",
+        "importantFeature": true,
+        "apiPort": 4567
+      }
+    }
+  ]
+}
diff --git a/ops/kontemplate/example/prod-cluster.yaml b/ops/kontemplate/example/prod-cluster.yaml
new file mode 100644
index 0000000000..9f300a4920
--- /dev/null
+++ b/ops/kontemplate/example/prod-cluster.yaml
@@ -0,0 +1,17 @@
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  # By default resource sets are included from a folder with the same
+  # name as the resource set's name
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
+
+  # Paths can also be specified manually (and point at single template
+  # files!)
+  - name: other-config
+    path: other-config.yaml
diff --git a/ops/kontemplate/example/some-api/some-api.yaml b/ops/kontemplate/example/some-api/some-api.yaml
new file mode 100644
index 0000000000..f0188f9dbd
--- /dev/null
+++ b/ops/kontemplate/example/some-api/some-api.yaml
@@ -0,0 +1,52 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-certificate
+data:
+  cert.pem: {{ passLookup "my/secret/certificate" | b64enc }}
+---
+apiVersion: extensions/v1beta1
+kind: ConfigMap
+metadata:
+  name: some-config
+data:
+  # The content of the example configuration file is templated in here
+  # by the 'insertFile' function and indented for YAML-compatibility
+  # with the 'indent' function:
+  some.cfg: |
+{{ insertFile "some.cfg" | indent 4 }}
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: some-api
+spec:
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app: some-api
+    spec:
+      containers:
+        - image: my.container.repo/some-api:{{ .version }}
+          name: some-api
+          env:
+            - name: ENABLE_IMPORTANT_FEATURE
+              value: {{ .importantFeature }}
+            - name: SOME_GLOBAL_VAR
+              value: {{ .globalVar }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: some-api
+  labels:
+    app: some-api
+spec:
+  selector:
+    app: some-api
+  ports:
+    - port: 80
+      targetPort: {{ .apiPort }}
+      name: http
diff --git a/ops/kontemplate/example/some-api/some.cfg b/ops/kontemplate/example/some-api/some.cfg
new file mode 100644
index 0000000000..733d5e1678
--- /dev/null
+++ b/ops/kontemplate/example/some-api/some.cfg
@@ -0,0 +1,4 @@
+{
+  "something": 1542,
+  "other-thing": "да"
+}
diff --git a/ops/kontemplate/image/Dockerfile b/ops/kontemplate/image/Dockerfile
new file mode 100644
index 0000000000..a40fa83b08
--- /dev/null
+++ b/ops/kontemplate/image/Dockerfile
@@ -0,0 +1,15 @@
+FROM alpine:3.10
+
+ADD hashes /root/hashes
+ADD https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl /usr/bin/kubectl
+ADD https://github.com/tazjin/kontemplate/releases/download/v1.8.0/kontemplate-1.8.0-6c3b299-linux-amd64.tar.gz /tmp/kontemplate.tar.gz
+
+# Pass release version is 1.7.3
+ADD https://raw.githubusercontent.com/zx2c4/password-store/74fdfb5022f317ad48d449e29543710bdad1afda/src/password-store.sh /usr/bin/pass
+
+RUN sha256sum -c /root/hashes && \
+    apk add -U bash tree gnupg git && \
+    chmod +x /usr/bin/kubectl /usr/bin/pass && \
+    tar xzvf /tmp/kontemplate.tar.gz && \
+    mv kontemplate /usr/bin/kontemplate && \
+    /usr/bin/kontemplate version
diff --git a/ops/kontemplate/image/README.md b/ops/kontemplate/image/README.md
new file mode 100644
index 0000000000..fe04765401
--- /dev/null
+++ b/ops/kontemplate/image/README.md
@@ -0,0 +1,12 @@
+Kontemplate Docker image
+========================
+
+This builds a simple Docker image available on the Docker Hub as `tazjin/kontemplate`.
+
+Builds are automated based on the Dockerfile contained here.
+
+It contains both `kontemplate` and `kubectl` and can be used as part of container-based
+CI pipelines.
+
+`pass` and its dependencies are also installed to enable the use of the `passLookup`
+template function if desired.
diff --git a/ops/kontemplate/image/hashes b/ops/kontemplate/image/hashes
new file mode 100644
index 0000000000..bfd87c0201
--- /dev/null
+++ b/ops/kontemplate/image/hashes
@@ -0,0 +1,2 @@
+a39dfdd77e4655acaabe301285cf389cb5fc8145060f5677dc93db1cc20911a4  /tmp/kontemplate.tar.gz
+6e805054a1fb2280abb53f75b57a1b92bf9c66ffe0d2cdcd46e81b079d93c322  /usr/bin/kubectl
diff --git a/ops/kontemplate/main.go b/ops/kontemplate/main.go
new file mode 100644
index 0000000000..e55d42465c
--- /dev/null
+++ b/ops/kontemplate/main.go
@@ -0,0 +1,242 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/tazjin/kontemplate/context"
+	"github.com/tazjin/kontemplate/templater"
+	"gopkg.in/alecthomas/kingpin.v2"
+)
+
+const version string = "1.8.0"
+
+// This variable will be initialised by the Go linker during the builder
+var gitHash string
+
+var (
+	app = kingpin.New("kontemplate", "simple Kubernetes resource templating")
+
+	// Global flags
+	includes   = app.Flag("include", "Resource sets to include explicitly").Short('i').Strings()
+	excludes   = app.Flag("exclude", "Resource sets to exclude explicitly").Short('e').Strings()
+	variables  = app.Flag("var", "Provide variables to templates explicitly").Strings()
+	kubectlBin = app.Flag("kubectl", "Path to the kubectl binary (default 'kubectl')").Default("kubectl").String()
+
+	// Commands
+	template          = app.Command("template", "Template resource sets and print them")
+	templateFile      = template.Arg("file", "Cluster configuration file to use").Required().String()
+	templateOutputDir = template.Flag("output", "Output directory in which to save templated files instead of printing them").Short('o').String()
+
+	apply       = app.Command("apply", "Template resources and pass to 'kubectl apply'")
+	applyFile   = apply.Arg("file", "Cluster configuration file to use").Required().String()
+	applyDryRun = apply.Flag("dry-run", "Print remote operations without executing them").Default("false").Bool()
+
+	replace     = app.Command("replace", "Template resources and pass to 'kubectl replace'")
+	replaceFile = replace.Arg("file", "Cluster configuration file to use").Required().String()
+
+	delete     = app.Command("delete", "Template resources and pass to 'kubectl delete'")
+	deleteFile = delete.Arg("file", "Cluster configuration file to use").Required().String()
+
+	create     = app.Command("create", "Template resources and pass to 'kubectl create'")
+	createFile = create.Arg("file", "Cluster configuration file to use").Required().String()
+
+	versionCmd = app.Command("version", "Show kontemplate version")
+)
+
+func main() {
+	app.HelpFlag.Short('h')
+
+	switch kingpin.MustParse(app.Parse(os.Args[1:])) {
+	case template.FullCommand():
+		templateCommand()
+
+	case apply.FullCommand():
+		applyCommand()
+
+	case replace.FullCommand():
+		replaceCommand()
+
+	case delete.FullCommand():
+		deleteCommand()
+
+	case create.FullCommand():
+		createCommand()
+
+	case versionCmd.FullCommand():
+		versionCommand()
+	}
+}
+
+func versionCommand() {
+	if gitHash == "" {
+		fmt.Printf("Kontemplate version %s (git commit unknown)\n", version)
+	} else {
+		fmt.Printf("Kontemplate version %s (git commit: %s)\n", version, gitHash)
+	}
+}
+
+func templateCommand() {
+	_, resourceSets := loadContextAndResources(templateFile)
+
+	for _, rs := range *resourceSets {
+		if len(rs.Resources) == 0 {
+			fmt.Fprintf(os.Stderr, "Warning: Resource set '%s' does not exist or contains no valid templates\n", rs.Name)
+			continue
+		}
+
+		if *templateOutputDir != "" {
+			templateIntoDirectory(templateOutputDir, rs)
+		} else {
+			for _, r := range rs.Resources {
+				fmt.Fprintf(os.Stderr, "Rendered file %s/%s:\n", rs.Name, r.Filename)
+				fmt.Println(r.Rendered)
+			}
+		}
+	}
+}
+
+func templateIntoDirectory(outputDir *string, rs templater.RenderedResourceSet) {
+	// Attempt to create the output directory if it does not
+	// already exist:
+	if err := os.MkdirAll(*templateOutputDir, 0775); err != nil {
+		app.Fatalf("Could not create output directory: %v\n", err)
+	}
+
+	// Nested resource sets may contain slashes in their names.
+	// These are replaced with dashes for the purpose of writing a
+	// flat list of output files:
+	setName := strings.Replace(rs.Name, "/", "-", -1)
+
+	for _, r := range rs.Resources {
+		filename := fmt.Sprintf("%s/%s-%s", *templateOutputDir, setName, r.Filename)
+		fmt.Fprintf(os.Stderr, "Writing file %s\n", filename)
+
+		file, err := os.Create(filename)
+		if err != nil {
+			app.Fatalf("Could not create file %s: %v\n", filename, err)
+		}
+
+		_, err = fmt.Fprintf(file, r.Rendered)
+		if err != nil {
+			app.Fatalf("Error writing file %s: %v\n", filename, err)
+		}
+	}
+}
+
+func applyCommand() {
+	ctx, resources := loadContextAndResources(applyFile)
+
+	var kubectlArgs []string
+
+	if *applyDryRun {
+		kubectlArgs = []string{"apply", "-f", "-", "--dry-run"}
+	} else {
+		kubectlArgs = []string{"apply", "-f", "-"}
+	}
+
+	if err := runKubectlWithResources(ctx, &kubectlArgs, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func replaceCommand() {
+	ctx, resources := loadContextAndResources(replaceFile)
+	args := []string{"replace", "--save-config=true", "-f", "-"}
+
+	if err := runKubectlWithResources(ctx, &args, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func deleteCommand() {
+	ctx, resources := loadContextAndResources(deleteFile)
+	args := []string{"delete", "-f", "-"}
+
+	if err := runKubectlWithResources(ctx, &args, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func createCommand() {
+	ctx, resources := loadContextAndResources(createFile)
+	args := []string{"create", "--save-config=true", "-f", "-"}
+
+	if err := runKubectlWithResources(ctx, &args, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func loadContextAndResources(file *string) (*context.Context, *[]templater.RenderedResourceSet) {
+	ctx, err := context.LoadContext(*file, variables)
+	if err != nil {
+		app.Fatalf("Error loading context: %v\n", err)
+	}
+
+	resources, err := templater.LoadAndApplyTemplates(includes, excludes, ctx)
+	if err != nil {
+		app.Fatalf("Error templating resource sets: %v\n", err)
+	}
+
+	return ctx, &resources
+}
+
+func runKubectlWithResources(c *context.Context, kubectlArgs *[]string, resourceSets *[]templater.RenderedResourceSet) error {
+	argsWithContext := append(*kubectlArgs, fmt.Sprintf("--context=%s", c.Name))
+
+	for _, rs := range *resourceSets {
+		if len(rs.Resources) == 0 {
+			fmt.Fprintf(os.Stderr, "Warning: Resource set '%s' contains no valid templates\n", rs.Name)
+			continue
+		}
+
+		argsWithResourceSetArgs := append(argsWithContext, rs.Args...)
+
+		kubectl := exec.Command(*kubectlBin, argsWithResourceSetArgs...)
+
+		stdin, err := kubectl.StdinPipe()
+		if err != nil {
+			return fmt.Errorf("kubectl error: %v", err)
+		}
+
+		kubectl.Stdout = os.Stdout
+		kubectl.Stderr = os.Stderr
+
+		if err = kubectl.Start(); err != nil {
+			return fmt.Errorf("kubectl error: %v", err)
+		}
+
+		for _, r := range rs.Resources {
+			fmt.Printf("Passing file %s/%s to kubectl\n", rs.Name, r.Filename)
+			fmt.Fprintln(stdin, r.Rendered)
+		}
+		stdin.Close()
+
+		if err = kubectl.Wait(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func failWithKubectlError(err error) {
+	app.Fatalf("Kubectl error: %v\n", err)
+}
diff --git a/ops/kontemplate/release.nix b/ops/kontemplate/release.nix
new file mode 100644
index 0000000000..6a3dbd5efe
--- /dev/null
+++ b/ops/kontemplate/release.nix
@@ -0,0 +1,58 @@
+# Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+#
+# This file is part of Kontemplate.
+#
+# Kontemplate is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This file is the Nix derivation used to build release binaries for
+# several different architectures and operating systems.
+
+let
+  pkgs = import
+    ((import <nixpkgs> { }).fetchFromGitHub {
+      owner = "NixOS";
+      repo = "nixpkgs-channels";
+      rev = "541d9cce8af7a490fb9085305939569567cb58e6";
+      sha256 = "0jgz72hhzkd5vyq5v69vpljjlnf0lqaz7fh327bvb3cvmwbfxrja";
+    })
+    { };
+in
+with pkgs; buildGoPackage rec {
+  name = "kontemplate-${version}";
+  version = "canon";
+  src = ./.;
+  goPackagePath = "github.com/tazjin/kontemplate";
+  goDeps = ./deps.nix;
+
+  # This configuration enables the building of statically linked
+  # executables. For some reason, those will have multiple references
+  # to the Go compiler's installation path in them, which is the
+  # reason for setting the 'allowGoReference' flag.
+  dontStrip = true; # Linker configuration handles stripping
+  allowGoReference = true;
+  CGO_ENABLED = "0";
+  GOCACHE = "off";
+
+  # Configure release builds via the "build-matrix" script:
+  buildInputs = [ git ];
+  buildPhase = ''
+    cd go/src/${goPackagePath}
+    patchShebangs build-release.sh
+    ./build-release.sh build
+  '';
+
+  outputs = [ "out" ];
+  installPhase = ''
+    mkdir $out
+    cp -r release/ $out
+  '';
+
+  meta = with lib; {
+    description = "A resource templating helper for Kubernetes";
+    homepage = "http://kontemplate.works/";
+    license = licenses.gpl3;
+  };
+}
diff --git a/ops/kontemplate/templater/dns.go b/ops/kontemplate/templater/dns.go
new file mode 100644
index 0000000000..6cd974dd93
--- /dev/null
+++ b/ops/kontemplate/templater/dns.go
@@ -0,0 +1,35 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This file contains the implementation of a template function for retrieving
+// IP addresses from DNS
+
+package templater
+
+import (
+	"fmt"
+	"net"
+	"os"
+)
+
+func GetIPsFromDNS(host string) ([]interface{}, error) {
+	fmt.Fprintf(os.Stderr, "Attempting to look up IP for %s in DNS\n", host)
+	ips, err := net.LookupIP(host)
+
+	if err != nil {
+		return nil, fmt.Errorf("IP address lookup failed: %v", err)
+	}
+
+	var result []interface{} = make([]interface{}, len(ips))
+	for i, ip := range ips {
+		result[i] = ip
+	}
+
+	return result, nil
+}
diff --git a/ops/kontemplate/templater/pass.go b/ops/kontemplate/templater/pass.go
new file mode 100644
index 0000000000..f7fbcb433d
--- /dev/null
+++ b/ops/kontemplate/templater/pass.go
@@ -0,0 +1,34 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This file contains the implementation of a template function for retrieving
+// variables from 'pass', the standard UNIX password manager.
+
+package templater
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+func GetFromPass(key string) (string, error) {
+	fmt.Fprintf(os.Stderr, "Attempting to look up %s in pass\n", key)
+	pass := exec.Command("pass", "show", key)
+
+	output, err := pass.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("Pass lookup failed: %s (%v)", output, err)
+	}
+
+	trimmed := strings.TrimSpace(string(output))
+
+	return trimmed, nil
+}
diff --git a/ops/kontemplate/templater/templater.go b/ops/kontemplate/templater/templater.go
new file mode 100644
index 0000000000..a8f0c670a6
--- /dev/null
+++ b/ops/kontemplate/templater/templater.go
@@ -0,0 +1,236 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package templater
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"text/template"
+
+	"github.com/Masterminds/sprig"
+	"github.com/tazjin/kontemplate/context"
+	"github.com/tazjin/kontemplate/util"
+)
+
+const failOnMissingKeys string = "missingkey=error"
+
+type RenderedResource struct {
+	Filename string
+	Rendered string
+}
+
+type RenderedResourceSet struct {
+	Name      string
+	Resources []RenderedResource
+	Args      []string
+}
+
+func LoadAndApplyTemplates(include *[]string, exclude *[]string, c *context.Context) ([]RenderedResourceSet, error) {
+	limitedResourceSets := applyLimits(&c.ResourceSets, include, exclude)
+	renderedResourceSets := make([]RenderedResourceSet, 0)
+
+	if len(*limitedResourceSets) == 0 {
+		return renderedResourceSets, fmt.Errorf("No valid resource sets included!")
+	}
+
+	for _, rs := range *limitedResourceSets {
+		set, err := processResourceSet(c, &rs)
+
+		if err != nil {
+			return nil, err
+		}
+
+		renderedResourceSets = append(renderedResourceSets, *set)
+	}
+
+	return renderedResourceSets, nil
+}
+
+func processResourceSet(ctx *context.Context, rs *context.ResourceSet) (*RenderedResourceSet, error) {
+	fmt.Fprintf(os.Stderr, "Loading resources for %s\n", rs.Name)
+
+	fileInfo, err := os.Stat(rs.Path)
+	if err != nil {
+		return nil, err
+	}
+
+	var files []os.FileInfo
+	var resources []RenderedResource
+
+	// Treat single-file resource paths separately from resource
+	// sets containing multiple templates
+	if fileInfo.IsDir() {
+		// Explicitly discard this error, which will give us an empty
+		// list of files instead.
+		// This will end up printing a warning to the user, but it
+		// won't stop the rest of the process.
+		files, _ = ioutil.ReadDir(rs.Path)
+		resources, err = processFiles(ctx, rs, files)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		resource, err := templateFile(ctx, rs, rs.Path)
+		if err != nil {
+			return nil, err
+		}
+
+		resources = []RenderedResource{resource}
+	}
+
+	return &RenderedResourceSet{
+		Name:      rs.Name,
+		Resources: resources,
+		Args:      rs.Args,
+	}, nil
+}
+
+func processFiles(ctx *context.Context, rs *context.ResourceSet, files []os.FileInfo) ([]RenderedResource, error) {
+	resources := make([]RenderedResource, 0)
+
+	for _, file := range files {
+		if !file.IsDir() && isResourceFile(file) {
+			path := path.Join(rs.Path, file.Name())
+			res, err := templateFile(ctx, rs, path)
+
+			if err != nil {
+				return resources, err
+			}
+
+			resources = append(resources, res)
+		}
+	}
+
+	return resources, nil
+}
+
+func templateFile(ctx *context.Context, rs *context.ResourceSet, filepath string) (RenderedResource, error) {
+	var resource RenderedResource
+
+	tpl, err := template.New(path.Base(filepath)).Funcs(templateFuncs(ctx, rs)).Option(failOnMissingKeys).ParseFiles(filepath)
+	if err != nil {
+		return resource, fmt.Errorf("Could not load template %s: %v", filepath, err)
+	}
+
+	var b bytes.Buffer
+	err = tpl.Execute(&b, rs.Values)
+	if err != nil {
+		return resource, fmt.Errorf("Error while templating %s: %v", filepath, err)
+	}
+
+	resource = RenderedResource{
+		Filename: path.Base(filepath),
+		Rendered: b.String(),
+	}
+
+	return resource, nil
+}
+
+// Applies the limits of explicitly included or excluded resources and returns the updated resource set.
+// Exclude takes priority over include
+func applyLimits(rs *[]context.ResourceSet, include *[]string, exclude *[]string) *[]context.ResourceSet {
+	if len(*include) == 0 && len(*exclude) == 0 {
+		return rs
+	}
+
+	// Exclude excluded resource sets
+	excluded := make([]context.ResourceSet, 0)
+	for _, r := range *rs {
+		if !matchesResourceSet(exclude, &r) {
+			excluded = append(excluded, r)
+		}
+	}
+
+	// Include included resource sets
+	if len(*include) == 0 {
+		return &excluded
+	}
+	included := make([]context.ResourceSet, 0)
+	for _, r := range excluded {
+		if matchesResourceSet(include, &r) {
+			included = append(included, r)
+		}
+	}
+
+	return &included
+}
+
+// Check whether an include/exclude string slice matches a resource set
+func matchesResourceSet(s *[]string, rs *context.ResourceSet) bool {
+	for _, r := range *s {
+		r = strings.TrimSuffix(r, "/")
+		if r == rs.Name || r == rs.Parent {
+			return true
+		}
+	}
+
+	return false
+}
+
+func templateFuncs(c *context.Context, rs *context.ResourceSet) template.FuncMap {
+	m := sprig.TxtFuncMap()
+	m["json"] = func(data interface{}) string {
+		b, _ := json.Marshal(data)
+		return string(b)
+	}
+	m["passLookup"] = GetFromPass
+	m["gitHEAD"] = func() (string, error) {
+		out, err := exec.Command("git", "-C", c.BaseDir, "rev-parse", "HEAD").Output()
+		if err != nil {
+			return "", err
+		}
+		output := strings.TrimSpace(string(out))
+		return output, nil
+	}
+	m["lookupIPAddr"] = GetIPsFromDNS
+	m["insertFile"] = func(file string) (string, error) {
+		data, err := ioutil.ReadFile(path.Join(rs.Path, file))
+		if err != nil {
+			return "", err
+		}
+
+		return string(data), nil
+	}
+	m["insertTemplate"] = func(file string) (string, error) {
+		data, err := templateFile(c, rs, path.Join(rs.Path, file))
+		if err != nil {
+			return "", err
+		}
+
+		return data.Rendered, nil
+	}
+	m["default"] = func(defaultVal interface{}, varName string) interface{} {
+		if val, ok := rs.Values[varName]; ok {
+			return val
+		}
+
+		return defaultVal
+	}
+	return m
+}
+
+// Checks whether a file is a resource file (i.e. is YAML or JSON) and not a default values file.
+func isResourceFile(f os.FileInfo) bool {
+	for _, defaultFile := range util.DefaultFilenames {
+		if f.Name() == defaultFile {
+			return false
+		}
+	}
+
+	return strings.HasSuffix(f.Name(), "yaml") ||
+		strings.HasSuffix(f.Name(), "yml") ||
+		strings.HasSuffix(f.Name(), "json")
+}
diff --git a/ops/kontemplate/templater/templater_test.go b/ops/kontemplate/templater/templater_test.go
new file mode 100644
index 0000000000..9d9fc8d1ff
--- /dev/null
+++ b/ops/kontemplate/templater/templater_test.go
@@ -0,0 +1,205 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package templater
+
+import (
+	"github.com/tazjin/kontemplate/context"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func TestApplyNoLimits(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name: "testResourceSet2",
+		},
+	}
+
+	result := applyLimits(&resources, &[]string{}, &[]string{})
+
+	if !reflect.DeepEqual(resources, *result) {
+		t.Error("Resource set slice changed, but shouldn't have.")
+		t.Errorf("Expected: %v\nResult: %v\n", resources, *result)
+		t.Fail()
+	}
+}
+
+func TestApplyIncludeLimits(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name: "testResourceSet2",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	includes := []string{"testResourceSet1", "included"}
+
+	result := applyLimits(&resources, &includes, &[]string{})
+
+	expected := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	if !reflect.DeepEqual(expected, *result) {
+		t.Error("Result does not contain expected resource sets.")
+		t.Errorf("Expected: %v\nResult: %v\n", expected, *result)
+		t.Fail()
+	}
+}
+
+func TestApplyExcludeLimits(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name: "testResourceSet2",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	exclude := []string{"testResourceSet2"}
+
+	result := applyLimits(&resources, &[]string{}, &exclude)
+
+	expected := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	if !reflect.DeepEqual(expected, *result) {
+		t.Error("Result does not contain expected resource sets.")
+		t.Errorf("Expected: %v\nResult: %v\n", expected, *result)
+		t.Fail()
+	}
+}
+
+func TestApplyLimitsExcludeIncludePrecedence(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name:   "collection/nested1",
+			Parent: "collection",
+		},
+		{
+			Name:   "collection/nested2",
+			Parent: "collection",
+		},
+		{
+			Name:   "collection/nested3",
+			Parent: "collection",
+		},
+		{
+			Name: "something-else",
+		},
+	}
+
+	include := []string{"collection"}
+	exclude := []string{"collection/nested2"}
+
+	result := applyLimits(&resources, &include, &exclude)
+
+	expected := []context.ResourceSet{
+		{
+			Name:   "collection/nested1",
+			Parent: "collection",
+		},
+		{
+			Name:   "collection/nested3",
+			Parent: "collection",
+		},
+	}
+
+	if !reflect.DeepEqual(expected, *result) {
+		t.Error("Result does not contain expected resource sets.")
+		t.Errorf("Expected: %v\nResult: %v\n", expected, *result)
+		t.Fail()
+	}
+}
+
+func TestFailOnMissingKeys(t *testing.T) {
+	ctx := context.Context{}
+	resourceSet := context.ResourceSet{}
+
+	_, err := templateFile(&ctx, &resourceSet, "testdata/test-template.txt")
+
+	if err == nil {
+		t.Errorf("Template with missing keys should have failed.\n")
+		t.Fail()
+	}
+
+	if !strings.Contains(err.Error(), "map has no entry for key \"testName\"") {
+		t.Errorf("Templating failed with unexpected error: %v\n", err)
+	}
+}
+
+func TestDefaultTemplateFunction(t *testing.T) {
+	ctx := context.Context{}
+	resourceSet := context.ResourceSet{}
+
+	res, err := templateFile(&ctx, &resourceSet, "testdata/test-default.txt")
+
+	if err != nil {
+		t.Errorf("Templating with default values should have succeeded.\n")
+		t.Fail()
+	}
+
+	if res.Rendered != "defaultValue\n" {
+		t.Error("Result does not contain expected rendered default value.")
+		t.Fail()
+	}
+}
+
+func TestInsertTemplateFunction(t *testing.T) {
+	ctx := context.Context{}
+	resourceSet := context.ResourceSet{
+		Path: "testdata",
+		Values: map[string]interface{}{
+			"testName": "TestInsertTemplateFunction",
+		},
+	}
+
+	res, err := templateFile(&ctx, &resourceSet, "testdata/test-insertTemplate.txt")
+
+	if err != nil {
+		t.Error(err)
+		t.Errorf("Templating with an insertTemplate call should have succeeded.\n")
+		t.Fail()
+	}
+
+	if res.Rendered != "Inserting \"Template for test TestInsertTemplateFunction\".\n" {
+		t.Error("Result does not contain expected rendered template value.")
+		t.Error(res.Rendered)
+		t.Fail()
+	}
+}
diff --git a/ops/kontemplate/templater/testdata/test-default.txt b/ops/kontemplate/templater/testdata/test-default.txt
new file mode 100644
index 0000000000..4f7997bd69
--- /dev/null
+++ b/ops/kontemplate/templater/testdata/test-default.txt
@@ -0,0 +1 @@
+{{ default "defaultValue" "missingVar" }}
diff --git a/ops/kontemplate/templater/testdata/test-insertTemplate.txt b/ops/kontemplate/templater/testdata/test-insertTemplate.txt
new file mode 100644
index 0000000000..8155e174fe
--- /dev/null
+++ b/ops/kontemplate/templater/testdata/test-insertTemplate.txt
@@ -0,0 +1 @@
+Inserting "{{ insertTemplate "test-template.txt" | trim }}".
diff --git a/ops/kontemplate/templater/testdata/test-template.txt b/ops/kontemplate/templater/testdata/test-template.txt
new file mode 100644
index 0000000000..06f1cfc630
--- /dev/null
+++ b/ops/kontemplate/templater/testdata/test-template.txt
@@ -0,0 +1 @@
+Template for test {{ .testName }}
diff --git a/ops/kontemplate/util/util.go b/ops/kontemplate/util/util.go
new file mode 100644
index 0000000000..56fa1e3fc9
--- /dev/null
+++ b/ops/kontemplate/util/util.go
@@ -0,0 +1,58 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package util
+
+import (
+	"io/ioutil"
+
+	"github.com/ghodss/yaml"
+)
+
+// Filenames excluded from templating for the purpose of containing default variable values inside a resource set.
+var DefaultFilenames []string = []string{"default.yml", "default.yaml", "default.json"}
+
+// Merges two maps together. Values from the second map override values in the first map.
+// The returned map is new if anything was changed.
+func Merge(in1 *map[string]interface{}, in2 *map[string]interface{}) *map[string]interface{} {
+	if in1 == nil || len(*in1) == 0 {
+		return in2
+	}
+
+	if in2 == nil || len(*in2) == 0 {
+		return in1
+	}
+
+	new := make(map[string]interface{})
+	for k, v := range *in1 {
+		new[k] = v
+	}
+
+	for k, v := range *in2 {
+		new[k] = v
+	}
+
+	return &new
+}
+
+// Loads either a YAML or JSON file from the specified path and
+// deserialises it into the provided interface.
+func LoadData(filename string, addr interface{}) error {
+	file, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return err
+	}
+
+	err = yaml.Unmarshal(file, addr)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/ops/kontemplate/util/util_test.go b/ops/kontemplate/util/util_test.go
new file mode 100644
index 0000000000..53c5608175
--- /dev/null
+++ b/ops/kontemplate/util/util_test.go
@@ -0,0 +1,83 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package util
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestMergeWithEmptyMap(t *testing.T) {
+	testMap := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	empty := make(map[string]interface{})
+
+	res1 := Merge(&testMap, &empty)
+	res2 := Merge(&empty, &testMap)
+
+	if res1 != &testMap || res2 != &testMap {
+		t.Error("A new map was returned incorrectly.")
+		t.Fail()
+	}
+}
+
+func TestMergeWithNilMap(t *testing.T) {
+	testMap := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	res1 := Merge(&testMap, nil)
+	res2 := Merge(nil, &testMap)
+
+	if res1 != &testMap || res2 != &testMap {
+		t.Error("A new map was returned incorrectly.")
+		t.Fail()
+	}
+}
+
+func TestMergeMaps(t *testing.T) {
+	map1 := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	map2 := map[string]interface{}{
+		"bar": "baz",
+	}
+
+	result := Merge(&map1, &map2)
+	expected := map[string]interface{}{
+		"foo": "bar",
+		"bar": "baz",
+	}
+
+	if !reflect.DeepEqual(*result, expected) {
+		t.Error("Maps were merged incorrectly.")
+		t.Fail()
+	}
+}
+
+func TestMergeMapsPrecedence(t *testing.T) {
+	map1 := map[string]interface{}{
+		"foo": "incorrect",
+	}
+
+	map2 := map[string]interface{}{
+		"foo": "correct",
+	}
+
+	result := Merge(&map1, &map2)
+
+	if (*result)["foo"] != "correct" {
+		t.Error("Map merge precedence test failed.")
+		t.Fail()
+	}
+}
diff --git a/ops/machines/all-systems.nix b/ops/machines/all-systems.nix
new file mode 100644
index 0000000000..e59187cabb
--- /dev/null
+++ b/ops/machines/all-systems.nix
@@ -0,0 +1,24 @@
+{ depot, ... }:
+
+(with depot.ops.machines; [
+  sanduny
+  whitby
+]) ++
+
+(with depot.users.tazjin.nixos; [
+  camden
+  frog
+  tverskoy
+  zamalek
+]) ++
+
+(with depot.users.grfn.system.system; [
+  yeren
+  mugwump
+]) ++
+
+(with depot.users.wpcarro.nixos; [
+  ava
+  marcus
+  tarasco
+])
diff --git a/ops/machines/sanduny/default.nix b/ops/machines/sanduny/default.nix
new file mode 100644
index 0000000000..5f30e08a75
--- /dev/null
+++ b/ops/machines/sanduny/default.nix
@@ -0,0 +1,111 @@
+# sanduny.tvl.su
+#
+# This is a VPS hosted with Bitfolk, intended to additionally serve
+# some of our public services like cgit, josh and the websites.
+#
+# In case of whitby going down, sanduny will keep depot available.
+
+_: # ignore readTree options
+
+{ config, depot, lib, pkgs, ... }:
+
+let
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+in
+{
+  imports = [
+    (mod "journaldriver.nix")
+    (mod "known-hosts.nix")
+    (mod "tvl-users.nix")
+    (mod "www/self-redirect.nix")
+  ];
+
+  networking = {
+    hostName = "sanduny";
+    domain = "tvl.su";
+    useDHCP = false;
+
+    interfaces.eth0 = {
+      ipv4.addresses = lib.singleton {
+        address = "85.119.82.231";
+        prefixLength = 21;
+      };
+
+      ipv6.addresses = lib.singleton {
+        address = "2001:ba8:1f1:f109::feed:edef:beef";
+        prefixLength = 64;
+      };
+    };
+
+    defaultGateway = "85.119.80.1";
+    defaultGateway6.address = "2001:ba8:1f1:f109::1";
+
+    firewall.allowedTCPPorts = [ 22 80 443 ];
+
+    # https://bitfolk.com/customer_information.html#toc_2_DNS
+    nameservers = [
+      "85.119.80.232"
+      "85.119.80.233"
+      "2001:ba8:1f1:f205::53"
+      "2001:ba8:1f1:f206::53"
+    ];
+  };
+
+  security.sudo.wheelNeedsPassword = false;
+
+  environment.systemPackages = with pkgs; [
+    emacs-nox
+    vim
+    curl
+    unzip
+    htop
+  ];
+
+  programs.mtr.enable = true;
+
+  services.openssh.enable = true;
+  services.fail2ban.enable = true;
+
+  # Automatically collect garbage from the Nix store.
+  services.depot.automatic-gc = {
+    enable = true;
+    interval = "1 hour";
+    diskThreshold = 2; # GiB
+    maxFreed = 5; # GiB
+    preserveGenerations = "90d";
+  };
+
+  time.timeZone = "UTC";
+
+  # GRUB does not actually need to be installed on disk; Bitfolk have
+  # their own way of booting systems as long as config is in place.
+  boot.loader.grub.device = "nodev";
+  boot.loader.grub.enable = true;
+  boot.loader.grub.version = 2;
+  boot.initrd.availableKernelModules = [ "xen_blkfront" ];
+
+  hardware.cpu.intel.updateMicrocode = true;
+
+  fileSystems = {
+    "/" = {
+      device = "/dev/disk/by-uuid/aabc3638-43ca-45f3-af89-c451e8448e92";
+      fsType = "ext4";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/75aa99d5-fed7-4c5c-8570-7745f6cff9f5";
+      fsType = "ext3";
+    };
+
+    "/nix" = {
+      device = "/dev/disk/by-uuid/d1721678-c294-482b-b72e-3b15f2c56c63";
+      fsType = "ext4";
+    };
+  };
+
+  swapDevices = lib.singleton {
+    device = "/dev/disk/by-uuid/df4ad9da-0a06-4c27-93e5-5d44e4750e55";
+  };
+
+  system.stateVersion = "22.05"; # Did you read the comment?
+}
diff --git a/ops/machines/whitby/OWNERS b/ops/machines/whitby/OWNERS
new file mode 100644
index 0000000000..b1b749e871
--- /dev/null
+++ b/ops/machines/whitby/OWNERS
@@ -0,0 +1,6 @@
+inherited: false
+
+# Want in on this list? Try paying!
+owners:
+  - lukegb
+  - tazjin
diff --git a/ops/machines/whitby/README.md b/ops/machines/whitby/README.md
new file mode 100644
index 0000000000..55287c5412
--- /dev/null
+++ b/ops/machines/whitby/README.md
@@ -0,0 +1,5 @@
+whitby
+======
+
+`whitby.tvl.fyi` is our dedicated server providing continuous
+integration services and other random nonsense.
diff --git a/ops/machines/whitby/default.nix b/ops/machines/whitby/default.nix
new file mode 100644
index 0000000000..fc5badd9d8
--- /dev/null
+++ b/ops/machines/whitby/default.nix
@@ -0,0 +1,653 @@
+{ depot, lib, pkgs, ... }: # readTree options
+{ config, ... }: # passed by module system
+
+let
+  inherit (builtins) listToAttrs;
+  inherit (lib) range;
+
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+in
+{
+  imports = [
+    (mod "atward.nix")
+    (mod "cgit/default.nix")
+    (mod "clbot.nix")
+    (mod "gerrit-queue.nix")
+    (mod "irccat.nix")
+    (mod "josh.nix")
+    (mod "journaldriver.nix")
+    (mod "known-hosts.nix")
+    (mod "monorepo-gerrit.nix")
+    (mod "nixery.nix")
+    (mod "oauth2_proxy.nix")
+    (mod "owothia.nix")
+    (mod "panettone.nix")
+    (mod "paroxysm.nix")
+    (mod "restic.nix")
+    (mod "smtprelay.nix")
+    (mod "sourcegraph.nix")
+    (mod "tvl-buildkite.nix")
+    (mod "tvl-slapd/default.nix")
+    (mod "tvl-users.nix")
+    (mod "www/atward.tvl.fyi.nix")
+    (mod "www/auth.tvl.fyi.nix")
+    (mod "www/b.tvl.fyi.nix")
+    (mod "www/cache.tvl.su.nix")
+    (mod "www/cl.tvl.fyi.nix")
+    (mod "www/code.tvl.fyi.nix")
+    (mod "www/cs.tvl.fyi.nix")
+    (mod "www/deploys.tvl.fyi.nix")
+    (mod "www/images.tvl.fyi.nix")
+    (mod "www/nixery.dev.nix")
+    (mod "www/self-redirect.nix")
+    (mod "www/static.tvl.fyi.nix")
+    (mod "www/status.tvl.su.nix")
+    (mod "www/tazj.in.nix")
+    (mod "www/todo.tvl.fyi.nix")
+    (mod "www/tvl.fyi.nix")
+    (mod "www/tvl.su.nix")
+    (mod "www/wigglydonke.rs.nix")
+
+    (depot.third_party.agenix.src + "/modules/age.nix")
+  ];
+
+  hardware = {
+    enableRedistributableFirmware = true;
+    cpu.amd.updateMicrocode = true;
+  };
+
+  boot = {
+    tmpOnTmpfs = true;
+    kernelModules = [ "kvm-amd" ];
+    supportedFilesystems = [ "zfs" ];
+
+    initrd = {
+      availableKernelModules = [
+        "igb"
+        "xhci_pci"
+        "nvme"
+        "ahci"
+        "usbhid"
+        "usb_storage"
+        "sr_mod"
+      ];
+
+      # Enable SSH in the initrd so that we can enter disk encryption
+      # passwords remotely.
+      network = {
+        enable = true;
+        ssh = {
+          enable = true;
+          port = 2222;
+          authorizedKeys =
+            depot.users.tazjin.keys.all
+            ++ depot.users.lukegb.keys.all
+            ++ [ depot.users.grfn.keys.whitby ];
+
+          hostKeys = [
+            /etc/secrets/initrd_host_ed25519_key
+          ];
+        };
+
+        # this will launch the zfs password prompt on login and kill the
+        # other prompt
+        postCommands = ''
+          echo "zfs load-key -a && killall zfs" >> /root/.profile
+        '';
+      };
+    };
+
+    kernel.sysctl = {
+      "net.ipv4.tcp_congestion_control" = "bbr";
+    };
+
+    loader.grub = {
+      enable = true;
+      version = 2;
+      efiSupport = true;
+      efiInstallAsRemovable = true;
+      device = "/dev/disk/by-id/nvme-SAMSUNG_MZQLB1T9HAJR-00007_S439NA0N201620";
+    };
+
+    zfs.requestEncryptionCredentials = true;
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "zroot/root";
+      fsType = "zfs";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/073E-7FBD";
+      fsType = "vfat";
+    };
+
+    "/nix" = {
+      device = "zroot/nix";
+      fsType = "zfs";
+    };
+
+    "/home" = {
+      device = "zroot/home";
+      fsType = "zfs";
+    };
+  };
+
+  networking = {
+    # Glass is boring, but Luke doesn't like Wapping - the Prospect of
+    # Whitby, however, is quite a pleasant establishment.
+    hostName = "whitby";
+    domain = "tvl.fyi";
+    hostId = "b38ca543";
+    useDHCP = false;
+
+    # Don't use Hetzner's DNS servers.
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+
+    defaultGateway6 = {
+      address = "fe80::1";
+      interface = "enp196s0";
+    };
+
+    firewall.allowedTCPPorts = [ 22 80 443 4238 8443 29418 ];
+    firewall.allowedUDPPorts = [ 8443 ];
+
+    interfaces.enp196s0.useDHCP = true;
+    interfaces.enp196s0.ipv6.addresses = [
+      {
+        address = "2a01:04f8:0242:5b21::feed:edef:beef";
+        prefixLength = 64;
+      }
+    ];
+  };
+
+  # Generate an immutable /etc/resolv.conf from the nameserver settings
+  # above (otherwise DHCP overwrites it):
+  environment.etc."resolv.conf" = with lib; {
+    source = pkgs.writeText "resolv.conf" ''
+      ${concatStringsSep "\n" (map (ns: "nameserver ${ns}") config.networking.nameservers)}
+      options edns0
+    '';
+  };
+
+  # Disable background git gc system-wide, as it has a tendency to break CI.
+  environment.etc."gitconfig".source = pkgs.writeText "gitconfig" ''
+    [gc]
+    autoDetach = false
+  '';
+
+  time.timeZone = "UTC";
+
+  nix = {
+    nrBuildUsers = 256;
+    maxJobs = lib.mkDefault 64;
+    extraOptions = ''
+      secret-key-files = /run/agenix/nix-cache-priv
+    '';
+
+    trustedUsers = [
+      "grfn"
+      "lukegb"
+      "tazjin"
+      "sterni"
+    ];
+
+    sshServe = {
+      enable = true;
+      keys = with depot.users;
+        tazjin.keys.all
+        ++ lukegb.keys.all
+        ++ [ grfn.keys.whitby ]
+        ++ sterni.keys.all
+      ;
+    };
+  };
+
+  programs.mtr.enable = true;
+  programs.mosh.enable = true;
+  services.openssh = {
+    enable = true;
+    passwordAuthentication = false;
+    challengeResponseAuthentication = false;
+  };
+
+  # Configure secrets for services that need them.
+  age.secrets =
+    let
+      secretFile = name: depot.ops.secrets."${name}.age";
+    in
+    {
+      clbot.file = secretFile "clbot";
+      gerrit-queue.file = secretFile "gerrit-queue";
+      grafana.file = secretFile "grafana";
+      irccat.file = secretFile "irccat";
+      keycloak-db.file = secretFile "keycloak-db";
+      nix-cache-priv.file = secretFile "nix-cache-priv";
+      oauth2_proxy.file = secretFile "oauth2_proxy";
+      owothia.file = secretFile "owothia";
+      panettone.file = secretFile "panettone";
+      smtprelay.file = secretFile "smtprelay";
+
+      buildkite-agent-token = {
+        file = secretFile "buildkite-agent-token";
+        mode = "0440";
+        group = "buildkite-agents";
+      };
+
+      buildkite-graphql-token = {
+        file = secretFile "buildkite-graphql-token";
+        mode = "0440";
+        group = "buildkite-agents";
+      };
+
+      buildkite-besadii-config = {
+        file = secretFile "besadii";
+        mode = "0440";
+        group = "buildkite-agents";
+      };
+
+      buildkite-private-key = {
+        file = secretFile "buildkite-ssh-private-key";
+        mode = "0440";
+        group = "buildkite-agents";
+      };
+
+      gerrit-besadii-config = {
+        file = secretFile "besadii";
+        owner = "git";
+      };
+
+      gerrit-secrets = {
+        file = secretFile "gerrit-secrets";
+        path = "/var/lib/gerrit/etc/secure.config";
+        owner = "git";
+        mode = "0400";
+      };
+
+      clbot-ssh = {
+        file = secretFile "clbot-ssh";
+        owner = "clbot";
+      };
+
+      # Not actually a secret
+      nix-cache-pub = {
+        file = secretFile "nix-cache-pub";
+        mode = "0444";
+      };
+    };
+
+  # Automatically collect garbage from the Nix store.
+  services.depot.automatic-gc = {
+    enable = true;
+    interval = "1 hour";
+    diskThreshold = 200; # GiB
+    maxFreed = 420; # GiB
+    preserveGenerations = "90d";
+  };
+
+  # Run a handful of Buildkite agents to support parallel builds.
+  services.depot.buildkite = {
+    enable = true;
+    agentCount = 32;
+  };
+
+  # Start a local SMTP relay to Gmail (used by gerrit)
+  services.depot.smtprelay = {
+    enable = true;
+    args = {
+      listen = ":2525";
+      remote_host = "smtp.gmail.com:587";
+      remote_auth = "plain";
+      remote_user = "tvlbot@tazj.in";
+    };
+  };
+
+  # Start a ZNC instance which bounces for tvlbot and owothia.
+  services.znc = {
+    enable = true;
+    useLegacyConfig = false;
+    config = {
+      LoadModule = [
+        "webadmin"
+        "adminlog"
+      ];
+
+      User.admin = {
+        Admin = true;
+        Pass.password = {
+          Method = "sha256";
+          Hash = "bb00aa8239de484c2925b1c3f6a196fb7612633f001daa9b674f83abe7e1103f";
+          Salt = "TiB0Ochb1CrtpMTl;2;j";
+        };
+      };
+
+      Listener.l = {
+        Host = "localhost";
+        Port = 2627; # bncr
+        SSL = false;
+      };
+    };
+  };
+
+  # Start the Gerrit->IRC bot
+  services.depot.clbot = {
+    enable = true;
+    channels = [ "#tvl" ];
+
+    # See //fun/clbot for details.
+    flags = {
+      gerrit_host = "cl.tvl.fyi:29418";
+      gerrit_ssh_auth_username = "clbot";
+      gerrit_ssh_auth_key = config.age.secretsDir + "/clbot-ssh";
+
+      irc_server = "localhost:${toString config.services.znc.config.Listener.l.Port}";
+      irc_user = "tvlbot";
+      irc_nick = "tvlbot";
+
+      notify_branches = "canon,refs/meta/config";
+      notify_repo = "depot";
+
+      # This secret is read from an environment variable, which is
+      # populated by a systemd EnvironmentFile.
+      irc_pass = "$CLBOT_PASS";
+    };
+  };
+
+  services.depot = {
+    # Run a SourceGraph code search instance
+    sourcegraph.enable = true;
+
+    # Run the Panettone issue tracker
+    panettone = {
+      enable = true;
+      dbUser = "panettone";
+      dbName = "panettone";
+      irccatChannel = "#tvl";
+    };
+
+    # Run the first cursed bot (quote bot)
+    paroxysm.enable = true;
+
+    # Run the second cursed bot
+    owothia = {
+      enable = true;
+      ircServer = "localhost";
+      ircPort = config.services.znc.config.Listener.l.Port;
+    };
+
+    # Run irccat to forward messages to IRC
+    irccat = {
+      enable = true;
+      config = {
+        tcp.listen = ":4722"; # "ircc"
+        irc = {
+          server = "localhost:${toString config.services.znc.config.Listener.l.Port}";
+          tls = false;
+          nick = "tvlbot";
+          # Note: irccat means 'ident' where it says 'realname', so
+          # this is critical for connecting to ZNC.
+          realname = "tvlbot";
+          channels = [
+            "#tvl"
+          ];
+        };
+      };
+    };
+
+    # Run atward, the search engine redirection thing.
+    atward.enable = true;
+
+    # Run a Nixery instance
+    nixery.enable = true;
+
+    # Run cgit & josh to serve git
+    cgit.enable = true;
+    josh.enable = true;
+
+    # Configure backups to GleSYS
+    restic = {
+      enable = true;
+      paths = [
+        "/var/backup/postgresql"
+        "/var/lib/grafana"
+        "/var/lib/znc"
+      ];
+    };
+
+    # Run autosubmit bot for Gerrit
+    gerrit-queue.enable = true;
+
+    # Run oauth2_proxy for internal service auth
+    oauth2_proxy.enable = true;
+  };
+
+  services.postgresql = {
+    enable = true;
+    enableTCPIP = true;
+
+    authentication = lib.mkForce ''
+      local all all trust
+      host all all 127.0.0.1/32 password
+      host all all ::1/128 password
+      hostnossl all all 127.0.0.1/32 password
+      hostnossl all all ::1/128  password
+    '';
+
+    ensureDatabases = [
+      "panettone"
+    ];
+
+    ensureUsers = [{
+      name = "panettone";
+      ensurePermissions = {
+        "DATABASE panettone" = "ALL PRIVILEGES";
+      };
+    }];
+  };
+
+  services.postgresqlBackup = {
+    enable = true;
+    databases = [
+      "keycloak"
+      "panettone"
+      "tvldb"
+    ];
+  };
+
+  services.nix-serve = {
+    enable = true;
+    port = 6443;
+    secretKeyFile = config.age.secretsDir + "/nix-cache-priv";
+    bindAddress = "localhost";
+  };
+
+  services.fail2ban.enable = true;
+
+  environment.systemPackages = (with pkgs; [
+    bat
+    bb
+    curl
+    direnv
+    emacs-nox
+    fd
+    git
+    htop
+    hyperfine
+    jq
+    nano
+    nvd
+    ripgrep
+    tree
+    unzip
+    vim
+    zfs
+    zfstools
+  ]) ++ (with depot; [
+    ops.deploy-whitby
+  ]);
+
+  # Required for prometheus to be able to scrape stats
+  services.nginx.statusPage = true;
+
+  # Configure Prometheus & Grafana. Exporter configuration for
+  # Prometheus is inside the respective service modules.
+  services.prometheus = {
+    enable = true;
+    retentionTime = "90d";
+
+    exporters = {
+      node = {
+        enable = true;
+
+        enabledCollectors = [
+          "logind"
+          "processes"
+          "systemd"
+        ];
+      };
+
+      nginx = {
+        enable = true;
+        sslVerify = false;
+        constLabels = [ "host=whitby" ];
+      };
+    };
+
+    scrapeConfigs = [{
+      job_name = "node";
+      scrape_interval = "5s";
+      static_configs = [{
+        targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
+      }];
+    }
+      {
+        job_name = "nginx";
+        scrape_interval = "5s";
+        static_configs = [{
+          targets = [ "localhost:${toString config.services.prometheus.exporters.nginx.port}" ];
+        }];
+      }];
+  };
+
+  services.grafana = {
+    enable = true;
+    port = 4723; # "graf" on phone keyboard
+    domain = "status.tvl.su";
+    rootUrl = "https://status.tvl.su";
+    analytics.reporting.enable = false;
+    extraOptions =
+      let
+        options = {
+          auth = {
+            generic_oauth = {
+              enabled = true;
+              client_id = "grafana";
+              scopes = "openid profile email";
+              name = "TVL";
+              email_attribute_path = "mail";
+              login_attribute_path = "sub";
+              name_attribute_path = "displayName";
+              auth_url = "https://auth.tvl.fyi/auth/realms/TVL/protocol/openid-connect/auth";
+              token_url = "https://auth.tvl.fyi/auth/realms/TVL/protocol/openid-connect/token";
+              api_url = "https://auth.tvl.fyi/auth/realms/TVL/protocol/openid-connect/userinfo";
+
+              # Give lukegb, grfn, tazjin "Admin" rights.
+              role_attribute_path = "((sub == 'lukegb' || sub == 'grfn' || sub == 'tazjin') && 'Admin') || 'Editor'";
+
+              # Allow creating new Grafana accounts from OAuth accounts.
+              allow_sign_up = true;
+            };
+
+            anonymous = {
+              enabled = true;
+              org_name = "The Virus Lounge";
+              org_role = "Viewer";
+            };
+
+            basic.enabled = false;
+            oauth_auto_login = true;
+            disable_login_form = true;
+          };
+        };
+        inherit (builtins) typeOf replaceStrings listToAttrs concatLists;
+        inherit (lib) toUpper mapAttrsToList nameValuePair concatStringsSep;
+
+        # Take ["auth" "generic_oauth" "enabled"] and turn it into OPTIONS_GENERIC_OAUTH_ENABLED.
+        encodeName = raw: replaceStrings [ "." ] [ "_" ] (toUpper (concatStringsSep "_" raw));
+
+        # Turn an option value into a string, but we want bools to be sensible strings and not "1" or "".
+        optionToString = value:
+          if (typeOf value) == "bool" then
+            if value then "true" else "false"
+          else builtins.toString value;
+
+        # Turn an nested options attrset into a flat listToAttrs-compatible list.
+        encodeOptions = prefix: inp: concatLists (mapAttrsToList
+          (name: value:
+            if (typeOf value) == "set"
+            then encodeOptions (prefix ++ [ name ]) value
+            else [ (nameValuePair (encodeName (prefix ++ [ name ])) (optionToString value)) ]
+          )
+          inp);
+      in
+      listToAttrs (encodeOptions [ ] options);
+
+    provision = {
+      enable = true;
+      datasources = [{
+        name = "Prometheus";
+        type = "prometheus";
+        url = "http://localhost:9090";
+      }];
+    };
+  };
+
+  # Contains GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET.
+  systemd.services.grafana.serviceConfig.EnvironmentFile = config.age.secretsDir + "/grafana";
+
+  services.keycloak = {
+    enable = true;
+    httpPort = "5925"; # "kycl"
+
+    settings = {
+      hostname = "auth.tvl.fyi";
+      http-relative-path = "/auth";
+      proxy = "edge";
+    };
+
+    database = {
+      type = "postgresql";
+      passwordFile = config.age.secretsDir + "/keycloak-db";
+      createLocally = false;
+    };
+  };
+
+  # Allow Keycloak access to the LDAP module by forcing in the JVM
+  # configuration
+  systemd.services.keycloak.environment.PREPEND_JAVA_OPTS =
+    "--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED";
+
+  security.sudo.extraRules = [
+    {
+      groups = [ "wheel" ];
+      commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
+    }
+  ];
+
+  users = {
+    # Set up a user & group for git shenanigans
+    groups.git = { };
+    users.git = {
+      group = "git";
+      isSystemUser = true;
+      createHome = true;
+      home = "/var/lib/git";
+    };
+  };
+
+  system.stateVersion = "20.03";
+}
diff --git a/ops/modules/.skip-subtree b/ops/modules/.skip-subtree
new file mode 100644
index 0000000000..09520f8c83
--- /dev/null
+++ b/ops/modules/.skip-subtree
@@ -0,0 +1 @@
+NixOS modules are not readTree compatible.
diff --git a/ops/modules/README.md b/ops/modules/README.md
new file mode 100644
index 0000000000..595b4c3344
--- /dev/null
+++ b/ops/modules/README.md
@@ -0,0 +1,7 @@
+NixOS modules
+=============
+
+This folder contains various NixOS modules shared by our NixOS
+configurations.
+
+It is not read by `readTree`.
diff --git a/ops/modules/atward.nix b/ops/modules/atward.nix
new file mode 100644
index 0000000000..f345a08e31
--- /dev/null
+++ b/ops/modules/atward.nix
@@ -0,0 +1,38 @@
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.atward;
+  description = "atward - (attempt to) cleverly route queries";
+in
+{
+  options.services.depot.atward = {
+    enable = lib.mkEnableOption description;
+
+    host = lib.mkOption {
+      type = lib.types.str;
+      default = "[::1]";
+      description = "Host on which atward should listen";
+    };
+
+    port = lib.mkOption {
+      type = lib.types.int;
+      default = 28973;
+      description = "Port on which atward should listen";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.atward = {
+      inherit description;
+      script = "${depot.web.atward}/bin/atward";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+      };
+
+      environment.ATWARD_LISTEN_ADDRESS = "${cfg.host}:${toString cfg.port}";
+    };
+  };
+}
diff --git a/ops/modules/auto-deploy.nix b/ops/modules/auto-deploy.nix
new file mode 100644
index 0000000000..c504906b2b
--- /dev/null
+++ b/ops/modules/auto-deploy.nix
@@ -0,0 +1,104 @@
+# Defines a service for automatically and periodically calling depot's
+# rebuild-system on a NixOS machine.
+#
+# Deploys can be stopped in emergency situations by creating an empty
+# file called `stop` in the state directory of the auto-deploy service
+# (typically /var/lib/auto-deploy).
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.auto-deploy;
+  description = "to automatically rebuild the current system's NixOS config from the latest checkout of depot";
+
+  rebuild-system = depot.ops.nixos.rebuildSystemWith "$STATE_DIRECTORY/deploy";
+  deployScript = pkgs.writeShellScript "auto-deploy" ''
+    set -ueo pipefail
+
+    if [[ $EUID -ne 0 ]]; then
+      echo "Oh no! Only root is allowed to run auto-deploy!" >&2
+      exit 1
+    fi
+
+    if [[ -f $STATE_DIRECTORY/stop ]]; then
+      echo "stop file exists in $STATE_DIRECTORY, not deploying!" >&2
+      exit 1
+    fi
+
+    readonly depot=$STATE_DIRECTORY/depot.git
+    readonly deploy=$STATE_DIRECTORY/deploy
+    readonly git="git -C $depot"
+
+    # find-or-create depot
+    if [ ! -d $depot ]; then
+      # cannot use $git here because $depot doesn't exist
+      git clone --bare ${cfg.git-remote} $depot
+    fi
+
+    function cleanup() {
+      $git worktree remove $deploy
+    }
+    trap cleanup EXIT
+
+    $git fetch origin
+    $git worktree add --force $deploy FETCH_HEAD
+    # unsure why, but without this switch-to-configuration attempts to install
+    # NixOS in $STATE_DIRECTORY
+    (cd / && ${rebuild-system}/bin/rebuild-system)
+  '';
+in
+{
+  options.services.depot.auto-deploy = {
+    enable = lib.mkEnableOption description;
+
+    git-remote = lib.mkOption {
+      type = lib.types.str;
+      default = "https://cl.tvl.fyi/depot.git";
+      description = ''
+        The (possibly remote) repository from which to clone as specified by the
+        GIT URLS section of `man git-clone`.
+      '';
+    };
+
+    interval = lib.mkOption {
+      type = lib.types.str;
+      example = "1h";
+      description = ''
+        Interval between Nix builds, specified in systemd.time(7) format.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.auto-deploy = {
+      inherit description;
+      script = "${deployScript}";
+      path = with pkgs; [
+        bash
+        git
+        gnutar
+        gzip
+      ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+
+      # We need to prevent NixOS from interrupting us while it attempts to
+      # restart systemd units.
+      restartIfChanged = false;
+
+      serviceConfig = {
+        Type = "oneshot";
+        StateDirectory = "auto-deploy";
+      };
+    };
+
+    systemd.timers.auto-deploy = {
+      inherit description;
+      wantedBy = [ "multi-user.target" ];
+
+      timerConfig = {
+        OnActiveSec = "1";
+        OnUnitActiveSec = cfg.interval;
+      };
+    };
+  };
+}
diff --git a/ops/modules/automatic-gc.nix b/ops/modules/automatic-gc.nix
new file mode 100644
index 0000000000..ad53a63f7f
--- /dev/null
+++ b/ops/modules/automatic-gc.nix
@@ -0,0 +1,92 @@
+# Defines a service for automatically collecting Nix garbage
+# periodically, without relying on the (ostensibly broken) Nix options
+# for min/max space available.
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.automatic-gc;
+  description = "Automatically collect Nix garbage";
+
+  GiBtoKiB = n: n * 1024 * 1024;
+  GiBtoBytes = n: n * 1024 * 1024 * 1024;
+
+  gcScript = pkgs.writeShellScript "automatic-nix-gc" ''
+    set -ueo pipefail
+
+    readonly MIN_THRESHOLD_KIB="${toString (GiBtoKiB cfg.diskThreshold)}"
+    readonly MAX_FREED_BYTES="${toString (GiBtoBytes cfg.maxFreed)}"
+    readonly GEN_THRESHOLD="${cfg.preserveGenerations}"
+    readonly AVAILABLE_KIB=$(df --sync /nix --output=avail | tail -n1)
+
+    if [ "''${AVAILABLE_KIB}" -lt "''${MIN_THRESHOLD_KIB}" ]; then
+      echo "Have ''${AVAILABLE_KIB} KiB, but want ''${MIN_THRESHOLD_KIB} KiB."
+      echo "Triggering Nix garbage collection up to ''${MAX_FREED_BYTES} bytes."
+      set -x
+      ${config.nix.package}/bin/nix-collect-garbage \
+        --delete-older-than "''${GEN_THRESHOLD}" \
+        --max-freed "''${MAX_FREED_BYTES}"
+    else
+      echo "Skipping GC, enough space available"
+    fi
+  '';
+in
+{
+  options.services.depot.automatic-gc = {
+    enable = lib.mkEnableOption description;
+
+    interval = lib.mkOption {
+      type = lib.types.str;
+      example = "1h";
+      description = ''
+        Interval between garbage collection runs, specified in
+        systemd.time(7) format.
+      '';
+    };
+
+    diskThreshold = lib.mkOption {
+      type = lib.types.int;
+      example = "100";
+      description = ''
+        Minimum amount of space that needs to be available (in GiB) on
+        the partition holding /nix. Garbage collection is triggered if
+        it falls below this.
+      '';
+    };
+
+    maxFreed = lib.mkOption {
+      type = lib.types.int;
+      example = "420";
+      description = ''
+        Maximum amount of space to free in a single GC run, in GiB.
+      '';
+    };
+
+    preserveGenerations = lib.mkOption {
+      type = lib.types.str;
+      default = "90d";
+      description = ''
+        Preserve NixOS generations younger than the specified value,
+        in the format expected by nix-collect-garbage(1).
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.automatic-gc = {
+      inherit description;
+      script = "${gcScript}";
+      serviceConfig.Type = "oneshot";
+    };
+
+    systemd.timers.automatic-gc = {
+      inherit description;
+      requisite = [ "nix-daemon.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      timerConfig = {
+        OnActiveSec = "1";
+        OnUnitActiveSec = cfg.interval;
+      };
+    };
+  };
+}
diff --git a/ops/modules/cgit/default.nix b/ops/modules/cgit/default.nix
new file mode 100644
index 0000000000..580b8384bd
--- /dev/null
+++ b/ops/modules/cgit/default.nix
@@ -0,0 +1,92 @@
+# Configuration for running the TVL cgit instance using thttpd.
+{ config, depot, lib, pkgs, ... }:
+
+let
+  inherit (pkgs) writeText;
+
+  cfg = config.services.depot.cgit;
+
+  cgitConfig = writeText "cgitrc" ''
+    # Global configuration
+    virtual-root=/
+    enable-http-clone=0
+    readme=:README.md
+    about-filter=${depot.tools.cheddar.about-filter}/bin/cheddar-about
+    source-filter=${depot.tools.cheddar}/bin/cheddar
+    enable-log-filecount=1
+    enable-log-linecount=1
+    enable-follow-links=1
+    enable-blame=1
+    mimetype-file=${pkgs.mime-types}/etc/mime.types
+    logo=https://static.tvl.fyi/${depot.web.static.drvHash}/logo-animated.svg
+
+    # Repository configuration
+    repo.url=depot
+    repo.path=/var/lib/gerrit/git/depot.git/
+    repo.desc=monorepo for the virus lounge
+    repo.owner=The Virus Lounge
+    repo.clone-url=https://code.tvl.fyi/depot.git
+  '';
+
+  thttpdConfig = writeText "thttpd.conf" ''
+    port=${toString cfg.port}
+    dir=${depot.third_party.cgit}/cgit
+    nochroot
+    novhost
+    cgipat=**.cgi
+  '';
+
+  # Patched version of thttpd that serves cgit.cgi as the index and
+  # sets the environment variable for pointing cgit at the correct
+  # configuration.
+  #
+  # Things are done this way because recompilation of thttpd is much
+  # faster than cgit.
+  thttpdConfigPatch = writeText "thttpd_cgit_conf.patch" ''
+    diff --git a/libhttpd.c b/libhttpd.c
+    index c6b1622..eef4b73 100644
+    --- a/libhttpd.c
+    +++ b/libhttpd.c
+    @@ -3055,4 +3055,6 @@ make_envp( httpd_conn* hc )
+
+         envn = 0;
+    +    // force cgit to load the correct configuration
+    +    envp[envn++] = "CGIT_CONFIG=${cgitConfig}";
+         envp[envn++] = build_env( "PATH=%s", CGI_PATH );
+     #ifdef CGI_LD_LIBRARY_PATH
+  '';
+
+  thttpdCgit = pkgs.thttpd.overrideAttrs (old: {
+    patches = [
+      ./thttpd_cgi_idx.patch
+      thttpdConfigPatch
+    ];
+  });
+in
+{
+  options.services.depot.cgit = with lib; {
+    enable = mkEnableOption "Run cgit web interface for depot";
+
+    port = mkOption {
+      description = "Port on which cgit should listen";
+      type = types.int;
+      default = 2448;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.cgit = {
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+        User = "git";
+        Group = "git";
+
+        ExecStart = pkgs.writeShellScript "cgit-launch" ''
+          exec ${thttpdCgit}/bin/thttpd -D -C ${thttpdConfig}
+        '';
+      };
+    };
+  };
+}
diff --git a/ops/modules/cgit/thttpd_cgi_idx.patch b/ops/modules/cgit/thttpd_cgi_idx.patch
new file mode 100644
index 0000000000..67dbc0c7ab
--- /dev/null
+++ b/ops/modules/cgit/thttpd_cgi_idx.patch
@@ -0,0 +1,13 @@
+diff --git a/config.h b/config.h
+index 65ab1e3..cde470f 100644
+--- a/config.h
++++ b/config.h
+@@ -327,7 +327,7 @@
+ /* CONFIGURE: A list of index filenames to check.  The files are searched
+ ** for in this order.
+ */
+-#define INDEX_NAMES "index.html", "index.htm", "index.xhtml", "index.xht", "Default.htm", "index.cgi"
++#define INDEX_NAMES "cgit.cgi"
+ 
+ /* CONFIGURE: If this is defined then thttpd will automatically generate
+ ** index pages for directories that don't have an explicit index file.
diff --git a/ops/modules/clbot.nix b/ops/modules/clbot.nix
new file mode 100644
index 0000000000..84575ed072
--- /dev/null
+++ b/ops/modules/clbot.nix
@@ -0,0 +1,82 @@
+# Module that configures CLBot, our Gerrit->IRC info bridge.
+{ depot, config, lib, pkgs, ... }:
+
+let
+  inherit (builtins) attrValues concatStringsSep mapAttrs readFile;
+  inherit (pkgs) runCommandNoCC;
+
+  inherit (lib)
+    listToAttrs
+    mkEnableOption
+    mkIf
+    mkOption
+    removeSuffix
+    types;
+
+  description = "Bot to forward CL notifications";
+  cfg = config.services.depot.clbot;
+
+  mkFlags = flags:
+    concatStringsSep " "
+      (attrValues (mapAttrs (key: value: "-${key} \"${toString value}\"") flags));
+
+  # Escapes a unit name for use in systemd
+  systemdEscape = name: removeSuffix "\n" (readFile (runCommandNoCC "unit-name" { } ''
+    ${pkgs.systemd}/bin/systemd-escape '${name}' >> $out
+  ''));
+
+  mkUnit = flags: channel: {
+    name = "clbot-${systemdEscape channel}";
+    value = {
+      description = "${description} to ${channel}";
+      wantedBy = [ "multi-user.target" ];
+
+      script = "${depot.fun.clbot}/bin/clbot ${mkFlags (cfg.flags // {
+        irc_channel = channel;
+      })} -alsologtostderr";
+
+      serviceConfig = {
+        User = "clbot";
+        EnvironmentFile = cfg.secretsFile;
+        Restart = "always";
+      };
+    };
+  };
+in
+{
+  options.services.depot.clbot = {
+    enable = mkEnableOption description;
+
+    flags = mkOption {
+      type = types.attrsOf types.str;
+      description = "Key value pairs for command line flags";
+    };
+
+    channels = mkOption {
+      type = with types; listOf str;
+      description = "Channels in which to post (generates one unit per channel)";
+    };
+
+    secretsFile = mkOption {
+      type = types.str;
+      description = "EnvironmentFile from which to load secrets";
+      default = config.age.secretsDir + "/clbot";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # This does not use DynamicUser because we need to make some files
+    # (notably the SSH private key) readable by this user outside of
+    # the module.
+    users = {
+      groups.clbot = { };
+
+      users.clbot = {
+        group = "clbot";
+        isSystemUser = true;
+      };
+    };
+
+    systemd.services = listToAttrs (map (mkUnit cfg.flags) cfg.channels);
+  };
+}
diff --git a/ops/modules/default-imports.nix b/ops/modules/default-imports.nix
new file mode 100644
index 0000000000..11514a437a
--- /dev/null
+++ b/ops/modules/default-imports.nix
@@ -0,0 +1,14 @@
+{ depot, ... }:
+
+# Default set of modules that are imported in all Depot nixos systems
+#
+# All modules here should be properly gated behind a `lib.mkEnableOption` with a
+# `lib.mkIf` for the config.
+
+{
+  imports = [
+    ./automatic-gc.nix
+    ./auto-deploy.nix
+    ./tvl-cache.nix
+  ];
+}
diff --git a/ops/modules/default.nix b/ops/modules/default.nix
new file mode 100644
index 0000000000..d747e8e131
--- /dev/null
+++ b/ops/modules/default.nix
@@ -0,0 +1,2 @@
+# Make readTree happy at this level.
+_: { }
diff --git a/ops/modules/gerrit-queue.nix b/ops/modules/gerrit-queue.nix
new file mode 100644
index 0000000000..4468bcf1c5
--- /dev/null
+++ b/ops/modules/gerrit-queue.nix
@@ -0,0 +1,52 @@
+# Configuration for the Gerrit autosubmit bot (//third_party/gerrit-queue)
+{ depot, pkgs, config, lib, ... }:
+
+let
+  cfg = config.services.depot.gerrit-queue;
+  description = "gerrit-queue - autosubmit bot for Gerrit";
+  mkStringOption = default: lib.mkOption {
+    inherit default;
+    type = lib.types.str;
+  };
+in
+{
+  options.services.depot.gerrit-queue = {
+    enable = lib.mkEnableOption description;
+    gerritUrl = mkStringOption "https://cl.tvl.fyi";
+    gerritProject = mkStringOption "depot";
+    gerritBranch = mkStringOption "canon";
+
+    interval = with lib; mkOption {
+      type = types.int;
+      default = 60;
+      description = "Interval (in seconds) for submit queue checks";
+    };
+
+    secretsFile = with lib; mkOption {
+      description = "Path to a systemd EnvironmentFile containing secrets";
+      default = config.age.secretsDir + "/gerrit-queue";
+      type = types.str;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.gerrit-queue = {
+      inherit description;
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${depot.third_party.gerrit-queue}/bin/gerrit-queue";
+        DynamicUser = true;
+        Restart = "always";
+        EnvironmentFile = cfg.secretsFile;
+      };
+
+      environment = {
+        GERRIT_URL = cfg.gerritUrl;
+        GERRIT_PROJECT = cfg.gerritProject;
+        GERRIT_BRANCH = cfg.gerritBranch;
+        SUBMIT_QUEUE_TRIGGER_INTERVAL = toString cfg.interval;
+      };
+    };
+  };
+}
diff --git a/ops/modules/irccat.nix b/ops/modules/irccat.nix
new file mode 100644
index 0000000000..0819c52a8d
--- /dev/null
+++ b/ops/modules/irccat.nix
@@ -0,0 +1,62 @@
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.irccat;
+  description = "irccat - forward messages to IRC";
+
+  # irccat expects to read its configuration from the *current
+  # directory*, and its configuration contains secrets.
+  #
+  # To make this work we construct the JSON configuration file and
+  # then recursively merge it with an on-disk secret using jq on
+  # service launch.
+  configJson = pkgs.writeText "irccat.json" (builtins.toJSON cfg.config);
+
+  # Right now, merging configuration file with secrets and running the main
+  # application needs to happen both in ExecStart=, due to
+  # https://github.com/systemd/systemd/issues/19604#issuecomment-989279884
+  mergeAndLaunch = pkgs.writeShellScript "merge-irccat-config" ''
+    if [ ! -f "$CREDENTIALS_DIRECTORY/secrets" ]; then
+      echo "irccat secrets file is missing"
+      exit 1
+    fi
+
+    # jq's * is the recursive merge operator
+    ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${configJson} "$CREDENTIALS_DIRECTORY/secrets" \
+      > /var/lib/irccat/irccat.json
+
+    exec ${depot.third_party.irccat}/bin/irccat
+  '';
+in
+{
+  options.services.depot.irccat = {
+    enable = lib.mkEnableOption description;
+
+    config = lib.mkOption {
+      type = lib.types.attrs; # varying value types
+      description = "Configuration structure (unchecked!)";
+    };
+
+    secretsFile = lib.mkOption {
+      type = lib.types.str;
+      description = "Path to the secrets file to be merged";
+      default = config.age.secretsDir + "/irccat";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.irccat = {
+      inherit description;
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${mergeAndLaunch}";
+        DynamicUser = true;
+        StateDirectory = "irccat";
+        WorkingDirectory = "/var/lib/irccat";
+        LoadCredential = "secrets:${cfg.secretsFile}";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/ops/modules/josh.nix b/ops/modules/josh.nix
new file mode 100644
index 0000000000..be9e9e966e
--- /dev/null
+++ b/ops/modules/josh.nix
@@ -0,0 +1,33 @@
+# Configures the public josh instance for serving the depot.
+{ config, depot, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.josh;
+in
+{
+  options.services.depot.josh = with lib; {
+    enable = mkEnableOption "Enable josh for serving the depot";
+
+    port = mkOption {
+      description = "Port on which josh should listen";
+      type = types.int;
+      default = 5674;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Run josh for the depot.
+    systemd.services.josh = {
+      description = "josh - partial cloning of monorepos";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.git pkgs.bash ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "josh";
+        Restart = "always";
+        ExecStart = "${depot.third_party.josh}/bin/josh-proxy --no-background --local /var/lib/josh --port ${toString cfg.port} --remote https://cl.tvl.fyi/";
+      };
+    };
+  };
+}
diff --git a/ops/modules/journaldriver.nix b/ops/modules/journaldriver.nix
new file mode 100644
index 0000000000..0d6b0bcc7f
--- /dev/null
+++ b/ops/modules/journaldriver.nix
@@ -0,0 +1,26 @@
+# Configures journaldriver to forward to the tvl-fyi GCP project from
+# TVL machines.
+{ config, depot, lib, pkgs, ... }:
+
+{
+  imports = [
+    (depot.third_party.agenix.src + "/modules/age.nix")
+  ];
+
+  age.secrets.journaldriver.file = depot.ops.secrets."journaldriver.age";
+
+  services.journaldriver = {
+    enable = true;
+    googleCloudProject = "tvl-fyi";
+    logStream = config.networking.hostName;
+  };
+
+  # Override the systemd service defined in the nixpkgs module to use
+  # the credentials provided by agenix.
+  systemd.services.journaldriver = {
+    serviceConfig = {
+      LoadCredential = "journaldriver.json:/run/agenix/journaldriver";
+      ExecStart = lib.mkForce "${pkgs.coreutils}/bin/env GOOGLE_APPLICATION_CREDENTIALS=\"\${CREDENTIALS_DIRECTORY}/journaldriver.json\" ${depot.ops.journaldriver}/bin/journaldriver";
+    };
+  };
+}
diff --git a/ops/modules/known-hosts.nix b/ops/modules/known-hosts.nix
new file mode 100644
index 0000000000..9ea689178e
--- /dev/null
+++ b/ops/modules/known-hosts.nix
@@ -0,0 +1,21 @@
+# Configure public keys for SSH hosts known to TVL.
+{ ... }:
+
+{
+  programs.ssh.knownHosts = {
+    whitby = {
+      hostNames = [ "whitby.tvl.fyi" "whitby.tvl.su" ];
+      publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILNh/w4BSKov0jdz3gKBc98tpoLta5bb87fQXWBhAl2I";
+    };
+
+    sanduny = {
+      hostNames = [ "sanduny.tvl.su" ];
+      publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOag0XhylaTVhmT6HB8EN2Fv5Ymrc4ZfypOXONUkykTX";
+    };
+
+    github = {
+      hostNames = [ "github.com" ];
+      publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl";
+    };
+  };
+}
diff --git a/ops/modules/monorepo-gerrit.nix b/ops/modules/monorepo-gerrit.nix
new file mode 100644
index 0000000000..509500c913
--- /dev/null
+++ b/ops/modules/monorepo-gerrit.nix
@@ -0,0 +1,152 @@
+# Gerrit configuration for the TVL monorepo
+{ depot, pkgs, config, lib, ... }:
+
+let
+  cfg = config.services.gerrit;
+
+  besadiiWithConfig = name: pkgs.writeShellScript "besadii-whitby" ''
+    export BESADII_CONFIG=/run/agenix/gerrit-besadii-config
+    exec -a ${name} ${depot.ops.besadii}/bin/besadii "$@"
+  '';
+
+  gerritHooks = pkgs.runCommandNoCC "gerrit-hooks" { } ''
+    mkdir -p $out
+    ln -s ${besadiiWithConfig "change-merged"} $out/change-merged
+    ln -s ${besadiiWithConfig "patchset-created"} $out/patchset-created
+  '';
+in
+{
+  services.gerrit = {
+    enable = true;
+    listenAddress = "[::]:4778"; # 4778 - grrt
+    serverId = "4fdfa107-4df9-4596-8e0a-1d2bbdd96e36";
+    builtinPlugins = [
+      "download-commands"
+      "hooks"
+    ];
+
+    plugins = with depot.third_party.gerrit_plugins; [
+      owners
+      oauth
+      depot.ops.gerrit-tvl
+    ];
+
+    package = depot.third_party.gerrit;
+
+    jvmHeapLimit = "4g";
+
+    # In some NixOS channel bump, the default version of OpenJDK has
+    # changed to one that is incompatible with our current version of
+    # Gerrit.
+    #
+    # TODO(tazjin): Update Gerrit and remove this when possible.
+    jvmPackage = pkgs.openjdk11_headless;
+
+    settings = {
+      core.packedGitLimit = "100m";
+      log.jsonLogging = true;
+      log.textLogging = false;
+      sshd.advertisedAddress = "code.tvl.fyi:29418";
+      hooks.path = "${gerritHooks}";
+      cache.web_sessions.maxAge = "3 months";
+      plugins.allowRemoteAdmin = false;
+      change.enableAttentionSet = true;
+      change.enableAssignee = false;
+
+      # Configures gerrit for being reverse-proxied by nginx as per
+      # https://gerrit-review.googlesource.com/Documentation/config-reverseproxy.html
+      gerrit = {
+        canonicalWebUrl = "https://cl.tvl.fyi";
+        docUrl = "/Documentation";
+      };
+
+      httpd.listenUrl = "proxy-https://${cfg.listenAddress}";
+
+      download.command = [
+        "checkout"
+        "cherry_pick"
+        "format_patch"
+        "pull"
+      ];
+
+      # Configure for cgit.
+      gitweb = {
+        type = "custom";
+        url = "https://code.tvl.fyi";
+        project = "/";
+        revision = "/commit/?id=\${commit}";
+        branch = "/log/?h=\${branch}";
+        tag = "/tag/?h=\${tag}";
+        roottree = "/tree/?h=\${commit}";
+        file = "/tree/\${file}?h=\${commit}";
+        filehistory = "/log/\${file}?h=\${branch}";
+        linkname = "cgit";
+      };
+
+      # Auto-link panettone bug links
+      commentlink.panettone = {
+        match = "b/(\\\\d+)";
+        html = "<a href=\"https://b.tvl.fyi/issues/$1\">b/$1</a>";
+      };
+
+      # Auto-link other CLs
+      commentlink.gerrit = {
+        match = "cl/(\\\\d+)";
+        html = "<a href=\"https://cl.tvl.fyi/$1\">cl/$1</a>";
+      };
+
+      # Configures integration with Keycloak, which then integrates with a
+      # variety of backends.
+      auth.type = "OAUTH";
+      plugin.gerrit-oauth-provider-keycloak-oauth = {
+        root-url = "https://auth.tvl.fyi";
+        realm = "TVL";
+        client-id = "gerrit";
+        # client-secret is set in /var/lib/gerrit/etc/secure.config.
+      };
+
+      # Allow users to add additional email addresses to their accounts.
+      oauth.allowRegisterNewEmail = true;
+
+      # Use Gerrit's built-in HTTP passwords, rather than trying to use the
+      # password against the backing OAuth provider.
+      auth.gitBasicAuthPolicy = "HTTP";
+
+      # Email sending (emails are relayed via the tazj.in domain's
+      # GSuite currently).
+      #
+      # Note that sendemail.smtpPass is stored in
+      # $site_path/etc/secure.config and is *not* controlled by Nix.
+      #
+      # Receiving email is not currently supported.
+      sendemail = {
+        enable = true;
+        html = false;
+        connectTimeout = "10sec";
+        from = "TVL Code Review <tvlbot@tazj.in>";
+        includeDiff = true;
+        smtpEncryption = "none";
+        smtpServer = "localhost";
+        smtpServerPort = 2525;
+      };
+    };
+  };
+
+  systemd.services.gerrit = {
+    serviceConfig = {
+      # There seems to be no easy way to get `DynamicUser` to play
+      # well with other services (e.g. by using SupplementaryGroups,
+      # which seem to have no effect) so we force the DynamicUser
+      # setting for the Gerrit service to be disabled and reuse the
+      # existing 'git' user.
+      DynamicUser = lib.mkForce false;
+      User = "git";
+      Group = "git";
+    };
+  };
+
+  services.depot.restic = {
+    paths = [ "/var/lib/gerrit" ];
+    exclude = [ "/var/lib/gerrit/tmp" ];
+  };
+}
diff --git a/ops/modules/nixery.nix b/ops/modules/nixery.nix
new file mode 100644
index 0000000000..4122f9ebbf
--- /dev/null
+++ b/ops/modules/nixery.nix
@@ -0,0 +1,43 @@
+# NixOS module to run Nixery, currently with local-storage as the
+# backend for storing/serving image layers.
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.nixery;
+  description = "Nixery - container images on-demand";
+  storagePath = "/var/lib/nixery/${pkgs.nixpkgsCommits.unstable}";
+in
+{
+  options.services.depot.nixery = {
+    enable = lib.mkEnableOption description;
+
+    port = lib.mkOption {
+      type = lib.types.int;
+      default = 45243; # "image"
+      description = "Port on which Nixery should listen";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.nixery = {
+      inherit description;
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "nixery";
+        Restart = "always";
+        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${storagePath}";
+        ExecStart = "${depot.tools.nixery.nixery}/bin/server";
+      };
+
+      environment = {
+        PORT = toString cfg.port;
+        NIXERY_PKGS_PATH = pkgs.path;
+        NIXERY_STORAGE_BACKEND = "filesystem";
+        NIX_TIMEOUT = "60"; # seconds
+        STORAGE_PATH = storagePath;
+      };
+    };
+  };
+}
diff --git a/ops/modules/oauth2_proxy.nix b/ops/modules/oauth2_proxy.nix
new file mode 100644
index 0000000000..23afa7bce0
--- /dev/null
+++ b/ops/modules/oauth2_proxy.nix
@@ -0,0 +1,60 @@
+# Configuration for oauth2_proxy, which is used as a handler for nginx
+# auth-request setups.
+#
+# This module exports a helper function at
+# `config.services.depot.oauth2_proxy.withAuth` that can be wrapped
+# around nginx server configuration blocks to configure their
+# authentication setup.
+{ config, depot, pkgs, lib, ... }:
+
+let
+  description = "OAuth2 proxy to authenticate TVL services";
+  cfg = config.services.depot.oauth2_proxy;
+  configFile = pkgs.writeText "oauth2_proxy.cfg" ''
+    email_domains = [ "*" ]
+    http_address = "127.0.0.1:${toString cfg.port}"
+    provider = "keycloak-oidc"
+    client_id = "oauth2-proxy"
+    oidc_issuer_url = "https://auth.tvl.fyi/auth/realms/TVL"
+    reverse_proxy = true
+    set_xauthrequest = true
+  '';
+
+  # Depend on the Keycloak service if it is running on the same
+  # machine.
+  depends_on = lib.optional config.services.keycloak.enable "keycloak.service";
+in
+{
+  options.services.depot.oauth2_proxy = {
+    enable = lib.mkEnableOption description;
+
+    port = lib.mkOption {
+      description = "Port to listen on";
+      type = lib.types.int;
+      default = 2884; # "auth"
+    };
+
+    secretsFile = lib.mkOption {
+      type = lib.types.str;
+      description = "EnvironmentFile from which to load secrets";
+      default = config.age.secretsDir + "/oauth2_proxy";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.oauth2_proxy = {
+      inherit description;
+      after = depends_on;
+      wants = depends_on;
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        DynamicUser = true;
+        EnvironmentFile = cfg.secretsFile;
+        ExecStart = "${pkgs.oauth2_proxy}/bin/oauth2-proxy --config ${configFile}";
+      };
+    };
+  };
+}
diff --git a/ops/modules/open_eid.nix b/ops/modules/open_eid.nix
new file mode 100644
index 0000000000..4bc35e298c
--- /dev/null
+++ b/ops/modules/open_eid.nix
@@ -0,0 +1,37 @@
+# NixOS module to configure the Estonian e-ID software.
+{ pkgs, ... }:
+
+let
+  # Wrapper script to tell to Chrome/Chromium to use p11-kit-proxy to load
+  # security devices.
+  # Each user needs to run this themselves, it does not work on a system level
+  # due to a bug in Chromium:
+  #
+  # https://bugs.chromium.org/p/chromium/issues/detail?id=16387
+  #
+  # Firefox users can just set
+  # extraPolicies.SecurityDevices.p11-kit-proxy "${pkgs.p11-kit}/lib/p11-kit-proxy.so";
+  # when overriding the firefox derivation.
+  setup-browser-eid = pkgs.writeShellScriptBin "setup-browser-eid" ''
+    NSSDB="''${HOME}/.pki/nssdb"
+    mkdir -p ''${NSSDB}
+
+    ${pkgs.nssTools}/bin/modutil -force -dbdir sql:$NSSDB -add p11-kit-proxy \
+      -libfile ${pkgs.p11-kit}/lib/p11-kit-proxy.so
+  '';
+in
+{
+  services.pcscd.enable = true;
+
+  # Tell p11-kit to load/proxy opensc-pkcs11.so, providing all available slots
+  # (PIN1 for authentication/decryption, PIN2 for signing).
+  environment.etc."pkcs11/modules/opensc-pkcs11".text = ''
+    module: ${pkgs.opensc}/lib/opensc-pkcs11.so
+  '';
+
+  environment.systemPackages = with pkgs; [
+    libdigidocpp # provides digidoc-tool(1)
+    qdigidoc
+    setup-browser-eid
+  ];
+}
diff --git a/ops/modules/owothia.nix b/ops/modules/owothia.nix
new file mode 100644
index 0000000000..b9746c1720
--- /dev/null
+++ b/ops/modules/owothia.nix
@@ -0,0 +1,68 @@
+# Run the owothia IRC bot.
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.owothia;
+  description = "owothia - i'm a service owo";
+in
+{
+  options.services.depot.owothia = {
+    enable = lib.mkEnableOption description;
+
+    secretsFile = lib.mkOption {
+      type = lib.types.str;
+      description = "File path from which systemd should read secrets";
+      default = config.age.secretsDir + "/owothia";
+    };
+
+    owoChance = lib.mkOption {
+      type = lib.types.int;
+      description = "How likely is owo?";
+      default = 200;
+    };
+
+    ircServer = lib.mkOption {
+      type = lib.types.str;
+      description = "IRC server hostname";
+    };
+
+    ircPort = lib.mkOption {
+      type = lib.types.int;
+      description = "IRC server port";
+    };
+
+    ircIdent = lib.mkOption {
+      type = lib.types.str;
+      description = "IRC username";
+      default = "owothia";
+    };
+
+    ircChannels = lib.mkOption {
+      type = with lib.types; listOf str;
+      description = "IRC channels to join";
+      default = [ "#tvl" ];
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.owothia = {
+      inherit description;
+      script = "${depot.fun.owothia}/bin/owothia";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        EnvironmentFile = cfg.secretsFile;
+      };
+
+      environment = {
+        OWO_CHANCE = toString cfg.owoChance;
+        IRC_SERVER = cfg.ircServer;
+        IRC_PORT = toString cfg.ircPort;
+        IRC_IDENT = cfg.ircIdent;
+        IRC_CHANNELS = builtins.toJSON cfg.ircChannels;
+      };
+    };
+  };
+}
diff --git a/ops/modules/panettone.nix b/ops/modules/panettone.nix
new file mode 100644
index 0000000000..2576ab16c5
--- /dev/null
+++ b/ops/modules/panettone.nix
@@ -0,0 +1,108 @@
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.panettone;
+in
+{
+  options.services.depot.panettone = with lib; {
+    enable = mkEnableOption "Panettone issue tracker";
+
+    port = mkOption {
+      description = "Port on which Panettone should listen";
+      type = types.int;
+      default = 7268;
+    };
+
+    dbHost = mkOption {
+      description = "Postgresql host to connect to for Panettone";
+      type = types.str;
+      default = "localhost";
+    };
+
+    dbName = mkOption {
+      description = "Name of the database for Panettone";
+      type = types.str;
+      default = "panettone";
+    };
+
+    dbUser = mkOption {
+      description = "Name of the database user for Panettone";
+      type = types.str;
+      default = "panettone";
+    };
+
+    secretsFile = mkOption {
+      description = ''
+        Path to a file containing secrets, in the format accepted
+        by systemd's EnvironmentFile
+      '';
+      type = types.str;
+      default = config.age.secretsDir + "/panettone";
+    };
+
+    irccatHost = mkOption {
+      description = "Hostname for the irccat instance";
+      type = types.str;
+      default = "localhost";
+    };
+
+    irccatPort = mkOption {
+      description = "Port for the irccat instance";
+      type = types.int;
+      default = 4722;
+    };
+
+    irccatChannel = mkOption {
+      description = "IRC channels to post to via irccat";
+      type = types.str;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [{
+      assertion =
+        cfg.dbHost != "localhost" || config.services.postgresql.enable;
+      message = "Panettone requires a postgresql database";
+    }
+      {
+        assertion =
+          cfg.dbHost != "localhost" || config.services.postgresql.enableTCPIP;
+        message = "Panettone can only connect to the postgresql database over TCP";
+      }
+      {
+        assertion =
+          cfg.dbHost != "localhost" || (lib.any
+            (user: user.name == cfg.dbUser)
+            config.services.postgresql.ensureUsers);
+        message = "Panettone requires a database user";
+      }
+      {
+        assertion =
+          cfg.dbHost != "localhost" || (lib.any
+            (db: db == cfg.dbName)
+            config.services.postgresql.ensureDatabases);
+        message = "Panettone requires a database";
+      }];
+
+    systemd.services.panettone = {
+      wantedBy = [ "multi-user.target" ];
+      script = "${depot.web.panettone}/bin/panettone";
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        EnvironmentFile = cfg.secretsFile;
+      };
+
+      environment = {
+        PANETTONE_PORT = toString cfg.port;
+        PGHOST = "localhost";
+        PGUSER = cfg.dbUser;
+        PGDATABASE = cfg.dbName;
+        IRCCATHOST = cfg.irccatHost;
+        IRCCATPORT = toString cfg.irccatPort;
+        ISSUECHANNEL = cfg.irccatChannel;
+      };
+    };
+  };
+}
diff --git a/ops/modules/paroxysm.nix b/ops/modules/paroxysm.nix
new file mode 100644
index 0000000000..070e7623db
--- /dev/null
+++ b/ops/modules/paroxysm.nix
@@ -0,0 +1,28 @@
+{ depot, config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.paroxysm;
+  description = "TVL's majestic IRC bot";
+in
+{
+  options.services.depot.paroxysm.enable = lib.mkEnableOption description;
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.paroxysm = {
+      inherit description;
+      script = "${depot.fun.paroxysm}/bin/paroxysm";
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        PARX_DATABASE_URL = "postgresql://tvldb:tvldb@localhost/tvldb";
+        PARX_IRC_CONFIG_PATH = "/var/lib/paroxysm/irc.toml";
+      };
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "paroxysm";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/ops/modules/prometheus-fail2ban-exporter.nix b/ops/modules/prometheus-fail2ban-exporter.nix
new file mode 100644
index 0000000000..349364f9b7
--- /dev/null
+++ b/ops/modules/prometheus-fail2ban-exporter.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, depot, ... }:
+
+let
+  cfg = config.services.prometheus-fail2ban-exporter;
+in
+
+{
+  options.services.prometheus-fail2ban-exporter = with lib; {
+    enable = mkEnableOption "Prometheus Fail2ban Exporter";
+
+    interval = mkOption {
+      description = "Systemd calendar expression for how often to run the interval";
+      type = types.string;
+      default = "minutely";
+      example = "hourly";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services."prometheus-fail2ban-exporter" = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "fail2ban.service" ];
+      serviceConfig = {
+        User = "root";
+        Type = "oneshot";
+        ExecStart = pkgs.writeShellScript "prometheus-fail2ban-exporter" ''
+          set -eo pipefail
+          mkdir -p /var/lib/prometheus/node-exporter
+          exec prometheus-fail2ban-exporter
+        '';
+      };
+
+      path = [
+        pkgs.fail2ban
+        depot.third_party.prometheus-fail2ban-exporter
+      ];
+    };
+
+    systemd.timers."prometheus-fail2ban-exporter" = {
+      wantedBy = [ "multi-user.target" ];
+      timerConfig.OnCalendar = cfg.interval;
+    };
+
+    services.prometheus.exporters.node = {
+      enabledCollectors = [ "textfile" ];
+
+      extraFlags = [
+        "--collector.textfile.directory=/var/lib/prometheus/node-exporter"
+      ];
+    };
+  };
+}
diff --git a/ops/modules/quassel.nix b/ops/modules/quassel.nix
new file mode 100644
index 0000000000..275e2809d7
--- /dev/null
+++ b/ops/modules/quassel.nix
@@ -0,0 +1,79 @@
+# A more modern module for running Quassel.
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.quassel;
+  quasselDaemon = pkgs.quassel.override {
+    monolithic = false;
+    enableDaemon = true;
+    withKDE = false;
+  };
+in
+{
+  options.services.depot.quassel = with lib; {
+    enable = mkEnableOption "Quassel IRC daemon";
+
+    acmeHost = mkOption {
+      description = "ACME host to use for the Quassel TLS certificate";
+      type = lib.types.str;
+    };
+
+    bindAddresses = mkOption {
+      description = "Addresses Quassel will bind to/listen on";
+      default = [ "127.0.0.1" ];
+    };
+
+    logLevel = mkOption {
+      description = "Log level for Quassel Core";
+      default = "Info";
+      type = lib.types.enum [
+        "Debug"
+        "Info"
+        "Warning"
+        "Error"
+      ];
+    };
+
+    port = mkOption {
+      default = 6698;
+      description = ''
+        The port number the Quassel daemon will be listening to.
+      '';
+    };
+  };
+
+  config = with lib; mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = [ cfg.port ];
+
+    systemd.services.quassel = {
+      description = "Quassel IRC daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      script = concatStringsSep " " [
+        "${quasselDaemon}/bin/quasselcore"
+        "--listen=${concatStringsSep "," cfg.bindAddresses}"
+        "--port=${toString cfg.port}"
+        "--configdir=/var/lib/quassel"
+        "--require-ssl"
+        "--ssl-cert=/var/lib/acme/${cfg.acmeHost}/full.pem"
+        "--loglevel=${cfg.logLevel}"
+      ];
+
+      serviceConfig = {
+        Restart = "always";
+        User = "quassel";
+        Group = "quassel";
+        StateDirectory = "quassel";
+      };
+    };
+
+    users = {
+      users.quassel = {
+        isSystemUser = true;
+        group = "quassel";
+      };
+
+      groups.quassel = { };
+    };
+  };
+}
diff --git a/ops/modules/restic.nix b/ops/modules/restic.nix
new file mode 100644
index 0000000000..8695396035
--- /dev/null
+++ b/ops/modules/restic.nix
@@ -0,0 +1,62 @@
+# Configure restic backups to S3-compatible storage, in our case
+# GleSYS object storage.
+#
+# Conventions:
+# - restic's cache lives in /var/backup/restic/cache
+# - repository password lives in /var/backup/restic/secret
+# - object storage credentials in /var/backup/restic/glesys-key
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.restic;
+  description = "Restic backups to GleSYS";
+  mkStringOption = default: lib.mkOption {
+    inherit default;
+    type = lib.types.str;
+  };
+in
+{
+  options.services.depot.restic = {
+    enable = lib.mkEnableOption description;
+    bucketEndpoint = mkStringOption "objects.dc-sto1.glesys.net";
+    bucketName = mkStringOption "aged-resonance";
+    bucketCredentials = mkStringOption "/var/backup/restic/glesys-key";
+    repository = mkStringOption config.networking.hostName;
+    interval = mkStringOption "hourly";
+
+    paths = with lib; mkOption {
+      description = "Directories that should be backed up";
+      type = types.listOf types.str;
+    };
+
+    exclude = with lib; mkOption {
+      description = "Files that should be excluded from backups";
+      type = types.listOf types.str;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.restic = {
+      description = "Backups to GleSYS";
+
+      script = "${pkgs.restic}/bin/restic backup ${lib.concatStringsSep " " cfg.paths}";
+
+      environment = {
+        RESTIC_REPOSITORY = "s3:${cfg.bucketEndpoint}/${cfg.bucketName}/${cfg.repository}";
+        AWS_SHARED_CREDENTIALS_FILE = cfg.bucketCredentials;
+        RESTIC_PASSWORD_FILE = "/var/backup/restic/secret";
+        RESTIC_CACHE_DIR = "/var/backup/restic/cache";
+
+        RESTIC_EXCLUDE_FILE =
+          builtins.toFile "exclude-files" (lib.concatStringsSep "\n" cfg.exclude);
+      };
+    };
+
+    systemd.timers.restic = {
+      wantedBy = [ "multi-user.target" ];
+      timerConfig.OnCalendar = cfg.interval;
+    };
+
+    environment.systemPackages = [ pkgs.restic ];
+  };
+}
diff --git a/ops/modules/smtprelay.nix b/ops/modules/smtprelay.nix
new file mode 100644
index 0000000000..f6ce262175
--- /dev/null
+++ b/ops/modules/smtprelay.nix
@@ -0,0 +1,61 @@
+# NixOS module for configuring the simple SMTP relay.
+{ depot, pkgs, config, lib, ... }:
+
+let
+  inherit (builtins) attrValues mapAttrs;
+  inherit (lib)
+    concatStringsSep
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+    ;
+
+  cfg = config.services.depot.smtprelay;
+  description = "Simple SMTP relay";
+
+  # Configuration values that are always overridden.
+  #
+  # - logging is pinned to stdout for journald compatibility
+  # - secret config is loaded through systemd's credential loading facility
+  overrideArgs = {
+    logfile = "";
+    config = "$CREDENTIALS_DIRECTORY/secrets";
+  };
+
+  # Creates the command line argument string for the service.
+  prepareArgs = args:
+    concatStringsSep " "
+      (attrValues (mapAttrs (key: value: "-${key} \"${toString value}\"")
+        (args // overrideArgs)));
+in
+{
+  options.services.depot.smtprelay = {
+    enable = mkEnableOption description;
+
+    args = mkOption {
+      type = types.attrsOf types.str;
+      description = "Key value pairs for command line arguments";
+    };
+
+    secretsFile = mkOption {
+      type = types.str;
+      default = config.age.secretsDir + "/smtprelay";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.smtprelay = {
+      inherit description;
+      script = "${depot.third_party.smtprelay}/bin/smtprelay ${prepareArgs cfg.args}";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "always";
+        StateDirectory = "smtprelay";
+        DynamicUser = true;
+        LoadCredential = "secrets:${cfg.secretsFile}";
+      };
+    };
+  };
+}
diff --git a/ops/modules/sourcegraph.nix b/ops/modules/sourcegraph.nix
new file mode 100644
index 0000000000..cbf836ab64
--- /dev/null
+++ b/ops/modules/sourcegraph.nix
@@ -0,0 +1,60 @@
+# Run sourcegraph, including its entire machinery, in a container.
+# Running it outside of a container is a futile endeavour for now.
+{ depot, config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.depot.sourcegraph;
+in
+{
+  options.services.depot.sourcegraph = with lib; {
+    enable = mkEnableOption "SourceGraph code search engine";
+
+    port = mkOption {
+      description = "Port on which SourceGraph should listen";
+      type = types.int;
+      default = 3463;
+    };
+
+    cheddarPort = mkOption {
+      description = "Port on which cheddar should listen";
+      type = types.int;
+      default = 4238;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Run a cheddar syntax highlighting server
+    systemd.services.cheddar-server = {
+      wantedBy = [ "multi-user.target" ];
+      script = "${depot.tools.cheddar}/bin/cheddar --listen 0.0.0.0:${toString cfg.cheddarPort} --sourcegraph-server";
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+      };
+    };
+
+    virtualisation.oci-containers.containers.sourcegraph = {
+      image = "sourcegraph/server:3.40.0";
+
+      ports = [
+        "127.0.0.1:${toString cfg.port}:7080"
+      ];
+
+      volumes = [
+        "/var/lib/sourcegraph/etc:/etc/sourcegraph"
+        "/var/lib/sourcegraph/data:/var/opt/sourcegraph"
+      ];
+
+      # TODO(tazjin): Figure out what changed in the protocol.
+      # environment.SRC_SYNTECT_SERVER = "http://172.17.0.1:${toString cfg.cheddarPort}";
+
+      # Sourcegraph needs a higher nofile limit, it logs warnings
+      # otherwise (unclear whether it actually affects the service).
+      extraOptions = [
+        "--ulimit"
+        "nofile=10000:10000"
+      ];
+    };
+  };
+}
diff --git a/ops/modules/tvl-buildkite.nix b/ops/modules/tvl-buildkite.nix
new file mode 100644
index 0000000000..4341ef01d7
--- /dev/null
+++ b/ops/modules/tvl-buildkite.nix
@@ -0,0 +1,77 @@
+# Configuration for the TVL buildkite agents.
+{ config, depot, pkgs, lib, ... }:
+
+let
+  cfg = config.services.depot.buildkite;
+  agents = lib.range 1 cfg.agentCount;
+  description = "Buildkite agents for TVL";
+
+  besadiiWithConfig = name: pkgs.writeShellScript "besadii-whitby" ''
+    export BESADII_CONFIG=/run/agenix/buildkite-besadii-config
+    exec -a ${name} ${depot.ops.besadii}/bin/besadii "$@"
+  '';
+
+  # All Buildkite hooks are actually besadii, but it's being invoked
+  # with different names.
+  buildkiteHooks = pkgs.runCommandNoCC "buildkite-hooks" { } ''
+    mkdir -p $out/bin
+    ln -s ${besadiiWithConfig "post-command"} $out/bin/post-command
+  '';
+
+  credentialHelper = pkgs.writeShellScriptBin "git-credential-gerrit-creds" ''
+    echo 'username=buildkite'
+    echo "password=$(jq -r '.gerritPassword' /run/agenix/buildkite-besadii-config)"
+  '';
+in
+{
+  options.services.depot.buildkite = {
+    enable = lib.mkEnableOption description;
+    agentCount = lib.mkOption {
+      type = lib.types.int;
+      description = "Number of Buildkite agents to launch";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Run the Buildkite agents using the default upstream module.
+    services.buildkite-agents = builtins.listToAttrs (map
+      (n: rec {
+        name = "whitby-${toString n}";
+        value = {
+          inherit name;
+          enable = true;
+          tokenPath = config.age.secretsDir + "/buildkite-agent-token";
+          privateSshKeyPath = config.age.secretsDir + "/buildkite-private-key";
+          hooks.post-command = "${buildkiteHooks}/bin/post-command";
+
+          runtimePackages = with pkgs; [
+            bash
+            coreutils
+            credentialHelper
+            curl
+            git
+            gnutar
+            gzip
+            jq
+            nix
+          ];
+        };
+      })
+      agents);
+
+    # Set up a group for all Buildkite agent users
+    users = {
+      groups.buildkite-agents = { };
+      users = builtins.listToAttrs (map
+        (n: rec {
+          name = "buildkite-agent-whitby-${toString n}";
+          value = {
+            isSystemUser = true;
+            group = lib.mkForce "buildkite-agents";
+            extraGroups = [ name "docker" ];
+          };
+        })
+        agents);
+    };
+  };
+}
diff --git a/ops/modules/tvl-cache.nix b/ops/modules/tvl-cache.nix
new file mode 100644
index 0000000000..4d574821df
--- /dev/null
+++ b/ops/modules/tvl-cache.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options = {
+    tvl.cache.enable = lib.mkEnableOption "the TVL binary cache";
+  };
+
+  config = lib.mkIf config.tvl.cache.enable {
+    nix = {
+      binaryCachePublicKeys = [
+        "cache.tvl.su:kjc6KOMupXc1vHVufJUoDUYeLzbwSr9abcAKdn/U1Jk="
+      ];
+
+      binaryCaches = [
+        "https://cache.tvl.su"
+      ];
+    };
+  };
+}
diff --git a/ops/modules/tvl-slapd/default.nix b/ops/modules/tvl-slapd/default.nix
new file mode 100644
index 0000000000..3979491f8c
--- /dev/null
+++ b/ops/modules/tvl-slapd/default.nix
@@ -0,0 +1,81 @@
+# Configures an OpenLDAP instance for TVL
+#
+# TODO(tazjin): Configure ldaps://
+{ depot, lib, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  user = struct {
+    username = string;
+    email = string;
+    password = string;
+    displayName = option string;
+  };
+
+  toLdif = defun [ user string ] (u: ''
+    dn: cn=${u.username},ou=users,dc=tvl,dc=fyi
+    objectClass: organizationalPerson
+    objectClass: inetOrgPerson
+    sn: ${u.username}
+    cn: ${u.username}
+    displayName: ${u.displayName or u.username}
+    mail: ${u.email}
+    userPassword: ${u.password}
+  '');
+
+  inherit (depot.ops) users;
+
+in
+{
+  services.openldap = {
+    enable = true;
+
+    settings.children = {
+      "olcDatabase={1}mdb".attrs = {
+        objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+        olcDatabase = "{1}mdb";
+        olcDbDirectory = "/var/lib/openldap";
+        olcSuffix = "dc=tvl,dc=fyi";
+        olcAccess = "to *  by * read";
+        olcRootDN = "cn=admin,dc=tvl,dc=fyi";
+        olcRootPW = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$OfcgkOQ96VQ3aJj7NfA9vQ$oS6HQOkYl/bUYg4SejpltQYy7kvqx/RUxvoR4zo1vXU";
+      };
+
+      "cn=module{0}".attrs = {
+        objectClass = "olcModuleList";
+        olcModuleLoad = "argon2";
+      };
+
+      "cn=schema".includes =
+        map (schema: "${pkgs.openldap}/etc/schema/${schema}.ldif")
+          [ "core" "cosine" "inetorgperson" "nis" ];
+    };
+
+    # Contents are immutable at runtime, and adding user accounts etc.
+    # is done statically in the LDIF-formatted contents in this folder.
+    declarativeContents."dc=tvl,dc=fyi" = ''
+      dn: dc=tvl,dc=fyi
+      dc: tvl
+      o: TVL LDAP server
+      description: Root entry for tvl.fyi
+      objectClass: top
+      objectClass: dcObject
+      objectClass: organization
+
+      dn: ou=users,dc=tvl,dc=fyi
+      ou: users
+      description: All users in TVL
+      objectClass: top
+      objectClass: organizationalUnit
+
+      dn: ou=groups,dc=tvl,dc=fyi
+      ou: groups
+      description: All groups in TVL
+      objectClass: top
+      objectClass: organizationalUnit
+
+      ${lib.concatStringsSep "\n" (map toLdif users)}
+    '';
+  };
+}
diff --git a/ops/modules/tvl-users.nix b/ops/modules/tvl-users.nix
new file mode 100644
index 0000000000..988b9eed8a
--- /dev/null
+++ b/ops/modules/tvl-users.nix
@@ -0,0 +1,94 @@
+# Standard NixOS users for TVL machines, as well as configuration that
+# should following along when they are added to a machine.
+{ depot, pkgs, ... }:
+
+{
+  users = {
+    users.tazjin = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      shell = pkgs.fish;
+      openssh.authorizedKeys.keys = depot.users.tazjin.keys.all;
+    };
+
+    users.lukegb = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      openssh.authorizedKeys.keys = depot.users.lukegb.keys.all;
+    };
+
+    users.grfn = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      openssh.authorizedKeys.keys = [
+        depot.users.grfn.keys.whitby
+      ];
+    };
+
+    users.edef = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.edef.keys.all;
+    };
+
+    users.qyliss = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.qyliss.keys.all;
+    };
+
+    users.eta = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.eta.keys.whitby;
+    };
+
+    users.cynthia = {
+      isNormalUser = true; # I'm normal OwO :3
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.cynthia.keys.all;
+    };
+
+    users.firefly = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.firefly.keys.whitby;
+    };
+
+    users.sterni = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      openssh.authorizedKeys.keys = depot.users.sterni.keys.all;
+    };
+
+    users.flokli = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.flokli.keys.all;
+    };
+
+    # Temporarily disabled (inactive) users.
+    users.isomer = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      shell = "${pkgs.shadow}/bin/nologin";
+      openssh.authorizedKeys.keys = depot.users.isomer.keys.all;
+    };
+
+    users.riking = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      shell = "${pkgs.shadow}/bin/nologin";
+      openssh.authorizedKeys.keys = depot.users.riking.keys.u2f ++ depot.users.riking.keys.passworded;
+    };
+  };
+
+  environment.systemPackages = with pkgs; [
+    alacritty.terminfo
+    foot.terminfo
+    rxvt_unicode.terminfo
+
+    # TODO(sterni): re-enable when the kitty build is fixed upstreams
+    # kitty.terminfo
+  ];
+}
diff --git a/ops/modules/v4l2loopback.nix b/ops/modules/v4l2loopback.nix
new file mode 100644
index 0000000000..636b2ff6cf
--- /dev/null
+++ b/ops/modules/v4l2loopback.nix
@@ -0,0 +1,12 @@
+{ config, lib, pkgs, ... }:
+
+{
+  boot = {
+    extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
+    kernelModules = [ "v4l2loopback" ];
+    extraModprobeConfig = ''
+      options v4l2loopback exclusive_caps=1
+    '';
+  };
+}
+
diff --git a/ops/modules/www/atward.tvl.fyi.nix b/ops/modules/www/atward.tvl.fyi.nix
new file mode 100644
index 0000000000..6b3672dd75
--- /dev/null
+++ b/ops/modules/www/atward.tvl.fyi.nix
@@ -0,0 +1,33 @@
+# Serve atward, the query redirection ... thing.
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    # Short link support (i.e. plain http://at) for users with a
+    # configured tvl.fyi/tvl.su search domain.
+    services.nginx.virtualHosts."at-shortlink" = {
+      serverName = "at";
+      extraConfig = "return 302 https://atward.tvl.fyi$request_uri;";
+    };
+
+    services.nginx.virtualHosts."atward" = {
+      serverName = "atward.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      serverAliases = [
+        "atward.tvl.su"
+        "at.tvl.fyi"
+        "at.tvl.su"
+      ];
+
+      locations."/" = {
+        proxyPass = "http://localhost:${toString config.services.depot.atward.port}";
+      };
+    };
+  };
+}
diff --git a/ops/modules/www/auth.tvl.fyi.nix b/ops/modules/www/auth.tvl.fyi.nix
new file mode 100644
index 0000000000..e0c031bf70
--- /dev/null
+++ b/ops/modules/www/auth.tvl.fyi.nix
@@ -0,0 +1,24 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."auth.tvl.fyi" = {
+      serverName = "auth.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:${config.services.keycloak.httpPort};
+          proxy_set_header X-Forwarded-For $remote_addr;
+          proxy_set_header X-Forwarded-Proto https;
+          proxy_set_header Host $host;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/b.tvl.fyi.nix b/ops/modules/www/b.tvl.fyi.nix
new file mode 100644
index 0000000000..45f6c6ed51
--- /dev/null
+++ b/ops/modules/www/b.tvl.fyi.nix
@@ -0,0 +1,32 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."b-shortlink" = {
+      serverName = "b";
+      extraConfig = "return 302 https://b.tvl.fyi$request_uri;";
+    };
+
+    services.nginx.virtualHosts."b.tvl.fyi" = {
+      serverName = "b.tvl.fyi";
+      serverAliases = [ "b.tvl.su" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        # Forward short links to issues to the issue itself (b/32)
+        location ~ ^/(\d+)$ {
+          return 302 https://b.tvl.fyi/issues$request_uri;
+        }
+
+        location / {
+          proxy_pass http://localhost:${toString config.services.depot.panettone.port};
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/base.nix b/ops/modules/www/base.nix
new file mode 100644
index 0000000000..50fceff0fa
--- /dev/null
+++ b/ops/modules/www/base.nix
@@ -0,0 +1,41 @@
+{ config, pkgs, ... }:
+
+{
+  config = {
+    security.acme = {
+      acceptTerms = true;
+      defaults.email = "letsencrypt@tvl.su";
+    };
+
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+
+      recommendedTlsSettings = true;
+      recommendedGzipSettings = true;
+      recommendedProxySettings = true;
+
+      commonHttpConfig = ''
+        log_format json_combined escape=json
+        '{'
+            '"remote_addr":"$remote_addr",'
+            '"method":"$request_method",'
+            '"host":"$host",'
+            '"uri":"$request_uri",'
+            '"status":$status,'
+            '"request_size":$request_length,'
+            '"response_size":$body_bytes_sent,'
+            '"response_time":$request_time,'
+            '"referrer":"$http_referer",'
+            '"user_agent":"$http_user_agent"'
+        '}';
+
+        access_log syslog:server=unix:/dev/log,nohostname json_combined;
+      '';
+
+      appendHttpConfig = ''
+        add_header Permissions-Policy "interest-cohort=()";
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/cache.tvl.su.nix b/ops/modules/www/cache.tvl.su.nix
new file mode 100644
index 0000000000..99bc008cd6
--- /dev/null
+++ b/ops/modules/www/cache.tvl.su.nix
@@ -0,0 +1,31 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."cache.tvl.su" = {
+      serverName = "cache.tvl.su";
+      serverAliases = [ "cache.tvl.fyi" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location = /cache-key.pub {
+          alias /run/agenix/nix-cache-pub;
+        }
+
+        location = /nix-cache-info {
+          add_header Content-Type text/plain;
+          return 200 "StoreDir: /nix/store\nWantMassQuery: 1\nPriority: 50\n";
+        }
+
+        location / {
+          proxy_pass http://localhost:${toString config.services.nix-serve.port};
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/cl.tvl.fyi.nix b/ops/modules/www/cl.tvl.fyi.nix
new file mode 100644
index 0000000000..470122c395
--- /dev/null
+++ b/ops/modules/www/cl.tvl.fyi.nix
@@ -0,0 +1,30 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."cl-shortlink" = {
+      serverName = "cl";
+      extraConfig = "return 302 https://cl.tvl.fyi$request_uri;";
+    };
+
+    services.nginx.virtualHosts.gerrit = {
+      serverName = "cl.tvl.fyi";
+      serverAliases = [ "cl.tvl.su" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:4778;
+          proxy_set_header  X-Forwarded-For $remote_addr;
+          # The :443 suffix is a workaround for https://b.tvl.fyi/issues/88.
+          proxy_set_header  Host $host:443;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/code.tvl.fyi.nix b/ops/modules/www/code.tvl.fyi.nix
new file mode 100644
index 0000000000..3f34a9422c
--- /dev/null
+++ b/ops/modules/www/code.tvl.fyi.nix
@@ -0,0 +1,45 @@
+{ depot, config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts.cgit = {
+      serverName = "code.tvl.fyi";
+      serverAliases = [ "code.tvl.su" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        # Serve the rendered Tvix component SVG.
+        #
+        # TODO(tazjin): Implement a way of serving this dynamically
+        location = /about/tvix/docs/component-flow.svg {
+            alias ${depot.tvix.docs.svg}/component-flow.svg;
+        }
+
+        # Git operations on depot.git hit josh
+        location /depot.git {
+            proxy_pass http://localhost:${toString config.services.depot.josh.port};
+        }
+
+        # Git clone operations on '/' should be redirected to josh now.
+        location = /info/refs {
+            return 302 https://code.tvl.fyi/depot.git/info/refs$is_args$args;
+        }
+
+        # Static assets must always hit the root.
+        location ~ ^/(favicon\.ico|cgit\.(css|png))$ {
+           proxy_pass http://localhost:2448;
+        }
+
+        # Everything else is forwarded to cgit for the web view
+        location / {
+            proxy_pass http://localhost:2448/cgit.cgi/depot/;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/cs.tvl.fyi.nix b/ops/modules/www/cs.tvl.fyi.nix
new file mode 100644
index 0000000000..fac814baf0
--- /dev/null
+++ b/ops/modules/www/cs.tvl.fyi.nix
@@ -0,0 +1,31 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."cs.tvl.fyi" = {
+      serverName = "cs.tvl.fyi";
+      serverAliases = [ "cs.tvl.su" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location = / {
+          return 301 https://cs.tvl.fyi/depot;
+        }
+
+        location / {
+          proxy_set_header X-Sg-Auth "Anonymous";
+          proxy_pass http://localhost:${toString config.services.depot.sourcegraph.port};
+        }
+
+        location /users/Anonymous/settings {
+          return 301 https://cs.tvl.fyi;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/deploys.tvl.fyi.nix b/ops/modules/www/deploys.tvl.fyi.nix
new file mode 100644
index 0000000000..ffbe225b58
--- /dev/null
+++ b/ops/modules/www/deploys.tvl.fyi.nix
@@ -0,0 +1,22 @@
+{ pkgs, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    # Ensure the directory for deployment diffs exists.
+    systemd.tmpfiles.rules = [
+      "d /var/html/deploys.tvl.fyi/diff 0755 nginx nginx -"
+    ];
+
+    services.nginx.virtualHosts."deploys.tvl.fyi" = {
+      enableACME = true;
+      forceSSL = true;
+      root = "/var/html/deploys.tvl.fyi";
+    };
+
+    services.depot.restic.paths = [ "/var/html/deploys.tvl.fyi" ];
+  };
+}
diff --git a/ops/modules/www/images.tvl.fyi.nix b/ops/modules/www/images.tvl.fyi.nix
new file mode 100644
index 0000000000..7d027b2991
--- /dev/null
+++ b/ops/modules/www/images.tvl.fyi.nix
@@ -0,0 +1,22 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."images.tvl.fyi" = {
+      serverName = "images.tvl.fyi";
+      serverAliases = [ "images.tvl.su" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:${toString config.services.depot.nixery.port};
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/nixery.dev.nix b/ops/modules/www/nixery.dev.nix
new file mode 100644
index 0000000000..05dc88c66a
--- /dev/null
+++ b/ops/modules/www/nixery.dev.nix
@@ -0,0 +1,21 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."nixery.dev" = {
+      serverName = "nixery.dev";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:${toString config.services.depot.nixery.port};
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/self-redirect.nix b/ops/modules/www/self-redirect.nix
new file mode 100644
index 0000000000..5bf1627be9
--- /dev/null
+++ b/ops/modules/www/self-redirect.nix
@@ -0,0 +1,27 @@
+# Redirect the hostname of a machine to its configuration in a web
+# browser.
+#
+# Works by convention, assuming that the machine has its configuration
+# at //ops/machines/${hostname}.
+{ config, ... }:
+
+let
+  host = "${config.networking.hostName}.${config.networking.domain}";
+in
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config.services.nginx.virtualHosts."${host}" = {
+    serverName = host;
+    addSSL = true; # SSL is not forced on these redirects
+    enableACME = true;
+
+    extraConfig = ''
+      location = / {
+        return 302 https://at.tvl.fyi/?q=%2F%2Fops%2Fmachines%2F${config.networking.hostName};
+      }
+    '';
+  };
+}
diff --git a/ops/modules/www/static.tvl.fyi.nix b/ops/modules/www/static.tvl.fyi.nix
new file mode 100644
index 0000000000..7312f78ecf
--- /dev/null
+++ b/ops/modules/www/static.tvl.fyi.nix
@@ -0,0 +1,42 @@
+# Host the static assets at static.tvl.fyi
+#
+# All assets are served from $base/$drvhash/$file, but can also be
+# included with `latest/` which will return a (non-permanent!)
+# redirect to the real location.
+#
+# For all purposes within depot, using the drvhash of web.static is
+# recommended.
+{ depot, pkgs, ... }:
+
+let staticHash = depot.web.static.drvHash;
+in {
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."static.tvl.fyi" = {
+      serverAliases = [ "static.tvl.su" ];
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location = / {
+          add_header Content-Type text/plain;
+          return 200 "looking for tvl.fyi or tvl.su?";
+        }
+
+        location /latest {
+          rewrite ^/latest/(.*) /${staticHash}/$1 redirect;
+        }
+
+        location /${staticHash}/ {
+          alias ${depot.web.static}/;
+          expires max;
+          add_header Access-Control-Allow-Origin "*";
+          add_header Cache-Control "public";
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/status.tvl.su.nix b/ops/modules/www/status.tvl.su.nix
new file mode 100644
index 0000000000..2bb6093c14
--- /dev/null
+++ b/ops/modules/www/status.tvl.su.nix
@@ -0,0 +1,25 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."status-fyi" = {
+      serverName = "status.tvl.fyi";
+      enableACME = true;
+      extraConfig = "return 302 https://status.tvl.su$request_uri;";
+    };
+
+    services.nginx.virtualHosts.grafana = {
+      serverName = "status.tvl.su";
+      enableACME = true;
+      forceSSL = true;
+
+      locations."/" = {
+        proxyPass = "http://localhost:${toString config.services.grafana.port}";
+      };
+    };
+  };
+}
diff --git a/ops/modules/www/tazj.in.nix b/ops/modules/www/tazj.in.nix
new file mode 100644
index 0000000000..7d658a5ec4
--- /dev/null
+++ b/ops/modules/www/tazj.in.nix
@@ -0,0 +1,40 @@
+# serve tazjin's website & blog
+{ depot, config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tazj.in" = {
+      enableACME = true;
+      forceSSL = true;
+      root = depot.users.tazjin.homepage;
+
+      extraConfig = ''
+        ${depot.users.tazjin.blog.oldRedirects}
+        location /blog/ {
+          alias ${depot.users.tazjin.blog.rendered}/;
+
+          if ($request_uri ~ ^/(.*)\.html$) {
+            return 302 /$1;
+          }
+
+          try_files $uri $uri.html $uri/ =404;
+        }
+
+        # Temporary place for serving static files.
+        location /blobs/ {
+          alias /var/lib/tazjins-blobs/;
+        }
+      '';
+    };
+
+    services.nginx.virtualHosts."git.tazj.in" = {
+      enableACME = true;
+      forceSSL = true;
+      extraConfig = "return 301 https://code.tvl.fyi$request_uri;";
+    };
+  };
+}
diff --git a/ops/modules/www/todo.tvl.fyi.nix b/ops/modules/www/todo.tvl.fyi.nix
new file mode 100644
index 0000000000..b53f5437e7
--- /dev/null
+++ b/ops/modules/www/todo.tvl.fyi.nix
@@ -0,0 +1,25 @@
+{ depot, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."todo.tvl.fyi" = {
+      serverName = "todo.tvl.fyi";
+      serverAliases = [ "todo.tvl.su" ];
+      root = depot.web.todolist;
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+        location ~* \.(webp|woff2)$ {
+          add_header Cache-Control "public, max-age=31536000";
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/tvl.fyi.nix b/ops/modules/www/tvl.fyi.nix
new file mode 100644
index 0000000000..59ee1bc27f
--- /dev/null
+++ b/ops/modules/www/tvl.fyi.nix
@@ -0,0 +1,47 @@
+{ depot, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tvl.fyi" = {
+      serverName = "tvl.fyi";
+      root = depot.web.tvl;
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+        rewrite ^/builds/?$ https://buildkite.com/tvl/depot/ last;
+
+        rewrite ^/monorepo-doc/?$ https://docs.google.com/document/d/1nnyByXcH0F6GOmEezNOUa2RFelpeRpDToBLYD_CtjWE/edit?usp=sharing last;
+
+        rewrite ^/irc/?$ ircs://irc.hackint.org:6697/#tvl last;
+        rewrite ^/webchat/?$ https://webirc.hackint.org/#ircs://irc.hackint.org/#tvl last;
+
+        location ~* \.(webp|woff2)$ {
+          add_header Cache-Control "public, max-age=31536000";
+        }
+
+        location /blog {
+          if ($request_uri ~ ^/(.*)\.html$) {
+            return 302 /$1;
+          }
+
+          try_files $uri $uri.html $uri/ =404;
+        }
+
+        location = /blog {
+          return 302 /#blog;
+        }
+
+        location = /blog/ {
+          return 302 /#blog;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/tvl.su.nix b/ops/modules/www/tvl.su.nix
new file mode 100644
index 0000000000..a7c4f6a217
--- /dev/null
+++ b/ops/modules/www/tvl.su.nix
@@ -0,0 +1,20 @@
+{ depot, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tvl.su" = {
+      serverName = "tvl.su";
+      root = depot.corp.website;
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/wigglydonke.rs.nix b/ops/modules/www/wigglydonke.rs.nix
new file mode 100644
index 0000000000..3d85e4eb98
--- /dev/null
+++ b/ops/modules/www/wigglydonke.rs.nix
@@ -0,0 +1,15 @@
+{ depot, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."wigglydonke.rs" = {
+      enableACME = true;
+      forceSSL = true;
+      root = "${depot.path + "/users/grfn/wigglydonke.rs"}";
+    };
+  };
+}
diff --git a/ops/mq_cli/.gitignore b/ops/mq_cli/.gitignore
new file mode 100644
index 0000000000..5bd19a47c3
--- /dev/null
+++ b/ops/mq_cli/.gitignore
@@ -0,0 +1,4 @@
+/target/
+**/*.rs.bk
+.idea/
+*.iml
diff --git a/ops/mq_cli/CODE_OF_CONDUCT.md b/ops/mq_cli/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..c4013ac13e
--- /dev/null
+++ b/ops/mq_cli/CODE_OF_CONDUCT.md
@@ -0,0 +1,20 @@
+A SERMON ON ETHICS AND LOVE
+===========================
+
+One day Mal-2 asked the messenger spirit Saint Gulik to approach the Goddess and request Her presence for some desperate advice. Shortly afterwards the radio came on by itself, and an ethereal female Voice said **YES?**
+
+"O! Eris! Blessed Mother of Man! Queen of Chaos! Daughter of Discord! Concubine of Confusion! O! Exquisite Lady, I beseech You to lift a heavy burden from my heart!"
+
+**WHAT BOTHERS YOU, MAL? YOU DON'T SOUND WELL.**
+
+"I am filled with fear and tormented with terrible visions of pain. Everywhere people are hurting one another, the planet is rampant with injustices, whole societies plunder groups of their own people, mothers imprison sons, children perish while brothers war. O, woe."
+
+**WHAT IS THE MATTER WITH THAT, IF IT IS WHAT YOU WANT TO DO?**
+
+"But nobody Wants it! Everybody hates it."
+
+**OH. WELL, THEN *STOP*.**
+
+At which moment She turned herself into an aspirin commercial and left The Polyfather stranded alone with his species.
+
+SINISTER DEXTER HAS A BROKEN SPIROMETER.
diff --git a/ops/mq_cli/Cargo.lock b/ops/mq_cli/Cargo.lock
new file mode 100644
index 0000000000..18fed3621d
--- /dev/null
+++ b/ops/mq_cli/Cargo.lock
@@ -0,0 +1,168 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mq_cli"
+version = "3773.0.0"
+dependencies = [
+ "clap",
+ "libc",
+ "nix",
+ "posix_mq",
+]
+
+[[package]]
+name = "nix"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "posix_mq"
+version = "3771.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f462ad79a99ea13f3ef76d9c271956e924183f5aeb67a8649c8c2b6bdd079da8"
+dependencies = [
+ "libc",
+ "nix",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/ops/mq_cli/Cargo.toml b/ops/mq_cli/Cargo.toml
new file mode 100644
index 0000000000..816a370759
--- /dev/null
+++ b/ops/mq_cli/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "mq_cli"
+description = "CLI tool for accessing POSIX message queues (mq_overview(7))"
+license = "MIT"
+version = "3773.0.0"
+authors = ["Vincent Ambo <tazjin@tvl.su>"]
+homepage = "https://cs.tvl.fyi/depot/-/tree/ops/mq_cli"
+repository = "https://code.tvl.fyi/depot.git:/ops/mq_cli.git"
+
+[dependencies]
+clap = "2.34"
+libc = "0.2"
+nix = "0.23"
+posix_mq = "3771.0.0"
diff --git a/ops/mq_cli/LICENSE b/ops/mq_cli/LICENSE
new file mode 100644
index 0000000000..e289cbab81
--- /dev/null
+++ b/ops/mq_cli/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2017-2018 Langler AS
+Copyright (c) 2019-2020 Vincent Ambo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ops/mq_cli/README.md b/ops/mq_cli/README.md
new file mode 100644
index 0000000000..1045de896b
--- /dev/null
+++ b/ops/mq_cli/README.md
@@ -0,0 +1,42 @@
+mq-cli
+======
+
+This project provides a very simple CLI interface to [POSIX message queues][].
+
+It can be used to create and inspect queues, as well as send and
+receive messages from them.
+
+```
+1.0.0
+Administrate and inspect POSIX message queues
+
+USAGE:
+    mq <SUBCOMMAND>
+
+FLAGS:
+    -h, --help       Prints help information
+    -V, --version    Prints version information
+
+SUBCOMMANDS:
+    create     Create a new queue
+    help       Prints this message or the help of the given subcommand(s)
+    inspect    inspect details about a queue
+    ls         list message queues
+    receive    Receive a message from a queue
+    rlimit     Get the message queue rlimit
+    send       Send a message to a queue
+```
+
+## Development
+
+Development happens in the [TVL
+monorepo](https://cs.tvl.fyi/depot/-/tree/ops/mq_cli).
+
+Starting from version `3773.0.0`, the version numbers correspond to
+_revisions_ of the TVL repository, available as git refs (e.g.
+`refs/r/3773`).
+
+See the TVL documentation for more information about how to contribute
+to the codebase.
+
+[POSIX message queues]: https://linux.die.net/man/7/mq_overview
diff --git a/ops/mq_cli/default.nix b/ops/mq_cli/default.nix
new file mode 100644
index 0000000000..6b0e32009a
--- /dev/null
+++ b/ops/mq_cli/default.nix
@@ -0,0 +1,3 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage ./.
diff --git a/ops/mq_cli/src/main.rs b/ops/mq_cli/src/main.rs
new file mode 100644
index 0000000000..927993b486
--- /dev/null
+++ b/ops/mq_cli/src/main.rs
@@ -0,0 +1,235 @@
+extern crate clap;
+extern crate libc;
+extern crate nix;
+extern crate posix_mq;
+
+use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
+use posix_mq::{Message, Name, Queue};
+use std::fs::{read_dir, File};
+use std::io::{self, Read, Write};
+use std::process::exit;
+
+fn run_ls() {
+    let mqueues = read_dir("/dev/mqueue").expect("Could not read message queues");
+
+    for queue in mqueues {
+        let path = queue.unwrap().path();
+        let status = {
+            let mut file = File::open(&path).expect("Could not open queue file");
+
+            let mut content = String::new();
+            file.read_to_string(&mut content)
+                .expect("Could not read queue file");
+
+            content
+        };
+
+        let queue_name = path
+            .components()
+            .last()
+            .unwrap()
+            .as_os_str()
+            .to_string_lossy();
+
+        println!("/{}: {}", queue_name, status)
+    }
+}
+
+fn run_inspect(queue_name: &str) {
+    let name = Name::new(queue_name).expect("Invalid queue name");
+    let queue = Queue::open(name).expect("Could not open queue");
+
+    println!("Queue {}:\n", queue_name);
+    println!("Max. message size: {} bytes", queue.max_size());
+    println!("Max. # of pending messages: {}", queue.max_pending());
+}
+
+fn run_create(cmd: &ArgMatches) {
+    if let Some(rlimit) = cmd.value_of("rlimit") {
+        set_rlimit(rlimit.parse().expect("Invalid rlimit value"));
+    }
+
+    let name = Name::new(cmd.value_of("queue").unwrap()).expect("Invalid queue name");
+
+    let max_pending: i64 = cmd.value_of("max-pending").unwrap().parse().unwrap();
+    let max_size: i64 = cmd.value_of("max-size").unwrap().parse().unwrap();
+
+    let queue = Queue::create(name, max_pending, max_size * 1024);
+
+    match queue {
+        Ok(_) => println!("Queue created successfully"),
+        Err(e) => {
+            writeln!(io::stderr(), "Could not create queue: {}", e).ok();
+            exit(1);
+        }
+    };
+}
+
+fn run_receive(queue_name: &str) {
+    let name = Name::new(queue_name).expect("Invalid queue name");
+    let queue = Queue::open(name).expect("Could not open queue");
+
+    let message = match queue.receive() {
+        Ok(msg) => msg,
+        Err(e) => {
+            writeln!(io::stderr(), "Failed to receive message: {}", e).ok();
+            exit(1);
+        }
+    };
+
+    // Attempt to write the message out as a string, but write out raw bytes if it turns out to not
+    // be UTF-8 encoded data.
+    match String::from_utf8(message.data.clone()) {
+        Ok(string) => println!("{}", string),
+        Err(_) => {
+            writeln!(io::stderr(), "Message not UTF-8 encoded!").ok();
+            io::stdout().write(message.data.as_ref()).ok();
+        }
+    };
+}
+
+fn run_send(queue_name: &str, content: &str) {
+    let name = Name::new(queue_name).expect("Invalid queue name");
+    let queue = Queue::open(name).expect("Could not open queue");
+
+    let message = Message {
+        data: content.as_bytes().to_vec(),
+        priority: 0,
+    };
+
+    match queue.send(&message) {
+        Ok(_) => (),
+        Err(e) => {
+            writeln!(io::stderr(), "Could not send message: {}", e).ok();
+            exit(1);
+        }
+    }
+}
+
+fn run_rlimit() {
+    let mut rlimit = libc::rlimit {
+        rlim_cur: 0,
+        rlim_max: 0,
+    };
+
+    let mut errno = 0;
+    unsafe {
+        let res = libc::getrlimit(libc::RLIMIT_MSGQUEUE, &mut rlimit);
+        if res != 0 {
+            errno = nix::errno::errno();
+        }
+    };
+
+    if errno != 0 {
+        writeln!(
+            io::stderr(),
+            "Could not get message queue rlimit: {}",
+            errno
+        )
+        .ok();
+    } else {
+        println!("Message queue rlimit:");
+        println!("Current limit: {}", rlimit.rlim_cur);
+        println!("Maximum limit: {}", rlimit.rlim_max);
+    }
+}
+
+fn set_rlimit(new_limit: u64) {
+    let rlimit = libc::rlimit {
+        rlim_cur: new_limit,
+        rlim_max: new_limit,
+    };
+
+    let mut errno: i32 = 0;
+    unsafe {
+        let res = libc::setrlimit(libc::RLIMIT_MSGQUEUE, &rlimit);
+        if res != 0 {
+            errno = nix::errno::errno();
+        }
+    }
+
+    match errno {
+        0 => println!("Set RLIMIT_MSGQUEUE hard limit to {}", new_limit),
+        _ => {
+            // Not mapping these error codes to messages for now, the user can
+            // look up the meaning in setrlimit(2).
+            panic!("Could not set hard limit: {}", errno);
+        }
+    };
+}
+
+fn main() {
+    let ls = SubCommand::with_name("ls").about("list message queues");
+
+    let queue_arg = Arg::with_name("queue").required(true).takes_value(true);
+
+    let rlimit_arg = Arg::with_name("rlimit")
+        .help("RLIMIT_MSGQUEUE to set for this command")
+        .long("rlimit")
+        .takes_value(true);
+
+    let inspect = SubCommand::with_name("inspect")
+        .about("inspect details about a queue")
+        .arg(&queue_arg);
+
+    let create = SubCommand::with_name("create")
+        .about("Create a new queue")
+        .arg(&queue_arg)
+        .arg(&rlimit_arg)
+        .arg(
+            Arg::with_name("max-size")
+                .help("maximum message size (in kB)")
+                .long("max-size")
+                .required(true)
+                .takes_value(true),
+        )
+        .arg(
+            Arg::with_name("max-pending")
+                .help("maximum # of pending messages")
+                .long("max-pending")
+                .required(true)
+                .takes_value(true),
+        );
+
+    let receive = SubCommand::with_name("receive")
+        .about("Receive a message from a queue")
+        .arg(&queue_arg);
+
+    let send = SubCommand::with_name("send")
+        .about("Send a message to a queue")
+        .arg(&queue_arg)
+        .arg(
+            Arg::with_name("message")
+                .help("the message to send")
+                .required(true),
+        );
+
+    let rlimit = SubCommand::with_name("rlimit")
+        .about("Get the message queue rlimit")
+        .setting(AppSettings::SubcommandRequiredElseHelp);
+
+    let matches = App::new("mq")
+        .setting(AppSettings::SubcommandRequiredElseHelp)
+        .version("1.0.0")
+        .about("Administrate and inspect POSIX message queues")
+        .subcommand(ls)
+        .subcommand(inspect)
+        .subcommand(create)
+        .subcommand(receive)
+        .subcommand(send)
+        .subcommand(rlimit)
+        .get_matches();
+
+    match matches.subcommand() {
+        ("ls", _) => run_ls(),
+        ("inspect", Some(cmd)) => run_inspect(cmd.value_of("queue").unwrap()),
+        ("create", Some(cmd)) => run_create(cmd),
+        ("receive", Some(cmd)) => run_receive(cmd.value_of("queue").unwrap()),
+        ("send", Some(cmd)) => run_send(
+            cmd.value_of("queue").unwrap(),
+            cmd.value_of("message").unwrap(),
+        ),
+        ("rlimit", _) => run_rlimit(),
+        _ => unimplemented!(),
+    }
+}
diff --git a/ops/nixos.nix b/ops/nixos.nix
new file mode 100644
index 0000000000..309f122977
--- /dev/null
+++ b/ops/nixos.nix
@@ -0,0 +1,63 @@
+# Helper functions for instantiating depot-compatible NixOS machines.
+{ depot, lib, pkgs, ... }@args:
+
+let inherit (lib) findFirst isAttrs;
+in rec {
+  # This provides our standard set of arguments to all NixOS modules.
+  baseModule = { ... }: {
+    # Ensure that pkgs == third_party.nix
+    nixpkgs.pkgs = depot.third_party.nixpkgs;
+    nix.nixPath =
+      let
+        # Due to nixpkgsBisectPath, pkgs.path is not always in the nix store
+        nixpkgsStorePath =
+          if lib.hasPrefix builtins.storeDir (toString pkgs.path)
+          then builtins.storePath pkgs.path # nixpkgs is already in the store
+          else pkgs.path; # we need to dump nixpkgs to the store either way
+      in
+      [
+        ("nixos=" + nixpkgsStorePath)
+        ("nixpkgs=" + nixpkgsStorePath)
+      ];
+  };
+
+  nixosFor = configuration: (depot.third_party.nixos {
+    configuration = { ... }: {
+      imports = [
+        baseModule
+        configuration
+      ];
+    };
+
+    specialArgs = {
+      inherit (args) depot;
+    };
+  });
+
+  findSystem = hostname:
+    (findFirst
+      (system: system.config.networking.hostName == hostname)
+      (throw "${hostname} is not a known NixOS host")
+      (map nixosFor depot.ops.machines.all-systems));
+
+  rebuild-system = rebuildSystemWith depot.path;
+
+  rebuildSystemWith = depotPath: pkgs.writeShellScriptBin "rebuild-system" ''
+    set -ue
+    if [[ $EUID -ne 0 ]]; then
+      echo "Oh no! Only root is allowed to rebuild the system!" >&2
+      exit 1
+    fi
+
+    echo "Rebuilding NixOS for $HOSTNAME"
+    system=$(${pkgs.nix}/bin/nix-build -E "((import ${depotPath} {}).ops.nixos.findSystem \"$HOSTNAME\").system" --no-out-link --show-trace)
+
+    ${pkgs.nix}/bin/nix-env -p /nix/var/nix/profiles/system --set $system
+    $system/bin/switch-to-configuration switch
+  '';
+
+  # Systems that should be built in CI
+  whitbySystem = (nixosFor depot.ops.machines.whitby).system;
+  sandunySystem = (nixosFor depot.ops.machines.sanduny).system;
+  meta.ci.targets = [ "sandunySystem" "whitbySystem" ];
+}
diff --git a/ops/pipelines/README.md b/ops/pipelines/README.md
new file mode 100644
index 0000000000..a3f94fd231
--- /dev/null
+++ b/ops/pipelines/README.md
@@ -0,0 +1,5 @@
+This folder contains the dynamic configuration for our [Buildkite CI
+setup](https://tvl.fyi/builds).
+
+The configuration is built and dynamically loaded by Buildkite at the start of
+each CI pipeline.
diff --git a/ops/pipelines/depot.nix b/ops/pipelines/depot.nix
new file mode 100644
index 0000000000..6d9e625e04
--- /dev/null
+++ b/ops/pipelines/depot.nix
@@ -0,0 +1,49 @@
+# This file configures the primary build pipeline used for the
+# top-level list of depot targets.
+{ depot, pkgs, externalArgs, ... }:
+
+let
+  # Protobuf check step which validates that changes to .proto files
+  # between revisions don't cause backwards-incompatible or otherwise
+  # flawed changes.
+  protoCheck = {
+    command = "${depot.nix.bufCheck}/bin/ci-buf-check";
+    label = ":water_buffalo:";
+  };
+
+  pipeline = depot.nix.buildkite.mkPipeline {
+    headBranch = "refs/heads/canon";
+    drvTargets = depot.ci.targets;
+    additionalSteps = [ protoCheck ];
+
+    parentTargetMap =
+      if (externalArgs ? parentTargetMap)
+      then builtins.fromJSON (builtins.readFile externalArgs.parentTargetMap)
+      else { };
+
+    postBuildSteps = [
+      # After successful builds, create a gcroot for builds on canon.
+      #
+      # This anchors *most* of the depot, in practice it's unimportant
+      # if there is a build race and we get +-1 of the targets.
+      #
+      # Unfortunately this requires a third evaluation of the graph, but
+      # since it happens after :duck: it should not affect the timing of
+      # status reporting back to Gerrit.
+      {
+        label = ":anchor:";
+        branches = "refs/heads/canon";
+        command = ''
+          nix-build -A ci.gcroot --out-link /nix/var/nix/gcroots/depot/canon
+        '';
+      }
+    ];
+  };
+
+  drvmap = depot.nix.buildkite.mkDrvmap depot.ci.targets;
+in
+pkgs.runCommandNoCC "depot-pipeline" { } ''
+  mkdir $out
+  cp -r ${pipeline}/* $out
+  cp ${drvmap} $out/drvmap.json
+''
diff --git a/ops/pipelines/static-pipeline.yaml b/ops/pipelines/static-pipeline.yaml
new file mode 100644
index 0000000000..2e35a8a179
--- /dev/null
+++ b/ops/pipelines/static-pipeline.yaml
@@ -0,0 +1,116 @@
+# This file defines the static Buildkite pipeline which attempts to
+# create the dynamic pipeline of all depot targets.
+#
+# If something fails during the creation of the pipeline, the fallback
+# is executed instead which will simply report an error to Gerrit.
+---
+env:
+  BUILDKITE_TOKEN_PATH: /run/agenix/buildkite-graphql-token
+steps:
+  # Run pipeline for tvl-kit when new commits arrive on canon. Since
+  # it is not part of the depot build tree, this is a useful
+  # verification to ensure we don't break external things (too much).
+  - trigger: "tvl-kit"
+    async: true
+    label: ":fork:"
+    branches: "refs/heads/canon"
+    build:
+      message: "Verification triggered by ${BUILDKITE_COMMIT}"
+
+  # Create a revision number for the current commit for builds on
+  # canon.
+  #
+  # This writes data back to Gerrit using the Buildkite agent
+  # credentials injected through a git credentials helper.
+  #
+  # Revision numbers are defined as the number of commits in the
+  # lineage of HEAD, following only the first parent of merges.
+  - label: ":git:"
+    branches: "refs/heads/canon"
+    command: |
+      git -c 'credential.helper=gerrit-creds' \
+        push origin "HEAD:refs/r/$(git rev-list --count --first-parent HEAD)"
+
+  # Generate & upload dynamic build steps
+  - label: ":llama:"
+    key: "pipeline-gen"
+    command: |
+      set -ue
+
+      if test -n "$${GERRIT_CHANGE_URL-}"; then
+        echo "This is a build of [cl/$$GERRIT_CHANGE_ID]($$GERRIT_CHANGE_URL) (at patchset #$$GERRIT_PATCHSET)" | \
+          buildkite-agent annotate
+      fi
+
+      # Attempt to fetch a target map from a parent commit on canon,
+      # except on builds of canon itself.
+      [ "${BUILDKITE_BRANCH}" != "refs/heads/canon" ] && \
+        nix/buildkite/fetch-parent-targets.sh
+
+      PIPELINE_ARGS=""
+      if [[ -f tmp/parent-target-map.json ]]; then
+        PIPELINE_ARGS="--arg parentTargetMap tmp/parent-target-map.json"
+      fi
+
+      nix-build --option restrict-eval true --include "depot=$${PWD}"\
+        --allowed-uris 'https://' \
+        -A ops.pipelines.depot \
+        -o pipeline --show-trace $$PIPELINE_ARGS
+
+      # Steps need to be uploaded in reverse order because pipeline
+      # upload prepends instead of appending.
+      ls pipeline/build-chunk-*.json | tac | while read chunk; do
+        buildkite-agent pipeline upload $$chunk
+      done
+
+      buildkite-agent artifact upload "pipeline/*"
+
+  # Wait for all previous steps to complete.
+  - wait: null
+    continue_on_failure: true
+
+  # Exit with success or failure depending on whether any other steps
+  # failed.
+  #
+  # This information is checked by querying the Buildkite GraphQL API
+  # and fetching the count of failed steps.
+  #
+  # This step must be :duck: (yes, really!) because the post-command
+  # hook will inspect this name.
+  #
+  # Note that this step has requirements for the agent environment, which
+  # are enforced in our NixOS configuration:
+  #
+  #  * curl and jq must be on the $PATH of build agents
+  #  * besadii configuration must be readable to the build agents
+  - label: ":duck:"
+    key: ":duck:"
+    command: |
+      set -ueo pipefail
+
+      readonly FAILED_JOBS=$(curl 'https://graphql.buildkite.com/v1' \
+        --silent \
+        -H "Authorization: Bearer $(cat ${BUILDKITE_TOKEN_PATH})" \
+        -d "{\"query\": \"query BuildStatusQuery { build(uuid: \\\"$BUILDKITE_BUILD_ID\\\") { jobs(passed: false) { count } } }\"}" | \
+        jq -r '.data.build.jobs.count')
+
+      echo "$$FAILED_JOBS build jobs failed."
+
+      if (( $$FAILED_JOBS > 0 )); then
+        exit 1
+      fi
+
+  # After duck, on success, upload and run any post-build steps that
+  # were output by the dynamic pipeline.
+  - label: ":arrow_heading_down:"
+    depends_on:
+      - step: ":duck:"
+        allow_failure: false
+    command: |
+      set -ueo pipefail
+
+      buildkite-agent artifact download "pipeline/*" .
+
+      find ./pipeline -name 'post-chunk-*.json' | tac | while read chunk; do
+        buildkite-agent pipeline upload $$chunk
+      done
diff --git a/ops/posix_mq.rs/.gitignore b/ops/posix_mq.rs/.gitignore
new file mode 100644
index 0000000000..e5b6fdb28e
--- /dev/null
+++ b/ops/posix_mq.rs/.gitignore
@@ -0,0 +1,3 @@
+/target/
+**/*.rs.bk
+.idea/
diff --git a/ops/posix_mq.rs/CODE_OF_CONDUCT.md b/ops/posix_mq.rs/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..c4013ac13e
--- /dev/null
+++ b/ops/posix_mq.rs/CODE_OF_CONDUCT.md
@@ -0,0 +1,20 @@
+A SERMON ON ETHICS AND LOVE
+===========================
+
+One day Mal-2 asked the messenger spirit Saint Gulik to approach the Goddess and request Her presence for some desperate advice. Shortly afterwards the radio came on by itself, and an ethereal female Voice said **YES?**
+
+"O! Eris! Blessed Mother of Man! Queen of Chaos! Daughter of Discord! Concubine of Confusion! O! Exquisite Lady, I beseech You to lift a heavy burden from my heart!"
+
+**WHAT BOTHERS YOU, MAL? YOU DON'T SOUND WELL.**
+
+"I am filled with fear and tormented with terrible visions of pain. Everywhere people are hurting one another, the planet is rampant with injustices, whole societies plunder groups of their own people, mothers imprison sons, children perish while brothers war. O, woe."
+
+**WHAT IS THE MATTER WITH THAT, IF IT IS WHAT YOU WANT TO DO?**
+
+"But nobody Wants it! Everybody hates it."
+
+**OH. WELL, THEN *STOP*.**
+
+At which moment She turned herself into an aspirin commercial and left The Polyfather stranded alone with his species.
+
+SINISTER DEXTER HAS A BROKEN SPIROMETER.
diff --git a/ops/posix_mq.rs/Cargo.lock b/ops/posix_mq.rs/Cargo.lock
new file mode 100644
index 0000000000..dc344613d0
--- /dev/null
+++ b/ops/posix_mq.rs/Cargo.lock
@@ -0,0 +1,63 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "cc"
+version = "1.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "libc"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "nix"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "posix_mq"
+version = "3771.0.0"
+dependencies = [
+ "libc",
+ "nix",
+]
diff --git a/ops/posix_mq.rs/Cargo.toml b/ops/posix_mq.rs/Cargo.toml
new file mode 100644
index 0000000000..8390b80b86
--- /dev/null
+++ b/ops/posix_mq.rs/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "posix_mq"
+version = "3771.0.0"
+authors = ["Vincent Ambo <tazjin@tvl.su>"]
+description = "(Higher-level) Rust bindings to POSIX message queues"
+license = "MIT"
+homepage = "https://cs.tvl.fyi/depot/-/tree/ops/posix_mq.rs"
+repository = "https://code.tvl.fyi/depot.git:/ops/posix_mq.rs.git"
+
+[dependencies]
+nix = "0.23"
+libc = "0.2"
diff --git a/ops/posix_mq.rs/LICENSE b/ops/posix_mq.rs/LICENSE
new file mode 100644
index 0000000000..2389546b13
--- /dev/null
+++ b/ops/posix_mq.rs/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-2020 Vincent Ambo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ops/posix_mq.rs/README.md b/ops/posix_mq.rs/README.md
new file mode 100644
index 0000000000..800d2221e4
--- /dev/null
+++ b/ops/posix_mq.rs/README.md
@@ -0,0 +1,44 @@
+posix_mq
+========
+
+[![crates.io](https://img.shields.io/crates/v/posix_mq.svg)](https://crates.io/crates/posix_mq)
+
+This is a simple, relatively high-level library for the POSIX [message queue API][]. It wraps the lower-level API in a
+simpler interface with more robust error handling.
+
+Check out this project's [sister library][] in Kotlin.
+
+Usage example:
+
+```rust
+// Values that need to undergo validation are wrapped in safe types:
+let name = Name::new("/test-queue").unwrap();
+
+// Queue creation with system defaults is simple:
+let queue = Queue::open_or_create(name).expect("Opening queue failed");
+
+// Sending a message:
+let message = Message {
+  data: "test-message".as_bytes().to_vec(),
+  priority: 0,
+};
+queue.send(&message).expect("message sending failed");
+
+// ... and receiving it!
+let result = queue.receive().expect("message receiving failed");
+```
+
+## Development
+
+Development happens in the [TVL
+monorepo](https://cs.tvl.fyi/depot/-/tree/ops/posix_mq.rs).
+
+Starting from version `3771.0.0`, the version numbers correspond to
+_revisions_ of the TVL repository, available as git refs (e.g.
+`refs/r/3771`).
+
+See the TVL documentation for more information about how to contribute
+to the codebase.
+
+[message queue API]: https://linux.die.net/man/7/mq_overview
+[sister library]: https://github.com/aprilabank/posix_mq.kt
diff --git a/ops/posix_mq.rs/default.nix b/ops/posix_mq.rs/default.nix
new file mode 100644
index 0000000000..6b0e32009a
--- /dev/null
+++ b/ops/posix_mq.rs/default.nix
@@ -0,0 +1,3 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage ./.
diff --git a/ops/posix_mq.rs/src/error.rs b/ops/posix_mq.rs/src/error.rs
new file mode 100644
index 0000000000..bacd2aeb39
--- /dev/null
+++ b/ops/posix_mq.rs/src/error.rs
@@ -0,0 +1,122 @@
+use nix;
+use std::{error, fmt, io, num};
+
+/// This module implements a simple error type to match the errors that can be thrown from the C
+/// functions as well as some extra errors resulting from internal validations.
+///
+/// As this crate exposes an opinionated API to the POSIX queues certain errors have been
+/// ignored:
+///
+/// * ETIMEDOUT: The low-level timed functions are not exported and this error can not occur.
+/// * EAGAIN: Non-blocking queue calls are not supported.
+/// * EINVAL: Same reason as ETIMEDOUT
+/// * EMSGSIZE: The message size is immutable after queue creation and this crate checks it.
+/// * ENAMETOOLONG: This crate performs name validation
+///
+/// If an unexpected error is encountered it will be wrapped appropriately and should be reported
+/// as a bug on https://b.tvl.fyi
+
+#[derive(Debug)]
+pub enum Error {
+    // These errors are raised inside of the library
+    InvalidQueueName(&'static str),
+    ValueReadingError(io::Error),
+    MessageSizeExceeded(),
+    MaximumMessageSizeExceeded(),
+    MaximumMessageCountExceeded(),
+
+    // These errors match what is described in the man pages (from mq_overview(7) onwards).
+    PermissionDenied(),
+    InvalidQueueDescriptor(),
+    QueueCallInterrupted(),
+    QueueAlreadyExists(),
+    QueueNotFound(),
+    InsufficientMemory(),
+    InsufficientSpace(),
+
+    // These two are (hopefully) unlikely in modern systems
+    ProcessFileDescriptorLimitReached(),
+    SystemFileDescriptorLimitReached(),
+
+    // If an unhandled / unknown / unexpected error occurs this error will be used.
+    // In those cases bug reports would be welcome!
+    UnknownForeignError(nix::errno::Errno),
+
+    // Some other unexpected / unknown error occured. This is probably an error from
+    // the nix crate. Bug reports also welcome for this!
+    UnknownInternalError(),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use Error::*;
+        f.write_str(match *self {
+            // This error contains more sensible description strings already
+            InvalidQueueName(e) => e,
+            ValueReadingError(_) => "error reading system configuration for message queues",
+            MessageSizeExceeded() => "message is larger than maximum size for specified queue",
+            MaximumMessageSizeExceeded() => "specified queue message size exceeds system maximum",
+            MaximumMessageCountExceeded() => "specified queue message count exceeds system maximum",
+            PermissionDenied() => "permission to the specified queue was denied",
+            InvalidQueueDescriptor() => "the internal queue descriptor was invalid",
+            QueueCallInterrupted() => "queue method interrupted by signal",
+            QueueAlreadyExists() => "the specified queue already exists",
+            QueueNotFound() => "the specified queue could not be found",
+            InsufficientMemory() => "insufficient memory to call queue method",
+            InsufficientSpace() => "insufficient space to call queue method",
+            ProcessFileDescriptorLimitReached() => {
+                "maximum number of process file descriptors reached"
+            }
+            SystemFileDescriptorLimitReached() => {
+                "maximum number of system file descriptors reached"
+            }
+            UnknownForeignError(_) => "unknown foreign error occured: please report a bug!",
+            UnknownInternalError() => "unknown internal error occured: please report a bug!",
+        })
+    }
+}
+
+impl error::Error for Error {
+    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+        match self {
+            Error::ValueReadingError(e) => Some(e),
+            Error::UnknownForeignError(e) => Some(e),
+            _ => None,
+        }
+    }
+}
+
+/// This from implementation is used to translate errors from the lower-level
+/// C-calls into sensible Rust errors.
+impl From<nix::errno::Errno> for Error {
+    fn from(err: nix::Error) -> Self {
+        use nix::errno::Errno::*;
+        match err {
+            EACCES => Error::PermissionDenied(),
+            EBADF => Error::InvalidQueueDescriptor(),
+            EINTR => Error::QueueCallInterrupted(),
+            EEXIST => Error::QueueAlreadyExists(),
+            EMFILE => Error::ProcessFileDescriptorLimitReached(),
+            ENFILE => Error::SystemFileDescriptorLimitReached(),
+            ENOENT => Error::QueueNotFound(),
+            ENOMEM => Error::InsufficientMemory(),
+            ENOSPC => Error::InsufficientSpace(),
+            _ => Error::UnknownForeignError(err),
+        }
+    }
+}
+
+// This implementation is used when reading system queue settings.
+impl From<io::Error> for Error {
+    fn from(e: io::Error) -> Self {
+        Error::ValueReadingError(e)
+    }
+}
+
+// This implementation is used when parsing system queue settings. The unknown error is returned
+// here because the system is probably seriously broken if those files don't contain numbers.
+impl From<num::ParseIntError> for Error {
+    fn from(_: num::ParseIntError) -> Self {
+        Error::UnknownInternalError()
+    }
+}
diff --git a/ops/posix_mq.rs/src/lib.rs b/ops/posix_mq.rs/src/lib.rs
new file mode 100644
index 0000000000..ed35fb03be
--- /dev/null
+++ b/ops/posix_mq.rs/src/lib.rs
@@ -0,0 +1,247 @@
+extern crate libc;
+extern crate nix;
+
+use error::Error;
+use libc::mqd_t;
+use nix::mqueue;
+use nix::sys::stat;
+use std::ffi::CString;
+use std::fs::File;
+use std::io::Read;
+use std::ops::Drop;
+use std::string::ToString;
+
+pub mod error;
+
+#[cfg(test)]
+mod tests;
+
+/// Wrapper type for queue names that performs basic validation of queue names before calling
+/// out to C code.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Name(CString);
+
+impl Name {
+    pub fn new<S: ToString>(s: S) -> Result<Self, Error> {
+        let string = s.to_string();
+
+        if !string.starts_with('/') {
+            return Err(Error::InvalidQueueName("Queue name must start with '/'"));
+        }
+
+        // The C library has a special error return for this case, so I assume people must actually
+        // have tried just using '/' as a queue name.
+        if string.len() == 1 {
+            return Err(Error::InvalidQueueName(
+                "Queue name must be a slash followed by one or more characters",
+            ));
+        }
+
+        if string.len() > 255 {
+            return Err(Error::InvalidQueueName(
+                "Queue name must not exceed 255 characters",
+            ));
+        }
+
+        if string.matches('/').count() > 1 {
+            return Err(Error::InvalidQueueName(
+                "Queue name can not contain more than one slash",
+            ));
+        }
+
+        // TODO: What error is being thrown away here? Is it possible?
+        Ok(Name(CString::new(string).unwrap()))
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Message {
+    pub data: Vec<u8>,
+    pub priority: u32,
+}
+
+/// Represents an open queue descriptor to a POSIX message queue. This carries information
+/// about the queue's limitations (i.e. maximum message size and maximum message count).
+#[derive(Debug)]
+pub struct Queue {
+    name: Name,
+
+    /// Internal file/queue descriptor.
+    queue_descriptor: mqd_t,
+
+    /// Maximum number of pending messages in this queue.
+    max_pending: i64,
+
+    /// Maximum size of this queue.
+    max_size: usize,
+}
+
+impl Queue {
+    /// Creates a new queue and fails if it already exists.
+    /// By default the queue will be read/writable by the current user with no access for other
+    /// users.
+    /// Linux users can change this setting themselves by modifying the queue file in /dev/mqueue.
+    pub fn create(name: Name, max_pending: i64, max_size: i64) -> Result<Queue, Error> {
+        if max_pending > read_i64_from_file(MSG_MAX)? {
+            return Err(Error::MaximumMessageCountExceeded());
+        }
+
+        if max_size > read_i64_from_file(MSGSIZE_MAX)? {
+            return Err(Error::MaximumMessageSizeExceeded());
+        }
+
+        let oflags = {
+            let mut flags = mqueue::MQ_OFlag::empty();
+            // Put queue in r/w mode
+            flags.toggle(mqueue::MQ_OFlag::O_RDWR);
+            // Enable queue creation
+            flags.toggle(mqueue::MQ_OFlag::O_CREAT);
+            // Fail if queue exists already
+            flags.toggle(mqueue::MQ_OFlag::O_EXCL);
+            flags
+        };
+
+        let attr = mqueue::MqAttr::new(0, max_pending, max_size, 0);
+
+        let queue_descriptor = mqueue::mq_open(&name.0, oflags, default_mode(), Some(&attr))?;
+
+        Ok(Queue {
+            name,
+            queue_descriptor,
+            max_pending,
+            max_size: max_size as usize,
+        })
+    }
+
+    /// Opens an existing queue.
+    pub fn open(name: Name) -> Result<Queue, Error> {
+        // No extra flags need to be constructed as the default is to open and fail if the
+        // queue does not exist yet - which is what we want here.
+        let oflags = mqueue::MQ_OFlag::O_RDWR;
+        let queue_descriptor = mqueue::mq_open(&name.0, oflags, default_mode(), None)?;
+
+        let attr = mq_getattr(queue_descriptor)?;
+
+        Ok(Queue {
+            name,
+            queue_descriptor,
+            max_pending: attr.mq_maxmsg,
+            max_size: attr.mq_msgsize as usize,
+        })
+    }
+
+    /// Opens an existing queue or creates a new queue with the OS default settings.
+    pub fn open_or_create(name: Name) -> Result<Queue, Error> {
+        let oflags = {
+            let mut flags = mqueue::MQ_OFlag::empty();
+            // Put queue in r/w mode
+            flags.toggle(mqueue::MQ_OFlag::O_RDWR);
+            // Enable queue creation
+            flags.toggle(mqueue::MQ_OFlag::O_CREAT);
+            flags
+        };
+
+        let default_pending = read_i64_from_file(MSG_DEFAULT)?;
+        let default_size = read_i64_from_file(MSGSIZE_DEFAULT)?;
+        let attr = mqueue::MqAttr::new(0, default_pending, default_size, 0);
+
+        let queue_descriptor = mqueue::mq_open(&name.0, oflags, default_mode(), Some(&attr))?;
+
+        let actual_attr = mq_getattr(queue_descriptor)?;
+
+        Ok(Queue {
+            name,
+            queue_descriptor,
+            max_pending: actual_attr.mq_maxmsg,
+            max_size: actual_attr.mq_msgsize as usize,
+        })
+    }
+
+    /// Delete a message queue from the system. This method will make the queue unavailable for
+    /// other processes after their current queue descriptors have been closed.
+    pub fn delete(self) -> Result<(), Error> {
+        mqueue::mq_unlink(&self.name.0)?;
+        drop(self);
+        Ok(())
+    }
+
+    /// Send a message to the message queue.
+    /// If the queue is full this call will block until a message has been consumed.
+    pub fn send(&self, msg: &Message) -> Result<(), Error> {
+        if msg.data.len() > self.max_size as usize {
+            return Err(Error::MessageSizeExceeded());
+        }
+
+        mqueue::mq_send(self.queue_descriptor, msg.data.as_ref(), msg.priority)
+            .map_err(|e| e.into())
+    }
+
+    /// Receive a message from the message queue.
+    /// If the queue is empty this call will block until a message arrives.
+    pub fn receive(&self) -> Result<Message, Error> {
+        let mut data: Vec<u8> = vec![0; self.max_size as usize];
+        let mut priority: u32 = 0;
+
+        let msg_size = mqueue::mq_receive(self.queue_descriptor, data.as_mut(), &mut priority)?;
+
+        data.truncate(msg_size);
+        Ok(Message { data, priority })
+    }
+
+    pub fn max_pending(&self) -> i64 {
+        self.max_pending
+    }
+
+    pub fn max_size(&self) -> usize {
+        self.max_size
+    }
+}
+
+impl Drop for Queue {
+    fn drop(&mut self) {
+        // Attempt to close the queue descriptor and discard any possible errors.
+        // The only error thrown in the C-code is EINVAL, which would mean that the
+        // descriptor has already been closed.
+        mqueue::mq_close(self.queue_descriptor).ok();
+    }
+}
+
+// Creates the default queue mode (0600).
+fn default_mode() -> stat::Mode {
+    let mut mode = stat::Mode::empty();
+    mode.toggle(stat::Mode::S_IRUSR);
+    mode.toggle(stat::Mode::S_IWUSR);
+    mode
+}
+
+/// This file defines the default number of maximum pending messages in a queue.
+const MSG_DEFAULT: &'static str = "/proc/sys/fs/mqueue/msg_default";
+
+/// This file defines the system maximum number of pending messages in a queue.
+const MSG_MAX: &'static str = "/proc/sys/fs/mqueue/msg_max";
+
+/// This file defines the default maximum size of messages in a queue.
+const MSGSIZE_DEFAULT: &'static str = "/proc/sys/fs/mqueue/msgsize_default";
+
+/// This file defines the system maximum size for messages in a queue.
+const MSGSIZE_MAX: &'static str = "/proc/sys/fs/mqueue/msgsize_max";
+
+/// This method is used in combination with the above constants to find system limits.
+fn read_i64_from_file(name: &str) -> Result<i64, Error> {
+    let mut file = File::open(name.to_string())?;
+    let mut content = String::new();
+    file.read_to_string(&mut content)?;
+    Ok(content.trim().parse()?)
+}
+
+/// The mq_getattr implementation in the nix crate hides the maximum message size and count, which
+/// is very impractical.
+/// To work around it, this method calls the C-function directly.
+fn mq_getattr(mqd: mqd_t) -> Result<libc::mq_attr, Error> {
+    use std::mem;
+    let mut attr = mem::MaybeUninit::<libc::mq_attr>::uninit();
+    let res = unsafe { libc::mq_getattr(mqd, attr.as_mut_ptr()) };
+    nix::errno::Errno::result(res)
+        .map(|_| unsafe { attr.assume_init() })
+        .map_err(|e| e.into())
+}
diff --git a/ops/posix_mq.rs/src/tests.rs b/ops/posix_mq.rs/src/tests.rs
new file mode 100644
index 0000000000..1f4ea9a58d
--- /dev/null
+++ b/ops/posix_mq.rs/src/tests.rs
@@ -0,0 +1,21 @@
+use super::*;
+
+#[test]
+fn test_open_delete() {
+    // Simple test with default queue settings
+    let name = Name::new("/test-queue").unwrap();
+    let queue = Queue::open_or_create(name).expect("Opening queue failed");
+
+    let message = Message {
+        data: "test-message".as_bytes().to_vec(),
+        priority: 0,
+    };
+
+    queue.send(&message).expect("message sending failed");
+
+    let result = queue.receive().expect("message receiving failed");
+
+    assert_eq!(message, result);
+
+    queue.delete().expect("deleting queue failed");
+}
diff --git a/ops/secrets/.skip-subtree b/ops/secrets/.skip-subtree
new file mode 100644
index 0000000000..80f63816f5
--- /dev/null
+++ b/ops/secrets/.skip-subtree
@@ -0,0 +1,2 @@
+The Nix configuration in here is read by agenix and not compatible
+with readTree.
diff --git a/ops/secrets/README.md b/ops/secrets/README.md
new file mode 100644
index 0000000000..e59b865413
--- /dev/null
+++ b/ops/secrets/README.md
@@ -0,0 +1 @@
+TVL's deployment secrets, encrypted with [agenix](https://github.com/ryantm/agenix/commits/main)
diff --git a/ops/secrets/besadii.age b/ops/secrets/besadii.age
new file mode 100644
index 0000000000..cfbe27b972
--- /dev/null
+++ b/ops/secrets/besadii.age
@@ -0,0 +1,19 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw WLrxQqBE+I1Z5BUcjAK+YfuRFlmmFYu+nwF4Z6eGZWI
+ADFyrki3ojHKjthCQ0MliiwEoCqYFVPFmSDbnbesIFU
+-> ssh-ed25519 zcCuhA j4NiKfnxBQlsthKUNUQMFJsazo9cL5R7RghHaFEGxAw
+lJNgCMjP7+2zDG/hJ++6Q7tBbsdVbzRZIbZZsfIwMSQ
+-> ssh-ed25519 CpJBgQ pVYsbcpywPaHDfnJQcnSZmNGw9Ppv5Un/xunzUG/KBg
+5uFarYtHRrXi6tEYWzbS7ZVsOEn3U0FoURcbx0OHPhU
+-> ssh-ed25519 aXKGcg dz+OWTc3c+odvXbaZR3lOq9t0EFu/t5qfhWOJp/tZxI
+j/z4v0C+Cy+fNuGVMspJfqoOqwkynhQXMK8fPC362PY
+-> ssh-ed25519 OkGqLg UQIzISNwV3+d3CEfeNcImttt+gPOyMuw1rbZ/TIsglo
+cHu007ebLhPE4+ayCjvOcINmG58FYukadAcimcMeTXI
+-> lP{m-grease
+yVvZbJopVdBDfLsbOqhss3DJZNU7ZcSWQP409nTzyy/iV4XwrE85Yj9SsDNRDtTp
+zoPwqQ
+--- O6W1Z0ixSR5N2A7KdPxl5HUKWXDvy/Wcb2fiUzxPK64
+g#lEŋ2~t+CQs1RǠM'	^2PJxsPhU
܂gg3v95$(b57WMpc	Ƨfe*'"],8|7$0qٷ5۳dwƲF^}!"|O|e

+Ձ>D4G~NiLސ@md|*빽၄Ӫa)G2\X3G\d~o{O;LRe]֏^v
+/jHV!UT})[mveX0@;]Z${mF
i<
+Z(SyƨC8AM6ATB˨9ϩ+~:Qo^n.lsJy;peL'LYv&0*0>J$vt
\ No newline at end of file
diff --git a/ops/secrets/buildkite-agent-token.age b/ops/secrets/buildkite-agent-token.age
new file mode 100644
index 0000000000..aef7b142b6
--- /dev/null
+++ b/ops/secrets/buildkite-agent-token.age
Binary files differdiff --git a/ops/secrets/buildkite-graphql-token.age b/ops/secrets/buildkite-graphql-token.age
new file mode 100644
index 0000000000..e656a6e04d
--- /dev/null
+++ b/ops/secrets/buildkite-graphql-token.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw L31em0JneG6XJikTp2LlYLSMDfsbDWjrNgQPQimIqWk
+3CJid3K/8RsE4cYEeZpqqaTmggMKH12GCDyalQMaK8s
+-> ssh-ed25519 zcCuhA LKq27N4Hx8OQ3eu0TDdBiXO0BcOdSfRZO0YNNG1Y8xE
+PQjl1SErWej6e7jwsddoj06TWQQwp2J/m8zvxR1pRhg
+-> ssh-ed25519 CpJBgQ dRMHEzXCpKPppncOBF4AmOYDZOSxZn+ta0o2H0zyAT0
+qNQFHL0QFxGlm7ZYnJ0H22iyVN3Ya7KYO596j2mN03Y
+-> ssh-ed25519 aXKGcg z31fIwcokphDOcPLNfBZB3ZN9nzG71pMmC68R60nWnU
+3U32x1lxd7brCQj9V8eglSzQ1lCwraxDnjLl68EIR18
+-> ssh-ed25519 OkGqLg 2jyx2iccmCeaXxs7pajP1WkRswZRwxrwVhNUKs1HzxE
+LjScnNDoWArkBXKWtSlJKnIlbnv0892nwn5aRyrF+sA
+-> 8Y8-grease \ObI# /"xHCp uyu Gn&q
+mLNOU8cvH8SB5PCkgKkBmxTb/cgwiQEBUbPI6GmMxvXy/8EMg5K1h3kpKSawW849
+jtLtHeLrM8FLeNtwZyIWpG4
+--- wnNSrutHnL4Trg5hNkuIHPguKl3JYjfEiJVCH4ScnVo
+:-$mY:yOLVLGdQgMbJÄ:O!6O5ɪOZ8*sA
\ No newline at end of file
diff --git a/ops/secrets/buildkite-ssh-private-key.age b/ops/secrets/buildkite-ssh-private-key.age
new file mode 100644
index 0000000000..485c90a9b7
--- /dev/null
+++ b/ops/secrets/buildkite-ssh-private-key.age
Binary files differdiff --git a/ops/secrets/clbot-ssh.age b/ops/secrets/clbot-ssh.age
new file mode 100644
index 0000000000..a5019e7b87
--- /dev/null
+++ b/ops/secrets/clbot-ssh.age
Binary files differdiff --git a/ops/secrets/clbot.age b/ops/secrets/clbot.age
new file mode 100644
index 0000000000..d5d5ae2f08
--- /dev/null
+++ b/ops/secrets/clbot.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw aKWeIQEoQpPT9lPUsV7tK/ySf/0WmFWw7xr7ic4RDFM
+OLRVTC6qVuhNhkYbGQwrxq4sQnqmuQEclKeQ9VPJrOw
+-> ssh-ed25519 zcCuhA j3JAw3UyZHR/x3O7pOTNkytbk5bTGnfBtsM030NolQk
+nt+9a3tJkO7j2nGI9C6S5YlYWYOCMqNOETU77PI4b10
+-> ssh-ed25519 CpJBgQ ScLyIj1cdn0wAwgaOSVGsusx/y3PD5/rDy7+OvjGIiU
+5tYuoEfVn0i1RtZ5XP+1HgyTSWkkRN4m36u6Fj3PkC4
+-> ssh-ed25519 aXKGcg 9p2LQFtV1X7jzG7n//GRUGmHGAsbGSCz6Q6SyBOZWwY
+wdOPCOHYkplGEoUOOTs99Kgde15xuJq8uzkZxudUo24
+-> ssh-ed25519 OkGqLg oLEc1KdRriCWobe5DF9OKVwDqQaW9RyjWDft1h5M4x8
+i/UEbhITzk3IOYme/xKuTfdbNMFNhLgRHbiiCAgKFBI
+-> %-grease 0 \^g*
+8aTar8xKZk24swVi7NVE0UN19BrexqAGcMWOeovRmQ
+--- N/kNOLE5d+yk7fAPRZmj8E1qMggLha56uKb9oj0/uHQ
+- >I1f9NF	wKlx055OZz~ys!gQtՇl1Wf9\sΰp.n
\ No newline at end of file
diff --git a/ops/secrets/default.nix b/ops/secrets/default.nix
new file mode 100644
index 0000000000..43f2a738bb
--- /dev/null
+++ b/ops/secrets/default.nix
@@ -0,0 +1,3 @@
+args:
+let mkSecrets = import ./mkSecrets.nix args; in
+mkSecrets ./. (import ./secrets.nix) // { inherit mkSecrets; }
diff --git a/ops/secrets/gerrit-queue.age b/ops/secrets/gerrit-queue.age
new file mode 100644
index 0000000000..eb9828847c
--- /dev/null
+++ b/ops/secrets/gerrit-queue.age
@@ -0,0 +1,17 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw qywg/yigMgYkhxORSqfuVsggQUMmQSPp6T9BjlEogGk
++vVPOuG9MqK/K5lkn/dTjd2RLJYL9F3uYnsK3I2r6nk
+-> ssh-ed25519 zcCuhA w1iPgVkUx3U/r64ooH4UhUMnrHC+Kqs5oooDIL+pbyA
+zUDp/32Hj3pEEXeL/8BJ0J5qQLqCOjpzbmQdsXGA9qk
+-> ssh-ed25519 CpJBgQ kRl0KlOJtcHsnNyJfyWlm9cW6ZQMrzmhgKaT+zYr03A
+lTprX0AfgP68w5towNfJw/YO3LoZFZYm0Y26Lb2La50
+-> ssh-ed25519 aXKGcg 4T+HCfrAPXDQORxNFm3lR9qJBfd4WcCQ/ny7bBs4mT8
+zKu2W42LJl6jUS6vYFJj30x+SaQQarx7OALCJ7fUTac
+-> ssh-ed25519 OkGqLg EEpq+VV3LC55VErd92bKnj7KqEzQqS6S60EZuCgb5Co
+XiyO6rELbfgj+2S3SQDu4Csz0Bw1NIGos69ixDPIEMU
+-> GY`K*hZ-grease VW)6 t.El^< @P
+dS5BLWUWe5RDzdf4uWzEOwW7lLrWtD8hqISTSWzFOFGnQgWX6cqZhtUlCmciRlCq
+RLXx5Nu3sSIEBX6FZR30PjmjyDQ7qArxc/Up0pkJ+ntG1d2lobyeB3qXsn8femUU
+Ku76
+--- 7KKYqquKMip1Qht63i2YH/9lGTv+MMso2YtIzF+6eis
+b>w~IRjW=ͳ?1:ZMJgJ2*nzEּwgq[3.^8i%!#|ub2darn=/TI-MTE¸˕N'0\Kd~-kɜf)
\ No newline at end of file
diff --git a/ops/secrets/gerrit-secrets.age b/ops/secrets/gerrit-secrets.age
new file mode 100644
index 0000000000..9869b0d46a
--- /dev/null
+++ b/ops/secrets/gerrit-secrets.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw UCZDSCgRCFAE07NoKG/fuyEE1k4wmNE0Dlg4APH/Exk
+VEnLQHoxfRgBqu3A0FTQ5KMo3w2pPSzr/dG0jT06xq0
+-> ssh-ed25519 zcCuhA fdZUx8wTyxVYd7DCngcLKligArm5mlYGTqCUKYsSflI
+hO8Hx6qJ3GRZipYDWseJmbGJEQ/FyU0/eMqsO9Sc9DM
+-> ssh-ed25519 CpJBgQ NCz9xhb6O5+XQxQSGTdJanwj68kg/mnhRm35/4L3+Sc
+kxZ6+NOd5b2VK/VjPKpbRNTmCW8wdNZV5IgY5OT2zEk
+-> ssh-ed25519 aXKGcg ZCfSQV7NUBsDLgb5sk0wRG3Zvhb8St98odNS7TmPu0c
+b3AqqM9bcNPI8G+in9a/ZPg0CiqXOYNMXMTrR42xr0k
+-> ssh-ed25519 OkGqLg SgctN3C54OmcYTCSrN0pA4+E7D1ry3byKebqV7Tjbg4
+Yk/2xD6lD69vhZH4+GQZh07hOOcOc0AKRq2zmkCrrjk
+-> ax~swz;-grease
++jjWRJAR+9n7HqxifiYbIvk2tFX9H/1H9O329yzf
+--- oZTj6wdxzeIDNWtZxBB5zo2DM4W1OcWPsTMqWJqYLSk
+ri-CT	}29K|CTQ$Im#xdň3>Dd{FSmaçZuaOeu:rϦ
d</,-Su?)Jo#RMv*xYMß[l℟Փaй?rrgrOi,ca$m3QEx(Dܯ{8|`2
\ No newline at end of file
diff --git a/ops/secrets/grafana.age b/ops/secrets/grafana.age
new file mode 100644
index 0000000000..d6022b4ea5
--- /dev/null
+++ b/ops/secrets/grafana.age
@@ -0,0 +1,17 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw FAneL6Ra+ipVGA37rsEOIbObsDK5L93n1tk6vsDiq08
+HcEABCYv388oK0Fk3zcCXdnpi+arLHvYWjqS+vMwlWg
+-> ssh-ed25519 zcCuhA n0FaAavgxFkJ1Lbd7bdDihV3m0aQ6IrD30G4N0NsNXU
+YumH3OYrbM/r/vgTFzJ8vEEWd7I/2yYdk6uBF4FLzG0
+-> ssh-ed25519 CpJBgQ +80Q06PTyeX+lnPZf1o5v4jBDoSfuIudOD49c72i5gc
+gNXrdBhVicCa0j7uGmvFrbZFMgN+4NQ5wxyojQUI8JE
+-> ssh-ed25519 aXKGcg cB4hgrcG47MEbgdvRQdJLBgQtGpyAw7rZTHQnE8mF2U
+vF46NzfPXjodk081WEd9D8LHMwB33Emswx65k2xiiQw
+-> ssh-ed25519 OkGqLg H4abrPcW2U+0h9ChEANdCoaYgIXW/2GMOfaPXc142lk
+OYQyK4tSDsyRIbqLhXxWc6ZgnS4/9YS8FD/M3N8ctG8
+-> 2UpS,n-grease 2@ A F$+@#Lk\ C4|Pa
+WKOTNBDihEkbp8U9elitxCVbpwa+RUXIUkWDKDdcLalK7no6DtfJVMyPAyPPymWg
+QOXPnkx1mw16wzj6elS86QU
+--- vEbbqmuObg1gVHyfCb+6CN3bkeNyyWam3r7uG5KiHec
+m26NR69.l@(_ώYUMD'Nq%y%(2yJ% Co	)m
+
\ No newline at end of file
diff --git a/ops/secrets/irccat.age b/ops/secrets/irccat.age
new file mode 100644
index 0000000000..b70abf636c
--- /dev/null
+++ b/ops/secrets/irccat.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw fiDNJcoEINRGGnkyaUN9j2bkXfFszU6Auje59dkfDRY
+vbwn4fQP8PQ5ZFLLah4kgxcV5MN4Zbr6ncjJYtZx6mw
+-> ssh-ed25519 zcCuhA GwygU+Rh3BADW/+WVL5QBb/LGEQ46HtZRWi8Ez5gdGI
+qwV4L6q9LZ5UondrZ8lZiFCqyVyNj41v15yzb/9Ha0o
+-> ssh-ed25519 CpJBgQ pBgC0bUHKNiB+NqHA7G2mJtTsWohDlDEVYZzZg1S3hA
+Zr8h2M1lz+kENM/H6fiWr6zsz1pRCE+0FcS+8epFIDM
+-> ssh-ed25519 aXKGcg Kuz7+ZHdExz4sbeOVK7MOjtldakqpuiUoCXa5BzBrlM
+lhOZVOKxSjZi/GUu2zDM5HTrbKTcxyk92JZHccJlAig
+-> ssh-ed25519 OkGqLg NJg2CK0q37agzqgqsLTlSIm8rhfaPwEIEPm+7eBp2So
+kB4p46ar71gLlbRBC1VdnHtGsA7oM8hBOqEo4X8ixNA
+-> VSn<1?+[-grease N=4\Egxn P:d\gl Ye-lT|k0
+v4W8MII8drZm1Eryx4Wzasc9WjargNyCe4R5Q+umIsuUNZebjQpvcmg1SlasTfi/
+oAdY1SZLaOJH0LmkP2v9ztz0MDsvOx9y5NZd/qw/NswgrI9FJmVh
+--- m4VHPIp1FsrSWHAdKwliymn9kdmFFrfo2SncDua+MEY
+{-`2k|K$>TzF6<K|cm5B)Ob5V@{03WE&<E Dj
\ No newline at end of file
diff --git a/ops/secrets/journaldriver.age b/ops/secrets/journaldriver.age
new file mode 100644
index 0000000000..823b527880
--- /dev/null
+++ b/ops/secrets/journaldriver.age
Binary files differdiff --git a/ops/secrets/keycloak-db.age b/ops/secrets/keycloak-db.age
new file mode 100644
index 0000000000..185f79da8b
--- /dev/null
+++ b/ops/secrets/keycloak-db.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw rG0ThGyx3bkL/WOz1K1iP3CmrKORLjsUrLNJbtb1WB0
+xbkyt7EUb1BhBKUYt3hh93kEU1avcqlCLKfHc3x+BEU
+-> ssh-ed25519 zcCuhA mwSN0urAXmA4vPCWIkzvCuDoE/LcA3eWpXr24Qab/lY
+Esa4Rfn55KYpIdYxsxGhBpPs40o28PJHbn8AEDn1n78
+-> ssh-ed25519 CpJBgQ ODm3P+PymrXBxEejSDi2YUTEadBVzJiIt6vYHpzH1C4
+nC9FY8yilVG65HXmRTtpvjKj2awE9SI1qp8duskNP7M
+-> ssh-ed25519 aXKGcg cdO7r0WCOktOmldIqvjVogyCximfA9sWd2Vq+bBgF2U
+1INC04f5PDwQgSQVeDpJomL5iZmyQfTwzHVu7BG+UUw
+-> ssh-ed25519 OkGqLg D6x2fkkNeoZToQrOhNVh69Y3kWN5NqZzXkUc2556nBY
+ZC4asUqTT6ZnQdnYV9Xn0yqTgLFt14Vo+3RncxWingU
+-> R^R|CZso-grease xq76HV<!
+MQSwHZCAIj24PlpplrTWjrZPAe5I31NC3xnWU80Q7Gk7FHUavAw
+--- NG3cBfD3zeP6McHAXxhPuWZVrC9au95/+r6fMi01Gjs
+`$|mR_!z[|伭2s"h0ž*0(-&
\ No newline at end of file
diff --git a/ops/secrets/mkSecrets.nix b/ops/secrets/mkSecrets.nix
new file mode 100644
index 0000000000..c99130835f
--- /dev/null
+++ b/ops/secrets/mkSecrets.nix
@@ -0,0 +1,27 @@
+# Expose secrets as part of the tree, making it possible to validate
+# their paths at eval time.
+#
+# Note that encrypted secrets end up in the Nix store, but this is
+# fine since they're publicly available anyways.
+{ depot, lib, ... }:
+
+let
+  inherit (depot.nix.yants)
+    attrs
+    any
+    defun
+    list
+    path
+    restrict
+    string
+    struct
+    ;
+  ssh-pubkey = restrict "SSH pubkey" (lib.hasPrefix "ssh-") string;
+  agenixSecret = struct "agenixSecret" { publicKeys = list ssh-pubkey; };
+in
+
+defun [ path (attrs agenixSecret) (attrs any) ]
+  (path: secrets:
+  depot.nix.readTree.drvTargets
+    # Import each secret into the Nix store
+    (builtins.mapAttrs (name: _: "${path}/${name}") secrets))
diff --git a/ops/secrets/nix-cache-priv.age b/ops/secrets/nix-cache-priv.age
new file mode 100644
index 0000000000..cc8513071a
--- /dev/null
+++ b/ops/secrets/nix-cache-priv.age
Binary files differdiff --git a/ops/secrets/nix-cache-pub.age b/ops/secrets/nix-cache-pub.age
new file mode 100644
index 0000000000..f628f2bbe4
--- /dev/null
+++ b/ops/secrets/nix-cache-pub.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw j+RSQPvmBUL+/tJpoZqbMyh//yPYelDkS8rGMBDeYBg
+w9XLo36I+Fh8yCgL9aL1V2dHA5PFIhA/mi+inpA0vO0
+-> ssh-ed25519 zcCuhA KTfCgCjc38/NRthB4ttrQV7aXbBgvs0Bgxitspo1TTo
+Zj7ZcjNxdiXgasq0pACRL6E3PvRsjsYsZeHFbX1mNYY
+-> ssh-ed25519 CpJBgQ 4nH14KX8d5AYlQOYpAq77Oz6QLLcqh+We7WT0yXx3EA
+YCIc6wFk++uaankNET+SATIRMPXh1C2NemJssGUexXA
+-> ssh-ed25519 aXKGcg x2izNmR+I9+2sRoHye4YUXU/6EZA8ZicIKUbjARVR28
+AV28t/cAwP6Js4lfYedJ88dCyAuKLq7RJU9SlhBx1FA
+-> ssh-ed25519 OkGqLg PpKqeVlQ015Qv2zvvrR8kTj+7kDHirLz4Zk8f32NoTA
+huaUh3Q3uJmsi9yWyuJgnEhgmsVjspfpR+IN6uT8FgA
+-> R2aR1C?^-grease
+7rumeWTufR7m6GRBOwKKVfzmMG8QRHzmt103vQfgmylhzGa2r6z2L3qSfFTqCW7T
+gMdbpgVvvTO+5aROt+iieBz9KFkHD3l/NXAhyZf8ydWRQlmDXcomY7QmSC3jLAE
+--- RX4Cux3g3rn4jdCZMpP8XenZ45uol6W4+wBk8jofI0E
+=[ބ$֘Klmdhe&*ERtΟD:;-=;W$0
\ No newline at end of file
diff --git a/ops/secrets/oauth2_proxy.age b/ops/secrets/oauth2_proxy.age
new file mode 100644
index 0000000000..816944684a
--- /dev/null
+++ b/ops/secrets/oauth2_proxy.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw pkxciQfQ/yrexMq/Djpq1KNLFYBRTnJSi3fo4iQ0MDk
+FAlEvIgT+h/7Lcj5E0BeEbaWlZAg1THoiqsQg6Sy1oI
+-> ssh-ed25519 zcCuhA sey8T2EXLHh5TF726U0DSn+MfXYYjimQxdsE67iflTc
+lPWYa9jrmwkac8KkCUypfZ5D3GCZwtdQPaXQRiM5xMo
+-> ssh-ed25519 CpJBgQ 6EzBbhxLD1Cjy1LRWnfum+tFvPRzxMoPT6P2HDN7qBs
+BPWNJiFIrAPdcOOK0um+RzclUGgrS7yJwCjx8X0pYTk
+-> ssh-ed25519 aXKGcg kMVeXntSlq3E5hbuNtu7e+iKoJpQDRR4isbx/WCYc0g
+fWvCPlcnjunuQ2LB02eQ51gr6SK2leaNuHttQOjJOyw
+-> ssh-ed25519 OkGqLg QFU47rj1sU5JuQtehbxyymEOpZYl0bWY6dRo81KrQxE
+5TXNy6e2sM5b+K5lSXEkLdJ8F4ZDJfYEetJ7/jsxAIY
+-> `!"O*HV-grease 1YD XwG${5; #Pr \7G
+CD72odW0Q4DMW6SGY+cUpBPhFePtjebkf1rpZJz0Twl8YrzbrXQfIgWv+tCUbr2d
+PKZKtlc9u0F+B6BKfVpZn0s0PD4/XGQ1PNLL/ZajxvYSB/w+UWbE67s
+--- U0nGetyOZONCTw7TQJ5QNUScp6v2noSVkrWCMJeROH8
+zsͮ~=H'Q|dL_Bx0;7Cg
*B3kp8{P+a%ؽ)EmF'`AUvUDzcLWox7PT(	n~a
\ No newline at end of file
diff --git a/ops/secrets/owothia.age b/ops/secrets/owothia.age
new file mode 100644
index 0000000000..c3ad07d232
--- /dev/null
+++ b/ops/secrets/owothia.age
Binary files differdiff --git a/ops/secrets/panettone.age b/ops/secrets/panettone.age
new file mode 100644
index 0000000000..542c866d61
--- /dev/null
+++ b/ops/secrets/panettone.age
@@ -0,0 +1,17 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw 0vXqVyiNwKAvIjBi1PPPWYzapFFuwFAGQqohfdaaThc
+cp+oevy9hbMvviVNTxKpws1Fsyirxr/nKZltlA08cWI
+-> ssh-ed25519 zcCuhA bFhpOsXo7H8GF3xLFwLs84aJegWj50+pEQDbyYYpwE0
+Y5iRW6/dhBNUHgNmObUEJu991Ms0RU1Y7xkeoz16A0U
+-> ssh-ed25519 CpJBgQ 5y0eXpmerwxRtySanRSBQeHCkMt96BOLVgR8S2lDSH4
++Z+3b9d8B5HZRVOL76SCNPIh9nhXKPSWq4lj0X2k2eg
+-> ssh-ed25519 aXKGcg HK5KeRoc+fhbYQ9RZTnum5x2y+vvyEQNKRpnNOISFn0
+TxZplwFO2e1YgY/V9tkLSVGxh9407xsxsT09N3jfcv4
+-> ssh-ed25519 OkGqLg otifGzPJ9Ykwdx9AkwlFW9AHAQL5OXnDexp8N4lJ6ys
+dFVgPNi8p3wQYbVbokxGqiNKUd3POXBs49LO3FAR6Js
+-> e"s'-grease :{S#]YZ MyRj r['U^ 0
++qc7
+--- Gnh5iyD6drHbPt2bE9JCGlXcPAPDPhkJl8A9+5SHNz4
+!u
+9wIV~Ep| *aGc3ZƩSQҾ\)f[)7gޘסPYEE5W$igLLCF=N
t
+b7ŏyFx;d9н+<r(U^P1W/%w#cWKk|)MrrYzL2/!]:Drya̳G
\ No newline at end of file
diff --git a/ops/secrets/secrets.nix b/ops/secrets/secrets.nix
new file mode 100644
index 0000000000..0d45f3b025
--- /dev/null
+++ b/ops/secrets/secrets.nix
@@ -0,0 +1,46 @@
+let
+  tazjin = [
+    # tverskoy
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1fGWz/gsq+ZeZXjvUrV+pBlanw1c3zJ9kLTax9FWQy"
+
+    # zamalek
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBRXeb8EuecLHP0bW4zuebXp4KRnXgJTZfeVWXQ1n1R"
+  ];
+
+  grfn = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMcBGBoWd5pPIIQQP52rcFOQN3wAY0J/+K2fuU6SffjA "
+  ];
+
+  sterni = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJk+KvgvI2oJTppMASNUfMcMkA2G5ZNt+HnWDzaXKLlo"
+  ];
+
+  sanduny = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOag0XhylaTVhmT6HB8EN2Fv5Ymrc4ZfypOXONUkykTX";
+  whitby = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILNh/w4BSKov0jdz3gKBc98tpoLta5bb87fQXWBhAl2I";
+
+  whitbyDefault.publicKeys = tazjin ++ grfn ++ sterni ++ [ whitby ];
+  allDefault.publicKeys = tazjin ++ grfn ++ sterni ++ [ sanduny whitby ];
+in
+{
+  "besadii.age" = whitbyDefault;
+  "buildkite-agent-token.age" = whitbyDefault;
+  "buildkite-ssh-private-key.age" = whitbyDefault;
+  "buildkite-graphql-token.age" = whitbyDefault;
+  "clbot-ssh.age" = whitbyDefault;
+  "clbot.age" = whitbyDefault;
+  "gerrit-queue.age" = whitbyDefault;
+  "gerrit-secrets.age" = whitbyDefault;
+  "grafana.age" = whitbyDefault;
+  "irccat.age" = whitbyDefault;
+  "journaldriver.age" = allDefault;
+  "keycloak-db.age" = whitbyDefault;
+  "nix-cache-priv.age" = whitbyDefault;
+  "nix-cache-pub.age" = whitbyDefault;
+  "oauth2_proxy.age" = whitbyDefault;
+  "owothia.age" = whitbyDefault;
+  "panettone.age" = whitbyDefault;
+  "smtprelay.age" = whitbyDefault;
+  "tf-glesys.age" = whitbyDefault;
+  "tf-keycloak.age" = whitbyDefault;
+  "tvl-alerts-bot-telegram-token.age" = whitbyDefault;
+}
diff --git a/ops/secrets/smtprelay.age b/ops/secrets/smtprelay.age
new file mode 100644
index 0000000000..3904107261
--- /dev/null
+++ b/ops/secrets/smtprelay.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw mqDmOqzDl7BY8xj7TuCHcIGrkiqURHK4Y4NkmUesyQE
+sfNvq6kuQUWuza3B6feUQtcWYhYh/aiN89fTOHdhHSY
+-> ssh-ed25519 zcCuhA rBlPiVmj7dSYHljc4/fhL7a9GSeCp/8FqG1R2f1kPgw
+o6Za8zm3n2LBnj9jQAU7Xtvt5ULIUesdiTi11DeRMY8
+-> ssh-ed25519 CpJBgQ vM3qI0XMQJY/ExxE3a0mmHhc5hY9rPDBzdJ4v9oZBlY
+lLHtL9j8ltx86eWwlPkyblcWjRd2iMjimwMXZptsRAc
+-> ssh-ed25519 aXKGcg +6heNooQufYnntQ1PJHlW/8aG4vijzY/CfXHUGPKMQE
+T95bxZSRC9Cdx9ZTaTnHWdeq0wKOkRL9mQxNo8j9SfA
+-> ssh-ed25519 OkGqLg HvpZmHz0DZIqWHiXvUsJ/OILlRhptl4WMDDiVF6dxko
+FoTSc84FRFnBh0rOYFX3M7t9p/hvn4DZMHZfU9jy0zo
+-> $<0F{v-grease
+blva6tBLrd967p8hOMGy0JT6Y19zWNdgowASEEBpoFzsmNlyKdbaYyMbxKTuqmCy
+8Wy5TpBj99pcUsEB
+--- DTMNC/wQr8xtJKIPPKjx90PmAZ15eimydKbYGnEa7Jc
+֐Զ"RA!]*	)
2)7ꗞ3AjӤʏL5EN"1:4tp.܋jpqG2(\mqߑF@1aŌ%
\ No newline at end of file
diff --git a/ops/secrets/tf-glesys.age b/ops/secrets/tf-glesys.age
new file mode 100644
index 0000000000..caeac0b1ee
--- /dev/null
+++ b/ops/secrets/tf-glesys.age
Binary files differdiff --git a/ops/secrets/tf-keycloak.age b/ops/secrets/tf-keycloak.age
new file mode 100644
index 0000000000..b450e84fb0
--- /dev/null
+++ b/ops/secrets/tf-keycloak.age
Binary files differdiff --git a/ops/secrets/tvl-alerts-bot-telegram-token.age b/ops/secrets/tvl-alerts-bot-telegram-token.age
new file mode 100644
index 0000000000..d9562ce924
--- /dev/null
+++ b/ops/secrets/tvl-alerts-bot-telegram-token.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw 14nPZssvAKQSzPdL+1iyz0BVA1DOdFDafdCyRfcmSWo
++ENcKRKyUN3G9+kd/Y9IpQbO3rIZdYiznqGO1cfVNZE
+-> ssh-ed25519 zcCuhA i/ag/HD84XrTpYigStOfwnWBLjOSypCnVuIYjtdVc2o
+T+dN0nl3H6J6OaMyLNHLgy99H8YJtSjgintxogJkWjo
+-> ssh-ed25519 CpJBgQ bbyerpmjpTkMmSaLnV5OuMQzqqtGao4eqE4kiFzm+Dw
+0Hskm4/Cks4Eu/Jr4Eh6302jWo64rdInvvJH6XJFyBk
+-> ssh-ed25519 aXKGcg sqdfN/2YLFmdhEWgn5Z/OAsmXwMORX/dPrmD4O7MlCE
+h/ej9LjZHn04rkEbvIaGAcLT3dMs9RdL3vFA+Rgdp3g
+-> ssh-ed25519 OkGqLg fK2cPxfOupCIfC1giMj2CFg/K/+4XX+fLpkqUmQHzDY
+uXTHT30ytEvliNAvmwlPyaySsYDVLarZgouV9Tfo6qo
+-> Me?Ykt-grease 4S m!3LR ^/)u#tFR
+1A
+--- UP4D68fCAMJC+1T1zbIiGCah3Ph+pJf7Z6wv2YJaOCQ
+js]U-Jγ6Y#^
+$$L1pwa:qwgq3Ԓb0zH%f!.0ΐ'֘!
\ No newline at end of file
diff --git a/ops/users/default.nix b/ops/users/default.nix
new file mode 100644
index 0000000000..9ade434497
--- /dev/null
+++ b/ops/users/default.nix
@@ -0,0 +1,167 @@
+{ ... }:
+
+[
+  {
+    username = "adisbladis";
+    email = "adisbladis@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$wdgoLRrUgZuz0Kin9YiNgQ$E40VIgzgpMpylZqkfByTKiWQnerupfuf7LDgOsU8tJA";
+  }
+  {
+    username = "andi";
+    email = "andi@notmuch.email";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$8lefg7+8UPAEh9Ott8zH0A$7YuLRraTC1IgxTNTxFJF03AWmqBS3GX2+vfD4XVTrb0";
+  }
+  {
+    username = "cschilling";
+    email = "christian.schilling.de@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$9VN3IS6ViW5FFbVKWOZI6Q$gZxuYAYk0Opq4E5i8cbcNjfznCQNc+RiP7Xv1CUnrQU";
+  }
+  {
+    username = "cynthia";
+    email = "me@cynthia.re";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=4,p=1$TxjbMGenhEmkyYLrg5uGhbr60THB86YeRZg5bPdiTJo$k9gbRlAPjmxwdUwzbavvsAVkckgQZ0jS2oTtvZBPysk";
+  }
+  {
+    username = "edef";
+    email = "edef@edef.eu";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$OORx4ERbkgvTmuYCJA8cIw$i5qaBzHkRVw7Tl+wZsTFTDqJwF0vuZqhW3VpknMYMc0";
+  }
+  {
+    username = "ericvolp12";
+    email = "ericvolp12@gmail.com";
+    password = "{SSHA}pSepaQ+/5KBLfJtRR5rfxGU8goAsXgvk";
+  }
+  {
+    username = "eta";
+    email = "tvl@eta.st";
+    password = "{SSHA}sOR5xzi7Lfv376XGQA8Hf6jyhTvo0XYc";
+  }
+  {
+    username = "etu";
+    email = "etu@failar.nu";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$RUrW8C9mWAkBSlkwSTH5dw$n3FXTeu41nDQfvJPI7TT3tcgwPmPJl8hPtaZ58qLq9A";
+  }
+  {
+    username = "firefly";
+    email = "firefly@firefly.nu";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$RYVVkFoi3A1yYkI8J2zUwg$GUERvgHvU8SGjQmilDJGZu50hYRAHw+ejtuL+Skygs8";
+  }
+  {
+    username = "flokli";
+    email = "flokli@flokli.de";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$TrezbwIY5TKLnJiii0wafQ$K0S2p9I8tiqP907nkgoK6IbG9ia4IuDiylTcIs5pesw";
+  }
+  {
+    username = "grfn";
+    email = "grfn@gws.fyi";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$5NEYPJ19nDITK5sGr4bzhQ$Xzpzth6y4w+HGvioHiYgzqFiwMDx0B7HAh+PVbkRuuk";
+  }
+  {
+    username = "htbf";
+    email = "h-tvl@htbf.dev";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$2iVXQQfd26icaIguHJg/CQ$hA9ziqn7kQ06AV6uQxJCGXoG8f+LWmH+nVlk00a1n/c";
+  }
+  {
+    username = "isomer";
+    email = "isomer@tvl.fyi";
+    password = "{SSHA}OhWQkPJgH1rRJqYIaMUbbKC4iLEzvCev";
+  }
+  {
+    username = "j4m3s";
+    email = "james.landrein@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$dMYmo+Uym9irtzAGXB2eNw$69OFcuqCqoLPBXKmmtYaQCquXximpyxsb2Kf8U7GdxM";
+  }
+  {
+    username = "kn";
+    email = "klemens@posteo.de";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$CoRZInysud4sduDoMjVOCw$/bdvAvyPO2DPxOcHlBiG2+rbTGF9XAcHUhPurxiIpZM";
+  }
+  {
+    username = "lukegb";
+    email = "lukegb@tvl.fyi";
+    password = "{SSHA}7a85VNhpFElFw+N5xcjgGmt4HnBsaGp4";
+  }
+  {
+    username = "nyanotech";
+    email = "nyanotechnology@gmail.com";
+    password = "{SSHA}NIJ2RCRb1+Q4Bs63cyE91VZyiN47DG6y";
+  }
+  {
+    username = "Profpatsch";
+    email = "mail@profpatsch.de";
+    password = "{SSHA}jcFXxRplMFxH4gpa0X5VdUzW64T95TwQ";
+  }
+  {
+    username = "sterni";
+    email = "sternenseemann@systemli.org";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$+NbF1izPMGqN5bASCBDV9g$aqBVplHwiyDpflZUmLtjkLWzKhxi7hwjm5fOwfbKohU";
+  }
+  {
+    username = "qyliss";
+    displayName = "Alyssa Ross";
+    email = "hi@alyssa.is";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$+uTpAKrN452D8wa7OFqPnw$GYi9/zns5iJCXDp1VuTPPsa35M5vkD6+rC8riT8cEHI";
+  }
+  {
+    username = "riking";
+    displayName = "kanepyork";
+    email = "rikingcoding@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$o2OcfhfKOry+UrcmODyQCw$qloaQgoIRDESwaA3yqPxxy8sgLk3mrjYFBbF41elVrM";
+  }
+  {
+    username = "tazjin";
+    email = "tazjin@tvl.su";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$wOPEl9D3kSke//oLtbvqrg$j0npwwXgaXQ/emefKUwL59tH8hdmtzbgH2rQzWSmE2Y";
+  }
+  {
+    username = "implr";
+    email = "implr@hackerspace.pl";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$SHRFps5sVgyUXYdmqGPw9g$tEx9DwKK1RjWlw52GLwOZ/iHep+QJboaZE83f1pXSwQ";
+  }
+  {
+    username = "ben";
+    email = "tvl@benjojo.co.uk";
+    password = "{SSHA}Zi48mSPsRMEPhff44w4RHi0SjjyhjWk1";
+  }
+  {
+    username = "jamie";
+    email = "jamie@kwiius.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$OkAMHVAfQ3nJhBffYJwk7Q$JV3DrF9eOU+4VL6I+nkaMUUOMqWuNzdp7N7U5Xwa3fg";
+  }
+  {
+    username = "milan";
+    email = "milan@petabyte.dev";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$VQAHgOqYVr7mzjEr8IAMdQ$eAXvy58eRkjg+96RKBCwUoRDpNyGDdes4rVtxoQbaeI";
+  }
+  {
+    username = "ezemtsov";
+    email = "eugene.zemtsov@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$eAEjTm0+JD0+p0o/GA8JmQ$uRtHCT+B/DBNr1rlOcLlROrkYsMj2T9ns/E9Ep3gJ1A";
+  }
+  {
+    username = "mdjnsn";
+    email = "mdj@mikejohnson.xyz";
+    displayName = "Mike Johnson";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$77+f5DFbuzs5myyIUN2nHg$xyXkRhIHFVaPMZUhxPk1uxMpLeEmU3BeyQjDsNPlJVw";
+  }
+  {
+    username = "smitop";
+    email = "me@smitop.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$H78rQtmhlzrPEifbXPoCVw$IBg7ePTm/u+e8r2A8aJ4iaaQBzMUw1isS9YJAZ8aT3o";
+  }
+  {
+    username = "asmundo";
+    email = "asmundo@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$oQvNAAGjshz5Cl8XW5i31A$eurlL9a7e5Ttw5JpTY9tOjSZyivWQsr1iCdTqshdfQU";
+  }
+  {
+    username = "wpcarro";
+    email = "wpcarro@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$NQdBVPNwh2ioDq9zWfMusA$2cABJGI8cU2JZirnVU5E5C28sTiePkiOPEAaqNUp/Fk";
+  }
+  {
+    username = "zseri";
+    email = "zseri.devel@ytrizja.de";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$wVNkImXloXIkCycnecdFeA$ECAdGdNzUUEq9sFGsIl0jb7AALGsHE+ndWRn6ilSmdE";
+  }
+]
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000000..3a26366d4d
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+edition = "2021"
diff --git a/third_party/README.md b/third_party/README.md
new file mode 100644
index 0000000000..267f234697
--- /dev/null
+++ b/third_party/README.md
@@ -0,0 +1,13 @@
+Third-Party Code
+================
+
+Code under this folder is one of the following:
+
+1. Externally developed dependencies which have been imported ("vendored") into
+   this repository. These dependencies come with their own licenses and whatever
+   else.
+
+2. Code that is developed inside of this repository, but released to an external
+   repository via [Copybara][].
+
+[Copybara]: https://github.com/google/copybara
diff --git a/third_party/agenix/default.nix b/third_party/agenix/default.nix
new file mode 100644
index 0000000000..f80dda512c
--- /dev/null
+++ b/third_party/agenix/default.nix
@@ -0,0 +1,13 @@
+{ pkgs, depot, ... }:
+
+let
+  src = depot.third_party.sources.agenix;
+
+  agenix = import src {
+    inherit pkgs;
+  };
+in
+{
+  inherit src;
+  cli = agenix.agenix;
+}
diff --git a/third_party/alsi/OWNERS b/third_party/alsi/OWNERS
new file mode 100644
index 0000000000..79a422fcde
--- /dev/null
+++ b/third_party/alsi/OWNERS
@@ -0,0 +1,3 @@
+inherit: true
+owners:
+ - grfn
diff --git a/third_party/alsi/default.nix b/third_party/alsi/default.nix
new file mode 100644
index 0000000000..8969374176
--- /dev/null
+++ b/third_party/alsi/default.nix
@@ -0,0 +1,25 @@
+{ pkgs, ... }:
+
+with pkgs;
+
+stdenv.mkDerivation {
+  name = "alsi";
+  pname = "alsi";
+  version = "0.4.8";
+
+  src = fetchFromGitHub {
+    owner = "trizen";
+    repo = "alsi";
+    rev = "fe2a925caad38d4cc7afe10d74ba60c5db09ee66";
+    sha256 = "060xlalfclrda5f1h3svj4v2gr19mdrsc62vrg7hgii0f3lib7j5";
+  };
+
+  buildInputs = [
+    (perl.withPackages (ps: with ps; [ DataDump ]))
+  ];
+
+  installPhase = ''
+    mkdir -p $out/bin
+    cp alsi $out/bin/alsi
+  '';
+}
diff --git a/third_party/bat_syntaxes/Prolog.sublime-syntax b/third_party/bat_syntaxes/Prolog.sublime-syntax
new file mode 100644
index 0000000000..b03066ac06
--- /dev/null
+++ b/third_party/bat_syntaxes/Prolog.sublime-syntax
@@ -0,0 +1,1319 @@
+# SPDX-License-Identifier: MIT
+# Generated from code at https://github.com/BenjaminSchaaf/swi-prolog-sublime-syntax
+---
+# http://www.sublimetext.com/docs/3/syntax.html
+name: Prolog
+file_extensions:
+  - pl
+  - pro
+first_line_match: '^#!.*\bswipl\b'
+scope: source.prolog
+contexts:
+  atom-entity|meta:
+    - meta_content_scope: entity.name.predicate.prolog
+    - match: ''
+      pop: true
+  atom-functor|meta:
+    - meta_content_scope: meta.path.prolog variable.function.functor.prolog
+    - match: ''
+      pop: true
+  atom-string|0:
+    - meta_include_prototype: false
+    - match: '\\([abcefnrstv''\"`\n\\]|x\h\h+\\?|u\h{4}|U\h{8})'
+      scope: constant.character.escape.prolog
+    - match: ''''
+      pop: true
+  compound-term|0:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [compound-term|1, value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [compound-term|1, value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [compound-term|1, value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [compound-term|1, value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [compound-term|1, value-without-comma|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [compound-term|1, value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [compound-term|1, value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [compound-term|1, value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [compound-term|1, value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [compound-term|1, value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [compound-term|1, value-without-comma|0, single-value|2]
+    - match: '\)'
+      scope: punctuation.section.parens.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  compound-term|1:
+    - match: ','
+      scope: punctuation.separator.sequence.prolog
+      push: compound-term|2
+    - match: '\)'
+      scope: punctuation.section.parens.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  compound-term|2:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: value-without-comma|0
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: value-without-comma|0
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: value-without-comma|0
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: value-without-comma|0
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: value-without-comma|0
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [value-without-comma|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  fact|0:
+    - match: ':-'
+      scope: keyword.operator.definition.begin.prolog
+      set: fact|1
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [fact|2, value|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [fact|2, value|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [fact|2, value|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [fact|2, value|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [fact|2, value|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [fact|2, value|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [fact|2, value|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [fact|2, value|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [fact|2, value|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [fact|2, value|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [fact|2, value|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [fact|2, value|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [fact|2, value|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [fact|2, value|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [fact|2, value|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [fact|2, value|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [fact|2, value|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [fact|2, value|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  fact|1:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [fact|2, value|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [fact|2, value|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [fact|2, value|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [fact|2, value|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [fact|2, value|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [fact|2, value|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [fact|2, value|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [fact|2, value|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [fact|2, value|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [fact|2, value|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [fact|2, value|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [fact|2, value|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [fact|2, value|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [fact|2, value|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [fact|2, value|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [fact|2, value|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [fact|2, value|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [fact|2, value|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  fact|2:
+    - match: '\.'
+      scope: keyword.operator.definition.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  list|0:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [list|1, value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [list|1, value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [list|1, value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [list|1, value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [list|1, value-without-comma|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [list|1, value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [list|1, value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [list|1, value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [list|1, value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [list|1, value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [list|1, value-without-comma|0, single-value|2]
+    - match: '\]'
+      scope: punctuation.section.brackets.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  list|1:
+    - match: ','
+      scope: punctuation.separator.sequence.prolog
+      push: list|2
+    - match: '\|'
+      scope: punctuation.separator.sequence.prolog
+      set: list|3
+    - match: '\]'
+      scope: punctuation.section.brackets.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  list|2:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: value-without-comma|0
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: value-without-comma|0
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: value-without-comma|0
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: value-without-comma|0
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: value-without-comma|0
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [value-without-comma|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  list|3:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [list|4, value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [list|4, value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [list|4, value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [list|4, value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [list|4, value-without-comma|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [list|4, value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [list|4, value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [list|4, value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [list|4, value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [list|4, value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [list|4, value-without-comma|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  list|4:
+    - match: '\]'
+      scope: punctuation.section.brackets.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  main:
+    - match: '^#!'
+      scope: comment.line.number-sign.prolog punctuation.definition.comment.number-sign.prolog
+      push: shebang|0
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: entity.name.predicate.prolog
+      push: rule|0
+    - match: ''''
+      scope: entity.name.predicate.prolog
+      push: [rule|0, atom-entity|meta, atom-string|0]
+    - match: '(?=\S)'
+      push: fact|0
+    - match: '\S'
+      scope: invalid.illegal.prolog
+  nested-comment|0:
+    - meta_content_scope: comment.block.nested.prolog
+    - match: '/\*(\*(?!/))?'
+      scope: comment.block.nested.prolog punctuation.definition.comment.prolog
+      push: nested-comment|0
+    - match: '\*/'
+      scope: comment.block.nested.prolog punctuation.definition.comment.prolog
+      pop: true
+  number|0:
+    - match: '[0-9_]+'
+      scope: constant.numeric.integer.prolog
+    - match: '(?=\S)'
+      pop: true
+  operator-compound-term|0:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [operator-compound-term|1, value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [operator-compound-term|1, value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [operator-compound-term|1, value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [operator-compound-term|1, value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [operator-compound-term|1, value-without-comma|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [operator-compound-term|1, value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [operator-compound-term|1, value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [operator-compound-term|1, value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [operator-compound-term|1, value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [operator-compound-term|1, value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [operator-compound-term|1, value-without-comma|0, single-value|2]
+    - match: '\)'
+      scope: punctuation.section.parans.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  operator-compound-term|1:
+    - match: ','
+      scope: punctuation.separator.sequence.prolog
+      push: operator-compound-term|2
+    - match: '\)'
+      scope: punctuation.section.parans.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  operator-compound-term|2:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: value-without-comma|0
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: value-without-comma|0
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: value-without-comma|0
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: value-without-comma|0
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: value-without-comma|0
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [value-without-comma|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  prototype:
+    - match: '(%+).*\n?'
+      scope: comment.line.percentage.prolog
+      captures:
+        1: punctuation.definition.comment.prolog
+    - match: '/\*(\*(?!/))?'
+      scope: comment.block.nested.prolog punctuation.definition.comment.prolog
+      push: nested-comment|0
+  rule|0:
+    - match: '\('
+      scope: punctuation.section.parens.begin.prolog
+      set: [rule|1, compound-term|0]
+    - match: ':-'
+      scope: keyword.operator.definition.begin.prolog
+      set: rule|2
+    - match: '\.'
+      scope: keyword.operator.definition.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  rule|1:
+    - match: ':-'
+      scope: keyword.operator.definition.begin.prolog
+      set: rule|2
+    - match: '\.'
+      scope: keyword.operator.definition.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  rule|2:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [rule|3, value|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [rule|3, value|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [rule|3, value|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [rule|3, value|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [rule|3, value|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [rule|3, value|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [rule|3, value|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [rule|3, value|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [rule|3, value|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [rule|3, value|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [rule|3, value|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [rule|3, value|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [rule|3, value|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [rule|3, value|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [rule|3, value|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [rule|3, value|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [rule|3, value|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [rule|3, value|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  rule|3:
+    - match: '\.'
+      scope: keyword.operator.definition.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  set|0:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [set|1, value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [set|1, value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [set|1, value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [set|1, value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [set|1, value-without-comma|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [set|1, value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [set|1, value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [set|1, value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [set|1, value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [set|1, value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [set|1, value-without-comma|0, single-value|2]
+    - match: '\}'
+      scope: punctuation.section.braces.begin.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  set|1:
+    - match: ','
+      scope: punctuation.separator.sequence.prolog
+      push: set|2
+    - match: '\}'
+      scope: punctuation.section.braces.begin.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  set|2:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [value-without-comma|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [value-without-comma|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: value-without-comma|0
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: value-without-comma|0
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: value-without-comma|0
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: value-without-comma|0
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [value-without-comma|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: value-without-comma|0
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: value-without-comma|0
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [value-without-comma|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [value-without-comma|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [value-without-comma|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [value-without-comma|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [value-without-comma|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  shebang|0:
+    - meta_content_scope: comment.line.number-sign.prolog
+    - match: '$\n?'
+      scope: comment.line.number-sign.prolog
+      pop: true
+  single-value|0:
+    - match: '\('
+      scope: punctuation.section.parens.begin.prolog
+      set: compound-term|0
+    - match: '(?=\S)'
+      pop: true
+  single-value|1:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: single-value|0
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: operator-compound-term|0
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      pop: true
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      pop: true
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      pop: true
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: number|0
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      pop: true
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      pop: true
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: string|0
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: list|0
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: set|0
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: single-value|1
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: single-value|1
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: single-value|2
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  single-value|2:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [single-value|3, value|0, single-value|0]
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [single-value|3, value|0, single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: [single-value|3, value|0, operator-compound-term|0]
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      set: [single-value|3, value|0]
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [single-value|3, value|0]
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [single-value|3, value|0]
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      set: [single-value|3, value|0]
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      set: [single-value|3, value|0]
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      set: [single-value|3, value|0]
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: [single-value|3, value|0, number|0]
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      set: [single-value|3, value|0]
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      set: [single-value|3, value|0]
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: [single-value|3, value|0, string|0]
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: [single-value|3, value|0, list|0]
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: [single-value|3, value|0, set|0]
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: [single-value|3, value|0, single-value|1]
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: [single-value|3, value|0, single-value|1]
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: [single-value|3, value|0, single-value|2]
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  single-value|3:
+    - match: '\)'
+      scope: punctuation.section.group.end.prolog
+      pop: true
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  string|0:
+    - meta_content_scope: meta.string.prolog string.quoted.double.prolog
+    - meta_include_prototype: false
+    - match: '\\([abcefnrstv''\"`\n\\]|x\h\h+\\?|u\h{4}|U\h{8})'
+      scope: constant.character.escape.prolog
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.end.prolog
+      pop: true
+  value-without-comma|0:
+    - match: '\bis\b|>>|\^|=\.\.|=?<|>=?|==?|\*\*?|\+|->?|/|#=|\\='
+      scope: keyword.operator.prolog
+      push: value-without-comma|1
+    - match: ';'
+      scope: keyword.operator.logical.or.prolog
+      push: value-without-comma|1
+    - match: '->'
+      scope: keyword.operator.logical.if.prolog
+      push: value-without-comma|1
+    - match: '(?=\S)'
+      pop: true
+  value-without-comma|1:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: single-value|0
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: operator-compound-term|0
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      pop: true
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      pop: true
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      pop: true
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: number|0
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      pop: true
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      pop: true
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: string|0
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: list|0
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: set|0
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: single-value|1
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: single-value|1
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: single-value|2
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
+  value|0:
+    - match: '\bis\b|>>|\^|=\.\.|=?<|>=?|==?|\*\*?|\+|->?|/|#=|\\='
+      scope: keyword.operator.prolog
+      push: value|1
+    - match: ';'
+      scope: keyword.operator.logical.or.prolog
+      push: value|1
+    - match: '->'
+      scope: keyword.operator.logical.if.prolog
+      push: value|1
+    - match: ','
+      scope: keyword.operator.logical.and.prolog
+      push: value|1
+    - match: '(?=\S)'
+      pop: true
+  value|1:
+    - match: '\b[a-z][[:alpha:]0-9_]*\b'
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: single-value|0
+    - match: ''''
+      scope: meta.path.prolog variable.function.functor.prolog
+      set: [single-value|0, atom-functor|meta, atom-string|0]
+    - match: '([~^&*\-+=|\\/<>][~^&*\-+=|\\/<>.,]*)(\()'
+      captures:
+        1: constant.character.swi-prolog.prolog
+        2: punctuation.section.parens.begin.prolog
+      set: operator-compound-term|0
+    - match: '!'
+      scope: keyword.control.cut.prolog
+      pop: true
+    - match: '(0b)[01_]+'
+      scope: constant.numeric.integer.binary.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '(0x)[\h_]+'
+      scope: constant.numeric.integer.hexadecimal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '(0o)[0-7_]+'
+      scope: constant.numeric.integer.octal.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+      pop: true
+    - match: '([0-9]{1,2})('')[0-9a-z]+'
+      scope: constant.numeric.integer.prolog
+      captures:
+        1: punctuation.definition.number.base.prolog
+        2: punctuation.separator.base.prolog
+      pop: true
+    - match: '[0-9]+\.[0-9]+'
+      scope: constant.numeric.float.prolog
+      pop: true
+    - match: '[+-]?[0-9_]+'
+      scope: constant.numeric.integer.prolog
+      set: number|0
+    - match: '\b[A-Z][[:alpha:]0-9_]*\b|\b_[[:alpha:]0-9_]+\b'
+      scope: variable.parameter.prolog
+      pop: true
+    - match: '_'
+      scope: language.constant.underscore.prolog
+      pop: true
+    - match: '"'
+      scope: meta.string.prolog string.quoted.double.prolog punctuation.definition.string.begin.prolog
+      set: string|0
+    - match: '\['
+      scope: punctuation.section.brackets.begin.prolog
+      set: list|0
+    - match: '\{'
+      scope: punctuation.section.braces.begin.prolog
+      set: set|0
+    - match: '\+|-'
+      scope: keyword.operator.arithmetic.prolog
+      set: single-value|1
+    - match: '\\\+'
+      scope: keyword.control.negation.prolog
+      set: single-value|1
+    - match: '\('
+      scope: punctuation.section.group.begin.prolog
+      set: single-value|2
+    - match: '\S'
+      scope: invalid.illegal.prolog
+      pop: true
diff --git a/third_party/bat_syntaxes/default.nix b/third_party/bat_syntaxes/default.nix
new file mode 100644
index 0000000000..a48962dd36
--- /dev/null
+++ b/third_party/bat_syntaxes/default.nix
@@ -0,0 +1,18 @@
+# For depot projects that make use of syntect (primarily
+# //tools/cheddar) the included syntax set is taken from bat.
+#
+# However, bat lacks some of the syntaxes we are interested in. This
+# package creates a new binary syntax set which bundles our additional
+# syntaxes on top of bat's existing ones.
+{ pkgs, ... }:
+
+let
+  inherit (pkgs) bat runCommandNoCC;
+in
+runCommandNoCC "bat-syntaxes.bin" { } ''
+  export HOME=$PWD
+  mkdir -p .config/bat/syntaxes
+  cp ${./Prolog.sublime-syntax} .config/bat/syntaxes
+  ${bat}/bin/bat cache --build
+  mv .cache/bat/syntaxes.bin $out
+''
diff --git a/third_party/bufbuild/default.nix b/third_party/bufbuild/default.nix
new file mode 100644
index 0000000000..12683b1062
--- /dev/null
+++ b/third_party/bufbuild/default.nix
@@ -0,0 +1,29 @@
+# buf.build is a Protobuf linter and breaking change detector.
+# Several binaries are produced.
+{ pkgs, lib, ... }:
+
+pkgs.buildGoModule {
+  pname = "buf";
+  version = "v0.20.1";
+  vendorSha256 = "1gg5c7aiqb4w1zxwsraxxpln33xkmkzlp1h69xgi9i08zvrfipqs";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "bufbuild";
+    repo = "buf";
+    rev = "5e8bf4c800de911764ffdf8d2188b7f6f54476e4";
+    sha256 = "1rni5swfnb4sbrd9rls4mc3902xhqrlsja96lfcdfjzx08g6kg20";
+  };
+
+  doCheck = false;
+
+  # TODO(riking): postinstall produce shell completions for bash, fish, zsh
+  # bin/buf bash-completion
+  # bin/buf zsh-completion
+  # # bin/buf manpages # not yet functional
+
+  meta = with lib; {
+    description = "Protobuf linter and breaking change detector";
+    homepage = "https://buf.build/docs/introduction";
+    license = licenses.asl20;
+  };
+}
diff --git a/third_party/buzz/default.nix b/third_party/buzz/default.nix
new file mode 100644
index 0000000000..8b112d9e79
--- /dev/null
+++ b/third_party/buzz/default.nix
@@ -0,0 +1,30 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = pkgs.fetchFromGitHub {
+    owner = "jonhoo";
+    repo = "buzz";
+    rev = "02479643ed1b0325050245dbb3b70411b8cffb7a";
+    sha256 = "1spklfv02qlinlail5rmhh1c4926gyrkr2ydd9g6z919rxkl0ywk";
+  };
+
+  buildInputs = with pkgs; [
+    pkgconfig
+    dbus
+    glib
+    openssl
+    cairo
+    pango
+    atk
+    gdk-pixbuf
+    gtk3
+    dbus-glib
+    libappindicator-gtk3
+    llvmPackages.llvm
+    llvmPackages.bintools
+    llvmPackages.clang
+    llvmPackages.libclang
+  ];
+
+  LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib/libclang.so";
+}
diff --git a/third_party/cgit/.gitignore b/third_party/cgit/.gitignore
new file mode 100644
index 0000000000..661df346c2
--- /dev/null
+++ b/third_party/cgit/.gitignore
@@ -0,0 +1,12 @@
+# Files I don't care to see in git-status/commit
+/cgit
+cgit.conf
+CGIT-CFLAGS
+VERSION
+cgitrc.5
+cgitrc.5.fo
+cgitrc.5.html
+cgitrc.5.pdf
+cgitrc.5.xml
+*.o
+*.d
diff --git a/third_party/cgit/.mailmap b/third_party/cgit/.mailmap
new file mode 100644
index 0000000000..03b54796cf
--- /dev/null
+++ b/third_party/cgit/.mailmap
@@ -0,0 +1,10 @@
+Florian Pritz <bluewind@xinu.at> <bluewind@xssn.at>
+Harley Laue <losinggeneration@gmail.com> <losinggeneration@aim.com>
+John Keeping <john@keeping.me.uk> <john@metanate.com>
+Lars Hjemli <hjemli@gmail.com> <larsh@hal-2004.(none)>
+Lars Hjemli <hjemli@gmail.com> <larsh@hatman.(none)>
+Lars Hjemli <hjemli@gmail.com> <larsh@slackbox.hjemli.net>
+Lars Hjemli <hjemli@gmail.com> <larsh@slaptop.hjemli.net>
+Lukas Fleischer <lfleischer@lfos.de> <cgit@cryptocrack.de>
+Lukas Fleischer <lfleischer@lfos.de> <info@cryptocrack.de>
+Stefan Bühler <source@stbuehler.de> <lighttpd@stbuehler.de>
diff --git a/third_party/cgit/.skip-subtree b/third_party/cgit/.skip-subtree
new file mode 100644
index 0000000000..c108a7d34f
--- /dev/null
+++ b/third_party/cgit/.skip-subtree
@@ -0,0 +1 @@
+Subtrees of this directory belong to cgit (third-party).
diff --git a/third_party/cgit/AUTHORS b/third_party/cgit/AUTHORS
new file mode 100644
index 0000000000..256ea6b3bc
--- /dev/null
+++ b/third_party/cgit/AUTHORS
@@ -0,0 +1,13 @@
+Maintainer:
+	June McEnroe <june@causal.agency>
+
+Contributors:
+	Jason A. Donenfeld <Jason@zx2c4.com>
+	Lukas Fleischer <cgit@cryptocrack.de>
+	Johan Herland <johan@herland.net>
+	Lars Hjemli <hjemli@gmail.com>
+	Ferry Huberts <ferry.huberts@pelagic.nl>
+	John Keeping <john@keeping.me.uk>
+
+Previous Maintainer:
+	Lars Hjemli <hjemli@gmail.com>
diff --git a/third_party/cgit/COPYING b/third_party/cgit/COPYING
new file mode 100644
index 0000000000..d159169d10
--- /dev/null
+++ b/third_party/cgit/COPYING
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/third_party/cgit/Makefile b/third_party/cgit/Makefile
new file mode 100644
index 0000000000..5967a327bb
--- /dev/null
+++ b/third_party/cgit/Makefile
@@ -0,0 +1,168 @@
+all::
+
+CGIT_VERSION = 1.4.1
+CGIT_SCRIPT_NAME = cgit.cgi
+CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
+CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
+CGIT_CONFIG = /etc/cgitrc
+CACHE_ROOT = /var/cache/cgit
+prefix = /usr/local
+libdir = $(prefix)/lib
+filterdir = $(libdir)/cgit/filters
+docdir = $(prefix)/share/doc/cgit
+htmldir = $(docdir)
+pdfdir = $(docdir)
+mandir = $(prefix)/share/man
+SHA1_HEADER = <openssl/sha.h>
+GIT_VER = 2.36.1
+GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.xz
+INSTALL = install
+COPYTREE = cp -r
+MAN5_TXT = $(wildcard *.5.txt)
+MAN_TXT  = $(MAN5_TXT)
+DOC_MAN5 = $(patsubst %.txt,%,$(MAN5_TXT))
+DOC_HTML = $(patsubst %.txt,%.html,$(MAN_TXT))
+DOC_PDF  = $(patsubst %.txt,%.pdf,$(MAN_TXT))
+
+ASCIIDOC = asciidoc
+ASCIIDOC_EXTRA =
+ASCIIDOC_HTML = xhtml11
+ASCIIDOC_COMMON = $(ASCIIDOC) $(ASCIIDOC_EXTRA)
+TXT_TO_HTML = $(ASCIIDOC_COMMON) -b $(ASCIIDOC_HTML)
+
+# Define NO_C99_FORMAT if your formatted IO functions (printf/scanf et.al.)
+# do not support the 'size specifiers' introduced by C99, namely ll, hh,
+# j, z, t. (representing long long int, char, intmax_t, size_t, ptrdiff_t).
+# some C compilers supported these specifiers prior to C99 as an extension.
+#
+# Define HAVE_LINUX_SENDFILE to use sendfile()
+
+#-include config.mak
+
+-include git/config.mak.uname
+#
+# Let the user override the above settings.
+#
+-include cgit.conf
+
+export CGIT_VERSION CGIT_SCRIPT_NAME CGIT_SCRIPT_PATH CGIT_DATA_PATH CGIT_CONFIG CACHE_ROOT
+
+#
+# Define a way to invoke make in subdirs quietly, shamelessly ripped
+# from git.git
+#
+QUIET_SUBDIR0  = +$(MAKE) -C # space to separate -C and subdir
+QUIET_SUBDIR1  =
+
+ifneq ($(findstring w,$(MAKEFLAGS)),w)
+PRINT_DIR = --no-print-directory
+else # "make -w"
+NO_SUBDIR = :
+endif
+
+ifndef V
+	QUIET_SUBDIR0  = +@subdir=
+	QUIET_SUBDIR1  = ;$(NO_SUBDIR) echo '   ' SUBDIR $$subdir; \
+			 $(MAKE) $(PRINT_DIR) -C $$subdir
+	QUIET_TAGS     = @echo '   ' TAGS $@;
+	export V
+endif
+
+.SUFFIXES:
+
+all:: cgit
+
+cgit:
+	$(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk ../cgit $(EXTRA_GIT_TARGETS) NO_CURL=1
+
+sparse:
+	$(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk NO_CURL=1 cgit-sparse
+
+test:
+	@$(MAKE) --no-print-directory cgit EXTRA_GIT_TARGETS=all
+	$(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
+
+install: all
+	$(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
+	$(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
+	$(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
+	$(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
+	$(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
+	$(INSTALL) -m 0644 robots.txt $(DESTDIR)$(CGIT_DATA_PATH)/robots.txt
+	$(INSTALL) -m 0755 -d $(DESTDIR)$(filterdir)
+	$(COPYTREE) filters/* $(DESTDIR)$(filterdir)
+
+install-doc: install-man install-html install-pdf
+
+install-man: doc-man
+	$(INSTALL) -m 0755 -d $(DESTDIR)$(mandir)/man5
+	$(INSTALL) -m 0644 $(DOC_MAN5) $(DESTDIR)$(mandir)/man5
+
+install-html: doc-html
+	$(INSTALL) -m 0755 -d $(DESTDIR)$(htmldir)
+	$(INSTALL) -m 0644 $(DOC_HTML) $(DESTDIR)$(htmldir)
+
+install-pdf: doc-pdf
+	$(INSTALL) -m 0755 -d $(DESTDIR)$(pdfdir)
+	$(INSTALL) -m 0644 $(DOC_PDF) $(DESTDIR)$(pdfdir)
+
+uninstall:
+	rm -f $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
+	rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
+	rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
+
+uninstall-doc: uninstall-man uninstall-html uninstall-pdf
+
+uninstall-man:
+	@for i in $(DOC_MAN5); do \
+	    rm -fv $(DESTDIR)$(mandir)/man5/$$i; \
+	done
+
+uninstall-html:
+	@for i in $(DOC_HTML); do \
+	    rm -fv $(DESTDIR)$(htmldir)/$$i; \
+	done
+
+uninstall-pdf:
+	@for i in $(DOC_PDF); do \
+	    rm -fv $(DESTDIR)$(pdfdir)/$$i; \
+	done
+
+doc: doc-man doc-html doc-pdf
+doc-man: doc-man5
+doc-man5: $(DOC_MAN5)
+doc-html: $(DOC_HTML)
+doc-pdf: $(DOC_PDF)
+
+%.5 : %.5.txt
+	a2x -f manpage $<
+
+$(DOC_HTML): %.html : %.txt
+	$(TXT_TO_HTML) -o $@+ $< && \
+	mv $@+ $@
+
+$(DOC_PDF): %.pdf : %.txt
+	a2x -f pdf cgitrc.5.txt
+
+clean: clean-doc
+	$(RM) cgit VERSION CGIT-CFLAGS *.o tags
+	$(RM) -r .deps
+
+cleanall: clean
+	$(MAKE) -C git clean
+
+clean-doc:
+	$(RM) cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo
+
+get-git:
+	curl -L $(GIT_URL) | tar -xJf - && rm -rf git && mv git-$(GIT_VER) git
+
+tags:
+	$(QUIET_TAGS)find . -name '*.[ch]' | xargs ctags
+
+.PHONY: all cgit git get-git
+.PHONY: clean clean-doc cleanall
+.PHONY: doc doc-html doc-man doc-pdf
+.PHONY: install install-doc install-html install-man install-pdf
+.PHONY: tags test
+.PHONY: uninstall uninstall-doc uninstall-html uninstall-man uninstall-pdf
diff --git a/third_party/cgit/README b/third_party/cgit/README
new file mode 100644
index 0000000000..2094b87df0
--- /dev/null
+++ b/third_party/cgit/README
@@ -0,0 +1,88 @@
+cgit-pink - CGI for Git
+=======================
+
+This is a fork of cgit, an attempt to create a fast web interface
+for the Git SCM, using a built-in cache to decrease server I/O
+pressure.
+
+Installation
+------------
+
+Building cgit involves building a proper version of Git. How to do this
+depends on how you obtained the cgit sources:
+
+a) If you're working in a cloned cgit repository, you first need to
+initialize and update the Git submodule:
+
+    $ git submodule init     # register the Git submodule in .git/config
+    $ $EDITOR .git/config    # if you want to specify a different url for git
+    $ git submodule update   # clone/fetch and checkout correct git version
+
+b) If you're building from a cgit tarball, you can download a proper git
+version like this:
+
+    $ make get-git
+
+When either a) or b) has been performed, you can build and install cgit like
+this:
+
+    $ make
+    $ sudo make install
+
+This will install `cgit.cgi` and `cgit.css` into `/var/www/htdocs/cgit`. You
+can configure this location (and a few other things) by providing a `cgit.conf`
+file (see the Makefile for details).
+
+
+Dependencies
+------------
+
+* libzip
+* libcrypto (OpenSSL)
+* libssl (OpenSSL)
+
+Apache configuration
+--------------------
+
+A new `Directory` section must probably be added for cgit, possibly something
+like this:
+
+    <Directory "/var/www/htdocs/cgit/">
+        AllowOverride None
+        Options +ExecCGI
+        Order allow,deny
+        Allow from all
+    </Directory>
+
+
+Runtime configuration
+---------------------
+
+The file `/etc/cgitrc` is read by cgit before handling a request. In addition
+to runtime parameters, this file may also contain a list of repositories
+displayed by cgit (see `cgitrc.5.txt` for further details).
+
+The cache
+---------
+
+When cgit is invoked it looks for a cache file matching the request and
+returns it to the client. If no such cache file exists (or if it has expired),
+the content for the request is written into the proper cache file before the
+file is returned.
+
+If the cache file has expired but cgit is unable to obtain a lock for it, the
+stale cache file is returned to the client. This is done to favour page
+throughput over page freshness.
+
+The generated content contains the complete response to the client, including
+the HTTP headers `Modified` and `Expires`.
+
+Online presence
+---------------
+
+* The cgit-pink homepage is hosted by cgit at
+  <https://git.causal.agency/cgit-pink/about>
+
+* Patches, bug reports, discussions and support should go to the cgit-pink
+  mailing list: <list+cgit@causal.agency>. Archives are available at:
+  <https://causal.agency/list/cgit.html>
diff --git a/third_party/cgit/cache.c b/third_party/cgit/cache.c
new file mode 100644
index 0000000000..59372541cb
--- /dev/null
+++ b/third_party/cgit/cache.c
@@ -0,0 +1,480 @@
+/* cache.c: cache management
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ *
+ *
+ * The cache is just a directory structure where each file is a cache slot,
+ * and each filename is based on the hash of some key (e.g. the cgit url).
+ * Each file contains the full key followed by the cached content for that
+ * key.
+ *
+ */
+
+#include "cgit.h"
+#include "cache.h"
+#include "html.h"
+#ifdef HAVE_LINUX_SENDFILE
+#include <sys/sendfile.h>
+#endif
+
+#define CACHE_BUFSIZE (1024 * 4)
+
+struct cache_slot {
+	const char *key;
+	size_t keylen;
+	int ttl;
+	cache_fill_fn fn;
+	int cache_fd;
+	int lock_fd;
+	int stdout_fd;
+	const char *cache_name;
+	const char *lock_name;
+	int match;
+	struct stat cache_st;
+	int bufsize;
+	char buf[CACHE_BUFSIZE];
+};
+
+/* Open an existing cache slot and fill the cache buffer with
+ * (part of) the content of the cache file. Return 0 on success
+ * and errno otherwise.
+ */
+static int open_slot(struct cache_slot *slot)
+{
+	char *bufz;
+	ssize_t bufkeylen = -1;
+
+	slot->cache_fd = open(slot->cache_name, O_RDONLY);
+	if (slot->cache_fd == -1)
+		return errno;
+
+	if (fstat(slot->cache_fd, &slot->cache_st))
+		return errno;
+
+	slot->bufsize = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
+	if (slot->bufsize < 0)
+		return errno;
+
+	bufz = memchr(slot->buf, 0, slot->bufsize);
+	if (bufz)
+		bufkeylen = bufz - slot->buf;
+
+	if (slot->key)
+		slot->match = bufkeylen == slot->keylen &&
+		    !memcmp(slot->key, slot->buf, bufkeylen + 1);
+
+	return 0;
+}
+
+/* Close the active cache slot */
+static int close_slot(struct cache_slot *slot)
+{
+	int err = 0;
+	if (slot->cache_fd > 0) {
+		if (close(slot->cache_fd))
+			err = errno;
+		else
+			slot->cache_fd = -1;
+	}
+	return err;
+}
+
+/* Print the content of the active cache slot (but skip the key). */
+static int print_slot(struct cache_slot *slot)
+{
+	off_t off;
+#ifdef HAVE_LINUX_SENDFILE
+	off_t size;
+#endif
+
+	off = slot->keylen + 1;
+
+#ifdef HAVE_LINUX_SENDFILE
+	size = slot->cache_st.st_size;
+
+	do {
+		ssize_t ret;
+		ret = sendfile(STDOUT_FILENO, slot->cache_fd, &off, size - off);
+		if (ret < 0) {
+			if (errno == EAGAIN || errno == EINTR)
+				continue;
+			/* Fall back to read/write on EINVAL or ENOSYS */
+			if (errno == EINVAL || errno == ENOSYS)
+				break;
+			return errno;
+		}
+		if (off == size)
+			return 0;
+	} while (1);
+#endif
+
+	if (lseek(slot->cache_fd, off, SEEK_SET) != off)
+		return errno;
+
+	do {
+		ssize_t ret;
+		ret = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
+		if (ret < 0)
+			return errno;
+		if (ret == 0)
+			return 0;
+		if (write_in_full(STDOUT_FILENO, slot->buf, ret) < 0)
+			return errno;
+	} while (1);
+}
+
+/* Check if the slot has expired */
+static int is_expired(struct cache_slot *slot)
+{
+	if (slot->ttl < 0)
+		return 0;
+	else
+		return slot->cache_st.st_mtime + slot->ttl * 60 < time(NULL);
+}
+
+/* Check if the slot has been modified since we opened it.
+ * NB: If stat() fails, we pretend the file is modified.
+ */
+static int is_modified(struct cache_slot *slot)
+{
+	struct stat st;
+
+	if (stat(slot->cache_name, &st))
+		return 1;
+	return (st.st_ino != slot->cache_st.st_ino ||
+		st.st_mtime != slot->cache_st.st_mtime ||
+		st.st_size != slot->cache_st.st_size);
+}
+
+/* Close an open lockfile */
+static int close_lock(struct cache_slot *slot)
+{
+	int err = 0;
+	if (slot->lock_fd > 0) {
+		if (close(slot->lock_fd))
+			err = errno;
+		else
+			slot->lock_fd = -1;
+	}
+	return err;
+}
+
+/* Create a lockfile used to store the generated content for a cache
+ * slot, and write the slot key + \0 into it.
+ * Returns 0 on success and errno otherwise.
+ */
+static int lock_slot(struct cache_slot *slot)
+{
+	struct flock lock = {
+		.l_type = F_WRLCK,
+		.l_whence = SEEK_SET,
+		.l_start = 0,
+		.l_len = 0,
+	};
+
+	slot->lock_fd = open(slot->lock_name, O_RDWR | O_CREAT,
+			     S_IRUSR | S_IWUSR);
+	if (slot->lock_fd == -1)
+		return errno;
+	if (fcntl(slot->lock_fd, F_SETLK, &lock) < 0) {
+		int saved_errno = errno;
+		close(slot->lock_fd);
+		slot->lock_fd = -1;
+		return saved_errno;
+	}
+	if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0)
+		return errno;
+	return 0;
+}
+
+/* Release the current lockfile. If `replace_old_slot` is set the
+ * lockfile replaces the old cache slot, otherwise the lockfile is
+ * just deleted.
+ */
+static int unlock_slot(struct cache_slot *slot, int replace_old_slot)
+{
+	int err;
+
+	if (replace_old_slot)
+		err = rename(slot->lock_name, slot->cache_name);
+	else
+		err = unlink(slot->lock_name);
+
+	/* Restore stdout and close the temporary FD. */
+	if (slot->stdout_fd >= 0) {
+		dup2(slot->stdout_fd, STDOUT_FILENO);
+		close(slot->stdout_fd);
+		slot->stdout_fd = -1;
+	}
+
+	if (err)
+		return errno;
+
+	return 0;
+}
+
+/* Generate the content for the current cache slot by redirecting
+ * stdout to the lock-fd and invoking the callback function
+ */
+static int fill_slot(struct cache_slot *slot)
+{
+	/* Preserve stdout */
+	slot->stdout_fd = dup(STDOUT_FILENO);
+	if (slot->stdout_fd == -1)
+		return errno;
+
+	/* Redirect stdout to lockfile */
+	if (dup2(slot->lock_fd, STDOUT_FILENO) == -1)
+		return errno;
+
+	/* Generate cache content */
+	slot->fn();
+
+	/* Make sure any buffered data is flushed to the file */
+	if (fflush(stdout))
+		return errno;
+
+	/* update stat info */
+	if (fstat(slot->lock_fd, &slot->cache_st))
+		return errno;
+
+	return 0;
+}
+
+/* Crude implementation of 32-bit FNV-1 hash algorithm,
+ * see http://www.isthe.com/chongo/tech/comp/fnv/ for details
+ * about the magic numbers.
+ */
+#define FNV_OFFSET 0x811c9dc5
+#define FNV_PRIME  0x01000193
+
+unsigned long hash_str(const char *str)
+{
+	unsigned long h = FNV_OFFSET;
+	unsigned char *s = (unsigned char *)str;
+
+	if (!s)
+		return h;
+
+	while (*s) {
+		h *= FNV_PRIME;
+		h ^= *s++;
+	}
+	return h;
+}
+
+static int process_slot(struct cache_slot *slot)
+{
+	int err;
+
+	/*
+	 * Make sure any buffered data is flushed before we redirect,
+	 * do sendfile(2) or write(2)
+	 */
+	if (fflush(stdout))
+		return errno;
+
+	err = open_slot(slot);
+	if (!err && slot->match) {
+		if (is_expired(slot)) {
+			if (!lock_slot(slot)) {
+				/* If the cachefile has been replaced between
+				 * `open_slot` and `lock_slot`, we'll just
+				 * serve the stale content from the original
+				 * cachefile. This way we avoid pruning the
+				 * newly generated slot. The same code-path
+				 * is chosen if fill_slot() fails for some
+				 * reason.
+				 *
+				 * TODO? check if the new slot contains the
+				 * same key as the old one, since we would
+				 * prefer to serve the newest content.
+				 * This will require us to open yet another
+				 * file-descriptor and read and compare the
+				 * key from the new file, so for now we're
+				 * lazy and just ignore the new file.
+				 */
+				if (is_modified(slot) || fill_slot(slot)) {
+					unlock_slot(slot, 0);
+					close_lock(slot);
+				} else {
+					close_slot(slot);
+					unlock_slot(slot, 1);
+					slot->cache_fd = slot->lock_fd;
+				}
+			}
+		}
+		if ((err = print_slot(slot)) != 0) {
+			cache_log("[cgit] error printing cache %s: %s (%d)\n",
+				  slot->cache_name,
+				  strerror(err),
+				  err);
+		}
+		close_slot(slot);
+		return err;
+	}
+
+	/* If the cache slot does not exist (or its key doesn't match the
+	 * current key), lets try to create a new cache slot for this
+	 * request. If this fails (for whatever reason), lets just generate
+	 * the content without caching it and fool the caller to believe
+	 * everything worked out (but print a warning on stdout).
+	 */
+
+	close_slot(slot);
+	if ((err = lock_slot(slot)) != 0) {
+		cache_log("[cgit] Unable to lock slot %s: %s (%d)\n",
+			  slot->lock_name, strerror(err), err);
+		slot->fn();
+		return 0;
+	}
+
+	if ((err = fill_slot(slot)) != 0) {
+		cache_log("[cgit] Unable to fill slot %s: %s (%d)\n",
+			  slot->lock_name, strerror(err), err);
+		unlock_slot(slot, 0);
+		close_lock(slot);
+		slot->fn();
+		return 0;
+	}
+	// We've got a valid cache slot in the lock file, which
+	// is about to replace the old cache slot. But if we
+	// release the lockfile and then try to open the new cache
+	// slot, we might get a race condition with a concurrent
+	// writer for the same cache slot (with a different key).
+	// Lets avoid such a race by just printing the content of
+	// the lock file.
+	slot->cache_fd = slot->lock_fd;
+	unlock_slot(slot, 1);
+	if ((err = print_slot(slot)) != 0) {
+		cache_log("[cgit] error printing cache %s: %s (%d)\n",
+			  slot->cache_name,
+			  strerror(err),
+			  err);
+	}
+	close_slot(slot);
+	return err;
+}
+
+/* Print cached content to stdout, generate the content if necessary. */
+int cache_process(int size, const char *path, const char *key, int ttl,
+		  cache_fill_fn fn)
+{
+	unsigned long hash;
+	int i;
+	struct strbuf filename = STRBUF_INIT;
+	struct strbuf lockname = STRBUF_INIT;
+	struct cache_slot slot;
+	int result;
+
+	/* If the cache is disabled, just generate the content */
+	if (size <= 0 || ttl == 0) {
+		fn();
+		return 0;
+	}
+
+	/* Verify input, calculate filenames */
+	if (!path) {
+		cache_log("[cgit] Cache path not specified, caching is disabled\n");
+		fn();
+		return 0;
+	}
+	if (!key)
+		key = "";
+	hash = hash_str(key) % size;
+	strbuf_addstr(&filename, path);
+	strbuf_ensure_end(&filename, '/');
+	for (i = 0; i < 8; i++) {
+		strbuf_addf(&filename, "%x", (unsigned char)(hash & 0xf));
+		hash >>= 4;
+	}
+	strbuf_addbuf(&lockname, &filename);
+	strbuf_addstr(&lockname, ".lock");
+	slot.fn = fn;
+	slot.ttl = ttl;
+	slot.stdout_fd = -1;
+	slot.cache_name = filename.buf;
+	slot.lock_name = lockname.buf;
+	slot.key = key;
+	slot.keylen = strlen(key);
+	result = process_slot(&slot);
+
+	strbuf_release(&filename);
+	strbuf_release(&lockname);
+	return result;
+}
+
+/* Return a strftime formatted date/time
+ * NB: the result from this function is to shared memory
+ */
+static char *sprintftime(const char *format, time_t time)
+{
+	static char buf[64];
+	struct tm tm;
+
+	if (!time)
+		return NULL;
+	gmtime_r(&time, &tm);
+	strftime(buf, sizeof(buf)-1, format, &tm);
+	return buf;
+}
+
+int cache_ls(const char *path)
+{
+	DIR *dir;
+	struct dirent *ent;
+	int err = 0;
+	struct cache_slot slot = { NULL };
+	struct strbuf fullname = STRBUF_INIT;
+	size_t prefixlen;
+
+	if (!path) {
+		cache_log("[cgit] cache path not specified\n");
+		return -1;
+	}
+	dir = opendir(path);
+	if (!dir) {
+		err = errno;
+		cache_log("[cgit] unable to open path %s: %s (%d)\n",
+			  path, strerror(err), err);
+		return err;
+	}
+	strbuf_addstr(&fullname, path);
+	strbuf_ensure_end(&fullname, '/');
+	prefixlen = fullname.len;
+	while ((ent = readdir(dir)) != NULL) {
+		if (strlen(ent->d_name) != 8)
+			continue;
+		strbuf_setlen(&fullname, prefixlen);
+		strbuf_addstr(&fullname, ent->d_name);
+		slot.cache_name = fullname.buf;
+		if ((err = open_slot(&slot)) != 0) {
+			cache_log("[cgit] unable to open path %s: %s (%d)\n",
+				  fullname.buf, strerror(err), err);
+			continue;
+		}
+		htmlf("%s %s %10"PRIuMAX" %s\n",
+		      fullname.buf,
+		      sprintftime("%Y-%m-%d %H:%M:%S",
+				  slot.cache_st.st_mtime),
+		      (uintmax_t)slot.cache_st.st_size,
+		      slot.buf);
+		close_slot(&slot);
+	}
+	closedir(dir);
+	strbuf_release(&fullname);
+	return 0;
+}
+
+/* Print a message to stdout */
+void cache_log(const char *format, ...)
+{
+	va_list args;
+	va_start(args, format);
+	vfprintf(stderr, format, args);
+	va_end(args);
+}
+
diff --git a/third_party/cgit/cache.h b/third_party/cgit/cache.h
new file mode 100644
index 0000000000..470da4fc15
--- /dev/null
+++ b/third_party/cgit/cache.h
@@ -0,0 +1,37 @@
+/*
+ * Since git has it's own cache.h which we include,
+ * lets test on CGIT_CACHE_H to avoid confusion
+ */
+
+#ifndef CGIT_CACHE_H
+#define CGIT_CACHE_H
+
+typedef void (*cache_fill_fn)(void);
+
+
+/* Print cached content to stdout, generate the content if necessary.
+ *
+ * Parameters
+ *   size    max number of cache files
+ *   path    directory used to store cache files
+ *   key     the key used to lookup cache files
+ *   ttl     max cache time in seconds for this key
+ *   fn      content generator function for this key
+ *
+ * Return value
+ *   0 indicates success, everything else is an error
+ */
+extern int cache_process(int size, const char *path, const char *key, int ttl,
+			 cache_fill_fn fn);
+
+
+/* List info about all cache entries on stdout */
+extern int cache_ls(const char *path);
+
+/* Print a message to stdout */
+__attribute__((format (printf,1,2)))
+extern void cache_log(const char *format, ...);
+
+extern unsigned long hash_str(const char *str);
+
+#endif /* CGIT_CACHE_H */
diff --git a/third_party/cgit/cgit.c b/third_party/cgit/cgit.c
new file mode 100644
index 0000000000..dd28a79315
--- /dev/null
+++ b/third_party/cgit/cgit.c
@@ -0,0 +1,1107 @@
+/* cgit.c: cgi for the git scm
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "cache.h"
+#include "cmd.h"
+#include "configfile.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "ui-stats.h"
+#include "ui-blob.h"
+#include "ui-summary.h"
+#include "scan-tree.h"
+
+const char *cgit_version = CGIT_VERSION;
+
+__attribute__((constructor))
+static void constructor_environment()
+{
+	/* Do not look in /etc/ for gitconfig and gitattributes. */
+	setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
+	setenv("GIT_ATTR_NOSYSTEM", "1", 1);
+	unsetenv("HOME");
+	unsetenv("XDG_CONFIG_HOME");
+}
+
+static void add_mimetype(const char *name, const char *value)
+{
+	struct string_list_item *item;
+
+	item = string_list_insert(&ctx.cfg.mimetypes, name);
+	item->util = xstrdup(value);
+}
+
+static void process_cached_repolist(const char *path);
+
+static void repo_config(struct cgit_repo *repo, const char *name, const char *value)
+{
+	const char *path;
+	struct string_list_item *item;
+
+	if (!strcmp(name, "name"))
+		repo->name = xstrdup(value);
+	else if (!strcmp(name, "clone-url"))
+		repo->clone_url = xstrdup(value);
+	else if (!strcmp(name, "desc"))
+		repo->desc = xstrdup(value);
+	else if (!strcmp(name, "owner"))
+		repo->owner = xstrdup(value);
+	else if (!strcmp(name, "homepage"))
+		repo->homepage = xstrdup(value);
+	else if (!strcmp(name, "defbranch"))
+		repo->defbranch = xstrdup(value);
+	else if (!strcmp(name, "extra-head-content"))
+		repo->extra_head_content = xstrdup(value);
+	else if (!strcmp(name, "snapshots"))
+		repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value);
+	else if (!strcmp(name, "enable-blame"))
+		repo->enable_blame = atoi(value);
+	else if (!strcmp(name, "enable-commit-graph"))
+		repo->enable_commit_graph = atoi(value);
+	else if (!strcmp(name, "enable-log-filecount"))
+		repo->enable_log_filecount = atoi(value);
+	else if (!strcmp(name, "enable-log-linecount"))
+		repo->enable_log_linecount = atoi(value);
+	else if (!strcmp(name, "enable-remote-branches"))
+		repo->enable_remote_branches = atoi(value);
+	else if (!strcmp(name, "enable-subject-links"))
+		repo->enable_subject_links = atoi(value);
+	else if (!strcmp(name, "enable-html-serving"))
+		repo->enable_html_serving = atoi(value);
+	else if (!strcmp(name, "branch-sort")) {
+		if (!strcmp(value, "age"))
+			repo->branch_sort = 1;
+		if (!strcmp(value, "name"))
+			repo->branch_sort = 0;
+	} else if (!strcmp(name, "commit-sort")) {
+		if (!strcmp(value, "date"))
+			repo->commit_sort = 1;
+		if (!strcmp(value, "topo"))
+			repo->commit_sort = 2;
+	} else if (!strcmp(name, "max-stats"))
+		repo->max_stats = cgit_find_stats_period(value, NULL);
+	else if (!strcmp(name, "module-link"))
+		repo->module_link= xstrdup(value);
+	else if (skip_prefix(name, "module-link.", &path)) {
+		item = string_list_append(&repo->submodules, xstrdup(path));
+		item->util = xstrdup(value);
+	} else if (!strcmp(name, "section"))
+		repo->section = xstrdup(value);
+	else if (!strcmp(name, "snapshot-prefix"))
+		repo->snapshot_prefix = xstrdup(value);
+	else if (!strcmp(name, "readme") && value != NULL) {
+		if (repo->readme.items == ctx.cfg.readme.items)
+			memset(&repo->readme, 0, sizeof(repo->readme));
+		string_list_append(&repo->readme, xstrdup(value));
+	} else if (!strcmp(name, "logo") && value != NULL)
+		repo->logo = xstrdup(value);
+	else if (!strcmp(name, "logo-link") && value != NULL)
+		repo->logo_link = xstrdup(value);
+	else if (!strcmp(name, "hide"))
+		repo->hide = atoi(value);
+	else if (!strcmp(name, "ignore"))
+		repo->ignore = atoi(value);
+	else if (ctx.cfg.enable_filter_overrides) {
+		if (!strcmp(name, "about-filter"))
+			repo->about_filter = cgit_new_filter(value, ABOUT);
+		else if (!strcmp(name, "commit-filter"))
+			repo->commit_filter = cgit_new_filter(value, COMMIT);
+		else if (!strcmp(name, "source-filter"))
+			repo->source_filter = cgit_new_filter(value, SOURCE);
+		else if (!strcmp(name, "email-filter"))
+			repo->email_filter = cgit_new_filter(value, EMAIL);
+		else if (!strcmp(name, "owner-filter"))
+			repo->owner_filter = cgit_new_filter(value, OWNER);
+	}
+}
+
+static void config_cb(const char *name, const char *value)
+{
+	const char *arg;
+
+	if (!strcmp(name, "section"))
+		ctx.cfg.section = xstrdup(value);
+	else if (!strcmp(name, "repo.url"))
+		ctx.repo = cgit_add_repo(value);
+	else if (ctx.repo && !strcmp(name, "repo.path"))
+		ctx.repo->path = trim_end(value, '/');
+	else if (ctx.repo && skip_prefix(name, "repo.", &arg))
+		repo_config(ctx.repo, arg, value);
+	else if (!strcmp(name, "readme"))
+		string_list_append(&ctx.cfg.readme, xstrdup(value));
+	else if (!strcmp(name, "root-title"))
+		ctx.cfg.root_title = xstrdup(value);
+	else if (!strcmp(name, "root-desc"))
+		ctx.cfg.root_desc = xstrdup(value);
+	else if (!strcmp(name, "root-readme"))
+		ctx.cfg.root_readme = xstrdup(value);
+	else if (!strcmp(name, "css"))
+		ctx.cfg.css = xstrdup(value);
+	else if (!strcmp(name, "favicon"))
+		ctx.cfg.favicon = xstrdup(value);
+	else if (!strcmp(name, "footer"))
+		ctx.cfg.footer = xstrdup(value);
+	else if (!strcmp(name, "head-include"))
+		ctx.cfg.head_include = xstrdup(value);
+	else if (!strcmp(name, "header"))
+		ctx.cfg.header = xstrdup(value);
+	else if (!strcmp(name, "logo"))
+		ctx.cfg.logo = xstrdup(value);
+	else if (!strcmp(name, "logo-link"))
+		ctx.cfg.logo_link = xstrdup(value);
+	else if (!strcmp(name, "module-link"))
+		ctx.cfg.module_link = xstrdup(value);
+	else if (!strcmp(name, "strict-export"))
+		ctx.cfg.strict_export = xstrdup(value);
+	else if (!strcmp(name, "virtual-root"))
+		ctx.cfg.virtual_root = ensure_end(value, '/');
+	else if (!strcmp(name, "noplainemail"))
+		ctx.cfg.noplainemail = atoi(value);
+	else if (!strcmp(name, "noheader"))
+		ctx.cfg.noheader = atoi(value);
+	else if (!strcmp(name, "snapshots"))
+		ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
+	else if (!strcmp(name, "enable-filter-overrides"))
+		ctx.cfg.enable_filter_overrides = atoi(value);
+	else if (!strcmp(name, "enable-follow-links"))
+		ctx.cfg.enable_follow_links = atoi(value);
+	else if (!strcmp(name, "enable-http-clone"))
+		ctx.cfg.enable_http_clone = atoi(value);
+	else if (!strcmp(name, "enable-index-links"))
+		ctx.cfg.enable_index_links = atoi(value);
+	else if (!strcmp(name, "enable-index-owner"))
+		ctx.cfg.enable_index_owner = atoi(value);
+	else if (!strcmp(name, "enable-blame"))
+		ctx.cfg.enable_blame = atoi(value);
+	else if (!strcmp(name, "enable-commit-graph"))
+		ctx.cfg.enable_commit_graph = atoi(value);
+	else if (!strcmp(name, "enable-log-filecount"))
+		ctx.cfg.enable_log_filecount = atoi(value);
+	else if (!strcmp(name, "enable-log-linecount"))
+		ctx.cfg.enable_log_linecount = atoi(value);
+	else if (!strcmp(name, "enable-remote-branches"))
+		ctx.cfg.enable_remote_branches = atoi(value);
+	else if (!strcmp(name, "enable-subject-links"))
+		ctx.cfg.enable_subject_links = atoi(value);
+	else if (!strcmp(name, "enable-html-serving"))
+		ctx.cfg.enable_html_serving = atoi(value);
+	else if (!strcmp(name, "enable-tree-linenumbers"))
+		ctx.cfg.enable_tree_linenumbers = atoi(value);
+	else if (!strcmp(name, "enable-git-config"))
+		ctx.cfg.enable_git_config = atoi(value);
+	else if (!strcmp(name, "max-stats"))
+		ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
+	else if (!strcmp(name, "cache-size"))
+		ctx.cfg.cache_size = atoi(value);
+	else if (!strcmp(name, "cache-root"))
+		ctx.cfg.cache_root = xstrdup(expand_macros(value));
+	else if (!strcmp(name, "cache-root-ttl"))
+		ctx.cfg.cache_root_ttl = atoi(value);
+	else if (!strcmp(name, "cache-repo-ttl"))
+		ctx.cfg.cache_repo_ttl = atoi(value);
+	else if (!strcmp(name, "cache-scanrc-ttl"))
+		ctx.cfg.cache_scanrc_ttl = atoi(value);
+	else if (!strcmp(name, "cache-static-ttl"))
+		ctx.cfg.cache_static_ttl = atoi(value);
+	else if (!strcmp(name, "cache-dynamic-ttl"))
+		ctx.cfg.cache_dynamic_ttl = atoi(value);
+	else if (!strcmp(name, "cache-about-ttl"))
+		ctx.cfg.cache_about_ttl = atoi(value);
+	else if (!strcmp(name, "cache-snapshot-ttl"))
+		ctx.cfg.cache_snapshot_ttl = atoi(value);
+	else if (!strcmp(name, "case-sensitive-sort"))
+		ctx.cfg.case_sensitive_sort = atoi(value);
+	else if (!strcmp(name, "about-filter"))
+		ctx.cfg.about_filter = cgit_new_filter(value, ABOUT);
+	else if (!strcmp(name, "commit-filter"))
+		ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT);
+	else if (!strcmp(name, "email-filter"))
+		ctx.cfg.email_filter = cgit_new_filter(value, EMAIL);
+	else if (!strcmp(name, "owner-filter"))
+		ctx.cfg.owner_filter = cgit_new_filter(value, OWNER);
+	else if (!strcmp(name, "auth-filter"))
+		ctx.cfg.auth_filter = cgit_new_filter(value, AUTH);
+	else if (!strcmp(name, "embedded"))
+		ctx.cfg.embedded = atoi(value);
+	else if (!strcmp(name, "max-atom-items"))
+		ctx.cfg.max_atom_items = atoi(value);
+	else if (!strcmp(name, "max-message-length"))
+		ctx.cfg.max_msg_len = atoi(value);
+	else if (!strcmp(name, "max-repodesc-length"))
+		ctx.cfg.max_repodesc_len = atoi(value);
+	else if (!strcmp(name, "max-blob-size"))
+		ctx.cfg.max_blob_size = atoi(value);
+	else if (!strcmp(name, "max-repo-count"))
+		ctx.cfg.max_repo_count = atoi(value);
+	else if (!strcmp(name, "max-commit-count"))
+		ctx.cfg.max_commit_count = atoi(value);
+	else if (!strcmp(name, "project-list"))
+		ctx.cfg.project_list = xstrdup(expand_macros(value));
+	else if (!strcmp(name, "scan-path"))
+		if (ctx.cfg.cache_size)
+			process_cached_repolist(expand_macros(value));
+		else if (ctx.cfg.project_list)
+			scan_projects(expand_macros(value),
+				      ctx.cfg.project_list, repo_config);
+		else
+			scan_tree(expand_macros(value), repo_config);
+	else if (!strcmp(name, "scan-hidden-path"))
+		ctx.cfg.scan_hidden_path = atoi(value);
+	else if (!strcmp(name, "section-from-path"))
+		ctx.cfg.section_from_path = atoi(value);
+	else if (!strcmp(name, "repository-sort"))
+		ctx.cfg.repository_sort = xstrdup(value);
+	else if (!strcmp(name, "section-sort"))
+		ctx.cfg.section_sort = atoi(value);
+	else if (!strcmp(name, "source-filter"))
+		ctx.cfg.source_filter = cgit_new_filter(value, SOURCE);
+	else if (!strcmp(name, "summary-log"))
+		ctx.cfg.summary_log = atoi(value);
+	else if (!strcmp(name, "summary-branches"))
+		ctx.cfg.summary_branches = atoi(value);
+	else if (!strcmp(name, "summary-tags"))
+		ctx.cfg.summary_tags = atoi(value);
+	else if (!strcmp(name, "side-by-side-diffs"))
+		ctx.cfg.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED;
+	else if (!strcmp(name, "agefile"))
+		ctx.cfg.agefile = xstrdup(value);
+	else if (!strcmp(name, "mimetype-file"))
+		ctx.cfg.mimetype_file = xstrdup(value);
+	else if (!strcmp(name, "renamelimit"))
+		ctx.cfg.renamelimit = atoi(value);
+	else if (!strcmp(name, "remove-suffix"))
+		ctx.cfg.remove_suffix = atoi(value);
+	else if (!strcmp(name, "robots"))
+		ctx.cfg.robots = xstrdup(value);
+	else if (!strcmp(name, "clone-prefix"))
+		ctx.cfg.clone_prefix = xstrdup(value);
+	else if (!strcmp(name, "clone-url"))
+		ctx.cfg.clone_url = xstrdup(value);
+	else if (!strcmp(name, "local-time"))
+		ctx.cfg.local_time = atoi(value);
+	else if (!strcmp(name, "commit-sort")) {
+		if (!strcmp(value, "date"))
+			ctx.cfg.commit_sort = 1;
+		if (!strcmp(value, "topo"))
+			ctx.cfg.commit_sort = 2;
+	} else if (!strcmp(name, "branch-sort")) {
+		if (!strcmp(value, "age"))
+			ctx.cfg.branch_sort = 1;
+		if (!strcmp(value, "name"))
+			ctx.cfg.branch_sort = 0;
+	} else if (skip_prefix(name, "mimetype.", &arg))
+		add_mimetype(arg, value);
+	else if (!strcmp(name, "include"))
+		parse_configfile(expand_macros(value), config_cb);
+}
+
+static void querystring_cb(const char *name, const char *value)
+{
+	if (!value)
+		value = "";
+
+	if (!strcmp(name,"r")) {
+		ctx.qry.repo = xstrdup(value);
+		ctx.repo = cgit_get_repoinfo(value);
+	} else if (!strcmp(name, "p")) {
+		ctx.qry.page = xstrdup(value);
+	} else if (!strcmp(name, "url")) {
+		if (*value == '/')
+			value++;
+		ctx.qry.url = xstrdup(value);
+		cgit_parse_url(value);
+	} else if (!strcmp(name, "qt")) {
+		ctx.qry.grep = xstrdup(value);
+	} else if (!strcmp(name, "q")) {
+		ctx.qry.search = xstrdup(value);
+	} else if (!strcmp(name, "h")) {
+		ctx.qry.head = xstrdup(value);
+		ctx.qry.has_symref = 1;
+	} else if (!strcmp(name, "id")) {
+		ctx.qry.oid = xstrdup(value);
+		ctx.qry.has_oid = 1;
+	} else if (!strcmp(name, "id2")) {
+		ctx.qry.oid2 = xstrdup(value);
+		ctx.qry.has_oid = 1;
+	} else if (!strcmp(name, "ofs")) {
+		ctx.qry.ofs = atoi(value);
+	} else if (!strcmp(name, "path")) {
+		ctx.qry.path = trim_end(value, '/');
+	} else if (!strcmp(name, "name")) {
+		ctx.qry.name = xstrdup(value);
+	} else if (!strcmp(name, "s")) {
+		ctx.qry.sort = xstrdup(value);
+	} else if (!strcmp(name, "showmsg")) {
+		ctx.qry.showmsg = atoi(value);
+	} else if (!strcmp(name, "period")) {
+		ctx.qry.period = xstrdup(value);
+	} else if (!strcmp(name, "dt")) {
+		ctx.qry.difftype = atoi(value);
+		ctx.qry.has_difftype = 1;
+	} else if (!strcmp(name, "ss")) {
+		/* No longer generated, but there may be links out there. */
+		ctx.qry.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED;
+		ctx.qry.has_difftype = 1;
+	} else if (!strcmp(name, "all")) {
+		ctx.qry.show_all = atoi(value);
+	} else if (!strcmp(name, "context")) {
+		ctx.qry.context = atoi(value);
+	} else if (!strcmp(name, "ignorews")) {
+		ctx.qry.ignorews = atoi(value);
+	} else if (!strcmp(name, "follow")) {
+		ctx.qry.follow = atoi(value);
+	}
+}
+
+static void prepare_context(void)
+{
+	memset(&ctx, 0, sizeof(ctx));
+	ctx.cfg.agefile = "info/web/last-modified";
+	ctx.cfg.cache_size = 0;
+	ctx.cfg.cache_max_create_time = 5;
+	ctx.cfg.cache_root = CGIT_CACHE_ROOT;
+	ctx.cfg.cache_about_ttl = 15;
+	ctx.cfg.cache_snapshot_ttl = 5;
+	ctx.cfg.cache_repo_ttl = 5;
+	ctx.cfg.cache_root_ttl = 5;
+	ctx.cfg.cache_scanrc_ttl = 15;
+	ctx.cfg.cache_dynamic_ttl = 5;
+	ctx.cfg.cache_static_ttl = -1;
+	ctx.cfg.case_sensitive_sort = 1;
+	ctx.cfg.branch_sort = 0;
+	ctx.cfg.commit_sort = 0;
+	ctx.cfg.css = "/cgit.css";
+	ctx.cfg.logo = "/cgit.png";
+	ctx.cfg.favicon = NULL;
+	ctx.cfg.local_time = 0;
+	ctx.cfg.enable_http_clone = 1;
+	ctx.cfg.enable_index_owner = 1;
+	ctx.cfg.enable_tree_linenumbers = 1;
+	ctx.cfg.enable_git_config = 0;
+	ctx.cfg.max_repo_count = 50;
+	ctx.cfg.max_commit_count = 50;
+	ctx.cfg.max_lock_attempts = 5;
+	ctx.cfg.max_msg_len = 80;
+	ctx.cfg.max_repodesc_len = 80;
+	ctx.cfg.max_blob_size = 0;
+	ctx.cfg.max_stats = 0;
+	ctx.cfg.project_list = NULL;
+	ctx.cfg.renamelimit = -1;
+	ctx.cfg.remove_suffix = 0;
+	ctx.cfg.robots = "index, nofollow";
+	ctx.cfg.root_title = "Git repository browser";
+	ctx.cfg.root_desc = "a fast webinterface for the git dscm";
+	ctx.cfg.scan_hidden_path = 0;
+	ctx.cfg.script_name = CGIT_SCRIPT_NAME;
+	ctx.cfg.section = "";
+	ctx.cfg.repository_sort = "name";
+	ctx.cfg.section_sort = 1;
+	ctx.cfg.summary_branches = 10;
+	ctx.cfg.summary_log = 10;
+	ctx.cfg.summary_tags = 10;
+	ctx.cfg.max_atom_items = 10;
+	ctx.cfg.difftype = DIFF_UNIFIED;
+	ctx.env.cgit_config = getenv("CGIT_CONFIG");
+	ctx.env.http_host = getenv("HTTP_HOST");
+	ctx.env.https = getenv("HTTPS");
+	ctx.env.no_http = getenv("NO_HTTP");
+	ctx.env.path_info = getenv("PATH_INFO");
+	ctx.env.query_string = getenv("QUERY_STRING");
+	ctx.env.request_method = getenv("REQUEST_METHOD");
+	ctx.env.script_name = getenv("SCRIPT_NAME");
+	ctx.env.server_name = getenv("SERVER_NAME");
+	ctx.env.server_port = getenv("SERVER_PORT");
+	ctx.env.http_cookie = getenv("HTTP_COOKIE");
+	ctx.env.http_referer = getenv("HTTP_REFERER");
+	ctx.env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0;
+	ctx.env.authenticated = 0;
+	ctx.page.mimetype = "text/html";
+	ctx.page.charset = PAGE_ENCODING;
+	ctx.page.filename = NULL;
+	ctx.page.size = 0;
+	ctx.page.modified = time(NULL);
+	ctx.page.expires = ctx.page.modified;
+	ctx.page.etag = NULL;
+	string_list_init_dup(&ctx.cfg.mimetypes);
+	if (ctx.env.script_name)
+		ctx.cfg.script_name = xstrdup(ctx.env.script_name);
+	if (ctx.env.query_string)
+		ctx.qry.raw = xstrdup(ctx.env.query_string);
+	if (!ctx.env.cgit_config)
+		ctx.env.cgit_config = CGIT_CONFIG;
+}
+
+struct refmatch {
+	char *req_ref;
+	char *first_ref;
+	int match;
+};
+
+static int find_current_ref(const char *refname, const struct object_id *oid,
+			    int flags, void *cb_data)
+{
+	struct refmatch *info;
+
+	info = (struct refmatch *)cb_data;
+	if (!strcmp(refname, info->req_ref))
+		info->match = 1;
+	if (!info->first_ref)
+		info->first_ref = xstrdup(refname);
+	return info->match;
+}
+
+static void free_refmatch_inner(struct refmatch *info)
+{
+	if (info->first_ref)
+		free(info->first_ref);
+}
+
+static char *find_default_branch(struct cgit_repo *repo)
+{
+	struct refmatch info;
+	char *ref;
+
+	info.req_ref = repo->defbranch;
+	info.first_ref = NULL;
+	info.match = 0;
+	for_each_branch_ref(find_current_ref, &info);
+	if (info.match)
+		ref = info.req_ref;
+	else
+		ref = info.first_ref;
+	if (ref)
+		ref = xstrdup(ref);
+	free_refmatch_inner(&info);
+
+	return ref;
+}
+
+static char *guess_defbranch(void)
+{
+	const char *ref, *refname;
+	struct object_id oid;
+
+	ref = resolve_ref_unsafe("HEAD", 0, &oid, NULL);
+	if (!ref || !skip_prefix(ref, "refs/heads/", &refname))
+		return "master";
+	return xstrdup(refname);
+}
+
+/* The caller must free filename and ref after calling this. */
+static inline void parse_readme(const char *readme, char **filename, char **ref, struct cgit_repo *repo)
+{
+	const char *colon;
+
+	*filename = NULL;
+	*ref = NULL;
+
+	if (!readme || !readme[0])
+		return;
+
+	/* Check if the readme is tracked in the git repo. */
+	colon = strchr(readme, ':');
+	if (colon && strlen(colon) > 1) {
+		/* If it starts with a colon, we want to use head given
+		 * from query or the default branch */
+		if (colon == readme && ctx.qry.head)
+			*ref = xstrdup(ctx.qry.head);
+		else if (colon == readme && repo->defbranch)
+			*ref = xstrdup(repo->defbranch);
+		else
+			*ref = xstrndup(readme, colon - readme);
+		readme = colon + 1;
+	}
+
+	/* Prepend repo path to relative readme path unless tracked. */
+	if (!(*ref) && readme[0] != '/')
+		*filename = fmtalloc("%s/%s", repo->path, readme);
+	else
+		*filename = xstrdup(readme);
+}
+static void choose_readme(struct cgit_repo *repo)
+{
+	int found;
+	char *filename, *ref;
+	struct string_list_item *entry;
+
+	if (!repo->readme.nr)
+		return;
+
+	found = 0;
+	for_each_string_list_item(entry, &repo->readme) {
+		parse_readme(entry->string, &filename, &ref, repo);
+		if (!filename) {
+			free(filename);
+			free(ref);
+			continue;
+		}
+		if (ref) {
+			if (cgit_ref_path_exists(filename, ref, 1)) {
+				found = 1;
+				break;
+			}
+		}
+		else if (!access(filename, R_OK)) {
+			found = 1;
+			break;
+		}
+		free(filename);
+		free(ref);
+	}
+	repo->readme.strdup_strings = 1;
+	string_list_clear(&repo->readme, 0);
+	repo->readme.strdup_strings = 0;
+	if (found)
+		string_list_append(&repo->readme, filename)->util = ref;
+}
+
+static void print_no_repo_clone_urls(const char *url)
+{
+        html("<tr><td><a rel='vcs-git' href='");
+        html_url_path(url);
+        html("' title='");
+        html_attr(ctx.repo->name);
+        html(" Git repository'>");
+        html_txt(url);
+        html("</a></td></tr>\n");
+}
+
+static void prepare_repo_env(int *nongit)
+{
+	/* The path to the git repository. */
+	setenv("GIT_DIR", ctx.repo->path, 1);
+
+	/* Setup the git directory and initialize the notes system. Both of these
+	 * load local configuration from the git repository, so we do them both while
+	 * the HOME variables are unset. */
+	setup_git_directory_gently(nongit);
+	load_display_notes(NULL);
+}
+
+static int prepare_repo_cmd(int nongit)
+{
+	struct object_id oid;
+	int rc;
+
+	if (nongit) {
+		const char *name = ctx.repo->name;
+		rc = errno;
+		ctx.page.title = fmtalloc("%s - %s", ctx.cfg.root_title,
+						"config error");
+		ctx.repo = NULL;
+		cgit_print_http_headers();
+		cgit_print_docstart();
+		cgit_print_pageheader();
+		cgit_print_error("Failed to open %s: %s", name,
+				 rc ? strerror(rc) : "Not a valid git repository");
+		cgit_print_docend();
+		return 1;
+	}
+	ctx.page.title = fmtalloc("%s - %s", ctx.repo->name, ctx.repo->desc);
+
+	if (!ctx.repo->defbranch)
+		ctx.repo->defbranch = guess_defbranch();
+
+	if (!ctx.qry.head) {
+		ctx.qry.nohead = 1;
+		ctx.qry.head = find_default_branch(ctx.repo);
+	}
+
+	if (!ctx.qry.head) {
+		cgit_print_http_headers();
+		cgit_print_docstart();
+		cgit_print_pageheader();
+		cgit_print_error("Repository seems to be empty");
+		if (!strcmp(ctx.qry.page, "summary")) {
+			html("<table class='list'><tr class='nohover'><td>&nbsp;</td></tr><tr class='nohover'><th class='left'>Clone</th></tr>\n");
+			cgit_prepare_repo_env(ctx.repo);
+			cgit_add_clone_urls(print_no_repo_clone_urls);
+			html("</table>\n");
+		}
+		cgit_print_docend();
+		return 1;
+	}
+
+	if (get_oid(ctx.qry.head, &oid)) {
+		char *old_head = ctx.qry.head;
+		ctx.qry.head = xstrdup(ctx.repo->defbranch);
+		cgit_print_error_page(404, "Not found",
+				"Invalid branch: %s", old_head);
+		free(old_head);
+		return 1;
+	}
+	string_list_sort(&ctx.repo->submodules);
+	cgit_prepare_repo_env(ctx.repo);
+	choose_readme(ctx.repo);
+	return 0;
+}
+
+static inline void open_auth_filter(const char *function)
+{
+	cgit_open_filter(ctx.cfg.auth_filter, function,
+		ctx.env.http_cookie ? ctx.env.http_cookie : "",
+		ctx.env.request_method ? ctx.env.request_method : "",
+		ctx.env.query_string ? ctx.env.query_string : "",
+		ctx.env.http_referer ? ctx.env.http_referer : "",
+		ctx.env.path_info ? ctx.env.path_info : "",
+		ctx.env.http_host ? ctx.env.http_host : "",
+		ctx.env.https ? ctx.env.https : "",
+		ctx.qry.repo ? ctx.qry.repo : "",
+		ctx.qry.page ? ctx.qry.page : "",
+		cgit_currentfullurl(),
+		cgit_loginurl());
+}
+
+/* We intentionally keep this rather small, instead of looping and
+ * feeding it to the filter a couple bytes at a time. This way, the
+ * filter itself does not need to handle any denial of service or
+ * buffer bloat issues. If this winds up being too small, people
+ * will complain on the mailing list, and we'll increase it as needed. */
+#define MAX_AUTHENTICATION_POST_BYTES 4096
+/* The filter is expected to spit out "Status: " and all headers. */
+static inline void authenticate_post(void)
+{
+	char buffer[MAX_AUTHENTICATION_POST_BYTES];
+	ssize_t len;
+
+	open_auth_filter("authenticate-post");
+	len = ctx.env.content_length;
+	if (len > MAX_AUTHENTICATION_POST_BYTES)
+		len = MAX_AUTHENTICATION_POST_BYTES;
+	if ((len = read(STDIN_FILENO, buffer, len)) < 0)
+		die_errno("Could not read POST from stdin");
+	if (fwrite(buffer, 1, len, stdout) < len)
+		die_errno("Could not write POST to stdout");
+	cgit_close_filter(ctx.cfg.auth_filter);
+	exit(0);
+}
+
+static inline void authenticate_cookie(void)
+{
+	/* If we don't have an auth_filter, consider all cookies valid, and thus return early. */
+	if (!ctx.cfg.auth_filter) {
+		ctx.env.authenticated = 1;
+		return;
+	}
+
+	/* If we're having something POST'd to /login, we're authenticating POST,
+	 * instead of the cookie, so call authenticate_post and bail out early.
+	 * This pattern here should match /?p=login with POST. */
+	if (ctx.env.request_method && ctx.qry.page && !ctx.repo && \
+	    !strcmp(ctx.env.request_method, "POST") && !strcmp(ctx.qry.page, "login")) {
+		authenticate_post();
+		return;
+	}
+
+	/* If we've made it this far, we're authenticating the cookie for real, so do that. */
+	open_auth_filter("authenticate-cookie");
+	ctx.env.authenticated = cgit_close_filter(ctx.cfg.auth_filter);
+}
+
+static void process_request(void)
+{
+	struct cgit_cmd *cmd;
+	int nongit = 0;
+
+	/* If we're not yet authenticated, no matter what page we're on,
+	 * display the authentication body from the auth_filter. This should
+	 * never be cached. */
+	if (!ctx.env.authenticated) {
+		ctx.page.title = "Authentication Required";
+		cgit_print_http_headers();
+		cgit_print_docstart();
+		cgit_print_pageheader();
+		open_auth_filter("body");
+		cgit_close_filter(ctx.cfg.auth_filter);
+		cgit_print_docend();
+		return;
+	}
+
+	if (ctx.repo)
+		prepare_repo_env(&nongit);
+
+	cmd = cgit_get_cmd();
+	if (!cmd) {
+		ctx.page.title = "cgit error";
+		cgit_print_error_page(404, "Not found", "Invalid request");
+		return;
+	}
+
+	if (!ctx.cfg.enable_http_clone && cmd->is_clone) {
+		ctx.page.title = "cgit error";
+		cgit_print_error_page(404, "Not found", "Invalid request");
+		return;
+	}
+
+	if (cmd->want_repo && !ctx.repo) {
+		cgit_print_error_page(400, "Bad request",
+				"No repository selected");
+		return;
+	}
+
+	/* If cmd->want_vpath is set, assume ctx.qry.path contains a "virtual"
+	 * in-project path limit to be made available at ctx.qry.vpath.
+	 * Otherwise, no path limit is in effect (ctx.qry.vpath = NULL).
+	 */
+	ctx.qry.vpath = cmd->want_vpath ? ctx.qry.path : NULL;
+
+	if (ctx.repo && prepare_repo_cmd(nongit))
+		return;
+
+	cmd->fn();
+}
+
+static int cmp_repos(const void *a, const void *b)
+{
+	const struct cgit_repo *ra = a, *rb = b;
+	return strcmp(ra->url, rb->url);
+}
+
+static char *build_snapshot_setting(int bitmap)
+{
+	const struct cgit_snapshot_format *f;
+	struct strbuf result = STRBUF_INIT;
+
+	for (f = cgit_snapshot_formats; f->suffix; f++) {
+		if (cgit_snapshot_format_bit(f) & bitmap) {
+			if (result.len)
+				strbuf_addch(&result, ' ');
+			strbuf_addstr(&result, f->suffix);
+		}
+	}
+	return strbuf_detach(&result, NULL);
+}
+
+static char *get_first_line(char *txt)
+{
+	char *t = xstrdup(txt);
+	char *p = strchr(t, '\n');
+	if (p)
+		*p = '\0';
+	return t;
+}
+
+static void print_repo(FILE *f, struct cgit_repo *repo)
+{
+	struct string_list_item *item;
+	fprintf(f, "repo.url=%s\n", repo->url);
+	fprintf(f, "repo.name=%s\n", repo->name);
+	fprintf(f, "repo.path=%s\n", repo->path);
+	if (repo->owner)
+		fprintf(f, "repo.owner=%s\n", repo->owner);
+	if (repo->desc) {
+		char *tmp = get_first_line(repo->desc);
+		fprintf(f, "repo.desc=%s\n", tmp);
+		free(tmp);
+	}
+	for_each_string_list_item(item, &repo->readme) {
+		if (item->util)
+			fprintf(f, "repo.readme=%s:%s\n", (char *)item->util, item->string);
+		else
+			fprintf(f, "repo.readme=%s\n", item->string);
+	}
+	if (repo->defbranch)
+		fprintf(f, "repo.defbranch=%s\n", repo->defbranch);
+	if (repo->extra_head_content)
+		fprintf(f, "repo.extra-head-content=%s\n", repo->extra_head_content);
+	if (repo->module_link)
+		fprintf(f, "repo.module-link=%s\n", repo->module_link);
+	if (repo->section)
+		fprintf(f, "repo.section=%s\n", repo->section);
+	if (repo->homepage)
+		fprintf(f, "repo.homepage=%s\n", repo->homepage);
+	if (repo->clone_url)
+		fprintf(f, "repo.clone-url=%s\n", repo->clone_url);
+	fprintf(f, "repo.enable-blame=%d\n",
+	        repo->enable_blame);
+	fprintf(f, "repo.enable-commit-graph=%d\n",
+	        repo->enable_commit_graph);
+	fprintf(f, "repo.enable-log-filecount=%d\n",
+	        repo->enable_log_filecount);
+	fprintf(f, "repo.enable-log-linecount=%d\n",
+	        repo->enable_log_linecount);
+	if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter)
+		cgit_fprintf_filter(repo->about_filter, f, "repo.about-filter=");
+	if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter)
+		cgit_fprintf_filter(repo->commit_filter, f, "repo.commit-filter=");
+	if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter)
+		cgit_fprintf_filter(repo->source_filter, f, "repo.source-filter=");
+	if (repo->email_filter && repo->email_filter != ctx.cfg.email_filter)
+		cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter=");
+	if (repo->owner_filter && repo->owner_filter != ctx.cfg.owner_filter)
+		cgit_fprintf_filter(repo->owner_filter, f, "repo.owner-filter=");
+	if (repo->snapshots != ctx.cfg.snapshots) {
+		char *tmp = build_snapshot_setting(repo->snapshots);
+		fprintf(f, "repo.snapshots=%s\n", tmp ? tmp : "");
+		free(tmp);
+	}
+	if (repo->snapshot_prefix)
+		fprintf(f, "repo.snapshot-prefix=%s\n", repo->snapshot_prefix);
+	if (repo->max_stats != ctx.cfg.max_stats)
+		fprintf(f, "repo.max-stats=%s\n",
+		        cgit_find_stats_periodname(repo->max_stats));
+	if (repo->logo)
+		fprintf(f, "repo.logo=%s\n", repo->logo);
+	if (repo->logo_link)
+		fprintf(f, "repo.logo-link=%s\n", repo->logo_link);
+	fprintf(f, "repo.enable-remote-branches=%d\n", repo->enable_remote_branches);
+	fprintf(f, "repo.enable-subject-links=%d\n", repo->enable_subject_links);
+	fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving);
+	if (repo->branch_sort == 1)
+		fprintf(f, "repo.branch-sort=age\n");
+	if (repo->commit_sort) {
+		if (repo->commit_sort == 1)
+			fprintf(f, "repo.commit-sort=date\n");
+		else if (repo->commit_sort == 2)
+			fprintf(f, "repo.commit-sort=topo\n");
+	}
+	fprintf(f, "repo.hide=%d\n", repo->hide);
+	fprintf(f, "repo.ignore=%d\n", repo->ignore);
+	fprintf(f, "\n");
+}
+
+static void print_repolist(FILE *f, struct cgit_repolist *list, int start)
+{
+	int i;
+
+	for (i = start; i < list->count; i++)
+		print_repo(f, &list->repos[i]);
+}
+
+/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc'
+ * and return 0 on success.
+ */
+static int generate_cached_repolist(const char *path, const char *cached_rc)
+{
+	struct strbuf locked_rc = STRBUF_INIT;
+	int result = 0;
+	int idx;
+	FILE *f;
+
+	strbuf_addf(&locked_rc, "%s.lock", cached_rc);
+	f = fopen(locked_rc.buf, "wx");
+	if (!f) {
+		/* Inform about the error unless the lockfile already existed,
+		 * since that only means we've got concurrent requests.
+		 */
+		result = errno;
+		if (result != EEXIST)
+			fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n",
+				locked_rc.buf, strerror(result), result);
+		goto out;
+	}
+	idx = cgit_repolist.count;
+	if (ctx.cfg.project_list)
+		scan_projects(path, ctx.cfg.project_list, repo_config);
+	else
+		scan_tree(path, repo_config);
+	print_repolist(f, &cgit_repolist, idx);
+	if (rename(locked_rc.buf, cached_rc))
+		fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n",
+			locked_rc.buf, cached_rc, strerror(errno), errno);
+	fclose(f);
+out:
+	strbuf_release(&locked_rc);
+	return result;
+}
+
+static void process_cached_repolist(const char *path)
+{
+	struct stat st;
+	struct strbuf cached_rc = STRBUF_INIT;
+	time_t age;
+	unsigned long hash;
+
+	hash = hash_str(path);
+	if (ctx.cfg.project_list)
+		hash += hash_str(ctx.cfg.project_list);
+	strbuf_addf(&cached_rc, "%s/rc-%8lx", ctx.cfg.cache_root, hash);
+
+	if (stat(cached_rc.buf, &st)) {
+		/* Nothing is cached, we need to scan without forking. And
+		 * if we fail to generate a cached repolist, we need to
+		 * invoke scan_tree manually.
+		 */
+		if (generate_cached_repolist(path, cached_rc.buf)) {
+			if (ctx.cfg.project_list)
+				scan_projects(path, ctx.cfg.project_list,
+					      repo_config);
+			else
+				scan_tree(path, repo_config);
+		}
+		goto out;
+	}
+
+	parse_configfile(cached_rc.buf, config_cb);
+
+	/* If the cached configfile hasn't expired, lets exit now */
+	age = time(NULL) - st.st_mtime;
+	if (age <= (ctx.cfg.cache_scanrc_ttl * 60))
+		goto out;
+
+	/* The cached repolist has been parsed, but it was old. So lets
+	 * rescan the specified path and generate a new cached repolist
+	 * in a child-process to avoid latency for the current request.
+	 */
+	if (fork())
+		goto out;
+
+	exit(generate_cached_repolist(path, cached_rc.buf));
+out:
+	strbuf_release(&cached_rc);
+}
+
+static void cgit_parse_args(int argc, const char **argv)
+{
+	int i;
+	const char *arg;
+	int scan = 0;
+
+	for (i = 1; i < argc; i++) {
+		if (!strcmp(argv[i], "--version")) {
+			printf("CGit-pink %s | https://git.causal.agency/cgit-pink/\n\nCompiled in features:\n", CGIT_VERSION);
+#ifndef HAVE_LINUX_SENDFILE
+			printf("[-] ");
+#else
+			printf("[+] ");
+#endif
+			printf("Linux sendfile() usage\n");
+
+			exit(0);
+		}
+		if (skip_prefix(argv[i], "--cache=", &arg)) {
+			ctx.cfg.cache_root = xstrdup(arg);
+		} else if (!strcmp(argv[i], "--nohttp")) {
+			ctx.env.no_http = "1";
+		} else if (skip_prefix(argv[i], "--query=", &arg)) {
+			ctx.qry.raw = xstrdup(arg);
+		} else if (skip_prefix(argv[i], "--repo=", &arg)) {
+			ctx.qry.repo = xstrdup(arg);
+		} else if (skip_prefix(argv[i], "--page=", &arg)) {
+			ctx.qry.page = xstrdup(arg);
+		} else if (skip_prefix(argv[i], "--head=", &arg)) {
+			ctx.qry.head = xstrdup(arg);
+			ctx.qry.has_symref = 1;
+		} else if (skip_prefix(argv[i], "--oid=", &arg)) {
+			ctx.qry.oid = xstrdup(arg);
+			ctx.qry.has_oid = 1;
+		} else if (skip_prefix(argv[i], "--ofs=", &arg)) {
+			ctx.qry.ofs = atoi(arg);
+		} else if (skip_prefix(argv[i], "--scan-tree=", &arg) ||
+		           skip_prefix(argv[i], "--scan-path=", &arg)) {
+			/*
+			 * HACK: The global snapshot bit mask defines the set
+			 * of allowed snapshot formats, but the config file
+			 * hasn't been parsed yet so the mask is currently 0.
+			 * By setting all bits high before scanning we make
+			 * sure that any in-repo cgitrc snapshot setting is
+			 * respected by scan_tree().
+			 *
+			 * NOTE: We assume that there aren't more than 8
+			 * different snapshot formats supported by cgit...
+			 */
+			ctx.cfg.snapshots = 0xFF;
+			scan++;
+			scan_tree(arg, repo_config);
+		}
+	}
+	if (scan) {
+		qsort(cgit_repolist.repos, cgit_repolist.count,
+			sizeof(struct cgit_repo), cmp_repos);
+		print_repolist(stdout, &cgit_repolist, 0);
+		exit(0);
+	}
+}
+
+static int calc_ttl(void)
+{
+	if (!ctx.repo)
+		return ctx.cfg.cache_root_ttl;
+
+	if (!ctx.qry.page)
+		return ctx.cfg.cache_repo_ttl;
+
+	if (!strcmp(ctx.qry.page, "about"))
+		return ctx.cfg.cache_about_ttl;
+
+	if (!strcmp(ctx.qry.page, "snapshot"))
+		return ctx.cfg.cache_snapshot_ttl;
+
+	if (ctx.qry.has_oid)
+		return ctx.cfg.cache_static_ttl;
+
+	if (ctx.qry.has_symref)
+		return ctx.cfg.cache_dynamic_ttl;
+
+	return ctx.cfg.cache_repo_ttl;
+}
+
+int cmd_main(int argc, const char **argv)
+{
+	const char *path;
+	int err, ttl;
+
+	atexit(cgit_cleanup_filters);
+
+	prepare_context();
+	cgit_repolist.length = 0;
+	cgit_repolist.count = 0;
+	cgit_repolist.repos = NULL;
+
+	cgit_parse_args(argc, argv);
+	parse_configfile(expand_macros(ctx.env.cgit_config), config_cb);
+	ctx.repo = NULL;
+	http_parse_querystring(ctx.qry.raw, querystring_cb);
+
+	/* If virtual-root isn't specified in cgitrc, lets pretend
+	 * that virtual-root equals SCRIPT_NAME, minus any possibly
+	 * trailing slashes.
+	 */
+	if (!ctx.cfg.virtual_root && ctx.cfg.script_name)
+		ctx.cfg.virtual_root = ensure_end(ctx.cfg.script_name, '/');
+
+	/* If no url parameter is specified on the querystring, lets
+	 * use PATH_INFO as url. This allows cgit to work with virtual
+	 * urls without the need for rewriterules in the webserver (as
+	 * long as PATH_INFO is included in the cache lookup key).
+	 */
+	path = ctx.env.path_info;
+	if (!ctx.qry.url && path) {
+		if (path[0] == '/')
+			path++;
+		ctx.qry.url = xstrdup(path);
+		if (ctx.qry.raw) {
+			char *newqry = fmtalloc("%s?%s", path, ctx.qry.raw);
+			free(ctx.qry.raw);
+			ctx.qry.raw = newqry;
+		} else
+			ctx.qry.raw = xstrdup(ctx.qry.url);
+		cgit_parse_url(ctx.qry.url);
+	}
+
+	/* Before we go any further, we set ctx.env.authenticated by checking to see
+	 * if the supplied cookie is valid. All cookies are valid if there is no
+	 * auth_filter. If there is an auth_filter, the filter decides. */
+	authenticate_cookie();
+
+	ttl = calc_ttl();
+	if (ttl < 0)
+		ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */
+	else
+		ctx.page.expires += ttl * 60;
+	if (!ctx.env.authenticated || (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")))
+		ctx.cfg.cache_size = 0;
+	err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
+			    ctx.qry.raw, ttl, process_request);
+	cgit_cleanup_filters();
+	if (err)
+		cgit_print_error("Error processing page: %s (%d)",
+				 strerror(err), err);
+	return err;
+}
diff --git a/third_party/cgit/cgit.css b/third_party/cgit/cgit.css
new file mode 100644
index 0000000000..51ddbf8337
--- /dev/null
+++ b/third_party/cgit/cgit.css
@@ -0,0 +1,881 @@
+div#cgit {
+	padding: 0em;
+	margin: 0em;
+	font-family: sans-serif;
+	font-size: 10pt;
+	color: #333;
+	background: white;
+	padding: 4px;
+}
+
+div#cgit a {
+	color: blue;
+	text-decoration: none;
+}
+
+div#cgit a:hover {
+	text-decoration: underline;
+}
+
+div#cgit table {
+	border-collapse: collapse;
+}
+
+div#cgit table#header {
+	width: 100%;
+	margin-bottom: 1em;
+}
+
+div#cgit table#header td.logo {
+	width: 96px;
+	vertical-align: top;
+}
+
+div#cgit table#header td.main {
+	font-size: 250%;
+	padding-left: 10px;
+	white-space: nowrap;
+}
+
+div#cgit table#header td.main a {
+	color: #000;
+}
+
+div#cgit table#header td.form {
+	text-align: right;
+	vertical-align: bottom;
+	padding-right: 1em;
+	padding-bottom: 2px;
+	white-space: nowrap;
+}
+
+div#cgit table#header td.form form,
+div#cgit table#header td.form input,
+div#cgit table#header td.form select {
+	font-size: 90%;
+}
+
+div#cgit table#header td.sub {
+	color: #777;
+	border-top: solid 1px #ccc;
+	padding-left: 10px;
+}
+
+div#cgit table.tabs {
+	border-bottom: solid 3px #ccc;
+	border-collapse: collapse;
+	margin-top: 2em;
+	margin-bottom: 0px;
+	width: 100%;
+}
+
+div#cgit table.tabs td {
+	padding: 0px 1em;
+	vertical-align: bottom;
+}
+
+div#cgit table.tabs td a {
+	padding: 2px 0.25em;
+	color: #777;
+	font-size: 110%;
+}
+
+div#cgit table.tabs td a.active {
+	color: #000;
+	background-color: #ccc;
+}
+
+div#cgit table.tabs a[href^="http://"]:after, div#cgit table.tabs a[href^="https://"]:after {
+	content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgAhcJDQY+gm2TAAAAHWlUWHRDb21tZW50AAAAAABDcmVhdGVkIHdpdGggR0lNUGQuZQcAAABbSURBVAhbY2BABs4MU4CwhYHBh2Erww4wrGFQZHjI8B8IgUIscJWyDHcggltQhI4zGDCcRwhChPggHIggP1QoAVmQkSETrGoHsiAEsACtBYN0oDAMbgU6EBcAAL2eHUt4XUU4AAAAAElFTkSuQmCC);
+	opacity: 0.5;
+	margin: 0 0 0 5px;
+}
+
+div#cgit table.tabs td.form {
+	text-align: right;
+}
+
+div#cgit table.tabs td.form form {
+	padding-bottom: 2px;
+	font-size: 90%;
+	white-space: nowrap;
+}
+
+div#cgit table.tabs td.form input,
+div#cgit table.tabs td.form select {
+	font-size: 90%;
+}
+
+div#cgit div.path {
+	margin: 0px;
+	padding: 5px 2em 2px 2em;
+	color: #000;
+	background-color: #eee;
+}
+
+div#cgit div.content {
+	margin: 0px;
+	padding: 2em;
+	border-bottom: solid 3px #ccc;
+}
+
+
+div#cgit table.list {
+	width: 100%;
+	border: none;
+	border-collapse: collapse;
+}
+
+div#cgit table.list tr {
+	background: white;
+}
+
+div#cgit table.list tr.logheader {
+	background: #eee;
+}
+
+div#cgit table.list tr:nth-child(even) {
+	background: #f7f7f7;
+}
+
+div#cgit table.list tr:nth-child(odd) {
+	background: white;
+}
+
+div#cgit table.list tr:hover {
+	background: #eee;
+}
+
+div#cgit table.list tr.nohover {
+	background: white;
+}
+
+div#cgit table.list tr.nohover:hover {
+	background: white;
+}
+
+div#cgit table.list tr.nohover-highlight:hover:nth-child(even) {
+	background: #f7f7f7;
+}
+
+div#cgit table.list tr.nohover-highlight:hover:nth-child(odd) {
+	background: white;
+}
+
+div#cgit table.list th {
+	font-weight: bold;
+	/* color: #888;
+	border-top: dashed 1px #888;
+	border-bottom: dashed 1px #888;
+	*/
+	padding: 0.1em 0.5em 0.05em 0.5em;
+	vertical-align: baseline;
+}
+
+div#cgit table.list td {
+	border: none;
+	padding: 0.1em 0.5em 0.1em 0.5em;
+}
+
+div#cgit table.list td.commitgraph {
+	font-family: monospace;
+	white-space: pre;
+}
+
+div#cgit table.list td.commitgraph .column1 {
+	color: #a00;
+}
+
+div#cgit table.list td.commitgraph .column2 {
+	color: #0a0;
+}
+
+div#cgit table.list td.commitgraph .column3 {
+	color: #aa0;
+}
+
+div#cgit table.list td.commitgraph .column4 {
+	color: #00a;
+}
+
+div#cgit table.list td.commitgraph .column5 {
+	color: #a0a;
+}
+
+div#cgit table.list td.commitgraph .column6 {
+	color: #0aa;
+}
+
+div#cgit table.list td.logsubject {
+	font-family: monospace;
+	font-weight: bold;
+}
+
+div#cgit table.list td.logmsg {
+	font-family: monospace;
+	white-space: pre;
+	padding: 0 0.5em;
+}
+
+div#cgit table.list td a {
+	color: black;
+}
+
+div#cgit table.list td a.ls-dir {
+	font-weight: bold;
+	color: #00f;
+}
+
+div#cgit table.list td a:hover {
+	color: #00f;
+}
+
+div#cgit img {
+	border: none;
+}
+
+div#cgit input#switch-btn {
+	margin: 2px 0px 0px 0px;
+}
+
+div#cgit td#sidebar input.txt {
+	width: 100%;
+	margin: 2px 0px 0px 0px;
+}
+
+div#cgit table#grid {
+	margin: 0px;
+}
+
+div#cgit td#content {
+	vertical-align: top;
+	padding: 1em 2em 1em 1em;
+	border: none;
+}
+
+div#cgit div#summary {
+	vertical-align: top;
+	margin-bottom: 1em;
+}
+
+div#cgit table#downloads {
+	float: right;
+	border-collapse: collapse;
+	border: solid 1px #777;
+	margin-left: 0.5em;
+	margin-bottom: 0.5em;
+}
+
+div#cgit table#downloads th {
+	background-color: #ccc;
+}
+
+div#cgit div#blob {
+	border: solid 1px black;
+}
+
+div#cgit div.error {
+	color: red;
+	font-weight: bold;
+	margin: 1em 2em;
+}
+
+div#cgit a.ls-blob, div#cgit a.ls-dir, div#cgit .ls-mod {
+	font-family: monospace;
+}
+
+div#cgit td.ls-size {
+	text-align: right;
+	font-family: monospace;
+	width: 10em;
+}
+
+div#cgit td.ls-mode {
+	font-family: monospace;
+	width: 10em;
+}
+
+div#cgit table.blob {
+	margin-top: 0.5em;
+	border-top: solid 1px black;
+}
+
+div#cgit table.blob td.hashes,
+div#cgit table.blob td.lines {
+	margin: 0; padding: 0 0 0 0.5em;
+	vertical-align: top;
+	color: black;
+}
+
+div#cgit table.blob td.linenumbers {
+	margin: 0; padding: 0 0.5em 0 0.5em;
+	vertical-align: top;
+	text-align: right;
+	border-right: 1px solid gray;
+}
+
+div#cgit table.blob pre {
+	padding: 0; margin: 0;
+}
+
+div#cgit table.blob td.linenumbers a,
+div#cgit table.ssdiff td.lineno a {
+	color: gray;
+	text-align: right;
+	text-decoration: none;
+}
+
+div#cgit table.blob td.linenumbers a:hover,
+div#cgit table.ssdiff td.lineno a:hover {
+	color: black;
+}
+
+div#cgit table.blame td.hashes,
+div#cgit table.blame td.lines,
+div#cgit table.blame td.linenumbers {
+	padding: 0;
+}
+
+div#cgit table.blame td.hashes div.alt,
+div#cgit table.blame td.lines div.alt {
+	padding: 0 0.5em 0 0.5em;
+}
+
+div#cgit table.blame td.linenumbers div.alt {
+	padding: 0 0.5em 0 0;
+}
+
+div#cgit table.blame div.alt:nth-child(even) {
+	background: #eee;
+}
+
+div#cgit table.blame div.alt:nth-child(odd) {
+	background: white;
+}
+
+div#cgit table.blame td.lines > div {
+	position: relative;
+}
+
+div#cgit table.blame td.lines > div > pre {
+	padding: 0 0 0 0.5em;
+	position: absolute;
+	top: 0;
+}
+
+div#cgit table.blame .oid {
+	font-size: 100%;
+}
+
+div#cgit table.bin-blob {
+	margin-top: 0.5em;
+	border: solid 1px black;
+}
+
+div#cgit table.bin-blob th {
+	font-family: monospace;
+	white-space: pre;
+	border: solid 1px #777;
+	padding: 0.5em 1em;
+}
+
+div#cgit table.bin-blob td {
+	font-family: monospace;
+	white-space: pre;
+	border-left: solid 1px #777;
+	padding: 0em 1em;
+}
+
+div#cgit table.nowrap td {
+	white-space: nowrap;
+}
+
+div#cgit table.commit-info {
+	border-collapse: collapse;
+	margin-top: 1.5em;
+}
+
+div#cgit div.cgit-panel {
+	float: right;
+	margin-top: 1.5em;
+}
+
+div#cgit div.cgit-panel table {
+	border-collapse: collapse;
+	border: solid 1px #aaa;
+	background-color: #eee;
+}
+
+div#cgit div.cgit-panel th {
+	text-align: center;
+}
+
+div#cgit div.cgit-panel td {
+	padding: 0.25em 0.5em;
+}
+
+div#cgit div.cgit-panel td.label {
+	padding-right: 0.5em;
+}
+
+div#cgit div.cgit-panel td.ctrl {
+	padding-left: 0.5em;
+}
+
+div#cgit table.commit-info th {
+	text-align: left;
+	font-weight: normal;
+	padding: 0.1em 1em 0.1em 0.1em;
+	vertical-align: top;
+}
+
+div#cgit table.commit-info td {
+	font-weight: normal;
+	padding: 0.1em 1em 0.1em 0.1em;
+}
+
+div#cgit div.commit-subject {
+	font-weight: bold;
+	font-size: 125%;
+	margin: 1.5em 0em 0.5em 0em;
+	padding: 0em;
+}
+
+div#cgit div.notes-header {
+	font-weight: bold;
+	padding-top: 1.5em;
+}
+
+div#cgit div.notes {
+	white-space: pre;
+	font-family: monospace;
+	border: solid 1px #ee9;
+	background-color: #ffd;
+	padding: 0.3em 2em 0.3em 1em;
+	float: left;
+}
+
+div#cgit div.notes-footer {
+	clear: left;
+}
+
+div#cgit div.diffstat-header {
+	font-weight: bold;
+	padding-top: 1.5em;
+}
+
+div#cgit table.diffstat {
+	border-collapse: collapse;
+	border: solid 1px #aaa;
+	background-color: #eee;
+}
+
+div#cgit table.diffstat th {
+	font-weight: normal;
+	text-align: left;
+	text-decoration: underline;
+	padding: 0.1em 1em 0.1em 0.1em;
+	font-size: 100%;
+}
+
+div#cgit table.diffstat td {
+	padding: 0.2em 0.2em 0.1em 0.1em;
+	font-size: 100%;
+	border: none;
+}
+
+div#cgit table.diffstat td.mode {
+	white-space: nowrap;
+}
+
+div#cgit table.diffstat td span.modechange {
+	padding-left: 1em;
+	color: red;
+}
+
+div#cgit table.diffstat td.add a {
+	color: green;
+}
+
+div#cgit table.diffstat td.del a {
+	color: red;
+}
+
+div#cgit table.diffstat td.upd a {
+	color: blue;
+}
+
+div#cgit table.diffstat td.graph {
+	width: 500px;
+	vertical-align: middle;
+}
+
+div#cgit table.diffstat td.graph table {
+	border: none;
+}
+
+div#cgit table.diffstat td.graph td {
+	padding: 0px;
+	border: 0px;
+	height: 7pt;
+}
+
+div#cgit table.diffstat td.graph td.add {
+	background-color: #5c5;
+}
+
+div#cgit table.diffstat td.graph td.rem {
+	background-color: #c55;
+}
+
+div#cgit div.diffstat-summary {
+	color: #888;
+	padding-top: 0.5em;
+}
+
+div#cgit table.diff {
+	width: 100%;
+}
+
+div#cgit table.diff td span.head {
+	font-weight: bold;
+	color: black;
+}
+
+div#cgit table.diff td span.hunk {
+	color: #009;
+}
+
+div#cgit table.diff td span.add {
+	color: green;
+}
+
+div#cgit table.diff td span.del {
+	color: red;
+}
+
+div#cgit .oid {
+	font-family: monospace;
+	font-size: 90%;
+}
+
+div#cgit .left {
+	text-align: left;
+}
+
+div#cgit .right {
+	text-align: right;
+}
+
+div#cgit table.list td.reposection {
+	font-style: italic;
+	color: #888;
+}
+
+div#cgit a.button {
+	font-size: 80%;
+}
+
+div#cgit a.primary {
+	font-size: 100%;
+}
+
+div#cgit a.secondary {
+	font-size: 90%;
+}
+
+div#cgit td.toplevel-repo {
+
+}
+
+div#cgit table.list td.sublevel-repo {
+	padding-left: 1.5em;
+}
+
+div#cgit ul.pager {
+	list-style-type: none;
+	text-align: center;
+	margin: 1em 0em 0em 0em;
+	padding: 0;
+}
+
+div#cgit ul.pager li {
+	display: inline-block;
+	margin: 0.25em 0.5em;
+}
+
+div#cgit ul.pager a {
+	color: #777;
+}
+
+div#cgit ul.pager .current {
+	font-weight: bold;
+}
+
+div#cgit span.age-mins {
+	font-weight: bold;
+	color: #080;
+}
+
+div#cgit span.age-hours {
+	color: #080;
+}
+
+div#cgit span.age-days {
+	color: #040;
+}
+
+div#cgit span.age-weeks {
+	color: #444;
+}
+
+div#cgit span.age-months {
+	color: #888;
+}
+
+div#cgit span.age-years {
+	color: #bbb;
+}
+
+div#cgit span.insertions {
+	color: #080;
+}
+
+div#cgit span.deletions {
+	color: #800;
+}
+
+div#cgit div.footer {
+	margin-top: 0.5em;
+	text-align: center;
+	font-size: 80%;
+	color: #ccc;
+}
+
+div#cgit div.footer a {
+	color: #ccc;
+	text-decoration: none;
+}
+
+div#cgit div.footer a:hover {
+	text-decoration: underline;
+}
+
+div#cgit a.branch-deco {
+	color: #000;
+	padding: 0px 0.25em;
+	background-color: #88ff88;
+	border: solid 1px #007700;
+}
+
+div#cgit a.tag-deco {
+	color: #000;
+	padding: 0px 0.25em;
+	background-color: #ffff88;
+	border: solid 1px #777700;
+}
+
+div#cgit a.tag-annotated-deco {
+	color: #000;
+	padding: 0px 0.25em;
+	background-color: #ffcc88;
+	border: solid 1px #777700;
+}
+
+div#cgit a.remote-deco {
+	color: #000;
+	padding: 0px 0.25em;
+	background-color: #ccccff;
+	border: solid 1px #000077;
+}
+
+div#cgit a.deco {
+	color: #000;
+	padding: 0px 0.25em;
+	background-color: #ff8888;
+	border: solid 1px #770000;
+}
+
+div#cgit div.commit-subject a.branch-deco,
+div#cgit div.commit-subject a.tag-deco,
+div#cgit div.commit-subject a.tag-annotated-deco,
+div#cgit div.commit-subject a.remote-deco,
+div#cgit div.commit-subject a.deco {
+	font-size: 75%;
+}
+
+div#cgit table.stats {
+	border: solid 1px black;
+	border-collapse: collapse;
+}
+
+div#cgit table.stats th {
+	text-align: left;
+	padding: 1px 0.5em;
+	background-color: #eee;
+	border: solid 1px black;
+}
+
+div#cgit table.stats td {
+	text-align: right;
+	padding: 1px 0.5em;
+	border: solid 1px black;
+}
+
+div#cgit table.stats td.total {
+	font-weight: bold;
+	text-align: left;
+}
+
+div#cgit table.stats td.sum {
+	color: #c00;
+	font-weight: bold;
+/*	background-color: #eee; */
+}
+
+div#cgit table.stats td.left {
+	text-align: left;
+}
+
+div#cgit table.vgraph {
+	border-collapse: separate;
+	border: solid 1px black;
+	height: 200px;
+}
+
+div#cgit table.vgraph th {
+	background-color: #eee;
+	font-weight: bold;
+	border: solid 1px white;
+	padding: 1px 0.5em;
+}
+
+div#cgit table.vgraph td {
+	vertical-align: bottom;
+	padding: 0px 10px;
+}
+
+div#cgit table.vgraph div.bar {
+	background-color: #eee;
+}
+
+div#cgit table.hgraph {
+	border: solid 1px black;
+	width: 800px;
+}
+
+div#cgit table.hgraph th {
+	background-color: #eee;
+	font-weight: bold;
+	border: solid 1px black;
+	padding: 1px 0.5em;
+}
+
+div#cgit table.hgraph td {
+	vertical-align: middle;
+	padding: 2px 2px;
+}
+
+div#cgit table.hgraph div.bar {
+	background-color: #eee;
+	height: 1em;
+}
+
+div#cgit table.ssdiff {
+	width: 100%;
+}
+
+div#cgit table.ssdiff td {
+	font-size: 75%;
+	font-family: monospace;
+	white-space: pre;
+	padding: 1px 4px 1px 4px;
+	border-left: solid 1px #aaa;
+	border-right: solid 1px #aaa;
+}
+
+div#cgit table.ssdiff td.add {
+	color: black;
+	background: #cfc;
+	min-width: 50%;
+}
+
+div#cgit table.ssdiff td.add_dark {
+	color: black;
+	background: #aca;
+	min-width: 50%;
+}
+
+div#cgit table.ssdiff span.add {
+	background: #cfc;
+	font-weight: bold;
+}
+
+div#cgit table.ssdiff td.del {
+	color: black;
+	background: #fcc;
+	min-width: 50%;
+}
+
+div#cgit table.ssdiff td.del_dark {
+	color: black;
+	background: #caa;
+	min-width: 50%;
+}
+
+div#cgit table.ssdiff span.del {
+	background: #fcc;
+	font-weight: bold;
+}
+
+div#cgit table.ssdiff td.changed {
+	color: black;
+	background: #ffc;
+	min-width: 50%;
+}
+
+div#cgit table.ssdiff td.changed_dark {
+	color: black;
+	background: #cca;
+	min-width: 50%;
+}
+
+div#cgit table.ssdiff td.lineno {
+	color: black;
+	background: #eee;
+	text-align: right;
+	width: 3em;
+	min-width: 3em;
+}
+
+div#cgit table.ssdiff td.hunk {
+	color: black;
+	background: #ccf;
+	border-top: solid 1px #aaa;
+	border-bottom: solid 1px #aaa;
+}
+
+div#cgit table.ssdiff td.head {
+	border-top: solid 1px #aaa;
+	border-bottom: solid 1px #aaa;
+}
+
+div#cgit table.ssdiff td.head div.head {
+	font-weight: bold;
+	color: black;
+}
+
+div#cgit table.ssdiff td.foot {
+	border-top: solid 1px #aaa;
+	border-left: none;
+	border-right: none;
+	border-bottom: none;
+}
+
+div#cgit table.ssdiff td.space {
+	border: none;
+}
+
+div#cgit table.ssdiff td.space div {
+	min-height: 3em;
+}
diff --git a/third_party/cgit/cgit.h b/third_party/cgit/cgit.h
new file mode 100644
index 0000000000..72fcd8498a
--- /dev/null
+++ b/third_party/cgit/cgit.h
@@ -0,0 +1,397 @@
+#ifndef CGIT_H
+#define CGIT_H
+
+
+#include <git-compat-util.h>
+#include <stdbool.h>
+
+#include <cache.h>
+#include <grep.h>
+#include <object.h>
+#include <object-store.h>
+#include <tree.h>
+#include <commit.h>
+#include <tag.h>
+#include <diff.h>
+#include <diffcore.h>
+#include <strvec.h>
+#include <refs.h>
+#include <revision.h>
+#include <log-tree.h>
+#include <archive.h>
+#include <string-list.h>
+#include <xdiff-interface.h>
+#include <xdiff/xdiff.h>
+#include <utf8.h>
+#include <notes.h>
+#include <graph.h>
+
+/* Add isgraph(x) to Git's sane ctype support (see git-compat-util.h) */
+#undef isgraph
+#define isgraph(x) (isprint((x)) && !isspace((x)))
+
+
+/*
+ * Limits used for relative dates
+ */
+#define TM_MIN    60
+#define TM_HOUR  (TM_MIN * 60)
+#define TM_DAY   (TM_HOUR * 24)
+#define TM_WEEK  (TM_DAY * 7)
+#define TM_YEAR  (TM_DAY * 365)
+#define TM_MONTH (TM_YEAR / 12.0)
+
+
+/*
+ * Default encoding
+ */
+#define PAGE_ENCODING "UTF-8"
+
+#define BIT(x)	(1U << (x))
+
+typedef void (*configfn)(const char *name, const char *value);
+typedef void (*filepair_fn)(struct diff_filepair *pair);
+typedef void (*linediff_fn)(char *line, int len);
+
+typedef enum {
+	DIFF_UNIFIED, DIFF_SSDIFF, DIFF_STATONLY
+} diff_type;
+
+typedef enum {
+	ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER
+} filter_type;
+
+struct cgit_filter {
+	int (*open)(struct cgit_filter *, va_list ap);
+	int (*close)(struct cgit_filter *);
+	void (*fprintf)(struct cgit_filter *, FILE *, const char *prefix);
+	void (*cleanup)(struct cgit_filter *);
+	int argument_count;
+};
+
+struct cgit_exec_filter {
+	struct cgit_filter base;
+	char *cmd;
+	char **argv;
+	int old_stdout;
+	int pid;
+};
+
+struct cgit_repo {
+	char *url;
+	char *name;
+	char *path;
+	char *desc;
+	char *extra_head_content;
+	char *owner;
+	char *homepage;
+	char *defbranch;
+	char *module_link;
+	struct string_list readme;
+	char *section;
+	char *clone_url;
+	char *logo;
+	char *logo_link;
+	char *snapshot_prefix;
+	int snapshots;
+	int enable_blame;
+	int enable_commit_graph;
+	int enable_log_filecount;
+	int enable_log_linecount;
+	int enable_remote_branches;
+	int enable_subject_links;
+	int enable_html_serving;
+	int max_stats;
+	int branch_sort;
+	int commit_sort;
+	time_t mtime;
+	struct cgit_filter *about_filter;
+	struct cgit_filter *commit_filter;
+	struct cgit_filter *source_filter;
+	struct cgit_filter *email_filter;
+	struct cgit_filter *owner_filter;
+	struct string_list submodules;
+	int hide;
+	int ignore;
+};
+
+typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name,
+	      const char *value);
+
+struct cgit_repolist {
+	int length;
+	int count;
+	struct cgit_repo *repos;
+};
+
+struct commitinfo {
+	struct commit *commit;
+	char *author;
+	char *author_email;
+	unsigned long author_date;
+	int author_tz;
+	char *committer;
+	char *committer_email;
+	unsigned long committer_date;
+	int committer_tz;
+	char *subject;
+	char *msg;
+	char *msg_encoding;
+};
+
+struct taginfo {
+	char *tagger;
+	char *tagger_email;
+	unsigned long tagger_date;
+	int tagger_tz;
+	char *msg;
+};
+
+struct refinfo {
+	const char *refname;
+	struct object *object;
+	union {
+		struct taginfo *tag;
+		struct commitinfo *commit;
+	};
+};
+
+struct reflist {
+	struct refinfo **refs;
+	int alloc;
+	int count;
+};
+
+struct cgit_query {
+	int has_symref;
+	int has_oid;
+	int has_difftype;
+	char *raw;
+	char *repo;
+	char *page;
+	char *search;
+	char *grep;
+	char *head;
+	char *oid;
+	char *oid2;
+	char *path;
+	char *name;
+	char *url;
+	char *period;
+	int   ofs;
+	int nohead;
+	char *sort;
+	int showmsg;
+	diff_type difftype;
+	int show_all;
+	int context;
+	int ignorews;
+	int follow;
+	char *vpath;
+};
+
+struct cgit_config {
+	char *agefile;
+	char *cache_root;
+	char *clone_prefix;
+	char *clone_url;
+	char *css;
+	char *favicon;
+	char *footer;
+	char *head_include;
+	char *header;
+	char *logo;
+	char *logo_link;
+	char *mimetype_file;
+	char *module_link;
+	char *project_list;
+	struct string_list readme;
+	char *robots;
+	char *root_title;
+	char *root_desc;
+	char *root_readme;
+	char *script_name;
+	char *section;
+	char *repository_sort;
+	char *virtual_root;	/* Always ends with '/'. */
+	char *strict_export;
+	int cache_size;
+	int cache_dynamic_ttl;
+	int cache_max_create_time;
+	int cache_repo_ttl;
+	int cache_root_ttl;
+	int cache_scanrc_ttl;
+	int cache_static_ttl;
+	int cache_about_ttl;
+	int cache_snapshot_ttl;
+	int case_sensitive_sort;
+	int embedded;
+	int enable_filter_overrides;
+	int enable_follow_links;
+	int enable_http_clone;
+	int enable_index_links;
+	int enable_index_owner;
+	int enable_blame;
+	int enable_commit_graph;
+	int enable_log_filecount;
+	int enable_log_linecount;
+	int enable_remote_branches;
+	int enable_subject_links;
+	int enable_html_serving;
+	int enable_tree_linenumbers;
+	int enable_git_config;
+	int local_time;
+	int max_atom_items;
+	int max_repo_count;
+	int max_commit_count;
+	int max_lock_attempts;
+	int max_msg_len;
+	int max_repodesc_len;
+	int max_blob_size;
+	int max_stats;
+	int noplainemail;
+	int noheader;
+	int renamelimit;
+	int remove_suffix;
+	int scan_hidden_path;
+	int section_from_path;
+	int snapshots;
+	int section_sort;
+	int summary_branches;
+	int summary_log;
+	int summary_tags;
+	diff_type difftype;
+	int branch_sort;
+	int commit_sort;
+	struct string_list mimetypes;
+	struct cgit_filter *about_filter;
+	struct cgit_filter *commit_filter;
+	struct cgit_filter *source_filter;
+	struct cgit_filter *email_filter;
+	struct cgit_filter *owner_filter;
+	struct cgit_filter *auth_filter;
+};
+
+struct cgit_page {
+	time_t modified;
+	time_t expires;
+	size_t size;
+	const char *mimetype;
+	const char *charset;
+	const char *filename;
+	const char *etag;
+	const char *title;
+	int status;
+	const char *statusmsg;
+};
+
+struct cgit_environment {
+	const char *cgit_config;
+	const char *http_host;
+	const char *https;
+	const char *no_http;
+	const char *path_info;
+	const char *query_string;
+	const char *request_method;
+	const char *script_name;
+	const char *server_name;
+	const char *server_port;
+	const char *http_cookie;
+	const char *http_referer;
+	unsigned int content_length;
+	int authenticated;
+};
+
+struct cgit_context {
+	struct cgit_environment env;
+	struct cgit_query qry;
+	struct cgit_config cfg;
+	struct cgit_repo *repo;
+	struct cgit_page page;
+};
+
+typedef int (*write_archive_fn_t)(const char *, const char *);
+
+struct cgit_snapshot_format {
+	const char *suffix;
+	const char *mimetype;
+	write_archive_fn_t write_func;
+};
+
+extern const char *cgit_version;
+
+extern struct cgit_repolist cgit_repolist;
+extern struct cgit_context ctx;
+extern const struct cgit_snapshot_format cgit_snapshot_formats[];
+
+extern char *cgit_default_repo_desc;
+extern struct cgit_repo *cgit_add_repo(const char *url);
+extern struct cgit_repo *cgit_get_repoinfo(const char *url);
+extern void cgit_repo_config_cb(const char *name, const char *value);
+
+extern int chk_zero(int result, char *msg);
+extern int chk_positive(int result, char *msg);
+extern int chk_non_negative(int result, char *msg);
+
+extern char *trim_end(const char *str, char c);
+extern char *ensure_end(const char *str, char c);
+
+extern void strbuf_ensure_end(struct strbuf *sb, char c);
+
+extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
+extern void cgit_free_reflist_inner(struct reflist *list);
+extern int cgit_refs_cb(const char *refname, const struct object_id *oid,
+			int flags, void *cb_data);
+
+extern void cgit_free_commitinfo(struct commitinfo *info);
+extern void cgit_free_taginfo(struct taginfo *info);
+
+void cgit_diff_tree_cb(struct diff_queue_struct *q,
+		       struct diff_options *options, void *data);
+
+extern int cgit_diff_files(const struct object_id *old_oid,
+			   const struct object_id *new_oid,
+			   unsigned long *old_size, unsigned long *new_size,
+			   int *binary, int context, int ignorews,
+			   linediff_fn fn);
+
+extern void cgit_diff_tree(const struct object_id *old_oid,
+			   const struct object_id *new_oid,
+			   filepair_fn fn, const char *prefix, int ignorews);
+
+extern void cgit_diff_commit(struct commit *commit, filepair_fn fn,
+			     const char *prefix);
+
+__attribute__((format (printf,1,2)))
+extern char *fmt(const char *format,...);
+
+__attribute__((format (printf,1,2)))
+extern char *fmtalloc(const char *format,...);
+
+extern struct commitinfo *cgit_parse_commit(struct commit *commit);
+extern struct taginfo *cgit_parse_tag(struct tag *tag);
+extern void cgit_parse_url(const char *url);
+
+extern const char *cgit_repobasename(const char *reponame);
+
+extern int cgit_parse_snapshots_mask(const char *str);
+extern const struct object_id *cgit_snapshot_get_sig(const char *ref,
+						     const struct cgit_snapshot_format *f);
+extern const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f);
+
+extern int cgit_open_filter(struct cgit_filter *filter, ...);
+extern int cgit_close_filter(struct cgit_filter *filter);
+extern void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix);
+extern void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv);
+extern struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype);
+extern void cgit_cleanup_filters(void);
+
+extern void cgit_prepare_repo_env(struct cgit_repo * repo);
+
+extern int readfile(const char *path, char **buf, size_t *size);
+
+extern char *expand_macros(const char *txt);
+
+extern char *get_mimetype_for_filename(const char *filename);
+
+#endif /* CGIT_H */
diff --git a/third_party/cgit/cgit.mk b/third_party/cgit/cgit.mk
new file mode 100644
index 0000000000..5b9ed5be8e
--- /dev/null
+++ b/third_party/cgit/cgit.mk
@@ -0,0 +1,114 @@
+# This Makefile is run in the "git" directory in order to re-use Git's
+# build variables and operating system detection.  Hence all files in
+# CGit's directory must be prefixed with "../".
+include Makefile
+
+CGIT_PREFIX = ../
+
+-include $(CGIT_PREFIX)cgit.conf
+
+# The CGIT_* variables are inherited when this file is called from the
+# main Makefile - they are defined there.
+
+$(CGIT_PREFIX)VERSION: force-version
+	@cd $(CGIT_PREFIX) && '$(SHELL_PATH_SQ)' ./gen-version.sh "$(CGIT_VERSION)"
+-include $(CGIT_PREFIX)VERSION
+.PHONY: force-version
+
+# CGIT_CFLAGS is a separate variable so that we can track it separately
+# and avoid rebuilding all of Git when these variables change.
+CGIT_CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
+CGIT_CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
+CGIT_CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
+
+PKG_CONFIG ?= pkg-config
+
+ifdef NO_C99_FORMAT
+	CFLAGS += -DNO_C99_FORMAT
+endif
+
+# Add -ldl to linker flags on systems that commonly use GNU libc.
+ifneq (,$(filter $(uname_S),Linux GNU GNU/kFreeBSD))
+	CGIT_LIBS += -ldl
+endif
+
+# glibc 2.1+ offers sendfile which the most common C library on Linux
+ifeq ($(uname_S),Linux)
+	HAVE_LINUX_SENDFILE = YesPlease
+endif
+
+ifdef HAVE_LINUX_SENDFILE
+	CGIT_CFLAGS += -DHAVE_LINUX_SENDFILE
+endif
+
+CGIT_OBJ_NAMES += cgit.o
+CGIT_OBJ_NAMES += cache.o
+CGIT_OBJ_NAMES += cmd.o
+CGIT_OBJ_NAMES += configfile.o
+CGIT_OBJ_NAMES += filter.o
+CGIT_OBJ_NAMES += html.o
+CGIT_OBJ_NAMES += parsing.o
+CGIT_OBJ_NAMES += scan-tree.o
+CGIT_OBJ_NAMES += shared.o
+CGIT_OBJ_NAMES += ui-atom.o
+CGIT_OBJ_NAMES += ui-blame.o
+CGIT_OBJ_NAMES += ui-blob.o
+CGIT_OBJ_NAMES += ui-clone.o
+CGIT_OBJ_NAMES += ui-commit.o
+CGIT_OBJ_NAMES += ui-diff.o
+CGIT_OBJ_NAMES += ui-log.o
+CGIT_OBJ_NAMES += ui-patch.o
+CGIT_OBJ_NAMES += ui-plain.o
+CGIT_OBJ_NAMES += ui-refs.o
+CGIT_OBJ_NAMES += ui-repolist.o
+CGIT_OBJ_NAMES += ui-shared.o
+CGIT_OBJ_NAMES += ui-snapshot.o
+CGIT_OBJ_NAMES += ui-ssdiff.o
+CGIT_OBJ_NAMES += ui-stats.o
+CGIT_OBJ_NAMES += ui-summary.o
+CGIT_OBJ_NAMES += ui-tag.o
+CGIT_OBJ_NAMES += ui-tree.o
+
+CGIT_OBJS := $(addprefix $(CGIT_PREFIX),$(CGIT_OBJ_NAMES))
+
+# Only cgit.c reference CGIT_VERSION so we only rebuild its objects when the
+# version changes.
+CGIT_VERSION_OBJS := $(addprefix $(CGIT_PREFIX),cgit.o cgit.sp)
+$(CGIT_VERSION_OBJS): $(CGIT_PREFIX)VERSION
+$(CGIT_VERSION_OBJS): EXTRA_CPPFLAGS = \
+	-DCGIT_VERSION='"$(CGIT_VERSION)"'
+
+# Git handles dependencies using ":=" so dependencies in CGIT_OBJ are not
+# handled by that and we must handle them ourselves.
+cgit_dep_files := $(foreach f,$(CGIT_OBJS),$(dir $f).depend/$(notdir $f).d)
+cgit_dep_files_present := $(wildcard $(cgit_dep_files))
+ifneq ($(cgit_dep_files_present),)
+include $(cgit_dep_files_present)
+endif
+
+ifeq ($(wildcard $(CGIT_PREFIX).depend),)
+missing_dep_dirs += $(CGIT_PREFIX).depend
+endif
+
+$(CGIT_PREFIX).depend:
+	@mkdir -p $@
+
+$(CGIT_PREFIX)CGIT-CFLAGS: FORCE
+	@FLAGS='$(subst ','\'',$(CGIT_CFLAGS))'; \
+	    if test x"$$FLAGS" != x"`cat ../CGIT-CFLAGS 2>/dev/null`" ; then \
+		echo 1>&2 "    * new CGit build flags"; \
+		echo "$$FLAGS" >$(CGIT_PREFIX)CGIT-CFLAGS; \
+            fi
+
+$(CGIT_OBJS): %.o: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS $(missing_dep_dirs)
+	$(QUIET_CC)$(CC) -o $*.o -c $(dep_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $<
+
+$(CGIT_PREFIX)cgit: $(CGIT_OBJS) GIT-LDFLAGS $(GITLIBS)
+	$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) $(CGIT_LIBS)
+
+CGIT_SP_OBJS := $(patsubst %.o,%.sp,$(CGIT_OBJS))
+
+$(CGIT_SP_OBJS): %.sp: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS FORCE
+	$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $(SPARSE_FLAGS) $<
+
+cgit-sparse: $(CGIT_SP_OBJS)
diff --git a/third_party/cgit/cgit.png b/third_party/cgit/cgit.png
new file mode 100644
index 0000000000..425528ee39
--- /dev/null
+++ b/third_party/cgit/cgit.png
Binary files differdiff --git a/third_party/cgit/cgitrc.5.txt b/third_party/cgit/cgitrc.5.txt
new file mode 100644
index 0000000000..cafb5355ea
--- /dev/null
+++ b/third_party/cgit/cgitrc.5.txt
@@ -0,0 +1,977 @@
+:man source:   cgit
+:man manual:   cgit
+
+CGITRC(5)
+========
+
+
+NAME
+----
+cgitrc - runtime configuration for cgit
+
+
+SYNOPSIS
+--------
+Cgitrc contains all runtime settings for cgit, including the list of git
+repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank
+lines, and lines starting with '#', are ignored.
+
+
+LOCATION
+--------
+The default location of cgitrc, defined at compile time, is /etc/cgitrc. At
+runtime, cgit will consult the environment variable CGIT_CONFIG and, if
+defined, use its value instead.
+
+
+GLOBAL SETTINGS
+---------------
+about-filter::
+	Specifies a command which will be invoked to format the content of
+	about pages (both top-level and for each repository). The command will
+	get the content of the about-file on its STDIN, the name of the file
+	as the first argument, and the STDOUT from the command will be
+	included verbatim on the about page. Default value: none. See
+	also: "FILTER API".
+
+agefile::
+	Specifies a path, relative to each repository path, which can be used
+	to specify the date and time of the youngest commit in the repository.
+	The first line in the file is used as input to the "parse_date"
+	function in libgit. Recommended timestamp-format is "yyyy-mm-dd
+	hh:mm:ss". You may want to generate this file from a post-receive
+	hook. Default value: "info/web/last-modified".
+
+auth-filter::
+	Specifies a command that will be invoked for authenticating repository
+	access. Receives quite a few arguments, and data on both stdin and
+	stdout for authentication processing. Details follow later in this
+	document. If no auth-filter is specified, no authentication is
+	performed. Default value: none. See also: "FILTER API".
+
+branch-sort::
+	Flag which, when set to "age", enables date ordering in the branch ref
+	list, and when set to "name" enables ordering by branch name. Default
+	value: "name".
+
+cache-about-ttl::
+	Number which specifies the time-to-live, in minutes, for the cached
+	version of the repository about page. See also: "CACHE". Default
+	value: "15".
+
+cache-dynamic-ttl::
+	Number which specifies the time-to-live, in minutes, for the cached
+	version of repository pages accessed without a fixed SHA1. See also:
+	"CACHE". Default value: "5".
+
+cache-repo-ttl::
+	Number which specifies the time-to-live, in minutes, for the cached
+	version of the repository summary page. See also: "CACHE". Default
+	value: "5".
+
+cache-root::
+	Path used to store the cgit cache entries. Default value:
+	"/var/cache/cgit". See also: "MACRO EXPANSION".
+
+cache-root-ttl::
+	Number which specifies the time-to-live, in minutes, for the cached
+	version of the repository index page. See also: "CACHE". Default
+	value: "5".
+
+cache-scanrc-ttl::
+	Number which specifies the time-to-live, in minutes, for the result
+	of scanning a path for git repositories. See also: "CACHE". Default
+	value: "15".
+
+case-sensitive-sort::
+	Sort items in the repo list case sensitively. Default value: "1".
+	See also: repository-sort, section-sort.
+
+cache-size::
+	The maximum number of entries in the cgit cache. When set to "0",
+	caching is disabled. See also: "CACHE". Default value: "0"
+
+cache-snapshot-ttl::
+	Number which specifies the time-to-live, in minutes, for the cached
+	version of snapshots. See also: "CACHE". Default value: "5".
+
+cache-static-ttl::
+	Number which specifies the time-to-live, in minutes, for the cached
+	version of repository pages accessed with a fixed SHA1. See also:
+	"CACHE". Default value: -1".
+
+clone-prefix::
+	Space-separated list of common prefixes which, when combined with a
+	repository url, generates valid clone urls for the repository. This
+	setting is only used if `repo.clone-url` is unspecified. Default value:
+	none.
+
+clone-url::
+	Space-separated list of clone-url templates. This setting is only
+	used if `repo.clone-url` is unspecified. Default value: none. See
+	also: "MACRO EXPANSION", "FILTER API".
+
+commit-filter::
+	Specifies a command which will be invoked to format commit messages.
+	The command will get the message on its STDIN, and the STDOUT from the
+	command will be included verbatim as the commit message, i.e. this can
+	be used to implement bugtracker integration. Default value: none.
+	See also: "FILTER API".
+
+commit-sort::
+	Flag which, when set to "date", enables strict date ordering in the
+	commit log, and when set to "topo" enables strict topological
+	ordering. If unset, the default ordering of "git log" is used. Default
+	value: unset.
+
+css::
+	Url which specifies the css document to include in all cgit pages.
+	Default value: "/cgit.css".
+
+email-filter::
+	Specifies a command which will be invoked to format names and email
+	address of committers, authors, and taggers, as represented in various
+	places throughout the cgit interface. This command will receive an
+	email address and an origin page string as its command line arguments,
+	and the text to format on STDIN. It is to write the formatted text back
+	out onto STDOUT. Default value: none. See also: "FILTER API".
+
+embedded::
+	Flag which, when set to "1", will make cgit generate a html fragment
+	suitable for embedding in other html pages. Default value: none. See
+	also: "noheader".
+
+enable-blame::
+	Flag which, when set to "1", will allow cgit to provide a "blame" page
+	for files, and will make it generate links to that page in appropriate
+	places. Default value: "0".
+
+enable-commit-graph::
+	Flag which, when set to "1", will make cgit print an ASCII-art commit
+	history graph to the left of the commit messages in the repository
+	log page. Default value: "0".
+
+enable-filter-overrides::
+	Flag which, when set to "1", allows all filter settings to be
+	overridden in repository-specific cgitrc files. Default value: none.
+
+enable-follow-links::
+	Flag which, when set to "1", allows users to follow a file in the log
+	view.  Default value: "0".
+
+enable-git-config::
+	Flag which, when set to "1", will allow cgit to use git config to set
+	any repo specific settings. This option is used in conjunction with
+	"scan-path", and must be defined prior, to augment repo-specific
+	settings. The keys gitweb.owner, gitweb.category, gitweb.description,
+	and gitweb.homepage will map to the cgit keys repo.owner, repo.section,
+	repo.desc, and repo.homepage respectively. All git config keys that begin
+	with "cgit." will be mapped to the corresponding "repo." key in cgit.
+	Default value: "0". See also: scan-path, section-from-path.
+
+enable-http-clone::
+	If set to "1", cgit will act as a dumb HTTP endpoint for git clones.
+	You can add "http://$HTTP_HOST$SCRIPT_NAME/$CGIT_REPO_URL" to clone-url
+	to expose this feature. If you use an alternate way of serving git
+	repositories, you may wish to disable this. Default value: "1".
+
+enable-html-serving::
+	Flag which, when set to "1", will allow the /plain handler to serve
+	mimetype headers that result in the file being treated as HTML by the
+	browser. When set to "0", such file types are returned instead as
+	text/plain or application/octet-stream. Default value: "0". See also:
+	"repo.enable-html-serving".
+
+enable-index-links::
+	Flag which, when set to "1", will make cgit generate extra links for
+	each repo in the repository index (specifically, to the "summary",
+	"commit" and "tree" pages). Default value: "0".
+
+enable-index-owner::
+	Flag which, when set to "1", will make cgit display the owner of
+	each repo in the repository index. Default value: "1".
+
+enable-log-filecount::
+	Flag which, when set to "1", will make cgit print the number of
+	modified files for each commit on the repository log page. Default
+	value: "0".
+
+enable-log-linecount::
+	Flag which, when set to "1", will make cgit print the number of added
+	and removed lines for each commit on the repository log page. Default
+	value: "0".
+
+enable-remote-branches::
+	Flag which, when set to "1", will make cgit display remote branches
+	in the summary and refs views. Default value: "0". See also:
+	"repo.enable-remote-branches".
+
+enable-subject-links::
+	Flag which, when set to "1", will make cgit use the subject of the
+	parent commit as link text when generating links to parent commits
+	in commit view. Default value: "0". See also:
+	"repo.enable-subject-links".
+
+enable-tree-linenumbers::
+	Flag which, when set to "1", will make cgit generate linenumber links
+	for plaintext blobs printed in the tree view. Default value: "1".
+
+favicon::
+	Url used as link to a shortcut icon for cgit. It is suggested to use
+	the value "/favicon.ico" since certain browsers will ignore other
+	values. Default value: none.
+
+footer::
+	The content of the file specified with this option will be included
+	verbatim at the bottom of all pages (i.e. it replaces the standard
+	"generated by..." message. Default value: none.
+
+head-include::
+	The content of the file specified with this option will be included
+	verbatim in the html HEAD section on all pages. Default value: none.
+
+header::
+	The content of the file specified with this option will be included
+	verbatim at the top of all pages. Default value: none.
+
+include::
+	Name of a configfile to include before the rest of the current config-
+	file is parsed. Default value: none. See also: "MACRO EXPANSION".
+
+local-time::
+	Flag which, if set to "1", makes cgit print commit and tag times in the
+	servers timezone. Default value: "0".
+
+logo::
+	Url which specifies the source of an image which will be used as a logo
+	on all cgit pages. Default value: "/cgit.png".
+
+logo-link::
+	Url loaded when clicking on the cgit logo image. If unspecified the
+	calculated url of the repository index page will be used. Default
+	value: none.
+
+max-atom-items::
+	Specifies the number of items to display in atom feeds view. Default
+	value: "10".
+
+max-blob-size::
+	Specifies the maximum size of a blob to display HTML for in KBytes.
+	Default value: "0" (limit disabled).
+
+max-commit-count::
+	Specifies the number of entries to list per page in "log" view. Default
+	value: "50".
+
+max-message-length::
+	Specifies the maximum number of commit message characters to display in
+	"log" view. Default value: "80".
+
+max-repo-count::
+	Specifies the number of entries to list per page on the	repository
+	index page. Default value: "50".
+
+max-repodesc-length::
+	Specifies the maximum number of repo description characters to display
+	on the repository index page. Default value: "80".
+
+max-stats::
+	Set the default maximum statistics period. Valid values are "week",
+	"month", "quarter" and "year". If unspecified, statistics are
+	disabled. Default value: none. See also: "repo.max-stats".
+
+mimetype.<ext>::
+	Set the mimetype for the specified filename extension. This is used
+	by the `plain` command when returning blob content.
+
+mimetype-file::
+	Specifies the file to use for automatic mimetype lookup. If specified
+	then this field is used as a fallback when no "mimetype.<ext>" match is
+	found. If unspecified then no such lookup is performed. The typical file
+	to use on a Linux system is /etc/mime.types. The format of the file must
+	comply to:
+	- a comment line is an empty line or a line starting with a hash (#),
+	  optionally preceded by whitespace
+	- a non-comment line starts with the mimetype (like image/png), followed
+	  by one or more file extensions (like jpg), all separated by whitespace
+	Default value: none. See also: "mimetype.<ext>".
+
+module-link::
+	Text which will be used as the formatstring for a hyperlink when a
+	submodule is printed in a directory listing. The arguments for the
+	formatstring are the path and SHA1 of the submodule commit. Default
+	value: none.
+
+noplainemail::
+	If set to "1" showing full author email addresses will be disabled.
+	Default value: "0".
+
+noheader::
+	Flag which, when set to "1", will make cgit omit the standard header
+	on all pages. Default value: none. See also: "embedded".
+
+owner-filter::
+	Specifies a command which will be invoked to format the Owner
+	column of the main page.  The command will get the owner on STDIN,
+	and the STDOUT from the command will be included verbatim in the
+	table.  This can be used to link to additional context such as an
+	owners home page.  When active this filter is used instead of the
+	default owner query url.  Default value: none.
+	See also: "FILTER API".
+
+project-list::
+	A list of subdirectories inside of scan-path, relative to it, that
+	should loaded as git repositories. This must be defined prior to
+	scan-path. Default value: none. See also: scan-path, "MACRO
+	EXPANSION".
+
+readme::
+	Text which will be used as default value for "repo.readme". Multiple
+	config keys may be specified, and cgit will use the first found file
+	in this list. This is useful in conjunction with scan-path. Default
+	value: none. See also: scan-path, repo.readme.
+
+remove-suffix::
+	If set to "1" and scan-path is enabled, if any repositories are found
+	with a suffix of ".git", this suffix will be removed for the url and
+	name. This must be defined prior to scan-path. Default value: "0".
+	See also: scan-path.
+
+renamelimit::
+	Maximum number of files to consider when detecting renames. The value
+	 "-1" uses the compiletime value in git (for further info, look at
+	  `man git-diff`). Default value: "-1".
+
+repository-sort::
+	The way in which repositories in each section are sorted. Valid values
+	are "name" for sorting by the repo name or "age" for sorting by the
+	most recently updated repository. Default value: "name". See also:
+	section, case-sensitive-sort, section-sort.
+
+robots::
+	Text used as content for the "robots" meta-tag. Default value:
+	"index, nofollow".
+
+root-desc::
+	Text printed below the heading on the repository index page. Default
+	value: "a fast webinterface for the git dscm".
+
+root-readme::
+	The content of the file specified with this option will be included
+	verbatim below the "about" link on the repository index page. Default
+	value: none.
+
+root-title::
+	Text printed as heading on the repository index page. Default value:
+	"Git Repository Browser".
+
+scan-hidden-path::
+	If set to "1" and scan-path is enabled, scan-path will recurse into
+	directories whose name starts with a period ('.'). Otherwise,
+	scan-path will stay away from such directories (considered as
+	"hidden"). Note that this does not apply to the ".git" directory in
+	non-bare repos. This must be defined prior to scan-path.
+	Default value: 0. See also: scan-path.
+
+scan-path::
+	A path which will be scanned for repositories. If caching is enabled,
+	the result will be cached as a cgitrc include-file in the cache
+	directory. If project-list has been defined prior to scan-path,
+	scan-path loads only the directories listed in the file pointed to by
+	project-list. Be advised that only the global settings taken
+	before the scan-path directive will be applied to each repository.
+	Default value: none. See also: cache-scanrc-ttl, project-list,
+	"MACRO EXPANSION".
+
+section::
+	The name of the current repository section - all repositories defined
+	after this option will inherit the current section name. Default value:
+	none.
+
+section-sort::
+	Flag which, when set to "1", will sort the sections on the repository
+	listing by name. Set this flag to "0" if the order in the cgitrc file should
+	be preserved. Default value: "1". See also: section,
+	case-sensitive-sort, repository-sort.
+
+section-from-path::
+	A number which, if defined prior to scan-path, specifies how many
+	path elements from each repo path to use as a default section name.
+	If negative, cgit will discard the specified number of path elements
+	above the repo directory. Default value: "0".
+
+side-by-side-diffs::
+	If set to "1" shows side-by-side diffs instead of unidiffs per
+	default. Default value: "0".
+
+snapshots::
+	Text which specifies the default set of snapshot formats that cgit
+	generates links for. The value is a space-separated list of zero or
+	more of the values "tar", "tar.gz", "tar.bz2", "tar.lz", "tar.xz",
+	"tar.zst" and "zip". The special value "all" enables all snapshot
+	formats. Default value: none.
+	All compressors use default settings. Some settings can be influenced
+	with environment variables, for example set ZSTD_CLEVEL=10 in web
+	server environment for higher (but slower) zstd compression.
+
+source-filter::
+	Specifies a command which will be invoked to format plaintext blobs
+	in the tree view. The command will get the blob content on its STDIN
+	and the name of the blob as its only command line argument. The STDOUT
+	from the command will be included verbatim as the blob contents, i.e.
+	this can be used to implement e.g. syntax highlighting. Default value:
+	none. See also: "FILTER API".
+
+summary-branches::
+	Specifies the number of branches to display in the repository "summary"
+	view. Default value: "10".
+
+summary-log::
+	Specifies the number of log entries to display in the repository
+	"summary" view. Default value: "10".
+
+summary-tags::
+	Specifies the number of tags to display in the repository "summary"
+	view. Default value: "10".
+
+strict-export::
+	Filename which, if specified, needs to be present within the repository
+	for cgit to allow access to that repository. This can be used to emulate
+	gitweb's EXPORT_OK and STRICT_EXPORT functionality and limit cgit's
+	repositories to match those exported by git-daemon. This option must
+	be defined prior to scan-path.
+
+virtual-root::
+	Url which, if specified, will be used as root for all cgit links. It
+	will also cause cgit to generate 'virtual urls', i.e. urls like
+	'/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
+	value: none.
+	NOTE: cgit has recently learned how to use PATH_INFO to achieve the
+	same kind of virtual urls, so this option will probably be deprecated.
+
+
+REPOSITORY SETTINGS
+-------------------
+repo.about-filter::
+	Override the default about-filter. Default value: none. See also:
+	"enable-filter-overrides". See also: "FILTER API".
+
+repo.branch-sort::
+	Flag which, when set to "age", enables date ordering in the branch ref
+	list, and when set to "name" enables ordering by branch name. Default
+	value: "name".
+
+repo.clone-url::
+	A list of space-separated urls which can be used to clone this repo.
+	Default value: none. See also: "MACRO EXPANSION".
+
+repo.commit-filter::
+	Override the default commit-filter. Default value: none. See also:
+	"enable-filter-overrides". See also: "FILTER API".
+
+repo.commit-sort::
+	Flag which, when set to "date", enables strict date ordering in the
+	commit log, and when set to "topo" enables strict topological
+	ordering. If unset, the default ordering of "git log" is used. Default
+	value: unset.
+
+repo.defbranch::
+	The name of the default branch for this repository. If no such branch
+	exists in the repository, the first branch name (when sorted) is used
+	as default instead. Default value: branch pointed to by HEAD, or
+	"master" if there is no suitable HEAD.
+
+repo.desc::
+	The value to show as repository description. Default value: none.
+
+repo.email-filter::
+	Override the default email-filter. Default value: none. See also:
+	"enable-filter-overrides". See also: "FILTER API".
+
+repo.enable-blame::
+	A flag which can be used to disable the global setting
+	`enable-blame'. Default value: none.
+
+repo.enable-commit-graph::
+	A flag which can be used to disable the global setting
+	`enable-commit-graph'. Default value: none.
+
+repo.enable-html-serving::
+	A flag which can be used to override the global setting
+	`enable-html-serving`. Default value: none.
+
+repo.enable-log-filecount::
+	A flag which can be used to disable the global setting
+	`enable-log-filecount'. Default value: none.
+
+repo.enable-log-linecount::
+	A flag which can be used to disable the global setting
+	`enable-log-linecount'. Default value: none.
+
+repo.enable-remote-branches::
+	Flag which, when set to "1", will make cgit display remote branches
+	in the summary and refs views. Default value: <enable-remote-branches>.
+
+repo.enable-subject-links::
+	A flag which can be used to override the global setting
+	`enable-subject-links'. Default value: none.
+
+repo.extra-head-content::
+	This value will be added verbatim to the head section of each page
+	displayed for this repo. Default value: none.
+
+repo.hide::
+	Flag which, when set to "1", hides the repository from the repository
+	index. The repository can still be accessed by providing a direct path.
+	Default value: "0". See also: "repo.ignore".
+
+repo.homepage::
+	The value to show as repository homepage. Default value: none.
+
+repo.ignore::
+	Flag which, when set to "1", ignores the repository. The repository
+	is not shown in the index and cannot be accessed by providing a direct
+	path. Default value: "0". See also: "repo.hide".
+
+repo.logo::
+	Url which specifies the source of an image which will be used as a logo
+	on this repo's pages. Default value: global logo.
+
+repo.logo-link::
+	Url loaded when clicking on the cgit logo image. If unspecified the
+	calculated url of the repository index page will be used. Default
+	value: global logo-link.
+
+repo.module-link::
+	Text which will be used as the formatstring for a hyperlink when a
+	submodule is printed in a directory listing. The arguments for the
+	formatstring are the path and SHA1 of the submodule commit. Default
+	value: <module-link>
+
+repo.module-link.<path>::
+	Text which will be used as the formatstring for a hyperlink when a
+	submodule with the specified subdirectory path is printed in a
+	directory listing. The only argument for the formatstring is the SHA1
+	of the submodule commit. Default value: none.
+
+repo.max-stats::
+	Override the default maximum statistics period. Valid values are equal
+	to the values specified for the global "max-stats" setting. Default
+	value: none.
+
+repo.name::
+	The value to show as repository name. Default value: <repo.url>.
+
+repo.owner::
+	A value used to identify the owner of the repository. Default value:
+	none.
+
+repo.owner-filter::
+	Override the default owner-filter. Default value: none. See also:
+	"enable-filter-overrides". See also: "FILTER API".
+
+repo.path::
+	An absolute path to the repository directory. For non-bare repositories
+	this is the .git-directory. Default value: none.
+
+repo.readme::
+	A path (relative to <repo.path>) which specifies a file to include
+	verbatim as the "About" page for this repo. You may also specify a
+	git refspec by head or by hash by prepending the refspec followed by
+	a colon. For example, "master:docs/readme.mkd". If the value begins
+	with a colon, i.e. ":docs/readme.rst", the head giving in query or
+	the default branch of the repository will be used. Sharing any file
+	will expose that entire directory tree to the "/about/PATH" endpoints,
+	so be sure that there are no non-public files located in the same
+	directory as the readme file. Default value: <readme>.
+
+repo.section::
+	Override the current section name for this repository. Default value:
+	none.
+
+repo.snapshots::
+	A mask of snapshot formats for this repo that cgit generates links for,
+	restricted by the global "snapshots" setting. Default value:
+	<snapshots>.
+
+repo.snapshot-prefix::
+	Prefix to use for snapshot links instead of the repository basename.
+	For example, the "linux-stable" repository may wish to set this to
+	"linux" so that snapshots are in the format "linux-3.15.4" instead
+	of "linux-stable-3.15.4".  Default value: <empty> meaning to use
+	the repository basename.
+
+repo.source-filter::
+	Override the default source-filter. Default value: none. See also:
+	"enable-filter-overrides". See also: "FILTER API".
+
+repo.url::
+	The relative url used to access the repository. This must be the first
+	setting specified for each repo. Default value: none.
+
+
+REPOSITORY-SPECIFIC CGITRC FILE
+-------------------------------
+When the option "scan-path" is used to auto-discover git repositories, cgit
+will try to parse the file "cgitrc" within any found repository. Such a
+repo-specific config file may contain any of the repo-specific options
+described above, except "repo.url" and "repo.path". Additionally, the "filter"
+options are only acknowledged in repo-specific config files when
+"enable-filter-overrides" is set to "1".
+
+Note: the "repo." prefix is dropped from the option names in repo-specific
+config files, e.g. "repo.desc" becomes "desc".
+
+
+FILTER API
+----------
+By default, filters are separate processes that are executed each time they
+are needed.  Alternative technologies may be used by prefixing the filter
+specification with the relevant string; available values are:
+
+'exec:'::
+	The default "one process per filter" mode.
+
+
+Parameters are provided to filters as follows.
+
+about filter::
+	This filter is given a single parameter: the filename of the source
+	file to filter. The filter can use the filename to determine (for
+	example) the type of syntax to follow when formatting the readme file.
+	The about text that is to be filtered is available on standard input
+	and the filtered text is expected on standard output.
+
+auth filter::
+	The authentication filter receives 12 parameters:
+	  - filter action, explained below, which specifies which action the
+	    filter is called for
+	  - http cookie
+	  - http method
+	  - http referer
+	  - http path
+	  - http https flag
+	  - cgit repo
+	  - cgit page
+	  - cgit url
+	  - cgit login url
+	When the filter action is "body", this filter must write to output the
+	HTML for displaying the login form, which POSTs to the login url. When
+	the filter action is "authenticate-cookie", this filter must validate
+	the http cookie and return a 0 if it is invalid or 1 if it is invalid,
+	in the exit code / close function. If the filter action is
+	"authenticate-post", this filter receives POST'd parameters on
+	standard input, and should write a complete CGI response, preferably
+	with a 302 redirect, and write to output one or more "Set-Cookie"
+	HTTP headers, each followed by a newline.
+
+commit filter::
+	This filter is given no arguments. The commit message text that is to
+	be filtered is available on standard input and the filtered text is
+	expected on standard output.
+
+email filter::
+	This filter is given two parameters: the email address of the relevant
+	author and a string indicating the originating page. The filter will
+	then receive the text string to format on standard input and is
+	expected to write to standard output the formatted text to be included
+	in the page.
+
+owner filter::
+	This filter is given no arguments.  The owner text is available on
+	standard input and the filter is expected to write to standard
+	output.  The output is included in the Owner column.
+
+source filter::
+	This filter is given a single parameter: the filename of the source
+	file to filter. The filter can use the filename to determine (for
+	example) the syntax highlighting mode. The contents of the source
+	file that is to be filtered is available on standard input and the
+	filtered contents is expected on standard output.
+
+
+All filters are handed the following environment variables:
+
+- CGIT_REPO_URL (from repo.url)
+- CGIT_REPO_NAME (from repo.name)
+- CGIT_REPO_PATH (from repo.path)
+- CGIT_REPO_OWNER (from repo.owner)
+- CGIT_REPO_DEFBRANCH (from repo.defbranch)
+- CGIT_REPO_SECTION (from repo.section)
+- CGIT_REPO_CLONE_URL (from repo.clone-url)
+
+If a setting is not defined for a repository and the corresponding global
+setting is also not defined (if applicable), then the corresponding
+environment variable will be unset.
+
+
+MACRO EXPANSION
+---------------
+The following cgitrc options support a simple macro expansion feature,
+where tokens prefixed with "$" are replaced with the value of a similarly
+named environment variable:
+
+- cache-root
+- include
+- project-list
+- scan-path
+
+Macro expansion will also happen on the content of $CGIT_CONFIG, if
+defined.
+
+One usage of this feature is virtual hosting, which in its simplest form
+can be accomplished by adding the following line to /etc/cgitrc:
+
+	include=/etc/cgitrc.d/$HTTP_HOST
+
+The following options are expanded during request processing, and support
+the environment variables defined in "FILTER API":
+
+- clone-url
+- repo.clone-url
+
+
+CACHE
+-----
+
+All cache ttl values are in minutes. Negative ttl values indicate that a page
+type will never expire, and thus the first time a URL is accessed, the result
+will be cached indefinitely, even if the underlying git repository changes.
+Conversely, when a ttl value is zero, the cache is disabled for that
+particular page type, and the page type is never cached.
+
+SIGNATURES
+----------
+
+Cgit can host .asc signatures corresponding to various snapshot formats,
+through use of git notes. For example, the following command may be used to
+add a signature to a .tar.xz archive:
+
+    git notes --ref=refs/notes/signatures/tar.xz add -C "$(
+	gpg --output - --armor --detach-sign cgit-1.1.tar.xz |
+	git hash-object -w --stdin
+    )" v1.1
+
+If it is instead desirable to attach a signature of the underlying .tar, this
+will be linked, as a special case, beside a .tar.* link that does not have its
+own signature. For example, a signature of a tarball of the latest tag might
+be added with a similar command:
+
+    tag="$(git describe --abbrev=0)"
+    git notes --ref=refs/notes/signatures/tar add -C "$(
+        git archive --format tar --prefix "cgit-${tag#v}/" "$tag" |
+        gpg --output - --armor --detach-sign |
+        git hash-object -w --stdin
+    )" "$tag"
+
+Since git-archive(1) is expected to produce stable output between versions,
+this allows one to generate a long-term signature of the contents of a given
+tag.
+
+EXAMPLE CGITRC FILE
+-------------------
+
+....
+# Enable caching of up to 1000 output entries
+cache-size=1000
+
+
+# Specify some default clone urls using macro expansion
+clone-url=git://foo.org/$CGIT_REPO_URL git@foo.org:$CGIT_REPO_URL
+
+# Specify the css url
+css=/css/cgit.css
+
+
+# Show owner on index page
+enable-index-owner=1
+
+
+# Allow http transport git clone
+enable-http-clone=1
+
+
+# Show extra links for each repository on the index page
+enable-index-links=1
+
+
+# Enable blame page and create links to it from tree page
+enable-blame=1
+
+
+# Enable ASCII art commit history graph on the log pages
+enable-commit-graph=1
+
+
+# Show number of affected files per commit on the log pages
+enable-log-filecount=1
+
+
+# Show number of added/removed lines per commit on the log pages
+enable-log-linecount=1
+
+
+# Sort branches by date
+branch-sort=age
+
+
+# Add a cgit favicon
+favicon=/favicon.ico
+
+
+# Use a custom logo
+logo=/img/mylogo.png
+
+
+# Enable statistics per week, month and quarter
+max-stats=quarter
+
+
+# Set the title and heading of the repository index page
+root-title=example.com git repositories
+
+
+# Set a subheading for the repository index page
+root-desc=tracking the foobar development
+
+
+# Include some more info about example.com on the index page
+root-readme=/var/www/htdocs/about.html
+
+
+# Allow download of tar.gz, tar.bz2 and zip-files
+snapshots=tar.gz tar.bz2 zip
+
+
+##
+## List of common mimetypes
+##
+
+mimetype.gif=image/gif
+mimetype.html=text/html
+mimetype.jpg=image/jpeg
+mimetype.jpeg=image/jpeg
+mimetype.pdf=application/pdf
+mimetype.png=image/png
+mimetype.svg=image/svg+xml
+
+
+# Highlight source code with python pygments-based highlighter
+source-filter=/var/www/cgit/filters/syntax-highlighting.py
+
+# Format markdown, restructuredtext, manpages, text files, and html files
+# through the right converters
+about-filter=/var/www/cgit/filters/about-formatting.sh
+
+##
+## Search for these files in the root of the default branch of repositories
+## for coming up with the about page:
+##
+readme=:README.md
+readme=:readme.md
+readme=:README.mkd
+readme=:readme.mkd
+readme=:README.rst
+readme=:readme.rst
+readme=:README.html
+readme=:readme.html
+readme=:README.htm
+readme=:readme.htm
+readme=:README.txt
+readme=:readme.txt
+readme=:README
+readme=:readme
+readme=:INSTALL.md
+readme=:install.md
+readme=:INSTALL.mkd
+readme=:install.mkd
+readme=:INSTALL.rst
+readme=:install.rst
+readme=:INSTALL.html
+readme=:install.html
+readme=:INSTALL.htm
+readme=:install.htm
+readme=:INSTALL.txt
+readme=:install.txt
+readme=:INSTALL
+readme=:install
+
+
+##
+## List of repositories.
+## PS: Any repositories listed when section is unset will not be
+##     displayed under a section heading
+## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
+##      and included like this:
+##        include=/etc/cgitrepos
+##
+
+
+repo.url=foo
+repo.path=/pub/git/foo.git
+repo.desc=the master foo repository
+repo.owner=fooman@example.com
+repo.readme=info/web/about.html
+
+
+repo.url=bar
+repo.path=/pub/git/bar.git
+repo.desc=the bars for your foo
+repo.owner=barman@example.com
+repo.readme=info/web/about.html
+
+
+# The next repositories will be displayed under the 'extras' heading
+section=extras
+
+
+repo.url=baz
+repo.path=/pub/git/baz.git
+repo.desc=a set of extensions for bar users
+
+repo.url=wiz
+repo.path=/pub/git/wiz.git
+repo.desc=the wizard of foo
+
+
+# Add some mirrored repositories
+section=mirrors
+
+
+repo.url=git
+repo.path=/pub/git/git.git
+repo.desc=the dscm
+
+
+repo.url=linux
+repo.path=/pub/git/linux.git
+repo.desc=the kernel
+
+# Disable adhoc downloads of this repo
+repo.snapshots=0
+
+# Disable line-counts for this repo
+repo.enable-log-linecount=0
+
+# Restrict the max statistics period for this repo
+repo.max-stats=month
+....
+
+
+BUGS
+----
+Comments currently cannot appear on the same line as a setting; the comment
+will be included as part of the value. E.g. this line:
+
+	robots=index  # allow indexing
+
+will generate the following html element:
+
+	<meta name='robots' content='index  # allow indexing'/>
+
+
+
+AUTHOR
+------
+Lars Hjemli <hjemli@gmail.com>
+Jason A. Donenfeld <Jason@zx2c4.com>
diff --git a/third_party/cgit/cmd.c b/third_party/cgit/cmd.c
new file mode 100644
index 0000000000..c664e894f7
--- /dev/null
+++ b/third_party/cgit/cmd.c
@@ -0,0 +1,186 @@
+/* cmd.c: the cgit command dispatcher
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "cmd.h"
+#include "cache.h"
+#include "ui-shared.h"
+#include "ui-atom.h"
+#include "ui-blame.h"
+#include "ui-blob.h"
+#include "ui-clone.h"
+#include "ui-commit.h"
+#include "ui-diff.h"
+#include "ui-log.h"
+#include "ui-patch.h"
+#include "ui-plain.h"
+#include "ui-refs.h"
+#include "ui-repolist.h"
+#include "ui-snapshot.h"
+#include "ui-stats.h"
+#include "ui-summary.h"
+#include "ui-tag.h"
+#include "ui-tree.h"
+
+static void HEAD_fn(void)
+{
+	cgit_clone_head();
+}
+
+static void atom_fn(void)
+{
+	cgit_print_atom(ctx.qry.head, ctx.qry.path, ctx.cfg.max_atom_items);
+}
+
+static void about_fn(void)
+{
+	cgit_print_repo_readme(ctx.qry.path);
+}
+
+static void blame_fn(void)
+{
+	if (ctx.repo->enable_blame)
+		cgit_print_blame();
+	else
+		cgit_print_error_page(403, "Forbidden", "Blame is disabled");
+}
+
+static void blob_fn(void)
+{
+	cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head, 0);
+}
+
+static void commit_fn(void)
+{
+	cgit_print_commit(ctx.qry.oid, ctx.qry.path);
+}
+
+static void diff_fn(void)
+{
+	cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 0);
+}
+
+static void rawdiff_fn(void)
+{
+	cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 1);
+}
+
+static void info_fn(void)
+{
+	cgit_clone_info();
+}
+
+static void log_fn(void)
+{
+	cgit_print_log(ctx.qry.oid, ctx.qry.ofs, ctx.cfg.max_commit_count,
+		       ctx.qry.grep, ctx.qry.search, ctx.qry.path, 1,
+		       ctx.repo->enable_commit_graph,
+		       ctx.repo->commit_sort);
+}
+
+static void ls_cache_fn(void)
+{
+	ctx.page.mimetype = "text/plain";
+	ctx.page.filename = "ls-cache.txt";
+	cgit_print_http_headers();
+	cache_ls(ctx.cfg.cache_root);
+}
+
+static void objects_fn(void)
+{
+	cgit_clone_objects();
+}
+
+static void repolist_fn(void)
+{
+	cgit_print_repolist();
+}
+
+static void patch_fn(void)
+{
+	cgit_print_patch(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path);
+}
+
+static void plain_fn(void)
+{
+	cgit_print_plain();
+}
+
+static void refs_fn(void)
+{
+	cgit_print_refs();
+}
+
+static void snapshot_fn(void)
+{
+	cgit_print_snapshot(ctx.qry.head, ctx.qry.oid, ctx.qry.path,
+			    ctx.qry.nohead);
+}
+
+static void stats_fn(void)
+{
+	cgit_show_stats();
+}
+
+static void summary_fn(void)
+{
+	cgit_print_summary();
+}
+
+static void tag_fn(void)
+{
+	cgit_print_tag(ctx.qry.oid);
+}
+
+static void tree_fn(void)
+{
+	cgit_print_tree(ctx.qry.oid, ctx.qry.path);
+}
+
+#define def_cmd(name, want_repo, want_vpath, is_clone) \
+	{#name, name##_fn, want_repo, want_vpath, is_clone}
+
+struct cgit_cmd *cgit_get_cmd(void)
+{
+	static struct cgit_cmd cmds[] = {
+		def_cmd(HEAD, 1, 0, 1),
+		def_cmd(atom, 1, 0, 0),
+		def_cmd(about, 1, 1, 0),
+		def_cmd(blame, 1, 1, 0),
+		def_cmd(blob, 1, 0, 0),
+		def_cmd(commit, 1, 1, 0),
+		def_cmd(diff, 1, 1, 0),
+		def_cmd(info, 1, 0, 1),
+		def_cmd(log, 1, 1, 0),
+		def_cmd(ls_cache, 0, 0, 0),
+		def_cmd(objects, 1, 0, 1),
+		def_cmd(patch, 1, 1, 0),
+		def_cmd(plain, 1, 0, 0),
+		def_cmd(rawdiff, 1, 1, 0),
+		def_cmd(refs, 1, 0, 0),
+		def_cmd(repolist, 0, 0, 0),
+		def_cmd(snapshot, 1, 0, 0),
+		def_cmd(stats, 1, 1, 0),
+		def_cmd(summary, 1, 0, 0),
+		def_cmd(tag, 1, 0, 0),
+		def_cmd(tree, 1, 1, 0),
+	};
+	int i;
+
+	if (ctx.qry.page == NULL) {
+		if (ctx.repo)
+			ctx.qry.page = "summary";
+		else
+			ctx.qry.page = "repolist";
+	}
+
+	for (i = 0; i < sizeof(cmds)/sizeof(*cmds); i++)
+		if (!strcmp(ctx.qry.page, cmds[i].name))
+			return &cmds[i];
+	return NULL;
+}
diff --git a/third_party/cgit/cmd.h b/third_party/cgit/cmd.h
new file mode 100644
index 0000000000..6249b1d892
--- /dev/null
+++ b/third_party/cgit/cmd.h
@@ -0,0 +1,16 @@
+#ifndef CMD_H
+#define CMD_H
+
+typedef void (*cgit_cmd_fn)(void);
+
+struct cgit_cmd {
+	const char *name;
+	cgit_cmd_fn fn;
+	unsigned int want_repo:1,
+		want_vpath:1,
+		is_clone:1;
+};
+
+extern struct cgit_cmd *cgit_get_cmd(void);
+
+#endif /* CMD_H */
diff --git a/third_party/cgit/configfile.c b/third_party/cgit/configfile.c
new file mode 100644
index 0000000000..e0391091e1
--- /dev/null
+++ b/third_party/cgit/configfile.c
@@ -0,0 +1,90 @@
+/* configfile.c: parsing of config files
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include <git-compat-util.h>
+#include "configfile.h"
+
+static int next_char(FILE *f)
+{
+	int c = fgetc(f);
+	if (c == '\r') {
+		c = fgetc(f);
+		if (c != '\n') {
+			ungetc(c, f);
+			c = '\r';
+		}
+	}
+	return c;
+}
+
+static void skip_line(FILE *f)
+{
+	int c;
+
+	while ((c = next_char(f)) && c != '\n' && c != EOF)
+		;
+}
+
+static int read_config_line(FILE *f, struct strbuf *name, struct strbuf *value)
+{
+	int c = next_char(f);
+
+	strbuf_reset(name);
+	strbuf_reset(value);
+
+	/* Skip comments and preceding spaces. */
+	for(;;) {
+		if (c == EOF)
+			return 0;
+		else if (c == '#' || c == ';')
+			skip_line(f);
+		else if (!isspace(c))
+			break;
+		c = next_char(f);
+	}
+
+	/* Read variable name. */
+	while (c != '=') {
+		if (c == '\n' || c == EOF)
+			return 0;
+		strbuf_addch(name, c);
+		c = next_char(f);
+	}
+
+	/* Read variable value. */
+	c = next_char(f);
+	while (c != '\n' && c != EOF) {
+		strbuf_addch(value, c);
+		c = next_char(f);
+	}
+
+	return 1;
+}
+
+int parse_configfile(const char *filename, configfile_value_fn fn)
+{
+	static int nesting;
+	struct strbuf name = STRBUF_INIT;
+	struct strbuf value = STRBUF_INIT;
+	FILE *f;
+
+	/* cancel deeply nested include-commands */
+	if (nesting > 8)
+		return -1;
+	if (!(f = fopen(filename, "r")))
+		return -1;
+	nesting++;
+	while (read_config_line(f, &name, &value))
+		fn(name.buf, value.buf);
+	nesting--;
+	fclose(f);
+	strbuf_release(&name);
+	strbuf_release(&value);
+	return 0;
+}
+
diff --git a/third_party/cgit/configfile.h b/third_party/cgit/configfile.h
new file mode 100644
index 0000000000..af7ca19735
--- /dev/null
+++ b/third_party/cgit/configfile.h
@@ -0,0 +1,10 @@
+#ifndef CONFIGFILE_H
+#define CONFIGFILE_H
+
+#include "cgit.h"
+
+typedef void (*configfile_value_fn)(const char *name, const char *value);
+
+extern int parse_configfile(const char *filename, configfile_value_fn fn);
+
+#endif /* CONFIGFILE_H */
diff --git a/third_party/cgit/contrib/hooks/post-receive.agefile b/third_party/cgit/contrib/hooks/post-receive.agefile
new file mode 100755
index 0000000000..2f72ae9c0d
--- /dev/null
+++ b/third_party/cgit/contrib/hooks/post-receive.agefile
@@ -0,0 +1,19 @@
+#!/bin/sh
+#
+# An example hook to update the "agefile" for CGit's idle time calculation.
+#
+# This hook assumes that you are using the default agefile location of
+# "info/web/last-modified".  If you change the value in your cgitrc then you
+# must also change it here.
+#
+# To install the hook, copy (or link) it to the file "hooks/post-receive" in
+# each of your repositories.
+#
+
+agefile="$(git rev-parse --git-dir)"/info/web/last-modified
+
+mkdir -p "$(dirname "$agefile")" &&
+git for-each-ref \
+	--sort=-authordate --count=1 \
+	--format='%(authordate:iso8601)' \
+	>"$agefile"
diff --git a/third_party/cgit/default.nix b/third_party/cgit/default.nix
new file mode 100644
index 0000000000..c783bda16e
--- /dev/null
+++ b/third_party/cgit/default.nix
@@ -0,0 +1,51 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  inherit (pkgs) stdenv gzip bzip2 xz lzip zstd zlib openssl;
+in
+stdenv.mkDerivation rec {
+  pname = "cgit-pink";
+  version = "master";
+  src = ./.;
+
+  buildInputs = [ openssl zlib ];
+
+  enableParallelBuilding = true;
+
+  postPatch = ''
+    sed -e 's|"gzip"|"${gzip}/bin/gzip"|' \
+        -e 's|"bzip2"|"${bzip2.bin}/bin/bzip2"|' \
+        -e 's|"lzip"|"${lzip}/bin/lzip"|' \
+        -e 's|"xz"|"${xz.bin}/bin/xz"|' \
+        -e 's|"zstd"|"${zstd}/bin/zstd"|' \
+        -i ui-snapshot.c
+  '';
+
+  # Give cgit the git source tree including depot patches. Note that
+  # the version expected by cgit should be kept in sync with the
+  # version available in nixpkgs.
+  #
+  # TODO(tazjin): Add an assert for this somewhere so we notice it on
+  # channel bumps.
+  preBuild = ''
+    rm -rf git # remove submodule dir ...
+    cp -r --no-preserve=ownership,mode ${pkgs.srcOnly depot.third_party.git} git
+    makeFlagsArray+=(prefix="$out" CGIT_SCRIPT_PATH="$out/cgit/")
+    cat tvl-extra.css >> cgit.css
+  '';
+
+  stripDebugList = [ "cgit" ];
+
+  # We don't use the filters and they require wrapping to find their deps
+  postInstall = ''
+    rm -rf "$out/lib/cgit/filters"
+    find "$out" -type d -empty -delete
+  '';
+
+  meta = {
+    hompepage = "https://git.causal.agency/cgit-pink/";
+    description = "cgit fork aiming for better maintenance";
+    license = lib.licenses.gpl2;
+    platforms = lib.platforms.linux;
+  };
+}
diff --git a/third_party/cgit/filter.c b/third_party/cgit/filter.c
new file mode 100644
index 0000000000..190fb5501b
--- /dev/null
+++ b/third_party/cgit/filter.c
@@ -0,0 +1,222 @@
+/* filter.c: filter framework functions
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "html.h"
+
+static inline void reap_filter(struct cgit_filter *filter)
+{
+	if (filter && filter->cleanup)
+		filter->cleanup(filter);
+}
+
+void cgit_cleanup_filters(void)
+{
+	int i;
+	reap_filter(ctx.cfg.about_filter);
+	reap_filter(ctx.cfg.commit_filter);
+	reap_filter(ctx.cfg.source_filter);
+	reap_filter(ctx.cfg.email_filter);
+	reap_filter(ctx.cfg.owner_filter);
+	reap_filter(ctx.cfg.auth_filter);
+	for (i = 0; i < cgit_repolist.count; ++i) {
+		reap_filter(cgit_repolist.repos[i].about_filter);
+		reap_filter(cgit_repolist.repos[i].commit_filter);
+		reap_filter(cgit_repolist.repos[i].source_filter);
+		reap_filter(cgit_repolist.repos[i].email_filter);
+		reap_filter(cgit_repolist.repos[i].owner_filter);
+	}
+}
+
+static int open_exec_filter(struct cgit_filter *base, va_list ap)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	int pipe_fh[2];
+	int i;
+
+	for (i = 0; i < filter->base.argument_count; i++)
+		filter->argv[i + 1] = va_arg(ap, char *);
+
+	chk_zero(fflush(stdout), "unable to flush STDOUT");
+	filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
+		"Unable to duplicate STDOUT");
+	chk_zero(pipe(pipe_fh), "Unable to create pipe to subprocess");
+	filter->pid = chk_non_negative(fork(), "Unable to create subprocess");
+	if (filter->pid == 0) {
+		close(pipe_fh[1]);
+		chk_non_negative(dup2(pipe_fh[0], STDIN_FILENO),
+			"Unable to use pipe as STDIN");
+		execvp(filter->cmd, filter->argv);
+		die_errno("Unable to exec subprocess %s", filter->cmd);
+	}
+	close(pipe_fh[0]);
+	chk_non_negative(dup2(pipe_fh[1], STDOUT_FILENO),
+		"Unable to use pipe as STDOUT");
+	close(pipe_fh[1]);
+	return 0;
+}
+
+static int close_exec_filter(struct cgit_filter *base)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	int i, exit_status = 0;
+
+	chk_zero(fflush(stdout), "unable to flush STDOUT");
+	chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO),
+		"Unable to restore STDOUT");
+	close(filter->old_stdout);
+	if (filter->pid < 0)
+		goto done;
+	waitpid(filter->pid, &exit_status, 0);
+	if (WIFEXITED(exit_status))
+		goto done;
+	die("Subprocess %s exited abnormally", filter->cmd);
+
+done:
+	for (i = 0; i < filter->base.argument_count; i++)
+		filter->argv[i + 1] = NULL;
+	return WEXITSTATUS(exit_status);
+
+}
+
+static void fprintf_exec_filter(struct cgit_filter *base, FILE *f, const char *prefix)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	fprintf(f, "%sexec:%s\n", prefix, filter->cmd);
+}
+
+static void cleanup_exec_filter(struct cgit_filter *base)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	if (filter->argv) {
+		free(filter->argv);
+		filter->argv = NULL;
+	}
+	if (filter->cmd) {
+		free(filter->cmd);
+		filter->cmd = NULL;
+	}
+}
+
+static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count)
+{
+	struct cgit_exec_filter *f;
+	int args_size = 0;
+
+	f = xmalloc(sizeof(*f));
+	/* We leave argv for now and assign it below. */
+	cgit_exec_filter_init(f, xstrdup(cmd), NULL);
+	f->base.argument_count = argument_count;
+	args_size = (2 + argument_count) * sizeof(char *);
+	f->argv = xmalloc(args_size);
+	memset(f->argv, 0, args_size);
+	f->argv[0] = f->cmd;
+	return &f->base;
+}
+
+void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv)
+{
+	memset(filter, 0, sizeof(*filter));
+	filter->base.open = open_exec_filter;
+	filter->base.close = close_exec_filter;
+	filter->base.fprintf = fprintf_exec_filter;
+	filter->base.cleanup = cleanup_exec_filter;
+	filter->cmd = cmd;
+	filter->argv = argv;
+	/* The argument count for open_filter is zero by default, unless called from new_filter, above. */
+	filter->base.argument_count = 0;
+}
+
+int cgit_open_filter(struct cgit_filter *filter, ...)
+{
+	int result;
+	va_list ap;
+	if (!filter)
+		return 0;
+	va_start(ap, filter);
+	result = filter->open(filter, ap);
+	va_end(ap);
+	return result;
+}
+
+int cgit_close_filter(struct cgit_filter *filter)
+{
+	if (!filter)
+		return 0;
+	return filter->close(filter);
+}
+
+void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix)
+{
+	(filter->fprintf)(filter, f, prefix);
+}
+
+
+
+static const struct {
+	const char *prefix;
+	struct cgit_filter *(*ctor)(const char *cmd, int argument_count);
+} filter_specs[] = {
+	{ "exec", new_exec_filter },
+};
+
+struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype)
+{
+	char *colon;
+	int i;
+	size_t len;
+	int argument_count;
+
+	if (!cmd || !cmd[0])
+		return NULL;
+
+	colon = strchr(cmd, ':');
+	len = colon - cmd;
+	/*
+	 * In case we're running on Windows, don't allow a single letter before
+	 * the colon.
+	 */
+	if (len == 1)
+		colon = NULL;
+
+	switch (filtertype) {
+		case AUTH:
+			argument_count = 12;
+			break;
+
+		case EMAIL:
+			argument_count = 2;
+			break;
+
+		case OWNER:
+			argument_count = 0;
+			break;
+
+		case SOURCE:
+		case ABOUT:
+			argument_count = 1;
+			break;
+
+		case COMMIT:
+		default:
+			argument_count = 0;
+			break;
+	}
+
+	/* If no prefix is given, exec filter is the default. */
+	if (!colon)
+		return new_exec_filter(cmd, argument_count);
+
+	for (i = 0; i < ARRAY_SIZE(filter_specs); i++) {
+		if (len == strlen(filter_specs[i].prefix) &&
+		    !strncmp(filter_specs[i].prefix, cmd, len))
+			return filter_specs[i].ctor(colon + 1, argument_count);
+	}
+
+	die("Invalid filter type: %.*s", (int) len, cmd);
+}
diff --git a/third_party/cgit/filters/about-formatting.sh b/third_party/cgit/filters/about-formatting.sh
new file mode 100755
index 0000000000..85daf9c26b
--- /dev/null
+++ b/third_party/cgit/filters/about-formatting.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+# This may be used with the about-filter or repo.about-filter setting in cgitrc.
+# It passes formatting of about pages to differing programs, depending on the usage.
+
+# Markdown support requires python and markdown-python.
+# RestructuredText support requires python and docutils.
+# Man page support requires groff.
+
+# The following environment variables can be used to retrieve the configuration
+# of the repository for which this script is called:
+# CGIT_REPO_URL        ( = repo.url       setting )
+# CGIT_REPO_NAME       ( = repo.name      setting )
+# CGIT_REPO_PATH       ( = repo.path      setting )
+# CGIT_REPO_OWNER      ( = repo.owner     setting )
+# CGIT_REPO_DEFBRANCH  ( = repo.defbranch setting )
+# CGIT_REPO_SECTION    ( = section        setting )
+# CGIT_REPO_CLONE_URL  ( = repo.clone-url setting )
+
+cd "$(dirname $0)/html-converters/"
+case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
+	*.markdown|*.mdown|*.md|*.mkd) exec ./md2html; ;;
+	*.rst) exec ./rst2html; ;;
+	*.[1-9]) exec ./man2html; ;;
+	*.htm|*.html) exec cat; ;;
+	*.txt|*) exec ./txt2html; ;;
+esac
diff --git a/third_party/cgit/filters/commit-links.sh b/third_party/cgit/filters/commit-links.sh
new file mode 100755
index 0000000000..796ac308d2
--- /dev/null
+++ b/third_party/cgit/filters/commit-links.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+# This script can be used to generate links in commit messages.
+#
+# To use this script, refer to this file with either the commit-filter or the
+# repo.commit-filter options in cgitrc.
+#
+# The following environment variables can be used to retrieve the configuration
+# of the repository for which this script is called:
+# CGIT_REPO_URL        ( = repo.url       setting )
+# CGIT_REPO_NAME       ( = repo.name      setting )
+# CGIT_REPO_PATH       ( = repo.path      setting )
+# CGIT_REPO_OWNER      ( = repo.owner     setting )
+# CGIT_REPO_DEFBRANCH  ( = repo.defbranch setting )
+# CGIT_REPO_SECTION    ( = section        setting )
+# CGIT_REPO_CLONE_URL  ( = repo.clone-url setting )
+#
+
+regex=''
+
+# This expression generates links to commits referenced by their SHA1.
+regex=$regex'
+s|\b([0-9a-fA-F]{7,64})\b|<a href="./?id=\1">\1</a>|g'
+
+# This expression generates links to a fictional bugtracker.
+regex=$regex'
+s|#([0-9]+)\b|<a href="http://bugs.example.com/?bug=\1">#\1</a>|g'
+
+sed -re "$regex"
diff --git a/third_party/cgit/filters/email-gravatar.py b/third_party/cgit/filters/email-gravatar.py
new file mode 100755
index 0000000000..012113c591
--- /dev/null
+++ b/third_party/cgit/filters/email-gravatar.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+# This script may be used with the email-filter or repo.email-filter settings in cgitrc.
+#
+# The following environment variables can be used to retrieve the configuration
+# of the repository for which this script is called:
+# CGIT_REPO_URL        ( = repo.url       setting )
+# CGIT_REPO_NAME       ( = repo.name      setting )
+# CGIT_REPO_PATH       ( = repo.path      setting )
+# CGIT_REPO_OWNER      ( = repo.owner     setting )
+# CGIT_REPO_DEFBRANCH  ( = repo.defbranch setting )
+# CGIT_REPO_SECTION    ( = section        setting )
+# CGIT_REPO_CLONE_URL  ( = repo.clone-url setting )
+#
+# It receives an email address on argv[1] and text on stdin. It prints
+# to stdout that text prepended by a gravatar at 10pt.
+
+import sys
+import hashlib
+import codecs
+
+email = sys.argv[1].lower().strip()
+if email[0] == '<':
+        email = email[1:]
+if email[-1] == '>':
+        email = email[0:-1]
+
+page = sys.argv[2]
+
+sys.stdin = codecs.getreader("utf-8")(sys.stdin.detach())
+sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
+
+md5 = hashlib.md5(email.encode()).hexdigest()
+text = sys.stdin.read().strip()
+
+print("<img src='//www.gravatar.com/avatar/" + md5 + "?s=13&amp;d=retro' width='13' height='13' alt='Gravatar' /> " + text)
diff --git a/third_party/cgit/filters/html-converters/man2html b/third_party/cgit/filters/html-converters/man2html
new file mode 100755
index 0000000000..0ef7884181
--- /dev/null
+++ b/third_party/cgit/filters/html-converters/man2html
@@ -0,0 +1,4 @@
+#!/bin/sh
+echo "<div style=\"font-family: monospace\">"
+groff -mandoc -T html -P -r -P -l | egrep -v '(<html>|<head>|<meta|<title>|</title>|</head>|<body>|</body>|</html>|<!DOCTYPE|"http://www.w3.org)'
+echo "</div>"
diff --git a/third_party/cgit/filters/html-converters/md2html b/third_party/cgit/filters/html-converters/md2html
new file mode 100755
index 0000000000..59f43a8416
--- /dev/null
+++ b/third_party/cgit/filters/html-converters/md2html
@@ -0,0 +1,304 @@
+#!/usr/bin/env python3
+import markdown
+import sys
+import io
+from pygments.formatters import HtmlFormatter
+from markdown.extensions.toc import TocExtension
+sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+sys.stdout.write('''
+<style>
+.markdown-body {
+    font-size: 14px;
+    line-height: 1.6;
+    overflow: hidden;
+}
+.markdown-body>*:first-child {
+    margin-top: 0 !important;
+}
+.markdown-body>*:last-child {
+    margin-bottom: 0 !important;
+}
+.markdown-body a.absent {
+    color: #c00;
+}
+.markdown-body a.anchor {
+    display: block;
+    padding-left: 30px;
+    margin-left: -30px;
+    cursor: pointer;
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+}
+.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
+    margin: 20px 0 10px;
+    padding: 0;
+    font-weight: bold;
+    -webkit-font-smoothing: antialiased;
+    cursor: text;
+    position: relative;
+}
+.markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link {
+    display: none;
+    color: #000;
+}
+.markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor {
+    text-decoration: none;
+    line-height: 1;
+    padding-left: 0;
+    margin-left: -22px;
+    top: 15%;
+}
+.markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link {
+    display: inline-block;
+}
+div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div#cgit .markdown-body h3 a.toclink, div#cgit .markdown-body h4 a.toclink, div#cgit .markdown-body h5 a.toclink, div#cgit .markdown-body h6 a.toclink {
+    color: black;
+}
+.markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code {
+    font-size: inherit;
+}
+.markdown-body h1 {
+    font-size: 28px;
+    color: #000;
+}
+.markdown-body h2 {
+    font-size: 24px;
+    border-bottom: 1px solid #ccc;
+    color: #000;
+}
+.markdown-body h3 {
+    font-size: 18px;
+}
+.markdown-body h4 {
+    font-size: 16px;
+}
+.markdown-body h5 {
+    font-size: 14px;
+}
+.markdown-body h6 {
+    color: #777;
+    font-size: 14px;
+}
+.markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre {
+    margin: 15px 0;
+}
+.markdown-body hr {
+    border: 2px solid #ccc;
+}
+.markdown-body>h2:first-child, .markdown-body>h1:first-child, .markdown-body>h1:first-child+h2, .markdown-body>h3:first-child, .markdown-body>h4:first-child, .markdown-body>h5:first-child, .markdown-body>h6:first-child {
+    margin-top: 0;
+    padding-top: 0;
+}
+.markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 {
+    margin-top: 0;
+    padding-top: 0;
+}
+.markdown-body h1+p, .markdown-body h2+p, .markdown-body h3+p, .markdown-body h4+p, .markdown-body h5+p, .markdown-body h6+p {
+    margin-top: 0;
+}
+.markdown-body li p.first {
+    display: inline-block;
+}
+.markdown-body ul, .markdown-body ol {
+    padding-left: 30px;
+}
+.markdown-body ul.no-list, .markdown-body ol.no-list {
+    list-style-type: none;
+    padding: 0;
+}
+.markdown-body ul li>:first-child, .markdown-body ul li ul:first-of-type, .markdown-body ul li ol:first-of-type, .markdown-body ol li>:first-child, .markdown-body ol li ul:first-of-type, .markdown-body ol li ol:first-of-type {
+    margin-top: 0px;
+}
+.markdown-body ul li p:last-of-type, .markdown-body ol li p:last-of-type {
+    margin-bottom: 0;
+}
+.markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul {
+    margin-bottom: 0;
+}
+.markdown-body dl {
+    padding: 0;
+}
+.markdown-body dl dt {
+    font-size: 14px;
+    font-weight: bold;
+    font-style: italic;
+    padding: 0;
+    margin: 15px 0 5px;
+}
+.markdown-body dl dt:first-child {
+    padding: 0;
+}
+.markdown-body dl dt>:first-child {
+    margin-top: 0px;
+}
+.markdown-body dl dt>:last-child {
+    margin-bottom: 0px;
+}
+.markdown-body dl dd {
+    margin: 0 0 15px;
+    padding: 0 15px;
+}
+.markdown-body dl dd>:first-child {
+    margin-top: 0px;
+}
+.markdown-body dl dd>:last-child {
+    margin-bottom: 0px;
+}
+.markdown-body blockquote {
+    border-left: 4px solid #DDD;
+    padding: 0 15px;
+    color: #777;
+}
+.markdown-body blockquote>:first-child {
+    margin-top: 0px;
+}
+.markdown-body blockquote>:last-child {
+    margin-bottom: 0px;
+}
+.markdown-body table th {
+    font-weight: bold;
+}
+.markdown-body table th, .markdown-body table td {
+    border: 1px solid #ccc;
+    padding: 6px 13px;
+}
+.markdown-body table tr {
+    border-top: 1px solid #ccc;
+    background-color: #fff;
+}
+.markdown-body table tr:nth-child(2n) {
+    background-color: #f8f8f8;
+}
+.markdown-body img {
+    max-width: 100%;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.markdown-body span.frame {
+    display: block;
+    overflow: hidden;
+}
+.markdown-body span.frame>span {
+    border: 1px solid #ddd;
+    display: block;
+    float: left;
+    overflow: hidden;
+    margin: 13px 0 0;
+    padding: 7px;
+    width: auto;
+}
+.markdown-body span.frame span img {
+    display: block;
+    float: left;
+}
+.markdown-body span.frame span span {
+    clear: both;
+    color: #333;
+    display: block;
+    padding: 5px 0 0;
+}
+.markdown-body span.align-center {
+    display: block;
+    overflow: hidden;
+    clear: both;
+}
+.markdown-body span.align-center>span {
+    display: block;
+    overflow: hidden;
+    margin: 13px auto 0;
+    text-align: center;
+}
+.markdown-body span.align-center span img {
+    margin: 0 auto;
+    text-align: center;
+}
+.markdown-body span.align-right {
+    display: block;
+    overflow: hidden;
+    clear: both;
+}
+.markdown-body span.align-right>span {
+    display: block;
+    overflow: hidden;
+    margin: 13px 0 0;
+    text-align: right;
+}
+.markdown-body span.align-right span img {
+    margin: 0;
+    text-align: right;
+}
+.markdown-body span.float-left {
+    display: block;
+    margin-right: 13px;
+    overflow: hidden;
+    float: left;
+}
+.markdown-body span.float-left span {
+    margin: 13px 0 0;
+}
+.markdown-body span.float-right {
+    display: block;
+    margin-left: 13px;
+    overflow: hidden;
+    float: right;
+}
+.markdown-body span.float-right>span {
+    display: block;
+    overflow: hidden;
+    margin: 13px auto 0;
+    text-align: right;
+}
+.markdown-body code, .markdown-body tt {
+    margin: 0 2px;
+    padding: 0px 5px;
+    border: 1px solid #eaeaea;
+    background-color: #f8f8f8;
+    border-radius: 3px;
+}
+.markdown-body code {
+    white-space: nowrap;
+}
+.markdown-body pre>code {
+    margin: 0;
+    padding: 0;
+    white-space: pre;
+    border: none;
+    background: transparent;
+}
+.markdown-body .highlight pre, .markdown-body pre {
+    background-color: #f8f8f8;
+    border: 1px solid #ccc;
+    font-size: 13px;
+    line-height: 19px;
+    overflow: auto;
+    padding: 6px 10px;
+    border-radius: 3px;
+}
+.markdown-body pre code, .markdown-body pre tt {
+    margin: 0;
+    padding: 0;
+    background-color: transparent;
+    border: none;
+}
+''')
+sys.stdout.write(HtmlFormatter(style='pastie').get_style_defs('.highlight'))
+sys.stdout.write('''
+</style>   
+''')
+sys.stdout.write("<div class='markdown-body'>")
+sys.stdout.flush()
+# Note: you may want to run this through bleach for sanitization
+markdown.markdownFromFile(
+	output_format="html5",
+	extensions=[
+		"markdown.extensions.fenced_code",
+		"markdown.extensions.codehilite",
+		"markdown.extensions.tables",
+		"markdown.extensions.sane_lists",
+		TocExtension(anchorlink=True)],
+	extension_configs={
+		"markdown.extensions.codehilite":{"css_class":"highlight"}})
+sys.stdout.write("</div>")
diff --git a/third_party/cgit/filters/html-converters/rst2html b/third_party/cgit/filters/html-converters/rst2html
new file mode 100755
index 0000000000..02d90f81c9
--- /dev/null
+++ b/third_party/cgit/filters/html-converters/rst2html
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec rst2html.py --template <(echo -e "%(stylesheet)s\n%(body_pre_docinfo)s\n%(docinfo)s\n%(body)s")
diff --git a/third_party/cgit/filters/html-converters/txt2html b/third_party/cgit/filters/html-converters/txt2html
new file mode 100755
index 0000000000..495eeceb27
--- /dev/null
+++ b/third_party/cgit/filters/html-converters/txt2html
@@ -0,0 +1,4 @@
+#!/bin/sh
+echo "<pre>"
+sed "s|&|\\&amp;|g;s|'|\\&apos;|g;s|\"|\\&quot;|g;s|<|\\&lt;|g;s|>|\\&gt;|g"
+echo "</pre>"
diff --git a/third_party/cgit/filters/syntax-highlighting.py b/third_party/cgit/filters/syntax-highlighting.py
new file mode 100755
index 0000000000..e912594c48
--- /dev/null
+++ b/third_party/cgit/filters/syntax-highlighting.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+
+# This script uses Pygments and Python3. You must have both installed
+# for this to work.
+#
+# http://pygments.org/
+# http://python.org/
+#
+# It may be used with the source-filter or repo.source-filter settings
+# in cgitrc.
+#
+# The following environment variables can be used to retrieve the
+# configuration of the repository for which this script is called:
+# CGIT_REPO_URL        ( = repo.url       setting )
+# CGIT_REPO_NAME       ( = repo.name      setting )
+# CGIT_REPO_PATH       ( = repo.path      setting )
+# CGIT_REPO_OWNER      ( = repo.owner     setting )
+# CGIT_REPO_DEFBRANCH  ( = repo.defbranch setting )
+# CGIT_REPO_SECTION    ( = section        setting )
+# CGIT_REPO_CLONE_URL  ( = repo.clone-url setting )
+
+
+import sys
+import io
+from pygments import highlight
+from pygments.util import ClassNotFound
+from pygments.lexers import TextLexer
+from pygments.lexers import guess_lexer
+from pygments.lexers import guess_lexer_for_filename
+from pygments.formatters import HtmlFormatter
+
+
+sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8', errors='replace')
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+data = sys.stdin.read()
+filename = sys.argv[1]
+formatter = HtmlFormatter(style='pastie', nobackground=True)
+
+try:
+	lexer = guess_lexer_for_filename(filename, data)
+except ClassNotFound:
+	# check if there is any shebang
+	if data[0:2] == '#!':
+		lexer = guess_lexer(data)
+	else:
+		lexer = TextLexer()
+except TypeError:
+	lexer = TextLexer()
+
+# highlight! :-)
+# printout pygments' css definitions as well
+sys.stdout.write('<style>')
+sys.stdout.write(formatter.get_style_defs('.highlight'))
+sys.stdout.write('</style>')
+sys.stdout.write(highlight(data, lexer, formatter, outfile=None))
diff --git a/third_party/cgit/filters/syntax-highlighting.sh b/third_party/cgit/filters/syntax-highlighting.sh
new file mode 100755
index 0000000000..840bc34fff
--- /dev/null
+++ b/third_party/cgit/filters/syntax-highlighting.sh
@@ -0,0 +1,121 @@
+#!/bin/sh
+# This script can be used to implement syntax highlighting in the cgit
+# tree-view by referring to this file with the source-filter or repo.source-
+# filter options in cgitrc.
+#
+# This script requires a shell supporting the ${var##pattern} syntax.
+# It is supported by at least dash and bash, however busybox environments
+# might have to use an external call to sed instead.
+#
+# Note: the highlight command (http://www.andre-simon.de/) uses css for syntax
+# highlighting, so you'll probably want something like the following included
+# in your css file:
+#
+# Style definition file generated by highlight 2.4.8, http://www.andre-simon.de/
+#
+# table.blob .num  { color:#2928ff; }
+# table.blob .esc  { color:#ff00ff; }
+# table.blob .str  { color:#ff0000; }
+# table.blob .dstr { color:#818100; }
+# table.blob .slc  { color:#838183; font-style:italic; }
+# table.blob .com  { color:#838183; font-style:italic; }
+# table.blob .dir  { color:#008200; }
+# table.blob .sym  { color:#000000; }
+# table.blob .kwa  { color:#000000; font-weight:bold; }
+# table.blob .kwb  { color:#830000; }
+# table.blob .kwc  { color:#000000; font-weight:bold; }
+# table.blob .kwd  { color:#010181; }
+#
+#
+# Style definition file generated by highlight 2.6.14, http://www.andre-simon.de/
+#
+# body.hl  { background-color:#ffffff; }
+# pre.hl   { color:#000000; background-color:#ffffff; font-size:10pt; font-family:'Courier New';}
+# .hl.num  { color:#2928ff; }
+# .hl.esc  { color:#ff00ff; }
+# .hl.str  { color:#ff0000; }
+# .hl.dstr { color:#818100; }
+# .hl.slc  { color:#838183; font-style:italic; }
+# .hl.com  { color:#838183; font-style:italic; }
+# .hl.dir  { color:#008200; }
+# .hl.sym  { color:#000000; }
+# .hl.line { color:#555555; }
+# .hl.mark { background-color:#ffffbb;}
+# .hl.kwa  { color:#000000; font-weight:bold; }
+# .hl.kwb  { color:#830000; }
+# .hl.kwc  { color:#000000; font-weight:bold; }
+# .hl.kwd  { color:#010181; }
+#
+#
+# Style definition file generated by highlight 3.8, http://www.andre-simon.de/
+#
+# body.hl { background-color:#e0eaee; }
+# pre.hl  { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New';}
+# .hl.num { color:#b07e00; }
+# .hl.esc { color:#ff00ff; }
+# .hl.str { color:#bf0303; }
+# .hl.pps { color:#818100; }
+# .hl.slc { color:#838183; font-style:italic; }
+# .hl.com { color:#838183; font-style:italic; }
+# .hl.ppc { color:#008200; }
+# .hl.opt { color:#000000; }
+# .hl.lin { color:#555555; }
+# .hl.kwa { color:#000000; font-weight:bold; }
+# .hl.kwb { color:#0057ae; }
+# .hl.kwc { color:#000000; font-weight:bold; }
+# .hl.kwd { color:#010181; }
+#
+#
+# Style definition file generated by highlight 3.13, http://www.andre-simon.de/
+#
+# body.hl { background-color:#e0eaee; }
+# pre.hl  { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New',monospace;}
+# .hl.num { color:#b07e00; }
+# .hl.esc { color:#ff00ff; }
+# .hl.str { color:#bf0303; }
+# .hl.pps { color:#818100; }
+# .hl.slc { color:#838183; font-style:italic; }
+# .hl.com { color:#838183; font-style:italic; }
+# .hl.ppc { color:#008200; }
+# .hl.opt { color:#000000; }
+# .hl.ipl { color:#0057ae; }
+# .hl.lin { color:#555555; }
+# .hl.kwa { color:#000000; font-weight:bold; }
+# .hl.kwb { color:#0057ae; }
+# .hl.kwc { color:#000000; font-weight:bold; }
+# .hl.kwd { color:#010181; }
+#
+#
+# The following environment variables can be used to retrieve the configuration
+# of the repository for which this script is called:
+# CGIT_REPO_URL        ( = repo.url       setting )
+# CGIT_REPO_NAME       ( = repo.name      setting )
+# CGIT_REPO_PATH       ( = repo.path      setting )
+# CGIT_REPO_OWNER      ( = repo.owner     setting )
+# CGIT_REPO_DEFBRANCH  ( = repo.defbranch setting )
+# CGIT_REPO_SECTION    ( = section        setting )
+# CGIT_REPO_CLONE_URL  ( = repo.clone-url setting )
+#
+
+# store filename and extension in local vars
+BASENAME="$1"
+EXTENSION="${BASENAME##*.}"
+
+[ "${BASENAME}" = "${EXTENSION}" ] && EXTENSION=txt
+[ -z "${EXTENSION}" ] && EXTENSION=txt
+
+# map Makefile and Makefile.* to .mk
+[ "${BASENAME%%.*}" = "Makefile" ] && EXTENSION=mk
+
+# highlight versions 2 and 3 have different commandline options. Specifically,
+# the -X option that is used for version 2 is replaced by the -O xhtml option
+# for version 3.
+#
+# Version 2 can be found (for example) on EPEL 5, while version 3 can be
+# found (for example) on EPEL 6.
+#
+# This is for version 2
+exec highlight --force -f -I -X -S "$EXTENSION" 2>/dev/null
+
+# This is for version 3
+#exec highlight --force -f -I -O xhtml -S "$EXTENSION" 2>/dev/null
diff --git a/third_party/cgit/gen-version.sh b/third_party/cgit/gen-version.sh
new file mode 100755
index 0000000000..80cf49af48
--- /dev/null
+++ b/third_party/cgit/gen-version.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+# Get version-info specified in Makefile
+V=$1
+
+# Use `git describe` to get current version if we're inside a git repo
+if test "$(git rev-parse --git-dir 2>/dev/null)" = '.git'
+then
+	V=$(git describe --abbrev=4 HEAD 2>/dev/null)
+fi
+
+new="CGIT_VERSION = $V"
+old=$(cat VERSION 2>/dev/null)
+
+# Exit if VERSION is uptodate
+test "$old" = "$new" && exit 0
+
+# Update VERSION with new version-info
+echo "$new" > VERSION
+cat VERSION
diff --git a/third_party/cgit/html.c b/third_party/cgit/html.c
new file mode 100644
index 0000000000..ced781adc6
--- /dev/null
+++ b/third_party/cgit/html.c
@@ -0,0 +1,344 @@
+/* html.c: helper functions for html output
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "html.h"
+#include "url.h"
+
+/* Percent-encoding of each character, except: a-zA-Z0-9!$()*,./:;@- */
+static const char* url_escape_table[256] = {
+	"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
+	"%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
+	"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
+	"%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
+	"%20", NULL,  "%22", "%23", NULL,  "%25", "%26", "%27",
+	NULL,  NULL,  NULL,  "%2b", NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  "%3c", "%3d", "%3e", "%3f",
+	NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  "%5c", NULL,  "%5e", NULL,
+	"%60", NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,
+	NULL,  NULL,  NULL,  "%7b", "%7c", "%7d", NULL,  "%7f",
+	"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
+	"%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
+	"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
+	"%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
+	"%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
+	"%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
+	"%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
+	"%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
+	"%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
+	"%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
+	"%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
+	"%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
+	"%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
+	"%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
+	"%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
+	"%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
+};
+
+char *fmt(const char *format, ...)
+{
+	static char buf[8][1024];
+	static int bufidx;
+	int len;
+	va_list args;
+
+	bufidx++;
+	bufidx &= 7;
+
+	va_start(args, format);
+	len = vsnprintf(buf[bufidx], sizeof(buf[bufidx]), format, args);
+	va_end(args);
+	if (len >= sizeof(buf[bufidx])) {
+		fprintf(stderr, "[html.c] string truncated: %s\n", format);
+		exit(1);
+	}
+	return buf[bufidx];
+}
+
+char *fmtalloc(const char *format, ...)
+{
+	struct strbuf sb = STRBUF_INIT;
+	va_list args;
+
+	va_start(args, format);
+	strbuf_vaddf(&sb, format, args);
+	va_end(args);
+
+	return strbuf_detach(&sb, NULL);
+}
+
+void html_raw(const char *data, size_t size)
+{
+	if (fwrite(data, 1, size, stdout) != size)
+		die_errno("write error on html output");
+}
+
+void html(const char *txt)
+{
+	html_raw(txt, strlen(txt));
+}
+
+void htmlf(const char *format, ...)
+{
+	va_list args;
+	struct strbuf buf = STRBUF_INIT;
+
+	va_start(args, format);
+	strbuf_vaddf(&buf, format, args);
+	va_end(args);
+	html(buf.buf);
+	strbuf_release(&buf);
+}
+
+void html_txtf(const char *format, ...)
+{
+	va_list args;
+
+	va_start(args, format);
+	html_vtxtf(format, args);
+	va_end(args);
+}
+
+void html_vtxtf(const char *format, va_list ap)
+{
+	va_list cp;
+	struct strbuf buf = STRBUF_INIT;
+
+	va_copy(cp, ap);
+	strbuf_vaddf(&buf, format, cp);
+	va_end(cp);
+	html_txt(buf.buf);
+	strbuf_release(&buf);
+}
+
+void html_txt(const char *txt)
+{
+	if (txt)
+		html_ntxt(txt, strlen(txt));
+}
+
+ssize_t html_ntxt(const char *txt, size_t len)
+{
+	const char *t = txt;
+	ssize_t slen;
+
+	if (len > SSIZE_MAX)
+		return -1;
+
+	slen = (ssize_t) len;
+	while (t && *t && slen--) {
+		int c = *t;
+		if (c == '<' || c == '>' || c == '&') {
+			html_raw(txt, t - txt);
+			if (c == '>')
+				html("&gt;");
+			else if (c == '<')
+				html("&lt;");
+			else if (c == '&')
+				html("&amp;");
+			txt = t + 1;
+		}
+		t++;
+	}
+	if (t != txt)
+		html_raw(txt, t - txt);
+	return slen;
+}
+
+void html_attrf(const char *fmt, ...)
+{
+	va_list ap;
+	struct strbuf sb = STRBUF_INIT;
+
+	va_start(ap, fmt);
+	strbuf_vaddf(&sb, fmt, ap);
+	va_end(ap);
+
+	html_attr(sb.buf);
+	strbuf_release(&sb);
+}
+
+void html_attr(const char *txt)
+{
+	const char *t = txt;
+	while (t && *t) {
+		int c = *t;
+		if (c == '<' || c == '>' || c == '\'' || c == '\"' || c == '&') {
+			html_raw(txt, t - txt);
+			if (c == '>')
+				html("&gt;");
+			else if (c == '<')
+				html("&lt;");
+			else if (c == '\'')
+				html("&#x27;");
+			else if (c == '"')
+				html("&quot;");
+			else if (c == '&')
+				html("&amp;");
+			txt = t + 1;
+		}
+		t++;
+	}
+	if (t != txt)
+		html(txt);
+}
+
+void html_url_path(const char *txt)
+{
+	const char *t = txt;
+	while (t && *t) {
+		unsigned char c = *t;
+		const char *e = url_escape_table[c];
+		if (e && c != '+' && c != '&') {
+			html_raw(txt, t - txt);
+			html(e);
+			txt = t + 1;
+		}
+		t++;
+	}
+	if (t != txt)
+		html(txt);
+}
+
+void html_url_arg(const char *txt)
+{
+	const char *t = txt;
+	while (t && *t) {
+		unsigned char c = *t;
+		const char *e = url_escape_table[c];
+		if (c == ' ')
+			e = "+";
+		if (e) {
+			html_raw(txt, t - txt);
+			html(e);
+			txt = t + 1;
+		}
+		t++;
+	}
+	if (t != txt)
+		html(txt);
+}
+
+void html_header_arg_in_quotes(const char *txt)
+{
+	const char *t = txt;
+	while (t && *t) {
+		unsigned char c = *t;
+		const char *e = NULL;
+		if (c == '\\')
+			e = "\\\\";
+		else if (c == '\r')
+			e = "\\r";
+		else if (c == '\n')
+			e = "\\n";
+		else if (c == '"')
+			e = "\\\"";
+		if (e) {
+			html_raw(txt, t - txt);
+			html(e);
+			txt = t + 1;
+		}
+		t++;
+	}
+	if (t != txt)
+		html(txt);
+
+}
+
+void html_hidden(const char *name, const char *value)
+{
+	html("<input type='hidden' name='");
+	html_attr(name);
+	html("' value='");
+	html_attr(value);
+	html("'/>");
+}
+
+void html_option(const char *value, const char *text, const char *selected_value)
+{
+	html("<option value='");
+	html_attr(value);
+	html("'");
+	if (selected_value && !strcmp(selected_value, value))
+		html(" selected='selected'");
+	html(">");
+	html_txt(text);
+	html("</option>\n");
+}
+
+void html_intoption(int value, const char *text, int selected_value)
+{
+	htmlf("<option value='%d'%s>", value,
+	      value == selected_value ? " selected='selected'" : "");
+	html_txt(text);
+	html("</option>");
+}
+
+void html_link_open(const char *url, const char *title, const char *class)
+{
+	html("<a href='");
+	html_attr(url);
+	if (title) {
+		html("' title='");
+		html_attr(title);
+	}
+	if (class) {
+		html("' class='");
+		html_attr(class);
+	}
+	html("'>");
+}
+
+void html_link_close(void)
+{
+	html("</a>");
+}
+
+void html_fileperm(unsigned short mode)
+{
+	htmlf("%c%c%c", (mode & 4 ? 'r' : '-'),
+	      (mode & 2 ? 'w' : '-'), (mode & 1 ? 'x' : '-'));
+}
+
+int html_include(const char *filename)
+{
+	FILE *f;
+	char buf[4096];
+	size_t len;
+
+	if (!(f = fopen(filename, "r"))) {
+		fprintf(stderr, "[cgit] Failed to include file %s: %s (%d).\n",
+			filename, strerror(errno), errno);
+		return -1;
+	}
+	while ((len = fread(buf, 1, 4096, f)) > 0)
+		html_raw(buf, len);
+	fclose(f);
+	return 0;
+}
+
+void http_parse_querystring(const char *txt, void (*fn)(const char *name, const char *value))
+{
+	const char *t = txt;
+
+	while (t && *t) {
+		char *name = url_decode_parameter_name(&t);
+		if (*name) {
+			char *value = url_decode_parameter_value(&t);
+			fn(name, value);
+			free(value);
+		}
+		free(name);
+	}
+}
diff --git a/third_party/cgit/html.h b/third_party/cgit/html.h
new file mode 100644
index 0000000000..fa4de77587
--- /dev/null
+++ b/third_party/cgit/html.h
@@ -0,0 +1,37 @@
+#ifndef HTML_H
+#define HTML_H
+
+#include "cgit.h"
+
+extern void html_raw(const char *txt, size_t size);
+extern void html(const char *txt);
+
+__attribute__((format (printf,1,2)))
+extern void htmlf(const char *format,...);
+
+__attribute__((format (printf,1,2)))
+extern void html_txtf(const char *format,...);
+
+__attribute__((format (printf,1,0)))
+extern void html_vtxtf(const char *format, va_list ap);
+
+__attribute__((format (printf,1,2)))
+extern void html_attrf(const char *format,...);
+
+extern void html_txt(const char *txt);
+extern ssize_t html_ntxt(const char *txt, size_t len);
+extern void html_attr(const char *txt);
+extern void html_url_path(const char *txt);
+extern void html_url_arg(const char *txt);
+extern void html_header_arg_in_quotes(const char *txt);
+extern void html_hidden(const char *name, const char *value);
+extern void html_option(const char *value, const char *text, const char *selected_value);
+extern void html_intoption(int value, const char *text, int selected_value);
+extern void html_link_open(const char *url, const char *title, const char *class);
+extern void html_link_close(void);
+extern void html_fileperm(unsigned short mode);
+extern int html_include(const char *filename);
+
+extern void http_parse_querystring(const char *txt, void (*fn)(const char *name, const char *value));
+
+#endif /* HTML_H */
diff --git a/third_party/cgit/parsing.c b/third_party/cgit/parsing.c
new file mode 100644
index 0000000000..e093aaf701
--- /dev/null
+++ b/third_party/cgit/parsing.c
@@ -0,0 +1,223 @@
+/* parsing.c: parsing of config files
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+
+/*
+ * url syntax: [repo ['/' cmd [ '/' path]]]
+ *   repo: any valid repo url, may contain '/'
+ *   cmd:  log | commit | diff | tree | view | blob | snapshot
+ *   path: any valid path, may contain '/'
+ *
+ */
+void cgit_parse_url(const char *url)
+{
+	char *c, *cmd, *p;
+	struct cgit_repo *repo;
+
+	if (!url || url[0] == '\0')
+		return;
+
+	ctx.qry.page = NULL;
+	ctx.repo = cgit_get_repoinfo(url);
+	if (ctx.repo) {
+		ctx.qry.repo = ctx.repo->url;
+		return;
+	}
+
+	cmd = NULL;
+	c = strchr(url, '/');
+	while (c) {
+		c[0] = '\0';
+		repo = cgit_get_repoinfo(url);
+		if (repo) {
+			ctx.repo = repo;
+			cmd = c;
+		}
+		c[0] = '/';
+		c = strchr(c + 1, '/');
+	}
+
+	if (ctx.repo) {
+		ctx.qry.repo = ctx.repo->url;
+		p = strchr(cmd + 1, '/');
+		if (p) {
+			p[0] = '\0';
+			if (p[1])
+				ctx.qry.path = trim_end(p + 1, '/');
+		}
+		if (cmd[1])
+			ctx.qry.page = xstrdup(cmd + 1);
+	}
+}
+
+static char *substr(const char *head, const char *tail)
+{
+	char *buf;
+
+	if (tail < head)
+		return xstrdup("");
+	buf = xmalloc(tail - head + 1);
+	strlcpy(buf, head, tail - head + 1);
+	return buf;
+}
+
+static void parse_user(const char *t, char **name, char **email, unsigned long *date, int *tz)
+{
+	struct ident_split ident;
+	unsigned email_len;
+
+	if (!split_ident_line(&ident, t, (uintptr_t)strchrnul(t, '\n') - (uintptr_t)t)) {
+		*name = substr(ident.name_begin, ident.name_end);
+
+		email_len = ident.mail_end - ident.mail_begin;
+		*email = xmalloc(strlen("<") + email_len + strlen(">") + 1);
+		xsnprintf(*email, email_len + 3, "<%.*s>", email_len, ident.mail_begin);
+
+		if (ident.date_begin)
+			*date = strtoul(ident.date_begin, NULL, 10);
+		if (ident.tz_begin)
+			*tz = atoi(ident.tz_begin);
+	}
+}
+
+#ifdef NO_ICONV
+#define reencode(a, b, c)
+#else
+static const char *reencode(char **txt, const char *src_enc, const char *dst_enc)
+{
+	char *tmp;
+
+	if (!txt)
+		return NULL;
+
+	if (!*txt || !src_enc || !dst_enc)
+		return *txt;
+
+	/* no encoding needed if src_enc equals dst_enc */
+	if (!strcasecmp(src_enc, dst_enc))
+		return *txt;
+
+	tmp = reencode_string(*txt, dst_enc, src_enc);
+	if (tmp) {
+		free(*txt);
+		*txt = tmp;
+	}
+	return *txt;
+}
+#endif
+
+static const char *next_header_line(const char *p)
+{
+	p = strchr(p, '\n');
+	if (!p)
+		return NULL;
+	return p + 1;
+}
+
+static int end_of_header(const char *p)
+{
+	return !p || (*p == '\n');
+}
+
+struct commitinfo *cgit_parse_commit(struct commit *commit)
+{
+	struct commitinfo *ret;
+	const char *p = repo_get_commit_buffer(the_repository, commit, NULL);
+	const char *t;
+
+	ret = xcalloc(1, sizeof(struct commitinfo));
+	ret->commit = commit;
+
+	if (!p)
+		return ret;
+
+	if (!skip_prefix(p, "tree ", &p))
+		die("Bad commit: %s", oid_to_hex(&commit->object.oid));
+	p += the_hash_algo->hexsz + 1;
+
+	while (skip_prefix(p, "parent ", &p))
+		p += the_hash_algo->hexsz + 1;
+
+	if (p && skip_prefix(p, "author ", &p)) {
+		parse_user(p, &ret->author, &ret->author_email,
+			&ret->author_date, &ret->author_tz);
+		p = next_header_line(p);
+	}
+
+	if (p && skip_prefix(p, "committer ", &p)) {
+		parse_user(p, &ret->committer, &ret->committer_email,
+			&ret->committer_date, &ret->committer_tz);
+		p = next_header_line(p);
+	}
+
+	if (p && skip_prefix(p, "encoding ", &p)) {
+		t = strchr(p, '\n');
+		if (t) {
+			ret->msg_encoding = substr(p, t + 1);
+			p = t + 1;
+		}
+	}
+
+	if (!ret->msg_encoding)
+		ret->msg_encoding = xstrdup("UTF-8");
+
+	while (!end_of_header(p))
+		p = next_header_line(p);
+	while (p && *p == '\n')
+		p++;
+	if (!p)
+		return ret;
+
+	t = strchrnul(p, '\n');
+	ret->subject = substr(p, t);
+	while (*t == '\n')
+		t++;
+	ret->msg = xstrdup(t);
+
+	reencode(&ret->author, ret->msg_encoding, PAGE_ENCODING);
+	reencode(&ret->author_email, ret->msg_encoding, PAGE_ENCODING);
+	reencode(&ret->committer, ret->msg_encoding, PAGE_ENCODING);
+	reencode(&ret->committer_email, ret->msg_encoding, PAGE_ENCODING);
+	reencode(&ret->subject, ret->msg_encoding, PAGE_ENCODING);
+	reencode(&ret->msg, ret->msg_encoding, PAGE_ENCODING);
+
+	return ret;
+}
+
+struct taginfo *cgit_parse_tag(struct tag *tag)
+{
+	void *data;
+	enum object_type type;
+	unsigned long size;
+	const char *p;
+	struct taginfo *ret = NULL;
+
+	data = read_object_file(&tag->object.oid, &type, &size);
+	if (!data || type != OBJ_TAG)
+		goto cleanup;
+
+	ret = xcalloc(1, sizeof(struct taginfo));
+
+	for (p = data; !end_of_header(p); p = next_header_line(p)) {
+		if (skip_prefix(p, "tagger ", &p)) {
+			parse_user(p, &ret->tagger, &ret->tagger_email,
+				&ret->tagger_date, &ret->tagger_tz);
+		}
+	}
+
+	while (p && *p == '\n')
+		p++;
+
+	if (p && *p)
+		ret->msg = xstrdup(p);
+
+cleanup:
+	free(data);
+	return ret;
+}
diff --git a/third_party/cgit/robots.txt b/third_party/cgit/robots.txt
new file mode 100644
index 0000000000..1b33266d53
--- /dev/null
+++ b/third_party/cgit/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Disallow: /*/snapshot/*
+Disallow: /*/blame/*
+Allow: /
diff --git a/third_party/cgit/scan-tree.c b/third_party/cgit/scan-tree.c
new file mode 100644
index 0000000000..1e3b43debf
--- /dev/null
+++ b/third_party/cgit/scan-tree.c
@@ -0,0 +1,268 @@
+/* scan-tree.c
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "scan-tree.h"
+#include "configfile.h"
+#include "html.h"
+#include <config.h>
+
+/* return 1 if path contains a objects/ directory and a HEAD file */
+static int is_git_dir(const char *path)
+{
+	struct stat st;
+	struct strbuf pathbuf = STRBUF_INIT;
+	int result = 0;
+
+	strbuf_addf(&pathbuf, "%s/objects", path);
+	if (stat(pathbuf.buf, &st)) {
+		if (errno != ENOENT)
+			fprintf(stderr, "Error checking path %s: %s (%d)\n",
+				path, strerror(errno), errno);
+		goto out;
+	}
+	if (!S_ISDIR(st.st_mode))
+		goto out;
+
+	strbuf_reset(&pathbuf);
+	strbuf_addf(&pathbuf, "%s/HEAD", path);
+	if (stat(pathbuf.buf, &st)) {
+		if (errno != ENOENT)
+			fprintf(stderr, "Error checking path %s: %s (%d)\n",
+				path, strerror(errno), errno);
+		goto out;
+	}
+	if (!S_ISREG(st.st_mode))
+		goto out;
+
+	result = 1;
+out:
+	strbuf_release(&pathbuf);
+	return result;
+}
+
+static struct cgit_repo *repo;
+static repo_config_fn config_fn;
+
+static void scan_tree_repo_config(const char *name, const char *value)
+{
+	config_fn(repo, name, value);
+}
+
+static int gitconfig_config(const char *key, const char *value, void *cb)
+{
+	const char *name;
+
+	if (!strcmp(key, "gitweb.owner"))
+		config_fn(repo, "owner", value);
+	else if (!strcmp(key, "gitweb.description"))
+		config_fn(repo, "desc", value);
+	else if (!strcmp(key, "gitweb.category"))
+		config_fn(repo, "section", value);
+	else if (!strcmp(key, "gitweb.homepage"))
+		config_fn(repo, "homepage", value);
+	else if (skip_prefix(key, "cgit.", &name))
+		config_fn(repo, name, value);
+
+	return 0;
+}
+
+static char *xstrrchr(char *s, char *from, int c)
+{
+	while (from >= s && *from != c)
+		from--;
+	return from < s ? NULL : from;
+}
+
+static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn)
+{
+	struct stat st;
+	struct passwd *pwd;
+	size_t pathlen;
+	struct strbuf rel = STRBUF_INIT;
+	char *p, *slash;
+	int n;
+	size_t size;
+
+	if (stat(path->buf, &st)) {
+		fprintf(stderr, "Error accessing %s: %s (%d)\n",
+			path->buf, strerror(errno), errno);
+		return;
+	}
+
+	strbuf_addch(path, '/');
+	pathlen = path->len;
+
+	if (ctx.cfg.strict_export) {
+		strbuf_addstr(path, ctx.cfg.strict_export);
+		if(stat(path->buf, &st))
+			return;
+		strbuf_setlen(path, pathlen);
+	}
+
+	strbuf_addstr(path, "noweb");
+	if (!stat(path->buf, &st))
+		return;
+	strbuf_setlen(path, pathlen);
+
+	if (!starts_with(path->buf, base))
+		strbuf_addbuf(&rel, path);
+	else
+		strbuf_addstr(&rel, path->buf + strlen(base) + 1);
+
+	if (!strcmp(rel.buf + rel.len - 5, "/.git"))
+		strbuf_setlen(&rel, rel.len - 5);
+	else if (rel.len && rel.buf[rel.len - 1] == '/')
+		strbuf_setlen(&rel, rel.len - 1);
+
+	repo = cgit_add_repo(rel.buf);
+	config_fn = fn;
+	if (ctx.cfg.enable_git_config) {
+		strbuf_addstr(path, "config");
+		git_config_from_file(gitconfig_config, path->buf, NULL);
+		strbuf_setlen(path, pathlen);
+	}
+
+	if (ctx.cfg.remove_suffix) {
+		size_t urllen;
+		strip_suffix(repo->url, ".git", &urllen);
+		strip_suffix_mem(repo->url, &urllen, "/");
+		repo->url[urllen] = '\0';
+	}
+	repo->path = xstrdup(path->buf);
+	while (!repo->owner) {
+		if ((pwd = getpwuid(st.st_uid)) == NULL) {
+			break;
+		}
+		if (pwd->pw_gecos)
+			if ((p = strchr(pwd->pw_gecos, ',')))
+				*p = '\0';
+		repo->owner = xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name);
+	}
+
+	if (repo->desc == cgit_default_repo_desc || !repo->desc) {
+		strbuf_addstr(path, "description");
+		if (!stat(path->buf, &st))
+			readfile(path->buf, &repo->desc, &size);
+		strbuf_setlen(path, pathlen);
+	}
+
+	if (ctx.cfg.section_from_path) {
+		n = ctx.cfg.section_from_path;
+		if (n > 0) {
+			slash = rel.buf - 1;
+			while (slash && n && (slash = strchr(slash + 1, '/')))
+				n--;
+		} else {
+			slash = rel.buf + rel.len;
+			while (slash && n && (slash = xstrrchr(rel.buf, slash - 1, '/')))
+				n++;
+		}
+		if (slash && !n) {
+			*slash = '\0';
+			repo->section = xstrdup(rel.buf);
+			*slash = '/';
+			if (starts_with(repo->name, repo->section)) {
+				repo->name += strlen(repo->section);
+				if (*repo->name == '/')
+					repo->name++;
+			}
+		}
+	}
+
+	strbuf_addstr(path, "cgitrc");
+	if (!stat(path->buf, &st))
+		parse_configfile(path->buf, &scan_tree_repo_config);
+
+	strbuf_release(&rel);
+}
+
+static void scan_path(const char *base, const char *path, repo_config_fn fn)
+{
+	DIR *dir = opendir(path);
+	struct dirent *ent;
+	struct strbuf pathbuf = STRBUF_INIT;
+	size_t pathlen = strlen(path);
+	struct stat st;
+
+	if (!dir) {
+		fprintf(stderr, "Error opening directory %s: %s (%d)\n",
+			path, strerror(errno), errno);
+		return;
+	}
+
+	strbuf_add(&pathbuf, path, strlen(path));
+	if (is_git_dir(pathbuf.buf)) {
+		add_repo(base, &pathbuf, fn);
+		goto end;
+	}
+	strbuf_addstr(&pathbuf, "/.git");
+	if (is_git_dir(pathbuf.buf)) {
+		add_repo(base, &pathbuf, fn);
+		goto end;
+	}
+	/*
+	 * Add one because we don't want to lose the trailing '/' when we
+	 * reset the length of pathbuf in the loop below.
+	 */
+	pathlen++;
+	while ((ent = readdir(dir)) != NULL) {
+		if (ent->d_name[0] == '.') {
+			if (ent->d_name[1] == '\0')
+				continue;
+			if (ent->d_name[1] == '.' && ent->d_name[2] == '\0')
+				continue;
+			if (!ctx.cfg.scan_hidden_path)
+				continue;
+		}
+		strbuf_setlen(&pathbuf, pathlen);
+		strbuf_addstr(&pathbuf, ent->d_name);
+		if (stat(pathbuf.buf, &st)) {
+			fprintf(stderr, "Error checking path %s: %s (%d)\n",
+				pathbuf.buf, strerror(errno), errno);
+			continue;
+		}
+		if (S_ISDIR(st.st_mode))
+			scan_path(base, pathbuf.buf, fn);
+	}
+end:
+	strbuf_release(&pathbuf);
+	closedir(dir);
+}
+
+void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn)
+{
+	struct strbuf line = STRBUF_INIT;
+	FILE *projects;
+	int err;
+
+	projects = fopen(projectsfile, "r");
+	if (!projects) {
+		fprintf(stderr, "Error opening projectsfile %s: %s (%d)\n",
+			projectsfile, strerror(errno), errno);
+		return;
+	}
+	while (strbuf_getline(&line, projects) != EOF) {
+		if (!line.len)
+			continue;
+		strbuf_insert(&line, 0, "/", 1);
+		strbuf_insert(&line, 0, path, strlen(path));
+		scan_path(path, line.buf, fn);
+	}
+	if ((err = ferror(projects))) {
+		fprintf(stderr, "Error reading from projectsfile %s: %s (%d)\n",
+			projectsfile, strerror(err), err);
+	}
+	fclose(projects);
+	strbuf_release(&line);
+}
+
+void scan_tree(const char *path, repo_config_fn fn)
+{
+	scan_path(path, path, fn);
+}
diff --git a/third_party/cgit/scan-tree.h b/third_party/cgit/scan-tree.h
new file mode 100644
index 0000000000..1afbd4bbcd
--- /dev/null
+++ b/third_party/cgit/scan-tree.h
@@ -0,0 +1,2 @@
+extern void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn);
+extern void scan_tree(const char *path, repo_config_fn fn);
diff --git a/third_party/cgit/shared.c b/third_party/cgit/shared.c
new file mode 100644
index 0000000000..0bceb98912
--- /dev/null
+++ b/third_party/cgit/shared.c
@@ -0,0 +1,577 @@
+/* shared.c: global vars + some callback functions
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+
+struct cgit_repolist cgit_repolist;
+struct cgit_context ctx;
+
+int chk_zero(int result, char *msg)
+{
+	if (result != 0)
+		die_errno("%s", msg);
+	return result;
+}
+
+int chk_positive(int result, char *msg)
+{
+	if (result <= 0)
+		die_errno("%s", msg);
+	return result;
+}
+
+int chk_non_negative(int result, char *msg)
+{
+	if (result < 0)
+		die_errno("%s", msg);
+	return result;
+}
+
+char *cgit_default_repo_desc = "[no description]";
+struct cgit_repo *cgit_add_repo(const char *url)
+{
+	struct cgit_repo *ret;
+
+	if (++cgit_repolist.count > cgit_repolist.length) {
+		if (cgit_repolist.length == 0)
+			cgit_repolist.length = 8;
+		else
+			cgit_repolist.length *= 2;
+		cgit_repolist.repos = xrealloc(cgit_repolist.repos,
+					       cgit_repolist.length *
+					       sizeof(struct cgit_repo));
+	}
+
+	ret = &cgit_repolist.repos[cgit_repolist.count-1];
+	memset(ret, 0, sizeof(struct cgit_repo));
+	ret->url = trim_end(url, '/');
+	ret->name = ret->url;
+	ret->path = NULL;
+	ret->desc = cgit_default_repo_desc;
+	ret->extra_head_content = NULL;
+	ret->owner = NULL;
+	ret->homepage = NULL;
+	ret->section = ctx.cfg.section;
+	ret->snapshots = ctx.cfg.snapshots;
+	ret->enable_blame = ctx.cfg.enable_blame;
+	ret->enable_commit_graph = ctx.cfg.enable_commit_graph;
+	ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
+	ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
+	ret->enable_remote_branches = ctx.cfg.enable_remote_branches;
+	ret->enable_subject_links = ctx.cfg.enable_subject_links;
+	ret->enable_html_serving = ctx.cfg.enable_html_serving;
+	ret->max_stats = ctx.cfg.max_stats;
+	ret->branch_sort = ctx.cfg.branch_sort;
+	ret->commit_sort = ctx.cfg.commit_sort;
+	ret->module_link = ctx.cfg.module_link;
+	ret->readme = ctx.cfg.readme;
+	ret->mtime = -1;
+	ret->about_filter = ctx.cfg.about_filter;
+	ret->commit_filter = ctx.cfg.commit_filter;
+	ret->source_filter = ctx.cfg.source_filter;
+	ret->email_filter = ctx.cfg.email_filter;
+	ret->owner_filter = ctx.cfg.owner_filter;
+	ret->clone_url = ctx.cfg.clone_url;
+	ret->submodules.strdup_strings = 1;
+	ret->hide = ret->ignore = 0;
+	return ret;
+}
+
+struct cgit_repo *cgit_get_repoinfo(const char *url)
+{
+	int i;
+	struct cgit_repo *repo;
+
+	for (i = 0; i < cgit_repolist.count; i++) {
+		repo = &cgit_repolist.repos[i];
+		if (repo->ignore)
+			continue;
+		if (!strcmp(repo->url, url))
+			return repo;
+	}
+	return NULL;
+}
+
+void cgit_free_commitinfo(struct commitinfo *info)
+{
+	free(info->author);
+	free(info->author_email);
+	free(info->committer);
+	free(info->committer_email);
+	free(info->subject);
+	free(info->msg);
+	free(info->msg_encoding);
+	free(info);
+}
+
+char *trim_end(const char *str, char c)
+{
+	int len;
+
+	if (str == NULL)
+		return NULL;
+	len = strlen(str);
+	while (len > 0 && str[len - 1] == c)
+		len--;
+	if (len == 0)
+		return NULL;
+	return xstrndup(str, len);
+}
+
+char *ensure_end(const char *str, char c)
+{
+	size_t len = strlen(str);
+	char *result;
+
+	if (len && str[len - 1] == c)
+		return xstrndup(str, len);
+
+	result = xmalloc(len + 2);
+	memcpy(result, str, len);
+	result[len] = '/';
+	result[len + 1] = '\0';
+	return result;
+}
+
+void strbuf_ensure_end(struct strbuf *sb, char c)
+{
+	if (!sb->len || sb->buf[sb->len - 1] != c)
+		strbuf_addch(sb, c);
+}
+
+void cgit_add_ref(struct reflist *list, struct refinfo *ref)
+{
+	size_t size;
+
+	if (list->count >= list->alloc) {
+		list->alloc += (list->alloc ? list->alloc : 4);
+		size = list->alloc * sizeof(struct refinfo *);
+		list->refs = xrealloc(list->refs, size);
+	}
+	list->refs[list->count++] = ref;
+}
+
+static struct refinfo *cgit_mk_refinfo(const char *refname, const struct object_id *oid)
+{
+	struct refinfo *ref;
+
+	ref = xmalloc(sizeof (struct refinfo));
+	ref->refname = xstrdup(refname);
+	ref->object = parse_object(the_repository, oid);
+	switch (ref->object->type) {
+	case OBJ_TAG:
+		ref->tag = cgit_parse_tag((struct tag *)ref->object);
+		break;
+	case OBJ_COMMIT:
+		ref->commit = cgit_parse_commit((struct commit *)ref->object);
+		break;
+	}
+	return ref;
+}
+
+void cgit_free_taginfo(struct taginfo *tag)
+{
+	if (tag->tagger)
+		free(tag->tagger);
+	if (tag->tagger_email)
+		free(tag->tagger_email);
+	if (tag->msg)
+		free(tag->msg);
+	free(tag);
+}
+
+static void cgit_free_refinfo(struct refinfo *ref)
+{
+	if (ref->refname)
+		free((char *)ref->refname);
+	switch (ref->object->type) {
+	case OBJ_TAG:
+		cgit_free_taginfo(ref->tag);
+		break;
+	case OBJ_COMMIT:
+		cgit_free_commitinfo(ref->commit);
+		break;
+	}
+	free(ref);
+}
+
+void cgit_free_reflist_inner(struct reflist *list)
+{
+	int i;
+
+	for (i = 0; i < list->count; i++) {
+		cgit_free_refinfo(list->refs[i]);
+	}
+	free(list->refs);
+}
+
+int cgit_refs_cb(const char *refname, const struct object_id *oid, int flags,
+		  void *cb_data)
+{
+	struct reflist *list = (struct reflist *)cb_data;
+	struct refinfo *info = cgit_mk_refinfo(refname, oid);
+
+	if (info)
+		cgit_add_ref(list, info);
+	return 0;
+}
+
+void cgit_diff_tree_cb(struct diff_queue_struct *q,
+		       struct diff_options *options, void *data)
+{
+	int i;
+
+	for (i = 0; i < q->nr; i++) {
+		if (q->queue[i]->status == 'U')
+			continue;
+		((filepair_fn)data)(q->queue[i]);
+	}
+}
+
+static int load_mmfile(mmfile_t *file, const struct object_id *oid)
+{
+	enum object_type type;
+
+	if (is_null_oid(oid)) {
+		file->ptr = (char *)"";
+		file->size = 0;
+	} else {
+		file->ptr = read_object_file(oid, &type,
+		                           (unsigned long *)&file->size);
+	}
+	return 1;
+}
+
+/*
+ * Receive diff-buffers from xdiff and concatenate them as
+ * needed across multiple callbacks.
+ *
+ * This is basically a copy of xdiff-interface.c/xdiff_outf(),
+ * ripped from git and modified to use globals instead of
+ * a special callback-struct.
+ */
+static char *diffbuf = NULL;
+static int buflen = 0;
+
+static int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf)
+{
+	int i;
+
+	for (i = 0; i < nbuf; i++) {
+		if (mb[i].ptr[mb[i].size-1] != '\n') {
+			/* Incomplete line */
+			diffbuf = xrealloc(diffbuf, buflen + mb[i].size);
+			memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size);
+			buflen += mb[i].size;
+			continue;
+		}
+
+		/* we have a complete line */
+		if (!diffbuf) {
+			((linediff_fn)priv)(mb[i].ptr, mb[i].size);
+			continue;
+		}
+		diffbuf = xrealloc(diffbuf, buflen + mb[i].size);
+		memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size);
+		((linediff_fn)priv)(diffbuf, buflen + mb[i].size);
+		free(diffbuf);
+		diffbuf = NULL;
+		buflen = 0;
+	}
+	if (diffbuf) {
+		((linediff_fn)priv)(diffbuf, buflen);
+		free(diffbuf);
+		diffbuf = NULL;
+		buflen = 0;
+	}
+	return 0;
+}
+
+int cgit_diff_files(const struct object_id *old_oid,
+		    const struct object_id *new_oid, unsigned long *old_size,
+		    unsigned long *new_size, int *binary, int context,
+		    int ignorews, linediff_fn fn)
+{
+	mmfile_t file1, file2;
+	xpparam_t diff_params;
+	xdemitconf_t emit_params;
+	xdemitcb_t emit_cb;
+
+	if (!load_mmfile(&file1, old_oid) || !load_mmfile(&file2, new_oid))
+		return 1;
+
+	*old_size = file1.size;
+	*new_size = file2.size;
+
+	if ((file1.ptr && buffer_is_binary(file1.ptr, file1.size)) ||
+	    (file2.ptr && buffer_is_binary(file2.ptr, file2.size))) {
+		*binary = 1;
+		if (file1.size)
+			free(file1.ptr);
+		if (file2.size)
+			free(file2.ptr);
+		return 0;
+	}
+
+	memset(&diff_params, 0, sizeof(diff_params));
+	memset(&emit_params, 0, sizeof(emit_params));
+	memset(&emit_cb, 0, sizeof(emit_cb));
+	diff_params.flags = XDF_NEED_MINIMAL;
+	if (ignorews)
+		diff_params.flags |= XDF_IGNORE_WHITESPACE;
+	emit_params.ctxlen = context > 0 ? context : 3;
+	emit_params.flags = XDL_EMIT_FUNCNAMES;
+	emit_cb.out_line = filediff_cb;
+	emit_cb.priv = fn;
+	xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb);
+	if (file1.size)
+		free(file1.ptr);
+	if (file2.size)
+		free(file2.ptr);
+	return 0;
+}
+
+void cgit_diff_tree(const struct object_id *old_oid,
+		    const struct object_id *new_oid,
+		    filepair_fn fn, const char *prefix, int ignorews)
+{
+	struct diff_options opt;
+	struct pathspec_item *item;
+
+	diff_setup(&opt);
+	opt.output_format = DIFF_FORMAT_CALLBACK;
+	opt.detect_rename = 1;
+	opt.rename_limit = ctx.cfg.renamelimit;
+	opt.flags.recursive = 1;
+	if (ignorews)
+		DIFF_XDL_SET(&opt, IGNORE_WHITESPACE);
+	opt.format_callback = cgit_diff_tree_cb;
+	opt.format_callback_data = fn;
+	if (prefix) {
+		item = xcalloc(1, sizeof(*item));
+		item->match = xstrdup(prefix);
+		item->len = strlen(prefix);
+		opt.pathspec.nr = 1;
+		opt.pathspec.items = item;
+	}
+	diff_setup_done(&opt);
+
+	if (old_oid && !is_null_oid(old_oid))
+		diff_tree_oid(old_oid, new_oid, "", &opt);
+	else
+		diff_root_tree_oid(new_oid, "", &opt);
+	diffcore_std(&opt);
+	diff_flush(&opt);
+}
+
+void cgit_diff_commit(struct commit *commit, filepair_fn fn, const char *prefix)
+{
+	const struct object_id *old_oid = NULL;
+
+	if (commit->parents)
+		old_oid = &commit->parents->item->object.oid;
+	cgit_diff_tree(old_oid, &commit->object.oid, fn, prefix,
+		       ctx.qry.ignorews);
+}
+
+int cgit_parse_snapshots_mask(const char *str)
+{
+	struct string_list tokens = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	const struct cgit_snapshot_format *f;
+	int rv = 0;
+
+	/* favor legacy setting */
+	if (atoi(str))
+		return 1;
+
+	if (strcmp(str, "all") == 0)
+		return INT_MAX;
+
+	string_list_split(&tokens, str, ' ', -1);
+	string_list_remove_empty_items(&tokens, 0);
+
+	for_each_string_list_item(item, &tokens) {
+		for (f = cgit_snapshot_formats; f->suffix; f++) {
+			if (!strcmp(item->string, f->suffix) ||
+			    !strcmp(item->string, f->suffix + 1)) {
+				rv |= cgit_snapshot_format_bit(f);
+				break;
+			}
+		}
+	}
+
+	string_list_clear(&tokens, 0);
+	return rv;
+}
+
+typedef struct {
+	char * name;
+	char * value;
+} cgit_env_var;
+
+void cgit_prepare_repo_env(struct cgit_repo * repo)
+{
+	cgit_env_var env_vars[] = {
+		{ .name = "CGIT_REPO_URL", .value = repo->url },
+		{ .name = "CGIT_REPO_NAME", .value = repo->name },
+		{ .name = "CGIT_REPO_PATH", .value = repo->path },
+		{ .name = "CGIT_REPO_OWNER", .value = repo->owner },
+		{ .name = "CGIT_REPO_DEFBRANCH", .value = repo->defbranch },
+		{ .name = "CGIT_REPO_SECTION", .value = repo->section },
+		{ .name = "CGIT_REPO_CLONE_URL", .value = repo->clone_url }
+	};
+	int env_var_count = ARRAY_SIZE(env_vars);
+	cgit_env_var *p, *q;
+	static char *warn = "cgit warning: failed to set env: %s=%s\n";
+
+	p = env_vars;
+	q = p + env_var_count;
+	for (; p < q; p++)
+		if (p->value && setenv(p->name, p->value, 1))
+			fprintf(stderr, warn, p->name, p->value);
+}
+
+/* Read the content of the specified file into a newly allocated buffer,
+ * zeroterminate the buffer and return 0 on success, errno otherwise.
+ */
+int readfile(const char *path, char **buf, size_t *size)
+{
+	int fd, e;
+	struct stat st;
+
+	fd = open(path, O_RDONLY);
+	if (fd == -1)
+		return errno;
+	if (fstat(fd, &st)) {
+		e = errno;
+		close(fd);
+		return e;
+	}
+	if (!S_ISREG(st.st_mode)) {
+		close(fd);
+		return EISDIR;
+	}
+	*buf = xmalloc(st.st_size + 1);
+	*size = read_in_full(fd, *buf, st.st_size);
+	e = errno;
+	(*buf)[*size] = '\0';
+	close(fd);
+	return (*size == st.st_size ? 0 : e);
+}
+
+static int is_token_char(char c)
+{
+	return isalnum(c) || c == '_';
+}
+
+/* Replace name with getenv(name), return pointer to zero-terminating char
+ */
+static char *expand_macro(char *name, int maxlength)
+{
+	char *value;
+	size_t len;
+
+	len = 0;
+	value = getenv(name);
+	if (value) {
+		len = strlen(value) + 1;
+		if (len > maxlength)
+			len = maxlength;
+		strlcpy(name, value, len);
+		--len;
+	}
+	return name + len;
+}
+
+#define EXPBUFSIZE (1024 * 8)
+
+/* Replace all tokens prefixed by '$' in the specified text with the
+ * value of the named environment variable.
+ * NB: the return value is a static buffer, i.e. it must be strdup'd
+ * by the caller.
+ */
+char *expand_macros(const char *txt)
+{
+	static char result[EXPBUFSIZE];
+	char *p, *start;
+	int len;
+
+	p = result;
+	start = NULL;
+	while (p < result + EXPBUFSIZE - 1 && txt && *txt) {
+		*p = *txt;
+		if (start) {
+			if (!is_token_char(*txt)) {
+				if (p - start > 0) {
+					*p = '\0';
+					len = result + EXPBUFSIZE - start - 1;
+					p = expand_macro(start, len) - 1;
+				}
+				start = NULL;
+				txt--;
+			}
+			p++;
+			txt++;
+			continue;
+		}
+		if (*txt == '$') {
+			start = p;
+			txt++;
+			continue;
+		}
+		p++;
+		txt++;
+	}
+	*p = '\0';
+	if (start && p - start > 0) {
+		len = result + EXPBUFSIZE - start - 1;
+		p = expand_macro(start, len);
+		*p = '\0';
+	}
+	return result;
+}
+
+char *get_mimetype_for_filename(const char *filename)
+{
+	char *ext, *mimetype, *token, line[1024], *saveptr;
+	FILE *file;
+	struct string_list_item *mime;
+
+	if (!filename)
+		return NULL;
+
+	ext = strrchr(filename, '.');
+	if (!ext)
+		return NULL;
+	++ext;
+	if (!ext[0])
+		return NULL;
+	mime = string_list_lookup(&ctx.cfg.mimetypes, ext);
+	if (mime)
+		return xstrdup(mime->util);
+
+	if (!ctx.cfg.mimetype_file)
+		return NULL;
+	file = fopen(ctx.cfg.mimetype_file, "r");
+	if (!file)
+		return NULL;
+	while (fgets(line, sizeof(line), file)) {
+		if (!line[0] || line[0] == '#')
+			continue;
+		mimetype = strtok_r(line, " \t\r\n", &saveptr);
+		while ((token = strtok_r(NULL, " \t\r\n", &saveptr))) {
+			if (!strcasecmp(ext, token)) {
+				fclose(file);
+				return xstrdup(mimetype);
+			}
+		}
+	}
+	fclose(file);
+	return NULL;
+}
diff --git a/third_party/cgit/tests/.gitignore b/third_party/cgit/tests/.gitignore
new file mode 100644
index 0000000000..3fd2e965c8
--- /dev/null
+++ b/third_party/cgit/tests/.gitignore
@@ -0,0 +1,2 @@
+trash\ directory.t*
+test-results
diff --git a/third_party/cgit/tests/Makefile b/third_party/cgit/tests/Makefile
new file mode 100644
index 0000000000..65e1117338
--- /dev/null
+++ b/third_party/cgit/tests/Makefile
@@ -0,0 +1,17 @@
+include ../git/config.mak.uname
+-include ../cgit.conf
+
+SHELL_PATH ?= $(SHELL)
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+
+T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
+
+all: $(T)
+
+$(T):
+	@'$(SHELL_PATH_SQ)' $@ $(CGIT_TEST_OPTS)
+
+clean:
+	$(RM) -rf trash
+
+.PHONY: $(T) clean
diff --git a/third_party/cgit/tests/filters/dump.sh b/third_party/cgit/tests/filters/dump.sh
new file mode 100755
index 0000000000..da6f7a1b18
--- /dev/null
+++ b/third_party/cgit/tests/filters/dump.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+[ "$#" -gt 0 ] && printf "%s " "$*"
+tr '[:lower:]' '[:upper:]'
diff --git a/third_party/cgit/tests/setup.sh b/third_party/cgit/tests/setup.sh
new file mode 100755
index 0000000000..31e7d5bb27
--- /dev/null
+++ b/third_party/cgit/tests/setup.sh
@@ -0,0 +1,161 @@
+# This file should be sourced by all test-scripts
+#
+# Main functions:
+#   prepare_tests(description) - setup for testing, i.e. create repos+config
+#   run_test(description, script) - run one test, i.e. eval script
+#
+# Helper functions
+#   cgit_query(querystring) - call cgit with the specified querystring
+#   cgit_url(url) - call cgit with the specified virtual url
+#
+# Example script:
+#
+# . setup.sh
+# prepare_tests "html validation"
+# run_test 'repo index' 'cgit_url "/" | tidy -e'
+# run_test 'repo summary' 'cgit_url "/foo" | tidy -e'
+
+# We don't want to run Git commands through Valgrind, so we filter out the
+# --valgrind option here and handle it ourselves.  We copy the arguments
+# assuming that none contain a newline, although other whitespace is
+# preserved.
+LF='
+'
+test_argv=
+
+while test $# != 0
+do
+	case "$1" in
+	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
+		cgit_valgrind=t
+		test_argv="$test_argv${LF}--verbose"
+		;;
+	*)
+		test_argv="$test_argv$LF$1"
+		;;
+	esac
+	shift
+done
+
+OLDIFS=$IFS
+IFS=$LF
+set -- $test_argv
+IFS=$OLDIFS
+
+: ${TEST_DIRECTORY=$(pwd)/../git/t}
+: ${TEST_OUTPUT_DIRECTORY=$(pwd)}
+TEST_NO_CREATE_REPO=YesPlease
+. "$TEST_DIRECTORY"/test-lib.sh
+
+# Prepend the directory containing cgit to PATH.
+if test -n "$cgit_valgrind"
+then
+	GIT_VALGRIND="$TEST_DIRECTORY/valgrind"
+	CGIT_VALGRIND=$(cd ../valgrind && pwd)
+	PATH="$CGIT_VALGRIND/bin:$PATH"
+	export GIT_VALGRIND CGIT_VALGRIND
+else
+	PATH="$(pwd)/../..:$PATH"
+fi
+
+FILTER_DIRECTORY=$(cd ../filters && pwd)
+
+mkrepo() {
+	name=$1
+	count=$2
+	test_create_repo "$name"
+	(
+		cd "$name"
+		n=1
+		while test $n -le $count
+		do
+			echo $n >file-$n
+			git add file-$n
+			git commit -m "commit $n"
+			n=$(expr $n + 1)
+		done
+		case "$3" in
+		testplus)
+			echo "hello" >a+b
+			git add a+b
+			git commit -m "add a+b"
+			git branch "1+2"
+			;;
+		commit-graph)
+			git commit-graph write
+			;;
+		esac
+	)
+}
+
+setup_repos()
+{
+	rm -rf cache
+	mkdir -p cache
+	mkrepo repos/foo 5 >/dev/null
+	mkrepo repos/bar 50 commit-graph >/dev/null
+	mkrepo repos/foo+bar 10 testplus >/dev/null
+	mkrepo "repos/with space" 2 >/dev/null
+	mkrepo repos/filter 5 testplus >/dev/null
+	cat >cgitrc <<EOF
+virtual-root=/
+cache-root=$PWD/cache
+
+cache-size=1021
+snapshots=tar.gz tar.bz tar.lz tar.xz tar.zst zip
+enable-log-filecount=1
+enable-log-linecount=1
+summary-log=5
+summary-branches=5
+summary-tags=5
+clone-url=git://example.org/\$CGIT_REPO_URL.git
+enable-filter-overrides=1
+
+repo.url=foo
+repo.path=$PWD/repos/foo/.git
+# Do not specify a description for this repo, as it then will be assigned
+# the constant value "[no description]" (which actually used to cause a
+# segfault).
+
+repo.url=bar
+repo.path=$PWD/repos/bar/.git
+repo.desc=the bar repo
+
+repo.url=foo+bar
+repo.path=$PWD/repos/foo+bar/.git
+repo.desc=the foo+bar repo
+
+repo.url=with space
+repo.path=$PWD/repos/with space/.git
+repo.desc=spaced repo
+
+repo.url=filter-exec
+repo.path=$PWD/repos/filter/.git
+repo.desc=filtered repo
+repo.about-filter=exec:$FILTER_DIRECTORY/dump.sh
+repo.commit-filter=exec:$FILTER_DIRECTORY/dump.sh
+repo.email-filter=exec:$FILTER_DIRECTORY/dump.sh
+repo.source-filter=exec:$FILTER_DIRECTORY/dump.sh
+repo.readme=master:a+b
+EOF
+}
+
+cgit_query()
+{
+	CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="$1" cgit
+}
+
+cgit_url()
+{
+	CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="url=$1" cgit
+}
+
+strip_headers() {
+	while read -r line
+	do
+		test -z "$line" && break
+	done
+	cat
+}
+
+test -z "$CGIT_TEST_NO_CREATE_REPOS" && setup_repos
diff --git a/third_party/cgit/tests/t0001-validate-git-versions.sh b/third_party/cgit/tests/t0001-validate-git-versions.sh
new file mode 100755
index 0000000000..dd84fe3fcb
--- /dev/null
+++ b/third_party/cgit/tests/t0001-validate-git-versions.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+if [ "${CGIT_TEST_NO_GIT_VERSION}" = "YesPlease" ]; then
+	exit 0
+fi
+
+test_description='Check Git version is correct'
+CGIT_TEST_NO_CREATE_REPOS=YesPlease
+. ./setup.sh
+
+test_expect_success 'extract Git version from Makefile' '
+	sed -n -e "/^GIT_VER[ 	]*=/ {
+		s/^GIT_VER[ 	]*=[ 	]*//
+		p
+	}" ../../Makefile >makefile_version
+'
+
+# Note that Git's GIT-VERSION-GEN script applies "s/-/./g" to the version
+# string to produce the internal version in the GIT-VERSION-FILE, so we
+# must apply the same transformation to the version in the Makefile before
+# comparing them.
+test_expect_success 'test Git version matches Makefile' '
+	( cat ../../git/GIT-VERSION-FILE || echo "No GIT-VERSION-FILE" ) |
+	sed -e "s/GIT_VERSION[ 	]*=[ 	]*//" -e "s/\\.dirty$//" >git_version &&
+	sed -e "s/-/./g" makefile_version >makefile_git_version &&
+	test_cmp git_version makefile_git_version
+'
+
+test_expect_success 'test submodule version matches Makefile' '
+	if ! test -e ../../git/.git
+	then
+		echo "git/ is not a Git repository" >&2
+	else
+		(
+			cd ../.. &&
+			sm_oid=$(git ls-files --stage -- git |
+				sed -e "s/^[0-9]* \\([0-9a-f]*\\) [0-9]	.*$/\\1/") &&
+			cd git &&
+			git describe --match "v[0-9]*" $sm_oid
+		) | sed -e "s/^v//" -e "s/-/./" >sm_version &&
+		test_cmp sm_version makefile_version
+	fi
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0010-validate-html.sh b/third_party/cgit/tests/t0010-validate-html.sh
new file mode 100755
index 0000000000..ca08d69d14
--- /dev/null
+++ b/third_party/cgit/tests/t0010-validate-html.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+test_description='Validate html with tidy'
+. ./setup.sh
+
+
+test_url()
+{
+	tidy_opt="-eq"
+	test -z "$NO_TIDY_WARNINGS" || tidy_opt+=" --show-warnings no"
+	cgit_url "$1" >tidy-$test_count.tmp || return
+	sed -e "1,4d" tidy-$test_count.tmp >tidy-$test_count || return
+	"$tidy" $tidy_opt tidy-$test_count
+	rc=$?
+
+	# tidy returns with exitcode 1 on warnings, 2 on error
+	if test $rc = 2
+	then
+		false
+	else
+		:
+	fi
+}
+
+tidy=`which tidy 2>/dev/null`
+test -n "$tidy" || {
+	skip_all='Skipping html validation tests: tidy not found'
+	test_done
+	exit
+}
+
+test_expect_success 'index page' 'test_url ""'
+test_expect_success 'foo' 'test_url "foo"'
+test_expect_success 'foo/log' 'test_url "foo/log"'
+test_expect_success 'foo/tree' 'test_url "foo/tree"'
+test_expect_success 'foo/tree/file-1' 'test_url "foo/tree/file-1"'
+test_expect_success 'foo/commit' 'test_url "foo/commit"'
+test_expect_success 'foo/diff' 'test_url "foo/diff"'
+
+test_done
diff --git a/third_party/cgit/tests/t0020-validate-cache.sh b/third_party/cgit/tests/t0020-validate-cache.sh
new file mode 100755
index 0000000000..657765d897
--- /dev/null
+++ b/third_party/cgit/tests/t0020-validate-cache.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+test_description='Validate cache'
+. ./setup.sh
+
+test_expect_success 'verify cache-size=0' '
+
+	rm -f cache/* &&
+	sed -e "s/cache-size=1021$/cache-size=0/" cgitrc >cgitrc.tmp &&
+	mv -f cgitrc.tmp cgitrc &&
+	cgit_url "" &&
+	cgit_url "foo" &&
+	cgit_url "foo/refs" &&
+	cgit_url "foo/tree" &&
+	cgit_url "foo/log" &&
+	cgit_url "foo/diff" &&
+	cgit_url "foo/patch" &&
+	cgit_url "bar" &&
+	cgit_url "bar/refs" &&
+	cgit_url "bar/tree" &&
+	cgit_url "bar/log" &&
+	cgit_url "bar/diff" &&
+	cgit_url "bar/patch" &&
+	ls cache >output &&
+	test_line_count = 0 output
+'
+
+test_expect_success 'verify cache-size=1' '
+
+	rm -f cache/* &&
+	sed -e "s/cache-size=0$/cache-size=1/" cgitrc >cgitrc.tmp &&
+	mv -f cgitrc.tmp cgitrc &&
+	cgit_url "" &&
+	cgit_url "foo" &&
+	cgit_url "foo/refs" &&
+	cgit_url "foo/tree" &&
+	cgit_url "foo/log" &&
+	cgit_url "foo/diff" &&
+	cgit_url "foo/patch" &&
+	cgit_url "bar" &&
+	cgit_url "bar/refs" &&
+	cgit_url "bar/tree" &&
+	cgit_url "bar/log" &&
+	cgit_url "bar/diff" &&
+	cgit_url "bar/patch" &&
+	ls cache >output &&
+	test_line_count = 1 output
+'
+
+test_expect_success 'verify cache-size=1021' '
+
+	rm -f cache/* &&
+	sed -e "s/cache-size=1$/cache-size=1021/" cgitrc >cgitrc.tmp &&
+	mv -f cgitrc.tmp cgitrc &&
+	cgit_url "" &&
+	cgit_url "foo" &&
+	cgit_url "foo/refs" &&
+	cgit_url "foo/tree" &&
+	cgit_url "foo/log" &&
+	cgit_url "foo/diff" &&
+	cgit_url "foo/patch" &&
+	cgit_url "bar" &&
+	cgit_url "bar/refs" &&
+	cgit_url "bar/tree" &&
+	cgit_url "bar/log" &&
+	cgit_url "bar/diff" &&
+	cgit_url "bar/patch" &&
+	ls cache >output &&
+	test_line_count = 13 output &&
+	cgit_url "foo/ls_cache" >output.full &&
+	strip_headers <output.full >output &&
+	test_line_count = 13 output &&
+	# Check that ls_cache output is cached correctly
+	cgit_url "foo/ls_cache" >output.second &&
+	test_cmp output.full output.second
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0101-index.sh b/third_party/cgit/tests/t0101-index.sh
new file mode 100755
index 0000000000..82ef9b04e5
--- /dev/null
+++ b/third_party/cgit/tests/t0101-index.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+test_description='Check content on index page'
+. ./setup.sh
+
+test_expect_success 'generate index page' 'cgit_url "" >tmp'
+test_expect_success 'find foo repo' 'grep "foo" tmp'
+test_expect_success 'find foo description' 'grep "\[no description\]" tmp'
+test_expect_success 'find bar repo' 'grep "bar" tmp'
+test_expect_success 'find bar description' 'grep "the bar repo" tmp'
+test_expect_success 'find foo+bar repo' 'grep ">foo+bar<" tmp'
+test_expect_success 'verify foo+bar link' 'grep "/foo+bar/" tmp'
+test_expect_success 'verify "with%20space" link' 'grep "/with%20space/" tmp'
+test_expect_success 'no tree-link' '! grep "foo/tree" tmp'
+test_expect_success 'no log-link' '! grep "foo/log" tmp'
+
+test_done
diff --git a/third_party/cgit/tests/t0102-summary.sh b/third_party/cgit/tests/t0102-summary.sh
new file mode 100755
index 0000000000..b8864cb187
--- /dev/null
+++ b/third_party/cgit/tests/t0102-summary.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+test_description='Check content on summary page'
+. ./setup.sh
+
+test_expect_success 'generate foo summary' 'cgit_url "foo" >tmp'
+test_expect_success 'find commit 1' 'grep "commit 1" tmp'
+test_expect_success 'find commit 5' 'grep "commit 5" tmp'
+test_expect_success 'find branch master' 'grep "master" tmp'
+test_expect_success 'no tags' '! grep "tags" tmp'
+test_expect_success 'clone-url expanded correctly' '
+	grep "git://example.org/foo.git" tmp
+'
+
+test_expect_success 'generate bar summary' 'cgit_url "bar" >tmp'
+test_expect_success 'no commit 45' '! grep "commit 45" tmp'
+test_expect_success 'find commit 46' 'grep "commit 46" tmp'
+test_expect_success 'find commit 50' 'grep "commit 50" tmp'
+test_expect_success 'find branch master' 'grep "master" tmp'
+test_expect_success 'no tags' '! grep "tags" tmp'
+test_expect_success 'clone-url expanded correctly' '
+	grep "git://example.org/bar.git" tmp
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0103-log.sh b/third_party/cgit/tests/t0103-log.sh
new file mode 100755
index 0000000000..bdf1435a16
--- /dev/null
+++ b/third_party/cgit/tests/t0103-log.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+test_description='Check content on log page'
+. ./setup.sh
+
+test_expect_success 'generate foo/log' 'cgit_url "foo/log" >tmp'
+test_expect_success 'find commit 1' 'grep "commit 1" tmp'
+test_expect_success 'find commit 5' 'grep "commit 5" tmp'
+
+test_expect_success 'generate bar/log' 'cgit_url "bar/log" >tmp'
+test_expect_success 'find commit 1' 'grep "commit 1" tmp'
+test_expect_success 'find commit 50' 'grep "commit 50" tmp'
+
+test_expect_success 'generate "with%20space/log?qt=grep&q=commit+1"' '
+	cgit_url "with+space/log&qt=grep&q=commit+1" >tmp
+'
+test_expect_success 'find commit 1' 'grep "commit 1" tmp'
+test_expect_success 'find link with %20 in path' 'grep "/with%20space/log/?qt=grep" tmp'
+test_expect_success 'find link with + in arg' 'grep "/log/?qt=grep&amp;q=commit+1" tmp'
+test_expect_success 'no links with space in path' '! grep "href=./with space/" tmp'
+test_expect_success 'no links with space in arg' '! grep "q=commit 1" tmp'
+test_expect_success 'commit 2 is not visible' '! grep "commit 2" tmp'
+
+test_done
diff --git a/third_party/cgit/tests/t0104-tree.sh b/third_party/cgit/tests/t0104-tree.sh
new file mode 100755
index 0000000000..2e140f5939
--- /dev/null
+++ b/third_party/cgit/tests/t0104-tree.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+test_description='Check content on tree page'
+. ./setup.sh
+
+test_expect_success 'generate bar/tree' 'cgit_url "bar/tree" >tmp'
+test_expect_success 'find file-1' 'grep "file-1" tmp'
+test_expect_success 'find file-50' 'grep "file-50" tmp'
+
+test_expect_success 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >tmp'
+
+test_expect_success 'find line 1' '
+	grep "<a id=.n1. href=.#n1.>1</a>" tmp
+'
+
+test_expect_success 'no line 2' '
+	! grep "<a id=.n2. href=.#n2.>2</a>" tmp
+'
+
+test_expect_success 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >tmp'
+
+test_expect_success 'verify a+b link' '
+	grep "/foo+bar/tree/a+b" tmp
+'
+
+test_expect_success 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >tmp'
+
+test_expect_success 'verify a+b?h=1+2 link' '
+	grep "/foo+bar/tree/a+b?h=1%2b2" tmp
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0105-commit.sh b/third_party/cgit/tests/t0105-commit.sh
new file mode 100755
index 0000000000..cfed1e7d69
--- /dev/null
+++ b/third_party/cgit/tests/t0105-commit.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+test_description='Check content on commit page'
+. ./setup.sh
+
+test_expect_success 'generate foo/commit' 'cgit_url "foo/commit" >tmp'
+test_expect_success 'find tree link' 'grep "<a href=./foo/tree/.>" tmp'
+test_expect_success 'find parent link' 'grep -E "<a href=./foo/commit/\?id=.+>" tmp'
+
+test_expect_success 'find commit subject' '
+	grep "<div class=.commit-subject.>commit 5<" tmp
+'
+
+test_expect_success 'find commit msg' 'grep "<pre class=.commit-msg.></pre>" tmp'
+test_expect_success 'find diffstat' 'grep "<table summary=.diffstat. class=.diffstat.>" tmp'
+
+test_expect_success 'find diff summary' '
+	grep "1 files changed, 1 insertions, 0 deletions" tmp
+'
+
+test_expect_success 'get root commit' '
+	root=$(cd repos/foo && git rev-list --reverse HEAD | head -1) &&
+	cgit_url "foo/commit&id=$root" >tmp &&
+	grep "</html>" tmp
+'
+
+test_expect_success 'root commit contains diffstat' '
+	grep "<a href=./foo/diff/file-1.id=[0-9a-f]\{40,64\}.>file-1</a>" tmp
+'
+
+test_expect_success 'root commit contains diff' '
+	grep ">diff --git a/file-1 b/file-1" tmp &&
+	grep "<span class=.add.>+1</span>" tmp
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0106-diff.sh b/third_party/cgit/tests/t0106-diff.sh
new file mode 100755
index 0000000000..62a0a74a64
--- /dev/null
+++ b/third_party/cgit/tests/t0106-diff.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+test_description='Check content on diff page'
+. ./setup.sh
+
+test_expect_success 'generate foo/diff' 'cgit_url "foo/diff" >tmp'
+test_expect_success 'find diff header' 'grep "a/file-5 b/file-5" tmp'
+test_expect_success 'find blob link' 'grep "<a href=./foo/tree/file-5?id=" tmp'
+test_expect_success 'find added file' 'grep "new file mode 100644" tmp'
+
+test_expect_success 'find hunk header' '
+	grep "<span class=.hunk.>@@ -0,0 +1 @@</span>" tmp
+'
+
+test_expect_success 'find added line' '
+	grep "<span class=.add.>+5</span>" tmp
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0107-snapshot.sh b/third_party/cgit/tests/t0107-snapshot.sh
new file mode 100755
index 0000000000..0811ec4074
--- /dev/null
+++ b/third_party/cgit/tests/t0107-snapshot.sh
@@ -0,0 +1,205 @@
+#!/bin/sh
+
+test_description='Verify snapshot'
+. ./setup.sh
+
+test_expect_success 'get foo/snapshot/master.tar.gz' '
+	cgit_url "foo/snapshot/master.tar.gz" >tmp
+'
+
+test_expect_success 'check html headers' '
+	head -n 1 tmp |
+	grep "Content-Type: application/x-gzip" &&
+
+	head -n 2 tmp |
+	grep "Content-Disposition: inline; filename=.master.tar.gz."
+'
+
+test_expect_success 'strip off the header lines' '
+	strip_headers <tmp >master.tar.gz
+'
+
+test_expect_success 'verify gzip format' '
+	gunzip --test master.tar.gz
+'
+
+test_expect_success 'untar' '
+	rm -rf master &&
+	gzip -dc master.tar.gz | tar -xf -
+'
+
+test_expect_success 'count files' '
+	ls master/ >output &&
+	test_line_count = 5 output
+'
+
+test_expect_success 'verify untarred file-5' '
+	grep "^5$" master/file-5 &&
+	test_line_count = 1 master/file-5
+'
+
+if test -n "$(which lzip 2>/dev/null)"; then
+	test_set_prereq LZIP
+else
+	say 'Skipping LZIP validation tests: lzip not found'
+fi
+
+test_expect_success LZIP 'get foo/snapshot/master.tar.lz' '
+	cgit_url "foo/snapshot/master.tar.lz" >tmp
+'
+
+test_expect_success LZIP 'check html headers' '
+	head -n 1 tmp |
+	grep "Content-Type: application/x-lzip" &&
+
+	head -n 2 tmp |
+	grep "Content-Disposition: inline; filename=.master.tar.lz."
+'
+
+test_expect_success LZIP 'strip off the header lines' '
+	strip_headers <tmp >master.tar.lz
+'
+
+test_expect_success LZIP 'verify lzip format' '
+	lzip --test master.tar.lz
+'
+
+test_expect_success LZIP 'untar' '
+	rm -rf master &&
+	lzip -dc master.tar.lz | tar -xf -
+'
+
+test_expect_success LZIP 'count files' '
+	ls master/ >output &&
+	test_line_count = 5 output
+'
+
+test_expect_success LZIP 'verify untarred file-5' '
+	grep "^5$" master/file-5 &&
+	test_line_count = 1 master/file-5
+'
+
+if test -n "$(which xz 2>/dev/null)"; then
+	test_set_prereq XZ
+else
+	say 'Skipping XZ validation tests: xz not found'
+fi
+
+test_expect_success XZ 'get foo/snapshot/master.tar.xz' '
+	cgit_url "foo/snapshot/master.tar.xz" >tmp
+'
+
+test_expect_success XZ 'check html headers' '
+	head -n 1 tmp |
+	grep "Content-Type: application/x-xz" &&
+
+	head -n 2 tmp |
+	grep "Content-Disposition: inline; filename=.master.tar.xz."
+'
+
+test_expect_success XZ 'strip off the header lines' '
+	strip_headers <tmp >master.tar.xz
+'
+
+test_expect_success XZ 'verify xz format' '
+	xz --test master.tar.xz
+'
+
+test_expect_success XZ 'untar' '
+	rm -rf master &&
+	xz -dc master.tar.xz | tar -xf -
+'
+
+test_expect_success XZ 'count files' '
+	ls master/ >output &&
+	test_line_count = 5 output
+'
+
+test_expect_success XZ 'verify untarred file-5' '
+	grep "^5$" master/file-5 &&
+	test_line_count = 1 master/file-5
+'
+
+if test -n "$(which zstd 2>/dev/null)"; then
+	test_set_prereq ZSTD
+else
+	say 'Skipping ZSTD validation tests: zstd not found'
+fi
+
+test_expect_success ZSTD 'get foo/snapshot/master.tar.zst' '
+	cgit_url "foo/snapshot/master.tar.zst" >tmp
+'
+
+test_expect_success ZSTD 'check html headers' '
+	head -n 1 tmp |
+	grep "Content-Type: application/x-zstd" &&
+
+	head -n 2 tmp |
+	grep "Content-Disposition: inline; filename=.master.tar.zst."
+'
+
+test_expect_success ZSTD 'strip off the header lines' '
+	strip_headers <tmp >master.tar.zst
+'
+
+test_expect_success ZSTD 'verify zstd format' '
+	zstd --test master.tar.zst
+'
+
+test_expect_success ZSTD 'untar' '
+	rm -rf master &&
+	zstd -dc master.tar.zst | tar -xf -
+'
+
+test_expect_success ZSTD 'count files' '
+	ls master/ >output &&
+	test_line_count = 5 output
+'
+
+test_expect_success ZSTD 'verify untarred file-5' '
+	grep "^5$" master/file-5 &&
+	test_line_count = 1 master/file-5
+'
+
+test_expect_success 'get foo/snapshot/master.zip' '
+	cgit_url "foo/snapshot/master.zip" >tmp
+'
+
+test_expect_success 'check HTML headers (zip)' '
+	head -n 1 tmp |
+	grep "Content-Type: application/x-zip" &&
+
+	head -n 2 tmp |
+	grep "Content-Disposition: inline; filename=.master.zip."
+'
+
+test_expect_success 'strip off the header lines (zip)' '
+	strip_headers <tmp >master.zip
+'
+
+if test -n "$(which unzip 2>/dev/null)"; then
+	test_set_prereq UNZIP
+else
+	say 'Skipping ZIP validation tests: unzip not found'
+fi
+
+test_expect_success UNZIP 'verify zip format' '
+	unzip -t master.zip
+'
+
+test_expect_success UNZIP 'unzip' '
+	rm -rf master &&
+	unzip master.zip
+'
+
+test_expect_success UNZIP 'count files (zip)' '
+	ls master/ >output &&
+	test_line_count = 5 output
+'
+
+test_expect_success UNZIP 'verify unzipped file-5' '
+	grep "^5$" master/file-5 &&
+	test_line_count = 1 master/file-5
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0108-patch.sh b/third_party/cgit/tests/t0108-patch.sh
new file mode 100755
index 0000000000..013d68024d
--- /dev/null
+++ b/third_party/cgit/tests/t0108-patch.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+test_description='Check content on patch page'
+. ./setup.sh
+
+test_expect_success 'generate foo/patch' '
+	cgit_query "url=foo/patch" >tmp
+'
+
+test_expect_success 'find `From:` line' '
+	grep "^From: " tmp
+'
+
+test_expect_success 'find `Date:` line' '
+	grep "^Date: " tmp
+'
+
+test_expect_success 'find `Subject:` line' '
+	grep "^Subject: commit 5" tmp
+'
+
+test_expect_success 'find `cgit` signature' '
+	tail -2 tmp | head -1 | grep "^cgit"
+'
+
+test_expect_success 'compare with output of git-format-patch(1)' '
+	CGIT_VERSION=$(sed -n "s/CGIT_VERSION = //p" ../../VERSION) &&
+	git --git-dir="$PWD/repos/foo/.git" format-patch --subject-prefix="" --signature="cgit $CGIT_VERSION" --stdout HEAD^ >tmp2 &&
+	strip_headers <tmp >tmp_ &&
+	test_cmp tmp_ tmp2
+'
+
+test_expect_success 'find initial commit' '
+	root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD)
+'
+
+test_expect_success 'generate patch for initial commit' '
+	cgit_query "url=foo/patch&id=$root" >tmp
+'
+
+test_expect_success 'find `cgit` signature' '
+	tail -2 tmp | head -1 | grep "^cgit"
+'
+
+test_expect_success 'generate patches for multiple commits' '
+	id=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD) &&
+	id2=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD~3) &&
+	cgit_query "url=foo/patch&id=$id&id2=$id2" >tmp
+'
+
+test_expect_success 'find `cgit` signature' '
+	tail -2 tmp | head -1 | grep "^cgit"
+'
+
+test_expect_success 'compare with output of git-format-patch(1)' '
+	CGIT_VERSION=$(sed -n "s/CGIT_VERSION = //p" ../../VERSION) &&
+	git --git-dir="$PWD/repos/foo/.git" format-patch -N --subject-prefix="" --signature="cgit $CGIT_VERSION" --stdout HEAD~3..HEAD >tmp2 &&
+	strip_headers <tmp >tmp_ &&
+	test_cmp tmp_ tmp2
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0109-gitconfig.sh b/third_party/cgit/tests/t0109-gitconfig.sh
new file mode 100755
index 0000000000..189ef28166
--- /dev/null
+++ b/third_party/cgit/tests/t0109-gitconfig.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+test_description='Ensure that git does not access $HOME'
+. ./setup.sh
+
+test -n "$(which strace 2>/dev/null)" || {
+	skip_all='Skipping access validation tests: strace not found'
+	test_done
+	exit
+}
+
+strace true 2>/dev/null || {
+	skip_all='Skipping access validation tests: strace not functional'
+	test_done
+	exit
+}
+
+test_no_home_access () {
+	non_existent_path="/path/to/some/place/that/does/not/possibly/exist"
+	while test -d "$non_existent_path"; do
+		non_existent_path="$non_existent_path/$(date +%N)"
+	done &&
+	strace \
+		-E HOME="$non_existent_path" \
+		-E CGIT_CONFIG="$PWD/cgitrc" \
+		-E QUERY_STRING="url=$1" \
+		-e access -f -o strace.out cgit &&
+	! grep "$non_existent_path" strace.out
+}
+
+test_no_home_access_success() {
+	test_expect_success "do not access \$HOME: $1" "
+		test_no_home_access '$1'
+	"
+}
+
+test_no_home_access_success
+test_no_home_access_success foo
+test_no_home_access_success foo/refs
+test_no_home_access_success foo/log
+test_no_home_access_success foo/tree
+test_no_home_access_success foo/tree/file-1
+test_no_home_access_success foo/commit
+test_no_home_access_success foo/diff
+test_no_home_access_success foo/patch
+test_no_home_access_success foo/snapshot/master.tar.gz
+
+test_done
diff --git a/third_party/cgit/tests/t0110-rawdiff.sh b/third_party/cgit/tests/t0110-rawdiff.sh
new file mode 100755
index 0000000000..66fa7d5d37
--- /dev/null
+++ b/third_party/cgit/tests/t0110-rawdiff.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='Check content on rawdiff page'
+. ./setup.sh
+
+test_expect_success 'generate foo/rawdiff' '
+	cgit_query "url=foo/rawdiff" >tmp
+'
+
+test_expect_success 'compare with output of git-diff(1)' '
+	git --git-dir="$PWD/repos/foo/.git" diff HEAD^.. >tmp2 &&
+	sed "1,4d" tmp >tmp_ &&
+	cmp tmp_ tmp2
+'
+
+test_expect_success 'find initial commit' '
+	root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD)
+'
+
+test_expect_success 'generate diff for initial commit' '
+	cgit_query "url=foo/rawdiff&id=$root" >tmp
+'
+
+test_expect_success 'compare with output of git-diff-tree(1)' '
+	git --git-dir="$PWD/repos/foo/.git" diff-tree -p --no-commit-id --root "$root" >tmp2 &&
+	sed "1,4d" tmp >tmp_ &&
+	cmp tmp_ tmp2
+'
+
+test_expect_success 'generate diff for multiple commits' '
+	id=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD) &&
+	id2=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD~3) &&
+	cgit_query "url=foo/rawdiff&id=$id&id2=$id2" >tmp
+'
+
+test_expect_success 'compare with output of git-diff(1)' '
+	git --git-dir="$PWD/repos/foo/.git" diff HEAD~3..HEAD >tmp2 &&
+	sed "1,4d" tmp >tmp_ &&
+	cmp tmp_ tmp2
+'
+
+test_done
diff --git a/third_party/cgit/tests/t0111-filter.sh b/third_party/cgit/tests/t0111-filter.sh
new file mode 100755
index 0000000000..e5d357507c
--- /dev/null
+++ b/third_party/cgit/tests/t0111-filter.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+test_description='Check filtered content'
+. ./setup.sh
+
+prefixes="exec"
+
+for prefix in $prefixes
+do
+	test_expect_success "generate filter-$prefix/tree/a%2bb" "
+		cgit_url 'filter-$prefix/tree/a%2bb' >tmp
+	"
+
+	test_expect_success "check whether the $prefix source filter works" '
+		grep "<code>a+b HELLO$" tmp
+	'
+
+	test_expect_success "generate filter-$prefix/about/" "
+		cgit_url 'filter-$prefix/about/' >tmp
+	"
+
+	test_expect_success "check whether the $prefix about filter works" '
+		grep "<div id='"'"'summary'"'"'>a+b HELLO$" tmp
+	'
+
+	test_expect_success "generate filter-$prefix/commit/" "
+		cgit_url 'filter-$prefix/commit/' >tmp
+	"
+
+	test_expect_success "check whether the $prefix commit filter works" '
+		grep "<div class='"'"'commit-subject'"'"'>ADD A+B" tmp
+	'
+
+	test_expect_success "check whether the $prefix email filter works for authors" '
+		grep "<author@example.com> commit A U THOR &LT;AUTHOR@EXAMPLE.COM&GT;" tmp
+	'
+
+	test_expect_success "check whether the $prefix email filter works for committers" '
+		grep "<committer@example.com> commit C O MITTER &LT;COMMITTER@EXAMPLE.COM&GT;" tmp
+	'
+done
+
+test_done
diff --git a/third_party/cgit/tests/valgrind/bin/cgit b/third_party/cgit/tests/valgrind/bin/cgit
new file mode 100755
index 0000000000..dcdfbe5320
--- /dev/null
+++ b/third_party/cgit/tests/valgrind/bin/cgit
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+# Note that we currently use Git's suppression file and there are variables
+# $GIT_VALGRIND and $CGIT_VALGRIND which point to different places.
+exec valgrind -q --error-exitcode=126 \
+	--suppressions="$GIT_VALGRIND/default.supp" \
+	--gen-suppressions=all \
+	--leak-check=no \
+	--track-origins=yes \
+	--log-fd=4 \
+	--input-fd=4 \
+	"$CGIT_VALGRIND/../../cgit" "$@"
diff --git a/third_party/cgit/tvl-extra.css b/third_party/cgit/tvl-extra.css
new file mode 100644
index 0000000000..41f5041d62
--- /dev/null
+++ b/third_party/cgit/tvl-extra.css
@@ -0,0 +1,35 @@
+/* limit the width of /about/** to help readability */
+.content #summary {
+  max-width: 800px;
+}
+
+/* highlight cheddar callouts in cgit about views */
+.cheddar-callout {
+    display: block;
+    padding: 10px;
+}
+
+.cheddar-question {
+    color: #3367d6;
+    background-color: #e8f0fe;
+}
+
+.cheddar-todo {
+    color: #616161;
+    background-color: #eeeeee;
+}
+
+.cheddar-tip {
+    color: #00796b;
+    background-color: #e0f2f1;
+}
+
+.cheddar-warning {
+    color: #a52714;
+    background-color: #fbe9e7;
+}
+
+/* add some padding next to the logo */
+td.logo {
+    padding-right: 10px;
+}
diff --git a/third_party/cgit/ui-atom.c b/third_party/cgit/ui-atom.c
new file mode 100644
index 0000000000..0cf8441d46
--- /dev/null
+++ b/third_party/cgit/ui-atom.c
@@ -0,0 +1,157 @@
+/* ui-atom.c: functions for atom feeds
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-atom.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static void add_entry(struct commit *commit, const char *host)
+{
+	char delim = '&';
+	char *hex;
+	char *mail, *t, *t2;
+	struct commitinfo *info;
+
+	info = cgit_parse_commit(commit);
+	hex = oid_to_hex(&commit->object.oid);
+	html("<entry>\n");
+	html("<title>");
+	html_txt(info->subject);
+	html("</title>\n");
+	html("<updated>");
+	html_txt(show_date(info->committer_date, 0,
+                    date_mode_from_type(DATE_ISO8601_STRICT)));
+	html("</updated>\n");
+	html("<author>\n");
+	if (info->author) {
+		html("<name>");
+		html_txt(info->author);
+		html("</name>\n");
+	}
+	if (info->author_email && !ctx.cfg.noplainemail) {
+		mail = xstrdup(info->author_email);
+		t = strchr(mail, '<');
+		if (t)
+			t++;
+		else
+			t = mail;
+		t2 = strchr(t, '>');
+		if (t2)
+			*t2 = '\0';
+		html("<email>");
+		html_txt(t);
+		html("</email>\n");
+		free(mail);
+	}
+	html("</author>\n");
+	html("<published>");
+	html_txt(show_date(info->author_date, 0,
+                    date_mode_from_type(DATE_ISO8601_STRICT)));
+	html("</published>\n");
+	if (host) {
+		char *pageurl;
+		html("<link rel='alternate' type='text/html' href='");
+		html(cgit_httpscheme());
+		html_attr(host);
+		pageurl = cgit_pageurl(ctx.repo->url, "commit", NULL);
+		html_attr(pageurl);
+		if (ctx.cfg.virtual_root)
+			delim = '?';
+		html_attrf("%cid=%s", delim, hex);
+		html("'/>\n");
+		free(pageurl);
+	}
+	html("<id>");
+	html_txtf("urn:%s:%s", the_hash_algo->name, hex);
+	html("</id>\n");
+	html("<content type='text'>\n");
+	html_txt(info->msg);
+	html("</content>\n");
+	html("</entry>\n");
+	cgit_free_commitinfo(info);
+}
+
+
+void cgit_print_atom(char *tip, const char *path, int max_count)
+{
+	char *host;
+	const char *argv[] = {NULL, tip, NULL, NULL, NULL};
+	struct commit *commit;
+	struct rev_info rev;
+	int argc = 2;
+	int first = 1;
+
+	if (ctx.qry.show_all)
+		argv[1] = "--all";
+	else if (!tip)
+		argv[1] = ctx.qry.head;
+
+	if (path) {
+		argv[argc++] = "--";
+		argv[argc++] = path;
+	}
+
+	init_revisions(&rev, NULL);
+	rev.abbrev = DEFAULT_ABBREV;
+	rev.commit_format = CMIT_FMT_DEFAULT;
+	rev.verbose_header = 1;
+	rev.show_root_diff = 0;
+	rev.max_count = max_count;
+	setup_revisions(argc, argv, &rev, NULL);
+	prepare_revision_walk(&rev);
+
+	host = cgit_hosturl();
+	ctx.page.mimetype = "text/xml";
+	ctx.page.charset = "utf-8";
+	cgit_print_http_headers();
+	html("<feed xmlns='http://www.w3.org/2005/Atom'>\n");
+	html("<title>");
+	html_txt(ctx.repo->name);
+	if (path) {
+		html("/");
+		html_txt(path);
+	}
+	if (tip && !ctx.qry.show_all) {
+		html(", branch ");
+		html_txt(tip);
+	}
+	html("</title>\n");
+	html("<subtitle>");
+	html_txt(ctx.repo->desc);
+	html("</subtitle>\n");
+	if (host) {
+		char *fullurl = cgit_currentfullurl();
+		char *repourl = cgit_repourl(ctx.repo->url);
+		html("<id>");
+		html_txtf("%s%s%s", cgit_httpscheme(), host, fullurl);
+		html("</id>\n");
+		html("<link rel='self' href='");
+		html_attrf("%s%s%s", cgit_httpscheme(), host, fullurl);
+		html("'/>\n");
+		html("<link rel='alternate' type='text/html' href='");
+		html_attrf("%s%s%s", cgit_httpscheme(), host, repourl);
+		html("'/>\n");
+		free(fullurl);
+		free(repourl);
+	}
+	while ((commit = get_revision(&rev)) != NULL) {
+		if (first) {
+			html("<updated>");
+			html_txt(show_date(commit->date, 0,
+				date_mode_from_type(DATE_ISO8601_STRICT)));
+			html("</updated>\n");
+			first = 0;
+		}
+		add_entry(commit, host);
+		release_commit_memory(the_repository->parsed_objects, commit);
+		commit->parents = NULL;
+	}
+	html("</feed>\n");
+	free(host);
+}
diff --git a/third_party/cgit/ui-atom.h b/third_party/cgit/ui-atom.h
new file mode 100644
index 0000000000..dda953bbf4
--- /dev/null
+++ b/third_party/cgit/ui-atom.h
@@ -0,0 +1,6 @@
+#ifndef UI_ATOM_H
+#define UI_ATOM_H
+
+extern void cgit_print_atom(char *tip, const char *path, int max_count);
+
+#endif
diff --git a/third_party/cgit/ui-blame.c b/third_party/cgit/ui-blame.c
new file mode 100644
index 0000000000..ca770994a6
--- /dev/null
+++ b/third_party/cgit/ui-blame.c
@@ -0,0 +1,315 @@
+/* ui-blame.c: functions for blame output
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-blame.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "strvec.h"
+#include "blame.h"
+
+
+static char *emit_suspect_detail(struct blame_origin *suspect)
+{
+	struct commitinfo *info;
+	struct strbuf detail = STRBUF_INIT;
+
+	info = cgit_parse_commit(suspect->commit);
+
+	strbuf_addf(&detail, "author  %s", info->author);
+	if (!ctx.cfg.noplainemail)
+		strbuf_addf(&detail, " %s", info->author_email);
+	strbuf_addf(&detail, "  %s\n",
+		    show_date(info->author_date, info->author_tz,
+				    cgit_date_mode(DATE_DOTTIME)));
+
+	strbuf_addf(&detail, "committer  %s", info->committer);
+	if (!ctx.cfg.noplainemail)
+		strbuf_addf(&detail, " %s", info->committer_email);
+	strbuf_addf(&detail, "  %s\n\n",
+		    show_date(info->committer_date, info->committer_tz,
+				    cgit_date_mode(DATE_DOTTIME)));
+
+	strbuf_addstr(&detail, info->subject);
+
+	cgit_free_commitinfo(info);
+	return strbuf_detach(&detail, NULL);
+}
+
+static void emit_blame_entry_hash(struct blame_entry *ent)
+{
+	struct blame_origin *suspect = ent->suspect;
+	struct object_id *oid = &suspect->commit->object.oid;
+	unsigned long line = 0;
+
+	char *detail = emit_suspect_detail(suspect);
+	html("<span class='oid'>");
+	cgit_commit_link(find_unique_abbrev(oid, DEFAULT_ABBREV), detail,
+			 NULL, ctx.qry.head, oid_to_hex(oid), suspect->path);
+	html("</span>");
+	free(detail);
+
+	if (!parse_commit(suspect->commit) && suspect->commit->parents) {
+		struct commit *parent = suspect->commit->parents->item;
+
+		html(" ");
+		cgit_blame_link("^", "Blame the previous revision", NULL,
+				ctx.qry.head, oid_to_hex(&parent->object.oid),
+				suspect->path);
+	}
+
+	while (line++ < ent->num_lines)
+		html("\n");
+}
+
+static void emit_blame_entry_linenumber(struct blame_entry *ent)
+{
+	const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
+
+	unsigned long lineno = ent->lno;
+	while (lineno < ent->lno + ent->num_lines)
+		htmlf(numberfmt, ++lineno);
+}
+
+static void emit_blame_entry_line_background(struct blame_scoreboard *sb,
+					     struct blame_entry *ent)
+{
+	unsigned long line;
+	size_t len, maxlen = 2;
+	const char* pos, *endpos;
+
+	for (line = ent->lno; line < ent->lno + ent->num_lines; line++) {
+		html("\n");
+		pos = blame_nth_line(sb, line);
+		endpos = blame_nth_line(sb, line + 1);
+		len = 0;
+		while (pos < endpos) {
+			len++;
+			if (*pos++ == '\t')
+				len = (len + 7) & ~7;
+		}
+		if (len > maxlen)
+			maxlen = len;
+	}
+
+	for (len = 0; len < maxlen - 1; len++)
+		html(" ");
+}
+
+struct walk_tree_context {
+	char *curr_rev;
+	int match_baselen;
+	int state;
+};
+
+static void print_object(const struct object_id *oid, const char *path,
+			 const char *basename, const char *rev)
+{
+	enum object_type type;
+	char *buf;
+	unsigned long size;
+	struct strvec rev_argv = STRVEC_INIT;
+	struct rev_info revs;
+	struct blame_scoreboard sb;
+	struct blame_origin *o;
+	struct blame_entry *ent = NULL;
+
+	type = oid_object_info(the_repository, oid, &size);
+	if (type == OBJ_BAD) {
+		cgit_print_error_page(404, "Not found", "Bad object name: %s",
+				      oid_to_hex(oid));
+		return;
+	}
+
+	buf = read_object_file(oid, &type, &size);
+	if (!buf) {
+		cgit_print_error_page(500, "Internal server error",
+			"Error reading object %s", oid_to_hex(oid));
+		return;
+	}
+
+	strvec_push(&rev_argv, "blame");
+	strvec_push(&rev_argv, rev);
+	init_revisions(&revs, NULL);
+	revs.diffopt.flags.allow_textconv = 1;
+	setup_revisions(rev_argv.nr, rev_argv.v, &revs, NULL);
+	init_scoreboard(&sb);
+	sb.revs = &revs;
+	sb.repo = the_repository;
+	sb.path = path;
+	setup_scoreboard(&sb, &o);
+	o->suspects = blame_entry_prepend(NULL, 0, sb.num_lines, o);
+	prio_queue_put(&sb.commits, o->commit);
+	blame_origin_decref(o);
+	sb.ent = NULL;
+	sb.path = path;
+	assign_blame(&sb, 0);
+	blame_sort_final(&sb);
+	blame_coalesce(&sb);
+
+	cgit_set_title_from_path(path);
+
+	cgit_print_layout_start();
+	htmlf("blob: %s (", oid_to_hex(oid));
+	cgit_plain_link("plain", NULL, NULL, ctx.qry.head, rev, path);
+	html(") (");
+	cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path);
+	html(")\n");
+
+	if (buffer_is_binary(buf, size)) {
+		html("<div class='error'>blob is binary.</div>");
+		goto cleanup;
+	}
+	if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
+		htmlf("<div class='error'>blob size (%ldKB)"
+		      " exceeds display size limit (%dKB).</div>",
+		      size / 1024, ctx.cfg.max_blob_size);
+		goto cleanup;
+	}
+
+	html("<table class='blame blob'>\n<tr>\n");
+
+	/* Commit hashes */
+	html("<td class='hashes'>");
+	for (ent = sb.ent; ent; ent = ent->next) {
+		html("<div class='alt'><pre>");
+		emit_blame_entry_hash(ent);
+		html("</pre></div>");
+	}
+	html("</td>\n");
+
+	/* Line numbers */
+	if (ctx.cfg.enable_tree_linenumbers) {
+		html("<td class='linenumbers'>");
+		for (ent = sb.ent; ent; ent = ent->next) {
+			html("<div class='alt'><pre>");
+			emit_blame_entry_linenumber(ent);
+			html("</pre></div>");
+		}
+		html("</td>\n");
+	}
+
+	html("<td class='lines'><div>");
+
+	/* Colored bars behind lines */
+	html("<div>");
+	for (ent = sb.ent; ent; ) {
+		struct blame_entry *e = ent->next;
+		html("<div class='alt'><pre>");
+		emit_blame_entry_line_background(&sb, ent);
+		html("</pre></div>");
+		free(ent);
+		ent = e;
+	}
+	html("</div>");
+
+	free((void *)sb.final_buf);
+
+	/* Lines */
+	html("<pre><code>");
+	if (ctx.repo->source_filter) {
+		char *filter_arg = xstrdup(basename);
+		cgit_open_filter(ctx.repo->source_filter, filter_arg);
+		html_raw(buf, size);
+		cgit_close_filter(ctx.repo->source_filter);
+		free(filter_arg);
+	} else {
+		html_txt(buf);
+	}
+	html("</code></pre>");
+
+	html("</div></td>\n");
+
+	html("</tr>\n</table>\n");
+
+	cgit_print_layout_end();
+
+cleanup:
+	free(buf);
+}
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+		     const char *pathname, unsigned mode, void *cbdata)
+{
+	struct walk_tree_context *walk_tree_ctx = cbdata;
+
+	if (base->len == walk_tree_ctx->match_baselen) {
+		if (S_ISREG(mode)) {
+			struct strbuf buffer = STRBUF_INIT;
+			strbuf_addbuf(&buffer, base);
+			strbuf_addstr(&buffer, pathname);
+			print_object(oid, buffer.buf, pathname,
+				     walk_tree_ctx->curr_rev);
+			strbuf_release(&buffer);
+			walk_tree_ctx->state = 1;
+		} else if (S_ISDIR(mode)) {
+			walk_tree_ctx->state = 2;
+		}
+	} else if (base->len < INT_MAX
+			&& (int)base->len > walk_tree_ctx->match_baselen) {
+		walk_tree_ctx->state = 2;
+	} else if (S_ISDIR(mode)) {
+		return READ_TREE_RECURSIVE;
+	}
+	return 0;
+}
+
+static int basedir_len(const char *path)
+{
+	char *p = strrchr(path, '/');
+	if (p)
+		return p - path + 1;
+	return 0;
+}
+
+void cgit_print_blame(void)
+{
+	const char *rev = ctx.qry.oid;
+	struct object_id oid;
+	struct commit *commit;
+	struct pathspec_item path_items = {
+		.match = ctx.qry.path,
+		.len = ctx.qry.path ? strlen(ctx.qry.path) : 0
+	};
+	struct pathspec paths = {
+		.nr = 1,
+		.items = &path_items
+	};
+	struct walk_tree_context walk_tree_ctx = {
+		.state = 0
+	};
+
+	if (!rev)
+		rev = ctx.qry.head;
+
+	if (get_oid(rev, &oid)) {
+		cgit_print_error_page(404, "Not found",
+			"Invalid revision name: %s", rev);
+		return;
+	}
+	commit = lookup_commit_reference(the_repository, &oid);
+	if (!commit || parse_commit(commit)) {
+		cgit_print_error_page(404, "Not found",
+			"Invalid commit reference: %s", rev);
+		return;
+	}
+
+	walk_tree_ctx.curr_rev = xstrdup(rev);
+	walk_tree_ctx.match_baselen = (path_items.match) ?
+				       basedir_len(path_items.match) : -1;
+
+	read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+		  &paths, walk_tree, &walk_tree_ctx);
+	if (!walk_tree_ctx.state)
+		cgit_print_error_page(404, "Not found", "Not found");
+	else if (walk_tree_ctx.state == 2)
+		cgit_print_error_page(404, "No blame for folders",
+			"Blame is not available for folders.");
+
+	free(walk_tree_ctx.curr_rev);
+}
diff --git a/third_party/cgit/ui-blame.h b/third_party/cgit/ui-blame.h
new file mode 100644
index 0000000000..5b97e03591
--- /dev/null
+++ b/third_party/cgit/ui-blame.h
@@ -0,0 +1,6 @@
+#ifndef UI_BLAME_H
+#define UI_BLAME_H
+
+extern void cgit_print_blame(void);
+
+#endif /* UI_BLAME_H */
diff --git a/third_party/cgit/ui-blob.c b/third_party/cgit/ui-blob.c
new file mode 100644
index 0000000000..c10ae42ebd
--- /dev/null
+++ b/third_party/cgit/ui-blob.c
@@ -0,0 +1,182 @@
+/* ui-blob.c: show blob content
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-blob.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct walk_tree_context {
+	const char *match_path;
+	struct object_id *matched_oid;
+	unsigned int found_path:1;
+	unsigned int file_only:1;
+};
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+		const char *pathname, unsigned mode, void *cbdata)
+{
+	struct walk_tree_context *walk_tree_ctx = cbdata;
+
+	if (walk_tree_ctx->file_only && !S_ISREG(mode))
+		return READ_TREE_RECURSIVE;
+	if (strncmp(base->buf, walk_tree_ctx->match_path, base->len)
+		|| strcmp(walk_tree_ctx->match_path + base->len, pathname))
+		return READ_TREE_RECURSIVE;
+	oidcpy(walk_tree_ctx->matched_oid, oid);
+	walk_tree_ctx->found_path = 1;
+	return 0;
+}
+
+int cgit_ref_path_exists(const char *path, const char *ref, int file_only)
+{
+	struct object_id oid;
+	unsigned long size;
+	struct pathspec_item path_items = {
+		.match = xstrdup(path),
+		.len = strlen(path)
+	};
+	struct pathspec paths = {
+		.nr = 1,
+		.items = &path_items
+	};
+	struct walk_tree_context walk_tree_ctx = {
+		.match_path = path,
+		.matched_oid = &oid,
+		.found_path = 0,
+		.file_only = file_only
+	};
+
+	if (get_oid(ref, &oid))
+		goto done;
+	if (oid_object_info(the_repository, &oid, &size) != OBJ_COMMIT)
+		goto done;
+	read_tree(the_repository,
+		  repo_get_commit_tree(the_repository, lookup_commit_reference(the_repository, &oid)),
+		  &paths, walk_tree, &walk_tree_ctx);
+
+done:
+	free(path_items.match);
+	return walk_tree_ctx.found_path;
+}
+
+int cgit_print_file(char *path, const char *head, int file_only)
+{
+	struct object_id oid;
+	enum object_type type;
+	char *buf;
+	unsigned long size;
+	struct commit *commit;
+	struct pathspec_item path_items = {
+		.match = path,
+		.len = strlen(path)
+	};
+	struct pathspec paths = {
+		.nr = 1,
+		.items = &path_items
+	};
+	struct walk_tree_context walk_tree_ctx = {
+		.match_path = path,
+		.matched_oid = &oid,
+		.found_path = 0,
+		.file_only = file_only
+	};
+
+	if (get_oid(head, &oid))
+		return -1;
+	type = oid_object_info(the_repository, &oid, &size);
+	if (type == OBJ_COMMIT) {
+		commit = lookup_commit_reference(the_repository, &oid);
+		read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+			  &paths, walk_tree, &walk_tree_ctx);
+		if (!walk_tree_ctx.found_path)
+			return -1;
+		type = oid_object_info(the_repository, &oid, &size);
+	}
+	if (type == OBJ_BAD)
+		return -1;
+	buf = read_object_file(&oid, &type, &size);
+	if (!buf)
+		return -1;
+	buf[size] = '\0';
+	html_raw(buf, size);
+	free(buf);
+	return 0;
+}
+
+void cgit_print_blob(const char *hex, char *path, const char *head, int file_only)
+{
+	struct object_id oid;
+	enum object_type type;
+	char *buf;
+	unsigned long size;
+	struct commit *commit;
+	struct pathspec_item path_items = {
+		.match = path,
+		.len = path ? strlen(path) : 0
+	};
+	struct pathspec paths = {
+		.nr = 1,
+		.items = &path_items
+	};
+	struct walk_tree_context walk_tree_ctx = {
+		.match_path = path,
+		.matched_oid = &oid,
+		.found_path = 0,
+		.file_only = file_only
+	};
+
+	if (hex) {
+		if (get_oid_hex(hex, &oid)) {
+			cgit_print_error_page(400, "Bad request",
+					"Bad hex value: %s", hex);
+			return;
+		}
+	} else {
+		if (get_oid(head, &oid)) {
+			cgit_print_error_page(404, "Not found",
+					"Bad ref: %s", head);
+			return;
+		}
+	}
+
+	type = oid_object_info(the_repository, &oid, &size);
+
+	if ((!hex) && type == OBJ_COMMIT && path) {
+		commit = lookup_commit_reference(the_repository, &oid);
+		read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+			  &paths, walk_tree, &walk_tree_ctx);
+		type = oid_object_info(the_repository, &oid, &size);
+	}
+
+	if (type == OBJ_BAD) {
+		cgit_print_error_page(404, "Not found",
+				"Bad object name: %s", hex);
+		return;
+	}
+
+	buf = read_object_file(&oid, &type, &size);
+	if (!buf) {
+		cgit_print_error_page(500, "Internal server error",
+				"Error reading object %s", hex);
+		return;
+	}
+
+	buf[size] = '\0';
+	if (buffer_is_binary(buf, size))
+		ctx.page.mimetype = "application/octet-stream";
+	else
+		ctx.page.mimetype = "text/plain";
+	ctx.page.filename = path;
+
+	html("X-Content-Type-Options: nosniff\n");
+	html("Content-Security-Policy: default-src 'none'\n");
+	cgit_print_http_headers();
+	html_raw(buf, size);
+	free(buf);
+}
diff --git a/third_party/cgit/ui-blob.h b/third_party/cgit/ui-blob.h
new file mode 100644
index 0000000000..16847b20b1
--- /dev/null
+++ b/third_party/cgit/ui-blob.h
@@ -0,0 +1,8 @@
+#ifndef UI_BLOB_H
+#define UI_BLOB_H
+
+extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only);
+extern int cgit_print_file(char *path, const char *head, int file_only);
+extern void cgit_print_blob(const char *hex, char *path, const char *head, int file_only);
+
+#endif /* UI_BLOB_H */
diff --git a/third_party/cgit/ui-clone.c b/third_party/cgit/ui-clone.c
new file mode 100644
index 0000000000..5dccb63976
--- /dev/null
+++ b/third_party/cgit/ui-clone.c
@@ -0,0 +1,126 @@
+/* ui-clone.c: functions for http cloning, based on
+ * git's http-backend.c by Shawn O. Pearce
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-clone.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "packfile.h"
+#include "object-store.h"
+
+static int print_ref_info(const char *refname, const struct object_id *oid,
+                          int flags, void *cb_data)
+{
+	struct object *obj;
+
+	if (!(obj = parse_object(the_repository, oid)))
+		return 0;
+
+	htmlf("%s\t%s\n", oid_to_hex(oid), refname);
+	if (obj->type == OBJ_TAG) {
+		if (!(obj = deref_tag(the_repository, obj, refname, 0)))
+			return 0;
+		htmlf("%s\t%s^{}\n", oid_to_hex(&obj->oid), refname);
+	}
+	return 0;
+}
+
+static void print_pack_info(void)
+{
+	struct packed_git *pack;
+	char *offset;
+
+	ctx.page.mimetype = "text/plain";
+	ctx.page.filename = "objects/info/packs";
+	cgit_print_http_headers();
+	reprepare_packed_git(the_repository);
+	for (pack = get_packed_git(the_repository); pack; pack = pack->next) {
+		if (pack->pack_local) {
+			offset = strrchr(pack->pack_name, '/');
+			if (offset && offset[1] != '\0')
+				++offset;
+			else
+				offset = pack->pack_name;
+			htmlf("P %s\n", offset);
+		}
+	}
+}
+
+static void send_file(const char *path)
+{
+	struct stat st;
+
+	if (stat(path, &st)) {
+		switch (errno) {
+		case ENOENT:
+			cgit_print_error_page(404, "Not found", "Not found");
+			break;
+		case EACCES:
+			cgit_print_error_page(403, "Forbidden", "Forbidden");
+			break;
+		default:
+			cgit_print_error_page(400, "Bad request", "Bad request");
+		}
+		return;
+	}
+	ctx.page.mimetype = "application/octet-stream";
+	ctx.page.filename = path;
+	skip_prefix(path, ctx.repo->path, &ctx.page.filename);
+	skip_prefix(ctx.page.filename, "/", &ctx.page.filename);
+	cgit_print_http_headers();
+	html_include(path);
+}
+
+void cgit_clone_info(void)
+{
+	if (!ctx.qry.path || strcmp(ctx.qry.path, "refs")) {
+		cgit_print_error_page(400, "Bad request", "Bad request");
+		return;
+	}
+
+	ctx.page.mimetype = "text/plain";
+	ctx.page.filename = "info/refs";
+	cgit_print_http_headers();
+	for_each_ref(print_ref_info, NULL);
+}
+
+void cgit_clone_objects(void)
+{
+	char *p;
+
+	if (!ctx.qry.path)
+		goto err;
+
+	if (!strcmp(ctx.qry.path, "info/packs")) {
+		print_pack_info();
+		return;
+	}
+
+	/* Avoid directory traversal by forbidding "..", but also work around
+	 * other funny business by just specifying a fairly strict format. For
+	 * example, now we don't have to stress out about the Cygwin port.
+	 */
+	for (p = ctx.qry.path; *p; ++p) {
+		if (*p == '.' && *(p + 1) == '.')
+			goto err;
+		if (!isalnum(*p) && *p != '/' && *p != '.' && *p != '-')
+			goto err;
+	}
+
+	send_file(git_path("objects/%s", ctx.qry.path));
+	return;
+
+err:
+	cgit_print_error_page(400, "Bad request", "Bad request");
+}
+
+void cgit_clone_head(void)
+{
+	send_file(git_path("%s", "HEAD"));
+}
diff --git a/third_party/cgit/ui-clone.h b/third_party/cgit/ui-clone.h
new file mode 100644
index 0000000000..3e460a3dbc
--- /dev/null
+++ b/third_party/cgit/ui-clone.h
@@ -0,0 +1,8 @@
+#ifndef UI_CLONE_H
+#define UI_CLONE_H
+
+void cgit_clone_info(void);
+void cgit_clone_objects(void);
+void cgit_clone_head(void);
+
+#endif /* UI_CLONE_H */
diff --git a/third_party/cgit/ui-commit.c b/third_party/cgit/ui-commit.c
new file mode 100644
index 0000000000..72cb98b72a
--- /dev/null
+++ b/third_party/cgit/ui-commit.c
@@ -0,0 +1,148 @@
+/* ui-commit.c: generate commit view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-commit.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "ui-diff.h"
+#include "ui-log.h"
+
+void cgit_print_commit(char *hex, const char *prefix)
+{
+	struct commit *commit, *parent;
+	struct commitinfo *info, *parent_info;
+	struct commit_list *p;
+	struct strbuf notes = STRBUF_INIT;
+	struct object_id oid;
+	char *tmp, *tmp2;
+	int parents = 0;
+
+	if (!hex)
+		hex = ctx.qry.head;
+
+	if (get_oid(hex, &oid)) {
+		cgit_print_error_page(400, "Bad request",
+				"Bad object id: %s", hex);
+		return;
+	}
+	commit = lookup_commit_reference(the_repository, &oid);
+	if (!commit) {
+		cgit_print_error_page(404, "Not found",
+				"Bad commit reference: %s", hex);
+		return;
+	}
+	info = cgit_parse_commit(commit);
+
+	format_display_notes(&oid, &notes, PAGE_ENCODING, 1);
+
+	load_ref_decorations(NULL, DECORATE_FULL_REFS);
+
+	ctx.page.title = fmtalloc("%s - %s", info->subject, ctx.page.title);
+	cgit_print_layout_start();
+	cgit_print_diff_ctrls();
+	html("<table summary='commit info' class='commit-info'>\n");
+	html("<tr><th>author</th><td>");
+	cgit_open_filter(ctx.repo->email_filter, info->author_email, "commit");
+	html_txt(info->author);
+	if (!ctx.cfg.noplainemail) {
+		html(" ");
+		html_txt(info->author_email);
+	}
+	cgit_close_filter(ctx.repo->email_filter);
+	html("</td><td class='right'>");
+	html_txt(show_date(info->author_date, info->author_tz,
+				cgit_date_mode(DATE_DOTTIME)));
+	html("</td></tr>\n");
+	html("<tr><th>committer</th><td>");
+	cgit_open_filter(ctx.repo->email_filter, info->committer_email, "commit");
+	html_txt(info->committer);
+	if (!ctx.cfg.noplainemail) {
+		html(" ");
+		html_txt(info->committer_email);
+	}
+	cgit_close_filter(ctx.repo->email_filter);
+	html("</td><td class='right'>");
+	html_txt(show_date(info->committer_date, info->committer_tz,
+				cgit_date_mode(DATE_DOTTIME)));
+	html("</td></tr>\n");
+	html("<tr><th>commit</th><td colspan='2' class='oid'>");
+	tmp = oid_to_hex(&commit->object.oid);
+	cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp, prefix);
+	html(" (");
+	cgit_patch_link("patch", NULL, NULL, NULL, tmp, prefix);
+	html(")</td></tr>\n");
+	html("<tr><th>tree</th><td colspan='2' class='oid'>");
+	tmp = xstrdup(hex);
+	cgit_tree_link(oid_to_hex(get_commit_tree_oid(commit)), NULL, NULL,
+		       ctx.qry.head, tmp, NULL);
+	if (prefix) {
+		html(" /");
+		cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tmp, prefix);
+	}
+	free(tmp);
+	html("</td></tr>\n");
+	for (p = commit->parents; p; p = p->next) {
+		parent = lookup_commit_reference(the_repository, &p->item->object.oid);
+		if (!parent) {
+			html("<tr><td colspan='3'>");
+			cgit_print_error("Error reading parent commit");
+			html("</td></tr>");
+			continue;
+		}
+		html("<tr><th>parent</th>"
+		     "<td colspan='2' class='oid'>");
+		tmp = tmp2 = oid_to_hex(&p->item->object.oid);
+		if (ctx.repo->enable_subject_links) {
+			parent_info = cgit_parse_commit(parent);
+			tmp2 = parent_info->subject;
+		}
+		cgit_commit_link(tmp2, NULL, NULL, ctx.qry.head, tmp, prefix);
+		html(" (");
+		cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex,
+			       oid_to_hex(&p->item->object.oid), prefix);
+		html(")</td></tr>");
+		parents++;
+	}
+	if (ctx.repo->snapshots) {
+		html("<tr><th>download</th><td colspan='2' class='oid'>");
+		cgit_print_snapshot_links(ctx.repo, hex, "<br/>");
+		html("</td></tr>");
+	}
+	html("</table>\n");
+	html("<div class='commit-subject'>");
+	cgit_open_filter(ctx.repo->commit_filter);
+	html_txt(info->subject);
+	cgit_close_filter(ctx.repo->commit_filter);
+	show_commit_decorations(commit);
+	html("</div>");
+	html("<pre class='commit-msg'>");
+	cgit_open_filter(ctx.repo->commit_filter);
+	html_txt(info->msg);
+	cgit_close_filter(ctx.repo->commit_filter);
+	html("</pre>");
+	if (notes.len != 0) {
+		html("<div class='notes-header'>Notes</div>");
+		html("<div class='notes'>");
+		cgit_open_filter(ctx.repo->commit_filter);
+		html_txt(notes.buf);
+		cgit_close_filter(ctx.repo->commit_filter);
+		html("</div>");
+		html("<div class='notes-footer'></div>");
+	}
+	if (parents < 3) {
+		if (parents)
+			tmp = oid_to_hex(&commit->parents->item->object.oid);
+		else
+			tmp = NULL;
+		cgit_print_diff(ctx.qry.oid, tmp, prefix, 0, 0);
+	}
+	strbuf_release(&notes);
+	cgit_free_commitinfo(info);
+	cgit_print_layout_end();
+}
diff --git a/third_party/cgit/ui-commit.h b/third_party/cgit/ui-commit.h
new file mode 100644
index 0000000000..8198b4bacc
--- /dev/null
+++ b/third_party/cgit/ui-commit.h
@@ -0,0 +1,6 @@
+#ifndef UI_COMMIT_H
+#define UI_COMMIT_H
+
+extern void cgit_print_commit(char *hex, const char *prefix);
+
+#endif /* UI_COMMIT_H */
diff --git a/third_party/cgit/ui-diff.c b/third_party/cgit/ui-diff.c
new file mode 100644
index 0000000000..2a64ae8f14
--- /dev/null
+++ b/third_party/cgit/ui-diff.c
@@ -0,0 +1,505 @@
+/* ui-diff.c: show diff between two blobs
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-diff.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "ui-ssdiff.h"
+
+struct object_id old_rev_oid[1];
+struct object_id new_rev_oid[1];
+
+static int files, slots;
+static int total_adds, total_rems, max_changes;
+static int lines_added, lines_removed;
+
+static struct fileinfo {
+	char status;
+	struct object_id old_oid[1];
+	struct object_id new_oid[1];
+	unsigned short old_mode;
+	unsigned short new_mode;
+	char *old_path;
+	char *new_path;
+	unsigned int added;
+	unsigned int removed;
+	unsigned long old_size;
+	unsigned long new_size;
+	unsigned int binary:1;
+} *items;
+
+static int use_ssdiff = 0;
+static struct diff_filepair *current_filepair;
+static const char *current_prefix;
+
+struct diff_filespec *cgit_get_current_old_file(void)
+{
+	return current_filepair->one;
+}
+
+struct diff_filespec *cgit_get_current_new_file(void)
+{
+	return current_filepair->two;
+}
+
+static void print_fileinfo(struct fileinfo *info)
+{
+	char *class;
+
+	switch (info->status) {
+	case DIFF_STATUS_ADDED:
+		class = "add";
+		break;
+	case DIFF_STATUS_COPIED:
+		class = "cpy";
+		break;
+	case DIFF_STATUS_DELETED:
+		class = "del";
+		break;
+	case DIFF_STATUS_MODIFIED:
+		class = "upd";
+		break;
+	case DIFF_STATUS_RENAMED:
+		class = "mov";
+		break;
+	case DIFF_STATUS_TYPE_CHANGED:
+		class = "typ";
+		break;
+	case DIFF_STATUS_UNKNOWN:
+		class = "unk";
+		break;
+	case DIFF_STATUS_UNMERGED:
+		class = "stg";
+		break;
+	default:
+		die("bug: unhandled diff status %c", info->status);
+	}
+
+	html("<tr>");
+	html("<td class='mode'>");
+	if (is_null_oid(info->new_oid)) {
+		cgit_print_filemode(info->old_mode);
+	} else {
+		cgit_print_filemode(info->new_mode);
+	}
+
+	if (info->old_mode != info->new_mode &&
+	    !is_null_oid(info->old_oid) &&
+	    !is_null_oid(info->new_oid)) {
+		html("<span class='modechange'>[");
+		cgit_print_filemode(info->old_mode);
+		html("]</span>");
+	}
+	htmlf("</td><td class='%s'>", class);
+	cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.oid,
+		       ctx.qry.oid2, info->new_path);
+	if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) {
+		htmlf(" (%s from ",
+		      info->status == DIFF_STATUS_COPIED ? "copied" : "renamed");
+		html_txt(info->old_path);
+		html(")");
+	}
+	html("</td><td class='right'>");
+	if (info->binary) {
+		htmlf("bin</td><td class='graph'>%ld -> %ld bytes",
+		      info->old_size, info->new_size);
+		return;
+	}
+	htmlf("%d", info->added + info->removed);
+	html("</td><td class='graph'>");
+	htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes));
+	htmlf("<td class='add' style='width: %.1f%%;'/>",
+	      info->added * 100.0 / max_changes);
+	htmlf("<td class='rem' style='width: %.1f%%;'/>",
+	      info->removed * 100.0 / max_changes);
+	htmlf("<td class='none' style='width: %.1f%%;'/>",
+	      (max_changes - info->removed - info->added) * 100.0 / max_changes);
+	html("</tr></table></td></tr>\n");
+}
+
+static void count_diff_lines(char *line, int len)
+{
+	if (line && (len > 0)) {
+		if (line[0] == '+')
+			lines_added++;
+		else if (line[0] == '-')
+			lines_removed++;
+	}
+}
+
+static int show_filepair(struct diff_filepair *pair)
+{
+	/* Always show if we have no limiting prefix. */
+	if (!current_prefix)
+		return 1;
+
+	/* Show if either path in the pair begins with the prefix. */
+	if (starts_with(pair->one->path, current_prefix) ||
+	    starts_with(pair->two->path, current_prefix))
+		return 1;
+
+	/* Otherwise we don't want to show this filepair. */
+	return 0;
+}
+
+static void inspect_filepair(struct diff_filepair *pair)
+{
+	int binary = 0;
+	unsigned long old_size = 0;
+	unsigned long new_size = 0;
+
+	if (!show_filepair(pair))
+		return;
+
+	files++;
+	lines_added = 0;
+	lines_removed = 0;
+	cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, &new_size,
+			&binary, 0, ctx.qry.ignorews, count_diff_lines);
+	if (files >= slots) {
+		if (slots == 0)
+			slots = 4;
+		else
+			slots = slots * 2;
+		items = xrealloc(items, slots * sizeof(struct fileinfo));
+	}
+	items[files-1].status = pair->status;
+	oidcpy(items[files-1].old_oid, &pair->one->oid);
+	oidcpy(items[files-1].new_oid, &pair->two->oid);
+	items[files-1].old_mode = pair->one->mode;
+	items[files-1].new_mode = pair->two->mode;
+	items[files-1].old_path = xstrdup(pair->one->path);
+	items[files-1].new_path = xstrdup(pair->two->path);
+	items[files-1].added = lines_added;
+	items[files-1].removed = lines_removed;
+	items[files-1].old_size = old_size;
+	items[files-1].new_size = new_size;
+	items[files-1].binary = binary;
+	if (lines_added + lines_removed > max_changes)
+		max_changes = lines_added + lines_removed;
+	total_adds += lines_added;
+	total_rems += lines_removed;
+}
+
+static void cgit_print_diffstat(const struct object_id *old_oid,
+				const struct object_id *new_oid,
+				const char *prefix)
+{
+	int i;
+
+	html("<div class='diffstat-header'>");
+	cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.oid,
+		       ctx.qry.oid2, NULL);
+	if (prefix) {
+		html(" (limited to '");
+		html_txt(prefix);
+		html("')");
+	}
+	html("</div>");
+	html("<table summary='diffstat' class='diffstat'>");
+	max_changes = 0;
+	cgit_diff_tree(old_oid, new_oid, inspect_filepair, prefix,
+		       ctx.qry.ignorews);
+	for (i = 0; i<files; i++)
+		print_fileinfo(&items[i]);
+	html("</table>");
+	html("<div class='diffstat-summary'>");
+	htmlf("%d files changed, %d insertions, %d deletions",
+	      files, total_adds, total_rems);
+	html("</div>");
+}
+
+
+/*
+ * print a single line returned from xdiff
+ */
+static void print_line(char *line, int len)
+{
+	char *class = "ctx";
+	char c = line[len-1];
+
+	if (line[0] == '+')
+		class = "add";
+	else if (line[0] == '-')
+		class = "del";
+	else if (line[0] == '@')
+		class = "hunk";
+
+	htmlf("<span class='%s'>", class);
+	line[len-1] = '\0';
+	html_txt(line);
+	line[len-1] = c;
+	html("</span>\n");
+}
+
+static void header(const struct object_id *oid1, char *path1, int mode1,
+		   const struct object_id *oid2, char *path2, int mode2)
+{
+	char *abbrev1, *abbrev2;
+	int subproject;
+
+	subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
+	html("<span class='head'>");
+	html("diff --git a/");
+	html_txt(path1);
+	html(" b/");
+	html_txt(path2);
+	html("\n");
+
+	if (mode1 == 0)
+		htmlf("new file mode %.6o\n", mode2);
+
+	if (mode2 == 0)
+		htmlf("deleted file mode %.6o\n", mode1);
+
+	if (!subproject) {
+		abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV));
+		abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV));
+		htmlf("index %s..%s", abbrev1, abbrev2);
+		free(abbrev1);
+		free(abbrev2);
+		if (mode1 != 0 && mode2 != 0) {
+			htmlf(" %.6o", mode1);
+			if (mode2 != mode1)
+				htmlf("..%.6o", mode2);
+		}
+		html("\n");
+		if (is_null_oid(oid1)) {
+			path1 = "dev/null";
+			html("--- /");
+		} else
+			html("--- a/");
+		if (mode1 != 0)
+			cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
+				       oid_to_hex(old_rev_oid), path1);
+		else
+			html_txt(path1);
+		html("\n");
+		if (is_null_oid(oid2)) {
+			path2 = "dev/null";
+			html("+++ /");
+		} else
+			html("+++ b/");
+		if (mode2 != 0)
+			cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
+				       oid_to_hex(new_rev_oid), path2);
+		else
+			html_txt(path2);
+		html("\n");
+	}
+	html("</span>");
+}
+
+static void filepair_cb(struct diff_filepair *pair)
+{
+	unsigned long old_size = 0;
+	unsigned long new_size = 0;
+	int binary = 0;
+	linediff_fn print_line_fn = print_line;
+
+	if (!show_filepair(pair))
+		return;
+
+	current_filepair = pair;
+	if (use_ssdiff) {
+		cgit_ssdiff_header_begin();
+		print_line_fn = cgit_ssdiff_line_cb;
+	}
+	header(&pair->one->oid, pair->one->path, pair->one->mode,
+	       &pair->two->oid, pair->two->path, pair->two->mode);
+	if (use_ssdiff)
+		cgit_ssdiff_header_end();
+	if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
+		if (S_ISGITLINK(pair->one->mode))
+			print_line_fn(fmt("-Subproject %s", oid_to_hex(&pair->one->oid)), 52);
+		if (S_ISGITLINK(pair->two->mode))
+			print_line_fn(fmt("+Subproject %s", oid_to_hex(&pair->two->oid)), 52);
+		if (use_ssdiff)
+			cgit_ssdiff_footer();
+		return;
+	}
+	if (cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size,
+			    &new_size, &binary, ctx.qry.context,
+			    ctx.qry.ignorews, print_line_fn))
+		cgit_print_error("Error running diff");
+	if (binary) {
+		if (use_ssdiff)
+			html("<tr><td colspan='4'>Binary files differ</td></tr>");
+		else
+			html("Binary files differ");
+	}
+	if (use_ssdiff)
+		cgit_ssdiff_footer();
+}
+
+void cgit_print_diff_ctrls(void)
+{
+	int i, curr;
+
+	html("<div class='cgit-panel'>");
+	html("<b>diff options</b>");
+	html("<form method='get'>");
+	cgit_add_hidden_formfields(1, 0, ctx.qry.page);
+	html("<table>");
+	html("<tr><td colspan='2'/></tr>");
+	html("<tr>");
+	html("<td class='label'>context:</td>");
+	html("<td class='ctrl'>");
+	html("<select name='context' onchange='this.form.submit();'>");
+	curr = ctx.qry.context;
+	if (!curr)
+		curr = 3;
+	for (i = 1; i <= 10; i++)
+		html_intoption(i, fmt("%d", i), curr);
+	for (i = 15; i <= 40; i += 5)
+		html_intoption(i, fmt("%d", i), curr);
+	html("</select>");
+	html("</td>");
+	html("</tr><tr>");
+	html("<td class='label'>space:</td>");
+	html("<td class='ctrl'>");
+	html("<select name='ignorews' onchange='this.form.submit();'>");
+	html_intoption(0, "include", ctx.qry.ignorews);
+	html_intoption(1, "ignore", ctx.qry.ignorews);
+	html("</select>");
+	html("</td>");
+	html("</tr><tr>");
+	html("<td class='label'>mode:</td>");
+	html("<td class='ctrl'>");
+	html("<select name='dt' onchange='this.form.submit();'>");
+	curr = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
+	html_intoption(0, "unified", curr);
+	html_intoption(1, "ssdiff", curr);
+	html_intoption(2, "stat only", curr);
+	html("</select></td></tr>");
+	html("<tr><td/><td class='ctrl'>");
+	html("<noscript><input type='submit' value='reload'/></noscript>");
+	html("</td></tr></table>");
+	html("</form>");
+	html("</div>");
+}
+
+void cgit_print_diff(const char *new_rev, const char *old_rev,
+		     const char *prefix, int show_ctrls, int raw)
+{
+	struct commit *commit, *commit2;
+	const struct object_id *old_tree_oid, *new_tree_oid;
+	diff_type difftype;
+
+	/*
+	 * If "follow" is set then the diff machinery needs to examine the
+	 * entire commit to detect renames so we must limit the paths in our
+	 * own callbacks and not pass the prefix to the diff machinery.
+	 */
+	if (ctx.qry.follow && ctx.cfg.enable_follow_links) {
+		current_prefix = prefix;
+		prefix = "";
+	} else {
+		current_prefix = NULL;
+	}
+
+	if (!new_rev)
+		new_rev = ctx.qry.head;
+	if (get_oid(new_rev, new_rev_oid)) {
+		cgit_print_error_page(404, "Not found",
+			"Bad object name: %s", new_rev);
+		return;
+	}
+	commit = lookup_commit_reference(the_repository, new_rev_oid);
+	if (!commit || parse_commit(commit)) {
+		cgit_print_error_page(404, "Not found",
+			"Bad commit: %s", oid_to_hex(new_rev_oid));
+		return;
+	}
+	new_tree_oid = get_commit_tree_oid(commit);
+
+	if (old_rev) {
+		if (get_oid(old_rev, old_rev_oid)) {
+			cgit_print_error_page(404, "Not found",
+				"Bad object name: %s", old_rev);
+			return;
+		}
+	} else if (commit->parents && commit->parents->item) {
+		oidcpy(old_rev_oid, &commit->parents->item->object.oid);
+	} else {
+		oidclr(old_rev_oid);
+	}
+
+	if (!is_null_oid(old_rev_oid)) {
+		commit2 = lookup_commit_reference(the_repository, old_rev_oid);
+		if (!commit2 || parse_commit(commit2)) {
+			cgit_print_error_page(404, "Not found",
+				"Bad commit: %s", oid_to_hex(old_rev_oid));
+			return;
+		}
+		old_tree_oid = get_commit_tree_oid(commit2);
+	} else {
+		old_tree_oid = NULL;
+	}
+
+	if (raw) {
+		struct diff_options diffopt;
+
+		diff_setup(&diffopt);
+		diffopt.output_format = DIFF_FORMAT_PATCH;
+		diffopt.flags.recursive = 1;
+		diff_setup_done(&diffopt);
+
+		ctx.page.mimetype = "text/plain";
+		cgit_print_http_headers();
+		if (old_tree_oid) {
+			diff_tree_oid(old_tree_oid, new_tree_oid, "",
+				       &diffopt);
+		} else {
+			diff_root_tree_oid(new_tree_oid, "", &diffopt);
+		}
+		diffcore_std(&diffopt);
+		diff_flush(&diffopt);
+
+		return;
+	}
+
+	difftype = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
+	use_ssdiff = difftype == DIFF_SSDIFF;
+
+	if (show_ctrls) {
+		cgit_print_layout_start();
+		cgit_print_diff_ctrls();
+	}
+
+	/*
+	 * Clicking on a link to a file in the diff stat should show a diff
+	 * of the file, showing the diff stat limited to a single file is
+	 * pretty useless.  All links from this point on will be to
+	 * individual files, so we simply reset the difftype in the query
+	 * here to avoid propagating DIFF_STATONLY to the individual files.
+	 */
+	if (difftype == DIFF_STATONLY)
+		ctx.qry.difftype = ctx.cfg.difftype;
+
+	cgit_print_diffstat(old_rev_oid, new_rev_oid, prefix);
+
+	if (difftype == DIFF_STATONLY)
+		return;
+
+	if (use_ssdiff) {
+		html("<table summary='ssdiff' class='ssdiff'>");
+	} else {
+		html("<table summary='diff' class='diff'>");
+		html("<tr><td><pre>");
+	}
+	cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix,
+		       ctx.qry.ignorews);
+	if (!use_ssdiff)
+		html("</pre></td></tr>");
+	html("</table>");
+
+	if (show_ctrls)
+		cgit_print_layout_end();
+}
diff --git a/third_party/cgit/ui-diff.h b/third_party/cgit/ui-diff.h
new file mode 100644
index 0000000000..39264a164f
--- /dev/null
+++ b/third_party/cgit/ui-diff.h
@@ -0,0 +1,15 @@
+#ifndef UI_DIFF_H
+#define UI_DIFF_H
+
+extern void cgit_print_diff_ctrls(void);
+
+extern void cgit_print_diff(const char *new_hex, const char *old_hex,
+			    const char *prefix, int show_ctrls, int raw);
+
+extern struct diff_filespec *cgit_get_current_old_file(void);
+extern struct diff_filespec *cgit_get_current_new_file(void);
+
+extern struct object_id old_rev_oid[1];
+extern struct object_id new_rev_oid[1];
+
+#endif /* UI_DIFF_H */
diff --git a/third_party/cgit/ui-log.c b/third_party/cgit/ui-log.c
new file mode 100644
index 0000000000..cfa9192b93
--- /dev/null
+++ b/third_party/cgit/ui-log.c
@@ -0,0 +1,553 @@
+/* ui-log.c: functions for log output
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-log.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "strvec.h"
+
+static int files, add_lines, rem_lines, lines_counted;
+
+/*
+ * The list of available column colors in the commit graph.
+ */
+static const char *column_colors_html[] = {
+	"<span class='column1'>",
+	"<span class='column2'>",
+	"<span class='column3'>",
+	"<span class='column4'>",
+	"<span class='column5'>",
+	"<span class='column6'>",
+	"</span>",
+};
+
+#define COLUMN_COLORS_HTML_MAX (ARRAY_SIZE(column_colors_html) - 1)
+
+static void count_lines(char *line, int size)
+{
+	if (size <= 0)
+		return;
+
+	if (line[0] == '+')
+		add_lines++;
+
+	else if (line[0] == '-')
+		rem_lines++;
+}
+
+static void inspect_files(struct diff_filepair *pair)
+{
+	unsigned long old_size = 0;
+	unsigned long new_size = 0;
+	int binary = 0;
+
+	files++;
+	if (ctx.repo->enable_log_linecount)
+		cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size,
+				&new_size, &binary, 0, ctx.qry.ignorews,
+				count_lines);
+}
+
+void show_commit_decorations(struct commit *commit)
+{
+	const struct name_decoration *deco;
+	static char buf[1024];
+
+	buf[sizeof(buf) - 1] = 0;
+	deco = get_name_decoration(&commit->object);
+	if (!deco)
+		return;
+	html("<span class='decoration'>");
+	while (deco) {
+		struct object_id oid_tag, peeled;
+		int is_annotated = 0;
+
+		strlcpy(buf, prettify_refname(deco->name), sizeof(buf));
+		switch(deco->type) {
+		case DECORATION_NONE:
+			/* If the git-core doesn't recognize it,
+			 * don't display anything. */
+			break;
+		case DECORATION_REF_LOCAL:
+			html(" ");
+			cgit_log_link(buf, NULL, "branch-deco", buf, NULL,
+				ctx.qry.vpath, 0, NULL, NULL,
+				ctx.qry.showmsg, 0);
+			break;
+		case DECORATION_REF_TAG:
+			html(" ");
+			if (!read_ref(deco->name, &oid_tag) && !peel_iterated_oid(&oid_tag, &peeled))
+				is_annotated = !oideq(&oid_tag, &peeled);
+			cgit_tag_link(buf, NULL, is_annotated ? "tag-annotated-deco" : "tag-deco", buf);
+			break;
+		case DECORATION_REF_REMOTE:
+			if (!ctx.repo->enable_remote_branches)
+				break;
+			html(" ");
+			cgit_log_link(buf, NULL, "remote-deco", NULL,
+				oid_to_hex(&commit->object.oid),
+				ctx.qry.vpath, 0, NULL, NULL,
+				ctx.qry.showmsg, 0);
+			break;
+		default:
+			html(" ");
+			cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
+					oid_to_hex(&commit->object.oid),
+					ctx.qry.vpath);
+			break;
+		}
+		deco = deco->next;
+	}
+	html("</span>");
+}
+
+static void handle_rename(struct diff_filepair *pair)
+{
+	/*
+	 * After we have seen a rename, we generate links to the previous
+	 * name of the file so that commit & diff views get fed the path
+	 * that is correct for the commit they are showing, avoiding the
+	 * need to walk the entire history leading back to every commit we
+	 * show in order detect renames.
+	 */
+	if (0 != strcmp(ctx.qry.vpath, pair->two->path)) {
+		free(ctx.qry.vpath);
+		ctx.qry.vpath = xstrdup(pair->two->path);
+	}
+	inspect_files(pair);
+}
+
+static int show_commit(struct commit *commit, struct rev_info *revs)
+{
+	struct commit_list *parents = commit->parents;
+	struct commit *parent;
+	int found = 0, saved_fmt;
+	struct diff_flags saved_flags = revs->diffopt.flags;
+
+	/* Always show if we're not in "follow" mode with a single file. */
+	if (!ctx.qry.follow)
+		return 1;
+
+	/*
+	 * In "follow" mode, we don't show merges.  This is consistent with
+	 * "git log --follow -- <file>".
+	 */
+	if (parents && parents->next)
+		return 0;
+
+	/*
+	 * If this is the root commit, do what rev_info tells us.
+	 */
+	if (!parents)
+		return revs->show_root_diff;
+
+	/* When we get here we have precisely one parent. */
+	parent = parents->item;
+	/* If we can't parse the commit, let print_commit() report an error. */
+	if (parse_commit(parent))
+		return 1;
+
+	files = 0;
+	add_lines = 0;
+	rem_lines = 0;
+
+	revs->diffopt.flags.recursive = 1;
+	diff_tree_oid(get_commit_tree_oid(parent),
+		      get_commit_tree_oid(commit),
+		      "", &revs->diffopt);
+	diffcore_std(&revs->diffopt);
+
+	found = !diff_queue_is_empty(&revs->diffopt);
+	saved_fmt = revs->diffopt.output_format;
+	revs->diffopt.output_format = DIFF_FORMAT_CALLBACK;
+	revs->diffopt.format_callback = cgit_diff_tree_cb;
+	revs->diffopt.format_callback_data = handle_rename;
+	diff_flush(&revs->diffopt);
+	revs->diffopt.output_format = saved_fmt;
+	revs->diffopt.flags = saved_flags;
+
+	lines_counted = 1;
+	return found;
+}
+
+static void print_commit(struct commit *commit, struct rev_info *revs)
+{
+	struct commitinfo *info;
+	int columns = revs->graph ? 4 : 3;
+	struct strbuf graphbuf = STRBUF_INIT;
+	struct strbuf msgbuf = STRBUF_INIT;
+
+	if (ctx.repo->enable_log_filecount)
+		columns++;
+	if (ctx.repo->enable_log_linecount)
+		columns++;
+
+	if (revs->graph) {
+		/* Advance graph until current commit */
+		while (!graph_next_line(revs->graph, &graphbuf)) {
+			/* Print graph segment in otherwise empty table row */
+			html("<tr class='nohover'><td class='commitgraph'>");
+			html(graphbuf.buf);
+			htmlf("</td><td colspan='%d' /></tr>\n", columns);
+			strbuf_setlen(&graphbuf, 0);
+		}
+		/* Current commit's graph segment is now ready in graphbuf */
+	}
+
+	info = cgit_parse_commit(commit);
+	htmlf("<tr%s>", ctx.qry.showmsg ? " class='logheader'" : "");
+
+	if (revs->graph) {
+		/* Print graph segment for current commit */
+		html("<td class='commitgraph'>");
+		html(graphbuf.buf);
+		html("</td>");
+		strbuf_setlen(&graphbuf, 0);
+	}
+	else {
+		html("<td>");
+		cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2);
+		html("</td>");
+	}
+
+	htmlf("<td%s>", ctx.qry.showmsg ? " class='logsubject'" : "");
+	if (ctx.qry.showmsg) {
+		/* line-wrap long commit subjects instead of truncating them */
+		size_t subject_len = strlen(info->subject);
+
+		if (subject_len > ctx.cfg.max_msg_len &&
+		    ctx.cfg.max_msg_len >= 15) {
+			/* symbol for signaling line-wrap (in PAGE_ENCODING) */
+			const char wrap_symbol[] = { ' ', 0xE2, 0x86, 0xB5, 0 };
+			int i = ctx.cfg.max_msg_len - strlen(wrap_symbol);
+
+			/* Rewind i to preceding space character */
+			while (i > 0 && !isspace(info->subject[i]))
+				--i;
+			if (!i) /* Oops, zero spaces. Reset i */
+				i = ctx.cfg.max_msg_len - strlen(wrap_symbol);
+
+			/* add remainder starting at i to msgbuf */
+			strbuf_add(&msgbuf, info->subject + i, subject_len - i);
+			strbuf_trim(&msgbuf);
+			strbuf_add(&msgbuf, "\n\n", 2);
+
+			/* Place wrap_symbol at position i in info->subject */
+			strlcpy(info->subject + i, wrap_symbol, subject_len - i + 1);
+		}
+	}
+	cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
+			 oid_to_hex(&commit->object.oid), ctx.qry.vpath);
+	show_commit_decorations(commit);
+	html("</td><td>");
+	cgit_open_filter(ctx.repo->email_filter, info->author_email, "log");
+	html_txt(info->author);
+	cgit_close_filter(ctx.repo->email_filter);
+
+	if (revs->graph) {
+		html("</td><td>");
+		cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2);
+	}
+
+	if (!lines_counted && (ctx.repo->enable_log_filecount ||
+			       ctx.repo->enable_log_linecount)) {
+		files = 0;
+		add_lines = 0;
+		rem_lines = 0;
+		cgit_diff_commit(commit, inspect_files, ctx.qry.vpath);
+	}
+
+	if (ctx.repo->enable_log_filecount)
+		htmlf("</td><td>%d", files);
+	if (ctx.repo->enable_log_linecount)
+		htmlf("</td><td><span class='deletions'>-%d</span>/"
+			"<span class='insertions'>+%d</span>", rem_lines, add_lines);
+
+	html("</td></tr>\n");
+
+	if ((revs->graph && !graph_is_commit_finished(revs->graph))
+			|| ctx.qry.showmsg) { /* Print a second table row */
+		html("<tr class='nohover-highlight'>");
+
+		if (ctx.qry.showmsg) {
+			/* Concatenate commit message + notes in msgbuf */
+			if (info->msg && *(info->msg)) {
+				strbuf_addstr(&msgbuf, info->msg);
+				strbuf_addch(&msgbuf, '\n');
+			}
+			format_display_notes(&commit->object.oid,
+					     &msgbuf, PAGE_ENCODING, 0);
+			strbuf_addch(&msgbuf, '\n');
+			strbuf_ltrim(&msgbuf);
+		}
+
+		if (revs->graph) {
+			int lines = 0;
+
+			/* Calculate graph padding */
+			if (ctx.qry.showmsg) {
+				/* Count #lines in commit message + notes */
+				const char *p = msgbuf.buf;
+				lines = 1;
+				while ((p = strchr(p, '\n'))) {
+					p++;
+					lines++;
+				}
+			}
+
+			/* Print graph padding */
+			html("<td class='commitgraph'>");
+			while (lines > 0 || !graph_is_commit_finished(revs->graph)) {
+				if (graphbuf.len)
+					html("\n");
+				strbuf_setlen(&graphbuf, 0);
+				graph_next_line(revs->graph, &graphbuf);
+				html(graphbuf.buf);
+				lines--;
+			}
+			html("</td>\n");
+		}
+		else
+			html("<td/>"); /* Empty 'Age' column */
+
+		/* Print msgbuf into remainder of table row */
+		htmlf("<td colspan='%d'%s>\n", columns - (revs->graph ? 1 : 0),
+			ctx.qry.showmsg ? " class='logmsg'" : "");
+		html_txt(msgbuf.buf);
+		html("</td></tr>\n");
+	}
+
+	strbuf_release(&msgbuf);
+	strbuf_release(&graphbuf);
+	cgit_free_commitinfo(info);
+}
+
+static const char *disambiguate_ref(const char *ref, int *must_free_result)
+{
+	struct object_id oid;
+	struct strbuf longref = STRBUF_INIT;
+
+	strbuf_addf(&longref, "refs/heads/%s", ref);
+	if (get_oid(longref.buf, &oid) == 0) {
+		*must_free_result = 1;
+		return strbuf_detach(&longref, NULL);
+	}
+
+	*must_free_result = 0;
+	strbuf_release(&longref);
+	return ref;
+}
+
+static char *next_token(char **src)
+{
+	char *result;
+
+	if (!src || !*src)
+		return NULL;
+	while (isspace(**src))
+		(*src)++;
+	if (!**src)
+		return NULL;
+	result = *src;
+	while (**src) {
+		if (isspace(**src)) {
+			**src = '\0';
+			(*src)++;
+			break;
+		}
+		(*src)++;
+	}
+	return result;
+}
+
+void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern,
+		    const char *path, int pager, int commit_graph, int commit_sort)
+{
+	struct rev_info rev;
+	struct commit *commit;
+	struct strvec rev_argv = STRVEC_INIT;
+	int i, columns = commit_graph ? 4 : 3;
+	int must_free_tip = 0;
+
+	/* rev_argv.argv[0] will be ignored by setup_revisions */
+	strvec_push(&rev_argv, "log_rev_setup");
+
+	if (!tip)
+		tip = ctx.qry.head;
+	tip = disambiguate_ref(tip, &must_free_tip);
+	strvec_push(&rev_argv, tip);
+
+	if (grep && pattern && *pattern) {
+		pattern = xstrdup(pattern);
+		if (!strcmp(grep, "grep") || !strcmp(grep, "author") ||
+		    !strcmp(grep, "committer")) {
+			strvec_pushf(&rev_argv, "--%s=%s", grep, pattern);
+		} else if (!strcmp(grep, "range")) {
+			char *arg;
+			/* Split the pattern at whitespace and add each token
+			 * as a revision expression. Do not accept other
+			 * rev-list options. Also, replace the previously
+			 * pushed tip (it's no longer relevant).
+			 */
+			strvec_pop(&rev_argv);
+			while ((arg = next_token(&pattern))) {
+				if (*arg == '-') {
+					fprintf(stderr, "Bad range expr: %s\n",
+						arg);
+					break;
+				}
+				strvec_push(&rev_argv, arg);
+			}
+		}
+	}
+
+	if (!path || !ctx.cfg.enable_follow_links) {
+		/*
+		 * If we don't have a path, "follow" is a no-op so make sure
+		 * the variable is set to false to avoid needing to check
+		 * both this and whether we have a path everywhere.
+		 */
+		ctx.qry.follow = 0;
+	}
+
+	if (commit_graph && !ctx.qry.follow) {
+		strvec_push(&rev_argv, "--graph");
+		strvec_push(&rev_argv, "--color");
+		graph_set_column_colors(column_colors_html,
+					COLUMN_COLORS_HTML_MAX);
+	}
+
+	if (commit_sort == 1)
+		strvec_push(&rev_argv, "--date-order");
+	else if (commit_sort == 2)
+		strvec_push(&rev_argv, "--topo-order");
+
+	if (path && ctx.qry.follow)
+		strvec_push(&rev_argv, "--follow");
+	strvec_push(&rev_argv, "--");
+	if (path)
+		strvec_push(&rev_argv, path);
+
+	init_revisions(&rev, NULL);
+	rev.abbrev = DEFAULT_ABBREV;
+	rev.commit_format = CMIT_FMT_DEFAULT;
+	rev.verbose_header = 1;
+	rev.show_root_diff = 0;
+	rev.ignore_missing = 1;
+	rev.simplify_history = 1;
+	setup_revisions(rev_argv.nr, rev_argv.v, &rev, NULL);
+	load_ref_decorations(NULL, DECORATE_FULL_REFS);
+	rev.show_decorations = 1;
+	rev.grep_filter.ignore_case = 1;
+
+	rev.diffopt.detect_rename = 1;
+	rev.diffopt.rename_limit = ctx.cfg.renamelimit;
+	if (ctx.qry.ignorews)
+		DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE);
+
+	compile_grep_patterns(&rev.grep_filter);
+	prepare_revision_walk(&rev);
+
+	if (pager) {
+		cgit_print_layout_start();
+		html("<table class='list nowrap'>");
+	}
+
+	html("<tr class='nohover'>");
+	if (commit_graph)
+		html("<th></th>");
+	else
+		html("<th class='left'>Age</th>");
+	html("<th class='left'>Commit message");
+	if (pager) {
+		html(" (");
+		cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
+			      NULL, ctx.qry.head, ctx.qry.oid,
+			      ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep,
+			      ctx.qry.search, ctx.qry.showmsg ? 0 : 1,
+			      ctx.qry.follow);
+		html(")");
+	}
+	html("</th><th class='left'>Author</th>");
+	if (rev.graph)
+		html("<th class='left'>Age</th>");
+	if (ctx.repo->enable_log_filecount) {
+		html("<th class='left'>Files</th>");
+		columns++;
+	}
+	if (ctx.repo->enable_log_linecount) {
+		html("<th class='left'>Lines</th>");
+		columns++;
+	}
+	html("</tr>\n");
+
+	if (ofs<0)
+		ofs = 0;
+
+	for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; /* nop */) {
+		if (show_commit(commit, &rev))
+			i++;
+		release_commit_memory(the_repository->parsed_objects, commit);
+		commit->parents = NULL;
+	}
+
+	for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; /* nop */) {
+		/*
+		 * In "follow" mode, we must count the files and lines the
+		 * first time we invoke diff on a given commit, and we need
+		 * to do that to see if the commit touches the path we care
+		 * about, so we do it in show_commit.  Hence we must clear
+		 * lines_counted here.
+		 *
+		 * This has the side effect of avoiding running diff twice
+		 * when we are both following renames and showing file
+		 * and/or line counts.
+		 */
+		lines_counted = 0;
+		if (show_commit(commit, &rev)) {
+			i++;
+			print_commit(commit, &rev);
+		}
+		release_commit_memory(the_repository->parsed_objects, commit);
+		commit->parents = NULL;
+	}
+	if (pager) {
+		html("</table><ul class='pager'>");
+		if (ofs > 0) {
+			html("<li>");
+			cgit_log_link("[prev]", NULL, NULL, ctx.qry.head,
+				      ctx.qry.oid, ctx.qry.vpath,
+				      ofs - cnt, ctx.qry.grep,
+				      ctx.qry.search, ctx.qry.showmsg,
+				      ctx.qry.follow);
+			html("</li>");
+		}
+		if ((commit = get_revision(&rev)) != NULL) {
+			html("<li>");
+			cgit_log_link("[next]", NULL, NULL, ctx.qry.head,
+				      ctx.qry.oid, ctx.qry.vpath,
+				      ofs + cnt, ctx.qry.grep,
+				      ctx.qry.search, ctx.qry.showmsg,
+				      ctx.qry.follow);
+			html("</li>");
+		}
+		html("</ul>");
+		cgit_print_layout_end();
+	} else if ((commit = get_revision(&rev)) != NULL) {
+		htmlf("<tr class='nohover'><td colspan='%d'>", columns);
+		cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL,
+			      ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg,
+			      ctx.qry.follow);
+		html("</td></tr>\n");
+	}
+
+	/* If we allocated tip then it is safe to cast away const. */
+	if (must_free_tip)
+		free((char*) tip);
+}
diff --git a/third_party/cgit/ui-log.h b/third_party/cgit/ui-log.h
new file mode 100644
index 0000000000..325607cdab
--- /dev/null
+++ b/third_party/cgit/ui-log.h
@@ -0,0 +1,9 @@
+#ifndef UI_LOG_H
+#define UI_LOG_H
+
+extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep,
+			   char *pattern, const char *path, int pager,
+			   int commit_graph, int commit_sort);
+extern void show_commit_decorations(struct commit *commit);
+
+#endif /* UI_LOG_H */
diff --git a/third_party/cgit/ui-patch.c b/third_party/cgit/ui-patch.c
new file mode 100644
index 0000000000..4ac03cbef1
--- /dev/null
+++ b/third_party/cgit/ui-patch.c
@@ -0,0 +1,98 @@
+/* ui-patch.c: generate patch view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-patch.h"
+#include "html.h"
+#include "ui-shared.h"
+
+/* two commit hashes with two dots in between and termination */
+#define REV_RANGE_LEN 2 * GIT_MAX_HEXSZ + 3
+
+void cgit_print_patch(const char *new_rev, const char *old_rev,
+		      const char *prefix)
+{
+	struct rev_info rev;
+	struct commit *commit;
+	struct object_id new_rev_oid, old_rev_oid;
+	char rev_range[REV_RANGE_LEN];
+	const char *rev_argv[] = { NULL, "--reverse", "--format=email", rev_range, "--", prefix, NULL };
+	int rev_argc = ARRAY_SIZE(rev_argv) - 1;
+	char *patchname;
+
+	if (!prefix)
+		rev_argc--;
+
+	if (!new_rev)
+		new_rev = ctx.qry.head;
+
+	if (get_oid(new_rev, &new_rev_oid)) {
+		cgit_print_error_page(404, "Not found",
+				"Bad object id: %s", new_rev);
+		return;
+	}
+	commit = lookup_commit_reference(the_repository, &new_rev_oid);
+	if (!commit) {
+		cgit_print_error_page(404, "Not found",
+				"Bad commit reference: %s", new_rev);
+		return;
+	}
+
+	if (old_rev) {
+		if (get_oid(old_rev, &old_rev_oid)) {
+			cgit_print_error_page(404, "Not found",
+					"Bad object id: %s", old_rev);
+			return;
+		}
+		if (!lookup_commit_reference(the_repository, &old_rev_oid)) {
+			cgit_print_error_page(404, "Not found",
+					"Bad commit reference: %s", old_rev);
+			return;
+		}
+	} else if (commit->parents && commit->parents->item) {
+		oidcpy(&old_rev_oid, &commit->parents->item->object.oid);
+	} else {
+		oidclr(&old_rev_oid);
+	}
+
+	if (is_null_oid(&old_rev_oid)) {
+		memcpy(rev_range, oid_to_hex(&new_rev_oid), the_hash_algo->hexsz + 1);
+	} else {
+		xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_rev_oid),
+			oid_to_hex(&new_rev_oid));
+	}
+
+	patchname = fmt("%s.patch", rev_range);
+	ctx.page.mimetype = "text/plain";
+	ctx.page.filename = patchname;
+	cgit_print_http_headers();
+
+	if (ctx.cfg.noplainemail) {
+		rev_argv[2] = "--format=format:From %H Mon Sep 17 00:00:00 "
+			      "2001%nFrom: %an%nDate: %aD%n%w(78,0,1)Subject: "
+			      "%s%n%n%w(0)%b";
+	}
+
+	init_revisions(&rev, NULL);
+	rev.abbrev = DEFAULT_ABBREV;
+	rev.verbose_header = 1;
+	rev.diff = 1;
+	rev.show_root_diff = 1;
+	rev.max_parents = 1;
+	rev.diffopt.output_format |= DIFF_FORMAT_DIFFSTAT |
+			DIFF_FORMAT_PATCH | DIFF_FORMAT_SUMMARY;
+	if (prefix)
+		rev.diffopt.stat_sep = fmt("(limited to '%s')\n\n", prefix);
+	setup_revisions(rev_argc, rev_argv, &rev, NULL);
+	prepare_revision_walk(&rev);
+
+	while ((commit = get_revision(&rev)) != NULL) {
+		log_tree_commit(&rev, commit);
+		printf("-- \ncgit %s\n\n", cgit_version);
+	}
+}
diff --git a/third_party/cgit/ui-patch.h b/third_party/cgit/ui-patch.h
new file mode 100644
index 0000000000..7a6cacd5ac
--- /dev/null
+++ b/third_party/cgit/ui-patch.h
@@ -0,0 +1,7 @@
+#ifndef UI_PATCH_H
+#define UI_PATCH_H
+
+extern void cgit_print_patch(const char *new_rev, const char *old_rev,
+			     const char *prefix);
+
+#endif /* UI_PATCH_H */
diff --git a/third_party/cgit/ui-plain.c b/third_party/cgit/ui-plain.c
new file mode 100644
index 0000000000..65a205fad7
--- /dev/null
+++ b/third_party/cgit/ui-plain.c
@@ -0,0 +1,207 @@
+/* ui-plain.c: functions for output of plain blobs by path
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-plain.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct walk_tree_context {
+	int match_baselen;
+	int match;
+};
+
+static int print_object(const struct object_id *oid, const char *path)
+{
+	enum object_type type;
+	char *buf, *mimetype;
+	unsigned long size;
+
+	type = oid_object_info(the_repository, oid, &size);
+	if (type == OBJ_BAD) {
+		cgit_print_error_page(404, "Not found", "Not found");
+		return 0;
+	}
+
+	buf = read_object_file(oid, &type, &size);
+	if (!buf) {
+		cgit_print_error_page(404, "Not found", "Not found");
+		return 0;
+	}
+
+	mimetype = get_mimetype_for_filename(path);
+	ctx.page.mimetype = mimetype;
+
+	if (!ctx.repo->enable_html_serving) {
+		html("X-Content-Type-Options: nosniff\n");
+		html("Content-Security-Policy: default-src 'none'\n");
+		if (mimetype) {
+			/* Built-in white list allows PDF and everything that isn't text/ and application/ */
+			if ((!strncmp(mimetype, "text/", 5) || !strncmp(mimetype, "application/", 12)) && strcmp(mimetype, "application/pdf"))
+				ctx.page.mimetype = NULL;
+		}
+	}
+
+	if (!ctx.page.mimetype) {
+		if (buffer_is_binary(buf, size)) {
+			ctx.page.mimetype = "application/octet-stream";
+			ctx.page.charset = NULL;
+		} else {
+			ctx.page.mimetype = "text/plain";
+		}
+	}
+	ctx.page.filename = path;
+	ctx.page.size = size;
+	ctx.page.etag = oid_to_hex(oid);
+	cgit_print_http_headers();
+	html_raw(buf, size);
+	free(mimetype);
+	free(buf);
+	return 1;
+}
+
+static char *buildpath(const char *base, int baselen, const char *path)
+{
+	if (path[0])
+		return fmtalloc("%.*s%s/", baselen, base, path);
+	else
+		return fmtalloc("%.*s/", baselen, base);
+}
+
+static void print_dir(const struct object_id *oid, const char *base,
+		      int baselen, const char *path)
+{
+	char *fullpath, *slash;
+	size_t len;
+
+	fullpath = buildpath(base, baselen, path);
+	slash = (fullpath[0] == '/' ? "" : "/");
+	ctx.page.etag = oid_to_hex(oid);
+	cgit_print_http_headers();
+	htmlf("<html><head><title>%s", slash);
+	html_txt(fullpath);
+	htmlf("</title></head>\n<body>\n<h2>%s", slash);
+	html_txt(fullpath);
+	html("</h2>\n<ul>\n");
+	len = strlen(fullpath);
+	if (len > 1) {
+		fullpath[len - 1] = 0;
+		slash = strrchr(fullpath, '/');
+		if (slash)
+			*(slash + 1) = 0;
+		else {
+			free(fullpath);
+			fullpath = NULL;
+		}
+		html("<li>");
+		cgit_plain_link("../", NULL, NULL, ctx.qry.head, ctx.qry.oid,
+				fullpath);
+		html("</li>\n");
+	}
+	free(fullpath);
+}
+
+static void print_dir_entry(const struct object_id *oid, const char *base,
+			    int baselen, const char *path, unsigned mode)
+{
+	char *fullpath;
+
+	fullpath = buildpath(base, baselen, path);
+	if (!S_ISDIR(mode) && !S_ISGITLINK(mode))
+		fullpath[strlen(fullpath) - 1] = 0;
+	html("  <li>");
+	if (S_ISGITLINK(mode)) {
+		cgit_submodule_link(NULL, fullpath, oid_to_hex(oid));
+	} else
+		cgit_plain_link(path, NULL, NULL, ctx.qry.head, ctx.qry.oid,
+				fullpath);
+	html("</li>\n");
+	free(fullpath);
+}
+
+static void print_dir_tail(void)
+{
+	html(" </ul>\n</body></html>\n");
+}
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+		const char *pathname, unsigned mode, void *cbdata)
+{
+	struct walk_tree_context *walk_tree_ctx = cbdata;
+
+	if (base->len == walk_tree_ctx->match_baselen) {
+		if (S_ISREG(mode) || S_ISLNK(mode)) {
+			if (print_object(oid, pathname))
+				walk_tree_ctx->match = 1;
+		} else if (S_ISDIR(mode)) {
+			print_dir(oid, base->buf, base->len, pathname);
+			walk_tree_ctx->match = 2;
+			return READ_TREE_RECURSIVE;
+		}
+	} else if (base->len < INT_MAX && (int)base->len > walk_tree_ctx->match_baselen) {
+		print_dir_entry(oid, base->buf, base->len, pathname, mode);
+		walk_tree_ctx->match = 2;
+	} else if (S_ISDIR(mode)) {
+		return READ_TREE_RECURSIVE;
+	}
+
+	return 0;
+}
+
+static int basedir_len(const char *path)
+{
+	char *p = strrchr(path, '/');
+	if (p)
+		return p - path + 1;
+	return 0;
+}
+
+void cgit_print_plain(void)
+{
+	const char *rev = ctx.qry.oid;
+	struct object_id oid;
+	struct commit *commit;
+	struct pathspec_item path_items = {
+		.match = ctx.qry.path,
+		.len = ctx.qry.path ? strlen(ctx.qry.path) : 0
+	};
+	struct pathspec paths = {
+		.nr = 1,
+		.items = &path_items
+	};
+	struct walk_tree_context walk_tree_ctx = {
+		.match = 0
+	};
+
+	if (!rev)
+		rev = ctx.qry.head;
+
+	if (get_oid(rev, &oid)) {
+		cgit_print_error_page(404, "Not found", "Not found");
+		return;
+	}
+	commit = lookup_commit_reference(the_repository, &oid);
+	if (!commit || parse_commit(commit)) {
+		cgit_print_error_page(404, "Not found", "Not found");
+		return;
+	}
+	if (!path_items.match) {
+		path_items.match = "";
+		walk_tree_ctx.match_baselen = -1;
+		print_dir(get_commit_tree_oid(commit), "", 0, "");
+		walk_tree_ctx.match = 2;
+	}
+	else
+		walk_tree_ctx.match_baselen = basedir_len(path_items.match);
+	read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+		  &paths, walk_tree, &walk_tree_ctx);
+	if (!walk_tree_ctx.match)
+		cgit_print_error_page(404, "Not found", "Not found");
+	else if (walk_tree_ctx.match == 2)
+		print_dir_tail();
+}
diff --git a/third_party/cgit/ui-plain.h b/third_party/cgit/ui-plain.h
new file mode 100644
index 0000000000..5bff07b83b
--- /dev/null
+++ b/third_party/cgit/ui-plain.h
@@ -0,0 +1,6 @@
+#ifndef UI_PLAIN_H
+#define UI_PLAIN_H
+
+extern void cgit_print_plain(void);
+
+#endif /* UI_PLAIN_H */
diff --git a/third_party/cgit/ui-refs.c b/third_party/cgit/ui-refs.c
new file mode 100644
index 0000000000..456f610df4
--- /dev/null
+++ b/third_party/cgit/ui-refs.c
@@ -0,0 +1,219 @@
+/* ui-refs.c: browse symbolic refs
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-refs.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static inline int cmp_age(int age1, int age2)
+{
+	/* age1 and age2 are assumed to be non-negative */
+	return age2 - age1;
+}
+
+static int cmp_ref_name(const void *a, const void *b)
+{
+	struct refinfo *r1 = *(struct refinfo **)a;
+	struct refinfo *r2 = *(struct refinfo **)b;
+
+	return strcmp(r1->refname, r2->refname);
+}
+
+static int cmp_branch_age(const void *a, const void *b)
+{
+	struct refinfo *r1 = *(struct refinfo **)a;
+	struct refinfo *r2 = *(struct refinfo **)b;
+
+	return cmp_age(r1->commit->committer_date, r2->commit->committer_date);
+}
+
+static int get_ref_age(struct refinfo *ref)
+{
+	if (!ref->object)
+		return 0;
+	switch (ref->object->type) {
+	case OBJ_TAG:
+		return ref->tag ? ref->tag->tagger_date : 0;
+	case OBJ_COMMIT:
+		return ref->commit ? ref->commit->committer_date : 0;
+	}
+	return 0;
+}
+
+static int cmp_tag_age(const void *a, const void *b)
+{
+	struct refinfo *r1 = *(struct refinfo **)a;
+	struct refinfo *r2 = *(struct refinfo **)b;
+
+	return cmp_age(get_ref_age(r1), get_ref_age(r2));
+}
+
+static int print_branch(struct refinfo *ref)
+{
+	struct commitinfo *info = ref->commit;
+	char *name = (char *)ref->refname;
+
+	if (!info)
+		return 1;
+	html("<tr><td>");
+	cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL,
+		      ctx.qry.showmsg, 0);
+	html("</td><td>");
+
+	if (ref->object->type == OBJ_COMMIT) {
+		cgit_commit_link(info->subject, NULL, NULL, name, NULL, NULL);
+		html("</td><td>");
+		cgit_open_filter(ctx.repo->email_filter, info->author_email, "refs");
+		html_txt(info->author);
+		cgit_close_filter(ctx.repo->email_filter);
+		html("</td><td colspan='2'>");
+		cgit_print_age(info->committer_date, info->committer_tz, -1);
+	} else {
+		html("</td><td></td><td>");
+		cgit_object_link(ref->object);
+	}
+	html("</td></tr>\n");
+	return 0;
+}
+
+static void print_tag_header(void)
+{
+	html("<tr class='nohover'><th class='left'>Tag</th>"
+	     "<th class='left'>Download</th>"
+	     "<th class='left'>Author</th>"
+	     "<th class='left' colspan='2'>Age</th></tr>\n");
+}
+
+static int print_tag(struct refinfo *ref)
+{
+	struct tag *tag = NULL;
+	struct taginfo *info = NULL;
+	char *name = (char *)ref->refname;
+	struct object *obj = ref->object;
+
+	if (obj->type == OBJ_TAG) {
+		tag = (struct tag *)obj;
+		obj = tag->tagged;
+		info = ref->tag;
+		if (!info)
+			return 1;
+	}
+
+	html("<tr><td>");
+	cgit_tag_link(name, NULL, NULL, name);
+	html("</td><td>");
+	if (ctx.repo->snapshots && (obj->type == OBJ_COMMIT))
+		cgit_print_snapshot_links(ctx.repo, name, "&nbsp;&nbsp;");
+	else
+		cgit_object_link(obj);
+	html("</td><td>");
+	if (info) {
+		if (info->tagger) {
+			cgit_open_filter(ctx.repo->email_filter, info->tagger_email, "refs");
+			html_txt(info->tagger);
+			cgit_close_filter(ctx.repo->email_filter);
+		}
+	} else if (ref->object->type == OBJ_COMMIT) {
+		cgit_open_filter(ctx.repo->email_filter, ref->commit->author_email, "refs");
+		html_txt(ref->commit->author);
+		cgit_close_filter(ctx.repo->email_filter);
+	}
+	html("</td><td colspan='2'>");
+	if (info) {
+		if (info->tagger_date > 0)
+			cgit_print_age(info->tagger_date, info->tagger_tz, -1);
+	} else if (ref->object->type == OBJ_COMMIT) {
+		cgit_print_age(ref->commit->commit->date, 0, -1);
+	}
+	html("</td></tr>\n");
+
+	return 0;
+}
+
+static void print_refs_link(const char *path)
+{
+	html("<tr class='nohover'><td colspan='5'>");
+	cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
+	html("</td></tr>");
+}
+
+void cgit_print_branches(int maxcount)
+{
+	struct reflist list;
+	int i;
+
+	html("<tr class='nohover'><th class='left'>Branch</th>"
+	     "<th class='left'>Commit message</th>"
+	     "<th class='left'>Author</th>"
+	     "<th class='left' colspan='2'>Age</th></tr>\n");
+
+	list.refs = NULL;
+	list.alloc = list.count = 0;
+	for_each_branch_ref(cgit_refs_cb, &list);
+	if (ctx.repo->enable_remote_branches)
+		for_each_remote_ref(cgit_refs_cb, &list);
+
+	if (maxcount == 0 || maxcount > list.count)
+		maxcount = list.count;
+
+	qsort(list.refs, list.count, sizeof(*list.refs), cmp_branch_age);
+	if (ctx.repo->branch_sort == 0)
+		qsort(list.refs, maxcount, sizeof(*list.refs), cmp_ref_name);
+
+	for (i = 0; i < maxcount; i++)
+		print_branch(list.refs[i]);
+
+	if (maxcount < list.count)
+		print_refs_link("heads");
+
+	cgit_free_reflist_inner(&list);
+}
+
+void cgit_print_tags(int maxcount)
+{
+	struct reflist list;
+	int i;
+
+	list.refs = NULL;
+	list.alloc = list.count = 0;
+	for_each_tag_ref(cgit_refs_cb, &list);
+	if (list.count == 0)
+		return;
+	qsort(list.refs, list.count, sizeof(*list.refs), cmp_tag_age);
+	if (!maxcount)
+		maxcount = list.count;
+	else if (maxcount > list.count)
+		maxcount = list.count;
+	print_tag_header();
+	for (i = 0; i < maxcount; i++)
+		print_tag(list.refs[i]);
+
+	if (maxcount < list.count)
+		print_refs_link("tags");
+
+	cgit_free_reflist_inner(&list);
+}
+
+void cgit_print_refs(void)
+{
+	cgit_print_layout_start();
+	html("<table class='list nowrap'>");
+
+	if (ctx.qry.path && starts_with(ctx.qry.path, "heads"))
+		cgit_print_branches(0);
+	else if (ctx.qry.path && starts_with(ctx.qry.path, "tags"))
+		cgit_print_tags(0);
+	else {
+		cgit_print_branches(0);
+		html("<tr class='nohover'><td colspan='5'>&nbsp;</td></tr>");
+		cgit_print_tags(0);
+	}
+	html("</table>");
+	cgit_print_layout_end();
+}
diff --git a/third_party/cgit/ui-refs.h b/third_party/cgit/ui-refs.h
new file mode 100644
index 0000000000..1d4a54a2c8
--- /dev/null
+++ b/third_party/cgit/ui-refs.h
@@ -0,0 +1,8 @@
+#ifndef UI_REFS_H
+#define UI_REFS_H
+
+extern void cgit_print_branches(int maxcount);
+extern void cgit_print_tags(int maxcount);
+extern void cgit_print_refs(void);
+
+#endif /* UI_REFS_H */
diff --git a/third_party/cgit/ui-repolist.c b/third_party/cgit/ui-repolist.c
new file mode 100644
index 0000000000..97b11c5f16
--- /dev/null
+++ b/third_party/cgit/ui-repolist.c
@@ -0,0 +1,381 @@
+/* ui-repolist.c: functions for generating the repolist page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-repolist.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static time_t read_agefile(const char *path)
+{
+	time_t result;
+	size_t size;
+	char *buf = NULL;
+	struct strbuf date_buf = STRBUF_INIT;
+
+	if (readfile(path, &buf, &size)) {
+		free(buf);
+		return 0;
+	}
+
+	if (parse_date(buf, &date_buf) == 0)
+		result = strtoul(date_buf.buf, NULL, 10);
+	else
+		result = 0;
+	free(buf);
+	strbuf_release(&date_buf);
+	return result;
+}
+
+static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
+{
+	struct strbuf path = STRBUF_INIT;
+	struct stat s;
+	struct cgit_repo *r = (struct cgit_repo *)repo;
+
+	if (repo->mtime != -1) {
+		*mtime = repo->mtime;
+		return 1;
+	}
+	strbuf_addf(&path, "%s/%s", repo->path, ctx.cfg.agefile);
+	if (stat(path.buf, &s) == 0) {
+		*mtime = read_agefile(path.buf);
+		if (*mtime) {
+			r->mtime = *mtime;
+			goto end;
+		}
+	}
+
+	strbuf_reset(&path);
+	strbuf_addf(&path, "%s/refs/heads/%s", repo->path,
+		    repo->defbranch ? repo->defbranch : "master");
+	if (stat(path.buf, &s) == 0) {
+		*mtime = s.st_mtime;
+		r->mtime = *mtime;
+		goto end;
+	}
+
+	strbuf_reset(&path);
+	strbuf_addf(&path, "%s/%s", repo->path, "packed-refs");
+	if (stat(path.buf, &s) == 0) {
+		*mtime = s.st_mtime;
+		r->mtime = *mtime;
+		goto end;
+	}
+
+	*mtime = 0;
+	r->mtime = *mtime;
+end:
+	strbuf_release(&path);
+	return (r->mtime != 0);
+}
+
+static void print_modtime(struct cgit_repo *repo)
+{
+	time_t t;
+	if (get_repo_modtime(repo, &t))
+		cgit_print_age(t, 0, -1);
+}
+
+static int is_match(struct cgit_repo *repo)
+{
+	if (!ctx.qry.search)
+		return 1;
+	if (repo->url && strcasestr(repo->url, ctx.qry.search))
+		return 1;
+	if (repo->name && strcasestr(repo->name, ctx.qry.search))
+		return 1;
+	if (repo->desc && strcasestr(repo->desc, ctx.qry.search))
+		return 1;
+	if (repo->owner && strcasestr(repo->owner, ctx.qry.search))
+		return 1;
+	return 0;
+}
+
+static int is_in_url(struct cgit_repo *repo)
+{
+	if (!ctx.qry.url)
+		return 1;
+	if (repo->url && starts_with(repo->url, ctx.qry.url))
+		return 1;
+	return 0;
+}
+
+static int is_visible(struct cgit_repo *repo)
+{
+	if (repo->hide || repo->ignore)
+		return 0;
+	if (!(is_match(repo) && is_in_url(repo)))
+		return 0;
+	return 1;
+}
+
+static int any_repos_visible(void)
+{
+	int i;
+
+	for (i = 0; i < cgit_repolist.count; i++) {
+		if (is_visible(&cgit_repolist.repos[i]))
+			return 1;
+	}
+	return 0;
+}
+
+static void print_sort_header(const char *title, const char *sort)
+{
+	char *currenturl = cgit_currenturl();
+	html("<th class='left'><a href='");
+	html_attr(currenturl);
+	htmlf("?s=%s", sort);
+	if (ctx.qry.search) {
+		html("&amp;q=");
+		html_url_arg(ctx.qry.search);
+	}
+	htmlf("'>%s</a></th>", title);
+	free(currenturl);
+}
+
+static void print_header(void)
+{
+	html("<tr class='nohover'>");
+	print_sort_header("Name", "name");
+	print_sort_header("Description", "desc");
+	if (ctx.cfg.enable_index_owner)
+		print_sort_header("Owner", "owner");
+	print_sort_header("Idle", "idle");
+	if (ctx.cfg.enable_index_links)
+		html("<th class='left'>Links</th>");
+	html("</tr>\n");
+}
+
+
+static void print_pager(int items, int pagelen, char *search, char *sort)
+{
+	int i, ofs;
+	char *class = NULL;
+	html("<ul class='pager'>");
+	for (i = 0, ofs = 0; ofs < items; i++, ofs = i * pagelen) {
+		class = (ctx.qry.ofs == ofs) ? "current" : NULL;
+		html("<li>");
+		cgit_index_link(fmt("[%d]", i + 1), fmt("Page %d", i + 1),
+				class, search, sort, ofs, 0);
+		html("</li>");
+	}
+	html("</ul>");
+}
+
+static int cmp(const char *s1, const char *s2)
+{
+	if (s1 && s2) {
+		if (ctx.cfg.case_sensitive_sort)
+			return strcmp(s1, s2);
+		else
+			return strcasecmp(s1, s2);
+	}
+	if (s1 && !s2)
+		return -1;
+	if (s2 && !s1)
+		return 1;
+	return 0;
+}
+
+static int sort_name(const void *a, const void *b)
+{
+	const struct cgit_repo *r1 = a;
+	const struct cgit_repo *r2 = b;
+
+	return cmp(r1->name, r2->name);
+}
+
+static int sort_desc(const void *a, const void *b)
+{
+	const struct cgit_repo *r1 = a;
+	const struct cgit_repo *r2 = b;
+
+	return cmp(r1->desc, r2->desc);
+}
+
+static int sort_owner(const void *a, const void *b)
+{
+	const struct cgit_repo *r1 = a;
+	const struct cgit_repo *r2 = b;
+
+	return cmp(r1->owner, r2->owner);
+}
+
+static int sort_idle(const void *a, const void *b)
+{
+	const struct cgit_repo *r1 = a;
+	const struct cgit_repo *r2 = b;
+	time_t t1, t2;
+
+	t1 = t2 = 0;
+	get_repo_modtime(r1, &t1);
+	get_repo_modtime(r2, &t2);
+	return t2 - t1;
+}
+
+static int sort_section(const void *a, const void *b)
+{
+	const struct cgit_repo *r1 = a;
+	const struct cgit_repo *r2 = b;
+	int result;
+
+	result = cmp(r1->section, r2->section);
+	if (!result) {
+		if (!strcmp(ctx.cfg.repository_sort, "age"))
+			result = sort_idle(r1, r2);
+		if (!result)
+			result = cmp(r1->name, r2->name);
+	}
+	return result;
+}
+
+struct sortcolumn {
+	const char *name;
+	int (*fn)(const void *a, const void *b);
+};
+
+static const struct sortcolumn sortcolumn[] = {
+	{"section", sort_section},
+	{"name", sort_name},
+	{"desc", sort_desc},
+	{"owner", sort_owner},
+	{"idle", sort_idle},
+	{NULL, NULL}
+};
+
+static int sort_repolist(char *field)
+{
+	const struct sortcolumn *column;
+
+	for (column = &sortcolumn[0]; column->name; column++) {
+		if (strcmp(field, column->name))
+			continue;
+		qsort(cgit_repolist.repos, cgit_repolist.count,
+			sizeof(struct cgit_repo), column->fn);
+		return 1;
+	}
+	return 0;
+}
+
+
+void cgit_print_repolist(void)
+{
+	int i, columns = 3, hits = 0, header = 0;
+	char *last_section = NULL;
+	char *section;
+	char *repourl;
+	int sorted = 0;
+
+	if (!any_repos_visible()) {
+		cgit_print_error_page(404, "Not found", "No repositories found");
+		return;
+	}
+
+	if (ctx.cfg.enable_index_links)
+		++columns;
+	if (ctx.cfg.enable_index_owner)
+		++columns;
+
+	ctx.page.title = ctx.cfg.root_title;
+	cgit_print_http_headers();
+	cgit_print_docstart();
+	cgit_print_pageheader();
+
+	if (ctx.qry.sort)
+		sorted = sort_repolist(ctx.qry.sort);
+	else if (ctx.cfg.section_sort)
+		sort_repolist("section");
+
+	html("<table summary='repository list' class='list nowrap'>");
+	for (i = 0; i < cgit_repolist.count; i++) {
+		ctx.repo = &cgit_repolist.repos[i];
+		if (!is_visible(ctx.repo))
+			continue;
+		hits++;
+		if (hits <= ctx.qry.ofs)
+			continue;
+		if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count)
+			continue;
+		if (!header++)
+			print_header();
+		section = ctx.repo->section;
+		if (section && !strcmp(section, ""))
+			section = NULL;
+		if (!sorted &&
+		    ((last_section == NULL && section != NULL) ||
+		    (last_section != NULL && section == NULL) ||
+		    (last_section != NULL && section != NULL &&
+		     strcmp(section, last_section)))) {
+			htmlf("<tr class='nohover-highlight'><td colspan='%d' class='reposection'>",
+			      columns);
+			html_txt(section);
+			html("</td></tr>");
+			last_section = section;
+		}
+		htmlf("<tr><td class='%s'>",
+		      !sorted && section ? "sublevel-repo" : "toplevel-repo");
+		cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
+		html("</td><td>");
+		repourl = cgit_repourl(ctx.repo->url);
+		html_link_open(repourl, NULL, NULL);
+		free(repourl);
+		if (html_ntxt(ctx.repo->desc, ctx.cfg.max_repodesc_len) < 0)
+			html("...");
+		html_link_close();
+		html("</td><td>");
+		if (ctx.cfg.enable_index_owner) {
+			if (ctx.repo->owner_filter) {
+				cgit_open_filter(ctx.repo->owner_filter);
+				html_txt(ctx.repo->owner);
+				cgit_close_filter(ctx.repo->owner_filter);
+			} else {
+				char *currenturl = cgit_currenturl();
+				html("<a href='");
+				html_attr(currenturl);
+				html("?q=");
+				html_url_arg(ctx.repo->owner);
+				html("'>");
+				html_txt(ctx.repo->owner);
+				html("</a>");
+				free(currenturl);
+			}
+			html("</td><td>");
+		}
+		print_modtime(ctx.repo);
+		html("</td>");
+		if (ctx.cfg.enable_index_links) {
+			html("<td>");
+			cgit_summary_link("summary", NULL, "button", NULL);
+			html(" ");
+			cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
+				      0, NULL, NULL, ctx.qry.showmsg, 0);
+			html(" ");
+			cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
+			html("</td>");
+		}
+		html("</tr>\n");
+	}
+	html("</table>");
+	if (hits > ctx.cfg.max_repo_count)
+		print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search, ctx.qry.sort);
+	cgit_print_docend();
+}
+
+void cgit_print_site_readme(void)
+{
+	cgit_print_layout_start();
+	if (!ctx.cfg.root_readme)
+		goto done;
+	cgit_open_filter(ctx.cfg.about_filter, ctx.cfg.root_readme);
+	html_include(ctx.cfg.root_readme);
+	cgit_close_filter(ctx.cfg.about_filter);
+done:
+	cgit_print_layout_end();
+}
diff --git a/third_party/cgit/ui-repolist.h b/third_party/cgit/ui-repolist.h
new file mode 100644
index 0000000000..1b6b3227db
--- /dev/null
+++ b/third_party/cgit/ui-repolist.h
@@ -0,0 +1,7 @@
+#ifndef UI_REPOLIST_H
+#define UI_REPOLIST_H
+
+extern void cgit_print_repolist(void);
+extern void cgit_print_site_readme(void);
+
+#endif /* UI_REPOLIST_H */
diff --git a/third_party/cgit/ui-shared.c b/third_party/cgit/ui-shared.c
new file mode 100644
index 0000000000..2e8896c982
--- /dev/null
+++ b/third_party/cgit/ui-shared.c
@@ -0,0 +1,1239 @@
+/* ui-shared.c: common web output functions
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-shared.h"
+#include "cmd.h"
+#include "html.h"
+#include "version.h"
+
+static const char cgit_doctype[] =
+"<!DOCTYPE html>\n";
+
+static char *http_date(time_t t)
+{
+	static char day[][4] =
+		{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+	static char month[][4] =
+		{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+		 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+	struct tm tm;
+	gmtime_r(&t, &tm);
+	return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm.tm_wday],
+		   tm.tm_mday, month[tm.tm_mon], 1900 + tm.tm_year,
+		   tm.tm_hour, tm.tm_min, tm.tm_sec);
+}
+
+void cgit_print_error(const char *fmt, ...)
+{
+	va_list ap;
+	va_start(ap, fmt);
+	cgit_vprint_error(fmt, ap);
+	va_end(ap);
+}
+
+void cgit_vprint_error(const char *fmt, va_list ap)
+{
+	va_list cp;
+	html("<div class='error'>");
+	va_copy(cp, ap);
+	html_vtxtf(fmt, cp);
+	va_end(cp);
+	html("</div>\n");
+}
+
+const char *cgit_httpscheme(void)
+{
+	if (ctx.env.https && !strcmp(ctx.env.https, "on"))
+		return "https://";
+	else
+		return "http://";
+}
+
+char *cgit_hosturl(void)
+{
+	if (ctx.env.http_host)
+		return xstrdup(ctx.env.http_host);
+	if (!ctx.env.server_name)
+		return NULL;
+	if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
+		return xstrdup(ctx.env.server_name);
+	return fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port);
+}
+
+char *cgit_currenturl(void)
+{
+	const char *root = cgit_rooturl();
+
+	if (!ctx.qry.url)
+		return xstrdup(root);
+	if (root[0] && root[strlen(root) - 1] == '/')
+		return fmtalloc("%s%s", root, ctx.qry.url);
+	return fmtalloc("%s/%s", root, ctx.qry.url);
+}
+
+char *cgit_currentfullurl(void)
+{
+	const char *root = cgit_rooturl();
+	const char *orig_query = ctx.env.query_string ? ctx.env.query_string : "";
+	size_t len = strlen(orig_query);
+	char *query = xmalloc(len + 2), *start_url, *ret;
+
+	/* Remove all url=... parts from query string */
+	memcpy(query + 1, orig_query, len + 1);
+	query[0] = '?';
+	start_url = query;
+	while ((start_url = strstr(start_url, "url=")) != NULL) {
+		if (start_url[-1] == '?' || start_url[-1] == '&') {
+			const char *end_url = strchr(start_url, '&');
+			if (end_url)
+				memmove(start_url, end_url + 1, strlen(end_url));
+			else
+				start_url[0] = '\0';
+		} else
+			++start_url;
+	}
+	if (!query[1])
+		query[0] = '\0';
+
+	if (!ctx.qry.url)
+		ret = fmtalloc("%s%s", root, query);
+	else if (root[0] && root[strlen(root) - 1] == '/')
+		ret = fmtalloc("%s%s%s", root, ctx.qry.url, query);
+	else
+		ret = fmtalloc("%s/%s%s", root, ctx.qry.url, query);
+	free(query);
+	return ret;
+}
+
+const char *cgit_rooturl(void)
+{
+	if (ctx.cfg.virtual_root)
+		return ctx.cfg.virtual_root;
+	else
+		return ctx.cfg.script_name;
+}
+
+const char *cgit_loginurl(void)
+{
+	static const char *login_url;
+	if (!login_url)
+		login_url = fmtalloc("%s?p=login", cgit_rooturl());
+	return login_url;
+}
+
+char *cgit_repourl(const char *reponame)
+{
+	// my cgit instance *only* serves the depot, hence that's the only value ever
+	// needed.
+	return fmtalloc("/");
+}
+
+char *cgit_fileurl(const char *reponame, const char *pagename,
+		   const char *filename, const char *query)
+{
+	struct strbuf sb = STRBUF_INIT;
+
+	strbuf_addf(&sb, "%s%s/%s", ctx.cfg.virtual_root,
+		pagename, (filename ? filename:""));
+
+	if (query) {
+		strbuf_addf(&sb, "%s%s", "?", query);
+	}
+
+	return strbuf_detach(&sb, NULL);
+}
+
+char *cgit_pageurl(const char *reponame, const char *pagename,
+		   const char *query)
+{
+	return cgit_fileurl(reponame, pagename, NULL, query);
+}
+
+const char *cgit_repobasename(const char *reponame)
+{
+	/* I assume we don't need to store more than one repo basename */
+	static char rvbuf[1024];
+	int p;
+	const char *rv;
+	size_t len;
+
+	len = strlcpy(rvbuf, reponame, sizeof(rvbuf));
+	if (len >= sizeof(rvbuf))
+		die("cgit_repobasename: truncated repository name '%s'", reponame);
+	p = len - 1;
+	/* strip trailing slashes */
+	while (p && rvbuf[p] == '/')
+		rvbuf[p--] = '\0';
+	/* strip trailing .git */
+	if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) {
+		p -= 3;
+		rvbuf[p--] = '\0';
+	}
+	/* strip more trailing slashes if any */
+	while (p && rvbuf[p] == '/')
+		rvbuf[p--] = '\0';
+	/* find last slash in the remaining string */
+	rv = strrchr(rvbuf, '/');
+	if (rv)
+		return ++rv;
+	return rvbuf;
+}
+
+const char *cgit_snapshot_prefix(const struct cgit_repo *repo)
+{
+	if (repo->snapshot_prefix)
+		return repo->snapshot_prefix;
+
+	return cgit_repobasename(repo->url);
+}
+
+static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root)
+{
+	char *delim = "?";
+
+	if (always_root || page)
+		html_attr(cgit_rooturl());
+	else {
+		char *currenturl = cgit_currenturl();
+		html_attr(currenturl);
+		free(currenturl);
+	}
+
+	if (page) {
+		htmlf("?p=%s", page);
+		delim = "&amp;";
+	}
+	if (search) {
+		html(delim);
+		html("q=");
+		html_attr(search);
+		delim = "&amp;";
+	}
+	if (sort) {
+		html(delim);
+		html("s=");
+		html_attr(sort);
+		delim = "&amp;";
+	}
+	if (ofs) {
+		html(delim);
+		htmlf("ofs=%d", ofs);
+	}
+}
+
+static void site_link(const char *page, const char *name, const char *title,
+		      const char *class, const char *search, const char *sort, int ofs, int always_root)
+{
+	html("<a");
+	if (title) {
+		html(" title='");
+		html_attr(title);
+		html("'");
+	}
+	if (class) {
+		html(" class='");
+		html_attr(class);
+		html("'");
+	}
+	html(" href='");
+	site_url(page, search, sort, ofs, always_root);
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_index_link(const char *name, const char *title, const char *class,
+		     const char *pattern, const char *sort, int ofs, int always_root)
+{
+	site_link(NULL, name, title, class, pattern, sort, ofs, always_root);
+}
+
+static char *repolink(const char *title, const char *class, const char *page,
+		      const char *head, const char *path)
+{
+	char *delim = "?";
+
+	html("<a");
+	if (title) {
+		html(" title='");
+		html_attr(title);
+		html("'");
+	}
+	if (class) {
+		html(" class='");
+		html_attr(class);
+		html("'");
+	}
+	html(" href='");
+	if (ctx.cfg.virtual_root) {
+		html_url_path(ctx.cfg.virtual_root);
+		if (page) {
+			html_url_path(page);
+			html("/");
+			if (path)
+				html_url_path(path);
+		}
+	} else {
+		html_url_path(ctx.cfg.script_name);
+		html("?url=");
+		html_url_arg(ctx.repo->url);
+		if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
+			html("/");
+		if (page) {
+			html_url_arg(page);
+			html("/");
+			if (path)
+				html_url_arg(path);
+		}
+		delim = "&amp;";
+	}
+	if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) {
+		html(delim);
+		html("h=");
+		html_url_arg(head);
+		delim = "&amp;";
+	}
+	return fmt("%s", delim);
+}
+
+static void reporevlink(const char *page, const char *name, const char *title,
+			const char *class, const char *head, const char *rev,
+			const char *path)
+{
+	char *delim;
+
+	delim = repolink(title, class, page, head, path);
+	if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) {
+		html(delim);
+		html("id=");
+		html_url_arg(rev);
+	}
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_summary_link(const char *name, const char *title, const char *class,
+		       const char *head)
+{
+	reporevlink(NULL, name, title, class, head, NULL, NULL);
+}
+
+void cgit_tag_link(const char *name, const char *title, const char *class,
+		   const char *tag)
+{
+	reporevlink("tag", name, title, class, tag, NULL, NULL);
+}
+
+void cgit_about_link(const char *name, const char *title, const char *class,
+		    const char *head, const char *rev, const char *path)
+{
+	reporevlink("about", name, title, class, head, rev, path);
+}
+
+void cgit_tree_link(const char *name, const char *title, const char *class,
+		    const char *head, const char *rev, const char *path)
+{
+	reporevlink("tree", name, title, class, head, rev, path);
+}
+
+void cgit_plain_link(const char *name, const char *title, const char *class,
+		     const char *head, const char *rev, const char *path)
+{
+	reporevlink("plain", name, title, class, head, rev, path);
+}
+
+void cgit_blame_link(const char *name, const char *title, const char *class,
+		     const char *head, const char *rev, const char *path)
+{
+	reporevlink("blame", name, title, class, head, rev, path);
+}
+
+void cgit_log_link(const char *name, const char *title, const char *class,
+		   const char *head, const char *rev, const char *path,
+		   int ofs, const char *grep, const char *pattern, int showmsg,
+		   int follow)
+{
+	char *delim;
+
+	delim = repolink(title, class, "log", head, path);
+	if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
+		html(delim);
+		html("id=");
+		html_url_arg(rev);
+		delim = "&amp;";
+	}
+	if (grep && pattern) {
+		html(delim);
+		html("qt=");
+		html_url_arg(grep);
+		delim = "&amp;";
+		html(delim);
+		html("q=");
+		html_url_arg(pattern);
+	}
+	if (ofs > 0) {
+		html(delim);
+		html("ofs=");
+		htmlf("%d", ofs);
+		delim = "&amp;";
+	}
+	if (showmsg) {
+		html(delim);
+		html("showmsg=1");
+		delim = "&amp;";
+	}
+	if (follow) {
+		html(delim);
+		html("follow=1");
+	}
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_commit_link(const char *name, const char *title, const char *class,
+		      const char *head, const char *rev, const char *path)
+{
+	char *delim;
+
+	delim = repolink(title, class, "commit", head, path);
+	if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
+		html(delim);
+		html("id=");
+		html_url_arg(rev);
+		delim = "&amp;";
+	}
+	if (ctx.qry.difftype) {
+		html(delim);
+		htmlf("dt=%d", ctx.qry.difftype);
+		delim = "&amp;";
+	}
+	if (ctx.qry.context > 0 && ctx.qry.context != 3) {
+		html(delim);
+		html("context=");
+		htmlf("%d", ctx.qry.context);
+		delim = "&amp;";
+	}
+	if (ctx.qry.ignorews) {
+		html(delim);
+		html("ignorews=1");
+		delim = "&amp;";
+	}
+	if (ctx.qry.follow) {
+		html(delim);
+		html("follow=1");
+	}
+	html("'>");
+	if (name[0] != '\0') {
+		if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
+			html_ntxt(name, ctx.cfg.max_msg_len - 3);
+			html("...");
+		} else
+			html_txt(name);
+	} else
+		html_txt("(no commit message)");
+	html("</a>");
+}
+
+void cgit_refs_link(const char *name, const char *title, const char *class,
+		    const char *head, const char *rev, const char *path)
+{
+	reporevlink("refs", name, title, class, head, rev, path);
+}
+
+void cgit_snapshot_link(const char *name, const char *title, const char *class,
+			const char *head, const char *rev,
+			const char *archivename)
+{
+	reporevlink("snapshot", name, title, class, head, rev, archivename);
+}
+
+void cgit_diff_link(const char *name, const char *title, const char *class,
+		    const char *head, const char *new_rev, const char *old_rev,
+		    const char *path)
+{
+	char *delim;
+
+	delim = repolink(title, class, "diff", head, path);
+	if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) {
+		html(delim);
+		html("id=");
+		html_url_arg(new_rev);
+		delim = "&amp;";
+	}
+	if (old_rev) {
+		html(delim);
+		html("id2=");
+		html_url_arg(old_rev);
+		delim = "&amp;";
+	}
+	if (ctx.qry.difftype) {
+		html(delim);
+		htmlf("dt=%d", ctx.qry.difftype);
+		delim = "&amp;";
+	}
+	if (ctx.qry.context > 0 && ctx.qry.context != 3) {
+		html(delim);
+		html("context=");
+		htmlf("%d", ctx.qry.context);
+		delim = "&amp;";
+	}
+	if (ctx.qry.ignorews) {
+		html(delim);
+		html("ignorews=1");
+		delim = "&amp;";
+	}
+	if (ctx.qry.follow) {
+		html(delim);
+		html("follow=1");
+	}
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_patch_link(const char *name, const char *title, const char *class,
+		     const char *head, const char *rev, const char *path)
+{
+	reporevlink("patch", name, title, class, head, rev, path);
+}
+
+void cgit_stats_link(const char *name, const char *title, const char *class,
+		     const char *head, const char *path)
+{
+	reporevlink("stats", name, title, class, head, NULL, path);
+}
+
+static void cgit_self_link(char *name, const char *title, const char *class)
+{
+	if (!strcmp(ctx.qry.page, "repolist"))
+		cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort,
+				ctx.qry.ofs, 1);
+        else if (!strcmp(ctx.qry.page, "about"))
+		cgit_about_link(name, title, class, ctx.qry.head,
+			        ctx.qry.has_oid ? ctx.qry.oid : NULL,
+			        ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "summary"))
+		cgit_summary_link(name, title, class, ctx.qry.head);
+	else if (!strcmp(ctx.qry.page, "tag"))
+		cgit_tag_link(name, title, class, ctx.qry.has_oid ?
+			       ctx.qry.oid : ctx.qry.head);
+	else if (!strcmp(ctx.qry.page, "tree"))
+		cgit_tree_link(name, title, class, ctx.qry.head,
+			       ctx.qry.has_oid ? ctx.qry.oid : NULL,
+			       ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "plain"))
+		cgit_plain_link(name, title, class, ctx.qry.head,
+				ctx.qry.has_oid ? ctx.qry.oid : NULL,
+				ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "blame"))
+		cgit_blame_link(name, title, class, ctx.qry.head,
+				ctx.qry.has_oid ? ctx.qry.oid : NULL,
+				ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "log"))
+		cgit_log_link(name, title, class, ctx.qry.head,
+			      ctx.qry.has_oid ? ctx.qry.oid : NULL,
+			      ctx.qry.path, ctx.qry.ofs,
+			      ctx.qry.grep, ctx.qry.search,
+			      ctx.qry.showmsg, ctx.qry.follow);
+	else if (!strcmp(ctx.qry.page, "commit"))
+		cgit_commit_link(name, title, class, ctx.qry.head,
+				 ctx.qry.has_oid ? ctx.qry.oid : NULL,
+				 ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "patch"))
+		cgit_patch_link(name, title, class, ctx.qry.head,
+				ctx.qry.has_oid ? ctx.qry.oid : NULL,
+				ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "refs"))
+		cgit_refs_link(name, title, class, ctx.qry.head,
+			       ctx.qry.has_oid ? ctx.qry.oid : NULL,
+			       ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "snapshot"))
+		cgit_snapshot_link(name, title, class, ctx.qry.head,
+				   ctx.qry.has_oid ? ctx.qry.oid : NULL,
+				   ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "diff"))
+		cgit_diff_link(name, title, class, ctx.qry.head,
+			       ctx.qry.oid, ctx.qry.oid2,
+			       ctx.qry.path);
+	else if (!strcmp(ctx.qry.page, "stats"))
+		cgit_stats_link(name, title, class, ctx.qry.head,
+				ctx.qry.path);
+	else {
+		/* Don't known how to make link for this page */
+		repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
+		html("><!-- cgit_self_link() doesn't know how to make link for page '");
+		html_txt(ctx.qry.page);
+		html("' -->");
+		html_txt(name);
+		html("</a>");
+	}
+}
+
+void cgit_object_link(struct object *obj)
+{
+	char *page, *shortrev, *fullrev, *name;
+
+	fullrev = oid_to_hex(&obj->oid);
+	shortrev = xstrdup(fullrev);
+	shortrev[10] = '\0';
+	if (obj->type == OBJ_COMMIT) {
+		cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
+				 ctx.qry.head, fullrev, NULL);
+		return;
+	} else if (obj->type == OBJ_TREE)
+		page = "tree";
+	else if (obj->type == OBJ_TAG)
+		page = "tag";
+	else
+		page = "blob";
+	name = fmt("%s %s...", type_name(obj->type), shortrev);
+	reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
+}
+
+static struct string_list_item *lookup_path(struct string_list *list,
+					    const char *path)
+{
+	struct string_list_item *item;
+
+	while (path && path[0]) {
+		if ((item = string_list_lookup(list, path)))
+			return item;
+		if (!(path = strchr(path, '/')))
+			break;
+		path++;
+	}
+	return NULL;
+}
+
+void cgit_submodule_link(const char *class, char *path, const char *rev)
+{
+	struct string_list *list;
+	struct string_list_item *item;
+	char tail, *dir;
+	size_t len;
+
+	len = 0;
+	tail = 0;
+	list = &ctx.repo->submodules;
+	item = lookup_path(list, path);
+	if (!item) {
+		len = strlen(path);
+		tail = path[len - 1];
+		if (tail == '/') {
+			path[len - 1] = 0;
+			item = lookup_path(list, path);
+		}
+	}
+	if (item || ctx.repo->module_link) {
+		html("<a ");
+		if (class)
+			htmlf("class='%s' ", class);
+		html("href='");
+		if (item) {
+			html_attrf(item->util, rev);
+		} else {
+			dir = strrchr(path, '/');
+			if (dir)
+				dir++;
+			else
+				dir = path;
+			html_attrf(ctx.repo->module_link, dir, rev);
+		}
+		html("'>");
+		html_txt(path);
+		html("</a>");
+	} else {
+		html("<span");
+		if (class)
+			htmlf(" class='%s'", class);
+		html(">");
+		html_txt(path);
+		html("</span>");
+	}
+	html_txtf(" @ %.7s", rev);
+	if (item && tail)
+		path[len - 1] = tail;
+}
+
+const struct date_mode *cgit_date_mode(enum date_mode_type type)
+{
+	static struct date_mode mode;
+	mode.type = type;
+	mode.local = ctx.cfg.local_time;
+	return &mode;
+}
+
+static void print_rel_date(time_t t, int tz, double value,
+	const char *class, const char *suffix)
+{
+	htmlf("<span class='%s' title='", class);
+	html_attr(show_date(t, tz, cgit_date_mode(DATE_DOTTIME)));
+	htmlf("'>%.0f %s</span>", value, suffix);
+}
+
+void cgit_print_age(time_t t, int tz, time_t max_relative)
+{
+	time_t now, secs;
+
+	if (!t)
+		return;
+	time(&now);
+	secs = now - t;
+	if (secs < 0)
+		secs = 0;
+
+	if (secs > max_relative && max_relative >= 0) {
+		html("<span title='");
+		html_attr(show_date(t, tz, cgit_date_mode(DATE_DOTTIME)));
+		html("'>");
+		html_txt(show_date(t, tz, cgit_date_mode(DATE_SHORT)));
+		html("</span>");
+		return;
+	}
+
+	if (secs < TM_HOUR * 2) {
+		print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min.");
+		return;
+	}
+	if (secs < TM_DAY * 2) {
+		print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours");
+		return;
+	}
+	if (secs < TM_WEEK * 2) {
+		print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days");
+		return;
+	}
+	if (secs < TM_MONTH * 2) {
+		print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks");
+		return;
+	}
+	if (secs < TM_YEAR * 2) {
+		print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months");
+		return;
+	}
+	print_rel_date(t, tz, secs * 1.0 / TM_YEAR, "age-years", "years");
+}
+
+void cgit_print_http_headers(void)
+{
+	if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
+		return;
+
+	if (ctx.page.status)
+		htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg);
+	if (ctx.page.mimetype && ctx.page.charset)
+		htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype,
+		      ctx.page.charset);
+	else if (ctx.page.mimetype)
+		htmlf("Content-Type: %s\n", ctx.page.mimetype);
+	if (ctx.page.size)
+		htmlf("Content-Length: %zd\n", ctx.page.size);
+	if (ctx.page.filename) {
+		html("Content-Disposition: inline; filename=\"");
+		html_header_arg_in_quotes(ctx.page.filename);
+		html("\"\n");
+	}
+	if (!ctx.env.authenticated)
+		html("Cache-Control: no-cache, no-store\n");
+	htmlf("Last-Modified: %s\n", http_date(ctx.page.modified));
+	htmlf("Expires: %s\n", http_date(ctx.page.expires));
+	if (ctx.page.etag)
+		htmlf("ETag: \"%s\"\n", ctx.page.etag);
+	html("\n");
+	if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
+		exit(0);
+}
+
+void cgit_redirect(const char *url, bool permanent)
+{
+	htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
+	html("Location: ");
+	html_url_path(url);
+	html("\n\n");
+}
+
+static void print_rel_vcs_link(const char *url)
+{
+	html("<link rel='vcs-git' href='");
+	html_attr(url);
+	html("' title='");
+	html_attr(ctx.repo->name);
+	html(" Git repository'/>\n");
+}
+
+void cgit_print_docstart(void)
+{
+	char *host = cgit_hosturl();
+
+	if (ctx.cfg.embedded) {
+		if (ctx.cfg.header)
+			html_include(ctx.cfg.header);
+		return;
+	}
+
+	html(cgit_doctype);
+	html("<html lang='en'>\n");
+	html("<head>\n");
+	html("<title>");
+	html_txt(ctx.page.title);
+	html("</title>\n");
+	htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
+	if (ctx.cfg.robots && *ctx.cfg.robots)
+		htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots);
+	html("<link rel='stylesheet' type='text/css' href='");
+	html_attr(ctx.cfg.css);
+	html("'/>\n");
+	if (ctx.cfg.favicon) {
+		html("<link rel='shortcut icon' href='");
+		html_attr(ctx.cfg.favicon);
+		html("'/>\n");
+	}
+	if (host && ctx.repo && ctx.qry.head) {
+		char *fileurl;
+		struct strbuf sb = STRBUF_INIT;
+		strbuf_addf(&sb, "h=%s", ctx.qry.head);
+
+		html("<link rel='alternate' title='Atom feed' href='");
+		html(cgit_httpscheme());
+		html_attr(host);
+		fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath,
+				       sb.buf);
+		html_attr(fileurl);
+		html("' type='application/atom+xml'/>\n");
+		strbuf_release(&sb);
+		free(fileurl);
+	}
+	if (ctx.repo)
+		cgit_add_clone_urls(print_rel_vcs_link);
+	if (ctx.cfg.head_include)
+		html_include(ctx.cfg.head_include);
+	if (ctx.repo && ctx.repo->extra_head_content)
+		html(ctx.repo->extra_head_content);
+	html("</head>\n");
+	html("<body>\n");
+	if (ctx.cfg.header)
+		html_include(ctx.cfg.header);
+	free(host);
+}
+
+void cgit_print_docend(void)
+{
+	html("</div> <!-- class=content -->\n");
+	if (ctx.cfg.embedded) {
+		html("</div> <!-- id=cgit -->\n");
+		if (ctx.cfg.footer)
+			html_include(ctx.cfg.footer);
+		return;
+	}
+	if (ctx.cfg.footer)
+		html_include(ctx.cfg.footer);
+	else {
+		htmlf("<div class='footer'>generated by <a href='https://git.causal.agency/cgit-pink/about/'>cgit-pink %s</a> "
+			"(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
+		html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_DOTTIME)));
+		html("</div>\n");
+	}
+	html("</div> <!-- id=cgit -->\n");
+	html("</body>\n</html>\n");
+}
+
+void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
+{
+	va_list ap;
+	ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
+	ctx.page.status = code;
+	ctx.page.statusmsg = msg;
+	cgit_print_layout_start();
+	va_start(ap, fmt);
+	cgit_vprint_error(fmt, ap);
+	va_end(ap);
+	cgit_print_layout_end();
+}
+
+void cgit_print_layout_start(void)
+{
+	cgit_print_http_headers();
+	cgit_print_docstart();
+	cgit_print_pageheader();
+}
+
+void cgit_print_layout_end(void)
+{
+	cgit_print_docend();
+}
+
+static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix)
+{
+	struct strbuf **url_list = strbuf_split_str(txt, ' ', 0);
+	int i;
+
+	for (i = 0; url_list[i]; i++) {
+		strbuf_rtrim(url_list[i]);
+		if (url_list[i]->len == 0)
+			continue;
+		if (suffix && *suffix)
+			strbuf_addf(url_list[i], "/%s", suffix);
+		fn(url_list[i]->buf);
+	}
+
+	strbuf_list_free(url_list);
+}
+
+void cgit_add_clone_urls(void (*fn)(const char *))
+{
+	if (ctx.repo->clone_url)
+		add_clone_urls(fn, expand_macros(ctx.repo->clone_url), NULL);
+	else if (ctx.cfg.clone_prefix)
+		add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
+}
+
+static int print_this_commit_option(void)
+{
+	struct object_id oid;
+	if (!ctx.qry.head || get_oid(ctx.qry.head, &oid))
+		return 1;
+	html_option(oid_to_hex(&oid), "this commit", ctx.qry.head);
+	return 0;
+}
+
+static int print_branch_option(const char *refname, const struct object_id *oid,
+			       int flags, void *cb_data)
+{
+	char *name = (char *)refname;
+	html_option(name, name, ctx.qry.head);
+	return 0;
+}
+
+void cgit_add_hidden_formfields(int incl_head, int incl_search,
+				const char *page)
+{
+	if (!ctx.cfg.virtual_root) {
+		struct strbuf url = STRBUF_INIT;
+
+		strbuf_addf(&url, "%s/%s", ctx.qry.repo, page);
+		if (ctx.qry.vpath)
+			strbuf_addf(&url, "/%s", ctx.qry.vpath);
+		html_hidden("url", url.buf);
+		strbuf_release(&url);
+	}
+
+	if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
+	    strcmp(ctx.qry.head, ctx.repo->defbranch))
+		html_hidden("h", ctx.qry.head);
+
+	if (ctx.qry.oid)
+		html_hidden("id", ctx.qry.oid);
+	if (ctx.qry.oid2)
+		html_hidden("id2", ctx.qry.oid2);
+	if (ctx.qry.showmsg)
+		html_hidden("showmsg", "1");
+
+	if (incl_search) {
+		if (ctx.qry.grep)
+			html_hidden("qt", ctx.qry.grep);
+		if (ctx.qry.search)
+			html_hidden("q", ctx.qry.search);
+	}
+}
+
+static const char *hc(const char *page)
+{
+	if (!ctx.qry.page)
+		return NULL;
+
+	return strcmp(ctx.qry.page, page) ? NULL : "active";
+}
+
+static void cgit_print_path_crumbs(char *path)
+{
+	char *old_path = ctx.qry.path;
+	char *p = path, *q, *end = path + strlen(path);
+	int levels = 0;
+
+	ctx.qry.path = NULL;
+	cgit_self_link("root", NULL, NULL);
+	ctx.qry.path = p = path;
+	while (p < end) {
+		if (!(q = strchr(p, '/')) || levels > 15)
+			q = end;
+		*q = '\0';
+		html_txt("/");
+		cgit_self_link(p, NULL, NULL);
+		if (q < end)
+			*q = '/';
+		p = q + 1;
+		++levels;
+	}
+	ctx.qry.path = old_path;
+}
+
+static void print_header(void)
+{
+	char *logo = NULL, *logo_link = NULL;
+
+	html("<table id='header'>\n");
+	html("<tr>\n");
+
+	if (ctx.repo && ctx.repo->logo && *ctx.repo->logo)
+		logo = ctx.repo->logo;
+	else
+		logo = ctx.cfg.logo;
+	if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link)
+		logo_link = ctx.repo->logo_link;
+	else
+		logo_link = ctx.cfg.logo_link;
+	if (logo && *logo) {
+		html("<td class='logo' rowspan='2'><a href='");
+		if (logo_link && *logo_link)
+			html_attr(logo_link);
+		else
+			html_attr(cgit_rooturl());
+		html("'><img src='");
+		html_attr(logo);
+		html("' alt='cgit logo'/></a></td>\n");
+	}
+
+	html("<td class='main'>");
+	if (ctx.repo) {
+		cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
+		if (ctx.env.authenticated) {
+			html("</td><td class='form'>");
+			html("<form method='get'>\n");
+			cgit_add_hidden_formfields(0, 1, ctx.qry.page);
+			html("<select name='h' onchange='this.form.submit();'>\n");
+			print_this_commit_option();
+			html("<optgroup label='branches'>");
+			for_each_branch_ref(print_branch_option, ctx.qry.head);
+			if (ctx.repo->enable_remote_branches)
+				for_each_remote_ref(print_branch_option, ctx.qry.head);
+			html("</optgroup>");
+			html("</select> ");
+			html("<input type='submit' value='switch'/>");
+			html("</form>");
+		}
+	} else
+		html_txt(ctx.cfg.root_title);
+	html("</td></tr>\n");
+
+	html("<tr><td class='sub'>");
+	if (ctx.repo) {
+		html_txt(ctx.repo->desc);
+		html("</td><td class='sub right'>");
+		if (ctx.repo->owner_filter) {
+			cgit_open_filter(ctx.repo->owner_filter);
+			html_txt(ctx.repo->owner);
+			cgit_close_filter(ctx.repo->owner_filter);
+		} else {
+			html_txt(ctx.repo->owner);
+		}
+	} else {
+		if (ctx.cfg.root_desc)
+			html_txt(ctx.cfg.root_desc);
+	}
+	html("</td></tr></table>\n");
+}
+
+void cgit_print_pageheader(void)
+{
+	html("<div id='cgit'>");
+	if (!ctx.env.authenticated || !ctx.cfg.noheader)
+		print_header();
+
+	html("<table class='tabs'><tr><td>\n");
+	if (ctx.env.authenticated && ctx.repo) {
+		if (ctx.repo->readme.nr) {
+			cgit_about_link("about", NULL, hc("about"), ctx.qry.head,
+					 ctx.qry.oid, ctx.qry.vpath);
+			html(" ");
+		}
+		cgit_summary_link("summary", NULL, hc("summary"),
+				  ctx.qry.head);
+		html(" ");
+		cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
+			       ctx.qry.oid, NULL);
+		html(" ");
+		cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
+			      NULL, ctx.qry.vpath, 0, NULL, NULL,
+			      ctx.qry.showmsg, ctx.qry.follow);
+		html(" ");
+		if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
+			cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
+				        ctx.qry.oid, ctx.qry.vpath);
+		else
+			cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
+				        ctx.qry.oid, ctx.qry.vpath);
+		html(" ");
+		cgit_commit_link("commit", NULL, hc("commit"),
+				 ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
+		html(" ");
+		cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
+			       ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
+		if (ctx.repo->max_stats) {
+			html(" ");
+			cgit_stats_link("stats", NULL, hc("stats"),
+					ctx.qry.head, ctx.qry.vpath);
+		}
+		if (ctx.repo->homepage) {
+			html(" <a href='");
+			html_attr(ctx.repo->homepage);
+			html("'>homepage</a>");
+		}
+		html("</td><td class='form'>");
+		html("<form class='right' method='get' action='");
+		if (ctx.cfg.virtual_root) {
+			char *fileurl = cgit_fileurl(ctx.qry.repo, "log",
+						   ctx.qry.vpath, NULL);
+			html_url_path(fileurl);
+			free(fileurl);
+		}
+		html("'>\n");
+		cgit_add_hidden_formfields(1, 0, "log");
+		html("<select name='qt'>\n");
+		html_option("grep", "log msg", ctx.qry.grep);
+		html_option("author", "author", ctx.qry.grep);
+		html_option("committer", "committer", ctx.qry.grep);
+		html_option("range", "range", ctx.qry.grep);
+		html("</select>\n");
+		html("<input class='txt' type='search' size='10' name='q' value='");
+		html_attr(ctx.qry.search);
+		html("'/>\n");
+		html("<input type='submit' value='search'/>\n");
+		html("</form>\n");
+	} else if (ctx.env.authenticated) {
+		char *currenturl = cgit_currenturl();
+		site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1);
+		if (ctx.cfg.root_readme)
+			site_link("about", "about", NULL, hc("about"),
+				  NULL, NULL, 0, 1);
+		html("</td><td class='form'>");
+		html("<form method='get' action='");
+		html_attr(currenturl);
+		html("'>\n");
+		html("<input type='search' name='q' size='10' value='");
+		html_attr(ctx.qry.search);
+		html("'/>\n");
+		html("<input type='submit' value='search'/>\n");
+		html("</form>");
+		free(currenturl);
+	}
+	html("</td></tr></table>\n");
+	if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) {
+		html("<div class='path'>");
+		html("path: ");
+		cgit_print_path_crumbs(ctx.qry.vpath);
+		if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) {
+			html(" (");
+			ctx.qry.follow = !ctx.qry.follow;
+			cgit_self_link(ctx.qry.follow ? "follow" : "unfollow",
+					NULL, NULL);
+			ctx.qry.follow = !ctx.qry.follow;
+			html(")");
+		}
+		html("</div>");
+	}
+	html("<div class='content'>");
+}
+
+void cgit_print_filemode(unsigned short mode)
+{
+	if (S_ISDIR(mode))
+		html("d");
+	else if (S_ISLNK(mode))
+		html("l");
+	else if (S_ISGITLINK(mode))
+		html("m");
+	else
+		html("-");
+	html_fileperm(mode >> 6);
+	html_fileperm(mode >> 3);
+	html_fileperm(mode);
+}
+
+void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
+				  const char *ref)
+{
+	struct object_id oid;
+
+	/*
+	 * Prettify snapshot names by stripping leading "v" or "V" if the tag
+	 * name starts with {v,V}[0-9] and the prettify mapping is injective,
+	 * i.e. each stripped tag can be inverted without ambiguities.
+	 */
+	if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 &&
+	    (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
+	    ((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) +
+	     (get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
+	     (get_oid(fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
+		ref++;
+
+	strbuf_addf(filename, "%s-%s", base, ref);
+}
+
+void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref,
+			       const char *separator)
+{
+	const struct cgit_snapshot_format *f;
+	struct strbuf filename = STRBUF_INIT;
+	const char *basename;
+	size_t prefixlen;
+
+	basename = cgit_snapshot_prefix(repo);
+	if (starts_with(ref, basename))
+		strbuf_addstr(&filename, ref);
+	else
+		cgit_compose_snapshot_prefix(&filename, basename, ref);
+
+	prefixlen = filename.len;
+	for (f = cgit_snapshot_formats; f->suffix; f++) {
+		if (!(repo->snapshots & cgit_snapshot_format_bit(f)))
+			continue;
+		strbuf_setlen(&filename, prefixlen);
+		strbuf_addstr(&filename, f->suffix);
+		cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL,
+				   filename.buf);
+		if (cgit_snapshot_get_sig(ref, f)) {
+			strbuf_addstr(&filename, ".asc");
+			html(" (");
+			cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
+					   filename.buf);
+			html(")");
+		} else if (starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])) {
+			strbuf_setlen(&filename, strlen(filename.buf) - strlen(f->suffix));
+			strbuf_addstr(&filename, ".tar.asc");
+			html(" (");
+			cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
+					   filename.buf);
+			html(")");
+		}
+		html(separator);
+	}
+	strbuf_release(&filename);
+}
+
+void cgit_set_title_from_path(const char *path)
+{
+	struct strbuf sb = STRBUF_INIT;
+	const char *slash, *last_slash;
+
+	if (!path)
+		return;
+
+	last_slash = path + strlen(path);
+	for (slash = last_slash; slash > path; --slash) {
+		if (*slash != '/') continue;
+		strbuf_add(&sb, slash + 1, last_slash - slash - 1);
+		strbuf_addstr(&sb, " \xc2\xab ");
+		last_slash = slash;
+	}
+	strbuf_add(&sb, path, last_slash - path);
+	strbuf_addf(&sb, " - %s", ctx.page.title);
+	ctx.page.title = strbuf_detach(&sb, NULL);
+}
diff --git a/third_party/cgit/ui-shared.h b/third_party/cgit/ui-shared.h
new file mode 100644
index 0000000000..6964873a63
--- /dev/null
+++ b/third_party/cgit/ui-shared.h
@@ -0,0 +1,87 @@
+#ifndef UI_SHARED_H
+#define UI_SHARED_H
+
+extern const char *cgit_httpscheme(void);
+extern char *cgit_hosturl(void);
+extern const char *cgit_rooturl(void);
+extern char *cgit_currenturl(void);
+extern char *cgit_currentfullurl(void);
+extern const char *cgit_loginurl(void);
+extern char *cgit_repourl(const char *reponame);
+extern char *cgit_fileurl(const char *reponame, const char *pagename,
+			  const char *filename, const char *query);
+extern char *cgit_pageurl(const char *reponame, const char *pagename,
+			  const char *query);
+
+extern void cgit_add_clone_urls(void (*fn)(const char *));
+
+extern void cgit_index_link(const char *name, const char *title,
+			    const char *class, const char *pattern, const char *sort, int ofs, int always_root);
+extern void cgit_summary_link(const char *name, const char *title,
+			      const char *class, const char *head);
+extern void cgit_tag_link(const char *name, const char *title,
+			  const char *class, const char *tag);
+extern void cgit_tree_link(const char *name, const char *title,
+			   const char *class, const char *head,
+			   const char *rev, const char *path);
+extern void cgit_plain_link(const char *name, const char *title,
+			    const char *class, const char *head,
+			    const char *rev, const char *path);
+extern void cgit_blame_link(const char *name, const char *title,
+			    const char *class, const char *head,
+			    const char *rev, const char *path);
+extern void cgit_log_link(const char *name, const char *title,
+			  const char *class, const char *head, const char *rev,
+			  const char *path, int ofs, const char *grep,
+			  const char *pattern, int showmsg, int follow);
+extern void cgit_commit_link(const char *name, const char *title,
+			     const char *class, const char *head,
+			     const char *rev, const char *path);
+extern void cgit_patch_link(const char *name, const char *title,
+			    const char *class, const char *head,
+			    const char *rev, const char *path);
+extern void cgit_refs_link(const char *name, const char *title,
+			   const char *class, const char *head,
+			   const char *rev, const char *path);
+extern void cgit_snapshot_link(const char *name, const char *title,
+			       const char *class, const char *head,
+			       const char *rev, const char *archivename);
+extern void cgit_diff_link(const char *name, const char *title,
+			   const char *class, const char *head,
+			   const char *new_rev, const char *old_rev,
+			   const char *path);
+extern void cgit_stats_link(const char *name, const char *title,
+			    const char *class, const char *head,
+			    const char *path);
+extern void cgit_object_link(struct object *obj);
+
+extern void cgit_submodule_link(const char *class, char *path,
+				const char *rev);
+
+extern void cgit_print_layout_start(void);
+extern void cgit_print_layout_end(void);
+
+__attribute__((format (printf,1,2)))
+extern void cgit_print_error(const char *fmt, ...);
+__attribute__((format (printf,1,0)))
+extern void cgit_vprint_error(const char *fmt, va_list ap);
+extern const struct date_mode *cgit_date_mode(enum date_mode_type type);
+extern void cgit_print_age(time_t t, int tz, time_t max_relative);
+extern void cgit_print_http_headers(void);
+extern void cgit_redirect(const char *url, bool permanent);
+extern void cgit_print_docstart(void);
+extern void cgit_print_docend(void);
+__attribute__((format (printf,3,4)))
+extern void cgit_print_error_page(int code, const char *msg, const char *fmt, ...);
+extern void cgit_print_pageheader(void);
+extern void cgit_print_filemode(unsigned short mode);
+extern void cgit_compose_snapshot_prefix(struct strbuf *filename,
+					 const char *base, const char *ref);
+extern void cgit_print_snapshot_links(const struct cgit_repo *repo,
+				      const char *ref, const char *separator);
+extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo);
+extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
+				       const char *page);
+
+extern void cgit_set_title_from_path(const char *path);
+#endif /* UI_SHARED_H */
diff --git a/third_party/cgit/ui-snapshot.c b/third_party/cgit/ui-snapshot.c
new file mode 100644
index 0000000000..280139355a
--- /dev/null
+++ b/third_party/cgit/ui-snapshot.c
@@ -0,0 +1,319 @@
+/* ui-snapshot.c: generate snapshot of a commit
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-snapshot.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static int write_archive_type(const char *format, const char *hex, const char *prefix)
+{
+	struct strvec argv = STRVEC_INIT;
+	const char **nargv;
+	int result;
+	strvec_push(&argv, "snapshot");
+	strvec_push(&argv, format);
+	if (prefix) {
+		struct strbuf buf = STRBUF_INIT;
+		strbuf_addstr(&buf, prefix);
+		strbuf_addch(&buf, '/');
+		strvec_push(&argv, "--prefix");
+		strvec_push(&argv, buf.buf);
+		strbuf_release(&buf);
+	}
+	strvec_push(&argv, hex);
+	/*
+	 * Now we need to copy the pointers to arguments into a new
+	 * structure because write_archive will rearrange its arguments
+	 * which may result in duplicated/missing entries causing leaks
+	 * or double-frees in strvec_clear.
+	 */
+	nargv = xmalloc(sizeof(char *) * (argv.nr + 1));
+	/* strvec guarantees a trailing NULL entry. */
+	memcpy(nargv, argv.v, sizeof(char *) * (argv.nr + 1));
+
+	if (fflush(stdout))
+		return errno;
+
+	result = write_archive(argv.nr, nargv, NULL, the_repository, NULL, 0);
+	strvec_clear(&argv);
+	free(nargv);
+	return result;
+}
+
+static int write_tar_archive(const char *hex, const char *prefix)
+{
+	return write_archive_type("--format=tar", hex, prefix);
+}
+
+static int write_zip_archive(const char *hex, const char *prefix)
+{
+	return write_archive_type("--format=zip", hex, prefix);
+}
+
+static int write_compressed_tar_archive(const char *hex,
+					const char *prefix,
+					char *filter_argv[])
+{
+	int rv;
+	struct cgit_exec_filter f;
+	cgit_exec_filter_init(&f, filter_argv[0], filter_argv);
+
+	cgit_open_filter(&f.base);
+	rv = write_tar_archive(hex, prefix);
+	cgit_close_filter(&f.base);
+	return rv;
+}
+
+static int write_tar_gzip_archive(const char *hex, const char *prefix)
+{
+	char *argv[] = { "gzip", "-n", NULL };
+	return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_bzip2_archive(const char *hex, const char *prefix)
+{
+	char *argv[] = { "bzip2", NULL };
+	return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_lzip_archive(const char *hex, const char *prefix)
+{
+	char *argv[] = { "lzip", NULL };
+	return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_xz_archive(const char *hex, const char *prefix)
+{
+	char *argv[] = { "xz", NULL };
+	return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_zstd_archive(const char *hex, const char *prefix)
+{
+	char *argv[] = { "zstd", "-T0", NULL };
+	return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+const struct cgit_snapshot_format cgit_snapshot_formats[] = {
+	/* .tar must remain the 0 index */
+	{ ".tar",	"application/x-tar",	write_tar_archive	},
+	{ ".tar.gz",	"application/x-gzip",	write_tar_gzip_archive	},
+	{ ".tar.bz2",	"application/x-bzip2",	write_tar_bzip2_archive	},
+	{ ".tar.lz",	"application/x-lzip",	write_tar_lzip_archive	},
+	{ ".tar.xz",	"application/x-xz",	write_tar_xz_archive	},
+	{ ".tar.zst",	"application/x-zstd",	write_tar_zstd_archive	},
+	{ ".zip",	"application/x-zip",	write_zip_archive	},
+	{ NULL }
+};
+
+static struct notes_tree snapshot_sig_notes[ARRAY_SIZE(cgit_snapshot_formats)];
+
+const struct object_id *cgit_snapshot_get_sig(const char *ref,
+					      const struct cgit_snapshot_format *f)
+{
+	struct notes_tree *tree;
+	struct object_id oid;
+
+	if (get_oid(ref, &oid))
+		return NULL;
+
+	tree = &snapshot_sig_notes[f - &cgit_snapshot_formats[0]];
+	if (!tree->initialized) {
+		struct strbuf notes_ref = STRBUF_INIT;
+
+		strbuf_addf(&notes_ref, "refs/notes/signatures/%s",
+			    f->suffix + 1);
+
+		init_notes(tree, notes_ref.buf, combine_notes_ignore, 0);
+		strbuf_release(&notes_ref);
+	}
+
+	return get_note(tree, &oid);
+}
+
+static const struct cgit_snapshot_format *get_format(const char *filename)
+{
+	const struct cgit_snapshot_format *fmt;
+
+	for (fmt = cgit_snapshot_formats; fmt->suffix; fmt++) {
+		if (ends_with(filename, fmt->suffix))
+			return fmt;
+	}
+	return NULL;
+}
+
+const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f)
+{
+	return BIT(f - &cgit_snapshot_formats[0]);
+}
+
+static int make_snapshot(const struct cgit_snapshot_format *format,
+			 const char *hex, const char *prefix,
+			 const char *filename)
+{
+	struct object_id oid;
+
+	if (get_oid(hex, &oid)) {
+		cgit_print_error_page(404, "Not found",
+				"Bad object id: %s", hex);
+		return 1;
+	}
+	if (!lookup_commit_reference(the_repository, &oid)) {
+		cgit_print_error_page(400, "Bad request",
+				"Not a commit reference: %s", hex);
+		return 1;
+	}
+	ctx.page.etag = oid_to_hex(&oid);
+	ctx.page.mimetype = xstrdup(format->mimetype);
+	ctx.page.filename = xstrdup(filename);
+	cgit_print_http_headers();
+	init_archivers();
+	format->write_func(hex, prefix);
+	return 0;
+}
+
+static int write_sig(const struct cgit_snapshot_format *format,
+		     const char *hex, const char *archive,
+		     const char *filename)
+{
+	const struct object_id *note = cgit_snapshot_get_sig(hex, format);
+	enum object_type type;
+	unsigned long size;
+	char *buf;
+
+	if (!note) {
+		cgit_print_error_page(404, "Not found",
+				"No signature for %s", archive);
+		return 0;
+	}
+
+	buf = read_object_file(note, &type, &size);
+	if (!buf) {
+		cgit_print_error_page(404, "Not found", "Not found");
+		return 0;
+	}
+
+	html("X-Content-Type-Options: nosniff\n");
+	html("Content-Security-Policy: default-src 'none'\n");
+	ctx.page.etag = oid_to_hex(note);
+	ctx.page.mimetype = xstrdup("application/pgp-signature");
+	ctx.page.filename = xstrdup(filename);
+	cgit_print_http_headers();
+
+	html_raw(buf, size);
+	free(buf);
+	return 0;
+}
+
+/* Try to guess the requested revision from the requested snapshot name.
+ * First the format extension is stripped, e.g. "cgit-0.7.2.tar.gz" become
+ * "cgit-0.7.2". If this is a valid commit object name we've got a winner.
+ * Otherwise, if the snapshot name has a prefix matching the result from
+ * repo_basename(), we strip the basename and any following '-' and '_'
+ * characters ("cgit-0.7.2" -> "0.7.2") and check the resulting name once
+ * more. If this still isn't a valid commit object name, we check if pre-
+ * pending a 'v' or a 'V' to the remaining snapshot name ("0.7.2" ->
+ * "v0.7.2") gives us something valid.
+ */
+static const char *get_ref_from_filename(const struct cgit_repo *repo,
+					 const char *filename,
+					 const struct cgit_snapshot_format *format)
+{
+	const char *reponame;
+	struct object_id oid;
+	struct strbuf snapshot = STRBUF_INIT;
+	int result = 1;
+
+	strbuf_addstr(&snapshot, filename);
+	strbuf_setlen(&snapshot, snapshot.len - strlen(format->suffix));
+
+	if (get_oid(snapshot.buf, &oid) == 0)
+		goto out;
+
+	reponame = cgit_snapshot_prefix(repo);
+	if (starts_with(snapshot.buf, reponame)) {
+		const char *new_start = snapshot.buf;
+		new_start += strlen(reponame);
+		while (new_start && (*new_start == '-' || *new_start == '_'))
+			new_start++;
+		strbuf_splice(&snapshot, 0, new_start - snapshot.buf, "", 0);
+	}
+
+	if (get_oid(snapshot.buf, &oid) == 0)
+		goto out;
+
+	strbuf_insert(&snapshot, 0, "v", 1);
+	if (get_oid(snapshot.buf, &oid) == 0)
+		goto out;
+
+	strbuf_splice(&snapshot, 0, 1, "V", 1);
+	if (get_oid(snapshot.buf, &oid) == 0)
+		goto out;
+
+	result = 0;
+	strbuf_release(&snapshot);
+
+out:
+	return result ? strbuf_detach(&snapshot, NULL) : NULL;
+}
+
+void cgit_print_snapshot(const char *head, const char *hex,
+			 const char *filename, int dwim)
+{
+	const struct cgit_snapshot_format* f;
+	const char *sig_filename = NULL;
+	char *adj_filename = NULL;
+	char *prefix = NULL;
+
+	if (!filename) {
+		cgit_print_error_page(400, "Bad request",
+				"No snapshot name specified");
+		return;
+	}
+
+	if (ends_with(filename, ".asc")) {
+		sig_filename = filename;
+
+		/* Strip ".asc" from filename for common format processing */
+		adj_filename = xstrdup(filename);
+		adj_filename[strlen(adj_filename) - 4] = '\0';
+		filename = adj_filename;
+	}
+
+	f = get_format(filename);
+	if (!f || (!sig_filename && !(ctx.repo->snapshots & cgit_snapshot_format_bit(f)))) {
+		cgit_print_error_page(400, "Bad request",
+				"Unsupported snapshot format: %s", filename);
+		return;
+	}
+
+	if (!hex && dwim) {
+		hex = get_ref_from_filename(ctx.repo, filename, f);
+		if (hex == NULL) {
+			cgit_print_error_page(404, "Not found", "Not found");
+			return;
+		}
+		prefix = xstrdup(filename);
+		prefix[strlen(filename) - strlen(f->suffix)] = '\0';
+	}
+
+	if (!hex)
+		hex = head;
+
+	if (!prefix)
+		prefix = xstrdup(cgit_snapshot_prefix(ctx.repo));
+
+	if (sig_filename)
+		write_sig(f, hex, filename, sig_filename);
+	else
+		make_snapshot(f, hex, prefix, filename);
+
+	free(prefix);
+	free(adj_filename);
+}
diff --git a/third_party/cgit/ui-snapshot.h b/third_party/cgit/ui-snapshot.h
new file mode 100644
index 0000000000..a8deec3697
--- /dev/null
+++ b/third_party/cgit/ui-snapshot.h
@@ -0,0 +1,7 @@
+#ifndef UI_SNAPSHOT_H
+#define UI_SNAPSHOT_H
+
+extern void cgit_print_snapshot(const char *head, const char *hex,
+				const char *filename, int dwim);
+
+#endif /* UI_SNAPSHOT_H */
diff --git a/third_party/cgit/ui-ssdiff.c b/third_party/cgit/ui-ssdiff.c
new file mode 100644
index 0000000000..af8bc9e065
--- /dev/null
+++ b/third_party/cgit/ui-ssdiff.c
@@ -0,0 +1,420 @@
+#include "cgit.h"
+#include "ui-ssdiff.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "ui-diff.h"
+
+extern int use_ssdiff;
+
+static int current_old_line, current_new_line;
+static int **L = NULL;
+
+struct deferred_lines {
+	int line_no;
+	char *line;
+	struct deferred_lines *next;
+};
+
+static struct deferred_lines *deferred_old, *deferred_old_last;
+static struct deferred_lines *deferred_new, *deferred_new_last;
+
+static void create_or_reset_lcs_table(void)
+{
+	int i;
+
+	if (L != NULL) {
+		memset(*L, 0, sizeof(int) * MAX_SSDIFF_SIZE);
+		return;
+	}
+
+	// xcalloc will die if we ran out of memory;
+	// not very helpful for debugging
+	L = (int**)xcalloc(MAX_SSDIFF_M, sizeof(int *));
+	*L = (int*)xcalloc(MAX_SSDIFF_SIZE, sizeof(int));
+
+	for (i = 1; i < MAX_SSDIFF_M; i++) {
+		L[i] = *L + i * MAX_SSDIFF_N;
+	}
+}
+
+static char *longest_common_subsequence(char *A, char *B)
+{
+	int i, j, ri;
+	int m = strlen(A);
+	int n = strlen(B);
+	int tmp1, tmp2;
+	int lcs_length;
+	char *result;
+
+	// We bail if the lines are too long
+	if (m >= MAX_SSDIFF_M || n >= MAX_SSDIFF_N)
+		return NULL;
+
+	create_or_reset_lcs_table();
+
+	for (i = m; i >= 0; i--) {
+		for (j = n; j >= 0; j--) {
+			if (A[i] == '\0' || B[j] == '\0') {
+				L[i][j] = 0;
+			} else if (A[i] == B[j]) {
+				L[i][j] = 1 + L[i + 1][j + 1];
+			} else {
+				tmp1 = L[i + 1][j];
+				tmp2 = L[i][j + 1];
+				L[i][j] = (tmp1 > tmp2 ? tmp1 : tmp2);
+			}
+		}
+	}
+
+	lcs_length = L[0][0];
+	result = xmalloc(lcs_length + 2);
+	memset(result, 0, sizeof(*result) * (lcs_length + 2));
+
+	ri = 0;
+	i = 0;
+	j = 0;
+	while (i < m && j < n) {
+		if (A[i] == B[j]) {
+			result[ri] = A[i];
+			ri += 1;
+			i += 1;
+			j += 1;
+		} else if (L[i + 1][j] >= L[i][j + 1]) {
+			i += 1;
+		} else {
+			j += 1;
+		}
+	}
+
+	return result;
+}
+
+static int line_from_hunk(char *line, char type)
+{
+	char *buf1, *buf2;
+	int len, res;
+
+	buf1 = strchr(line, type);
+	if (buf1 == NULL)
+		return 0;
+	buf1 += 1;
+	buf2 = strchr(buf1, ',');
+	if (buf2 == NULL)
+		return 0;
+	len = buf2 - buf1;
+	buf2 = xmalloc(len + 1);
+	strlcpy(buf2, buf1, len + 1);
+	res = atoi(buf2);
+	free(buf2);
+	return res;
+}
+
+static char *replace_tabs(char *line)
+{
+	char *prev_buf = line;
+	char *cur_buf;
+	size_t linelen = strlen(line);
+	int n_tabs = 0;
+	int i;
+	char *result;
+	size_t result_len;
+
+	if (linelen == 0) {
+		result = xmalloc(1);
+		result[0] = '\0';
+		return result;
+	}
+
+	for (i = 0; i < linelen; i++) {
+		if (line[i] == '\t')
+			n_tabs += 1;
+	}
+	result_len = linelen + n_tabs * 8;
+	result = xmalloc(result_len + 1);
+	result[0] = '\0';
+
+	for (;;) {
+		cur_buf = strchr(prev_buf, '\t');
+		if (!cur_buf) {
+			linelen = strlen(result);
+			strlcpy(&result[linelen], prev_buf, result_len - linelen + 1);
+			break;
+		} else {
+			linelen = strlen(result);
+			strlcpy(&result[linelen], prev_buf, cur_buf - prev_buf + 1);
+			linelen = strlen(result);
+			memset(&result[linelen], ' ', 8 - (linelen % 8));
+			result[linelen + 8 - (linelen % 8)] = '\0';
+		}
+		prev_buf = cur_buf + 1;
+	}
+	return result;
+}
+
+static int calc_deferred_lines(struct deferred_lines *start)
+{
+	struct deferred_lines *item = start;
+	int result = 0;
+	while (item) {
+		result += 1;
+		item = item->next;
+	}
+	return result;
+}
+
+static void deferred_old_add(char *line, int line_no)
+{
+	struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
+	item->line = xstrdup(line);
+	item->line_no = line_no;
+	item->next = NULL;
+	if (deferred_old) {
+		deferred_old_last->next = item;
+		deferred_old_last = item;
+	} else {
+		deferred_old = deferred_old_last = item;
+	}
+}
+
+static void deferred_new_add(char *line, int line_no)
+{
+	struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
+	item->line = xstrdup(line);
+	item->line_no = line_no;
+	item->next = NULL;
+	if (deferred_new) {
+		deferred_new_last->next = item;
+		deferred_new_last = item;
+	} else {
+		deferred_new = deferred_new_last = item;
+	}
+}
+
+static void print_part_with_lcs(char *class, char *line, char *lcs)
+{
+	int line_len = strlen(line);
+	int i, j;
+	char c[2] = " ";
+	int same = 1;
+
+	j = 0;
+	for (i = 0; i < line_len; i++) {
+		c[0] = line[i];
+		if (same) {
+			if (line[i] == lcs[j])
+				j += 1;
+			else {
+				same = 0;
+				htmlf("<span class='%s'>", class);
+			}
+		} else if (line[i] == lcs[j]) {
+			same = 1;
+			html("</span>");
+			j += 1;
+		}
+		html_txt(c);
+	}
+	if (!same)
+		html("</span>");
+}
+
+static void print_ssdiff_line(char *class,
+			      int old_line_no,
+			      char *old_line,
+			      int new_line_no,
+			      char *new_line, int individual_chars)
+{
+	char *lcs = NULL;
+
+	if (old_line)
+		old_line = replace_tabs(old_line + 1);
+	if (new_line)
+		new_line = replace_tabs(new_line + 1);
+	if (individual_chars && old_line && new_line)
+		lcs = longest_common_subsequence(old_line, new_line);
+	html("<tr>\n");
+	if (old_line_no > 0) {
+		struct diff_filespec *old_file = cgit_get_current_old_file();
+		char *lineno_str = fmt("n%d", old_line_no);
+		char *id_str = fmt("id=%s#%s", is_null_oid(&old_file->oid)?"HEAD":oid_to_hex(old_rev_oid), lineno_str);
+		char *fileurl = cgit_fileurl(ctx.repo->url, "tree", old_file->path, id_str);
+		html("<td class='lineno'><a href='");
+		html(fileurl);
+		htmlf("'>%s</a>", lineno_str + 1);
+		html("</td>");
+		htmlf("<td class='%s'>", class);
+		free(fileurl);
+	} else if (old_line)
+		htmlf("<td class='lineno'></td><td class='%s'>", class);
+	else
+		htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
+	if (old_line) {
+		if (lcs)
+			print_part_with_lcs("del", old_line, lcs);
+		else
+			html_txt(old_line);
+	}
+
+	html("</td>\n");
+	if (new_line_no > 0) {
+		struct diff_filespec *new_file = cgit_get_current_new_file();
+		char *lineno_str = fmt("n%d", new_line_no);
+		char *id_str = fmt("id=%s#%s", is_null_oid(&new_file->oid)?"HEAD":oid_to_hex(new_rev_oid), lineno_str);
+		char *fileurl = cgit_fileurl(ctx.repo->url, "tree", new_file->path, id_str);
+		html("<td class='lineno'><a href='");
+		html(fileurl);
+		htmlf("'>%s</a>", lineno_str + 1);
+		html("</td>");
+		htmlf("<td class='%s'>", class);
+		free(fileurl);
+	} else if (new_line)
+		htmlf("<td class='lineno'></td><td class='%s'>", class);
+	else
+		htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
+	if (new_line) {
+		if (lcs)
+			print_part_with_lcs("add", new_line, lcs);
+		else
+			html_txt(new_line);
+	}
+
+	html("</td></tr>");
+	if (lcs)
+		free(lcs);
+	if (new_line)
+		free(new_line);
+	if (old_line)
+		free(old_line);
+}
+
+static void print_deferred_old_lines(void)
+{
+	struct deferred_lines *iter_old, *tmp;
+	iter_old = deferred_old;
+	while (iter_old) {
+		print_ssdiff_line("del", iter_old->line_no,
+				  iter_old->line, -1, NULL, 0);
+		tmp = iter_old->next;
+		free(iter_old);
+		iter_old = tmp;
+	}
+}
+
+static void print_deferred_new_lines(void)
+{
+	struct deferred_lines *iter_new, *tmp;
+	iter_new = deferred_new;
+	while (iter_new) {
+		print_ssdiff_line("add", -1, NULL,
+				  iter_new->line_no, iter_new->line, 0);
+		tmp = iter_new->next;
+		free(iter_new);
+		iter_new = tmp;
+	}
+}
+
+static void print_deferred_changed_lines(void)
+{
+	struct deferred_lines *iter_old, *iter_new, *tmp;
+	int n_old_lines = calc_deferred_lines(deferred_old);
+	int n_new_lines = calc_deferred_lines(deferred_new);
+	int individual_chars = (n_old_lines == n_new_lines ? 1 : 0);
+
+	iter_old = deferred_old;
+	iter_new = deferred_new;
+	while (iter_old || iter_new) {
+		if (iter_old && iter_new)
+			print_ssdiff_line("changed", iter_old->line_no,
+					  iter_old->line,
+					  iter_new->line_no, iter_new->line,
+					  individual_chars);
+		else if (iter_old)
+			print_ssdiff_line("changed", iter_old->line_no,
+					  iter_old->line, -1, NULL, 0);
+		else if (iter_new)
+			print_ssdiff_line("changed", -1, NULL,
+					  iter_new->line_no, iter_new->line, 0);
+		if (iter_old) {
+			tmp = iter_old->next;
+			free(iter_old);
+			iter_old = tmp;
+		}
+
+		if (iter_new) {
+			tmp = iter_new->next;
+			free(iter_new);
+			iter_new = tmp;
+		}
+	}
+}
+
+void cgit_ssdiff_print_deferred_lines(void)
+{
+	if (!deferred_old && !deferred_new)
+		return;
+	if (deferred_old && !deferred_new)
+		print_deferred_old_lines();
+	else if (!deferred_old && deferred_new)
+		print_deferred_new_lines();
+	else
+		print_deferred_changed_lines();
+	deferred_old = deferred_old_last = NULL;
+	deferred_new = deferred_new_last = NULL;
+}
+
+/*
+ * print a single line returned from xdiff
+ */
+void cgit_ssdiff_line_cb(char *line, int len)
+{
+	char c = line[len - 1];
+	line[len - 1] = '\0';
+	if (line[0] == '@') {
+		current_old_line = line_from_hunk(line, '-');
+		current_new_line = line_from_hunk(line, '+');
+	}
+
+	if (line[0] == ' ') {
+		if (deferred_old || deferred_new)
+			cgit_ssdiff_print_deferred_lines();
+		print_ssdiff_line("ctx", current_old_line, line,
+				  current_new_line, line, 0);
+		current_old_line += 1;
+		current_new_line += 1;
+	} else if (line[0] == '+') {
+		deferred_new_add(line, current_new_line);
+		current_new_line += 1;
+	} else if (line[0] == '-') {
+		deferred_old_add(line, current_old_line);
+		current_old_line += 1;
+	} else if (line[0] == '@') {
+		html("<tr><td colspan='4' class='hunk'>");
+		html_txt(line);
+		html("</td></tr>");
+	} else {
+		html("<tr><td colspan='4' class='ctx'>");
+		html_txt(line);
+		html("</td></tr>");
+	}
+	line[len - 1] = c;
+}
+
+void cgit_ssdiff_header_begin(void)
+{
+	current_old_line = -1;
+	current_new_line = -1;
+	html("<tr><td class='space' colspan='4'><div></div></td></tr>");
+	html("<tr><td class='head' colspan='4'>");
+}
+
+void cgit_ssdiff_header_end(void)
+{
+	html("</td></tr>");
+}
+
+void cgit_ssdiff_footer(void)
+{
+	if (deferred_old || deferred_new)
+		cgit_ssdiff_print_deferred_lines();
+	html("<tr><td class='foot' colspan='4'></td></tr>");
+}
diff --git a/third_party/cgit/ui-ssdiff.h b/third_party/cgit/ui-ssdiff.h
new file mode 100644
index 0000000000..11f2714407
--- /dev/null
+++ b/third_party/cgit/ui-ssdiff.h
@@ -0,0 +1,25 @@
+#ifndef UI_SSDIFF_H
+#define UI_SSDIFF_H
+
+/*
+ * ssdiff line limits
+ */
+#ifndef MAX_SSDIFF_M
+#define MAX_SSDIFF_M 128
+#endif
+
+#ifndef MAX_SSDIFF_N
+#define MAX_SSDIFF_N 128
+#endif
+#define MAX_SSDIFF_SIZE ((MAX_SSDIFF_M) * (MAX_SSDIFF_N))
+
+extern void cgit_ssdiff_print_deferred_lines(void);
+
+extern void cgit_ssdiff_line_cb(char *line, int len);
+
+extern void cgit_ssdiff_header_begin(void);
+extern void cgit_ssdiff_header_end(void);
+
+extern void cgit_ssdiff_footer(void);
+
+#endif /* UI_SSDIFF_H */
diff --git a/third_party/cgit/ui-stats.c b/third_party/cgit/ui-stats.c
new file mode 100644
index 0000000000..40ed6c21df
--- /dev/null
+++ b/third_party/cgit/ui-stats.c
@@ -0,0 +1,425 @@
+#include "cgit.h"
+#include "ui-stats.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct authorstat {
+	long total;
+	struct string_list list;
+};
+
+#define DAY_SECS (60 * 60 * 24)
+#define WEEK_SECS (DAY_SECS * 7)
+
+static void trunc_week(struct tm *tm)
+{
+	time_t t = timegm(tm);
+	t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
+	gmtime_r(&t, tm);
+}
+
+static void dec_week(struct tm *tm)
+{
+	time_t t = timegm(tm);
+	t -= WEEK_SECS;
+	gmtime_r(&t, tm);
+}
+
+static void inc_week(struct tm *tm)
+{
+	time_t t = timegm(tm);
+	t += WEEK_SECS;
+	gmtime_r(&t, tm);
+}
+
+static char *pretty_week(struct tm *tm)
+{
+	static char buf[10];
+
+	strftime(buf, sizeof(buf), "W%V %G", tm);
+	return buf;
+}
+
+static void trunc_month(struct tm *tm)
+{
+	tm->tm_mday = 1;
+}
+
+static void dec_month(struct tm *tm)
+{
+	tm->tm_mon--;
+	if (tm->tm_mon < 0) {
+		tm->tm_year--;
+		tm->tm_mon = 11;
+	}
+}
+
+static void inc_month(struct tm *tm)
+{
+	tm->tm_mon++;
+	if (tm->tm_mon > 11) {
+		tm->tm_year++;
+		tm->tm_mon = 0;
+	}
+}
+
+static char *pretty_month(struct tm *tm)
+{
+	static const char *months[] = {
+		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+	};
+	return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
+}
+
+static void trunc_quarter(struct tm *tm)
+{
+	trunc_month(tm);
+	while (tm->tm_mon % 3 != 0)
+		dec_month(tm);
+}
+
+static void dec_quarter(struct tm *tm)
+{
+	dec_month(tm);
+	dec_month(tm);
+	dec_month(tm);
+}
+
+static void inc_quarter(struct tm *tm)
+{
+	inc_month(tm);
+	inc_month(tm);
+	inc_month(tm);
+}
+
+static char *pretty_quarter(struct tm *tm)
+{
+	return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
+}
+
+static void trunc_year(struct tm *tm)
+{
+	trunc_month(tm);
+	tm->tm_mon = 0;
+}
+
+static void dec_year(struct tm *tm)
+{
+	tm->tm_year--;
+}
+
+static void inc_year(struct tm *tm)
+{
+	tm->tm_year++;
+}
+
+static char *pretty_year(struct tm *tm)
+{
+	return fmt("%d", tm->tm_year + 1900);
+}
+
+static const struct cgit_period periods[] = {
+	{'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
+	{'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
+	{'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
+	{'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
+};
+
+/* Given a period code or name, return a period index (1, 2, 3 or 4)
+ * and update the period pointer to the correcsponding struct.
+ * If no matching code is found, return 0.
+ */
+int cgit_find_stats_period(const char *expr, const struct cgit_period **period)
+{
+	int i;
+	char code = '\0';
+
+	if (!expr)
+		return 0;
+
+	if (strlen(expr) == 1)
+		code = expr[0];
+
+	for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
+		if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
+			if (period)
+				*period = &periods[i];
+			return i + 1;
+		}
+	return 0;
+}
+
+const char *cgit_find_stats_periodname(int idx)
+{
+	if (idx > 0 && idx < 4)
+		return periods[idx - 1].name;
+	else
+		return "";
+}
+
+static void add_commit(struct string_list *authors, struct commit *commit,
+	const struct cgit_period *period)
+{
+	struct commitinfo *info;
+	struct string_list_item *author, *item;
+	struct authorstat *authorstat;
+	struct string_list *items;
+	char *tmp;
+	struct tm date;
+	time_t t;
+	uintptr_t *counter;
+
+	info = cgit_parse_commit(commit);
+	tmp = xstrdup(info->author);
+	author = string_list_insert(authors, tmp);
+	if (!author->util)
+		author->util = xcalloc(1, sizeof(struct authorstat));
+	else
+		free(tmp);
+	authorstat = author->util;
+	items = &authorstat->list;
+	t = info->committer_date;
+	gmtime_r(&t, &date);
+	period->trunc(&date);
+	tmp = xstrdup(period->pretty(&date));
+	item = string_list_insert(items, tmp);
+	counter = (uintptr_t *)&item->util;
+	if (*counter)
+		free(tmp);
+	(*counter)++;
+
+	authorstat->total++;
+	cgit_free_commitinfo(info);
+}
+
+static int cmp_total_commits(const void *a1, const void *a2)
+{
+	const struct string_list_item *i1 = a1;
+	const struct string_list_item *i2 = a2;
+	const struct authorstat *auth1 = i1->util;
+	const struct authorstat *auth2 = i2->util;
+
+	return auth2->total - auth1->total;
+}
+
+/* Walk the commit DAG and collect number of commits per author per
+ * timeperiod into a nested string_list collection.
+ */
+static struct string_list collect_stats(const struct cgit_period *period)
+{
+	struct string_list authors;
+	struct rev_info rev;
+	struct commit *commit;
+	const char *argv[] = {NULL, ctx.qry.head, NULL, NULL, NULL, NULL};
+	int argc = 3;
+	time_t now;
+	long i;
+	struct tm tm;
+	char tmp[11];
+
+	time(&now);
+	gmtime_r(&now, &tm);
+	period->trunc(&tm);
+	for (i = 1; i < period->count; i++)
+		period->dec(&tm);
+	strftime(tmp, sizeof(tmp), "%Y-%m-%d", &tm);
+	argv[2] = xstrdup(fmt("--since=%s", tmp));
+	if (ctx.qry.path) {
+		argv[3] = "--";
+		argv[4] = ctx.qry.path;
+		argc += 2;
+	}
+	init_revisions(&rev, NULL);
+	rev.abbrev = DEFAULT_ABBREV;
+	rev.commit_format = CMIT_FMT_DEFAULT;
+	rev.max_parents = 1;
+	rev.verbose_header = 1;
+	rev.show_root_diff = 0;
+	setup_revisions(argc, argv, &rev, NULL);
+	prepare_revision_walk(&rev);
+	memset(&authors, 0, sizeof(authors));
+	while ((commit = get_revision(&rev)) != NULL) {
+		add_commit(&authors, commit, period);
+		release_commit_memory(the_repository->parsed_objects, commit);
+		commit->parents = NULL;
+	}
+	return authors;
+}
+
+static void print_combined_authorrow(struct string_list *authors, int from,
+				     int to, const char *name,
+				     const char *leftclass,
+				     const char *centerclass,
+				     const char *rightclass,
+				     const struct cgit_period *period)
+{
+	struct string_list_item *author;
+	struct authorstat *authorstat;
+	struct string_list *items;
+	struct string_list_item *date;
+	time_t now;
+	long i, j, total, subtotal;
+	struct tm tm;
+	char *tmp;
+
+	time(&now);
+	gmtime_r(&now, &tm);
+	period->trunc(&tm);
+	for (i = 1; i < period->count; i++)
+		period->dec(&tm);
+
+	total = 0;
+	htmlf("<tr><td class='%s'>%s</td>", leftclass,
+		fmt(name, to - from + 1));
+	for (j = 0; j < period->count; j++) {
+		tmp = period->pretty(&tm);
+		period->inc(&tm);
+		subtotal = 0;
+		for (i = from; i <= to; i++) {
+			author = &authors->items[i];
+			authorstat = author->util;
+			items = &authorstat->list;
+			date = string_list_lookup(items, tmp);
+			if (date)
+				subtotal += (uintptr_t)date->util;
+		}
+		htmlf("<td class='%s'>%ld</td>", centerclass, subtotal);
+		total += subtotal;
+	}
+	htmlf("<td class='%s'>%ld</td></tr>", rightclass, total);
+}
+
+static void print_authors(struct string_list *authors, int top,
+			  const struct cgit_period *period)
+{
+	struct string_list_item *author;
+	struct authorstat *authorstat;
+	struct string_list *items;
+	struct string_list_item *date;
+	time_t now;
+	long i, j, total;
+	struct tm tm;
+	char *tmp;
+
+	time(&now);
+	gmtime_r(&now, &tm);
+	period->trunc(&tm);
+	for (i = 1; i < period->count; i++)
+		period->dec(&tm);
+
+	html("<table class='stats'><tr><th>Author</th>");
+	for (j = 0; j < period->count; j++) {
+		tmp = period->pretty(&tm);
+		htmlf("<th>%s</th>", tmp);
+		period->inc(&tm);
+	}
+	html("<th>Total</th></tr>\n");
+
+	if (top <= 0 || top > authors->nr)
+		top = authors->nr;
+
+	for (i = 0; i < top; i++) {
+		author = &authors->items[i];
+		html("<tr><td class='left'>");
+		html_txt(author->string);
+		html("</td>");
+		authorstat = author->util;
+		items = &authorstat->list;
+		total = 0;
+		for (j = 0; j < period->count; j++)
+			period->dec(&tm);
+		for (j = 0; j < period->count; j++) {
+			tmp = period->pretty(&tm);
+			period->inc(&tm);
+			date = string_list_lookup(items, tmp);
+			if (!date)
+				html("<td>0</td>");
+			else {
+				htmlf("<td>%lu</td>", (uintptr_t)date->util);
+				total += (uintptr_t)date->util;
+			}
+		}
+		htmlf("<td class='sum'>%ld</td></tr>", total);
+	}
+
+	if (top < authors->nr)
+		print_combined_authorrow(authors, top, authors->nr - 1,
+			"Others (%ld)", "left", "", "sum", period);
+
+	print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
+		"total", "sum", "sum", period);
+	html("</table>");
+}
+
+/* Create a sorted string_list with one entry per author. The util-field
+ * for each author is another string_list which is used to calculate the
+ * number of commits per time-interval.
+ */
+void cgit_show_stats(void)
+{
+	struct string_list authors;
+	const struct cgit_period *period;
+	int top, i;
+	const char *code = "w";
+
+	if (ctx.qry.period)
+		code = ctx.qry.period;
+
+	i = cgit_find_stats_period(code, &period);
+	if (!i) {
+		cgit_print_error_page(404, "Not found",
+			"Unknown statistics type: %c", code[0]);
+		return;
+	}
+	if (i > ctx.repo->max_stats) {
+		cgit_print_error_page(400, "Bad request",
+			"Statistics type disabled: %s", period->name);
+		return;
+	}
+	authors = collect_stats(period);
+	qsort(authors.items, authors.nr, sizeof(struct string_list_item),
+		cmp_total_commits);
+
+	top = ctx.qry.ofs;
+	if (!top)
+		top = 10;
+
+	cgit_print_layout_start();
+	html("<div class='cgit-panel'>");
+	html("<b>stat options</b>");
+	html("<form method='get'>");
+	cgit_add_hidden_formfields(1, 0, "stats");
+	html("<table><tr><td colspan='2'/></tr>");
+	if (ctx.repo->max_stats > 1) {
+		html("<tr><td class='label'>Period:</td>");
+		html("<td class='ctrl'><select name='period' onchange='this.form.submit();'>");
+		for (i = 0; i < ctx.repo->max_stats; i++)
+			html_option(fmt("%c", periods[i].code),
+				    periods[i].name, fmt("%c", period->code));
+		html("</select></td></tr>");
+	}
+	html("<tr><td class='label'>Authors:</td>");
+	html("<td class='ctrl'><select name='ofs' onchange='this.form.submit();'>");
+	html_intoption(10, "10", top);
+	html_intoption(25, "25", top);
+	html_intoption(50, "50", top);
+	html_intoption(100, "100", top);
+	html_intoption(-1, "all", top);
+	html("</select></td></tr>");
+	html("<tr><td/><td class='ctrl'>");
+	html("<noscript><input type='submit' value='Reload'/></noscript>");
+	html("</td></tr></table>");
+	html("</form>");
+	html("</div>");
+	htmlf("<h2>Commits per author per %s", period->name);
+	if (ctx.qry.path) {
+		html(" (path '");
+		html_txt(ctx.qry.path);
+		html("')");
+	}
+	html("</h2>");
+	print_authors(&authors, top, period);
+	cgit_print_layout_end();
+}
+
diff --git a/third_party/cgit/ui-stats.h b/third_party/cgit/ui-stats.h
new file mode 100644
index 0000000000..0e61b03da3
--- /dev/null
+++ b/third_party/cgit/ui-stats.h
@@ -0,0 +1,28 @@
+#ifndef UI_STATS_H
+#define UI_STATS_H
+
+#include "cgit.h"
+
+struct cgit_period {
+	const char code;
+	const char *name;
+	int max_periods;
+	int count;
+
+	/* Convert a tm value to the first day in the period */
+	void (*trunc)(struct tm *tm);
+
+	/* Update tm value to start of next/previous period */
+	void (*dec)(struct tm *tm);
+	void (*inc)(struct tm *tm);
+
+	/* Pretty-print a tm value */
+	char *(*pretty)(struct tm *tm);
+};
+
+extern int cgit_find_stats_period(const char *expr, const struct cgit_period **period);
+extern const char *cgit_find_stats_periodname(int idx);
+
+extern void cgit_show_stats(void);
+
+#endif /* UI_STATS_H */
diff --git a/third_party/cgit/ui-summary.c b/third_party/cgit/ui-summary.c
new file mode 100644
index 0000000000..ec7b76fabf
--- /dev/null
+++ b/third_party/cgit/ui-summary.c
@@ -0,0 +1,163 @@
+/* ui-summary.c: functions for generating repo summary page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-summary.h"
+#include "html.h"
+#include "ui-blob.h"
+#include "ui-log.h"
+#include "ui-plain.h"
+#include "ui-refs.h"
+#include "ui-shared.h"
+
+static int urls;
+
+static void print_url(const char *url)
+{
+	int columns = 3;
+
+	if (ctx.repo->enable_log_filecount)
+		columns++;
+	if (ctx.repo->enable_log_linecount)
+		columns++;
+
+	if (urls++ == 0) {
+		htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+		htmlf("<tr class='nohover'><th class='left' colspan='%d'>Clone</th></tr>\n", columns);
+	}
+
+	htmlf("<tr><td colspan='%d'><a rel='vcs-git' href='", columns);
+	html_url_path(url);
+	html("' title='");
+	html_attr(ctx.repo->name);
+	html(" Git repository'>");
+	html_txt(url);
+	html("</a></td></tr>\n");
+}
+
+void cgit_print_summary(void)
+{
+	int columns = 3;
+
+	if (ctx.repo->enable_log_filecount)
+		columns++;
+	if (ctx.repo->enable_log_linecount)
+		columns++;
+
+	cgit_print_layout_start();
+	html("<table summary='repository info' class='list nowrap'>");
+	cgit_print_branches(ctx.cfg.summary_branches);
+	htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+	cgit_print_tags(ctx.cfg.summary_tags);
+	if (ctx.cfg.summary_log > 0) {
+		htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+		cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL,
+			       NULL, NULL, 0, 0, 0);
+	}
+	urls = 0;
+	cgit_add_clone_urls(print_url);
+	html("</table>");
+	cgit_print_layout_end();
+}
+
+/* The caller must free the return value. */
+static char* append_readme_path(const char *filename, const char *ref, const char *path)
+{
+	char *file, *base_dir, *full_path, *resolved_base = NULL, *resolved_full = NULL;
+	/* If a subpath is specified for the about page, make it relative
+	 * to the directory containing the configured readme. */
+
+	file = xstrdup(filename);
+	base_dir = dirname(file);
+	if (!strcmp(base_dir, ".") || !strcmp(base_dir, "..")) {
+		if (!ref) {
+			free(file);
+			return NULL;
+		}
+		full_path = xstrdup(path);
+	} else
+		full_path = fmtalloc("%s/%s", base_dir, path);
+
+	if (!ref) {
+		resolved_base = realpath(base_dir, NULL);
+		resolved_full = realpath(full_path, NULL);
+		if (!resolved_base || !resolved_full || !starts_with(resolved_full, resolved_base)) {
+			free(full_path);
+			full_path = NULL;
+		}
+	}
+
+	free(file);
+	free(resolved_base);
+	free(resolved_full);
+
+	return full_path;
+}
+
+void cgit_print_repo_readme(const char *path)
+{
+	char *filename, *ref, *mimetype;
+	int free_filename = 0;
+
+	mimetype = get_mimetype_for_filename(path);
+	if (mimetype && (!strncmp(mimetype, "image/", 6) || !strncmp(mimetype, "video/", 6))) {
+		ctx.page.mimetype = mimetype;
+		ctx.page.charset = NULL;
+		cgit_print_plain();
+		free(mimetype);
+		return;
+	}
+	free(mimetype);
+
+	if (path)
+		ctx.page.title = fmtalloc("%s - %s", path, ctx.page.title);
+
+	cgit_print_layout_start();
+	if (ctx.repo->readme.nr == 0)
+		goto done;
+
+	filename = ctx.repo->readme.items[0].string;
+	ref = ctx.repo->readme.items[0].util;
+
+	if (path) {
+		free_filename = 1;
+		filename = append_readme_path(filename, ref, path);
+		if (!filename)
+			goto done;
+	}
+
+	/* Determine which file to serve by checking whether the given filename is
+	 * already a valid file and otherwise appending the expected file name of
+	 * the readme.
+	 *
+	 * If neither yield a valid file, the user gets a blank page. Could probably
+	 * do with an error message in between there, but whatever.
+	 */
+	if (path && ref && !cgit_ref_path_exists(filename, ref, 1)) {
+	  filename = fmtalloc("%s/%s", path, ctx.repo->readme.items[0].string);
+	  free_filename = 1;
+	}
+
+	/* Print the calculated readme, either from the git repo or from the
+	 * filesystem, while applying the about-filter.
+	 */
+	html("<div id='summary'>");
+	cgit_open_filter(ctx.repo->about_filter, filename);
+	if (ref)
+		cgit_print_file(filename, ref, 1);
+	else
+		html_include(filename);
+	cgit_close_filter(ctx.repo->about_filter);
+
+	html("</div>");
+	if (free_filename)
+		free(filename);
+
+done:
+	cgit_print_layout_end();
+}
diff --git a/third_party/cgit/ui-summary.h b/third_party/cgit/ui-summary.h
new file mode 100644
index 0000000000..cba696af53
--- /dev/null
+++ b/third_party/cgit/ui-summary.h
@@ -0,0 +1,7 @@
+#ifndef UI_SUMMARY_H
+#define UI_SUMMARY_H
+
+extern void cgit_print_summary(void);
+extern void cgit_print_repo_readme(const char *path);
+
+#endif /* UI_SUMMARY_H */
diff --git a/third_party/cgit/ui-tag.c b/third_party/cgit/ui-tag.c
new file mode 100644
index 0000000000..7be2808149
--- /dev/null
+++ b/third_party/cgit/ui-tag.c
@@ -0,0 +1,120 @@
+/* ui-tag.c: display a tag
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-tag.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static void print_tag_content(char *buf)
+{
+	char *p;
+
+	if (!buf)
+		return;
+
+	html("<div class='commit-subject'>");
+	p = strchr(buf, '\n');
+	if (p)
+		*p = '\0';
+	html_txt(buf);
+	html("</div>");
+	if (p) {
+		html("<pre class='commit-msg'>");
+		html_txt(++p);
+		html("</pre>");
+	}
+}
+
+static void print_download_links(char *revname)
+{
+	html("<tr><th>download</th><td class='oid'>");
+	cgit_print_snapshot_links(ctx.repo, revname, "<br/>");
+	html("</td></tr>");
+}
+
+void cgit_print_tag(char *revname)
+{
+	struct strbuf fullref = STRBUF_INIT;
+	struct object_id oid;
+	struct object *obj;
+
+	if (!revname)
+		revname = ctx.qry.head;
+
+	strbuf_addf(&fullref, "refs/tags/%s", revname);
+	if (get_oid(fullref.buf, &oid)) {
+		cgit_print_error_page(404, "Not found",
+			"Bad tag reference: %s", revname);
+		goto cleanup;
+	}
+	obj = parse_object(the_repository, &oid);
+	if (!obj) {
+		cgit_print_error_page(500, "Internal server error",
+			"Bad object id: %s", oid_to_hex(&oid));
+		goto cleanup;
+	}
+	if (obj->type == OBJ_TAG) {
+		struct tag *tag;
+		struct taginfo *info;
+
+		tag = lookup_tag(the_repository, &oid);
+		if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) {
+			cgit_print_error_page(500, "Internal server error",
+				"Bad tag object: %s", revname);
+			goto cleanup;
+		}
+		cgit_print_layout_start();
+		html("<table class='commit-info'>\n");
+		html("<tr><td>tag name</td><td>");
+		html_txt(revname);
+		htmlf(" (%s)</td></tr>\n", oid_to_hex(&oid));
+		if (info->tagger_date > 0) {
+			html("<tr><td>tag date</td><td>");
+			html_txt(show_date(info->tagger_date, info->tagger_tz,
+						cgit_date_mode(DATE_DOTTIME)));
+			html("</td></tr>\n");
+		}
+		if (info->tagger) {
+			html("<tr><td>tagged by</td><td>");
+			cgit_open_filter(ctx.repo->email_filter, info->tagger_email, "tag");
+			html_txt(info->tagger);
+			if (info->tagger_email && !ctx.cfg.noplainemail) {
+				html(" ");
+				html_txt(info->tagger_email);
+			}
+			cgit_close_filter(ctx.repo->email_filter);
+			html("</td></tr>\n");
+		}
+		html("<tr><td>tagged object</td><td class='oid'>");
+		cgit_object_link(tag->tagged);
+		html("</td></tr>\n");
+		if (ctx.repo->snapshots)
+			print_download_links(revname);
+		html("</table>\n");
+		print_tag_content(info->msg);
+		cgit_print_layout_end();
+		cgit_free_taginfo(info);
+	} else {
+		cgit_print_layout_start();
+		html("<table class='commit-info'>\n");
+		html("<tr><td>tag name</td><td>");
+		html_txt(revname);
+		html("</td></tr>\n");
+		html("<tr><td>tagged object</td><td class='oid'>");
+		cgit_object_link(obj);
+		html("</td></tr>\n");
+		if (ctx.repo->snapshots)
+			print_download_links(revname);
+		html("</table>\n");
+		cgit_print_layout_end();
+	}
+
+cleanup:
+	strbuf_release(&fullref);
+}
diff --git a/third_party/cgit/ui-tag.h b/third_party/cgit/ui-tag.h
new file mode 100644
index 0000000000..d295cdcdbd
--- /dev/null
+++ b/third_party/cgit/ui-tag.h
@@ -0,0 +1,6 @@
+#ifndef UI_TAG_H
+#define UI_TAG_H
+
+extern void cgit_print_tag(char *revname);
+
+#endif /* UI_TAG_H */
diff --git a/third_party/cgit/ui-tree.c b/third_party/cgit/ui-tree.c
new file mode 100644
index 0000000000..21e0b88448
--- /dev/null
+++ b/third_party/cgit/ui-tree.c
@@ -0,0 +1,411 @@
+/* ui-tree.c: functions for tree output
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-tree.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct walk_tree_context {
+	char *curr_rev;
+	char *match_path;
+	int state;
+};
+
+static void print_text_buffer(const char *name, char *buf, unsigned long size)
+{
+	unsigned long lineno, idx;
+	const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
+
+	html("<table summary='blob content' class='blob'>\n");
+
+	if (ctx.cfg.enable_tree_linenumbers) {
+		html("<tr><td class='linenumbers'><pre>");
+		idx = 0;
+		lineno = 0;
+
+		if (size) {
+			htmlf(numberfmt, ++lineno);
+			while (idx < size - 1) { // skip absolute last newline
+				if (buf[idx] == '\n')
+					htmlf(numberfmt, ++lineno);
+				idx++;
+			}
+		}
+		html("</pre></td>\n");
+	}
+	else {
+		html("<tr>\n");
+	}
+
+	if (ctx.repo->source_filter) {
+		char *filter_arg = xstrdup(name);
+		html("<td class='lines'><pre><code>");
+		cgit_open_filter(ctx.repo->source_filter, filter_arg);
+		html_raw(buf, size);
+		cgit_close_filter(ctx.repo->source_filter);
+		free(filter_arg);
+		html("</code></pre></td></tr></table>\n");
+		return;
+	}
+
+	html("<td class='lines'><pre><code>");
+	html_txt(buf);
+	html("</code></pre></td></tr></table>\n");
+}
+
+#define ROWLEN 32
+
+static void print_binary_buffer(char *buf, unsigned long size)
+{
+	unsigned long ofs, idx;
+	static char ascii[ROWLEN + 1];
+
+	html("<table summary='blob content' class='bin-blob'>\n");
+	html("<tr><th>ofs</th><th>hex dump</th><th>ascii</th></tr>");
+	for (ofs = 0; ofs < size; ofs += ROWLEN, buf += ROWLEN) {
+		htmlf("<tr><td class='right'>%04lx</td><td class='hex'>", ofs);
+		for (idx = 0; idx < ROWLEN && ofs + idx < size; idx++)
+			htmlf("%*s%02x",
+			      idx == 16 ? 4 : 1, "",
+			      buf[idx] & 0xff);
+		html(" </td><td class='hex'>");
+		for (idx = 0; idx < ROWLEN && ofs + idx < size; idx++)
+			ascii[idx] = isgraph(buf[idx]) ? buf[idx] : '.';
+		ascii[idx] = '\0';
+		html_txt(ascii);
+		html("</td></tr>\n");
+	}
+	html("</table>\n");
+}
+
+static void print_object(const struct object_id *oid, const char *path, const char *basename, const char *rev)
+{
+	enum object_type type;
+	char *buf;
+	unsigned long size;
+	int is_binary;
+
+	type = oid_object_info(the_repository, oid, &size);
+	if (type == OBJ_BAD) {
+		cgit_print_error_page(404, "Not found",
+			"Bad object name: %s", oid_to_hex(oid));
+		return;
+	}
+
+	buf = read_object_file(oid, &type, &size);
+	if (!buf) {
+		cgit_print_error_page(500, "Internal server error",
+			"Error reading object %s", oid_to_hex(oid));
+		return;
+	}
+	is_binary = buffer_is_binary(buf, size);
+
+	cgit_set_title_from_path(path);
+
+	cgit_print_layout_start();
+	htmlf("blob: %s (", oid_to_hex(oid));
+	cgit_plain_link("plain", NULL, NULL, ctx.qry.head,
+		        rev, path);
+	if (ctx.repo->enable_blame && !is_binary) {
+		html(") (");
+		cgit_blame_link("blame", NULL, NULL, ctx.qry.head,
+			        rev, path);
+	}
+	html(")\n");
+
+	if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
+		htmlf("<div class='error'>blob size (%ldKB) exceeds display size limit (%dKB).</div>",
+				size / 1024, ctx.cfg.max_blob_size);
+		return;
+	}
+
+	if (is_binary)
+		print_binary_buffer(buf, size);
+	else
+		print_text_buffer(basename, buf, size);
+
+	free(buf);
+}
+
+struct single_tree_ctx {
+	struct strbuf *path;
+	struct object_id oid;
+	char *name;
+	size_t count;
+};
+
+static int single_tree_cb(const struct object_id *oid, struct strbuf *base,
+			  const char *pathname, unsigned mode, void *cbdata)
+{
+	struct single_tree_ctx *ctx = cbdata;
+
+	if (++ctx->count > 1)
+		return -1;
+
+	if (!S_ISDIR(mode)) {
+		ctx->count = 2;
+		return -1;
+	}
+
+	ctx->name = xstrdup(pathname);
+	oidcpy(&ctx->oid, oid);
+	strbuf_addf(ctx->path, "/%s", pathname);
+	return 0;
+}
+
+static void write_tree_link(const struct object_id *oid, char *name,
+			    char *rev, struct strbuf *fullpath)
+{
+	size_t initial_length = fullpath->len;
+	struct tree *tree;
+	struct single_tree_ctx tree_ctx = {
+		.path = fullpath,
+		.count = 1,
+	};
+	struct pathspec paths = {
+		.nr = 0
+	};
+
+	oidcpy(&tree_ctx.oid, oid);
+
+	while (tree_ctx.count == 1) {
+		cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, rev,
+			       fullpath->buf);
+
+		tree = lookup_tree(the_repository, &tree_ctx.oid);
+		if (!tree)
+			return;
+
+		free(tree_ctx.name);
+		tree_ctx.name = NULL;
+		tree_ctx.count = 0;
+
+		read_tree(the_repository, tree, &paths, single_tree_cb, &tree_ctx);
+
+		if (tree_ctx.count != 1)
+			break;
+
+		html(" / ");
+		name = tree_ctx.name;
+	}
+
+	strbuf_setlen(fullpath, initial_length);
+}
+
+static int ls_item(const struct object_id *oid, struct strbuf *base,
+		const char *pathname, unsigned mode, void *cbdata)
+{
+	struct walk_tree_context *walk_tree_ctx = cbdata;
+	char *name;
+	struct strbuf fullpath = STRBUF_INIT;
+	struct strbuf linkpath = STRBUF_INIT;
+	struct strbuf class = STRBUF_INIT;
+	enum object_type type;
+	unsigned long size = 0;
+	char *buf;
+
+	name = xstrdup(pathname);
+	strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "",
+		    ctx.qry.path ? "/" : "", name);
+
+	if (!S_ISGITLINK(mode)) {
+		type = oid_object_info(the_repository, oid, &size);
+		if (type == OBJ_BAD) {
+			htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>",
+			      name,
+			      oid_to_hex(oid));
+			goto cleanup;
+		}
+	}
+
+	html("<tr><td class='ls-mode'>");
+	cgit_print_filemode(mode);
+	html("</td><td>");
+	if (S_ISGITLINK(mode)) {
+		cgit_submodule_link("ls-mod", fullpath.buf, oid_to_hex(oid));
+	} else if (S_ISDIR(mode)) {
+		write_tree_link(oid, name, walk_tree_ctx->curr_rev,
+				&fullpath);
+	} else {
+		char *ext = strrchr(name, '.');
+		strbuf_addstr(&class, "ls-blob");
+		if (ext)
+			strbuf_addf(&class, " %s", ext + 1);
+		cgit_tree_link(name, NULL, class.buf, ctx.qry.head,
+			       walk_tree_ctx->curr_rev, fullpath.buf);
+	}
+	if (S_ISLNK(mode)) {
+		html(" -> ");
+		buf = read_object_file(oid, &type, &size);
+		if (!buf) {
+			htmlf("Error reading object: %s", oid_to_hex(oid));
+			goto cleanup;
+		}
+		strbuf_addbuf(&linkpath, &fullpath);
+		strbuf_addf(&linkpath, "/../%s", buf);
+		strbuf_normalize_path(&linkpath);
+		cgit_tree_link(buf, NULL, class.buf, ctx.qry.head,
+			walk_tree_ctx->curr_rev, linkpath.buf);
+		free(buf);
+		strbuf_release(&linkpath);
+	}
+	htmlf("</td><td class='ls-size'>%li</td>", size);
+
+	html("<td>");
+	cgit_log_link("log", NULL, "button", ctx.qry.head,
+		      walk_tree_ctx->curr_rev, fullpath.buf, 0, NULL, NULL,
+		      ctx.qry.showmsg, 0);
+	if (ctx.repo->max_stats) {
+		html(" ");
+		cgit_stats_link("stats", NULL, "button", ctx.qry.head,
+				fullpath.buf);
+	}
+	if (!S_ISGITLINK(mode)) {
+		html(" ");
+		cgit_plain_link("plain", NULL, "button", ctx.qry.head,
+				walk_tree_ctx->curr_rev, fullpath.buf);
+	}
+	if (!S_ISDIR(mode) && ctx.repo->enable_blame) {
+		html(" ");
+		cgit_blame_link("blame", NULL, "button", ctx.qry.head,
+				walk_tree_ctx->curr_rev, fullpath.buf);
+	}
+	html("</td></tr>\n");
+
+cleanup:
+	free(name);
+	strbuf_release(&fullpath);
+	strbuf_release(&class);
+	return 0;
+}
+
+static void ls_head(void)
+{
+	cgit_print_layout_start();
+	html("<table summary='tree listing' class='list'>\n");
+	html("<tr class='nohover'>");
+	html("<th class='left'>Mode</th>");
+	html("<th class='left'>Name</th>");
+	html("<th class='right'>Size</th>");
+	html("<th/>");
+	html("</tr>\n");
+}
+
+static void ls_tail(void)
+{
+	html("</table>\n");
+	cgit_print_layout_end();
+}
+
+static void ls_tree(const struct object_id *oid, const char *path, struct walk_tree_context *walk_tree_ctx)
+{
+	struct tree *tree;
+	struct pathspec paths = {
+		.nr = 0
+	};
+
+	tree = parse_tree_indirect(oid);
+	if (!tree) {
+		cgit_print_error_page(404, "Not found",
+			"Not a tree object: %s", oid_to_hex(oid));
+		return;
+	}
+
+	ls_head();
+	read_tree(the_repository, tree, &paths, ls_item, walk_tree_ctx);
+	ls_tail();
+}
+
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+		const char *pathname, unsigned mode, void *cbdata)
+{
+	struct walk_tree_context *walk_tree_ctx = cbdata;
+
+	if (walk_tree_ctx->state == 0) {
+		struct strbuf buffer = STRBUF_INIT;
+
+		strbuf_addbuf(&buffer, base);
+		strbuf_addstr(&buffer, pathname);
+		if (strcmp(walk_tree_ctx->match_path, buffer.buf))
+			return READ_TREE_RECURSIVE;
+
+		if (S_ISDIR(mode)) {
+			walk_tree_ctx->state = 1;
+			cgit_set_title_from_path(buffer.buf);
+			strbuf_release(&buffer);
+			ls_head();
+			return READ_TREE_RECURSIVE;
+		} else {
+			walk_tree_ctx->state = 2;
+			print_object(oid, buffer.buf, pathname, walk_tree_ctx->curr_rev);
+			strbuf_release(&buffer);
+			return 0;
+		}
+	}
+	ls_item(oid, base, pathname, mode, walk_tree_ctx);
+	return 0;
+}
+
+/*
+ * Show a tree or a blob
+ *   rev:  the commit pointing at the root tree object
+ *   path: path to tree or blob
+ */
+void cgit_print_tree(const char *rev, char *path)
+{
+	struct object_id oid;
+	struct commit *commit;
+	struct pathspec_item path_items = {
+		.match = path,
+		.len = path ? strlen(path) : 0
+	};
+	struct pathspec paths = {
+		.nr = path ? 1 : 0,
+		.items = &path_items
+	};
+	struct walk_tree_context walk_tree_ctx = {
+		.match_path = path,
+		.state = 0
+	};
+
+	if (!rev)
+		rev = ctx.qry.head;
+
+	if (get_oid(rev, &oid)) {
+		cgit_print_error_page(404, "Not found",
+			"Invalid revision name: %s", rev);
+		return;
+	}
+	commit = lookup_commit_reference(the_repository, &oid);
+	if (!commit || parse_commit(commit)) {
+		cgit_print_error_page(404, "Not found",
+			"Invalid commit reference: %s", rev);
+		return;
+	}
+
+	walk_tree_ctx.curr_rev = xstrdup(rev);
+
+	if (path == NULL) {
+		ls_tree(get_commit_tree_oid(commit), NULL, &walk_tree_ctx);
+		goto cleanup;
+	}
+
+	read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+		  &paths, walk_tree, &walk_tree_ctx);
+	if (walk_tree_ctx.state == 1)
+		ls_tail();
+	else if (walk_tree_ctx.state == 2)
+		cgit_print_layout_end();
+	else
+		cgit_print_error_page(404, "Not found", "Path not found");
+
+cleanup:
+	free(walk_tree_ctx.curr_rev);
+}
diff --git a/third_party/cgit/ui-tree.h b/third_party/cgit/ui-tree.h
new file mode 100644
index 0000000000..bbd34e3566
--- /dev/null
+++ b/third_party/cgit/ui-tree.h
@@ -0,0 +1,6 @@
+#ifndef UI_TREE_H
+#define UI_TREE_H
+
+extern void cgit_print_tree(const char *rev, char *path);
+
+#endif /* UI_TREE_H */
diff --git a/third_party/clj2nix/OWNERS b/third_party/clj2nix/OWNERS
new file mode 100644
index 0000000000..79a422fcde
--- /dev/null
+++ b/third_party/clj2nix/OWNERS
@@ -0,0 +1,3 @@
+inherit: true
+owners:
+ - grfn
diff --git a/third_party/clj2nix/default.nix b/third_party/clj2nix/default.nix
new file mode 100644
index 0000000000..f582debf29
--- /dev/null
+++ b/third_party/clj2nix/default.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.callPackage "${(pkgs.fetchFromGitHub {
+  owner = "hlolli";
+  repo = "clj2nix";
+  rev = "3d0a38c954c8e0926f57de1d80d357df05fc2f94";
+  sha256 = "0y77b988qdgsrp4w72v1f5rrh33awbps2qdgp2wr2nmmi44541w5";
+})}/clj2nix.nix"
+{ }
diff --git a/third_party/default.nix b/third_party/default.nix
new file mode 100644
index 0000000000..493301102f
--- /dev/null
+++ b/third_party/default.nix
@@ -0,0 +1,56 @@
+# This file defines the root of all external dependency imports (i.e.
+# third-party code) in the TVL package tree.
+#
+# There are two categories of third-party programs:
+#
+# 1) Programs in nixpkgs, the NixOS package set. For these, you might
+#    want to look at //third_party/nixpkgs (for the package set
+#    imports) and //third_party/overlays (for modifications in these
+#    imported package sets).
+#
+# 2) Third-party software packaged in this repository. This is all
+#    other folders below //third_party, other than the ones mentioned
+#    above.
+
+{ pkgs, depot, ... }:
+
+{
+  # Expose a partially applied NixOS, expecting an attribute set with
+  # a `configuration` key. Exposing it like this makes it possible to
+  # modify some of the base configuration used by NixOS.
+  #
+  # This partially reimplements the code in
+  # <nixpkgs/nixos/default.nix> as we need to modify its internals to
+  # be able to pass `specialArgs`. We depend on this because `depot`
+  # needs to be partially evaluated in NixOS configuration before
+  # module imports are resolved.
+  nixos =
+    { configuration
+    , specialArgs ? { }
+    , system ? builtins.currentSystem
+    , ...
+    }:
+    let
+      eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
+        inherit specialArgs system;
+        modules = [
+          configuration
+          (import (depot.path.origSrc + "/ops/modules/default-imports.nix"))
+        ];
+      };
+
+      # This is for `nixos-rebuild build-vm'.
+      vmConfig = (import (pkgs.path + "/nixos/lib/eval-config.nix") {
+        inherit specialArgs system;
+        modules = [
+          configuration
+          (pkgs.path + "/nixos/modules/virtualisation/qemu-vm.nix")
+        ];
+      }).config;
+    in
+    {
+      inherit (eval) pkgs config options;
+      system = eval.config.system.build.toplevel;
+      vm = vmConfig.system.build.vm;
+    };
+}
diff --git a/third_party/elmPackages_0_18/default.nix b/third_party/elmPackages_0_18/default.nix
new file mode 100644
index 0000000000..e1e4f6f9c2
--- /dev/null
+++ b/third_party/elmPackages_0_18/default.nix
@@ -0,0 +1,17 @@
+# Backports Elm packages for Elm 0.18 from an older channel of
+# nixpkgs.
+#
+# Elm 0.19 changed the language & package ecosystem completely,
+# essentially requiring a partial rewrite of all Elm apps. However,
+# //fun/gemma uses Elm 0.18 and I don't have time to rewrite it.
+
+{ pkgs, ... }:
+
+(import
+  (pkgs.fetchFromGitHub {
+    owner = "NixOS";
+    repo = "nixpkgs";
+    rev = "14f9ee66e63077539252f8b4550049381a082518";
+    sha256 = "1wn7nmb1cqfk2j91l3rwc6yhimfkzxprb8wknw5wi57yhq9m6lv1";
+  })
+  { }).elmPackages
diff --git a/third_party/emacs/rcirc/default.nix b/third_party/emacs/rcirc/default.nix
new file mode 100644
index 0000000000..f34cbed789
--- /dev/null
+++ b/third_party/emacs/rcirc/default.nix
@@ -0,0 +1,7 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage rec {
+  pname = "rcirc";
+  version = "1";
+  src = ./rcirc.el;
+}
diff --git a/third_party/emacs/rcirc/rcirc.el b/third_party/emacs/rcirc/rcirc.el
new file mode 100644
index 0000000000..b59cb4e9af
--- /dev/null
+++ b/third_party/emacs/rcirc/rcirc.el
@@ -0,0 +1,3133 @@
+;;; rcirc.el --- default, simple IRC client          -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2005-2019 Free Software Foundation, Inc.
+
+;; Author: Ryan Yeske <rcyeske@gmail.com>
+;; Maintainers: Ryan Yeske <rcyeske@gmail.com>,
+;;		Leo Liu <sdl.web@gmail.com>
+;; Keywords: comm
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Internet Relay Chat (IRC) is a form of instant communication over
+;; the Internet. It is mainly designed for group (many-to-many)
+;; communication in discussion forums called channels, but also allows
+;; one-to-one communication.
+
+;; Rcirc has simple defaults and clear and consistent behavior.
+;; Message arrival timestamps, activity notification on the mode line,
+;; message filling, nick completion, and keepalive pings are all
+;; enabled by default, but can easily be adjusted or turned off.  Each
+;; discussion takes place in its own buffer and there is a single
+;; server buffer per connection.
+
+;; Open a new irc connection with:
+;; M-x irc RET
+
+;;; Todo:
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ring)
+(require 'time-date)
+(require 'subr-x)
+
+(defgroup rcirc nil
+  "Simple IRC client."
+  :version "22.1"
+  :prefix "rcirc-"
+  :link '(custom-manual "(rcirc)")
+  :group 'applications)
+
+(defcustom rcirc-server-alist
+  '(("irc.freenode.net" :channels ("#rcirc")
+     ;; Don't use the TLS port by default, in case gnutls is not available.
+     ;; :port 7000 :encryption tls
+     ))
+  "An alist of IRC connections to establish when running `rcirc'.
+Each element looks like (SERVER-NAME PARAMETERS).
+
+SERVER-NAME is a string describing the server to connect
+to.
+
+The optional PARAMETERS come in pairs PARAMETER VALUE.
+
+The following parameters are recognized:
+
+`:nick'
+
+VALUE must be a string.  If absent, `rcirc-default-nick' is used
+for this connection.
+
+`:port'
+
+VALUE must be a number or string.  If absent,
+`rcirc-default-port' is used.
+
+`:user-name'
+
+VALUE must be a string.  If absent, `rcirc-default-user-name' is
+used.
+
+`:password'
+
+VALUE must be a string.  If absent, no PASS command will be sent
+to the server.
+
+`:full-name'
+
+VALUE must be a string.  If absent, `rcirc-default-full-name' is
+used.
+
+`:channels'
+
+VALUE must be a list of strings describing which channels to join
+when connecting to this server.  If absent, no channels will be
+connected to automatically.
+
+`:encryption'
+
+VALUE must be `plain' (the default) for unencrypted connections, or `tls'
+for connections using SSL/TLS.
+
+`:server-alias'
+
+VALUE must be a string that will be used instead of the server name for
+display purposes. If absent, the real server name will be displayed instead."
+  :type '(alist :key-type string
+		:value-type (plist :options
+                                   ((:nick string)
+                                    (:port integer)
+                                    (:user-name string)
+                                    (:password string)
+                                    (:full-name string)
+                                    (:channels (repeat string))
+                                    (:encryption (choice (const tls)
+                                                         (const plain)))
+                                    (:server-alias string))))
+  :group 'rcirc)
+
+(defcustom rcirc-default-port 6667
+  "The default port to connect to."
+  :type 'integer
+  :group 'rcirc)
+
+(defcustom rcirc-default-nick (user-login-name)
+  "Your nick."
+  :type 'string
+  :group 'rcirc)
+
+(defcustom rcirc-default-user-name "user"
+  "Your user name sent to the server when connecting."
+  :version "24.1"                       ; changed default
+  :type 'string
+  :group 'rcirc)
+
+(defcustom rcirc-default-full-name "unknown"
+  "The full name sent to the server when connecting."
+  :version "24.1"                       ; changed default
+  :type 'string
+  :group 'rcirc)
+
+(defcustom rcirc-fill-flag t
+  "Non-nil means line-wrap messages printed in channel buffers."
+  :type 'boolean
+  :group 'rcirc)
+
+(defcustom rcirc-fill-column nil
+  "Column beyond which automatic line-wrapping should happen.
+If nil, use value of `fill-column'.
+If a function (e.g., `frame-text-width' or `window-text-width'),
+call it to compute the number of columns."
+  :risky t                              ; can get funcalled
+  :type '(choice (const :tag "Value of `fill-column'" nil)
+		 (integer :tag "Number of columns")
+		 (function :tag "Function returning the number of columns"))
+  :group 'rcirc)
+
+(defcustom rcirc-fill-prefix nil
+  "Text to insert before filled lines.
+If nil, calculate the prefix dynamically to line up text
+underneath each nick."
+  :type '(choice (const :tag "Dynamic" nil)
+		 (string :tag "Prefix text"))
+  :group 'rcirc)
+
+(defvar rcirc-ignore-buffer-activity-flag nil
+  "If non-nil, ignore activity in this buffer.")
+(make-variable-buffer-local 'rcirc-ignore-buffer-activity-flag)
+
+(defvar rcirc-low-priority-flag nil
+  "If non-nil, activity in this buffer is considered low priority.")
+(make-variable-buffer-local 'rcirc-low-priority-flag)
+
+(defcustom rcirc-omit-responses
+  '("JOIN" "PART" "QUIT" "NICK")
+  "Responses which will be hidden when `rcirc-omit-mode' is enabled."
+  :type '(repeat string)
+  :group 'rcirc)
+
+(defvar rcirc-prompt-start-marker nil)
+
+(define-minor-mode rcirc-omit-mode
+  "Toggle the hiding of \"uninteresting\" lines.
+With a prefix argument ARG, enable Rcirc-Omit mode if ARG is
+positive, and disable it otherwise. If called from Lisp, enable
+the mode if ARG is omitted or nil.
+
+Uninteresting lines are those whose responses are listed in
+`rcirc-omit-responses'."
+  nil " Omit" nil
+  (if rcirc-omit-mode
+      (progn
+	(add-to-invisibility-spec '(rcirc-omit . nil))
+	(message "Rcirc-Omit mode enabled"))
+    (remove-from-invisibility-spec '(rcirc-omit . nil))
+    (message "Rcirc-Omit mode disabled"))
+  (dolist (window (get-buffer-window-list (current-buffer)))
+    (with-selected-window window
+      (recenter (when (> (point) rcirc-prompt-start-marker) -1)))))
+
+(defcustom rcirc-time-format "%H:%M "
+  "Describes how timestamps are printed.
+Used as the first arg to `format-time-string'."
+  :type 'string
+  :group 'rcirc)
+
+(defcustom rcirc-input-ring-size 1024
+  "Size of input history ring."
+  :type 'integer
+  :group 'rcirc)
+
+(defcustom rcirc-read-only-flag t
+  "Non-nil means make text in IRC buffers read-only."
+  :type 'boolean
+  :group 'rcirc)
+
+(defcustom rcirc-buffer-maximum-lines nil
+  "The maximum size in lines for rcirc buffers.
+Channel buffers are truncated from the top to be no greater than this
+number.  If zero or nil, no truncating is done."
+  :type '(choice (const :tag "No truncation" nil)
+		 (integer :tag "Number of lines"))
+  :group 'rcirc)
+
+(defcustom rcirc-scroll-show-maximum-output t
+  "If non-nil, scroll buffer to keep the point at the bottom of
+the window."
+  :type 'boolean
+  :group 'rcirc)
+
+(defcustom rcirc-authinfo nil
+  "List of authentication passwords.
+Each element of the list is a list with a SERVER-REGEXP string
+and a method symbol followed by method specific arguments.
+
+The valid METHOD symbols are `nickserv', `chanserv' and
+`bitlbee'.
+
+The ARGUMENTS for each METHOD symbol are:
+  `nickserv': NICK PASSWORD [NICKSERV-NICK]
+  `chanserv': NICK CHANNEL PASSWORD
+  `bitlbee': NICK PASSWORD
+  `quakenet': ACCOUNT PASSWORD
+
+Examples:
+ ((\"freenode\" nickserv \"bob\" \"p455w0rd\")
+  (\"freenode\" chanserv \"bob\" \"#bobland\" \"passwd99\")
+  (\"bitlbee\" bitlbee \"robert\" \"sekrit\")
+  (\"dal.net\" nickserv \"bob\" \"sekrit\" \"NickServ@services.dal.net\")
+  (\"quakenet.org\" quakenet \"bobby\" \"sekrit\"))"
+  :type '(alist :key-type (string :tag "Server")
+		:value-type (choice (list :tag "NickServ"
+					  (const nickserv)
+					  (string :tag "Nick")
+					  (string :tag "Password"))
+				    (list :tag "ChanServ"
+					  (const chanserv)
+					  (string :tag "Nick")
+					  (string :tag "Channel")
+					  (string :tag "Password"))
+				    (list :tag "BitlBee"
+					  (const bitlbee)
+					  (string :tag "Nick")
+					  (string :tag "Password"))
+                                    (list :tag "QuakeNet"
+                                          (const quakenet)
+                                          (string :tag "Account")
+                                          (string :tag "Password"))))
+  :group 'rcirc)
+
+(defcustom rcirc-auto-authenticate-flag t
+  "Non-nil means automatically send authentication string to server.
+See also `rcirc-authinfo'."
+  :type 'boolean
+  :group 'rcirc)
+
+(defcustom rcirc-authenticate-before-join t
+  "Non-nil means authenticate to services before joining channels.
+Currently only works with NickServ on some networks."
+  :version "24.1"
+  :type 'boolean
+  :group 'rcirc)
+
+(defcustom rcirc-prompt "> "
+  "Prompt string to use in IRC buffers.
+
+The following replacements are made:
+%n is your nick.
+%s is the server.
+%t is the buffer target, a channel or a user.
+
+Setting this alone will not affect the prompt;
+use either M-x customize or also call `rcirc-update-prompt'."
+  :type 'string
+  :set 'rcirc-set-changed
+  :initialize 'custom-initialize-default
+  :group 'rcirc)
+
+(defcustom rcirc-keywords nil
+  "List of keywords to highlight in message text."
+  :type '(repeat string)
+  :group 'rcirc)
+
+(defcustom rcirc-ignore-list ()
+  "List of ignored nicks.
+Use /ignore to list them, use /ignore NICK to add or remove a nick."
+  :type '(repeat string)
+  :group 'rcirc)
+
+(defvar rcirc-ignore-list-automatic ()
+  "List of ignored nicks added to `rcirc-ignore-list' because of renaming.
+When an ignored person renames, their nick is added to both lists.
+Nicks will be removed from the automatic list on follow-up renamings or
+parts.")
+
+(defcustom rcirc-bright-nicks nil
+  "List of nicks to be emphasized.
+See `rcirc-bright-nick' face."
+  :type '(repeat string)
+  :group 'rcirc)
+
+(defcustom rcirc-dim-nicks nil
+  "List of nicks to be deemphasized.
+See `rcirc-dim-nick' face."
+  :type '(repeat string)
+  :group 'rcirc)
+
+(define-obsolete-variable-alias 'rcirc-print-hooks
+  'rcirc-print-functions "24.3")
+(defcustom rcirc-print-functions nil
+  "Hook run after text is printed.
+Called with 5 arguments, PROCESS, SENDER, RESPONSE, TARGET and TEXT."
+  :type 'hook
+  :group 'rcirc)
+
+(defvar rcirc-authenticated-hook nil
+  "Hook run after successfully authenticated.")
+
+(defcustom rcirc-always-use-server-buffer-flag nil
+  "Non-nil means messages without a channel target will go to the server buffer."
+  :type 'boolean
+  :group 'rcirc)
+
+(defcustom rcirc-decode-coding-system 'utf-8
+  "Coding system used to decode incoming irc messages.
+Set to `undecided' if you want the encoding of the incoming
+messages autodetected."
+  :type 'coding-system
+  :group 'rcirc)
+
+(defcustom rcirc-encode-coding-system 'utf-8
+  "Coding system used to encode outgoing irc messages."
+  :type 'coding-system
+  :group 'rcirc)
+
+(defcustom rcirc-coding-system-alist nil
+  "Alist to decide a coding system to use for a channel I/O operation.
+The format is ((PATTERN . VAL) ...).
+PATTERN is either a string or a cons of strings.
+If PATTERN is a string, it is used to match a target.
+If PATTERN is a cons of strings, the car part is used to match a
+target, and the cdr part is used to match a server.
+VAL is either a coding system or a cons of coding systems.
+If VAL is a coding system, it is used for both decoding and encoding
+messages.
+If VAL is a cons of coding systems, the car part is used for decoding,
+and the cdr part is used for encoding."
+  :type '(alist :key-type (choice (string :tag "Channel Regexp")
+					  (cons (string :tag "Channel Regexp")
+						(string :tag "Server Regexp")))
+		:value-type (choice coding-system
+				    (cons (coding-system :tag "Decode")
+					  (coding-system :tag "Encode"))))
+  :group 'rcirc)
+
+(defcustom rcirc-multiline-major-mode 'fundamental-mode
+  "Major-mode function to use in multiline edit buffers."
+  :type 'function
+  :group 'rcirc)
+
+(defcustom rcirc-nick-completion-format "%s: "
+  "Format string to use in nick completions.
+
+The format string is only used when completing at the beginning
+of a line.  The string is passed as the first argument to
+`format' with the nickname as the second argument."
+  :version "24.1"
+  :type 'string
+  :group 'rcirc)
+
+(defcustom rcirc-kill-channel-buffers nil
+  "When non-nil, kill channel buffers when the server buffer is killed.
+Only the channel buffers associated with the server in question
+will be killed."
+  :version "24.3"
+  :type 'boolean
+  :group 'rcirc)
+
+(defvar rcirc-nick nil)
+
+(defvar rcirc-prompt-end-marker nil)
+
+(defvar rcirc-nick-table nil)
+
+(defvar rcirc-recent-quit-alist nil
+  "Alist of nicks that have recently quit or parted the channel.")
+
+(defvar rcirc-nick-syntax-table
+  (let ((table (make-syntax-table text-mode-syntax-table)))
+    (mapc (lambda (c) (modify-syntax-entry c "w" table))
+          "[]\\`_^{|}-")
+    (modify-syntax-entry ?' "_" table)
+    table)
+  "Syntax table which includes all nick characters as word constituents.")
+
+;; each process has an alist of (target . buffer) pairs
+(defvar rcirc-buffer-alist nil)
+
+(defvar rcirc-activity nil
+  "List of buffers with unviewed activity.")
+
+(defvar rcirc-activity-string ""
+  "String displayed in mode line representing `rcirc-activity'.")
+(put 'rcirc-activity-string 'risky-local-variable t)
+
+(defvar rcirc-server-buffer nil
+  "The server buffer associated with this channel buffer.")
+
+(defvar rcirc-target nil
+  "The channel or user associated with this buffer.")
+
+(defvar rcirc-urls nil
+  "List of URLs seen in the current buffer and their start positions.")
+(put 'rcirc-urls 'permanent-local t)
+
+(defvar rcirc-timeout-seconds 600
+  "Kill connection after this many seconds if there is no activity.")
+
+(defconst rcirc-id-string (concat "rcirc on GNU Emacs " emacs-version))
+
+(defvar rcirc-startup-channels nil)
+
+(defvar rcirc-server-name-history nil
+  "History variable for \\[rcirc] call.")
+
+(defvar rcirc-server-port-history nil
+  "History variable for \\[rcirc] call.")
+
+(defvar rcirc-nick-name-history nil
+  "History variable for \\[rcirc] call.")
+
+(defvar rcirc-user-name-history nil
+  "History variable for \\[rcirc] call.")
+
+(defvar rcirc-last-message-time nil)
+
+;;;###autoload
+(defun rcirc (arg)
+  "Connect to all servers in `rcirc-server-alist'.
+
+Do not connect to a server if it is already connected.
+
+If ARG is non-nil, instead prompt for connection parameters."
+  (interactive "P")
+  (if arg
+      (let* ((server (completing-read "IRC Server: "
+				      rcirc-server-alist
+				      nil nil
+				      (caar rcirc-server-alist)
+				      'rcirc-server-name-history))
+	     (server-plist (cdr (assoc-string server rcirc-server-alist)))
+	     (port (read-string "IRC Port: "
+				(number-to-string
+				 (or (plist-get server-plist :port)
+				     rcirc-default-port))
+				'rcirc-server-port-history))
+	     (nick (read-string "IRC Nick: "
+				(or (plist-get server-plist :nick)
+				    rcirc-default-nick)
+				'rcirc-nick-name-history))
+	     (user-name (read-string "IRC Username: "
+                                     (or (plist-get server-plist :user-name)
+                                         rcirc-default-user-name)
+                                     'rcirc-user-name-history))
+	     (password (read-passwd "IRC Password: " nil
+                                    (plist-get server-plist :password)))
+	     (channels (split-string
+			(read-string "IRC Channels: "
+				     (mapconcat 'identity
+						(plist-get server-plist
+							   :channels)
+						" "))
+			"[, ]+" t))
+             (encryption (rcirc-prompt-for-encryption server-plist)))
+	(rcirc-connect server port nick user-name
+		       rcirc-default-full-name
+		       channels password encryption))
+    ;; connect to servers in `rcirc-server-alist'
+    (let (connected-servers)
+      (dolist (c rcirc-server-alist)
+	(let ((server (car c))
+	      (nick (or (plist-get (cdr c) :nick) rcirc-default-nick))
+	      (port (or (plist-get (cdr c) :port) rcirc-default-port))
+	      (user-name (or (plist-get (cdr c) :user-name)
+			     rcirc-default-user-name))
+	      (full-name (or (plist-get (cdr c) :full-name)
+			     rcirc-default-full-name))
+	      (channels (plist-get (cdr c) :channels))
+              (password (plist-get (cdr c) :password))
+              (encryption (plist-get (cdr c) :encryption))
+              (server-alias (plist-get (cdr c) :server-alias))
+              contact)
+	  (when server
+	    (let (connected)
+	      (dolist (p (rcirc-process-list))
+		(when (string= (or server-alias server) (process-name p))
+		  (setq connected p)))
+	      (if (not connected)
+		  (condition-case nil
+		      (rcirc-connect server port nick user-name
+                                     full-name channels password encryption
+                                     server-alias)
+		    (quit (message "Quit connecting to %s"
+                                   (or server-alias server))))
+		(with-current-buffer (process-buffer connected)
+                  (setq contact (process-contact
+                                 (get-buffer-process (current-buffer)) :name))
+                  (setq connected-servers
+                        (cons (if (stringp contact)
+                                  contact (or server-alias server))
+                              connected-servers))))))))
+      (when connected-servers
+	(message "Already connected to %s"
+		 (if (cdr connected-servers)
+		     (concat (mapconcat 'identity (butlast connected-servers) ", ")
+			     ", and "
+			     (car (last connected-servers)))
+		   (car connected-servers)))))))
+
+;;;###autoload
+(defalias 'irc 'rcirc)
+
+
+(defvar rcirc-process-output nil)
+(defvar rcirc-topic nil)
+(defvar rcirc-keepalive-timer nil)
+(defvar rcirc-last-server-message-time nil)
+(defvar rcirc-server nil)		; server provided by server
+(defvar rcirc-server-name nil)		; server name given by 001 response
+(defvar rcirc-timeout-timer nil)
+(defvar rcirc-user-authenticated nil)
+(defvar rcirc-user-disconnect nil)
+(defvar rcirc-connecting nil)
+(defvar rcirc-connection-info nil)
+(defvar rcirc-process nil)
+
+;;;###autoload
+(defun rcirc-connect (server &optional port nick user-name
+                             full-name startup-channels password encryption
+                             server-alias)
+  (save-excursion
+    (message "Connecting to %s..." (or server-alias server))
+    (let* ((inhibit-eol-conversion)
+           (port-number (if port
+			    (if (stringp port)
+				(string-to-number port)
+			      port)
+			  rcirc-default-port))
+	   (nick (or nick rcirc-default-nick))
+	   (user-name (or user-name rcirc-default-user-name))
+	   (full-name (or full-name rcirc-default-full-name))
+	   (startup-channels startup-channels)
+           (process (open-network-stream
+                     (or server-alias server) nil server port-number
+                     :type (or encryption 'plain))))
+      ;; set up process
+      (set-process-coding-system process 'raw-text 'raw-text)
+      (switch-to-buffer (rcirc-generate-new-buffer-name process nil))
+      (set-process-buffer process (current-buffer))
+      (rcirc-mode process nil)
+      (set-process-sentinel process 'rcirc-sentinel)
+      (set-process-filter process 'rcirc-filter)
+
+      (setq-local rcirc-connection-info
+		  (list server port nick user-name full-name startup-channels
+			password encryption server-alias))
+      (setq-local rcirc-process process)
+      (setq-local rcirc-server server)
+      (setq-local rcirc-server-name
+                  (or server-alias server)) ; Update when we get 001 response.
+      (setq-local rcirc-buffer-alist nil)
+      (setq-local rcirc-nick-table (make-hash-table :test 'equal))
+      (setq-local rcirc-nick nick)
+      (setq-local rcirc-process-output nil)
+      (setq-local rcirc-startup-channels startup-channels)
+      (setq-local rcirc-last-server-message-time (current-time))
+
+      (setq-local rcirc-timeout-timer nil)
+      (setq-local rcirc-user-disconnect nil)
+      (setq-local rcirc-user-authenticated nil)
+      (setq-local rcirc-connecting t)
+
+      (add-hook 'auto-save-hook 'rcirc-log-write)
+
+      ;; identify
+      (unless (zerop (length password))
+        (rcirc-send-string process (concat "PASS " password)))
+      (rcirc-send-string process (concat "NICK " nick))
+      (rcirc-send-string process "CAP LS 302")
+      (rcirc-send-string process (concat "USER " user-name
+                                         " 0 * :" full-name))
+
+      ;; setup ping timer if necessary
+      (unless rcirc-keepalive-timer
+	(setq rcirc-keepalive-timer
+	      (run-at-time 0 (/ rcirc-timeout-seconds 2) 'rcirc-keepalive)))
+
+      (message "Connecting to %s...done" (or server-alias server))
+
+      ;; return process object
+      process)))
+
+(defmacro with-rcirc-process-buffer (process &rest body)
+  (declare (indent 1) (debug t))
+  `(with-current-buffer (process-buffer ,process)
+     ,@body))
+
+(defmacro with-rcirc-server-buffer (&rest body)
+  (declare (indent 0) (debug t))
+  `(with-current-buffer rcirc-server-buffer
+     ,@body))
+
+(define-obsolete-function-alias 'rcirc-float-time 'float-time "26.1")
+
+(defun rcirc-prompt-for-encryption (server-plist)
+  "Prompt the user for the encryption method to use.
+SERVER-PLIST is the property list for the server."
+  (let ((msg "Encryption (default %s): ")
+        (choices '("plain" "tls"))
+        (default (or (plist-get server-plist :encryption)
+                     'plain)))
+    (intern
+     (completing-read (format msg default)
+                      choices nil t nil nil (symbol-name default)))))
+
+(defun rcirc-keepalive ()
+  "Send keep alive pings to active rcirc processes.
+Kill processes that have not received a server message since the
+last ping."
+  (if (rcirc-process-list)
+      (mapc (lambda (process)
+	      (with-rcirc-process-buffer process
+		(when (not rcirc-connecting)
+                  (rcirc-send-ctcp process
+                                   rcirc-nick
+                                   (format "KEEPALIVE %f"
+                                           (float-time))))))
+            (rcirc-process-list))
+    ;; no processes, clean up timer
+    (when (timerp rcirc-keepalive-timer)
+      (cancel-timer rcirc-keepalive-timer))
+    (setq rcirc-keepalive-timer nil)))
+
+(defun rcirc-handler-ctcp-KEEPALIVE (process _target _sender message)
+  (with-rcirc-process-buffer process
+    (setq header-line-format (format "%f" (- (float-time)
+					     (string-to-number message))))))
+
+(defvar rcirc-debug-buffer "*rcirc debug*")
+(defvar rcirc-debug-flag nil
+  "If non-nil, write information to `rcirc-debug-buffer'.")
+(defun rcirc-debug (process text)
+  "Add an entry to the debug log including PROCESS and TEXT.
+Debug text is written to `rcirc-debug-buffer' if `rcirc-debug-flag'
+is non-nil."
+  (when rcirc-debug-flag
+    (with-current-buffer (get-buffer-create rcirc-debug-buffer)
+      (goto-char (point-max))
+      (insert (concat
+	       "["
+	       (format-time-string "%Y-%m-%dT%T ") (process-name process)
+	       "] "
+	       text)))))
+
+(define-obsolete-variable-alias 'rcirc-sentinel-hooks
+  'rcirc-sentinel-functions "24.3")
+(defvar rcirc-sentinel-functions nil
+  "Hook functions called when the process sentinel is called.
+Functions are called with PROCESS and SENTINEL arguments.")
+
+(defcustom rcirc-reconnect-delay 0
+  "The minimum interval in seconds between reconnect attempts.
+When 0, do not auto-reconnect."
+  :version "25.1"
+  :type 'integer
+  :group 'rcirc)
+
+(defvar rcirc-last-connect-time nil
+  "The last time the buffer was connected.")
+
+(defun rcirc-sentinel (process sentinel)
+  "Called when PROCESS receives SENTINEL."
+  (let ((sentinel (replace-regexp-in-string "\n" "" sentinel)))
+    (rcirc-debug process (format "SENTINEL: %S %S\n" process sentinel))
+    (with-rcirc-process-buffer process
+      (dolist (buffer (cons nil (mapcar 'cdr rcirc-buffer-alist)))
+	(with-current-buffer (or buffer (current-buffer))
+	  (rcirc-print process "rcirc.el" "ERROR" rcirc-target
+		       (format "%s: %s (%S)"
+			       (process-name process)
+			       sentinel
+			       (process-status process))
+                       (not rcirc-target))
+	  (rcirc-disconnect-buffer)))
+      (when (and (string= sentinel "deleted")
+                 (< 0 rcirc-reconnect-delay))
+        (let ((now (current-time)))
+          (when (or (null rcirc-last-connect-time)
+                    (< rcirc-reconnect-delay
+                       (float-time (time-subtract now rcirc-last-connect-time))))
+            (setq rcirc-last-connect-time now)
+            (rcirc-cmd-reconnect nil))))
+      (run-hook-with-args 'rcirc-sentinel-functions process sentinel))))
+
+(defun rcirc-disconnect-buffer (&optional buffer)
+  (with-current-buffer (or buffer (current-buffer))
+    ;; set rcirc-target to nil for each channel so cleanup
+    ;; doesn't happen when we reconnect
+    (setq rcirc-target nil)
+    (setq mode-line-process ":disconnected")))
+
+(defun rcirc-process-list ()
+  "Return a list of rcirc processes."
+  (let (ps)
+    (mapc (lambda (p)
+            (when (buffer-live-p (process-buffer p))
+              (with-rcirc-process-buffer p
+                (when (eq major-mode 'rcirc-mode)
+                  (setq ps (cons p ps))))))
+          (process-list))
+    ps))
+
+(define-obsolete-variable-alias 'rcirc-receive-message-hooks
+  'rcirc-receive-message-functions "24.3")
+(defvar rcirc-receive-message-functions nil
+  "Hook functions run when a message is received from server.
+Function is called with PROCESS, COMMAND, SENDER, ARGS and LINE.")
+(defun rcirc-filter (process output)
+  "Called when PROCESS receives OUTPUT."
+  (rcirc-debug process output)
+  (rcirc-reschedule-timeout process)
+  (with-rcirc-process-buffer process
+    (setq rcirc-last-server-message-time (current-time))
+    (setq rcirc-process-output (concat rcirc-process-output output))
+    (when (= ?\n (aref rcirc-process-output
+                       (1- (length rcirc-process-output))))
+      (let ((lines (split-string rcirc-process-output "[\n\r]" t)))
+        (setq rcirc-process-output nil)
+        (dolist (line lines)
+          (rcirc-process-server-response process line))))))
+
+(defun rcirc-reschedule-timeout (process)
+  (with-rcirc-process-buffer process
+    (when (not rcirc-connecting)
+      (with-rcirc-process-buffer process
+	(when rcirc-timeout-timer (cancel-timer rcirc-timeout-timer))
+	(setq rcirc-timeout-timer (run-at-time rcirc-timeout-seconds nil
+					       'rcirc-delete-process
+					       process))))))
+
+(defun rcirc-delete-process (process)
+  (delete-process process))
+
+(defvar rcirc-trap-errors-flag t)
+(defun rcirc-process-server-response (process text)
+  (if rcirc-trap-errors-flag
+      (condition-case err
+          (rcirc-process-server-response-1 process text)
+        (error
+         (rcirc-print process "RCIRC" "ERROR" nil
+                      (format "\"%s\" %s" text err) t)))
+    (rcirc-process-server-response-1 process text)))
+
+(defun rcirc-handle-message-tags (tags)
+  (if-let* ((time (cdr (assoc "time" tags)))
+            (timestamp (floor (float-time (date-to-time time)))))
+      (setq rcirc-last-message-time timestamp)))
+
+(defun rcirc-parse-tags (tags)
+  "Parse TAGS message prefix."
+  (mapcar (lambda (tag)
+            (let ((p (split-string tag "=")))
+              `(,(car p) . ,(cadr p))))
+          (split-string tags ";")))
+
+(defun rcirc-process-server-response-1 (process text)
+
+  ;; attempt to extract and handle IRCv3 message tags (which contain server-time)
+  (if (string-match "^\\(@\\([^ ]+\\) \\)?\\(\\(:[^ ]+ \\)?[^ ]+ .+\\)$" text)
+      (let ((tags (match-string 2 text))
+            (rest (match-string 3 text)))
+        (when tags
+          (rcirc-handle-message-tags (rcirc-parse-tags tags)))
+        (setq text rest)))
+
+  (if (string-match "^\\(:\\([^ ]+\\) \\)?\\([^ ]+\\) \\(.+\\)$" text)
+      (let* ((user (match-string 2 text))
+	     (sender (rcirc-user-nick user))
+             (cmd (match-string 3 text))
+             (args (match-string 4 text))
+             (handler (intern-soft (concat "rcirc-handler-" cmd))))
+        (string-match "^\\([^:]*\\):?\\(.+\\)?$" args)
+        (let* ((args1 (match-string 1 args))
+               (args2 (match-string 2 args))
+               (args (delq nil (append (split-string args1 " " t)
+				       (list args2)))))
+          (if (not (fboundp handler))
+              (rcirc-handler-generic process cmd sender args text)
+            (funcall handler process sender args text))
+          (run-hook-with-args 'rcirc-receive-message-functions
+                              process cmd sender args text)))
+    (message "UNHANDLED: %s" text)))
+
+(defvar rcirc-responses-no-activity '("305" "306")
+  "Responses that don't trigger activity in the mode-line indicator.")
+
+(defun rcirc-handler-generic (process response sender args _text)
+  "Generic server response handler."
+  (rcirc-print process sender response nil
+               (mapconcat 'identity (cdr args) " ")
+	       (not (member response rcirc-responses-no-activity))))
+
+(defun rcirc--connection-open-p (process)
+  (memq (process-status process) '(run open)))
+
+(defun rcirc-send-string (process string)
+  "Send PROCESS a STRING plus a newline."
+  (let ((string (concat (encode-coding-string string rcirc-encode-coding-system)
+                        "\n")))
+    (unless (rcirc--connection-open-p process)
+      (error "Network connection to %s is not open"
+             (process-name process)))
+    (rcirc-debug process string)
+    (process-send-string process string)))
+
+(defun rcirc-send-privmsg (process target string)
+  (rcirc-send-string process (format "PRIVMSG %s :%s" target string)))
+
+(defun rcirc-send-ctcp (process target request &optional args)
+  (let ((args (if args (concat " " args) "")))
+    (rcirc-send-privmsg process target
+                        (format "\C-a%s%s\C-a" request args))))
+
+(defun rcirc-buffer-process (&optional buffer)
+  "Return the process associated with channel BUFFER.
+With no argument or nil as argument, use the current buffer."
+  (let ((buffer (or buffer (and (buffer-live-p rcirc-server-buffer)
+				rcirc-server-buffer))))
+    (if buffer
+        (with-current-buffer buffer rcirc-process)
+      rcirc-process)))
+
+(defun rcirc-server-name (process)
+  "Return PROCESS server name, given by the 001 response."
+  (with-rcirc-process-buffer process
+    (or rcirc-server-name
+	(warn "server name for process %S unknown" process))))
+
+(defun rcirc-nick (process)
+  "Return PROCESS nick."
+  (with-rcirc-process-buffer process
+    (or rcirc-nick rcirc-default-nick)))
+
+(defun rcirc-buffer-nick (&optional buffer)
+  "Return the nick associated with BUFFER.
+With no argument or nil as argument, use the current buffer."
+  (with-current-buffer (or buffer (current-buffer))
+    (with-current-buffer rcirc-server-buffer
+      (or rcirc-nick rcirc-default-nick))))
+
+(defvar rcirc-max-message-length 420
+  "Messages longer than this value will be split.")
+
+(defun rcirc-split-message (message)
+  "Split MESSAGE into chunks within `rcirc-max-message-length'."
+  ;; `rcirc-encode-coding-system' can have buffer-local value.
+  (let ((encoding rcirc-encode-coding-system))
+    (with-temp-buffer
+      (insert message)
+      (goto-char (point-min))
+      (let (result)
+	(while (not (eobp))
+	  (goto-char (or (byte-to-position rcirc-max-message-length)
+			 (point-max)))
+	  ;; max message length is 512 including CRLF
+	  (while (and (not (bobp))
+		      (> (length (encode-coding-region
+				  (point-min) (point) encoding t))
+			 rcirc-max-message-length))
+	    (forward-char -1))
+	  (push (delete-and-extract-region (point-min) (point)) result))
+	(nreverse result)))))
+
+(defun rcirc-send-message (process target message &optional noticep silent)
+  "Send TARGET associated with PROCESS a privmsg with text MESSAGE.
+If NOTICEP is non-nil, send a notice instead of privmsg.
+If SILENT is non-nil, do not print the message in any irc buffer."
+  (let ((response (if noticep "NOTICE" "PRIVMSG")))
+    (rcirc-get-buffer-create process target)
+    (dolist (msg (rcirc-split-message message))
+      (rcirc-send-string process (concat response " " target " :" msg))
+      (unless silent
+	(rcirc-print process (rcirc-nick process) response target msg)))))
+
+(defvar rcirc-input-ring nil)
+(defvar rcirc-input-ring-index 0)
+
+(defun rcirc-prev-input-string (arg)
+  (ring-ref rcirc-input-ring (+ rcirc-input-ring-index arg)))
+
+(defun rcirc-insert-prev-input ()
+  (interactive)
+  (when (<= rcirc-prompt-end-marker (point))
+    (delete-region rcirc-prompt-end-marker (point-max))
+    (insert (rcirc-prev-input-string 0))
+    (setq rcirc-input-ring-index (1+ rcirc-input-ring-index))))
+
+(defun rcirc-insert-next-input ()
+  (interactive)
+  (when (<= rcirc-prompt-end-marker (point))
+    (delete-region rcirc-prompt-end-marker (point-max))
+    (setq rcirc-input-ring-index (1- rcirc-input-ring-index))
+    (insert (rcirc-prev-input-string -1))))
+
+(defvar rcirc-server-commands
+  '("/admin"   "/away"   "/connect" "/die"      "/error"   "/info"
+    "/invite"  "/ison"   "/join"    "/kick"     "/kill"    "/links"
+    "/list"    "/lusers" "/mode"    "/motd"     "/names"   "/nick"
+    "/notice"  "/oper"   "/part"    "/pass"     "/ping"    "/pong"
+    "/privmsg" "/quit"   "/rehash"  "/restart"  "/service" "/servlist"
+    "/server"  "/squery" "/squit"   "/stats"    "/summon"  "/time"
+    "/topic"   "/trace"  "/user"    "/userhost" "/users"   "/version"
+    "/wallops" "/who"    "/whois"   "/whowas")
+  "A list of user commands by IRC server.
+The value defaults to RFCs 1459 and 2812.")
+
+;; /me and /ctcp are not defined by `defun-rcirc-command'.
+(defvar rcirc-client-commands '("/me" "/ctcp")
+  "A list of user commands defined by IRC client rcirc.
+The list is updated automatically by `defun-rcirc-command'.")
+
+(defun rcirc-completion-at-point ()
+  "Function used for `completion-at-point-functions' in `rcirc-mode'."
+  (and (rcirc-looking-at-input)
+       (let* ((beg (save-excursion
+                     ;; On some networks it is common to message or
+                     ;; mention someone using @nick instead of just
+                     ;; nick.
+		     (if (re-search-backward "[[:space:]@]" rcirc-prompt-end-marker t)
+			 (1+ (point))
+		       rcirc-prompt-end-marker)))
+	      (table (if (and (= beg rcirc-prompt-end-marker)
+			      (eq (char-after beg) ?/))
+			 (delete-dups
+			  (nconc (sort (copy-sequence rcirc-client-commands)
+				       'string-lessp)
+				 (sort (copy-sequence rcirc-server-commands)
+				       'string-lessp)))
+		       (rcirc-channel-nicks (rcirc-buffer-process)
+					    rcirc-target))))
+	 (list beg (point) table))))
+
+(defvar rcirc-completions nil)
+(defvar rcirc-completion-start nil)
+
+(defun rcirc-complete ()
+  "Cycle through completions from list of nicks in channel or IRC commands.
+IRC command completion is performed only if `/' is the first input char."
+  (interactive)
+  (unless (rcirc-looking-at-input)
+    (error "Point not located after rcirc prompt"))
+  (if (eq last-command this-command)
+      (setq rcirc-completions
+	    (append (cdr rcirc-completions) (list (car rcirc-completions))))
+    (let ((completion-ignore-case t)
+	  (table (rcirc-completion-at-point)))
+      (setq rcirc-completion-start (car table))
+      (setq rcirc-completions
+	    (and rcirc-completion-start
+		 (all-completions (buffer-substring rcirc-completion-start
+						    (cadr table))
+				  (nth 2 table))))))
+  (let ((completion (car rcirc-completions)))
+    (when completion
+      (delete-region rcirc-completion-start (point))
+      (insert
+       (cond
+        ((= (aref completion 0) ?/) (concat completion " "))
+        ((= rcirc-completion-start rcirc-prompt-end-marker)
+         (format rcirc-nick-completion-format completion))
+        (t completion))))))
+
+(defun set-rcirc-decode-coding-system (coding-system)
+  "Set the decode coding system used in this channel."
+  (interactive "zCoding system for incoming messages: ")
+  (setq-local rcirc-decode-coding-system coding-system))
+
+(defun set-rcirc-encode-coding-system (coding-system)
+  "Set the encode coding system used in this channel."
+  (interactive "zCoding system for outgoing messages: ")
+  (setq-local rcirc-encode-coding-system coding-system))
+
+(defvar rcirc-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "RET") 'rcirc-send-input)
+    (define-key map (kbd "M-p") 'rcirc-insert-prev-input)
+    (define-key map (kbd "M-n") 'rcirc-insert-next-input)
+    (define-key map (kbd "TAB") 'rcirc-complete)
+    (define-key map (kbd "C-c C-b") 'rcirc-browse-url)
+    (define-key map (kbd "C-c C-c") 'rcirc-edit-multiline)
+    (define-key map (kbd "C-c C-j") 'rcirc-cmd-join)
+    (define-key map (kbd "C-c C-k") 'rcirc-cmd-kick)
+    (define-key map (kbd "C-c C-l") 'rcirc-toggle-low-priority)
+    (define-key map (kbd "C-c C-d") 'rcirc-cmd-mode)
+    (define-key map (kbd "C-c C-m") 'rcirc-cmd-msg)
+    (define-key map (kbd "C-c C-r") 'rcirc-cmd-nick) ; rename
+    (define-key map (kbd "C-c C-o") 'rcirc-omit-mode)
+    (define-key map (kbd "C-c C-p") 'rcirc-cmd-part)
+    (define-key map (kbd "C-c C-q") 'rcirc-cmd-query)
+    (define-key map (kbd "C-c C-t") 'rcirc-cmd-topic)
+    (define-key map (kbd "C-c C-n") 'rcirc-cmd-names)
+    (define-key map (kbd "C-c C-w") 'rcirc-cmd-whois)
+    (define-key map (kbd "C-c C-x") 'rcirc-cmd-quit)
+    (define-key map (kbd "C-c TAB") ; C-i
+      'rcirc-toggle-ignore-buffer-activity)
+    (define-key map (kbd "C-c C-s") 'rcirc-switch-to-server-buffer)
+    (define-key map (kbd "C-c C-a") 'rcirc-jump-to-first-unread-line)
+    map)
+  "Keymap for rcirc mode.")
+
+(defvar rcirc-short-buffer-name nil
+  "Generated abbreviation to use to indicate buffer activity.")
+
+(defvar rcirc-mode-hook nil
+  "Hook run when setting up rcirc buffer.")
+
+(defvar rcirc-last-post-time nil)
+
+(defvar rcirc-log-alist nil
+  "Alist of lines to log to disk when `rcirc-log-flag' is non-nil.
+Each element looks like (FILENAME . TEXT).")
+
+(defvar rcirc-current-line 0
+  "The current number of responses printed in this channel.
+This number is independent of the number of lines in the buffer.")
+
+(defun rcirc-mode (process target)
+  ;; FIXME: Use define-derived-mode.
+  "Major mode for IRC channel buffers.
+
+\\{rcirc-mode-map}"
+  (kill-all-local-variables)
+  (use-local-map rcirc-mode-map)
+  (setq mode-name "rcirc")
+  (setq major-mode 'rcirc-mode)
+  (setq mode-line-process nil)
+
+  (setq-local rcirc-input-ring
+	      ;; If rcirc-input-ring is already a ring with desired
+	      ;; size do not re-initialize.
+	      (if (and (ring-p rcirc-input-ring)
+		       (= (ring-size rcirc-input-ring)
+			  rcirc-input-ring-size))
+		  rcirc-input-ring
+		(make-ring rcirc-input-ring-size)))
+  (setq-local rcirc-server-buffer (process-buffer process))
+  (setq-local rcirc-target target)
+  (setq-local rcirc-topic nil)
+  (setq-local rcirc-last-post-time (current-time))
+  (setq-local fill-paragraph-function 'rcirc-fill-paragraph)
+  (setq-local rcirc-recent-quit-alist nil)
+  (setq-local rcirc-current-line 0)
+  (setq-local rcirc-last-connect-time (current-time))
+
+  (use-hard-newlines t)
+  (setq-local rcirc-short-buffer-name nil)
+  (setq-local rcirc-urls nil)
+
+  ;; setup for omitting responses
+  (setq buffer-invisibility-spec '())
+  (setq buffer-display-table (make-display-table))
+  (set-display-table-slot buffer-display-table 4
+			  (let ((glyph (make-glyph-code
+					?. 'font-lock-keyword-face)))
+			    (make-vector 3 glyph)))
+
+  (dolist (i rcirc-coding-system-alist)
+    (let ((chan (if (consp (car i)) (caar i) (car i)))
+	  (serv (if (consp (car i)) (cdar i) "")))
+      (when (and (string-match chan (or target ""))
+		 (string-match serv (rcirc-server-name process)))
+	(setq-local rcirc-decode-coding-system
+		    (if (consp (cdr i)) (cadr i) (cdr i)))
+        (setq-local rcirc-encode-coding-system
+		    (if (consp (cdr i)) (cddr i) (cdr i))))))
+
+  ;; setup the prompt and markers
+  (setq-local rcirc-prompt-start-marker (point-max-marker))
+  (setq-local rcirc-prompt-end-marker (point-max-marker))
+  (rcirc-update-prompt)
+  (goto-char rcirc-prompt-end-marker)
+
+  (setq-local overlay-arrow-position (make-marker))
+
+  ;; if the user changes the major mode or kills the buffer, there is
+  ;; cleanup work to do
+  (add-hook 'change-major-mode-hook 'rcirc-change-major-mode-hook nil t)
+  (add-hook 'kill-buffer-hook 'rcirc-kill-buffer-hook nil t)
+
+  ;; add to buffer list, and update buffer abbrevs
+  (when target				; skip server buffer
+    (let ((buffer (current-buffer)))
+      (with-rcirc-process-buffer process
+	(setq rcirc-buffer-alist (cons (cons target buffer)
+				       rcirc-buffer-alist))))
+    (rcirc-update-short-buffer-names))
+
+  (add-hook 'completion-at-point-functions
+            'rcirc-completion-at-point nil 'local)
+
+  (run-mode-hooks 'rcirc-mode-hook))
+
+(defun rcirc-update-prompt (&optional all)
+  "Reset the prompt string in the current buffer.
+
+If ALL is non-nil, update prompts in all IRC buffers."
+  (if all
+      (mapc (lambda (process)
+	      (mapc (lambda (buffer)
+		      (with-current-buffer buffer
+			(rcirc-update-prompt)))
+		    (with-rcirc-process-buffer process
+		      (mapcar 'cdr rcirc-buffer-alist))))
+	    (rcirc-process-list))
+    (let ((inhibit-read-only t)
+	  (prompt (or rcirc-prompt "")))
+      (mapc (lambda (rep)
+	      (setq prompt
+		    (replace-regexp-in-string (car rep) (cdr rep) prompt)))
+	    (list (cons "%n" (rcirc-buffer-nick))
+		  (cons "%s" (with-rcirc-server-buffer rcirc-server-name))
+		  (cons "%t" (or rcirc-target ""))))
+      (save-excursion
+	(delete-region rcirc-prompt-start-marker rcirc-prompt-end-marker)
+	(goto-char rcirc-prompt-start-marker)
+	(let ((start (point)))
+	  (insert-before-markers prompt)
+	  (set-marker rcirc-prompt-start-marker start)
+	  (when (not (zerop (- rcirc-prompt-end-marker
+			       rcirc-prompt-start-marker)))
+	    (add-text-properties rcirc-prompt-start-marker
+				 rcirc-prompt-end-marker
+				 (list 'face 'rcirc-prompt
+				       'read-only t 'field t
+				       'front-sticky t 'rear-nonsticky t))))))))
+
+(defun rcirc-set-changed (option value)
+  "Set OPTION to VALUE and do updates after a customization change."
+  (set-default option value)
+  (cond ((eq option 'rcirc-prompt)
+	 (rcirc-update-prompt 'all))
+	(t
+	 (error "Bad option %s" option))))
+
+(defun rcirc-channel-p (target)
+  "Return t if TARGET is a channel name."
+  (and target
+       (not (zerop (length target)))
+       (or (eq (aref target 0) ?#)
+           (eq (aref target 0) ?&))))
+
+(defcustom rcirc-log-directory "~/.emacs.d/rcirc-log"
+  "Directory to keep IRC logfiles."
+  :type 'directory
+  :group 'rcirc)
+
+(defcustom rcirc-log-flag nil
+  "Non-nil means log IRC activity to disk.
+Logfiles are kept in `rcirc-log-directory'."
+  :type 'boolean
+  :group 'rcirc)
+
+(defun rcirc-kill-buffer-hook ()
+  "Part the channel when killing an rcirc buffer.
+
+If `rcirc-kill-channel-buffers' is non-nil and the killed buffer
+is a server buffer, kills all of the channel buffers associated
+with it."
+  (when (eq major-mode 'rcirc-mode)
+    (when (and rcirc-log-flag
+               rcirc-log-directory)
+      (rcirc-log-write))
+    (rcirc-clean-up-buffer "Killed buffer")
+    (when (and rcirc-buffer-alist ;; it's a server buffer
+               rcirc-kill-channel-buffers)
+      (dolist (channel rcirc-buffer-alist)
+	(kill-buffer (cdr channel))))))
+
+(defun rcirc-change-major-mode-hook ()
+  "Part the channel when changing the major-mode."
+  (rcirc-clean-up-buffer "Changed major mode"))
+
+(defun rcirc-clean-up-buffer (reason)
+  (let ((buffer (current-buffer)))
+    (rcirc-clear-activity buffer)
+    (when (and (rcirc-buffer-process)
+	       (rcirc--connection-open-p (rcirc-buffer-process)))
+      (with-rcirc-server-buffer
+       (setq rcirc-buffer-alist
+	     (rassq-delete-all buffer rcirc-buffer-alist)))
+      (rcirc-update-short-buffer-names)
+      (if (rcirc-channel-p rcirc-target)
+	  (rcirc-send-string (rcirc-buffer-process)
+			     (concat "PART " rcirc-target " :" reason))
+	(when rcirc-target
+	  (rcirc-remove-nick-channel (rcirc-buffer-process)
+				     (rcirc-buffer-nick)
+				     rcirc-target))))
+    (setq rcirc-target nil)))
+
+(defun rcirc-generate-new-buffer-name (process target)
+  "Return a buffer name based on PROCESS and TARGET.
+This is used for the initial name given to IRC buffers."
+  (substring-no-properties
+   (if target
+       (concat target "@" (process-name process))
+     (concat "*" (process-name process) "*"))))
+
+(defun rcirc-get-buffer (process target &optional server)
+  "Return the buffer associated with the PROCESS and TARGET.
+
+If optional argument SERVER is non-nil, return the server buffer
+if there is no existing buffer for TARGET, otherwise return nil."
+  (with-rcirc-process-buffer process
+    (if (null target)
+	(current-buffer)
+      (let ((buffer (cdr (assoc-string target rcirc-buffer-alist t))))
+	(or buffer (when server (current-buffer)))))))
+
+(defun rcirc-get-buffer-create (process target)
+  "Return the buffer associated with the PROCESS and TARGET.
+Create the buffer if it doesn't exist."
+  (let ((buffer (rcirc-get-buffer process target)))
+    (if (and buffer (buffer-live-p buffer))
+	(with-current-buffer buffer
+	  (when (not rcirc-target)
+ 	    (setq rcirc-target target))
+	  buffer)
+      ;; create the buffer
+      (with-rcirc-process-buffer process
+	(let ((new-buffer (get-buffer-create
+			   (rcirc-generate-new-buffer-name process target))))
+	  (with-current-buffer new-buffer
+	    (rcirc-mode process target)
+	    (rcirc-put-nick-channel process (rcirc-nick process) target
+				    rcirc-current-line))
+	  new-buffer)))))
+
+(defun rcirc-send-input ()
+  "Send input to target associated with the current buffer."
+  (interactive)
+  (if (< (point) rcirc-prompt-end-marker)
+      ;; copy the line down to the input area
+      (progn
+	(forward-line 0)
+	(let ((start (if (eq (point) (point-min))
+			 (point)
+		       (if (get-text-property (1- (point)) 'hard)
+			   (point)
+			 (previous-single-property-change (point) 'hard))))
+	      (end (next-single-property-change (1+ (point)) 'hard)))
+	  (goto-char (point-max))
+	  (insert (replace-regexp-in-string
+		   "\n\\s-+" " "
+		   (buffer-substring-no-properties start end)))))
+    ;; process input
+    (goto-char (point-max))
+    (when (not (equal 0 (- (point) rcirc-prompt-end-marker)))
+      ;; delete a trailing newline
+      (when (eq (point) (point-at-bol))
+	(delete-char -1))
+      (let ((input (buffer-substring-no-properties
+		    rcirc-prompt-end-marker (point))))
+	(dolist (line (split-string input "\n"))
+	  (rcirc-process-input-line line))
+	;; add to input-ring
+	(save-excursion
+	  (ring-insert rcirc-input-ring input)
+	  (setq rcirc-input-ring-index 0))))))
+
+(defun rcirc-fill-paragraph (&optional justify)
+  (interactive "P")
+  (when (> (point) rcirc-prompt-end-marker)
+    (save-restriction
+      (narrow-to-region rcirc-prompt-end-marker (point-max))
+      (let ((fill-column rcirc-max-message-length))
+	(fill-region (point-min) (point-max) justify)))))
+
+(defun rcirc-process-input-line (line)
+  (if (string-match "^/\\([^ ]+\\) ?\\(.*\\)$" line)
+      (rcirc-process-command (match-string 1 line)
+			     (match-string 2 line)
+			     line)
+    (rcirc-process-message line)))
+
+(defun rcirc-process-message (line)
+  (if (not rcirc-target)
+      (message "Not joined (no target)")
+    (delete-region rcirc-prompt-end-marker (point))
+    (rcirc-send-message (rcirc-buffer-process) rcirc-target line)
+    (setq rcirc-last-post-time (current-time))))
+
+(defun rcirc-process-command (command args line)
+  (if (eq (aref command 0) ?/)
+      ;; "//text" will send "/text" as a message
+      (rcirc-process-message (substring line 1))
+    (let ((fun (intern-soft (concat "rcirc-cmd-" command)))
+	  (process (rcirc-buffer-process)))
+      (newline)
+      (with-current-buffer (current-buffer)
+	(delete-region rcirc-prompt-end-marker (point))
+	(if (string= command "me")
+	    (rcirc-print process (rcirc-buffer-nick)
+			 "ACTION" rcirc-target args)
+	  (rcirc-print process (rcirc-buffer-nick)
+		       "COMMAND" rcirc-target line))
+	(set-marker rcirc-prompt-end-marker (point))
+	(if (fboundp fun)
+	    (funcall fun args process rcirc-target)
+	  (rcirc-send-string process
+			     (concat command " :" args)))))))
+
+(defvar rcirc-parent-buffer nil)
+(make-variable-buffer-local 'rcirc-parent-buffer)
+(put 'rcirc-parent-buffer 'permanent-local t)
+(defvar rcirc-window-configuration nil)
+(defun rcirc-edit-multiline ()
+  "Move current edit to a dedicated buffer."
+  (interactive)
+  (let ((pos (1+ (- (point) rcirc-prompt-end-marker))))
+    (goto-char (point-max))
+    (let ((text (buffer-substring-no-properties rcirc-prompt-end-marker
+						(point)))
+          (parent (buffer-name)))
+      (delete-region rcirc-prompt-end-marker (point))
+      (setq rcirc-window-configuration (current-window-configuration))
+      (pop-to-buffer (concat "*multiline " parent "*"))
+      (funcall rcirc-multiline-major-mode)
+      (rcirc-multiline-minor-mode 1)
+      (setq rcirc-parent-buffer parent)
+      (insert text)
+      (and (> pos 0) (goto-char pos))
+      (message "Type C-c C-c to return text to %s, or C-c C-k to cancel" parent))))
+
+(defvar rcirc-multiline-minor-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "C-c C-c") 'rcirc-multiline-minor-submit)
+    (define-key map (kbd "C-x C-s") 'rcirc-multiline-minor-submit)
+    (define-key map (kbd "C-c C-k") 'rcirc-multiline-minor-cancel)
+    (define-key map (kbd "ESC ESC ESC") 'rcirc-multiline-minor-cancel)
+    map)
+  "Keymap for multiline mode in rcirc.")
+
+(define-minor-mode rcirc-multiline-minor-mode
+  "Minor mode for editing multiple lines in rcirc.
+With a prefix argument ARG, enable the mode if ARG is positive,
+and disable it otherwise.  If called from Lisp, enable the mode
+if ARG is omitted or nil."
+  :init-value nil
+  :lighter " rcirc-mline"
+  :keymap rcirc-multiline-minor-mode-map
+  :global nil
+  :group 'rcirc
+  (setq fill-column rcirc-max-message-length))
+
+(defun rcirc-multiline-minor-submit ()
+  "Send the text in buffer back to parent buffer."
+  (interactive)
+  (untabify (point-min) (point-max))
+  (let ((text (buffer-substring (point-min) (point-max)))
+        (buffer (current-buffer))
+        (pos (point)))
+    (set-buffer rcirc-parent-buffer)
+    (goto-char (point-max))
+    (insert text)
+    (kill-buffer buffer)
+    (set-window-configuration rcirc-window-configuration)
+    (goto-char (+ rcirc-prompt-end-marker (1- pos)))))
+
+(defun rcirc-multiline-minor-cancel ()
+  "Cancel the multiline edit."
+  (interactive)
+  (kill-buffer (current-buffer))
+  (set-window-configuration rcirc-window-configuration))
+
+(defun rcirc-any-buffer (process)
+  "Return a buffer for PROCESS, either the one selected or the process buffer."
+  (if rcirc-always-use-server-buffer-flag
+      (process-buffer process)
+    (let ((buffer (window-buffer)))
+      (if (and buffer
+	       (with-current-buffer buffer
+		 (and (eq major-mode 'rcirc-mode)
+		      (eq (rcirc-buffer-process) process))))
+	  buffer
+	(process-buffer process)))))
+
+(defcustom rcirc-response-formats
+  '(("PRIVMSG" . "<%N> %m")
+    ("NOTICE"  . "-%N- %m")
+    ("ACTION"  . "[%N %m]")
+    ("COMMAND" . "%m")
+    ("ERROR"   . "%fw!!! %m")
+    (t         . "%fp*** %fs%n %r %m"))
+  "An alist of formats used for printing responses.
+The format is looked up using the response-type as a key;
+if no match is found, the default entry (with a key of t) is used.
+
+The entry's value part should be a string, which is inserted with
+the of the following escape sequences replaced by the described values:
+
+  %m        The message text
+  %n        The sender's nick
+  %N        The sender's nick (with face `rcirc-my-nick' or `rcirc-other-nick')
+  %r        The response-type
+  %t        The target
+  %fw       Following text uses the face `font-lock-warning-face'
+  %fp       Following text uses the face `rcirc-server-prefix'
+  %fs       Following text uses the face `rcirc-server'
+  %f[FACE]  Following text uses the face FACE
+  %f-       Following text uses the default face
+  %%        A literal `%' character"
+  :type '(alist :key-type (choice (string :tag "Type")
+				  (const :tag "Default" t))
+		:value-type string)
+  :group 'rcirc)
+
+(defun rcirc-format-response-string (process sender response target text)
+  "Return a nicely-formatted response string, incorporating TEXT
+\(and perhaps other arguments).  The specific formatting used
+is found by looking up RESPONSE in `rcirc-response-formats'."
+  (with-temp-buffer
+    (insert (or (cdr (assoc response rcirc-response-formats))
+		(cdr (assq t rcirc-response-formats))))
+    (goto-char (point-min))
+    (let ((start (point-min))
+	  (sender (if (or (not sender)
+			  (string= (rcirc-server-name process) sender))
+		      ""
+		    sender))
+	  face)
+      (while (re-search-forward "%\\(\\(f\\(.\\)\\)\\|\\(.\\)\\)" nil t)
+	(rcirc-add-face start (match-beginning 0) face)
+	(setq start (match-beginning 0))
+	(replace-match
+	 (cl-case (aref (match-string 1) 0)
+	    (?f (setq face
+		      (cl-case (string-to-char (match-string 3))
+			(?w 'font-lock-warning-face)
+			(?p 'rcirc-server-prefix)
+			(?s 'rcirc-server)
+			(t nil)))
+		"")
+	    (?n sender)
+	    (?N (let ((my-nick (rcirc-nick process)))
+		  (save-match-data
+		    (with-syntax-table rcirc-nick-syntax-table
+		      (rcirc-facify sender
+				    (cond ((string= sender my-nick)
+					   'rcirc-my-nick)
+					  ((and rcirc-bright-nicks
+						(string-match
+						 (regexp-opt rcirc-bright-nicks
+							     'words)
+						 sender))
+					   'rcirc-bright-nick)
+					  ((and rcirc-dim-nicks
+						(string-match
+						 (regexp-opt rcirc-dim-nicks
+							     'words)
+						 sender))
+					   'rcirc-dim-nick)
+					  (t
+					   'rcirc-other-nick)))))))
+	    (?m (propertize text 'rcirc-text text))
+	    (?r response)
+	    (?t (or target ""))
+	    (t (concat "UNKNOWN CODE:" (match-string 0))))
+	 t t nil 0)
+	(rcirc-add-face (match-beginning 0) (match-end 0) face))
+      (rcirc-add-face start (match-beginning 0) face))
+      (buffer-substring (point-min) (point-max))))
+
+(defun rcirc-target-buffer (process sender response target _text)
+  "Return a buffer to print the server response."
+  (cl-assert (not (bufferp target)))
+  (with-rcirc-process-buffer process
+    (cond ((not target)
+	   (rcirc-any-buffer process))
+	  ((not (rcirc-channel-p target))
+	   ;; message from another user
+	   (if (or (string= response "PRIVMSG")
+		   (string= response "ACTION"))
+	       (rcirc-get-buffer-create process (if (string= sender rcirc-nick)
+						    target
+						  sender))
+	     (rcirc-get-buffer process target t)))
+	  ((or (rcirc-get-buffer process target)
+	       (rcirc-any-buffer process))))))
+
+(defvar rcirc-activity-types nil)
+(make-variable-buffer-local 'rcirc-activity-types)
+(defvar rcirc-last-sender nil)
+(make-variable-buffer-local 'rcirc-last-sender)
+
+(defcustom rcirc-omit-threshold 100
+  "Number of lines since last activity from a nick before `rcirc-omit-responses' are omitted."
+  :type 'integer
+  :group 'rcirc)
+
+(defcustom rcirc-log-process-buffers nil
+  "Non-nil if rcirc process buffers should be logged to disk."
+  :group 'rcirc
+  :type 'boolean
+  :version "24.1")
+
+(defun rcirc-last-quit-line (process nick target)
+  "Return the line number where NICK left TARGET.
+Returns nil if the information is not recorded."
+  (let ((chanbuf (rcirc-get-buffer process target)))
+    (when chanbuf
+      (cdr (assoc-string nick (with-current-buffer chanbuf
+				rcirc-recent-quit-alist))))))
+
+(defun rcirc-last-line (process nick target)
+  "Return the line from the last activity from NICK in TARGET."
+  (let ((line (or (cdr (assoc-string target
+				     (gethash nick (with-rcirc-server-buffer
+						     rcirc-nick-table)) t))
+		  (rcirc-last-quit-line process nick target))))
+    (if line
+	line
+      ;;(message "line is nil for %s in %s" nick target)
+      nil)))
+
+(defun rcirc-elapsed-lines (process nick target)
+  "Return the number of lines since activity from NICK in TARGET."
+  (let ((last-activity-line (rcirc-last-line process nick target)))
+    (when (and last-activity-line
+	       (> last-activity-line 0))
+      (- rcirc-current-line last-activity-line))))
+
+(defvar rcirc-markup-text-functions
+  '(rcirc-markup-attributes
+    rcirc-markup-my-nick
+    rcirc-markup-urls
+    rcirc-markup-keywords
+    rcirc-markup-bright-nicks)
+
+  "List of functions used to manipulate text before it is printed.
+
+Each function takes two arguments, SENDER, and RESPONSE.  The
+buffer is narrowed with the text to be printed and the point is
+at the beginning of the `rcirc-text' propertized text.")
+
+(defun rcirc-print (process sender response target text &optional activity)
+  "Print TEXT in the buffer associated with TARGET.
+Format based on SENDER and RESPONSE.  If ACTIVITY is non-nil,
+record activity."
+  (or text (setq text ""))
+  (unless (and (or (member sender rcirc-ignore-list)
+		   (member (with-syntax-table rcirc-nick-syntax-table
+			     (when (string-match "^\\([^/]\\w*\\)[:,]" text)
+			       (match-string 1 text)))
+			   rcirc-ignore-list))
+	       ;; do not ignore if we sent the message
+ 	       (not (string= sender (rcirc-nick process))))
+    (let* ((buffer (rcirc-target-buffer process sender response target text))
+	   (inhibit-read-only t))
+      (with-current-buffer buffer
+	(let ((moving (= (point) rcirc-prompt-end-marker))
+	      (old-point (point-marker))
+	      (fill-start (marker-position rcirc-prompt-start-marker)))
+
+	  (setq text (decode-coding-string text rcirc-decode-coding-system))
+	  (unless (string= sender (rcirc-nick process))
+	    ;; mark the line with overlay arrow
+	    (unless (or (marker-position overlay-arrow-position)
+			(get-buffer-window (current-buffer))
+			(member response rcirc-omit-responses))
+	      (set-marker overlay-arrow-position
+			  (marker-position rcirc-prompt-start-marker))))
+
+	  ;; temporarily set the marker insertion-type because
+	  ;; insert-before-markers results in hidden text in new buffers
+	  (goto-char rcirc-prompt-start-marker)
+	  (set-marker-insertion-type rcirc-prompt-start-marker t)
+	  (set-marker-insertion-type rcirc-prompt-end-marker t)
+
+	  (let ((start (point)))
+	    (insert (rcirc-format-response-string process sender response nil
+						  text)
+		    (propertize "\n" 'hard t))
+
+ 	    ;; squeeze spaces out of text before rcirc-text
+	    (fill-region fill-start
+			 (1- (or (next-single-property-change fill-start
+							      'rcirc-text)
+				 rcirc-prompt-end-marker)))
+
+	    ;; run markup functions
+ 	    (save-excursion
+ 	      (save-restriction
+ 		(narrow-to-region start rcirc-prompt-start-marker)
+		(goto-char (or (next-single-property-change start 'rcirc-text)
+			       (point)))
+		(when (rcirc-buffer-process)
+		  (save-excursion (rcirc-markup-timestamp sender response))
+		  (dolist (fn rcirc-markup-text-functions)
+		    (save-excursion (funcall fn sender response)))
+		  (when rcirc-fill-flag
+		    (save-excursion (rcirc-markup-fill sender response))))
+
+		(when rcirc-read-only-flag
+		  (add-text-properties (point-min) (point-max)
+				       '(read-only t front-sticky t))))
+	      ;; make text omittable
+	      (let ((last-activity-lines (rcirc-elapsed-lines process sender target)))
+		(if (and (not (string= (rcirc-nick process) sender))
+			 (member response rcirc-omit-responses)
+			 (or (not last-activity-lines)
+			     (< rcirc-omit-threshold last-activity-lines)))
+		    (put-text-property (1- start) (1- rcirc-prompt-start-marker)
+				       'invisible 'rcirc-omit)
+		  ;; otherwise increment the line count
+		  (setq rcirc-current-line (1+ rcirc-current-line))))))
+
+	  (set-marker-insertion-type rcirc-prompt-start-marker nil)
+	  (set-marker-insertion-type rcirc-prompt-end-marker nil)
+
+	  ;; truncate buffer if it is very long
+	  (save-excursion
+	    (when (and rcirc-buffer-maximum-lines
+		       (> rcirc-buffer-maximum-lines 0)
+		       (= (forward-line (- rcirc-buffer-maximum-lines)) 0))
+	      (delete-region (point-min) (point))))
+
+	  ;; set the window point for buffers show in windows
+	  (walk-windows (lambda (w)
+			  (when (and (not (eq (selected-window) w))
+				     (eq (current-buffer)
+					 (window-buffer w))
+				     (>= (window-point w)
+					 rcirc-prompt-end-marker))
+			      (set-window-point w (point-max))))
+			nil t)
+
+	  ;; restore the point
+	  (goto-char (if moving rcirc-prompt-end-marker old-point))
+
+	  ;; keep window on bottom line if it was already there
+	  (when rcirc-scroll-show-maximum-output
+	    (let ((window (get-buffer-window)))
+	      (when window
+		(with-selected-window window
+		  (when (eq major-mode 'rcirc-mode)
+		    (when (<= (- (window-height)
+				 (count-screen-lines (window-point)
+						     (window-start))
+				 1)
+			      0)
+		      (recenter -1)))))))
+
+	  ;; flush undo (can we do something smarter here?)
+	  (buffer-disable-undo)
+	  (buffer-enable-undo))
+
+	;; record mode line activity
+	(when (and activity
+		   (not rcirc-ignore-buffer-activity-flag)
+		   (not (and rcirc-dim-nicks sender
+			     (string-match (regexp-opt rcirc-dim-nicks) sender)
+			     (rcirc-channel-p target))))
+	      (rcirc-record-activity (current-buffer)
+				     (when (not (rcirc-channel-p rcirc-target))
+				       'nick)))
+
+	(when (and rcirc-log-flag
+		   (or target
+		       rcirc-log-process-buffers))
+	  (rcirc-log process sender response target text))
+
+	(sit-for 0)			; displayed text before hook
+	(run-hook-with-args 'rcirc-print-functions
+			    process sender response target text)))))
+
+(defun rcirc-generate-log-filename (process target)
+  (if target
+      (rcirc-generate-new-buffer-name process target)
+    (process-name process)))
+
+(defcustom rcirc-log-filename-function 'rcirc-generate-log-filename
+  "A function to generate the filename used by rcirc's logging facility.
+
+It is called with two arguments, PROCESS and TARGET (see
+`rcirc-generate-new-buffer-name' for their meaning), and should
+return the filename, or nil if no logging is desired for this
+session.
+
+If the returned filename is absolute (`file-name-absolute-p'
+returns t), then it is used as-is, otherwise the resulting file
+is put into `rcirc-log-directory'.
+
+The filename is then cleaned using `convert-standard-filename' to
+guarantee valid filenames for the current OS."
+  :group 'rcirc
+  :type 'function)
+
+(defun rcirc-log (process sender response target text)
+  "Record line in `rcirc-log', to be later written to disk."
+  (let ((filename (funcall rcirc-log-filename-function process target)))
+    (unless (null filename)
+      (let ((cell (assoc-string filename rcirc-log-alist))
+	    (line (concat (format-time-string rcirc-time-format)
+			  (substring-no-properties
+			   (rcirc-format-response-string process sender
+							 response target text))
+			  "\n")))
+	(if cell
+	    (setcdr cell (concat (cdr cell) line))
+	  (setq rcirc-log-alist
+		(cons (cons filename line) rcirc-log-alist)))))))
+
+(defun rcirc-log-write ()
+  "Flush `rcirc-log-alist' data to disk.
+
+Log data is written to `rcirc-log-directory', except for
+log-files with absolute names (see `rcirc-log-filename-function')."
+  (dolist (cell rcirc-log-alist)
+    (let ((filename (convert-standard-filename
+                     (expand-file-name (car cell)
+                                       rcirc-log-directory)))
+	  (coding-system-for-write 'utf-8))
+      (make-directory (file-name-directory filename) t)
+      (with-temp-buffer
+	(insert (cdr cell))
+	(write-region (point-min) (point-max) filename t 'quiet))))
+  (setq rcirc-log-alist nil))
+
+(defun rcirc-view-log-file ()
+  "View logfile corresponding to the current buffer."
+  (interactive)
+  (find-file-other-window
+   (expand-file-name (funcall rcirc-log-filename-function
+			      (rcirc-buffer-process) rcirc-target)
+		     rcirc-log-directory)))
+
+(defun rcirc-join-channels (process channels)
+  "Join CHANNELS."
+  (save-window-excursion
+    (dolist (channel channels)
+      (with-rcirc-process-buffer process
+	(rcirc-cmd-join channel process)))))
+
+;;; nick management
+(defvar rcirc-nick-prefix-chars "~&@%+")
+(defun rcirc-user-nick (user)
+  "Return the nick from USER.  Remove any non-nick junk."
+  (save-match-data
+    (if (string-match (concat "^[" rcirc-nick-prefix-chars
+			      "]?\\([^! ]+\\)!?") (or user ""))
+	(match-string 1 user)
+      user)))
+
+(defun rcirc-nick-channels (process nick)
+  "Return list of channels for NICK."
+  (with-rcirc-process-buffer process
+    (mapcar (lambda (x) (car x))
+	    (gethash nick rcirc-nick-table))))
+
+(defun rcirc-put-nick-channel (process nick channel &optional line)
+  "Add CHANNEL to list associated with NICK.
+Update the associated linestamp if LINE is non-nil.
+
+If the record doesn't exist, and LINE is nil, set the linestamp
+to zero."
+  (let ((nick (rcirc-user-nick nick)))
+    (with-rcirc-process-buffer process
+      (let* ((chans (gethash nick rcirc-nick-table))
+	     (record (assoc-string channel chans t)))
+	(if record
+	    (when line (setcdr record line))
+	  (puthash nick (cons (cons channel (or line 0))
+			      chans)
+		   rcirc-nick-table))))))
+
+(defun rcirc-nick-remove (process nick)
+  "Remove NICK from table."
+  (with-rcirc-process-buffer process
+    (remhash nick rcirc-nick-table)))
+
+(defun rcirc-remove-nick-channel (process nick channel)
+  "Remove the CHANNEL from list associated with NICK."
+  (with-rcirc-process-buffer process
+    (let* ((chans (gethash nick rcirc-nick-table))
+           (newchans
+	    ;; instead of assoc-string-delete-all:
+	    (let ((record (assoc-string channel chans t)))
+	      (when record
+		(setcar record 'delete)
+		(assq-delete-all 'delete chans)))))
+      (if newchans
+          (puthash nick newchans rcirc-nick-table)
+        (remhash nick rcirc-nick-table)))))
+
+(defun rcirc-channel-nicks (process target)
+  "Return the list of nicks associated with TARGET sorted by last activity."
+  (when target
+    (if (rcirc-channel-p target)
+	(with-rcirc-process-buffer process
+	  (let (nicks)
+	    (maphash
+	     (lambda (k v)
+	       (let ((record (assoc-string target v t)))
+		 (if record
+		     (setq nicks (cons (cons k (cdr record)) nicks)))))
+	     rcirc-nick-table)
+	    (mapcar (lambda (x) (car x))
+		    (sort nicks (lambda (x y)
+				  (let ((lx (or (cdr x) 0))
+					(ly (or (cdr y) 0)))
+				    (< ly lx)))))))
+      (list target))))
+
+(defun rcirc-ignore-update-automatic (nick)
+  "Remove NICK from `rcirc-ignore-list'
+if NICK is also on `rcirc-ignore-list-automatic'."
+  (when (member nick rcirc-ignore-list-automatic)
+      (setq rcirc-ignore-list-automatic
+	    (delete nick rcirc-ignore-list-automatic)
+	    rcirc-ignore-list
+	    (delete nick rcirc-ignore-list))))
+
+(defun rcirc-nickname< (s1 s2)
+  "Return t if IRC nickname S1 is less than S2, and nil otherwise.
+Operator nicknames (@) are considered less than voiced
+nicknames (+).  Any other nicknames are greater than voiced
+nicknames.  The comparison is case-insensitive."
+  (setq s1 (downcase s1)
+        s2 (downcase s2))
+  (let* ((s1-op (eq ?@ (string-to-char s1)))
+         (s2-op (eq ?@ (string-to-char s2))))
+    (if s1-op
+        (if s2-op
+            (string< (substring s1 1) (substring s2 1))
+          t)
+      (if s2-op
+          nil
+        (string< s1 s2)))))
+
+(defun rcirc-sort-nicknames-join (input sep)
+  "Return a string of sorted nicknames.
+INPUT is a string containing nicknames separated by SEP.
+This function does not alter the INPUT string."
+  (let* ((parts (split-string input sep t))
+         (sorted (sort parts 'rcirc-nickname<)))
+    (mapconcat 'identity sorted sep)))
+
+;;; activity tracking
+(defvar rcirc-track-minor-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "C-c C-@") 'rcirc-next-active-buffer)
+    (define-key map (kbd "C-c C-SPC") 'rcirc-next-active-buffer)
+    map)
+  "Keymap for rcirc track minor mode.")
+
+;;;###autoload
+(define-minor-mode rcirc-track-minor-mode
+  "Global minor mode for tracking activity in rcirc buffers.
+With a prefix argument ARG, enable the mode if ARG is positive,
+and disable it otherwise.  If called from Lisp, enable the mode
+if ARG is omitted or nil."
+  :init-value nil
+  :lighter ""
+  :keymap rcirc-track-minor-mode-map
+  :global t
+  :group 'rcirc
+  (or global-mode-string (setq global-mode-string '("")))
+  ;; toggle the mode-line channel indicator
+  (if rcirc-track-minor-mode
+      (progn
+	(and (not (memq 'rcirc-activity-string global-mode-string))
+	     (setq global-mode-string
+		   (append global-mode-string '(rcirc-activity-string))))
+	(add-hook 'window-configuration-change-hook
+		  'rcirc-window-configuration-change))
+    (setq global-mode-string
+	  (delete 'rcirc-activity-string global-mode-string))
+    (remove-hook 'window-configuration-change-hook
+		 'rcirc-window-configuration-change)))
+
+(or (assq 'rcirc-ignore-buffer-activity-flag minor-mode-alist)
+    (setq minor-mode-alist
+          (cons '(rcirc-ignore-buffer-activity-flag " Ignore") minor-mode-alist)))
+(or (assq 'rcirc-low-priority-flag minor-mode-alist)
+    (setq minor-mode-alist
+          (cons '(rcirc-low-priority-flag " LowPri") minor-mode-alist)))
+
+(defun rcirc-toggle-ignore-buffer-activity ()
+  "Toggle the value of `rcirc-ignore-buffer-activity-flag'."
+  (interactive)
+  (setq rcirc-ignore-buffer-activity-flag
+	(not rcirc-ignore-buffer-activity-flag))
+  (message (if rcirc-ignore-buffer-activity-flag
+	       "Ignore activity in this buffer"
+	     "Notice activity in this buffer"))
+  (force-mode-line-update))
+
+(defun rcirc-toggle-low-priority ()
+  "Toggle the value of `rcirc-low-priority-flag'."
+  (interactive)
+  (setq rcirc-low-priority-flag
+	(not rcirc-low-priority-flag))
+  (message (if rcirc-low-priority-flag
+	       "Activity in this buffer is low priority"
+	     "Activity in this buffer is normal priority"))
+  (force-mode-line-update))
+
+(defun rcirc-switch-to-server-buffer ()
+  "Switch to the server buffer associated with current channel buffer."
+  (interactive)
+  (unless (buffer-live-p rcirc-server-buffer)
+    (error "No such buffer"))
+  (switch-to-buffer rcirc-server-buffer))
+
+(defun rcirc-jump-to-first-unread-line ()
+  "Move the point to the first unread line in this buffer."
+  (interactive)
+  (if (marker-position overlay-arrow-position)
+      (goto-char overlay-arrow-position)
+    (message "No unread messages")))
+
+(defun rcirc-bury-buffers ()
+  "Bury all RCIRC buffers."
+  (interactive)
+  (dolist (buf (buffer-list))
+    (when (eq 'rcirc-mode (with-current-buffer buf major-mode))
+      (bury-buffer buf)         ; buffers not shown
+      (quit-windows-on buf))))  ; buffers shown in a window
+
+(defun rcirc-next-active-buffer (arg)
+  "Switch to the next rcirc buffer with activity.
+With prefix ARG, go to the next low priority buffer with activity."
+  (interactive "P")
+  (let* ((pair (rcirc-split-activity rcirc-activity))
+	 (lopri (car pair))
+	 (hipri (cdr pair)))
+    (if (or (and (not arg) hipri)
+	    (and arg lopri))
+	(progn
+	  (switch-to-buffer (car (if arg lopri hipri)))
+	  (when (> (point) rcirc-prompt-start-marker)
+	    (recenter -1)))
+      (rcirc-bury-buffers)
+      (message "No IRC activity.%s"
+               (if lopri
+                   (concat
+                    "  Type C-u " (key-description (this-command-keys))
+                    " for low priority activity.")
+                 "")))))
+
+(define-obsolete-variable-alias 'rcirc-activity-hooks
+  'rcirc-activity-functions "24.3")
+(defvar rcirc-activity-functions nil
+  "Hook to be run when there is channel activity.
+
+Functions are called with a single argument, the buffer with the
+activity.  Only run if the buffer is not visible and
+`rcirc-ignore-buffer-activity-flag' is non-nil.")
+
+(defun rcirc-record-activity (buffer &optional type)
+  "Record BUFFER activity with TYPE."
+  (with-current-buffer buffer
+    (let ((old-activity rcirc-activity)
+	  (old-types rcirc-activity-types))
+      (when (not (get-buffer-window (current-buffer) t))
+	(setq rcirc-activity
+	      (sort (if (memq (current-buffer) rcirc-activity) rcirc-activity
+                      (cons (current-buffer) rcirc-activity))
+		    (lambda (b1 b2)
+		      (let ((t1 (with-current-buffer b1 rcirc-last-post-time))
+			    (t2 (with-current-buffer b2 rcirc-last-post-time)))
+			(time-less-p t2 t1)))))
+	(cl-pushnew type rcirc-activity-types)
+	(unless (and (equal rcirc-activity old-activity)
+		     (member type old-types))
+	  (rcirc-update-activity-string)))))
+  (run-hook-with-args 'rcirc-activity-functions buffer))
+
+(defun rcirc-clear-activity (buffer)
+  "Clear the BUFFER activity."
+  (setq rcirc-activity (remove buffer rcirc-activity))
+  (with-current-buffer buffer
+    (setq rcirc-activity-types nil)))
+
+(defun rcirc-clear-unread (buffer)
+  "Erase the last read message arrow from BUFFER."
+  (when (buffer-live-p buffer)
+    (with-current-buffer buffer
+      (set-marker overlay-arrow-position nil))))
+
+(defun rcirc-split-activity (activity)
+  "Return a cons cell with ACTIVITY split into (lopri . hipri)."
+  (let (lopri hipri)
+    (dolist (buf activity)
+      (with-current-buffer buf
+	(if (and rcirc-low-priority-flag
+		 (not (member 'nick rcirc-activity-types)))
+	    (push buf lopri)
+	  (push buf hipri))))
+    (cons (nreverse lopri) (nreverse hipri))))
+
+(defvar rcirc-update-activity-string-hook nil
+  "Hook run whenever the activity string is updated.")
+
+;; TODO: add mouse properties
+(defun rcirc-update-activity-string ()
+  "Update mode-line string."
+  (let* ((pair (rcirc-split-activity rcirc-activity))
+	 (lopri (car pair))
+	 (hipri (cdr pair)))
+    (setq rcirc-activity-string
+	  (cond ((or hipri lopri)
+		 (concat (and hipri "[")
+			 (rcirc-activity-string hipri)
+			 (and hipri lopri ",")
+			 (and lopri
+			      (concat "("
+				      (rcirc-activity-string lopri)
+				      ")"))
+			 (and hipri "]")))
+		((not (null (rcirc-process-list)))
+		 "[]")
+		(t "[]")))
+    (run-hooks 'rcirc-update-activity-string-hook)))
+
+(defun rcirc-activity-string (buffers)
+  (mapconcat (lambda (b)
+	       (let ((s (substring-no-properties (rcirc-short-buffer-name b))))
+		 (with-current-buffer b
+		   (dolist (type rcirc-activity-types)
+		     (rcirc-add-face 0 (length s)
+				     (cl-case type
+				       (nick 'rcirc-track-nick)
+				       (keyword 'rcirc-track-keyword))
+				     s)))
+		 s))
+	     buffers ","))
+
+(defun rcirc-short-buffer-name (buffer)
+  "Return a short name for BUFFER to use in the mode line indicator."
+  (with-current-buffer buffer
+    (or rcirc-short-buffer-name (buffer-name))))
+
+(defun rcirc-visible-buffers ()
+  "Return a list of the visible buffers that are in rcirc-mode."
+  (let (acc)
+    (walk-windows (lambda (w)
+		    (with-current-buffer (window-buffer w)
+		      (when (eq major-mode 'rcirc-mode)
+			(push (current-buffer) acc)))))
+    acc))
+
+(defvar rcirc-visible-buffers nil)
+(defun rcirc-window-configuration-change ()
+  (unless (minibuffer-window-active-p (minibuffer-window))
+    ;; delay this until command has finished to make sure window is
+    ;; actually visible before clearing activity
+    (add-hook 'post-command-hook 'rcirc-window-configuration-change-1)))
+
+(defun rcirc-window-configuration-change-1 ()
+  ;; clear activity and overlay arrows
+  (let* ((old-activity rcirc-activity)
+	 (hidden-buffers rcirc-visible-buffers))
+
+    (setq rcirc-visible-buffers (rcirc-visible-buffers))
+
+    (dolist (vbuf rcirc-visible-buffers)
+      (setq hidden-buffers (delq vbuf hidden-buffers))
+      ;; clear activity for all visible buffers
+      (rcirc-clear-activity vbuf))
+
+    ;; clear unread arrow from recently hidden buffers
+    (dolist (hbuf hidden-buffers)
+      (rcirc-clear-unread hbuf))
+
+    ;; remove any killed buffers from list
+    (setq rcirc-activity
+	  (delq nil (mapcar (lambda (buf) (when (buffer-live-p buf) buf))
+			    rcirc-activity)))
+    ;; update the mode-line string
+    (unless (equal old-activity rcirc-activity)
+      (rcirc-update-activity-string)))
+
+  (remove-hook 'post-command-hook 'rcirc-window-configuration-change-1))
+
+
+;;; buffer name abbreviation
+(defun rcirc-update-short-buffer-names ()
+  (let ((bufalist
+	 (apply 'append (mapcar (lambda (process)
+				  (with-rcirc-process-buffer process
+				    rcirc-buffer-alist))
+				(rcirc-process-list)))))
+    (dolist (i (rcirc-abbreviate bufalist))
+      (when (buffer-live-p (cdr i))
+	(with-current-buffer (cdr i)
+	  (setq rcirc-short-buffer-name (car i)))))))
+
+(defun rcirc-abbreviate (pairs)
+  (apply 'append (mapcar 'rcirc-rebuild-tree (rcirc-make-trees pairs))))
+
+(defun rcirc-rebuild-tree (tree &optional acc)
+  (let ((ch (char-to-string (car tree))))
+    (dolist (x (cdr tree))
+      (if (listp x)
+	  (setq acc (append acc
+			   (mapcar (lambda (y)
+				     (cons (concat ch (car y))
+					   (cdr y)))
+				   (rcirc-rebuild-tree x))))
+	(setq acc (cons (cons ch x) acc))))
+    acc))
+
+(defun rcirc-make-trees (pairs)
+  (let (alist)
+    (mapc (lambda (pair)
+	    (if (consp pair)
+		(let* ((str (car pair))
+		       (data (cdr pair))
+		       (char (unless (zerop (length str))
+			       (aref str 0)))
+		       (rest (unless (zerop (length str))
+			       (substring str 1)))
+		       (part (if char (assq char alist))))
+		  (if part
+		      ;; existing partition
+		      (setcdr part (cons (cons rest data) (cdr part)))
+		    ;; new partition
+		    (setq alist (cons (if char
+					  (list char (cons rest data))
+					data)
+				      alist))))
+	      (setq alist (cons pair alist))))
+	  pairs)
+    ;; recurse into cdrs of alist
+    (mapc (lambda (x)
+	    (when (and (listp x) (listp (cadr x)))
+	      (setcdr x (if (> (length (cdr x)) 1)
+			    (rcirc-make-trees (cdr x))
+			  (setcdr x (list (cl-cdadr x)))))))
+	  alist)))
+
+;;; /commands these are called with 3 args: PROCESS, TARGET, which is
+;; the current buffer/channel/user, and ARGS, which is a string
+;; containing the text following the /cmd.
+
+(defmacro defun-rcirc-command (command argument docstring interactive-form
+				       &rest body)
+  "Define a command."
+  `(progn
+     (add-to-list 'rcirc-client-commands ,(concat "/" (symbol-name command)))
+     (defun ,(intern (concat "rcirc-cmd-" (symbol-name command)))
+       (,@argument &optional process target)
+       ,(concat docstring "\n\nNote: If PROCESS or TARGET are nil, the values given"
+		"\nby `rcirc-buffer-process' and `rcirc-target' will be used.")
+       ,interactive-form
+       (let ((process (or process (rcirc-buffer-process)))
+	     (target (or target rcirc-target)))
+         (ignore target)        ; mark `target' variable as ignorable
+	 ,@body))))
+
+(defun-rcirc-command msg (message)
+  "Send private MESSAGE to TARGET."
+  (interactive "i")
+  (if (null message)
+      (progn
+        (setq target (completing-read "Message nick: "
+                                      (with-rcirc-server-buffer
+					rcirc-nick-table)))
+        (when (> (length target) 0)
+          (setq message (read-string (format "Message %s: " target)))
+          (when (> (length message) 0)
+            (rcirc-send-message process target message))))
+    (if (not (string-match "\\([^ ]+\\) \\(.+\\)" message))
+        (message "Not enough args, or something.")
+      (setq target (match-string 1 message)
+            message (match-string 2 message))
+      (rcirc-send-message process target message))))
+
+(defun-rcirc-command query (nick)
+  "Open a private chat buffer to NICK."
+  (interactive (list (completing-read "Query nick: "
+                                      (with-rcirc-server-buffer rcirc-nick-table))))
+  (let ((existing-buffer (rcirc-get-buffer process nick)))
+    (switch-to-buffer (or existing-buffer
+			  (rcirc-get-buffer-create process nick)))
+    (when (not existing-buffer)
+      (rcirc-cmd-whois nick))))
+
+(defun-rcirc-command join (channels)
+  "Join CHANNELS.
+CHANNELS is a comma- or space-separated string of channel names."
+  (interactive "sJoin channels: ")
+  (let* ((split-channels (split-string channels "[ ,]" t))
+         (buffers (mapcar (lambda (ch)
+                            (rcirc-get-buffer-create process ch))
+                          split-channels))
+         (channels (mapconcat 'identity split-channels ",")))
+    (rcirc-send-string process (concat "JOIN " channels))
+    (when (not (eq (selected-window) (minibuffer-window)))
+      (dolist (b buffers) ;; order the new channel buffers in the buffer list
+        (switch-to-buffer b)))))
+
+(defun-rcirc-command invite (nick-channel)
+  "Invite NICK to CHANNEL."
+  (interactive (list
+		(concat
+		 (completing-read "Invite nick: "
+				  (with-rcirc-server-buffer rcirc-nick-table))
+		 " "
+		 (read-string "Channel: "))))
+  (rcirc-send-string process (concat "INVITE " nick-channel)))
+
+;; TODO: /part #channel reason, or consider removing #channel altogether
+(defun-rcirc-command part (channel)
+  "Part CHANNEL."
+  (interactive "sPart channel: ")
+  (let ((channel (if (> (length channel) 0) channel target)))
+    (rcirc-send-string process (concat "PART " channel " :" rcirc-id-string))))
+
+(defun-rcirc-command quit (reason)
+  "Send a quit message to server with REASON."
+  (interactive "sQuit reason: ")
+  (rcirc-send-string process (concat "QUIT :"
+				     (if (not (zerop (length reason)))
+					 reason
+				       rcirc-id-string))))
+
+(defun-rcirc-command reconnect (_)
+  "Reconnect to current server."
+  (interactive "i")
+  (with-rcirc-server-buffer
+    (cond
+     (rcirc-connecting (message "Already connecting"))
+     ((process-live-p process) (message "Server process is alive"))
+     (t (let ((conn-info rcirc-connection-info))
+	  (setf (nth 5 conn-info)
+		(cl-remove-if-not #'rcirc-channel-p
+				  (mapcar #'car rcirc-buffer-alist)))
+	  (apply #'rcirc-connect conn-info))))))
+
+(defun-rcirc-command nick (nick)
+  "Change nick to NICK."
+  (interactive "i")
+  (when (null nick)
+    (setq nick (read-string "New nick: " (rcirc-nick process))))
+  (rcirc-send-string process (concat "NICK " nick)))
+
+(defun-rcirc-command names (channel)
+  "Display list of names in CHANNEL or in current channel if CHANNEL is nil.
+If called interactively, prompt for a channel when prefix arg is supplied."
+  (interactive "P")
+  (if (called-interactively-p 'interactive)
+      (if channel
+          (setq channel (read-string "List names in channel: " target))))
+  (let ((channel (if (> (length channel) 0)
+                     channel
+                   target)))
+    (rcirc-send-string process (concat "NAMES " channel))))
+
+(defun-rcirc-command topic (topic)
+  "List TOPIC for the TARGET channel.
+With a prefix arg, prompt for new topic."
+  (interactive "P")
+  (if (and (called-interactively-p 'interactive) topic)
+      (setq topic (read-string "New Topic: " rcirc-topic)))
+  (rcirc-send-string process (concat "TOPIC " target
+                                     (when (> (length topic) 0)
+                                       (concat " :" topic)))))
+
+(defun-rcirc-command whois (nick)
+  "Request information from server about NICK."
+  (interactive (list
+                (completing-read "Whois: "
+                                 (with-rcirc-server-buffer rcirc-nick-table))))
+  (rcirc-send-string process (concat "WHOIS " nick)))
+
+(defun-rcirc-command mode (args)
+  "Set mode with ARGS."
+  (interactive (list (concat (read-string "Mode nick or channel: ")
+                             " " (read-string "Mode: "))))
+  (rcirc-send-string process (concat "MODE " args)))
+
+(defun-rcirc-command list (channels)
+  "Request information on CHANNELS from server."
+  (interactive "sList Channels: ")
+  (rcirc-send-string process (concat "LIST " channels)))
+
+(defun-rcirc-command oper (args)
+  "Send operator command to server."
+  (interactive "sOper args: ")
+  (rcirc-send-string process (concat "OPER " args)))
+
+(defun-rcirc-command quote (message)
+  "Send MESSAGE literally to server."
+  (interactive "sServer message: ")
+  (rcirc-send-string process message))
+
+(defun-rcirc-command kick (arg)
+  "Kick NICK from current channel."
+  (interactive (list
+                (concat (completing-read "Kick nick: "
+                                         (rcirc-channel-nicks
+					  (rcirc-buffer-process)
+					  rcirc-target))
+                        (read-from-minibuffer "Kick reason: "))))
+  (let* ((arglist (split-string arg))
+         (argstring (concat (car arglist) " :"
+                            (mapconcat 'identity (cdr arglist) " "))))
+    (rcirc-send-string process (concat "KICK " target " " argstring))))
+
+(defun rcirc-cmd-ctcp (args &optional process _target)
+  (if (string-match "^\\([^ ]+\\)\\s-+\\(.+\\)$" args)
+      (let* ((target (match-string 1 args))
+             (request (upcase (match-string 2 args)))
+             (function (intern-soft (concat "rcirc-ctcp-sender-" request))))
+        (if (fboundp function) ;; use special function if available
+            (funcall function process target request)
+          (rcirc-send-ctcp process target request)))
+    (rcirc-print process (rcirc-nick process) "ERROR" nil
+                 "usage: /ctcp NICK REQUEST")))
+
+(defun rcirc-ctcp-sender-PING (process target _request)
+  "Send a CTCP PING message to TARGET."
+  (let ((timestamp (format-time-string "%s")))
+    (rcirc-send-ctcp process target "PING" timestamp)))
+
+(defun rcirc-cmd-me (args &optional process target)
+  (rcirc-send-ctcp process target "ACTION" args))
+
+(defun rcirc-add-or-remove (set &rest elements)
+  (dolist (elt elements)
+    (if (and elt (not (string= "" elt)))
+	(setq set (if (member-ignore-case elt set)
+		      (delete elt set)
+		    (cons elt set)))))
+  set)
+
+(defun-rcirc-command ignore (nick)
+  "Manage the ignore list.
+Ignore NICK, unignore NICK if already ignored, or list ignored
+nicks when no NICK is given.  When listing ignored nicks, the
+ones added to the list automatically are marked with an asterisk."
+  (interactive "sToggle ignoring of nick: ")
+  (setq rcirc-ignore-list
+	(apply #'rcirc-add-or-remove rcirc-ignore-list
+	       (split-string nick nil t)))
+  (rcirc-print process nil "IGNORE" target
+	       (mapconcat
+		(lambda (nick)
+		  (concat nick
+			  (if (member nick rcirc-ignore-list-automatic)
+			      "*" "")))
+		rcirc-ignore-list " ")))
+
+(defun-rcirc-command bright (nick)
+  "Manage the bright nick list."
+  (interactive "sToggle emphasis of nick: ")
+  (setq rcirc-bright-nicks
+	(apply #'rcirc-add-or-remove rcirc-bright-nicks
+	       (split-string nick nil t)))
+  (rcirc-print process nil "BRIGHT" target
+	       (mapconcat 'identity rcirc-bright-nicks " ")))
+
+(defun-rcirc-command dim (nick)
+  "Manage the dim nick list."
+  (interactive "sToggle deemphasis of nick: ")
+  (setq rcirc-dim-nicks
+	(apply #'rcirc-add-or-remove rcirc-dim-nicks
+	       (split-string nick nil t)))
+  (rcirc-print process nil "DIM" target
+	       (mapconcat 'identity rcirc-dim-nicks " ")))
+
+(defun-rcirc-command keyword (keyword)
+  "Manage the keyword list.
+Mark KEYWORD, unmark KEYWORD if already marked, or list marked
+keywords when no KEYWORD is given."
+  (interactive "sToggle highlighting of keyword: ")
+  (setq rcirc-keywords
+	(apply #'rcirc-add-or-remove rcirc-keywords
+	       (split-string keyword nil t)))
+  (rcirc-print process nil "KEYWORD" target
+	       (mapconcat 'identity rcirc-keywords " ")))
+
+
+(defun rcirc-add-face (start end name &optional object)
+  "Add face NAME to the face text property of the text from START to END."
+  (when name
+    (let ((pos start)
+	  next prop)
+      (while (< pos end)
+	(setq prop (get-text-property pos 'font-lock-face object)
+	      next (next-single-property-change pos 'font-lock-face object end))
+	(unless (member name (get-text-property pos 'font-lock-face object))
+	  (add-text-properties pos next
+			       (list 'font-lock-face (cons name prop)) object))
+	(setq pos next)))))
+
+(defun rcirc-facify (string face)
+  "Return a copy of STRING with FACE property added."
+  (let ((string (or string "")))
+    (rcirc-add-face 0 (length string) face string)
+    string))
+
+(defvar rcirc-url-regexp
+  (concat
+   "\\b\\(\\(www\\.\\|\\(s?https?\\|ftp\\|file\\|gopher\\|"
+   "nntp\\|news\\|telnet\\|wais\\|mailto\\|info\\):\\)"
+   "\\(//[-a-z0-9_.]+:[0-9]*\\)?"
+   (if (string-match "[[:digit:]]" "1") ;; Support POSIX?
+       (let ((chars "-a-z0-9_=#$@~%&*+\\/[:word:]")
+	     (punct "!?:;.,"))
+	 (concat
+	  "\\(?:"
+	  ;; Match paired parentheses, e.g. in Wikipedia URLs:
+	  "[" chars punct "]+" "(" "[" chars punct "]+" "[" chars "]*)" "[" chars "]"
+	  "\\|"
+	  "[" chars punct     "]+" "[" chars "]"
+	  "\\)"))
+     (concat ;; XEmacs 21.4 doesn't support POSIX.
+      "\\([-a-z0-9_=!?#$@~%&*+\\/:;.,]\\|\\w\\)+"
+      "\\([-a-z0-9_=#$@~%&*+\\/]\\|\\w\\)"))
+   "\\)")
+  "Regexp matching URLs.  Set to nil to disable URL features in rcirc.")
+
+;; cf cl-remove-if-not
+(defun rcirc-condition-filter (condp lst)
+  "Remove all items not satisfying condition CONDP in list LST.
+CONDP is a function that takes a list element as argument and returns
+non-nil if that element should be included.  Returns a new list."
+  (delq nil (mapcar (lambda (x) (and (funcall condp x) x)) lst)))
+
+(defun rcirc-browse-url (&optional arg)
+  "Prompt for URL to browse based on URLs in buffer before point.
+
+If ARG is given, opens the URL in a new browser window."
+  (interactive "P")
+  (let* ((point (point))
+         (filtered (rcirc-condition-filter
+                    (lambda (x) (>= point (cdr x)))
+                    rcirc-urls))
+         (completions (mapcar (lambda (x) (car x)) filtered))
+         (defaults (mapcar (lambda (x) (car x)) filtered)))
+    (browse-url (completing-read "Rcirc browse-url: "
+                                 completions nil nil (car defaults) nil defaults)
+                arg)))
+
+(defun rcirc-markup-timestamp (_sender _response)
+  (goto-char (point-min))
+  (insert (rcirc-facify (format-time-string rcirc-time-format
+                                            (let ((time rcirc-last-message-time))
+                                              (when time (setq rcirc-last-message-time nil))
+                                              time))
+			'rcirc-timestamp)))
+
+(defun rcirc-markup-attributes (_sender _response)
+  (while (re-search-forward "\\([\C-b\C-_\C-v]\\).*?\\(\\1\\|\C-o\\)" nil t)
+    (rcirc-add-face (match-beginning 0) (match-end 0)
+		    (cl-case (char-after (match-beginning 1))
+		      (?\C-b 'bold)
+		      (?\C-v 'italic)
+		      (?\C-_ 'underline)))
+    ;; keep the ^O since it could terminate other attributes
+    (when (not (eq ?\C-o (char-before (match-end 2))))
+      (delete-region (match-beginning 2) (match-end 2)))
+    (delete-region (match-beginning 1) (match-end 1))
+    (goto-char (match-beginning 1)))
+  ;; remove the ^O characters now
+  (goto-char (point-min))
+  (while (re-search-forward "\C-o+" nil t)
+    (delete-region (match-beginning 0) (match-end 0))))
+
+(defun rcirc-markup-my-nick (_sender response)
+  (with-syntax-table rcirc-nick-syntax-table
+    (while (re-search-forward (concat "\\b"
+				      (regexp-quote (rcirc-nick
+						     (rcirc-buffer-process)))
+				      "\\b")
+			      nil t)
+      (rcirc-add-face (match-beginning 0) (match-end 0)
+		      'rcirc-nick-in-message)
+      (when (string= response "PRIVMSG")
+	(rcirc-add-face (point-min) (point-max)
+			'rcirc-nick-in-message-full-line)
+	(rcirc-record-activity (current-buffer) 'nick)))))
+
+(defun rcirc-markup-urls (_sender _response)
+  (while (and rcirc-url-regexp ;; nil means disable URL catching
+              (re-search-forward rcirc-url-regexp nil t))
+    (let* ((start (match-beginning 0))
+           (end (match-end 0))
+           (url (match-string-no-properties 0))
+           (link-text (buffer-substring-no-properties start end)))
+      ;; Add a button for the URL.  Note that we use `make-text-button',
+      ;; rather than `make-button', as text-buttons are much faster in
+      ;; large buffers.
+      (make-text-button start end
+			'face 'rcirc-url
+			'follow-link t
+			'rcirc-url url
+			'action (lambda (button)
+				  (browse-url (button-get button 'rcirc-url))))
+      ;; record the url if it is not already the latest stored url
+      (when (not (string= link-text (caar rcirc-urls)))
+        (push (cons link-text start) rcirc-urls)))))
+
+(defun rcirc-markup-keywords (sender response)
+  (when (and (string= response "PRIVMSG")
+	     (not (string= sender (rcirc-nick (rcirc-buffer-process)))))
+    (let* ((target (or rcirc-target ""))
+	   (keywords (delq nil (mapcar (lambda (keyword)
+					 (when (not (string-match keyword
+								  target))
+					   keyword))
+				       rcirc-keywords))))
+      (when keywords
+	(while (re-search-forward (regexp-opt keywords 'words) nil t)
+	  (rcirc-add-face (match-beginning 0) (match-end 0) 'rcirc-keyword)
+	  (rcirc-record-activity (current-buffer) 'keyword))))))
+
+(defun rcirc-markup-bright-nicks (_sender response)
+  (when (and rcirc-bright-nicks
+	     (string= response "NAMES"))
+    (with-syntax-table rcirc-nick-syntax-table
+      (while (re-search-forward (regexp-opt rcirc-bright-nicks 'words) nil t)
+	(rcirc-add-face (match-beginning 0) (match-end 0)
+			'rcirc-bright-nick)))))
+
+(defun rcirc-markup-fill (_sender response)
+  (when (not (string= response "372")) 	; /motd
+    (let ((fill-prefix
+	   (or rcirc-fill-prefix
+	       (make-string (- (point) (line-beginning-position)) ?\s)))
+	  (fill-column (- (cond ((null rcirc-fill-column) fill-column)
+                                ((functionp rcirc-fill-column)
+				 (funcall rcirc-fill-column))
+				(t rcirc-fill-column))
+			  ;; make sure ... doesn't cause line wrapping
+			  3)))
+      (fill-region (point) (point-max) nil t))))
+
+;;; handlers
+;; these are called with the server PROCESS, the SENDER, which is a
+;; server or a user, depending on the command, the ARGS, which is a
+;; list of strings, and the TEXT, which is the original server text,
+;; verbatim
+(defun rcirc-handler-001 (process sender args text)
+  (rcirc-handler-generic process "001" sender args text)
+  (with-rcirc-process-buffer process
+    (setq rcirc-connecting nil)
+    (rcirc-reschedule-timeout process)
+    (setq rcirc-server-name sender)
+    (setq rcirc-nick (car args))
+    (rcirc-update-prompt)
+    (if rcirc-auto-authenticate-flag
+        (if (and rcirc-authenticate-before-join
+		 ;; We have to ensure that there's an authentication
+		 ;; entry for that server.  Else,
+		 ;; rcirc-authenticated-hook won't be triggered, and
+		 ;; autojoin won't happen at all.
+		 (let (auth-required)
+		   (dolist (s rcirc-authinfo auth-required)
+		     (when (string-match (car s) rcirc-server-name)
+		       (setq auth-required t)))))
+            (progn
+	      (add-hook 'rcirc-authenticated-hook 'rcirc-join-channels-post-auth t t)
+              (rcirc-authenticate))
+          (rcirc-authenticate)
+          (rcirc-join-channels process rcirc-startup-channels))
+      (rcirc-join-channels process rcirc-startup-channels))))
+
+(defun rcirc-join-channels-post-auth (process)
+  "Join `rcirc-startup-channels' after authenticating."
+  (with-rcirc-process-buffer process
+    (rcirc-join-channels process rcirc-startup-channels)))
+
+(defun rcirc-handler-PRIVMSG (process sender args text)
+  (rcirc-check-auth-status process sender args text)
+  (let ((target (if (rcirc-channel-p (car args))
+                    (car args)
+                  sender))
+        (message (or (cadr args) "")))
+    (if (string-match "^\C-a\\(.*\\)\C-a$" message)
+        (rcirc-handler-CTCP process target sender (match-string 1 message))
+      (rcirc-print process sender "PRIVMSG" target message t))
+    ;; update nick linestamp
+    (with-current-buffer (rcirc-get-buffer process target t)
+      (rcirc-put-nick-channel process sender target rcirc-current-line))))
+
+(defun rcirc-handler-NOTICE (process sender args text)
+  (rcirc-check-auth-status process sender args text)
+  (let ((target (car args))
+        (message (cadr args)))
+    (if (string-match "^\C-a\\(.*\\)\C-a$" message)
+        (rcirc-handler-CTCP-response process target sender
+				     (match-string 1 message))
+      (rcirc-print process sender "NOTICE"
+		   (cond ((rcirc-channel-p target)
+			  target)
+			 ;;; -ChanServ- [#gnu] Welcome...
+			 ((string-match "\\[\\(#[^] ]+\\)\\]" message)
+			  (match-string 1 message))
+			 (sender
+			  (if (string= sender (rcirc-server-name process))
+			      nil	; server notice
+			    sender)))
+                 message t))))
+
+(defun rcirc-check-auth-status (process sender args _text)
+  "Check if the user just authenticated.
+If authenticated, runs `rcirc-authenticated-hook' with PROCESS as
+the only argument."
+  (with-rcirc-process-buffer process
+    (when (and (not rcirc-user-authenticated)
+               rcirc-authenticate-before-join
+               rcirc-auto-authenticate-flag)
+      (let ((target (car args))
+            (message (cadr args)))
+        (when (or
+               (and ;; nickserv
+                (string= sender "NickServ")
+                (string= target rcirc-nick)
+                (member message
+                        (list
+                         (format "You are now identified for \C-b%s\C-b." rcirc-nick)
+			 (format "You are successfully identified as \C-b%s\C-b." rcirc-nick)
+                         "Password accepted - you are now recognized."
+                         )))
+               (and ;; quakenet
+                (string= sender "Q")
+                (string= target rcirc-nick)
+                (string-match "\\`You are now logged in as .+\\.\\'" message)))
+          (setq rcirc-user-authenticated t)
+          (run-hook-with-args 'rcirc-authenticated-hook process)
+          (remove-hook 'rcirc-authenticated-hook 'rcirc-join-channels-post-auth t))))))
+
+(defun rcirc-handler-WALLOPS (process sender args _text)
+  (rcirc-print process sender "WALLOPS" sender (car args) t))
+
+(defun rcirc-handler-JOIN (process sender args _text)
+  (let ((channel (car args)))
+    (with-current-buffer (rcirc-get-buffer-create process channel)
+      ;; when recently rejoining, restore the linestamp
+      (rcirc-put-nick-channel process sender channel
+			      (let ((last-activity-lines
+				     (rcirc-elapsed-lines process sender channel)))
+				(when (and last-activity-lines
+					   (< last-activity-lines rcirc-omit-threshold))
+                                  (rcirc-last-line process sender channel))))
+      ;; reset mode-line-process in case joining a channel with an
+      ;; already open buffer (after getting kicked e.g.)
+      (setq mode-line-process nil))
+
+    (rcirc-print process sender "JOIN" channel "")
+
+    ;; print in private chat buffer if it exists
+    (when (rcirc-get-buffer (rcirc-buffer-process) sender)
+      (rcirc-print process sender "JOIN" sender channel))))
+
+;; PART and KICK are handled the same way
+(defun rcirc-handler-PART-or-KICK (process _response channel _sender nick _args)
+  (rcirc-ignore-update-automatic nick)
+  (if (not (string= nick (rcirc-nick process)))
+      ;; this is someone else leaving
+      (progn
+	(rcirc-maybe-remember-nick-quit process nick channel)
+	(rcirc-remove-nick-channel process nick channel))
+    ;; this is us leaving
+    (mapc (lambda (n)
+	    (rcirc-remove-nick-channel process n channel))
+	  (rcirc-channel-nicks process channel))
+
+    ;; if the buffer is still around, make it inactive
+    (let ((buffer (rcirc-get-buffer process channel)))
+      (when buffer
+	(rcirc-disconnect-buffer buffer)))))
+
+(defun rcirc-handler-PART (process sender args _text)
+  (let* ((channel (car args))
+	 (reason (cadr args))
+	 (message (concat channel " " reason)))
+    (rcirc-print process sender "PART" channel message)
+    ;; print in private chat buffer if it exists
+    (when (rcirc-get-buffer (rcirc-buffer-process) sender)
+      (rcirc-print process sender "PART" sender message))
+
+    (rcirc-handler-PART-or-KICK process "PART" channel sender sender reason)))
+
+(defun rcirc-handler-KICK (process sender args _text)
+  (let* ((channel (car args))
+	 (nick (cadr args))
+	 (reason (nth 2 args))
+	 (message (concat nick " " channel " " reason)))
+    (rcirc-print process sender "KICK" channel message t)
+    ;; print in private chat buffer if it exists
+    (when (rcirc-get-buffer (rcirc-buffer-process) nick)
+      (rcirc-print process sender "KICK" nick message))
+
+    (rcirc-handler-PART-or-KICK process "KICK" channel sender nick reason)))
+
+(defun rcirc-maybe-remember-nick-quit (process nick channel)
+  "Remember NICK as leaving CHANNEL if they recently spoke."
+  (let ((elapsed-lines (rcirc-elapsed-lines process nick channel)))
+    (when (and elapsed-lines
+	       (< elapsed-lines rcirc-omit-threshold))
+      (let ((buffer (rcirc-get-buffer process channel)))
+	(when buffer
+	  (with-current-buffer buffer
+	    (let ((record (assoc-string nick rcirc-recent-quit-alist t))
+		  (line (rcirc-last-line process nick channel)))
+	      (if record
+		  (setcdr record line)
+		(setq rcirc-recent-quit-alist
+		      (cons (cons nick line)
+			    rcirc-recent-quit-alist))))))))))
+
+(defun rcirc-handler-QUIT (process sender args _text)
+  (rcirc-ignore-update-automatic sender)
+  (mapc (lambda (channel)
+	  ;; broadcast quit message each channel
+	  (rcirc-print process sender "QUIT" channel (apply 'concat args))
+	  ;; record nick in quit table if they recently spoke
+	  (rcirc-maybe-remember-nick-quit process sender channel))
+	(rcirc-nick-channels process sender))
+  (rcirc-nick-remove process sender))
+
+(defun rcirc-handler-NICK (process sender args _text)
+  (let* ((old-nick sender)
+         (new-nick (car args))
+         (channels (rcirc-nick-channels process old-nick)))
+    ;; update list of ignored nicks
+    (rcirc-ignore-update-automatic old-nick)
+    (when (member old-nick rcirc-ignore-list)
+      (add-to-list 'rcirc-ignore-list new-nick)
+      (add-to-list 'rcirc-ignore-list-automatic new-nick))
+    ;; print message to nick's channels
+    (dolist (target channels)
+      (rcirc-print process sender "NICK" target new-nick))
+    ;; update private chat buffer, if it exists
+    (let ((chat-buffer (rcirc-get-buffer process old-nick)))
+      (when chat-buffer
+	(with-current-buffer chat-buffer
+	  (rcirc-print process sender "NICK" old-nick new-nick)
+	  (setq rcirc-target new-nick)
+	  (rename-buffer (rcirc-generate-new-buffer-name process new-nick)))))
+    ;; remove old nick and add new one
+    (with-rcirc-process-buffer process
+      (let ((v (gethash old-nick rcirc-nick-table)))
+        (remhash old-nick rcirc-nick-table)
+        (puthash new-nick v rcirc-nick-table))
+      ;; if this is our nick...
+      (when (string= old-nick rcirc-nick)
+        (setq rcirc-nick new-nick)
+	(rcirc-update-prompt t)
+        ;; reauthenticate
+        (when rcirc-auto-authenticate-flag (rcirc-authenticate))))))
+
+(defun rcirc-handler-PING (process _sender args _text)
+  (rcirc-send-string process (concat "PONG :" (car args))))
+
+(defun rcirc-handler-PONG (_process _sender _args _text)
+  ;; do nothing
+  )
+
+(defun rcirc-handler-TOPIC (process sender args _text)
+  (let ((topic (cadr args)))
+    (rcirc-print process sender "TOPIC" (car args) topic)
+    (with-current-buffer (rcirc-get-buffer process (car args))
+      (setq rcirc-topic topic))))
+
+(defvar rcirc-nick-away-alist nil)
+(defun rcirc-handler-301 (process _sender args text)
+  "RPL_AWAY"
+  (let* ((nick (cadr args))
+	 (rec (assoc-string nick rcirc-nick-away-alist))
+	 (away-message (nth 2 args)))
+    (when (or (not rec)
+	      (not (string= (cdr rec) away-message)))
+      ;; away message has changed
+      (rcirc-handler-generic process "AWAY" nick (cdr args) text)
+      (if rec
+	  (setcdr rec away-message)
+	(setq rcirc-nick-away-alist (cons (cons nick away-message)
+					  rcirc-nick-away-alist))))))
+
+(defun rcirc-handler-317 (process sender args _text)
+  "RPL_WHOISIDLE"
+  (let* ((nick (nth 1 args))
+         (idle-secs (string-to-number (nth 2 args)))
+         (idle-string
+          (if (< idle-secs most-positive-fixnum)
+              (format-seconds "%yy %dd %hh %mm %z%ss" idle-secs)
+            "a very long time"))
+         (signon-time (seconds-to-time (string-to-number (nth 3 args))))
+         (signon-string (format-time-string "%c" signon-time))
+         (message (format "%s idle for %s, signed on %s"
+                          nick idle-string signon-string)))
+    (rcirc-print process sender "317" nil message t)))
+
+(defun rcirc-handler-332 (process _sender args _text)
+  "RPL_TOPIC"
+  (let ((buffer (or (rcirc-get-buffer process (cadr args))
+		    (rcirc-get-temp-buffer-create process (cadr args)))))
+    (with-current-buffer buffer
+      (setq rcirc-topic (nth 2 args)))))
+
+(defun rcirc-handler-333 (process sender args _text)
+  "333 says who set the topic and when.
+Not in rfc1459.txt"
+  (let ((buffer (or (rcirc-get-buffer process (cadr args))
+		    (rcirc-get-temp-buffer-create process (cadr args)))))
+    (with-current-buffer buffer
+      (let ((setter (nth 2 args))
+	    (time (current-time-string
+		   (seconds-to-time
+		    (string-to-number (cl-cadddr args))))))
+	(rcirc-print process sender "TOPIC" (cadr args)
+		     (format "%s (%s on %s)" rcirc-topic setter time))))))
+
+(defun rcirc-handler-477 (process sender args _text)
+  "ERR_NOCHANMODES"
+  (rcirc-print process sender "477" (cadr args) (nth 2 args)))
+
+(defun rcirc-handler-MODE (process sender args _text)
+  (let ((target (car args))
+        (msg (mapconcat 'identity (cdr args) " ")))
+    (rcirc-print process sender "MODE"
+                 (if (string= target (rcirc-nick process))
+                     nil
+                   target)
+                 msg)
+
+    ;; print in private chat buffers if they exist
+    (mapc (lambda (nick)
+	    (when (rcirc-get-buffer process nick)
+	      (rcirc-print process sender "MODE" nick msg)))
+	  (cddr args))))
+
+(defun rcirc-get-temp-buffer-create (process channel)
+  "Return a buffer based on PROCESS and CHANNEL."
+  (let ((tmpnam (concat " " (downcase channel) "TMP" (process-name process))))
+    (get-buffer-create tmpnam)))
+
+(defun rcirc-handler-353 (process _sender args _text)
+  "RPL_NAMREPLY"
+  (let ((channel (nth 2 args))
+	(names (or (nth 3 args) "")))
+    (mapc (lambda (nick)
+            (rcirc-put-nick-channel process nick channel))
+          (split-string names " " t))
+    ;; create a temporary buffer to insert the names into
+    ;; rcirc-handler-366 (RPL_ENDOFNAMES) will handle it
+    (with-current-buffer (rcirc-get-temp-buffer-create process channel)
+      (goto-char (point-max))
+      (insert (car (last args)) " "))))
+
+(defun rcirc-handler-366 (process sender args _text)
+  "RPL_ENDOFNAMES"
+  (let* ((channel (cadr args))
+         (buffer (rcirc-get-temp-buffer-create process channel)))
+    (with-current-buffer buffer
+      (rcirc-print process sender "NAMES" channel
+                   (let ((content (buffer-substring (point-min) (point-max))))
+		     (rcirc-sort-nicknames-join content " "))))
+    (kill-buffer buffer)))
+
+(defun rcirc-handler-433 (process sender args text)
+  "ERR_NICKNAMEINUSE"
+  (rcirc-handler-generic process "433" sender args text)
+  (let* ((new-nick (concat (cadr args) "`")))
+    (with-rcirc-process-buffer process
+      (rcirc-cmd-nick new-nick nil process))))
+
+(defun rcirc-authenticate ()
+  "Send authentication to process associated with current buffer.
+Passwords are stored in `rcirc-authinfo' (which see)."
+  (interactive)
+  (with-rcirc-server-buffer
+    (dolist (i rcirc-authinfo)
+      (let ((process (rcirc-buffer-process))
+	    (server (car i))
+	    (nick (nth 2 i))
+	    (method (cadr i))
+	    (args (cl-cdddr i)))
+	(when (and (string-match server rcirc-server))
+          (if (and (memq method '(nickserv chanserv bitlbee))
+                   (string-match nick rcirc-nick))
+              ;; the following methods rely on the user's nickname.
+              (cl-case method
+                (nickserv
+                 (rcirc-send-privmsg
+                  process
+                  (or (cadr args) "NickServ")
+                  (concat "IDENTIFY " (car args))))
+                (chanserv
+                 (rcirc-send-privmsg
+                  process
+                  "ChanServ"
+                  (format "IDENTIFY %s %s" (car args) (cadr args))))
+                (bitlbee
+                 (rcirc-send-privmsg
+                  process
+                  "&bitlbee"
+                  (concat "IDENTIFY " (car args)))))
+            ;; quakenet authentication doesn't rely on the user's nickname.
+            ;; the variable `nick' here represents the Q account name.
+            (when (eq method 'quakenet)
+              (rcirc-send-privmsg
+               process
+               "Q@CServe.quakenet.org"
+               (format "AUTH %s %s" nick (car args))))))))))
+
+(defun rcirc-handler-INVITE (process sender args _text)
+  (rcirc-print process sender "INVITE" nil (mapconcat 'identity args " ") t))
+
+(defun rcirc-handler-ERROR (process sender args _text)
+  (rcirc-print process sender "ERROR" nil (mapconcat 'identity args " ")))
+
+(defun rcirc-handler-CTCP (process target sender text)
+  (if (string-match "^\\([^ ]+\\) *\\(.*\\)$" text)
+      (let* ((request (upcase (match-string 1 text)))
+             (args (match-string 2 text))
+             (handler (intern-soft (concat "rcirc-handler-ctcp-" request))))
+        (if (not (fboundp handler))
+            (rcirc-print process sender "ERROR" target
+                         (format "%s sent unsupported ctcp: %s" sender text)
+			 t)
+          (funcall handler process target sender args)
+          (unless (or (string= request "ACTION")
+		      (string= request "KEEPALIVE"))
+              (rcirc-print process sender "CTCP" target
+			   (format "%s" text) t))))))
+
+(defun rcirc-handler-ctcp-VERSION (process _target sender _args)
+  (rcirc-send-string process
+                     (concat "NOTICE " sender
+                             " :\C-aVERSION " rcirc-id-string
+                             "\C-a")))
+
+(defun rcirc-handler-ctcp-ACTION (process target sender args)
+  (rcirc-print process sender "ACTION" target args t))
+
+(defun rcirc-handler-ctcp-TIME (process _target sender _args)
+  (rcirc-send-string process
+                     (concat "NOTICE " sender
+                             " :\C-aTIME " (current-time-string) "\C-a")))
+
+(defun rcirc-handler-CTCP-response (process _target sender message)
+  (rcirc-print process sender "CTCP" nil message t))
+
+(defun rcirc-handler-CAP (process _sender args _text)
+  (when (equal (cadr args) "LS")
+    (rcirc-send-string process "CAP REQ :server-time"))
+
+  (when (or (equal (cadr args) "ACK")
+            (equal (cadr args) "NAK"))
+    ;; Capability negotiation is best-effort here, I know that my
+    ;; servers support server-time and thus we end negotiation
+    ;; immediately.
+    (rcirc-send-string process "CAP END")))
+
+(defgroup rcirc-faces nil
+  "Faces for rcirc."
+  :group 'rcirc
+  :group 'faces)
+
+(defface rcirc-my-nick			; font-lock-function-name-face
+  '((((class color) (min-colors 88) (background light)) :foreground "Blue1")
+    (((class color) (min-colors 88) (background dark))  :foreground "LightSkyBlue")
+    (((class color) (min-colors 16) (background light)) :foreground "Blue")
+    (((class color) (min-colors 16) (background dark))  :foreground "LightSkyBlue")
+    (((class color) (min-colors 8)) :foreground "blue" :weight bold)
+    (t :inverse-video t :weight bold))
+  "Rcirc face for my messages."
+  :group 'rcirc-faces)
+
+(defface rcirc-other-nick	     ; font-lock-variable-name-face
+  '((((class grayscale) (background light))
+     :foreground "Gray90" :weight bold :slant italic)
+    (((class grayscale) (background dark))
+     :foreground "DimGray" :weight bold :slant italic)
+    (((class color) (min-colors 88) (background light)) :foreground "DarkGoldenrod")
+    (((class color) (min-colors 88) (background dark))  :foreground "LightGoldenrod")
+    (((class color) (min-colors 16) (background light)) :foreground "DarkGoldenrod")
+    (((class color) (min-colors 16) (background dark))  :foreground "LightGoldenrod")
+    (((class color) (min-colors 8)) :foreground "yellow" :weight light)
+    (t :weight bold :slant italic))
+  "Rcirc face for other users' messages."
+  :group 'rcirc-faces)
+
+(defface rcirc-bright-nick
+  '((((class grayscale) (background light))
+     :foreground "LightGray" :weight bold :underline t)
+    (((class grayscale) (background dark))
+     :foreground "Gray50" :weight bold :underline t)
+    (((class color) (min-colors 88) (background light)) :foreground "CadetBlue")
+    (((class color) (min-colors 88) (background dark))  :foreground "Aquamarine")
+    (((class color) (min-colors 16) (background light)) :foreground "CadetBlue")
+    (((class color) (min-colors 16) (background dark))  :foreground "Aquamarine")
+    (((class color) (min-colors 8)) :foreground "magenta")
+    (t :weight bold :underline t))
+  "Rcirc face for nicks matched by `rcirc-bright-nicks'."
+  :group 'rcirc-faces)
+
+(defface rcirc-dim-nick
+  '((t :inherit default))
+  "Rcirc face for nicks in `rcirc-dim-nicks'."
+  :group 'rcirc-faces)
+
+(defface rcirc-server			; font-lock-comment-face
+  '((((class grayscale) (background light))
+     :foreground "DimGray" :weight bold :slant italic)
+    (((class grayscale) (background dark))
+     :foreground "LightGray" :weight bold :slant italic)
+    (((class color) (min-colors 88) (background light))
+     :foreground "Firebrick")
+    (((class color) (min-colors 88) (background dark))
+     :foreground "chocolate1")
+    (((class color) (min-colors 16) (background light))
+     :foreground "red")
+    (((class color) (min-colors 16) (background dark))
+     :foreground "red1")
+    (((class color) (min-colors 8) (background light)))
+    (((class color) (min-colors 8) (background dark)))
+    (t :weight bold :slant italic))
+  "Rcirc face for server messages."
+  :group 'rcirc-faces)
+
+(defface rcirc-server-prefix	 ; font-lock-comment-delimiter-face
+  '((default :inherit rcirc-server)
+    (((class grayscale)))
+    (((class color) (min-colors 16)))
+    (((class color) (min-colors 8) (background light))
+     :foreground "red")
+    (((class color) (min-colors 8) (background dark))
+     :foreground "red1"))
+  "Rcirc face for server prefixes."
+  :group 'rcirc-faces)
+
+(defface rcirc-timestamp
+  '((t :inherit default))
+  "Rcirc face for timestamps."
+  :group 'rcirc-faces)
+
+(defface rcirc-nick-in-message		; font-lock-keyword-face
+  '((((class grayscale) (background light)) :foreground "LightGray" :weight bold)
+    (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
+    (((class color) (min-colors 88) (background light)) :foreground "Purple")
+    (((class color) (min-colors 88) (background dark))  :foreground "Cyan1")
+    (((class color) (min-colors 16) (background light)) :foreground "Purple")
+    (((class color) (min-colors 16) (background dark))  :foreground "Cyan")
+    (((class color) (min-colors 8)) :foreground "cyan" :weight bold)
+    (t :weight bold))
+  "Rcirc face for instances of your nick within messages."
+  :group 'rcirc-faces)
+
+(defface rcirc-nick-in-message-full-line '((t :weight bold))
+  "Rcirc face for emphasizing the entire message when your nick is mentioned."
+  :group 'rcirc-faces)
+
+(defface rcirc-prompt			; comint-highlight-prompt
+  '((((min-colors 88) (background dark)) :foreground "cyan1")
+    (((background dark)) :foreground "cyan")
+    (t :foreground "dark blue"))
+  "Rcirc face for prompts."
+  :group 'rcirc-faces)
+
+(defface rcirc-track-nick
+  '((((type tty)) :inherit default)
+    (t :inverse-video t))
+  "Rcirc face used in the mode-line when your nick is mentioned."
+  :group 'rcirc-faces)
+
+(defface rcirc-track-keyword '((t :weight bold))
+  "Rcirc face used in the mode-line when keywords are mentioned."
+  :group 'rcirc-faces)
+
+(defface rcirc-url '((t :weight bold))
+  "Rcirc face used to highlight urls."
+  :group 'rcirc-faces)
+
+(defface rcirc-keyword '((t :inherit highlight))
+  "Rcirc face used to highlight keywords."
+  :group 'rcirc-faces)
+
+
+;; When using M-x flyspell-mode, only check words after the prompt
+(put 'rcirc-mode 'flyspell-mode-predicate 'rcirc-looking-at-input)
+(defun rcirc-looking-at-input ()
+  "Returns true if point is past the input marker."
+  (>= (point) rcirc-prompt-end-marker))
+
+
+(provide 'rcirc)
+
+;;; rcirc.el ends here
diff --git a/third_party/exwm/.elpaignore b/third_party/exwm/.elpaignore
new file mode 100644
index 0000000000..b43bf86b50
--- /dev/null
+++ b/third_party/exwm/.elpaignore
@@ -0,0 +1 @@
+README.md
diff --git a/third_party/exwm/.gitignore b/third_party/exwm/.gitignore
new file mode 100644
index 0000000000..9e4b0ee5b4
--- /dev/null
+++ b/third_party/exwm/.gitignore
@@ -0,0 +1,3 @@
+*.elc
+*-pkg.el
+*-autoloads.el
diff --git a/third_party/exwm/LICENSE b/third_party/exwm/LICENSE
new file mode 100644
index 0000000000..9cecc1d466
--- /dev/null
+++ b/third_party/exwm/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    {one line to give the program's name and a brief idea of what it does.}
+    Copyright (C) {year}  {name of author}
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    {project}  Copyright (C) {year}  {fullname}
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/third_party/exwm/README.md b/third_party/exwm/README.md
new file mode 100644
index 0000000000..6d7e0dd1ff
--- /dev/null
+++ b/third_party/exwm/README.md
@@ -0,0 +1,21 @@
+# Emacs X Window Manager
+
+EXWM (Emacs X Window Manager) is a full-featured tiling X window manager
+for Emacs built on top of [XELB](https://github.com/ch11ng/xelb).
+It features:
++ Fully keyboard-driven operations
++ Hybrid layout modes (tiling & stacking)
++ Dynamic workspace support
++ ICCCM/EWMH compliance
++ (Optional) RandR (multi-monitor) support
++ (Optional) Builtin system tray
++ (Optional) Builtin input method
+
+Please check out the
+[screenshots](https://github.com/ch11ng/exwm/wiki/Screenshots)
+to get an overview of what EXWM is capable of,
+and the [user guide](https://github.com/ch11ng/exwm/wiki)
+for a detailed explanation of its usage.
+
+**Note**: If you install EXWM from source, it's recommended to install
+XELB also from source (otherwise install both from GNU ELPA).
diff --git a/third_party/exwm/exwm-cm.el b/third_party/exwm/exwm-cm.el
new file mode 100644
index 0000000000..8a45010030
--- /dev/null
+++ b/third_party/exwm/exwm-cm.el
@@ -0,0 +1,50 @@
+;;; exwm-cm.el --- Compositing Manager for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2016-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module is obsolete since EXWM now supports third-party compositors.
+
+;;; Code:
+
+(make-obsolete-variable 'exwm-cm-opacity
+                        "This variable should no longer be used." "26")
+
+(defun exwm-cm-set-opacity (&rest _args)
+  (declare (obsolete nil "26")))
+
+(defun exwm-cm-enable ()
+  (declare (obsolete nil "26")))
+
+(defun exwm-cm-start ()
+  (declare (obsolete nil "26")))
+
+(defun exwm-cm-stop ()
+  (declare (obsolete nil "26")))
+
+(defun exwm-cm-toggle ()
+  (declare (obsolete nil "26")))
+
+
+
+(provide 'exwm-cm)
+
+;;; exwm-cm.el ends here
diff --git a/third_party/exwm/exwm-config.el b/third_party/exwm/exwm-config.el
new file mode 100644
index 0000000000..9609f4cf3c
--- /dev/null
+++ b/third_party/exwm/exwm-config.el
@@ -0,0 +1,131 @@
+;;; exwm-config.el --- Predefined configurations  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module contains typical (yet minimal) configurations of EXWM.
+
+;;; Code:
+
+(require 'exwm)
+(require 'ido)
+
+(define-obsolete-function-alias 'exwm-config-default
+  #'exwm-config-example "27.1")
+
+(defun exwm-config-example ()
+  "Default configuration of EXWM."
+  ;; Set the initial workspace number.
+  (unless (get 'exwm-workspace-number 'saved-value)
+    (setq exwm-workspace-number 4))
+  ;; Make class name the buffer name
+  (add-hook 'exwm-update-class-hook
+            (lambda ()
+              (exwm-workspace-rename-buffer exwm-class-name)))
+  ;; Global keybindings.
+  (unless (get 'exwm-input-global-keys 'saved-value)
+    (setq exwm-input-global-keys
+          `(
+            ;; 's-r': Reset (to line-mode).
+            ([?\s-r] . exwm-reset)
+            ;; 's-w': Switch workspace.
+            ([?\s-w] . exwm-workspace-switch)
+            ;; 's-&': Launch application.
+            ([?\s-&] . (lambda (command)
+                         (interactive (list (read-shell-command "$ ")))
+                         (start-process-shell-command command nil command)))
+            ;; 's-N': Switch to certain workspace.
+            ,@(mapcar (lambda (i)
+                        `(,(kbd (format "s-%d" i)) .
+                          (lambda ()
+                            (interactive)
+                            (exwm-workspace-switch-create ,i))))
+                      (number-sequence 0 9)))))
+  ;; Line-editing shortcuts
+  (unless (get 'exwm-input-simulation-keys 'saved-value)
+    (setq exwm-input-simulation-keys
+          '(([?\C-b] . [left])
+            ([?\C-f] . [right])
+            ([?\C-p] . [up])
+            ([?\C-n] . [down])
+            ([?\C-a] . [home])
+            ([?\C-e] . [end])
+            ([?\M-v] . [prior])
+            ([?\C-v] . [next])
+            ([?\C-d] . [delete])
+            ([?\C-k] . [S-end delete]))))
+  ;; Enable EXWM
+  (exwm-enable)
+  ;; Configure Ido
+  (exwm-config-ido)
+  ;; Other configurations
+  (exwm-config-misc))
+
+(defun exwm-config--fix/ido-buffer-window-other-frame ()
+  "Fix `ido-buffer-window-other-frame'."
+  (defalias 'exwm-config-ido-buffer-window-other-frame
+    (symbol-function #'ido-buffer-window-other-frame))
+  (defun ido-buffer-window-other-frame (buffer)
+    "This is a version redefined by EXWM.
+
+You can find the original one at `exwm-config-ido-buffer-window-other-frame'."
+    (with-current-buffer (window-buffer (selected-window))
+      (if (and (derived-mode-p 'exwm-mode)
+               exwm--floating-frame)
+          ;; Switch from a floating frame.
+          (with-current-buffer buffer
+            (if (and (derived-mode-p 'exwm-mode)
+                     exwm--floating-frame
+                     (eq exwm--frame exwm-workspace--current))
+                ;; Switch to another floating frame.
+                (frame-root-window exwm--floating-frame)
+              ;; Do not switch if the buffer is not on the current workspace.
+              (or (get-buffer-window buffer exwm-workspace--current)
+                  (selected-window))))
+        (with-current-buffer buffer
+          (when (derived-mode-p 'exwm-mode)
+            (if (eq exwm--frame exwm-workspace--current)
+                (when exwm--floating-frame
+                  ;; Switch to a floating frame on the current workspace.
+                  (frame-selected-window exwm--floating-frame))
+              ;; Do not switch to exwm-mode buffers on other workspace (which
+              ;; won't work unless `exwm-layout-show-all-buffers' is set)
+              (unless exwm-layout-show-all-buffers
+                (selected-window)))))))))
+
+(defun exwm-config-ido ()
+  "Configure Ido to work with EXWM."
+  (ido-mode 1)
+  (add-hook 'exwm-init-hook #'exwm-config--fix/ido-buffer-window-other-frame))
+
+(defun exwm-config-misc ()
+  "Other configurations."
+  ;; Make more room
+  (menu-bar-mode -1)
+  (tool-bar-mode -1)
+  (scroll-bar-mode -1)
+  (fringe-mode 1))
+
+
+
+(provide 'exwm-config)
+
+;;; exwm-config.el ends here
diff --git a/third_party/exwm/exwm-core.el b/third_party/exwm/exwm-core.el
new file mode 100644
index 0000000000..995b590dc5
--- /dev/null
+++ b/third_party/exwm/exwm-core.el
@@ -0,0 +1,390 @@
+;;; exwm-core.el --- Core definitions  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module includes core definitions of variables, macros, functions, etc
+;; shared by various other modules.
+
+;;; Code:
+
+(require 'kmacro)
+
+(require 'xcb)
+(require 'xcb-icccm)
+(require 'xcb-ewmh)
+(require 'xcb-debug)
+
+(defcustom exwm-debug-log-time-function #'exwm-debug-log-uptime
+  "Function used for generating timestamps in `exwm-debug' logs.
+
+Here are some predefined candidates:
+`exwm-debug-log-uptime': Display the uptime of this Emacs instance.
+`exwm-debug-log-time': Display time of day.
+`nil': Disable timestamp."
+  :group 'exwm-debug
+  :type `(choice (const :tag "Emacs uptime" ,#'exwm-debug-log-uptime)
+                 (const :tag "Time of day" ,#'exwm-debug-log-time)
+                 (const :tag "Off" nil)
+                 (function :tag "Other"))
+  :set (lambda (symbol value)
+         (set-default symbol value)
+         ;; Also change the format for XELB to make logs consistent
+         ;; (as they share the same buffer).
+         (setq xcb-debug:log-time-function value)))
+
+(defalias 'exwm-debug-log-uptime 'xcb-debug:log-uptime
+  "Add uptime to `exwm-debug' logs.")
+
+(defalias 'exwm-debug-log-time 'xcb-debug:log-time
+  "Add time of day to `exwm-debug' logs.")
+
+(defvar exwm--connection nil "X connection.")
+
+(defvar exwm--wmsn-window nil
+  "An X window owning the WM_S0 selection.")
+
+(defvar exwm--wmsn-acquire-timeout 3
+  "Number of seconds to wait for other window managers to release the selection.")
+
+(defvar exwm--guide-window nil
+  "An X window separating workspaces and X windows.")
+
+(defvar exwm--id-buffer-alist nil "Alist of (<X window ID> . <Emacs buffer>).")
+
+(defvar exwm--root nil "Root window.")
+
+(defvar exwm-input--global-prefix-keys)
+(defvar exwm-input--simulation-keys)
+(defvar exwm-input-line-mode-passthrough)
+(defvar exwm-input-prefix-keys)
+(declare-function exwm-input--fake-key "exwm-input.el" (event))
+(declare-function exwm-input--on-KeyPress-line-mode "exwm-input.el"
+                  (key-press raw-data))
+(declare-function exwm-floating-hide "exwm-floating.el")
+(declare-function exwm-floating-toggle-floating "exwm-floating.el")
+(declare-function exwm-input-release-keyboard "exwm-input.el")
+(declare-function exwm-input-send-next-key "exwm-input.el" (times))
+(declare-function exwm-layout-set-fullscreen "exwm-layout.el" (&optional id))
+(declare-function exwm-layout-toggle-mode-line "exwm-layout.el")
+(declare-function exwm-manage--kill-buffer-query-function "exwm-manage.el")
+(declare-function exwm-workspace-move-window "exwm-workspace.el"
+                  (frame-or-index &optional id))
+
+(define-minor-mode exwm-debug
+  "Debug-logging enabled if non-nil"
+  :global t)
+
+(defmacro exwm--debug (&rest forms)
+  (when exwm-debug `(progn ,@forms)))
+
+(defmacro exwm--log (&optional format-string &rest objects)
+  "Emit a message prepending the name of the function being executed.
+
+FORMAT-STRING is a string specifying the message to output, as in
+`format'.  The OBJECTS arguments specify the substitutions."
+  (unless format-string (setq format-string ""))
+  `(when exwm-debug
+     (xcb-debug:message ,(concat "%s%s:\t" format-string "\n")
+                        (if exwm-debug-log-time-function
+                            (funcall exwm-debug-log-time-function)
+                          "")
+                        (xcb-debug:compile-time-function-name)
+                        ,@objects)
+     nil))
+
+(defsubst exwm--id->buffer (id)
+  "X window ID => Emacs buffer."
+  (cdr (assoc id exwm--id-buffer-alist)))
+
+(defsubst exwm--buffer->id (buffer)
+  "Emacs buffer BUFFER => X window ID."
+  (car (rassoc buffer exwm--id-buffer-alist)))
+
+(defun exwm--lock (&rest _args)
+  "Lock (disable all events)."
+  (exwm--log)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ChangeWindowAttributes
+                     :window exwm--root
+                     :value-mask xcb:CW:EventMask
+                     :event-mask xcb:EventMask:NoEvent))
+  (xcb:flush exwm--connection))
+
+(defun exwm--unlock (&rest _args)
+  "Unlock (enable all events)."
+  (exwm--log)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ChangeWindowAttributes
+                     :window exwm--root
+                     :value-mask xcb:CW:EventMask
+                     :event-mask (eval-when-compile
+                                   (logior xcb:EventMask:SubstructureRedirect
+                                           xcb:EventMask:StructureNotify))))
+  (xcb:flush exwm--connection))
+
+(defun exwm--set-geometry (xwin x y width height)
+  "Set the geometry of X window XWIN to WIDTHxHEIGHT+X+Y.
+
+Nil can be passed as placeholder."
+  (exwm--log "Setting #x%x to %sx%s+%s+%s" xwin width height x y)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ConfigureWindow
+                     :window xwin
+                     :value-mask (logior (if x xcb:ConfigWindow:X 0)
+                                         (if y xcb:ConfigWindow:Y 0)
+                                         (if width xcb:ConfigWindow:Width 0)
+                                         (if height xcb:ConfigWindow:Height 0))
+                     :x x :y y :width width :height height)))
+
+(defun exwm--intern-atom (atom)
+  "Intern X11 ATOM."
+  (slot-value (xcb:+request-unchecked+reply exwm--connection
+                  (make-instance 'xcb:InternAtom
+                                 :only-if-exists 0
+                                 :name-len (length atom)
+                                 :name atom))
+              'atom))
+
+(defmacro exwm--defer (secs function &rest args)
+  "Defer the execution of FUNCTION.
+
+The action is to call FUNCTION with arguments ARGS.  If Emacs is not idle,
+defer the action until Emacs is idle.  Otherwise, defer the action until at
+least SECS seconds later."
+  `(run-with-idle-timer (+ (float-time (or (current-idle-time)
+                                           (seconds-to-time (- ,secs))))
+                           ,secs)
+                        nil
+                        ,function
+                        ,@args))
+
+(defun exwm--get-client-event-mask ()
+  "Return event mask set on all managed windows."
+  (logior xcb:EventMask:StructureNotify
+          xcb:EventMask:PropertyChange
+          (if mouse-autoselect-window
+              xcb:EventMask:EnterWindow 0)))
+
+(defun exwm--color->pixel (color)
+  "Convert COLOR to PIXEL (index in TrueColor colormap)."
+  (when (and color
+             (eq (x-display-visual-class) 'true-color))
+    (let ((rgb (x-color-values color)))
+      (logior (lsh (lsh (pop rgb) -8) 16)
+              (lsh (lsh (pop rgb) -8) 8)
+              (lsh (pop rgb) -8)))))
+
+(defun exwm--get-visual-depth-colormap (conn id)
+  "Get visual, depth and colormap from X window ID.
+Return a three element list with the respective results."
+  (let (ret-visual ret-depth ret-colormap)
+    (with-slots (visual colormap)
+        (xcb:+request-unchecked+reply conn
+            (make-instance 'xcb:GetWindowAttributes :window id))
+      (setq ret-visual visual)
+      (setq ret-colormap colormap))
+    (with-slots (depth)
+        (xcb:+request-unchecked+reply conn
+            (make-instance 'xcb:GetGeometry :drawable id))
+      (setq ret-depth depth))
+    (list ret-visual ret-depth ret-colormap)))
+
+;; Internal variables
+(defvar-local exwm--id nil)               ;window ID
+(defvar-local exwm--configurations nil)   ;initial configurations.
+(defvar-local exwm--frame nil)            ;workspace frame
+(defvar-local exwm--floating-frame nil)   ;floating frame
+(defvar-local exwm--mode-line-format nil) ;save mode-line-format
+(defvar-local exwm--floating-frame-position nil) ;set when hidden.
+(defvar-local exwm--fixed-size nil)              ;fixed size
+(defvar-local exwm--selected-input-mode 'line-mode
+  "Input mode as selected by the user.
+One of `line-mode' or `char-mode'.")
+(defvar-local exwm--input-mode 'line-mode
+  "Actual input mode, i.e. whether mouse and keyboard are grabbed.")
+;; Properties
+(defvar-local exwm--desktop nil "_NET_WM_DESKTOP.")
+(defvar-local exwm-window-type nil "_NET_WM_WINDOW_TYPE.")
+(defvar-local exwm--geometry nil)
+(defvar-local exwm-class-name nil "Class name in WM_CLASS.")
+(defvar-local exwm-instance-name nil "Instance name in WM_CLASS.")
+(defvar-local exwm-title nil "Window title (either _NET_WM_NAME or WM_NAME)")
+(defvar-local exwm--title-is-utf8 nil)
+(defvar-local exwm-transient-for nil "WM_TRANSIENT_FOR.")
+(defvar-local exwm--protocols nil)
+(defvar-local exwm-state xcb:icccm:WM_STATE:NormalState "WM_STATE.")
+(defvar-local exwm--ewmh-state nil "_NET_WM_STATE.")
+;; _NET_WM_NORMAL_HINTS
+(defvar-local exwm--normal-hints-x nil)
+(defvar-local exwm--normal-hints-y nil)
+(defvar-local exwm--normal-hints-width nil)
+(defvar-local exwm--normal-hints-height nil)
+(defvar-local exwm--normal-hints-min-width nil)
+(defvar-local exwm--normal-hints-min-height nil)
+(defvar-local exwm--normal-hints-max-width nil)
+(defvar-local exwm--normal-hints-max-height nil)
+;; (defvar-local exwm--normal-hints-win-gravity nil)
+;; WM_HINTS
+(defvar-local exwm--hints-input nil)
+(defvar-local exwm--hints-urgency nil)
+;; _MOTIF_WM_HINTS
+(defvar-local exwm--mwm-hints-decorations t)
+
+(defvar exwm-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "\C-c\C-d\C-l" #'xcb-debug:clear)
+    (define-key map "\C-c\C-d\C-m" #'xcb-debug:mark)
+    (define-key map "\C-c\C-d\C-t" #'exwm-debug)
+    (define-key map "\C-c\C-f" #'exwm-layout-set-fullscreen)
+    (define-key map "\C-c\C-h" #'exwm-floating-hide)
+    (define-key map "\C-c\C-k" #'exwm-input-release-keyboard)
+    (define-key map "\C-c\C-m" #'exwm-workspace-move-window)
+    (define-key map "\C-c\C-q" #'exwm-input-send-next-key)
+    (define-key map "\C-c\C-t\C-f" #'exwm-floating-toggle-floating)
+    (define-key map "\C-c\C-t\C-m" #'exwm-layout-toggle-mode-line)
+    map)
+  "Keymap for `exwm-mode'.")
+
+(defvar exwm--kmacro-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map [t]
+      (lambda ()
+        (interactive)
+        (cond
+         ((or exwm-input-line-mode-passthrough
+              ;; Do not test `exwm-input--during-command'.
+              (active-minibuffer-window)
+              (memq last-input-event exwm-input--global-prefix-keys)
+              (memq last-input-event exwm-input-prefix-keys)
+              (lookup-key exwm-mode-map (vector last-input-event))
+              (gethash last-input-event exwm-input--simulation-keys))
+          (set-transient-map (make-composed-keymap (list exwm-mode-map
+                                                         global-map)))
+          (push last-input-event unread-command-events))
+         (t
+          (exwm-input--fake-key last-input-event)))))
+    map)
+  "Keymap used when executing keyboard macros.")
+
+;; This menu mainly acts as an reminder for users.  Thus it should be as
+;; detailed as possible, even some entries do not make much sense here.
+;; Also, inactive entries should be disabled rather than hidden.
+(easy-menu-define exwm-mode-menu exwm-mode-map
+  "Menu for `exwm-mode'."
+  '("EXWM"
+    "---"
+    "*General*"
+    "---"
+    ["Toggle floating" exwm-floating-toggle-floating]
+    ["Toggle fullscreen mode" exwm-layout-toggle-fullscreen]
+    ["Hide window" exwm-floating-hide exwm--floating-frame]
+    ["Close window" (kill-buffer (current-buffer))]
+
+    "---"
+    "*Resizing*"
+    "---"
+    ["Toggle mode-line" exwm-layout-toggle-mode-line]
+    ["Enlarge window vertically" exwm-layout-enlarge-window]
+    ["Enlarge window horizontally" exwm-layout-enlarge-window-horizontally]
+    ["Shrink window vertically" exwm-layout-shrink-window]
+    ["Shrink window horizontally" exwm-layout-shrink-window-horizontally]
+
+    "---"
+    "*Keyboard*"
+    "---"
+    ["Toggle keyboard mode" exwm-input-toggle-keyboard]
+    ["Send key" exwm-input-send-next-key (eq exwm--input-mode 'line-mode)]
+    ;; This is merely a reference.
+    ("Send simulation key" :filter
+     (lambda (&rest _args)
+       (let (result)
+         (maphash
+          (lambda (key value)
+            (when (sequencep key)
+              (setq result (append result
+                                   `([
+                                      ,(format "Send '%s'"
+                                               (key-description value))
+                                      (lambda ()
+                                        (interactive)
+                                        (dolist (i ',value)
+                                          (exwm-input--fake-key i)))
+                                      :keys ,(key-description key)])))))
+          exwm-input--simulation-keys)
+         result)))
+
+    ["Define global binding" exwm-input-set-key]
+
+    "---"
+    "*Workspace*"
+    "---"
+    ["Add workspace" exwm-workspace-add]
+    ["Delete current workspace" exwm-workspace-delete]
+    ["Move workspace to" exwm-workspace-move]
+    ["Swap workspaces" exwm-workspace-swap]
+    ["Move X window to" exwm-workspace-move-window]
+    ["Move X window from" exwm-workspace-switch-to-buffer]
+    ["Toggle minibuffer" exwm-workspace-toggle-minibuffer]
+    ["Switch workspace" exwm-workspace-switch]
+    ;; Place this entry at bottom to avoid selecting others by accident.
+    ("Switch to" :filter
+     (lambda (&rest _args)
+       (mapcar (lambda (i)
+                 `[,(format "Workspace %d" i)
+                   (lambda ()
+                     (interactive)
+                     (exwm-workspace-switch ,i))
+                   (/= ,i exwm-workspace-current-index)])
+               (number-sequence 0 (1- (exwm-workspace--count))))))))
+
+(define-derived-mode exwm-mode nil "EXWM"
+  "Major mode for managing X windows.
+
+\\{exwm-mode-map}"
+  ;;
+  (setq mode-name
+        '(:eval (propertize "EXWM" 'face
+                            (when (cl-some (lambda (i)
+                                             (frame-parameter i 'exwm-urgency))
+                                           exwm-workspace--list)
+                              'font-lock-warning-face))))
+  ;; Change major-mode is not allowed
+  (add-hook 'change-major-mode-hook #'kill-buffer nil t)
+  ;; Kill buffer -> close window
+  (add-hook 'kill-buffer-query-functions
+            #'exwm-manage--kill-buffer-query-function nil t)
+  ;; Redirect events when executing keyboard macros.
+  (push `(executing-kbd-macro . ,exwm--kmacro-map)
+        minor-mode-overriding-map-alist)
+  (setq buffer-read-only t
+        cursor-type nil
+        left-margin-width nil
+        right-margin-width nil
+        left-fringe-width 0
+        right-fringe-width 0
+        vertical-scroll-bar nil))
+
+
+
+(provide 'exwm-core)
+
+;;; exwm-core.el ends here
diff --git a/third_party/exwm/exwm-floating.el b/third_party/exwm/exwm-floating.el
new file mode 100644
index 0000000000..a9f9315b71
--- /dev/null
+++ b/third_party/exwm/exwm-floating.el
@@ -0,0 +1,783 @@
+;;; exwm-floating.el --- Floating Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module deals with the conversion between floating and non-floating
+;; states and implements moving/resizing operations on floating windows.
+
+;;; Code:
+
+(require 'xcb-cursor)
+(require 'exwm-core)
+
+(defgroup exwm-floating nil
+  "Floating."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-floating-setup-hook nil
+  "Normal hook run when an X window has been made floating, in the
+context of the corresponding buffer."
+  :type 'hook)
+
+(defcustom exwm-floating-exit-hook nil
+  "Normal hook run when an X window has exited floating state, in the
+context of the corresponding buffer."
+  :type 'hook)
+
+(defcustom exwm-floating-border-color "navy"
+  "Border color of floating windows."
+  :type 'color
+  :initialize #'custom-initialize-default
+  :set (lambda (symbol value)
+         (set-default symbol value)
+         ;; Change border color for all floating X windows.
+         (when exwm--connection
+           (let ((border-pixel (exwm--color->pixel value)))
+             (when border-pixel
+               (dolist (pair exwm--id-buffer-alist)
+                 (with-current-buffer (cdr pair)
+                   (when exwm--floating-frame
+                     (xcb:+request exwm--connection
+                         (make-instance 'xcb:ChangeWindowAttributes
+                                        :window
+                                        (frame-parameter exwm--floating-frame
+                                                         'exwm-container)
+                                        :value-mask xcb:CW:BorderPixel
+                                        :border-pixel border-pixel)))))
+               (xcb:flush exwm--connection))))))
+
+(defcustom exwm-floating-border-width 1
+  "Border width of floating windows."
+  :type '(integer
+          :validate (lambda (widget)
+                      (when (< (widget-value widget) 0)
+                        (widget-put widget :error "Border width is at least 0")
+                        widget)))
+  :initialize #'custom-initialize-default
+  :set (lambda (symbol value)
+         (let ((delta (- value exwm-floating-border-width))
+               container)
+           (set-default symbol value)
+           ;; Change border width for all floating X windows.
+           (dolist (pair exwm--id-buffer-alist)
+             (with-current-buffer (cdr pair)
+               (when exwm--floating-frame
+                 (setq container (frame-parameter exwm--floating-frame
+                                                  'exwm-container))
+                 (with-slots (x y)
+                     (xcb:+request-unchecked+reply exwm--connection
+                         (make-instance 'xcb:GetGeometry
+                                        :drawable container))
+                   (xcb:+request exwm--connection
+                       (make-instance 'xcb:ConfigureWindow
+                                      :window container
+                                      :value-mask
+                                      (logior xcb:ConfigWindow:X
+                                              xcb:ConfigWindow:Y
+                                              xcb:ConfigWindow:BorderWidth)
+                                      :border-width value
+                                      :x (- x delta)
+                                      :y (- y delta)))))))
+           (when exwm--connection
+             (xcb:flush exwm--connection)))))
+
+;; Cursors for moving/resizing a window
+(defvar exwm-floating--cursor-move nil)
+(defvar exwm-floating--cursor-top-left nil)
+(defvar exwm-floating--cursor-top nil)
+(defvar exwm-floating--cursor-top-right nil)
+(defvar exwm-floating--cursor-right nil)
+(defvar exwm-floating--cursor-bottom-right nil)
+(defvar exwm-floating--cursor-bottom nil)
+(defvar exwm-floating--cursor-bottom-left nil)
+(defvar exwm-floating--cursor-left nil)
+
+(defvar exwm-floating--moveresize-calculate nil
+  "Calculate move/resize parameters [buffer event-mask x y width height].")
+
+(defvar exwm-workspace--current)
+(defvar exwm-workspace--frame-y-offset)
+(defvar exwm-workspace--window-y-offset)
+(defvar exwm-workspace--workareas)
+(declare-function exwm-layout--hide "exwm-layout.el" (id))
+(declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id))
+(declare-function exwm-layout--refresh "exwm-layout.el" ())
+(declare-function exwm-layout--show "exwm-layout.el" (id &optional window))
+(declare-function exwm-workspace--position "exwm-workspace.el" (frame))
+(declare-function exwm-workspace--update-offsets "exwm-workspace.el" ())
+
+(defun exwm-floating--set-allowed-actions (id tilling)
+  "Set _NET_WM_ALLOWED_ACTIONS."
+  (exwm--log "#x%x" id)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_WM_ALLOWED_ACTIONS
+                     :window id
+                     :data (if tilling
+                               (vector xcb:Atom:_NET_WM_ACTION_MINIMIZE
+                                       xcb:Atom:_NET_WM_ACTION_FULLSCREEN
+                                       xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP
+                                       xcb:Atom:_NET_WM_ACTION_CLOSE)
+                             (vector xcb:Atom:_NET_WM_ACTION_MOVE
+                                     xcb:Atom:_NET_WM_ACTION_RESIZE
+                                     xcb:Atom:_NET_WM_ACTION_MINIMIZE
+                                     xcb:Atom:_NET_WM_ACTION_FULLSCREEN
+                                     xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP
+                                     xcb:Atom:_NET_WM_ACTION_CLOSE)))))
+
+(defun exwm-floating--set-floating (id)
+  "Make window ID floating."
+  (let ((window (get-buffer-window (exwm--id->buffer id))))
+    (when window
+      ;; Hide the non-floating X window first.
+      (set-window-buffer window (other-buffer nil t))))
+  (let* ((original-frame (buffer-local-value 'exwm--frame
+                                             (exwm--id->buffer id)))
+         ;; Create new frame
+         (frame (with-current-buffer
+                    (or (get-buffer "*scratch*")
+                        (progn
+                          (set-buffer-major-mode
+                           (get-buffer-create "*scratch*"))
+                          (get-buffer "*scratch*")))
+                  (make-frame
+                   `((minibuffer . ,(minibuffer-window exwm--frame))
+                     (left . ,(* window-min-width -10000))
+                     (top . ,(* window-min-height -10000))
+                     (width . ,window-min-width)
+                     (height . ,window-min-height)
+                     (unsplittable . t))))) ;and fix the size later
+         (outer-id (string-to-number (frame-parameter frame 'outer-window-id)))
+         (window-id (string-to-number (frame-parameter frame 'window-id)))
+         (frame-container (xcb:generate-id exwm--connection))
+         (window (frame-first-window frame)) ;and it's the only window
+         (x (slot-value exwm--geometry 'x))
+         (y (slot-value exwm--geometry 'y))
+         (width (slot-value exwm--geometry 'width))
+         (height (slot-value exwm--geometry 'height)))
+    ;; Force drawing menu-bar & tool-bar.
+    (redisplay t)
+    (exwm-workspace--update-offsets)
+    (exwm--log "Floating geometry (original): %dx%d%+d%+d" width height x y)
+    ;; Save frame parameters.
+    (set-frame-parameter frame 'exwm-outer-id outer-id)
+    (set-frame-parameter frame 'exwm-id window-id)
+    (set-frame-parameter frame 'exwm-container frame-container)
+    ;; Fix illegal parameters
+    ;; FIXME: check normal hints restrictions
+    (let* ((workarea (elt exwm-workspace--workareas
+                          (exwm-workspace--position original-frame)))
+           (x* (aref workarea 0))
+           (y* (aref workarea 1))
+           (width* (aref workarea 2))
+           (height* (aref workarea 3)))
+      ;; Center floating windows
+      (when (and (or (= x 0) (= x x*))
+                 (or (= y 0) (= y y*)))
+        (let ((buffer (exwm--id->buffer exwm-transient-for))
+              window edges)
+          (when (and buffer (setq window (get-buffer-window buffer)))
+            (setq edges (window-inside-absolute-pixel-edges window))
+            (unless (and (<= width (- (elt edges 2) (elt edges 0)))
+                         (<= height (- (elt edges 3) (elt edges 1))))
+              (setq edges nil)))
+          (if edges
+              ;; Put at the center of leading window
+              (setq x (+ x* (/ (- (elt edges 2) (elt edges 0) width) 2))
+                    y (+ y* (/ (- (elt edges 3) (elt edges 1) height) 2)))
+            ;; Put at the center of screen
+            (setq x (/ (- width* width) 2)
+                  y (/ (- height* height) 2)))))
+      (if (> width width*)
+          ;; Too wide
+          (progn (setq x x*
+                       width width*))
+        ;; Invalid width
+        (when (= 0 width) (setq width (/ width* 2)))
+        ;; Make sure at least half of the window is visible
+        (unless (< x* (+ x (/ width 2)) (+ x* width*))
+          (setq x (+ x* (/ (- width* width) 2)))))
+      (if (> height height*)
+          ;; Too tall
+          (setq y y*
+                height height*)
+        ;; Invalid height
+        (when (= 0 height) (setq height (/ height* 2)))
+        ;; Make sure at least half of the window is visible
+        (unless (< y* (+ y (/ height 2)) (+ y* height*))
+          (setq y (+ y* (/ (- height* height) 2)))))
+      ;; The geometry can be overridden by user options.
+      (let ((x** (plist-get exwm--configurations 'x))
+            (y** (plist-get exwm--configurations 'y))
+            (width** (plist-get exwm--configurations 'width))
+            (height** (plist-get exwm--configurations 'height)))
+        (if (integerp x**)
+            (setq x (+ x* x**))
+          (when (and (floatp x**)
+                     (>= 1 x** 0))
+            (setq x (+ x* (round (* x** width*))))))
+        (if (integerp y**)
+            (setq y (+ y* y**))
+          (when (and (floatp y**)
+                     (>= 1 y** 0))
+            (setq y (+ y* (round (* y** height*))))))
+        (if (integerp width**)
+            (setq width width**)
+          (when (and (floatp width**)
+                     (> 1 width** 0))
+            (setq width (max 1 (round (* width** width*))))))
+        (if (integerp height**)
+            (setq height height**)
+          (when (and (floatp height**)
+                     (> 1 height** 0))
+            (setq height (max 1 (round (* height** height*))))))))
+    (exwm--set-geometry id x y nil nil)
+    (xcb:flush exwm--connection)
+    (exwm--log "Floating geometry (corrected): %dx%d%+d%+d" width height x y)
+    ;; Fit frame to client
+    ;; It seems we have to make the frame invisible in order to resize it
+    ;; timely.
+    ;; The frame will be made visible by `select-frame-set-input-focus'.
+    (make-frame-invisible frame)
+    (let* ((edges (window-inside-pixel-edges window))
+           (frame-width (+ width (- (frame-pixel-width frame)
+                                    (- (elt edges 2) (elt edges 0)))))
+           (frame-height (+ height (- (frame-pixel-height frame)
+                                      (- (elt edges 3) (elt edges 1)))
+                            ;; Use `frame-outer-height' in the future.
+                            exwm-workspace--frame-y-offset))
+           (floating-mode-line (plist-get exwm--configurations
+                                          'floating-mode-line))
+           (floating-header-line (plist-get exwm--configurations
+                                            'floating-header-line))
+           (border-pixel (exwm--color->pixel exwm-floating-border-color)))
+      (if floating-mode-line
+          (setq exwm--mode-line-format (or exwm--mode-line-format
+                                           mode-line-format)
+                mode-line-format floating-mode-line)
+        (if (and (not (plist-member exwm--configurations 'floating-mode-line))
+                 exwm--mwm-hints-decorations)
+            (when exwm--mode-line-format
+              (setq mode-line-format exwm--mode-line-format))
+          ;; The mode-line need to be hidden in floating mode.
+          (setq frame-height (- frame-height (window-mode-line-height
+                                              (frame-root-window frame)))
+                exwm--mode-line-format (or exwm--mode-line-format
+                                           mode-line-format)
+                mode-line-format nil)))
+      (if floating-header-line
+          (setq header-line-format floating-header-line)
+        (if (and (not (plist-member exwm--configurations
+                                    'floating-header-line))
+                 exwm--mwm-hints-decorations)
+            (setq header-line-format nil)
+          ;; The header-line need to be hidden in floating mode.
+          (setq frame-height (- frame-height (window-header-line-height
+                                              (frame-root-window frame)))
+                header-line-format nil)))
+      (set-frame-size frame frame-width frame-height t)
+      ;; Create the frame container as the parent of the frame.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:CreateWindow
+                         :depth 0
+                         :wid frame-container
+                         :parent exwm--root
+                         :x x
+                         :y (- y exwm-workspace--window-y-offset)
+                         :width width
+                         :height height
+                         :border-width
+                         (with-current-buffer (exwm--id->buffer id)
+                           (let ((border-witdh (plist-get exwm--configurations
+                                                          'border-width)))
+                             (if (and (integerp border-witdh)
+                                      (>= border-witdh 0))
+                                 border-witdh
+                               exwm-floating-border-width)))
+                         :class xcb:WindowClass:InputOutput
+                         :visual 0
+                         :value-mask (logior xcb:CW:BackPixmap
+                                             (if border-pixel
+                                                 xcb:CW:BorderPixel 0)
+                                             xcb:CW:OverrideRedirect)
+                         :background-pixmap xcb:BackPixmap:ParentRelative
+                         :border-pixel border-pixel
+                         :override-redirect 1))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                         :window frame-container
+                         :data
+                         (format "EXWM floating frame container for 0x%x" id)))
+      ;; Map it.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:MapWindow :window frame-container))
+      ;; Put the X window right above this frame container.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window id
+                         :value-mask (logior xcb:ConfigWindow:Sibling
+                                             xcb:ConfigWindow:StackMode)
+                         :sibling frame-container
+                         :stack-mode xcb:StackMode:Above)))
+    ;; Reparent this frame to its container.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ReparentWindow
+                       :window outer-id :parent frame-container :x 0 :y 0))
+    (exwm-floating--set-allowed-actions id nil)
+    (xcb:flush exwm--connection)
+    ;; Set window/buffer
+    (with-current-buffer (exwm--id->buffer id)
+      (setq window-size-fixed exwm--fixed-size
+            exwm--floating-frame frame)
+      ;; Do the refresh manually.
+      (remove-hook 'window-configuration-change-hook #'exwm-layout--refresh)
+      (set-window-buffer window (current-buffer)) ;this changes current buffer
+      (add-hook 'window-configuration-change-hook #'exwm-layout--refresh)
+      (set-window-dedicated-p window t)
+      (exwm-layout--show id window))
+    (with-current-buffer (exwm--id->buffer id)
+      (if (exwm-layout--iconic-state-p id)
+          ;; Hide iconic floating X windows.
+          (exwm-floating-hide)
+        (with-selected-frame exwm--frame
+          (exwm-layout--refresh)))
+      (select-frame-set-input-focus frame))
+    ;; FIXME: Strangely, the Emacs frame can move itself at this point
+    ;;        when there are left/top struts set.  Force resetting its
+    ;;        position seems working, but it'd better to figure out why.
+    ;; FIXME: This also happens in another case (#220) where the cause is
+    ;;        still unclear.
+    (exwm--set-geometry outer-id 0 0 nil nil)
+    (xcb:flush exwm--connection))
+  (with-current-buffer (exwm--id->buffer id)
+    (run-hooks 'exwm-floating-setup-hook))
+  ;; Redraw the frame.
+  (redisplay t))
+
+(defun exwm-floating--unset-floating (id)
+  "Make window ID non-floating."
+  (exwm--log "#x%x" id)
+  (let ((buffer (exwm--id->buffer id)))
+    (with-current-buffer buffer
+      (when exwm--floating-frame
+        ;; The X window is already mapped.
+        ;; Unmap the X window.
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ChangeWindowAttributes
+                           :window id :value-mask xcb:CW:EventMask
+                           :event-mask xcb:EventMask:NoEvent))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:UnmapWindow :window id))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ChangeWindowAttributes
+                           :window id :value-mask xcb:CW:EventMask
+                           :event-mask (exwm--get-client-event-mask)))
+        ;; Reparent the floating frame back to the root window.
+        (let ((frame-id (frame-parameter exwm--floating-frame 'exwm-outer-id))
+              (frame-container (frame-parameter exwm--floating-frame
+                                                'exwm-container)))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:UnmapWindow :window frame-id))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:ReparentWindow
+                             :window frame-id
+                             :parent exwm--root
+                             :x 0 :y 0))
+          ;; Also destroy its container.
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:DestroyWindow :window frame-container))))
+      ;; Place the X window just above the reference X window.
+      ;; (the stacking order won't change from now on).
+      ;; Also hide the possible floating border.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window id
+                         :value-mask (logior xcb:ConfigWindow:BorderWidth
+                                             xcb:ConfigWindow:Sibling
+                                             xcb:ConfigWindow:StackMode)
+                         :border-width 0
+                         :sibling exwm--guide-window
+                         :stack-mode xcb:StackMode:Above)))
+    (exwm-floating--set-allowed-actions id t)
+    (xcb:flush exwm--connection)
+    (with-current-buffer buffer
+      (when exwm--floating-frame        ;from floating to non-floating
+        (set-window-dedicated-p (frame-first-window exwm--floating-frame) nil)
+        ;; Select a tiling window and delete the old frame.
+        (select-window (frame-selected-window exwm-workspace--current))
+        (with-current-buffer buffer
+          (delete-frame exwm--floating-frame))))
+    (with-current-buffer buffer
+      (setq window-size-fixed nil
+            exwm--floating-frame nil)
+      (if (not (plist-member exwm--configurations 'tiling-mode-line))
+          (when exwm--mode-line-format
+            (setq mode-line-format exwm--mode-line-format))
+        (setq exwm--mode-line-format (or exwm--mode-line-format
+                                         mode-line-format)
+              mode-line-format (plist-get exwm--configurations
+                                          'tiling-mode-line)))
+      (if (not (plist-member exwm--configurations 'tiling-header-line))
+          (setq header-line-format nil)
+        (setq header-line-format (plist-get exwm--configurations
+                                            'tiling-header-line))))
+    ;; Only show X windows in normal state.
+    (unless (exwm-layout--iconic-state-p)
+      (pop-to-buffer-same-window buffer)))
+  (with-current-buffer (exwm--id->buffer id)
+    (run-hooks 'exwm-floating-exit-hook)))
+
+;;;###autoload
+(cl-defun exwm-floating-toggle-floating ()
+  "Toggle the current window between floating and non-floating states."
+  (interactive)
+  (exwm--log)
+  (unless (derived-mode-p 'exwm-mode)
+    (cl-return-from exwm-floating-toggle-floating))
+  (with-current-buffer (window-buffer)
+    (if exwm--floating-frame
+        (exwm-floating--unset-floating exwm--id)
+      (exwm-floating--set-floating exwm--id))))
+
+;;;###autoload
+(defun exwm-floating-hide ()
+  "Hide the current floating X window (which would show again when selected)."
+  (interactive)
+  (exwm--log)
+  (when (and (derived-mode-p 'exwm-mode)
+             exwm--floating-frame)
+    (exwm-layout--hide exwm--id)
+    (select-frame-set-input-focus exwm-workspace--current)))
+
+(defun exwm-floating--start-moveresize (id &optional type)
+  "Start move/resize."
+  (exwm--log "#x%x" id)
+  (let ((buffer-or-id (or (exwm--id->buffer id) id))
+        frame container-or-id x y width height cursor)
+    (if (bufferp buffer-or-id)
+        ;; Managed.
+        (with-current-buffer buffer-or-id
+          (setq frame exwm--floating-frame
+                container-or-id (frame-parameter exwm--floating-frame
+                                                 'exwm-container)))
+      ;; Unmanaged.
+      (setq container-or-id id))
+    (when (and container-or-id
+               ;; Test if the pointer can be grabbed
+               (= xcb:GrabStatus:Success
+                  (slot-value
+                   (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:GrabPointer
+                                      :owner-events 0
+                                      :grab-window container-or-id
+                                      :event-mask xcb:EventMask:NoEvent
+                                      :pointer-mode xcb:GrabMode:Async
+                                      :keyboard-mode xcb:GrabMode:Async
+                                      :confine-to xcb:Window:None
+                                      :cursor xcb:Cursor:None
+                                      :time xcb:Time:CurrentTime))
+                   'status)))
+      (with-slots (root-x root-y win-x win-y)
+          (xcb:+request-unchecked+reply exwm--connection
+              (make-instance 'xcb:QueryPointer :window id))
+        (if (not (bufferp buffer-or-id))
+            ;; Unmanaged.
+            (unless (eq type xcb:ewmh:_NET_WM_MOVERESIZE_MOVE)
+              (with-slots ((width* width)
+                           (height* height))
+                  (xcb:+request-unchecked+reply exwm--connection
+                      (make-instance 'xcb:GetGeometry :drawable id))
+                (setq width width*
+                      height height*)))
+          ;; Managed.
+          (select-window (frame-first-window frame)) ;transfer input focus
+          (setq width (frame-pixel-width frame)
+                height (frame-pixel-height frame))
+          (unless type
+            ;; Determine the resize type according to the pointer position
+            ;; Clicking the center 1/3 part to resize has no effect
+            (setq x (/ (* 3 win-x) (float width))
+                  y (/ (* 3 win-y) (float height))
+                  type (cond ((and (< x 1) (< y 1))
+                              xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPLEFT)
+                             ((and (> x 2) (< y 1))
+                              xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPRIGHT)
+                             ((and (> x 2) (> y 2))
+                              xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT)
+                             ((and (< x 1) (> y 2))
+                              xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT)
+                             ((> x 2) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_RIGHT)
+                             ((> y 2) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOM)
+                             ((< x 1) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_LEFT)
+                             ((< y 1) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOP)))))
+        (if (not type)
+            (exwm-floating--stop-moveresize)
+          (cond ((= type xcb:ewmh:_NET_WM_MOVERESIZE_MOVE)
+                 (setq cursor exwm-floating--cursor-move
+                       exwm-floating--moveresize-calculate
+                       (lambda (x y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:X
+                                           xcb:ConfigWindow:Y))
+                                 (- x win-x) (- y win-y) 0 0))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPLEFT)
+                 (setq cursor exwm-floating--cursor-top-left
+                       exwm-floating--moveresize-calculate
+                       (lambda (x y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:X
+                                           xcb:ConfigWindow:Y
+                                           xcb:ConfigWindow:Width
+                                           xcb:ConfigWindow:Height))
+                                 (- x win-x) (- y win-y)
+                                 (- (+ root-x width) x)
+                                 (- (+ root-y height) y)))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOP)
+                 (setq cursor exwm-floating--cursor-top
+                       exwm-floating--moveresize-calculate
+                       (lambda (_x y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:Y
+                                           xcb:ConfigWindow:Height))
+                                 0 (- y win-y) 0 (- (+ root-y height) y)))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPRIGHT)
+                 (setq cursor exwm-floating--cursor-top-right
+                       exwm-floating--moveresize-calculate
+                       (lambda (x y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:Y
+                                           xcb:ConfigWindow:Width
+                                           xcb:ConfigWindow:Height))
+                                 0 (- y win-y) (- x (- root-x width))
+                                 (- (+ root-y height) y)))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_RIGHT)
+                 (setq cursor exwm-floating--cursor-right
+                       exwm-floating--moveresize-calculate
+                       (lambda (x _y)
+                         (vector buffer-or-id
+                                 xcb:ConfigWindow:Width
+                                 0 0 (- x (- root-x width)) 0))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT)
+                 (setq cursor exwm-floating--cursor-bottom-right
+                       exwm-floating--moveresize-calculate
+                       (lambda (x y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:Width
+                                           xcb:ConfigWindow:Height))
+                                 0 0 (- x (- root-x width))
+                                 (- y (- root-y height))))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOM)
+                 (setq cursor exwm-floating--cursor-bottom
+                       exwm-floating--moveresize-calculate
+                       (lambda (_x y)
+                         (vector buffer-or-id
+                                 xcb:ConfigWindow:Height
+                                 0 0 0 (- y (- root-y height))))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT)
+                 (setq cursor exwm-floating--cursor-bottom-left
+                       exwm-floating--moveresize-calculate
+                       (lambda (x y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:X
+                                           xcb:ConfigWindow:Width
+                                           xcb:ConfigWindow:Height))
+                                 (- x win-x)
+                                 0
+                                 (- (+ root-x width) x)
+                                 (- y (- root-y height))))))
+                ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_LEFT)
+                 (setq cursor exwm-floating--cursor-left
+                       exwm-floating--moveresize-calculate
+                       (lambda (x _y)
+                         (vector buffer-or-id
+                                 (eval-when-compile
+                                   (logior xcb:ConfigWindow:X
+                                           xcb:ConfigWindow:Width))
+                                 (- x win-x) 0 (- (+ root-x width) x) 0)))))
+          ;; Select events and change cursor (should always succeed)
+          (xcb:+request-unchecked+reply exwm--connection
+              (make-instance 'xcb:GrabPointer
+                             :owner-events 0 :grab-window container-or-id
+                             :event-mask (eval-when-compile
+                                           (logior xcb:EventMask:ButtonRelease
+                                                   xcb:EventMask:ButtonMotion))
+                             :pointer-mode xcb:GrabMode:Async
+                             :keyboard-mode xcb:GrabMode:Async
+                             :confine-to xcb:Window:None
+                             :cursor cursor
+                             :time xcb:Time:CurrentTime)))))))
+
+(defun exwm-floating--stop-moveresize (&rest _args)
+  "Stop move/resize."
+  (exwm--log)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:UngrabPointer :time xcb:Time:CurrentTime))
+  (when exwm-floating--moveresize-calculate
+    (let (result buffer-or-id outer-id container-id)
+      (setq result (funcall exwm-floating--moveresize-calculate 0 0)
+            buffer-or-id (aref result 0))
+      (when (bufferp buffer-or-id)
+        (with-current-buffer buffer-or-id
+          (setq outer-id (frame-parameter exwm--floating-frame 'exwm-outer-id)
+                container-id (frame-parameter exwm--floating-frame
+                                              'exwm-container))
+          (with-slots (x y width height border-width)
+              (xcb:+request-unchecked+reply exwm--connection
+                  (make-instance 'xcb:GetGeometry
+                                 :drawable container-id))
+            ;; Notify Emacs frame about this the position change.
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:SendEvent
+                               :propagate 0
+                               :destination outer-id
+                               :event-mask xcb:EventMask:StructureNotify
+                               :event
+                               (xcb:marshal
+                                (make-instance 'xcb:ConfigureNotify
+                                               :event outer-id
+                                               :window outer-id
+                                               :above-sibling xcb:Window:None
+                                               :x (+ x border-width)
+                                               :y (+ y border-width)
+                                               :width width
+                                               :height height
+                                               :border-width 0
+                                               :override-redirect 0)
+                                exwm--connection)))
+            (xcb:flush exwm--connection))
+          (exwm-layout--show exwm--id
+                             (frame-root-window exwm--floating-frame)))))
+    (setq exwm-floating--moveresize-calculate nil)))
+
+(defun exwm-floating--do-moveresize (data _synthetic)
+  "Perform move/resize."
+  (when exwm-floating--moveresize-calculate
+    (let* ((obj (make-instance 'xcb:MotionNotify))
+           result value-mask x y width height buffer-or-id container-or-id)
+      (xcb:unmarshal obj data)
+      (setq result (funcall exwm-floating--moveresize-calculate
+                            (slot-value obj 'root-x) (slot-value obj 'root-y))
+            buffer-or-id (aref result 0)
+            value-mask (aref result 1)
+            x (aref result 2)
+            y (aref result 3)
+            width (max 1 (aref result 4))
+            height (max 1 (aref result 5)))
+      (if (not (bufferp buffer-or-id))
+          ;; Unmanaged.
+          (setq container-or-id buffer-or-id)
+        ;; Managed.
+        (setq container-or-id
+              (with-current-buffer buffer-or-id
+                (frame-parameter exwm--floating-frame 'exwm-container))
+              x (- x exwm-floating-border-width)
+              ;; Use `frame-outer-height' in the future.
+              y (- y exwm-floating-border-width
+                   exwm-workspace--window-y-offset)
+              height (+ height exwm-workspace--window-y-offset)))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window container-or-id
+                         :value-mask (aref result 1)
+                         :x x
+                         :y y
+                         :width width
+                         :height height))
+      (when (bufferp buffer-or-id)
+        ;; Managed.
+        (setq value-mask (logand value-mask (logior xcb:ConfigWindow:Width
+                                                    xcb:ConfigWindow:Height)))
+        (when (/= 0 value-mask)
+          (with-current-buffer buffer-or-id
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:ConfigureWindow
+                               :window (frame-parameter exwm--floating-frame
+                                                        'exwm-outer-id)
+                               :value-mask value-mask
+                               :width width
+                               :height height)))))
+      (xcb:flush exwm--connection))))
+
+(defun exwm-floating-move (&optional delta-x delta-y)
+  "Move a floating window right by DELTA-X pixels and down by DELTA-Y pixels.
+
+Both DELTA-X and DELTA-Y default to 1.  This command should be bound locally."
+  (exwm--log "delta-x: %s, delta-y: %s" delta-x delta-y)
+  (unless (and (derived-mode-p 'exwm-mode) exwm--floating-frame)
+    (user-error "[EXWM] `exwm-floating-move' is only for floating X windows"))
+  (unless delta-x (setq delta-x 1))
+  (unless delta-y (setq delta-y 1))
+  (unless (and (= 0 delta-x) (= 0 delta-y))
+    (let* ((floating-container (frame-parameter exwm--floating-frame
+                                                'exwm-container))
+           (geometry (xcb:+request-unchecked+reply exwm--connection
+                         (make-instance 'xcb:GetGeometry
+                                        :drawable floating-container)))
+           (edges (window-inside-absolute-pixel-edges)))
+      (with-slots (x y) geometry
+        (exwm--set-geometry floating-container
+                            (+ x delta-x) (+ y delta-y) nil nil))
+      (exwm--set-geometry exwm--id
+                          (+ (pop edges) delta-x)
+                          (+ (pop edges) delta-y)
+                          nil nil))
+    (xcb:flush exwm--connection)))
+
+(defun exwm-floating--init ()
+  "Initialize floating module."
+  (exwm--log)
+  ;; Initialize cursors for moving/resizing a window
+  (xcb:cursor:init exwm--connection)
+  (setq exwm-floating--cursor-move
+        (xcb:cursor:load-cursor exwm--connection "fleur")
+        exwm-floating--cursor-top-left
+        (xcb:cursor:load-cursor exwm--connection "top_left_corner")
+        exwm-floating--cursor-top
+        (xcb:cursor:load-cursor exwm--connection "top_side")
+        exwm-floating--cursor-top-right
+        (xcb:cursor:load-cursor exwm--connection "top_right_corner")
+        exwm-floating--cursor-right
+        (xcb:cursor:load-cursor exwm--connection "right_side")
+        exwm-floating--cursor-bottom-right
+        (xcb:cursor:load-cursor exwm--connection "bottom_right_corner")
+        exwm-floating--cursor-bottom
+        (xcb:cursor:load-cursor exwm--connection "bottom_side")
+        exwm-floating--cursor-bottom-left
+        (xcb:cursor:load-cursor exwm--connection "bottom_left_corner")
+        exwm-floating--cursor-left
+        (xcb:cursor:load-cursor exwm--connection "left_side")))
+
+(defun exwm-floating--exit ()
+  "Exit the floating module."
+  (exwm--log))
+
+
+
+(provide 'exwm-floating)
+
+;;; exwm-floating.el ends here
diff --git a/third_party/exwm/exwm-input.el b/third_party/exwm/exwm-input.el
new file mode 100644
index 0000000000..50676217f1
--- /dev/null
+++ b/third_party/exwm/exwm-input.el
@@ -0,0 +1,1243 @@
+;;; exwm-input.el --- Input Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module deals with key/mouse matters, including:
+;; + Input focus,
+;; + Key/Button event handling,
+;; + Key events filtering and simulation.
+
+;; Todo:
+;; + Pointer simulation mode (e.g. 'C-c 1'/'C-c 2' for single/double click,
+;;   move with arrow keys).
+;; + Simulation keys to mimic Emacs key bindings for text edit (redo, select,
+;;   cancel, clear, etc).  Some of them are not present on common keyboard
+;;   (keycode = 0).  May need to use XKB extension.
+
+;;; Code:
+
+(require 'xcb-keysyms)
+(require 'exwm-core)
+
+(defgroup exwm-input nil
+  "Input."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-input-prefix-keys
+  '(?\C-x ?\C-u ?\C-h ?\M-x ?\M-` ?\M-& ?\M-:)
+  "List of prefix keys EXWM should forward to Emacs when in line-mode.
+
+The point is to make keys like 'C-x C-f' forwarded to Emacs in line-mode.
+There is no need to add prefix keys for global/simulation keys or those
+defined in `exwm-mode-map' here."
+  :type '(repeat key-sequence)
+  :get (lambda (symbol)
+         (mapcar #'vector (default-value symbol)))
+  :set (lambda (symbol value)
+         (set symbol (mapcar (lambda (i)
+                               (if (sequencep i)
+                                   (aref i 0)
+                                 i))
+                             value))))
+
+(defcustom exwm-input-move-event 's-down-mouse-1
+  "Emacs event to start moving a window."
+  :type 'key-sequence
+  :get (lambda (symbol)
+         (let ((value (default-value symbol)))
+           (if (mouse-event-p value)
+               value
+             (vector value))))
+  :set (lambda (symbol value)
+         (set symbol (if (sequencep value)
+                         (aref value 0)
+                       value))))
+
+(defcustom exwm-input-resize-event 's-down-mouse-3
+  "Emacs event to start resizing a window."
+  :type 'key-sequence
+  :get (lambda (symbol)
+         (let ((value (default-value symbol)))
+           (if (mouse-event-p value)
+               value
+             (vector value))))
+  :set (lambda (symbol value)
+         (set symbol (if (sequencep value)
+                         (aref value 0)
+                       value))))
+
+(defcustom exwm-input-line-mode-passthrough nil
+  "Non-nil makes 'line-mode' forward all events to Emacs."
+  :type 'boolean)
+
+;; Input focus update requests should be accumulated for a short time
+;; interval so that only the last one need to be processed.  This not
+;; improves the overall performance, but avoids the problem of input
+;; focus loop, which is a result of the interaction with Emacs frames.
+;;
+;; FIXME: The time interval is hard to decide and perhaps machine-dependent.
+;;        A value too small can cause redundant updates of input focus,
+;;        and even worse, dead loops.  OTOH a large value would bring
+;;        laggy experience.
+(defconst exwm-input--update-focus-interval 0.01
+  "Time interval (in seconds) for accumulating input focus update requests.")
+
+(defvar exwm-input--during-command nil
+  "Indicate whether between `pre-command-hook' and `post-command-hook'.")
+
+(defvar exwm-input--global-keys nil "Global key bindings.")
+
+(defvar exwm-input--global-prefix-keys nil
+  "List of prefix keys of global key bindings.")
+
+(defvar exwm-input--line-mode-cache nil "Cache for incomplete key sequence.")
+
+(defvar exwm-input--local-simulation-keys nil
+  "Whether simulation keys are local.")
+
+(defvar exwm-input--simulation-keys nil "Simulation keys in line-mode.")
+
+(defvar exwm-input--temp-line-mode nil
+  "Non-nil indicates it's in temporary line-mode for char-mode.")
+
+(defvar exwm-input--timestamp-atom nil)
+
+(defvar exwm-input--timestamp-callback nil)
+
+(defvar exwm-input--timestamp-window nil)
+
+(defvar exwm-input--update-focus-defer-timer nil "Timer for polling the lock.")
+
+(defvar exwm-input--update-focus-lock nil
+  "Lock for solving input focus update contention.")
+
+(defvar exwm-input--update-focus-timer nil
+  "Timer for deferring the update of input focus.")
+
+(defvar exwm-input--update-focus-window nil "The (Emacs) window to be focused.
+It also helps us discern whether a `buffer-list-update-hook' was caused by a
+different window having been selected.
+
+This value should always be overwritten.")
+
+(defvar exwm-input--update-focus-window-buffer nil
+  "Buffer displayed in `exwm-input--update-focus-window'.
+Helps us discern whether a `buffer-list-update-hook' was caused by the selected
+window switching to a different buffer.")
+
+(defvar exwm-input--echo-area-timer nil "Timer for detecting echo area dirty.")
+
+(defvar exwm-input--event-hook nil
+  "Hook to run when EXWM receives an event.")
+
+(defvar exwm-input-input-mode-change-hook nil
+  "Hook to run when an input mode changes on an `exwm-mode' buffer.
+Current buffer will be the `exwm-mode' buffer when this hook runs.")
+
+(defvar exwm-workspace--current)
+(declare-function exwm-floating--do-moveresize "exwm-floating.el"
+                  (data _synthetic))
+(declare-function exwm-floating--start-moveresize "exwm-floating.el"
+                  (id &optional type))
+(declare-function exwm-floating--stop-moveresize "exwm-floating.el"
+                  (&rest _args))
+(declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id))
+(declare-function exwm-layout--show "exwm-layout.el" (id &optional window))
+(declare-function exwm-reset "exwm.el" ())
+(declare-function exwm-workspace--client-p "exwm-workspace.el"
+                  (&optional frame))
+(declare-function exwm-workspace--minibuffer-own-frame-p "exwm-workspace.el")
+(declare-function exwm-workspace--workspace-p "exwm-workspace.el" (workspace))
+(declare-function exwm-workspace-switch "exwm-workspace.el"
+                  (frame-or-index &optional force))
+
+(defun exwm-input--set-focus (id)
+  "Set input focus to window ID in a proper way."
+  (let ((from (slot-value (xcb:+request-unchecked+reply exwm--connection
+                              (make-instance 'xcb:GetInputFocus))
+                          'focus))
+        tree)
+    (if (or (exwm--id->buffer from)
+            (eq from id))
+        (exwm--log "#x%x => #x%x" (or from 0) (or id 0))
+      ;; Attempt to find the top-level X window for a 'focus proxy'.
+      (unless (= from xcb:Window:None)
+        (setq tree (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:QueryTree
+                                      :window from)))
+        (when tree
+          (setq from (slot-value tree 'parent))))
+      (exwm--log "#x%x (corrected) => #x%x" (or from 0) (or id 0)))
+    (when (and (exwm--id->buffer id)
+               ;; Avoid redundant input focus transfer.
+               (not (eq from id)))
+      (with-current-buffer (exwm--id->buffer id)
+        (exwm-input--update-timestamp
+         (lambda (timestamp id send-input-focus wm-take-focus)
+           (when send-input-focus
+             (xcb:+request exwm--connection
+                 (make-instance 'xcb:SetInputFocus
+                                :revert-to xcb:InputFocus:Parent
+                                :focus id
+                                :time timestamp)))
+           (when wm-take-focus
+             (let ((event (make-instance 'xcb:icccm:WM_TAKE_FOCUS
+                                         :window id
+                                         :time timestamp)))
+               (setq event (xcb:marshal event exwm--connection))
+               (xcb:+request exwm--connection
+                   (make-instance 'xcb:icccm:SendEvent
+                                  :destination id
+                                  :event event))))
+           (exwm-input--set-active-window id)
+           (xcb:flush exwm--connection))
+         id
+         (or exwm--hints-input
+             (not (memq xcb:Atom:WM_TAKE_FOCUS exwm--protocols)))
+         (memq xcb:Atom:WM_TAKE_FOCUS exwm--protocols))))))
+
+(defun exwm-input--update-timestamp (callback &rest args)
+  "Fetch the latest timestamp from the server and feed it to CALLBACK.
+
+ARGS are additional arguments to CALLBACK."
+  (setq exwm-input--timestamp-callback (cons callback args))
+  (exwm--log)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ChangeProperty
+                     :mode xcb:PropMode:Replace
+                     :window exwm-input--timestamp-window
+                     :property exwm-input--timestamp-atom
+                     :type xcb:Atom:CARDINAL
+                     :format 32
+                     :data-len 0
+                     :data nil))
+  (xcb:flush exwm--connection))
+
+(defun exwm-input--on-PropertyNotify (data _synthetic)
+  "Handle PropertyNotify events."
+  (exwm--log)
+  (when exwm-input--timestamp-callback
+    (let ((obj (make-instance 'xcb:PropertyNotify)))
+      (xcb:unmarshal obj data)
+      (when (= exwm-input--timestamp-window
+               (slot-value obj 'window))
+        (apply (car exwm-input--timestamp-callback)
+               (slot-value obj 'time)
+               (cdr exwm-input--timestamp-callback))
+        (setq exwm-input--timestamp-callback nil)))))
+
+(defvar exwm-input--last-enter-notify-position nil)
+
+(defun exwm-input--on-EnterNotify (data _synthetic)
+  "Handle EnterNotify events."
+  (let ((evt (make-instance 'xcb:EnterNotify))
+        buffer window frame frame-xid edges fake-evt)
+    (xcb:unmarshal evt data)
+    (with-slots (time root event root-x root-y event-x event-y state) evt
+      (setq buffer (exwm--id->buffer event)
+            window (get-buffer-window buffer t))
+      (exwm--log "buffer=%s; window=%s" buffer window)
+      (when (and buffer window (not (eq window (selected-window)))
+                 (not (equal exwm-input--last-enter-notify-position
+                             (vector root-x root-y))))
+        (setq frame (window-frame window)
+              frame-xid (frame-parameter frame 'exwm-id))
+        (unless (eq frame exwm-workspace--current)
+          (if (exwm-workspace--workspace-p frame)
+              ;; The X window is on another workspace.
+              (exwm-workspace-switch frame)
+            (with-current-buffer buffer
+              (when (and (derived-mode-p 'exwm-mode)
+                         (not (eq exwm--frame exwm-workspace--current)))
+                ;; The floating X window is on another workspace.
+                (exwm-workspace-switch exwm--frame)))))
+        ;; Send a fake MotionNotify event to Emacs.
+        (setq edges (window-inside-pixel-edges window)
+              fake-evt (make-instance 'xcb:MotionNotify
+                                      :detail 0
+                                      :time time
+                                      :root root
+                                      :event frame-xid
+                                      :child xcb:Window:None
+                                      :root-x root-x
+                                      :root-y root-y
+                                      :event-x (+ event-x (elt edges 0))
+                                      :event-y (+ event-y (elt edges 1))
+                                      :state state
+                                      :same-screen 1))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:SendEvent
+                           :propagate 0
+                           :destination frame-xid
+                           :event-mask xcb:EventMask:NoEvent
+                           :event (xcb:marshal fake-evt exwm--connection)))
+        (xcb:flush exwm--connection))
+      (setq exwm-input--last-enter-notify-position (vector root-x root-y)))))
+
+(defun exwm-input--on-keysyms-update ()
+  (exwm--log)
+  (let ((exwm-input--global-prefix-keys nil))
+    (exwm-input--update-global-prefix-keys)))
+
+(defun exwm-input--on-buffer-list-update ()
+  "Run in `buffer-list-update-hook' to track input focus."
+  ;; `buffer-list-update-hook' is invoked by several functions
+  ;; (`get-buffer-create', `select-window', `with-temp-buffer', etc.), but we
+  ;; just want to notice when a different window has been selected, or when the
+  ;; selected window displays a different buffer, so that we can set the focus
+  ;; to the associated X window (in case of an `exwm-mode' buffer).  In order to
+  ;; differentiate, we keep track of the last selected window and buffer in the
+  ;; `exwm-input--update-focus-window' and
+  ;; `exwm-input--update-focus-window-buffer' variables.
+  (let* ((win (selected-window))
+         (buf (window-buffer win)))
+    (when (and (not (exwm-workspace--client-p))
+               (not (and (eq exwm-input--update-focus-window win)
+                         (eq exwm-input--update-focus-window-buffer buf))))
+      (exwm--log "selected-window=%S current-buffer=%S" win buf)
+      (setq exwm-input--update-focus-window win)
+      (setq exwm-input--update-focus-window-buffer buf)
+      (redirect-frame-focus (selected-frame) nil)
+      (exwm-input--update-focus-defer))))
+
+(defun exwm-input--update-focus-defer ()
+  "Defer updating input focus."
+  (when exwm-input--update-focus-defer-timer
+    (cancel-timer exwm-input--update-focus-defer-timer))
+  (if exwm-input--update-focus-lock
+      (setq exwm-input--update-focus-defer-timer
+            (exwm--defer 0 #'exwm-input--update-focus-defer))
+    (setq exwm-input--update-focus-defer-timer nil)
+    (when exwm-input--update-focus-timer
+      (cancel-timer exwm-input--update-focus-timer))
+    (setq exwm-input--update-focus-timer
+          ;; Attempt to accumulate successive events close enough.
+          (run-with-timer exwm-input--update-focus-interval
+                          nil
+                          #'exwm-input--update-focus-commit
+                          exwm-input--update-focus-window))))
+
+(defun exwm-input--update-focus-commit (window)
+  "Commit updating input focus."
+  (setq exwm-input--update-focus-lock t)
+  (unwind-protect
+      (exwm-input--update-focus window)
+    (setq exwm-input--update-focus-lock nil)))
+
+(defun exwm-input--update-focus (window)
+  "Update input focus."
+  (when (window-live-p window)
+    (exwm--log "focus-window=%s focus-buffer=%s" window (window-buffer window))
+    (with-current-buffer (window-buffer window)
+      (if (derived-mode-p 'exwm-mode)
+          (if (not (eq exwm--frame exwm-workspace--current))
+              (progn
+                (set-frame-parameter exwm--frame 'exwm-selected-window window)
+                (exwm--defer 0 #'exwm-workspace-switch exwm--frame))
+            (exwm--log "Set focus on #x%x" exwm--id)
+            (when exwm--floating-frame
+              ;; Adjust stacking orders of the floating X window.
+              (xcb:+request exwm--connection
+                  (make-instance 'xcb:ConfigureWindow
+                                 :window exwm--id
+                                 :value-mask xcb:ConfigWindow:StackMode
+                                 :stack-mode xcb:StackMode:TopIf))
+              (xcb:+request exwm--connection
+                  (make-instance 'xcb:ConfigureWindow
+                                 :window (frame-parameter exwm--floating-frame
+                                                          'exwm-container)
+                                 :value-mask (logior
+                                              xcb:ConfigWindow:Sibling
+                                              xcb:ConfigWindow:StackMode)
+                                 :sibling exwm--id
+                                 :stack-mode xcb:StackMode:Below))
+              ;; This floating X window might be hide by `exwm-floating-hide'.
+              (when (exwm-layout--iconic-state-p)
+                (exwm-layout--show exwm--id window))
+              (xcb:flush exwm--connection))
+            (exwm-input--set-focus exwm--id))
+        (when (eq (selected-window) window)
+          (exwm--log "Focus on %s" window)
+          (if (and (exwm-workspace--workspace-p (selected-frame))
+                   (not (eq (selected-frame) exwm-workspace--current)))
+              ;; The focus is on another workspace (e.g. it got clicked)
+              ;; so switch to it.
+              (progn
+                (exwm--log "Switching to %s's workspace %s (%s)"
+                           window
+                           (window-frame window)
+                           (selected-frame))
+                (set-frame-parameter (selected-frame) 'exwm-selected-window
+                                     window)
+                (exwm--defer 0 #'exwm-workspace-switch (selected-frame)))
+            ;; The focus is still on the current workspace.
+            (if (not (and (exwm-workspace--minibuffer-own-frame-p)
+                          (minibufferp)))
+                (x-focus-frame (window-frame window))
+              ;; X input focus should be set on the previously selected
+              ;; frame.
+              (x-focus-frame (window-frame (minibuffer-window))))
+            (exwm-input--set-active-window)
+            (xcb:flush exwm--connection)))))))
+
+(defun exwm-input--set-active-window (&optional id)
+  "Set _NET_ACTIVE_WINDOW."
+  (exwm--log)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_ACTIVE_WINDOW
+                     :window exwm--root
+                     :data (or id xcb:Window:None))))
+
+(defun exwm-input--on-ButtonPress (data _synthetic)
+  "Handle ButtonPress event."
+  (let ((obj (make-instance 'xcb:ButtonPress))
+        (mode xcb:Allow:SyncPointer)
+        button-event window buffer frame fake-last-command)
+    (xcb:unmarshal obj data)
+    (exwm--log "major-mode=%s buffer=%s"
+               major-mode (buffer-name (current-buffer)))
+    (with-slots (detail event state) obj
+      (setq button-event (xcb:keysyms:keysym->event exwm--connection
+                                                    detail state)
+            buffer (exwm--id->buffer event)
+            window (get-buffer-window buffer t))
+      (cond ((and (eq button-event exwm-input-move-event)
+                  buffer
+                  ;; Either an undecorated or a floating X window.
+                  (with-current-buffer buffer
+                    (or (not (derived-mode-p 'exwm-mode))
+                        exwm--floating-frame)))
+             ;; Move
+             (exwm-floating--start-moveresize
+              event xcb:ewmh:_NET_WM_MOVERESIZE_MOVE))
+            ((and (eq button-event exwm-input-resize-event)
+                  buffer
+                  (with-current-buffer buffer
+                    (or (not (derived-mode-p 'exwm-mode))
+                        exwm--floating-frame)))
+             ;; Resize
+             (exwm-floating--start-moveresize event))
+            (buffer
+             ;; Click to focus
+             (setq fake-last-command t)
+             (unless (eq window (selected-window))
+               (setq frame (window-frame window))
+               (unless (eq frame exwm-workspace--current)
+                 (if (exwm-workspace--workspace-p frame)
+                     ;; The X window is on another workspace
+                     (exwm-workspace-switch frame)
+                   (with-current-buffer buffer
+                     (when (and (derived-mode-p 'exwm-mode)
+                                (not (eq exwm--frame
+                                         exwm-workspace--current)))
+                       ;; The floating X window is on another workspace
+                       (exwm-workspace-switch exwm--frame)))))
+               ;; It has been reported that the `window' may have be deleted
+               (if (window-live-p window)
+                   (select-window window)
+                 (setq window (get-buffer-window buffer t))
+                 (when window (select-window window))))
+             ;; Also process keybindings.
+             (with-current-buffer buffer
+               (when (derived-mode-p 'exwm-mode)
+                 (cl-case exwm--input-mode
+                   (line-mode
+                    (setq mode (exwm-input--on-ButtonPress-line-mode
+                                buffer button-event)))
+                   (char-mode
+                    (setq mode (exwm-input--on-ButtonPress-char-mode)))))))
+            (t
+             ;; Replay this event by default.
+             (setq fake-last-command t)
+             (setq mode xcb:Allow:ReplayPointer))))
+    (when fake-last-command
+      (exwm-input--fake-last-command))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:AllowEvents :mode mode :time xcb:Time:CurrentTime))
+    (xcb:flush exwm--connection))
+  (run-hooks 'exwm-input--event-hook))
+
+(defun exwm-input--on-KeyPress (data _synthetic)
+  "Handle KeyPress event."
+  (with-current-buffer (window-buffer (selected-window))
+    (let ((obj (make-instance 'xcb:KeyPress)))
+      (xcb:unmarshal obj data)
+      (exwm--log "major-mode=%s buffer=%s"
+                 major-mode (buffer-name (current-buffer)))
+      (if (derived-mode-p 'exwm-mode)
+          (cl-case exwm--input-mode
+            (line-mode
+             (exwm-input--on-KeyPress-line-mode obj data))
+            (char-mode
+             (exwm-input--on-KeyPress-char-mode obj data)))
+        (exwm-input--on-KeyPress-char-mode obj)))
+    (run-hooks 'exwm-input--event-hook)))
+
+(defun exwm-input--on-CreateNotify (data _synthetic)
+  "Handle CreateNotify events."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:CreateNotify)))
+    (xcb:unmarshal evt data)
+    (with-slots (window) evt
+      (exwm-input--grab-global-prefix-keys window))))
+
+(defun exwm-input--update-global-prefix-keys ()
+  "Update `exwm-input--global-prefix-keys'."
+  (exwm--log)
+  (when exwm--connection
+    (let ((original exwm-input--global-prefix-keys))
+      (setq exwm-input--global-prefix-keys nil)
+      (dolist (i exwm-input--global-keys)
+        (cl-pushnew (elt i 0) exwm-input--global-prefix-keys))
+      (unless (equal original exwm-input--global-prefix-keys)
+        (apply #'exwm-input--grab-global-prefix-keys
+               (slot-value (xcb:+request-unchecked+reply exwm--connection
+                               (make-instance 'xcb:QueryTree
+                                              :window exwm--root))
+                           'children))))))
+
+(defun exwm-input--grab-global-prefix-keys (&rest xwins)
+  (exwm--log)
+  (let ((req (make-instance 'xcb:GrabKey
+                            :owner-events 0
+                            :grab-window nil
+                            :modifiers nil
+                            :key nil
+                            :pointer-mode xcb:GrabMode:Async
+                            :keyboard-mode xcb:GrabMode:Async))
+        keysyms keycode alt-modifier)
+    (dolist (k exwm-input--global-prefix-keys)
+      (setq keysyms (xcb:keysyms:event->keysyms exwm--connection k))
+      (if (not keysyms)
+          (warn "Key unavailable: %s" (key-description (vector k)))
+        (setq keycode (xcb:keysyms:keysym->keycode exwm--connection
+                                                   (caar keysyms)))
+        (exwm--log "Grabbing key=%s (keysyms=%s keycode=%s)"
+                   (single-key-description k) keysyms keycode)
+        (dolist (keysym keysyms)
+          (setf (slot-value req 'modifiers) (cdr keysym)
+                (slot-value req 'key) keycode)
+          ;; Also grab this key with num-lock mask set.
+          (when (and (/= 0 xcb:keysyms:num-lock-mask)
+                     (= 0 (logand (cdr keysym) xcb:keysyms:num-lock-mask)))
+            (setf alt-modifier (logior (cdr keysym)
+                                       xcb:keysyms:num-lock-mask)))
+          (dolist (xwin xwins)
+            (setf (slot-value req 'grab-window) xwin)
+            (xcb:+request exwm--connection req)
+            (when alt-modifier
+              (setf (slot-value req 'modifiers) alt-modifier)
+              (xcb:+request exwm--connection req))))))
+    (xcb:flush exwm--connection)))
+
+(defun exwm-input--set-key (key command)
+  (exwm--log "key: %s, command: %s" key command)
+  (global-set-key key command)
+  (cl-pushnew key exwm-input--global-keys))
+
+(defcustom exwm-input-global-keys nil
+  "Global keys.
+
+It is an alist of the form (key . command), meaning giving KEY (a key
+sequence) a global binding as COMMAND.
+
+Notes:
+* Setting the value directly (rather than customizing it) after EXWM
+  finishes initialization has no effect."
+  :type '(alist :key-type key-sequence :value-type function)
+  :set (lambda (symbol value)
+         (when (boundp symbol)
+           (dolist (i (symbol-value symbol))
+             (global-unset-key (car i))))
+         (set symbol value)
+         (setq exwm-input--global-keys nil)
+         (dolist (i value)
+           (exwm-input--set-key (car i) (cdr i)))
+         (when exwm--connection
+           (exwm-input--update-global-prefix-keys))))
+
+;;;###autoload
+(defun exwm-input-set-key (key command)
+  "Set a global key binding.
+
+The new key binding only takes effect in real time when this command is
+called interactively, and is lost when this session ends unless it's
+specifically saved in the Customize interface for `exwm-input-global-keys'.
+
+In configuration you should customize or set `exwm-input-global-keys'
+instead."
+  (interactive "KSet key globally: \nCSet key %s to command: ")
+  (exwm--log)
+  (setq exwm-input-global-keys (append exwm-input-global-keys
+                                       (list (cons key command))))
+  (exwm-input--set-key key command)
+  (when (called-interactively-p 'any)
+    (exwm-input--update-global-prefix-keys)))
+
+;; Putting (t . EVENT) into `unread-command-events' does not really work
+;; as documented for Emacs < 26.2.
+(eval-and-compile
+  (if (or (< emacs-major-version 26)
+          (and (= emacs-major-version 26)
+               (< emacs-minor-version 2)))
+      (defsubst exwm-input--unread-event (event)
+        (setq unread-command-events
+              (append unread-command-events (list event))))
+    (defsubst exwm-input--unread-event (event)
+      (setq unread-command-events
+            (append unread-command-events `((t . ,event)))))))
+
+(defun exwm-input--mimic-read-event (event)
+  "Process EVENT as if it were returned by `read-event'."
+  (exwm--log)
+  (unless (eq 0 extra-keyboard-modifiers)
+    (setq event (event-convert-list (append (event-modifiers
+                                             extra-keyboard-modifiers)
+                                            event))))
+  (when (characterp event)
+    (let ((event* (when keyboard-translate-table
+                    (aref keyboard-translate-table event))))
+      (when event*
+        (setq event event*))))
+  event)
+
+(cl-defun exwm-input--translate (key)
+  (let (translation)
+    (dolist (map (list input-decode-map
+                       local-function-key-map
+                       key-translation-map))
+      (setq translation (lookup-key map key))
+      (if (functionp translation)
+          (cl-return-from exwm-input--translate (funcall translation nil))
+        (when (vectorp translation)
+          (cl-return-from exwm-input--translate translation)))))
+  key)
+
+(defun exwm-input--cache-event (event &optional temp-line-mode)
+  "Cache EVENT."
+  (exwm--log "%s" event)
+  (setq exwm-input--line-mode-cache
+        (vconcat exwm-input--line-mode-cache (vector event)))
+  ;; Attempt to translate this key sequence.
+  (setq exwm-input--line-mode-cache
+        (exwm-input--translate exwm-input--line-mode-cache))
+  ;; When the key sequence is complete (not a keymap).
+  ;; Note that `exwm-input--line-mode-cache' might get translated to nil, for
+  ;; example 'mouse--down-1-maybe-follows-link' does this.
+  (if (and exwm-input--line-mode-cache
+           (keymapp (key-binding exwm-input--line-mode-cache)))
+      ;; Grab keyboard temporarily to intercept the complete key sequence.
+      (when temp-line-mode
+        (setq exwm-input--temp-line-mode t)
+        (exwm-input--grab-keyboard))
+    (setq exwm-input--line-mode-cache nil)
+    (when exwm-input--temp-line-mode
+      (setq exwm-input--temp-line-mode nil)
+      (exwm-input--release-keyboard))))
+
+(defun exwm-input--event-passthrough-p (event)
+  "Whether EVENT should be passed to Emacs.
+Current buffer must be an `exwm-mode' buffer."
+  (or exwm-input-line-mode-passthrough
+      exwm-input--during-command
+      ;; Forward the event when there is an incomplete key
+      ;; sequence or when the minibuffer is active.
+      exwm-input--line-mode-cache
+      (eq (active-minibuffer-window) (selected-window))
+      ;;
+      (memq event exwm-input--global-prefix-keys)
+      (memq event exwm-input-prefix-keys)
+      (when overriding-terminal-local-map
+        (lookup-key overriding-terminal-local-map
+                    (vector event)))
+      (lookup-key (current-local-map) (vector event))
+      (gethash event exwm-input--simulation-keys)))
+
+(defun exwm-input--noop (&rest _args)
+  "A placeholder command."
+  (interactive))
+
+(defun exwm-input--fake-last-command ()
+  "Fool some packages into thinking there is a change in the buffer."
+  (setq last-command #'exwm-input--noop)
+  (run-hooks 'pre-command-hook)
+  (run-hooks 'post-command-hook))
+
+(defun exwm-input--on-KeyPress-line-mode (key-press raw-data)
+  "Parse X KeyPress event to Emacs key event and then feed the command loop."
+  (with-slots (detail state) key-press
+    (let ((keysym (xcb:keysyms:keycode->keysym exwm--connection detail state))
+          event raw-event mode)
+      (exwm--log "%s" keysym)
+      (when (and (/= 0 (car keysym))
+                 (setq raw-event (xcb:keysyms:keysym->event
+                                  exwm--connection (car keysym)
+                                  (logand state (lognot (cdr keysym)))))
+                 (setq event (exwm-input--mimic-read-event raw-event))
+                 (exwm-input--event-passthrough-p event))
+        (setq mode xcb:Allow:AsyncKeyboard)
+        (exwm-input--cache-event event)
+        (exwm-input--unread-event raw-event))
+      (unless mode
+        (if (= 0 (logand #x6000 state)) ;Check the 13~14 bits.
+            ;; Not an XKB state; just replay it.
+            (setq mode xcb:Allow:ReplayKeyboard)
+          ;; An XKB state; sent it with SendEvent.
+          ;; FIXME: Can this also be replayed?
+          ;; FIXME: KeyRelease events are lost.
+          (setq mode xcb:Allow:AsyncKeyboard)
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:SendEvent
+                             :propagate 0
+                             :destination (slot-value key-press 'event)
+                             :event-mask xcb:EventMask:NoEvent
+                             :event raw-data)))
+        (when event
+          (if (not defining-kbd-macro)
+              (exwm-input--fake-last-command)
+            ;; Make Emacs aware of this event when defining keyboard macros.
+            (set-transient-map `(keymap (t . ,#'exwm-input--noop)))
+            (exwm-input--unread-event event))))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:AllowEvents
+                         :mode mode
+                         :time xcb:Time:CurrentTime))
+      (xcb:flush exwm--connection))))
+
+(defun exwm-input--on-KeyPress-char-mode (key-press &optional _raw-data)
+  "Handle KeyPress event in char-mode."
+  (with-slots (detail state) key-press
+    (let ((keysym (xcb:keysyms:keycode->keysym exwm--connection detail state))
+          event raw-event)
+      (exwm--log "%s" keysym)
+      (when (and (/= 0 (car keysym))
+                 (setq raw-event (xcb:keysyms:keysym->event
+                                  exwm--connection (car keysym)
+                                  (logand state (lognot (cdr keysym)))))
+                 (setq event (exwm-input--mimic-read-event raw-event)))
+        (if (not (derived-mode-p 'exwm-mode))
+            (exwm-input--unread-event raw-event)
+          (exwm-input--cache-event event t)
+          (exwm-input--unread-event raw-event)))))
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:AllowEvents
+                     :mode xcb:Allow:AsyncKeyboard
+                     :time xcb:Time:CurrentTime))
+  (xcb:flush exwm--connection))
+
+(defun exwm-input--on-ButtonPress-line-mode (buffer button-event)
+  "Handle button events in line mode.
+BUFFER is the `exwm-mode' buffer the event was generated
+on. BUTTON-EVENT is the X event converted into an Emacs event.
+
+The return value is used as event_mode to release the original
+button event."
+  (with-current-buffer buffer
+    (let ((read-event (exwm-input--mimic-read-event button-event)))
+      (exwm--log "%s" read-event)
+      (if (and read-event
+               (exwm-input--event-passthrough-p read-event))
+          ;; The event should be forwarded to emacs
+          (progn
+            (exwm-input--cache-event read-event)
+            (exwm-input--unread-event button-event)
+            xcb:Allow:SyncPointer)
+        ;; The event should be replayed
+        xcb:Allow:ReplayPointer))))
+
+(defun exwm-input--on-ButtonPress-char-mode ()
+  "Handle button events in char-mode.
+The return value is used as event_mode to release the original
+button event."
+  (exwm--log)
+  xcb:Allow:ReplayPointer)
+
+(defun exwm-input--update-mode-line (id)
+  "Update the propertized `mode-line-process' for window ID."
+  (exwm--log "#x%x" id)
+  (let (help-echo cmd mode)
+    (with-current-buffer (exwm--id->buffer id)
+      (cl-case exwm--input-mode
+        (line-mode
+         (setq mode "line"
+               help-echo "mouse-1: Switch to char-mode"
+               cmd (lambda ()
+                     (interactive)
+                     (exwm-input-release-keyboard id))))
+        (char-mode
+         (setq mode "char"
+               help-echo "mouse-1: Switch to line-mode"
+               cmd (lambda ()
+                     (interactive)
+                     (exwm-input-grab-keyboard id)))))
+      (setq mode-line-process
+            `(": "
+              (:propertize ,mode
+                           help-echo ,help-echo
+                           mouse-face mode-line-highlight
+                           local-map
+                           (keymap
+                            (mode-line
+                             keymap
+                             (down-mouse-1 . ,cmd))))))
+      (force-mode-line-update))))
+
+(defun exwm-input--grab-keyboard (&optional id)
+  "Grab all key events on window ID."
+  (unless id (setq id (exwm--buffer->id (window-buffer))))
+  (when id
+    (exwm--log "id=#x%x" id)
+    (when (xcb:+request-checked+request-check exwm--connection
+              (make-instance 'xcb:GrabKey
+                             :owner-events 0
+                             :grab-window id
+                             :modifiers xcb:ModMask:Any
+                             :key xcb:Grab:Any
+                             :pointer-mode xcb:GrabMode:Async
+                             :keyboard-mode xcb:GrabMode:Sync))
+      (exwm--log "Failed to grab keyboard for #x%x" id))
+    (let ((buffer (exwm--id->buffer id)))
+      (when buffer
+        (with-current-buffer buffer
+          (setq exwm--input-mode 'line-mode)
+          (run-hooks 'exwm-input-input-mode-change-hook))))))
+
+(defun exwm-input--release-keyboard (&optional id)
+  "Ungrab all key events on window ID."
+  (unless id (setq id (exwm--buffer->id (window-buffer))))
+  (when id
+    (exwm--log "id=#x%x" id)
+    (when (xcb:+request-checked+request-check exwm--connection
+              (make-instance 'xcb:UngrabKey
+                             :key xcb:Grab:Any
+                             :grab-window id
+                             :modifiers xcb:ModMask:Any))
+      (exwm--log "Failed to release keyboard for #x%x" id))
+    (exwm-input--grab-global-prefix-keys id)
+    (let ((buffer (exwm--id->buffer id)))
+      (when buffer
+        (with-current-buffer buffer
+          (setq exwm--input-mode 'char-mode)
+          (run-hooks 'exwm-input-input-mode-change-hook))))))
+
+;;;###autoload
+(defun exwm-input-grab-keyboard (&optional id)
+  "Switch to line-mode."
+  (interactive (list (when (derived-mode-p 'exwm-mode)
+                       (exwm--buffer->id (window-buffer)))))
+  (when id
+    (exwm--log "id=#x%x" id)
+    (setq exwm--selected-input-mode 'line-mode)
+    (exwm-input--grab-keyboard id)
+    (exwm-input--update-mode-line id)))
+
+;;;###autoload
+(defun exwm-input-release-keyboard (&optional id)
+  "Switch to char-mode."
+  (interactive (list (when (derived-mode-p 'exwm-mode)
+                       (exwm--buffer->id (window-buffer)))))
+  (when id
+    (exwm--log "id=#x%x" id)
+    (setq exwm--selected-input-mode  'char-mode)
+    (exwm-input--release-keyboard id)
+    (exwm-input--update-mode-line id)))
+
+;;;###autoload
+(defun exwm-input-toggle-keyboard (&optional id)
+  "Toggle between 'line-mode' and 'char-mode'."
+  (interactive (list (when (derived-mode-p 'exwm-mode)
+                       (exwm--buffer->id (window-buffer)))))
+  (when id
+    (exwm--log "id=#x%x" id)
+    (with-current-buffer (exwm--id->buffer id)
+      (cl-case exwm--input-mode
+        (line-mode
+         (exwm-input-release-keyboard id))
+        (char-mode
+         (exwm-reset))))))
+
+(defun exwm-input--fake-key (event)
+  "Fake a key event equivalent to Emacs event EVENT."
+  (let* ((keysyms (xcb:keysyms:event->keysyms exwm--connection event))
+         keycode id)
+    (when (= 0 (caar keysyms))
+      (user-error "[EXWM] Invalid key: %s" (single-key-description event)))
+    (setq keycode (xcb:keysyms:keysym->keycode exwm--connection
+                                               (caar keysyms)))
+    (when (/= 0 keycode)
+      (setq id (exwm--buffer->id (window-buffer (selected-window))))
+      (exwm--log "id=#x%x event=%s keycode" id event keycode)
+      (dolist (class '(xcb:KeyPress xcb:KeyRelease))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:SendEvent
+                           :propagate 0 :destination id
+                           :event-mask xcb:EventMask:NoEvent
+                           :event (xcb:marshal
+                                   (make-instance class
+                                                  :detail keycode
+                                                  :time xcb:Time:CurrentTime
+                                                  :root exwm--root :event id
+                                                  :child 0
+                                                  :root-x 0 :root-y 0
+                                                  :event-x 0 :event-y 0
+                                                  :state (cdar keysyms)
+                                                  :same-screen 1)
+                                   exwm--connection)))))
+    (xcb:flush exwm--connection)))
+
+;;;###autoload
+(cl-defun exwm-input-send-next-key (times &optional end-key)
+  "Send next key to client window.
+
+EXWM will prompt for the key to send.  This command can be prefixed to send
+multiple keys.  If END-KEY is non-nil, stop sending keys if it's pressed."
+  (interactive "p")
+  (exwm--log)
+  (unless (derived-mode-p 'exwm-mode)
+    (cl-return-from exwm-input-send-next-key))
+  (when (> times 12) (setq times 12))
+  (let (key keys)
+    (dotimes (i times)
+      ;; Skip events not from keyboard
+      (let ((exwm-input-line-mode-passthrough t))
+        (catch 'break
+          (while t
+            (setq key (read-key (format "Send key: %s (%d/%d) %s"
+                                        (key-description keys)
+                                        (1+ i) times
+                                        (if end-key
+                                            (concat "To exit, press: "
+                                                    (key-description
+                                                     (list end-key)))
+                                          ""))))
+            (unless (listp key) (throw 'break nil)))))
+      (setq keys (vconcat keys (vector key)))
+      (when (eq key end-key) (cl-return-from exwm-input-send-next-key))
+      (exwm-input--fake-key key))))
+
+(defun exwm-input--set-simulation-keys (simulation-keys &optional no-refresh)
+  "Set simulation keys."
+  (exwm--log "%s" simulation-keys)
+  (unless no-refresh
+    ;; Unbind simulation keys.
+    (let ((hash (buffer-local-value 'exwm-input--simulation-keys
+                                    (current-buffer))))
+      (when (hash-table-p hash)
+        (maphash (lambda (key _value)
+                   (when (sequencep key)
+                     (if exwm-input--local-simulation-keys
+                         (local-unset-key key)
+                       (define-key exwm-mode-map key nil))))
+                 hash)))
+    ;; Abandon the old hash table.
+    (setq exwm-input--simulation-keys (make-hash-table :test #'equal)))
+  (dolist (i simulation-keys)
+    (let ((original (vconcat (car i)))
+          (simulated (cdr i)))
+      (setq simulated (if (sequencep simulated)
+                          (append simulated nil)
+                        (list simulated)))
+      ;; The key stored is a key sequence (vector).
+      ;; The value stored is a list of key events.
+      (puthash original simulated exwm-input--simulation-keys)
+      ;; Also mark the prefix key as used.
+      (puthash (aref original 0) t exwm-input--simulation-keys)))
+  ;; Update keymaps.
+  (maphash (lambda (key _value)
+             (when (sequencep key)
+               (if exwm-input--local-simulation-keys
+                   (local-set-key key #'exwm-input-send-simulation-key)
+                 (define-key exwm-mode-map key
+                   #'exwm-input-send-simulation-key))))
+           exwm-input--simulation-keys))
+
+(defun exwm-input-set-simulation-keys (simulation-keys)
+  "Please customize or set `exwm-input-simulation-keys' instead."
+  (declare (obsolete nil "26"))
+  (exwm-input--set-simulation-keys simulation-keys))
+
+(defcustom exwm-input-simulation-keys nil
+  "Simulation keys.
+
+It is an alist of the form (original-key . simulated-key), where both
+original-key and simulated-key are key sequences.  Original-key is what you
+type to an X window in line-mode which then gets translated to simulated-key
+by EXWM and forwarded to the X window.
+
+Notes:
+* Setting the value directly (rather than customizing it) after EXWM
+  finishes initialization has no effect.
+* Original-keys consist of multiple key events are only supported in Emacs
+  26.2 and later.
+* A minority of applications do not accept simulated keys by default.  It's
+  required to customize them to accept events sent by SendEvent.
+* The predefined examples in the Customize interface are not guaranteed to
+  work for all applications.  This can be tweaked on a per application basis
+  with `exwm-input-set-local-simulation-keys'."
+  :type '(alist :key-type (key-sequence :tag "Original")
+                :value-type (choice (key-sequence :tag "User-defined")
+                                    (key-sequence :tag "Move left" [left])
+                                    (key-sequence :tag "Move right" [right])
+                                    (key-sequence :tag "Move up" [up])
+                                    (key-sequence :tag "Move down" [down])
+                                    (key-sequence :tag "Move to BOL" [home])
+                                    (key-sequence :tag "Move to EOL" [end])
+                                    (key-sequence :tag "Page up" [prior])
+                                    (key-sequence :tag "Page down" [next])
+                                    (key-sequence :tag "Copy" [C-c])
+                                    (key-sequence :tag "Paste" [C-v])
+                                    (key-sequence :tag "Delete" [delete])
+                                    (key-sequence :tag "Delete to EOL"
+                                                  [S-end delete])))
+  :set (lambda (symbol value)
+         (set symbol value)
+         (exwm-input--set-simulation-keys value)))
+
+(defcustom exwm-input-pre-post-command-blacklist '(exit-minibuffer
+                                                   abort-recursive-edit
+                                                   minibuffer-keyboard-quit)
+  "Commands impossible to detect with `post-command-hook'."
+  :type '(repeat function))
+
+(cl-defun exwm-input--read-keys (prompt stop-key)
+  (let ((cursor-in-echo-area t)
+        keys key)
+    (while (not (eq key stop-key))
+      (setq key (read-key (format "%s (terminate with %s): %s"
+                                  prompt
+                                  (key-description (vector stop-key))
+                                  (key-description keys)))
+            keys (vconcat keys (vector key))))
+    (when (> (length keys) 1)
+      (substring keys 0 -1))))
+
+;;;###autoload
+(defun exwm-input-set-simulation-key (original-key simulated-key)
+  "Set a simulation key.
+
+The simulation key takes effect in real time, but is lost when this session
+ends unless it's specifically saved in the Customize interface for
+`exwm-input-simulation-keys'."
+  (interactive
+   (let (original simulated)
+     (setq original (exwm-input--read-keys "Translate from" ?\C-g))
+     (when original
+       (setq simulated (exwm-input--read-keys
+                        (format "Translate from %s to"
+                                (key-description original))
+                        ?\C-g)))
+     (list original simulated)))
+  (exwm--log "original: %s, simulated: %s" original-key simulated-key)
+  (when (and original-key simulated-key)
+    (let ((entry `((,original-key . ,simulated-key))))
+      (setq exwm-input-simulation-keys (append exwm-input-simulation-keys
+                                               entry))
+      (exwm-input--set-simulation-keys entry t))))
+
+(defun exwm-input--unset-simulation-keys ()
+  "Clear simulation keys and key bindings defined."
+  (exwm--log)
+  (when (hash-table-p exwm-input--simulation-keys)
+    (maphash (lambda (key _value)
+               (when (sequencep key)
+                 (define-key exwm-mode-map key nil)))
+             exwm-input--simulation-keys)
+    (clrhash exwm-input--simulation-keys)))
+
+(defun exwm-input-set-local-simulation-keys (simulation-keys)
+  "Set buffer-local simulation keys.
+
+SIMULATION-KEYS is an alist of the form (original-key . simulated-key),
+where both ORIGINAL-KEY and SIMULATED-KEY are key sequences."
+  (exwm--log)
+  (make-local-variable 'exwm-input--simulation-keys)
+  (use-local-map (copy-keymap exwm-mode-map))
+  (let ((exwm-input--local-simulation-keys t))
+    (exwm-input--set-simulation-keys simulation-keys)))
+
+;;;###autoload
+(cl-defun exwm-input-send-simulation-key (times)
+  "Fake a key event according to the last input key sequence."
+  (interactive "p")
+  (exwm--log)
+  (unless (derived-mode-p 'exwm-mode)
+    (cl-return-from exwm-input-send-simulation-key))
+  (let ((keys (gethash (this-single-command-keys)
+                       exwm-input--simulation-keys)))
+    (dotimes (_ times)
+      (dolist (key keys)
+        (exwm-input--fake-key key)))))
+
+;;;###autoload
+(defmacro exwm-input-invoke-factory (keys)
+  "Make a command that invokes KEYS when called.
+
+One use is to access the keymap bound to KEYS (as prefix keys) in char-mode."
+  (let* ((keys (kbd keys))
+         (description (key-description keys)))
+    `(defun ,(intern (concat "exwm-input--invoke--" description)) ()
+       ,(format "Invoke `%s'." description)
+       (interactive)
+       (mapc (lambda (key)
+               (exwm-input--cache-event key t)
+               (exwm-input--unread-event key))
+             ',(listify-key-sequence keys)))))
+
+(defun exwm-input--on-pre-command ()
+  "Run in `pre-command-hook'."
+  (unless (or (eq this-command #'exwm-input--noop)
+              (memq this-command exwm-input-pre-post-command-blacklist))
+    (setq exwm-input--during-command t)))
+
+(defun exwm-input--on-post-command ()
+  "Run in `post-command-hook'."
+  (unless (eq this-command #'exwm-input--noop)
+    (setq exwm-input--during-command nil)))
+
+(defun exwm-input--on-minibuffer-setup ()
+  "Run in `minibuffer-setup-hook' to grab keyboard if necessary."
+  (exwm--log)
+  (with-current-buffer
+      (window-buffer (frame-selected-window exwm-workspace--current))
+    (when (and (derived-mode-p 'exwm-mode)
+               (not (exwm-workspace--client-p))
+               (eq exwm--selected-input-mode 'char-mode))
+      (exwm-input--grab-keyboard exwm--id))))
+
+(defun exwm-input--on-minibuffer-exit ()
+  "Run in `minibuffer-exit-hook' to release keyboard if necessary."
+  (exwm--log)
+  (with-current-buffer
+      (window-buffer (frame-selected-window exwm-workspace--current))
+    (when (and (derived-mode-p 'exwm-mode)
+               (not (exwm-workspace--client-p))
+               (eq exwm--selected-input-mode 'char-mode)
+               (eq exwm--input-mode 'line-mode))
+      (exwm-input--release-keyboard exwm--id))))
+
+(defun exwm-input--on-echo-area-dirty ()
+  "Run when new message arrives to grab keyboard if necessary."
+  (exwm--log)
+  (when (and (not (active-minibuffer-window))
+             (not (exwm-workspace--client-p))
+             cursor-in-echo-area)
+    (exwm-input--on-minibuffer-setup)))
+
+(defun exwm-input--on-echo-area-clear ()
+  "Run in `echo-area-clear-hook' to release keyboard if necessary."
+  (exwm--log)
+  (unless (current-message)
+    (exwm-input--on-minibuffer-exit)))
+
+(defun exwm-input--init ()
+  "Initialize the keyboard module."
+  (exwm--log)
+  ;; Refresh keyboard mapping
+  (xcb:keysyms:init exwm--connection #'exwm-input--on-keysyms-update)
+  ;; Create the X window and intern the atom used to fetch timestamp.
+  (setq exwm-input--timestamp-window (xcb:generate-id exwm--connection))
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:CreateWindow
+                     :depth 0
+                     :wid exwm-input--timestamp-window
+                     :parent exwm--root
+                     :x -1
+                     :y -1
+                     :width 1
+                     :height 1
+                     :border-width 0
+                     :class xcb:WindowClass:CopyFromParent
+                     :visual 0
+                     :value-mask xcb:CW:EventMask
+                     :event-mask xcb:EventMask:PropertyChange))
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                     :window exwm-input--timestamp-window
+                     :data "EXWM: exwm-input--timestamp-window"))
+  (setq exwm-input--timestamp-atom (exwm--intern-atom "_TIME"))
+  ;; Initialize global keys.
+  (dolist (i exwm-input-global-keys)
+    (exwm-input--set-key (car i) (cdr i)))
+  ;; Initialize simulation keys.
+  (when exwm-input-simulation-keys
+    (exwm-input--set-simulation-keys exwm-input-simulation-keys))
+  ;; Attach event listeners
+  (xcb:+event exwm--connection 'xcb:PropertyNotify
+              #'exwm-input--on-PropertyNotify)
+  (xcb:+event exwm--connection 'xcb:CreateNotify #'exwm-input--on-CreateNotify)
+  (xcb:+event exwm--connection 'xcb:KeyPress #'exwm-input--on-KeyPress)
+  (xcb:+event exwm--connection 'xcb:ButtonPress #'exwm-input--on-ButtonPress)
+  (xcb:+event exwm--connection 'xcb:ButtonRelease
+              #'exwm-floating--stop-moveresize)
+  (xcb:+event exwm--connection 'xcb:MotionNotify
+              #'exwm-floating--do-moveresize)
+  (when mouse-autoselect-window
+    (xcb:+event exwm--connection 'xcb:EnterNotify
+                #'exwm-input--on-EnterNotify))
+  ;; Control `exwm-input--during-command'
+  (add-hook 'pre-command-hook #'exwm-input--on-pre-command)
+  (add-hook 'post-command-hook #'exwm-input--on-post-command)
+  ;; Grab/Release keyboard when minibuffer/echo becomes active/inactive.
+  (add-hook 'minibuffer-setup-hook #'exwm-input--on-minibuffer-setup)
+  (add-hook 'minibuffer-exit-hook #'exwm-input--on-minibuffer-exit)
+  (setq exwm-input--echo-area-timer
+        (run-with-idle-timer 0 t #'exwm-input--on-echo-area-dirty))
+  (add-hook 'echo-area-clear-hook #'exwm-input--on-echo-area-clear)
+  ;; Update focus when buffer list updates
+  (add-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update))
+
+(defun exwm-input--post-init ()
+  "The second stage in the initialization of the input module."
+  (exwm--log)
+  (exwm-input--update-global-prefix-keys))
+
+(defun exwm-input--exit ()
+  "Exit the input module."
+  (exwm--log)
+  (exwm-input--unset-simulation-keys)
+  (remove-hook 'pre-command-hook #'exwm-input--on-pre-command)
+  (remove-hook 'post-command-hook #'exwm-input--on-post-command)
+  (remove-hook 'minibuffer-setup-hook #'exwm-input--on-minibuffer-setup)
+  (remove-hook 'minibuffer-exit-hook #'exwm-input--on-minibuffer-exit)
+  (when exwm-input--echo-area-timer
+    (cancel-timer exwm-input--echo-area-timer)
+    (setq exwm-input--echo-area-timer nil))
+  (remove-hook 'echo-area-clear-hook #'exwm-input--on-echo-area-clear)
+  (remove-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update)
+  (when exwm-input--update-focus-defer-timer
+    (cancel-timer exwm-input--update-focus-defer-timer))
+  (when exwm-input--update-focus-timer
+    (cancel-timer exwm-input--update-focus-timer))
+  ;; Make input focus working even without a WM.
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:SetInputFocus
+                     :revert-to xcb:InputFocus:PointerRoot
+                     :focus exwm--root
+                     :time xcb:Time:CurrentTime))
+  (xcb:flush exwm--connection))
+
+
+
+(provide 'exwm-input)
+
+;;; exwm-input.el ends here
diff --git a/third_party/exwm/exwm-layout.el b/third_party/exwm/exwm-layout.el
new file mode 100644
index 0000000000..9173a1c049
--- /dev/null
+++ b/third_party/exwm/exwm-layout.el
@@ -0,0 +1,620 @@
+;;; exwm-layout.el --- Layout Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module is responsible for keeping X client window properly displayed.
+
+;;; Code:
+
+(require 'exwm-core)
+
+(defgroup exwm-layout nil
+  "Layout."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-layout-auto-iconify t
+  "Non-nil to automatically iconify unused X windows when possible."
+  :type 'boolean)
+
+(defcustom exwm-layout-show-all-buffers nil
+  "Non-nil to allow switching to buffers on other workspaces."
+  :type 'boolean)
+
+(defconst exwm-layout--floating-hidden-position -101
+  "Where to place hidden floating X windows.")
+
+(defvar exwm-layout--other-buffer-exclude-buffers nil
+  "List of buffers that should not be selected by `other-buffer'.")
+
+(defvar exwm-layout--other-buffer-exclude-exwm-mode-buffers nil
+  "When non-nil, prevent EXWM buffers from being selected by `other-buffer'.")
+
+(defvar exwm-layout--timer nil "Timer used to track echo area changes.")
+
+(defvar exwm-workspace--current)
+(defvar exwm-workspace--frame-y-offset)
+(declare-function exwm-input--release-keyboard "exwm-input.el")
+(declare-function exwm-input--grab-keyboard "exwm-input.el")
+(declare-function exwm-input-grab-keyboard "exwm-input.el")
+(declare-function exwm-workspace--active-p "exwm-workspace.el" (frame))
+(declare-function exwm-workspace--client-p "exwm-workspace.el"
+                  (&optional frame))
+(declare-function exwm-workspace--minibuffer-own-frame-p "exwm-workspace.el")
+(declare-function exwm-workspace--workspace-p "exwm-workspace.el"
+                  (workspace))
+(declare-function exwm-workspace-move-window "exwm-workspace.el"
+                  (frame-or-index &optional id))
+
+(defun exwm-layout--set-state (id state)
+  "Set WM_STATE."
+  (exwm--log "id=#x%x" id)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:icccm:set-WM_STATE
+                     :window id :state state :icon xcb:Window:None))
+  (with-current-buffer (exwm--id->buffer id)
+    (setq exwm-state state)))
+
+(defun exwm-layout--iconic-state-p (&optional id)
+  (= xcb:icccm:WM_STATE:IconicState
+     (if id
+         (buffer-local-value 'exwm-state (exwm--id->buffer id))
+       exwm-state)))
+
+(defun exwm-layout--set-ewmh-state (xwin)
+  "Set _NET_WM_STATE."
+  (with-current-buffer (exwm--id->buffer xwin)
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_STATE
+                       :window exwm--id
+                       :data exwm--ewmh-state))))
+
+(defun exwm-layout--fullscreen-p ()
+  (when (derived-mode-p 'exwm-mode)
+    (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state)))
+
+(defun exwm-layout--auto-iconify ()
+  (when (and exwm-layout-auto-iconify
+             (not exwm-transient-for))
+    (let ((xwin exwm--id)
+          (state exwm-state))
+      (dolist (pair exwm--id-buffer-alist)
+        (with-current-buffer (cdr pair)
+          (when (and exwm--floating-frame
+                     (eq exwm-transient-for xwin)
+                     (not (eq exwm-state state)))
+            (if (eq state xcb:icccm:WM_STATE:NormalState)
+                (exwm-layout--refresh-floating exwm--floating-frame)
+              (exwm-layout--hide exwm--id))))))))
+
+(defun exwm-layout--show (id &optional window)
+  "Show window ID exactly fit in the Emacs window WINDOW."
+  (exwm--log "Show #x%x in %s" id window)
+  (let* ((edges (window-inside-absolute-pixel-edges window))
+         (x (pop edges))
+         (y (pop edges))
+         (width (- (pop edges) x))
+         (height (- (pop edges) y))
+         frame-x frame-y frame-width frame-height)
+    (with-current-buffer (exwm--id->buffer id)
+      (when exwm--floating-frame
+        (setq frame-width (frame-pixel-width exwm--floating-frame)
+              frame-height (+ (frame-pixel-height exwm--floating-frame)
+                              ;; Use `frame-outer-height' in the future.
+                              exwm-workspace--frame-y-offset))
+        (when exwm--floating-frame-position
+          (setq frame-x (elt exwm--floating-frame-position 0)
+                frame-y (elt exwm--floating-frame-position 1)
+                x (+ x frame-x (- exwm-layout--floating-hidden-position))
+                y (+ y frame-y (- exwm-layout--floating-hidden-position)))
+          (setq exwm--floating-frame-position nil))
+        (exwm--set-geometry (frame-parameter exwm--floating-frame
+                                             'exwm-container)
+                            frame-x frame-y frame-width frame-height))
+      (when (exwm-layout--fullscreen-p)
+        (with-slots ((x* x)
+                     (y* y)
+                     (width* width)
+                     (height* height))
+            (exwm-workspace--get-geometry exwm--frame)
+          (setq x x*
+                y y*
+                width width*
+                height height*)))
+      (exwm--set-geometry id x y width height)
+      (xcb:+request exwm--connection (make-instance 'xcb:MapWindow :window id))
+      (exwm-layout--set-state id xcb:icccm:WM_STATE:NormalState)
+      (setq exwm--ewmh-state
+            (delq xcb:Atom:_NET_WM_STATE_HIDDEN exwm--ewmh-state))
+      (exwm-layout--set-ewmh-state id)
+      (exwm-layout--auto-iconify)))
+  (xcb:flush exwm--connection))
+
+(defun exwm-layout--hide (id)
+  "Hide window ID."
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (or (exwm-layout--iconic-state-p)
+                (and exwm--floating-frame
+                     (eq 4294967295. exwm--desktop)))
+      (exwm--log "Hide #x%x" id)
+      (when exwm--floating-frame
+        (let* ((container (frame-parameter exwm--floating-frame
+                                           'exwm-container))
+               (geometry (xcb:+request-unchecked+reply exwm--connection
+                             (make-instance 'xcb:GetGeometry
+                                            :drawable container))))
+          (setq exwm--floating-frame-position
+                (vector (slot-value geometry 'x) (slot-value geometry 'y)))
+          (exwm--set-geometry container exwm-layout--floating-hidden-position
+                              exwm-layout--floating-hidden-position
+                              1
+                              1)))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ChangeWindowAttributes
+                         :window id :value-mask xcb:CW:EventMask
+                         :event-mask xcb:EventMask:NoEvent))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:UnmapWindow :window id))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ChangeWindowAttributes
+                         :window id :value-mask xcb:CW:EventMask
+                         :event-mask (exwm--get-client-event-mask)))
+      (exwm-layout--set-state id xcb:icccm:WM_STATE:IconicState)
+      (cl-pushnew xcb:Atom:_NET_WM_STATE_HIDDEN exwm--ewmh-state)
+      (exwm-layout--set-ewmh-state id)
+      (exwm-layout--auto-iconify)
+      (xcb:flush exwm--connection))))
+
+;;;###autoload
+(cl-defun exwm-layout-set-fullscreen (&optional id)
+  "Make window ID fullscreen."
+  (interactive)
+  (exwm--log "id=#x%x" (or id 0))
+  (unless (and (or id (derived-mode-p 'exwm-mode))
+               (not (exwm-layout--fullscreen-p)))
+    (cl-return-from exwm-layout-set-fullscreen))
+  (with-current-buffer (if id (exwm--id->buffer id) (window-buffer))
+    ;; Expand the X window to fill the whole screen.
+    (with-slots (x y width height) (exwm-workspace--get-geometry exwm--frame)
+      (exwm--set-geometry exwm--id x y width height))
+    ;; Raise the X window.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ConfigureWindow
+                       :window exwm--id
+                       :value-mask (logior xcb:ConfigWindow:BorderWidth
+                                           xcb:ConfigWindow:StackMode)
+                       :border-width 0
+                       :stack-mode xcb:StackMode:Above))
+    (cl-pushnew xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state)
+    (exwm-layout--set-ewmh-state exwm--id)
+    (xcb:flush exwm--connection)
+    (set-window-dedicated-p (get-buffer-window) t)
+    (exwm-input--release-keyboard exwm--id)))
+
+;;;###autoload
+(cl-defun exwm-layout-unset-fullscreen (&optional id)
+  "Restore window from fullscreen state."
+  (interactive)
+  (exwm--log "id=#x%x" (or id 0))
+  (unless (and (or id (derived-mode-p 'exwm-mode))
+               (exwm-layout--fullscreen-p))
+    (cl-return-from exwm-layout-unset-fullscreen))
+  (with-current-buffer (if id (exwm--id->buffer id) (window-buffer))
+    ;; `exwm-layout--show' relies on `exwm--ewmh-state' to decide whether to
+    ;; fullscreen the window.
+    (setq exwm--ewmh-state
+          (delq xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state))
+    (exwm-layout--set-ewmh-state exwm--id)
+    (if exwm--floating-frame
+        (exwm-layout--show exwm--id (frame-root-window exwm--floating-frame))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window exwm--id
+                         :value-mask (logior xcb:ConfigWindow:Sibling
+                                             xcb:ConfigWindow:StackMode)
+                         :sibling exwm--guide-window
+                         :stack-mode xcb:StackMode:Above))
+      (let ((window (get-buffer-window nil t)))
+        (when window
+          (exwm-layout--show exwm--id window))))
+    (xcb:flush exwm--connection)
+    (set-window-dedicated-p (get-buffer-window) nil)
+    (when (eq 'line-mode exwm--selected-input-mode)
+      (exwm-input--grab-keyboard exwm--id))))
+
+;;;###autoload
+(cl-defun exwm-layout-toggle-fullscreen (&optional id)
+  "Toggle fullscreen mode."
+  (interactive (list (exwm--buffer->id (window-buffer))))
+  (exwm--log "id=#x%x" (or id 0))
+  (unless (or id (derived-mode-p 'exwm-mode))
+    (cl-return-from exwm-layout-toggle-fullscreen))
+  (when id
+    (with-current-buffer (exwm--id->buffer id)
+      (if (exwm-layout--fullscreen-p)
+          (exwm-layout-unset-fullscreen id)
+        (exwm-layout-set-fullscreen id)))))
+
+(defun exwm-layout--other-buffer-predicate (buffer)
+  "Return non-nil when the BUFFER may be displayed in selected frame.
+
+Prevents EXWM-mode buffers already being displayed on some other window from
+being selected.
+
+Should be set as `buffer-predicate' frame parameter for all
+frames.  Used by `other-buffer'.
+
+When variable `exwm-layout--other-buffer-exclude-exwm-mode-buffers'
+is t EXWM buffers are never selected by `other-buffer'.
+
+When variable `exwm-layout--other-buffer-exclude-buffers' is a
+list of buffers, EXWM buffers belonging to that list are never
+selected by `other-buffer'."
+  (or (not (with-current-buffer buffer (derived-mode-p 'exwm-mode)))
+      (and (not exwm-layout--other-buffer-exclude-exwm-mode-buffers)
+           (not (memq buffer exwm-layout--other-buffer-exclude-buffers))
+           ;; Do not select if already shown in some window.
+           (not (get-buffer-window buffer t)))))
+
+(defun exwm-layout--set-client-list-stacking ()
+  "Set _NET_CLIENT_LIST_STACKING."
+  (exwm--log)
+  (let (id clients-floating clients clients-iconic clients-other)
+    (dolist (pair exwm--id-buffer-alist)
+      (setq id (car pair))
+      (with-current-buffer (cdr pair)
+        (if (eq exwm--frame exwm-workspace--current)
+            (if exwm--floating-frame
+                ;; A floating X window on the current workspace.
+                (setq clients-floating (cons id clients-floating))
+              (if (get-buffer-window (cdr pair) exwm-workspace--current)
+                  ;; A normal tilling X window on the current workspace.
+                  (setq clients (cons id clients))
+                ;; An iconic tilling X window on the current workspace.
+                (setq clients-iconic (cons id clients-iconic))))
+          ;; X window on other workspaces.
+          (setq clients-other (cons id clients-other)))))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_CLIENT_LIST_STACKING
+                       :window exwm--root
+                       :data (vconcat (append clients-other clients-iconic
+                                              clients clients-floating))))))
+
+(defun exwm-layout--refresh (&optional frame)
+  "Refresh layout."
+  ;; `window-size-change-functions' sets this argument while
+  ;; `window-configuration-change-hook' makes the frame selected.
+  (unless frame
+    (setq frame (selected-frame)))
+  (exwm--log "frame=%s" frame)
+  (if (not (exwm-workspace--workspace-p frame))
+      (if (frame-parameter frame 'exwm-outer-id)
+          (exwm-layout--refresh-floating frame)
+        (exwm-layout--refresh-other frame))
+    (exwm-layout--refresh-workspace frame)))
+
+(defun exwm-layout--refresh-floating (frame)
+  "Refresh floating frame FRAME."
+  (exwm--log "Refresh floating %s" frame)
+  (let ((window (frame-first-window frame)))
+    (with-current-buffer (window-buffer window)
+      (when (and (derived-mode-p 'exwm-mode)
+                 ;; It may be a buffer waiting to be killed.
+                 (exwm--id->buffer exwm--id))
+        (exwm--log "Refresh floating window #x%x" exwm--id)
+        (if (exwm-workspace--active-p exwm--frame)
+            (exwm-layout--show exwm--id window)
+          (exwm-layout--hide exwm--id))))))
+
+(defun exwm-layout--refresh-other (frame)
+  "Refresh client or nox frame FRAME."
+  ;; Other frames (e.g. terminal/graphical frame of emacsclient)
+  ;; We shall bury all `exwm-mode' buffers in this case
+  (exwm--log "Refresh other %s" frame)
+  (let ((windows (window-list frame 'nomini)) ;exclude minibuffer
+        (exwm-layout--other-buffer-exclude-exwm-mode-buffers t))
+    (dolist (window windows)
+      (with-current-buffer (window-buffer window)
+        (when (derived-mode-p 'exwm-mode)
+          (if (window-prev-buffers window)
+              (switch-to-prev-buffer window)
+            (switch-to-next-buffer window)))))))
+
+(defun exwm-layout--refresh-workspace (frame)
+  "Refresh workspace frame FRAME."
+  (exwm--log "Refresh workspace %s" frame)
+  ;; Workspaces other than the active one can also be refreshed (RandR)
+  (let (covered-buffers   ;EXWM-buffers covered by a new X window.
+        vacated-windows)  ;Windows previously displaying EXWM-buffers.
+    (dolist (pair exwm--id-buffer-alist)
+      (with-current-buffer (cdr pair)
+        (when (and (not exwm--floating-frame) ;exclude floating X windows
+                   (or exwm-layout-show-all-buffers
+                       ;; Exclude X windows on other workspaces
+                       (eq frame exwm--frame)))
+          (let (;; List of windows in current frame displaying the `exwm-mode'
+                ;; buffers.
+                (windows (get-buffer-window-list (current-buffer) 'nomini
+                                                 frame)))
+            (if (not windows)
+                (when (eq frame exwm--frame)
+                  ;; Hide it if it was being shown in this workspace.
+                  (exwm-layout--hide exwm--id))
+              (let ((window (car windows)))
+                (if (eq frame exwm--frame)
+                    ;; Show it if `frame' is active, hide otherwise.
+                    (if (exwm-workspace--active-p frame)
+                        (exwm-layout--show exwm--id window)
+                      (exwm-layout--hide exwm--id))
+                  ;; It was last shown in other workspace; move it here.
+                  (exwm-workspace-move-window frame exwm--id))
+                ;; Vacate any other windows (in any workspace) showing this
+                ;; `exwm-mode' buffer.
+                (setq vacated-windows
+                      (append vacated-windows (remove
+                                               window
+                                               (get-buffer-window-list
+                                                (current-buffer) 'nomini t))))
+                ;; Note any `exwm-mode' buffer is being covered by another
+                ;; `exwm-mode' buffer.  We want to avoid that `exwm-mode'
+                ;; buffer to be reappear in any of the vacated windows.
+                (let ((prev-buffer (car-safe
+                                    (car-safe (window-prev-buffers window)))))
+                  (and
+                   prev-buffer
+                   (with-current-buffer prev-buffer
+                     (derived-mode-p 'exwm-mode))
+                   (push prev-buffer covered-buffers)))))))))
+    ;; Set some sensible buffer to vacated windows.
+    (let ((exwm-layout--other-buffer-exclude-buffers covered-buffers))
+      (dolist (window vacated-windows)
+        (if (window-prev-buffers window)
+            (switch-to-prev-buffer window)
+          (switch-to-next-buffer window))))
+    ;; Make sure windows floating / on other workspaces are excluded
+    (let ((exwm-layout--other-buffer-exclude-exwm-mode-buffers t))
+      (dolist (window (window-list frame 'nomini))
+        (with-current-buffer (window-buffer window)
+          (when (and (derived-mode-p 'exwm-mode)
+                     (or exwm--floating-frame (not (eq frame exwm--frame))))
+            (if (window-prev-buffers window)
+                (switch-to-prev-buffer window)
+              (switch-to-next-buffer window))))))
+    (exwm-layout--set-client-list-stacking)
+    (xcb:flush exwm--connection)))
+
+(defun exwm-layout--on-minibuffer-setup ()
+  "Refresh layout when minibuffer grows."
+  (exwm--log)
+  (unless (exwm-workspace--client-p)
+    (exwm--defer 0 (lambda ()
+                     (when (< 1 (window-height (minibuffer-window)))
+                       (exwm-layout--refresh))))))
+
+(defun exwm-layout--on-echo-area-change (&optional dirty)
+  "Run when message arrives or in `echo-area-clear-hook' to refresh layout."
+  (when (and (current-message)
+             (not (exwm-workspace--client-p))
+             (or (cl-position ?\n (current-message))
+                 (> (length (current-message))
+                    (frame-width exwm-workspace--current))))
+    (exwm--log)
+    (if dirty
+        (exwm-layout--refresh)
+      (exwm--defer 0 #'exwm-layout--refresh))))
+
+;;;###autoload
+(defun exwm-layout-enlarge-window (delta &optional horizontal)
+  "Make the selected window DELTA pixels taller.
+
+If no argument is given, make the selected window one pixel taller.  If the
+optional argument HORIZONTAL is non-nil, make selected window DELTA pixels
+wider.  If DELTA is negative, shrink selected window by -DELTA pixels.
+
+Normal hints are checked and regarded if the selected window is displaying an
+`exwm-mode' buffer.  However, this may violate the normal hints set on other X
+windows."
+  (interactive "p")
+  (exwm--log)
+  (cond
+   ((zerop delta))                     ;no operation
+   ((window-minibuffer-p))             ;avoid resize minibuffer-window
+   ((not (and (derived-mode-p 'exwm-mode) exwm--floating-frame))
+    ;; Resize on tiling layout
+    (unless (= 0 (window-resizable nil delta horizontal nil t)) ;not resizable
+      (let ((window-resize-pixelwise t))
+        (window-resize nil delta horizontal nil t))))
+   ;; Resize on floating layout
+   (exwm--fixed-size)                   ;fixed size
+   (horizontal
+    (let* ((width (frame-pixel-width))
+           (edges (window-inside-pixel-edges))
+           (inner-width (- (elt edges 2) (elt edges 0)))
+           (margin (- width inner-width)))
+      (if (> delta 0)
+          (if (not exwm--normal-hints-max-width)
+              (cl-incf width delta)
+            (if (>= inner-width exwm--normal-hints-max-width)
+                (setq width nil)
+              (setq width (min (+ exwm--normal-hints-max-width margin)
+                               (+ width delta)))))
+        (if (not exwm--normal-hints-min-width)
+            (cl-incf width delta)
+          (if (<= inner-width exwm--normal-hints-min-width)
+              (setq width nil)
+            (setq width (max (+ exwm--normal-hints-min-width margin)
+                             (+ width delta))))))
+      (when (and width (> width 0))
+        (setf (slot-value exwm--geometry 'width) width)
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ConfigureWindow
+                           :window (frame-parameter exwm--floating-frame
+                                                    'exwm-outer-id)
+                           :value-mask xcb:ConfigWindow:Width
+                           :width width))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ConfigureWindow
+                           :window (frame-parameter exwm--floating-frame
+                                                    'exwm-container)
+                           :value-mask xcb:ConfigWindow:Width
+                           :width width))
+        (xcb:flush exwm--connection))))
+   (t
+    (let* ((height (+ (frame-pixel-height) exwm-workspace--frame-y-offset))
+           (edges (window-inside-pixel-edges))
+           (inner-height (- (elt edges 3) (elt edges 1)))
+           (margin (- height inner-height)))
+      (if (> delta 0)
+          (if (not exwm--normal-hints-max-height)
+              (cl-incf height delta)
+            (if (>= inner-height exwm--normal-hints-max-height)
+                (setq height nil)
+              (setq height (min (+ exwm--normal-hints-max-height margin)
+                                (+ height delta)))))
+        (if (not exwm--normal-hints-min-height)
+            (cl-incf height delta)
+          (if (<= inner-height exwm--normal-hints-min-height)
+              (setq height nil)
+            (setq height (max (+ exwm--normal-hints-min-height margin)
+                              (+ height delta))))))
+      (when (and height (> height 0))
+        (setf (slot-value exwm--geometry 'height) height)
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ConfigureWindow
+                           :window (frame-parameter exwm--floating-frame
+                                                    'exwm-outer-id)
+                           :value-mask xcb:ConfigWindow:Height
+                           :height height))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ConfigureWindow
+                           :window (frame-parameter exwm--floating-frame
+                                                    'exwm-container)
+                           :value-mask xcb:ConfigWindow:Height
+                           :height height))
+        (xcb:flush exwm--connection))))))
+
+;;;###autoload
+(defun exwm-layout-enlarge-window-horizontally (delta)
+  "Make the selected window DELTA pixels wider.
+
+See also `exwm-layout-enlarge-window'."
+  (interactive "p")
+  (exwm--log "%s" delta)
+  (exwm-layout-enlarge-window delta t))
+
+;;;###autoload
+(defun exwm-layout-shrink-window (delta)
+  "Make the selected window DELTA pixels lower.
+
+See also `exwm-layout-enlarge-window'."
+  (interactive "p")
+  (exwm--log "%s" delta)
+  (exwm-layout-enlarge-window (- delta)))
+
+;;;###autoload
+(defun exwm-layout-shrink-window-horizontally (delta)
+  "Make the selected window DELTA pixels narrower.
+
+See also `exwm-layout-enlarge-window'."
+  (interactive "p")
+  (exwm--log "%s" delta)
+  (exwm-layout-enlarge-window (- delta) t))
+
+;;;###autoload
+(defun exwm-layout-hide-mode-line ()
+  "Hide mode-line."
+  (interactive)
+  (exwm--log)
+  (when (and (derived-mode-p 'exwm-mode) mode-line-format)
+    (let (mode-line-height)
+      (when exwm--floating-frame
+        (setq mode-line-height (window-mode-line-height
+                                (frame-root-window exwm--floating-frame))))
+      (setq exwm--mode-line-format mode-line-format
+            mode-line-format nil)
+      (if (not exwm--floating-frame)
+          (exwm-layout--show exwm--id)
+        (set-frame-height exwm--floating-frame
+                          (- (frame-pixel-height exwm--floating-frame)
+                             mode-line-height)
+                          nil t)))))
+
+;;;###autoload
+(defun exwm-layout-show-mode-line ()
+  "Show mode-line."
+  (interactive)
+  (exwm--log)
+  (when (and (derived-mode-p 'exwm-mode) (not mode-line-format))
+    (setq mode-line-format exwm--mode-line-format
+          exwm--mode-line-format nil)
+    (if (not exwm--floating-frame)
+        (exwm-layout--show exwm--id)
+      (set-frame-height exwm--floating-frame
+                        (+ (frame-pixel-height exwm--floating-frame)
+                           (window-mode-line-height (frame-root-window
+                                                     exwm--floating-frame)))
+                        nil t)
+      (call-interactively #'exwm-input-grab-keyboard))
+    (force-mode-line-update)))
+
+;;;###autoload
+(defun exwm-layout-toggle-mode-line ()
+  "Toggle the display of mode-line."
+  (interactive)
+  (exwm--log)
+  (when (derived-mode-p 'exwm-mode)
+    (if mode-line-format
+        (exwm-layout-hide-mode-line)
+      (exwm-layout-show-mode-line))))
+
+(defun exwm-layout--init ()
+  "Initialize layout module."
+  ;; Auto refresh layout
+  (exwm--log)
+  (add-hook 'window-configuration-change-hook #'exwm-layout--refresh)
+  ;; The behavior of `window-configuration-change-hook' will be changed.
+  (when (fboundp 'window-pixel-width-before-size-change)
+    (add-hook 'window-size-change-functions #'exwm-layout--refresh))
+  (unless (exwm-workspace--minibuffer-own-frame-p)
+    ;; Refresh when minibuffer grows
+    (add-hook 'minibuffer-setup-hook #'exwm-layout--on-minibuffer-setup t)
+    (setq exwm-layout--timer
+          (run-with-idle-timer 0 t #'exwm-layout--on-echo-area-change t))
+    (add-hook 'echo-area-clear-hook #'exwm-layout--on-echo-area-change)))
+
+(defun exwm-layout--exit ()
+  "Exit the layout module."
+  (exwm--log)
+  (remove-hook 'window-configuration-change-hook #'exwm-layout--refresh)
+  (when (fboundp 'window-pixel-width-before-size-change)
+    (remove-hook 'window-size-change-functions #'exwm-layout--refresh))
+  (remove-hook 'minibuffer-setup-hook #'exwm-layout--on-minibuffer-setup)
+  (when exwm-layout--timer
+    (cancel-timer exwm-layout--timer)
+    (setq exwm-layout--timer nil))
+  (remove-hook 'echo-area-clear-hook #'exwm-layout--on-echo-area-change))
+
+
+
+(provide 'exwm-layout)
+
+;;; exwm-layout.el ends here
diff --git a/third_party/exwm/exwm-manage.el b/third_party/exwm/exwm-manage.el
new file mode 100644
index 0000000000..e940257fc9
--- /dev/null
+++ b/third_party/exwm/exwm-manage.el
@@ -0,0 +1,802 @@
+;;; exwm-manage.el --- Window Management Module for  -*- lexical-binding: t -*-
+;;;                    EXWM
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is the fundamental module of EXWM that deals with window management.
+
+;;; Code:
+
+(require 'exwm-core)
+
+(defgroup exwm-manage nil
+  "Manage."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-manage-finish-hook nil
+  "Normal hook run after a window is just managed, in the context of the
+corresponding buffer."
+  :type 'hook)
+
+(defcustom exwm-manage-force-tiling nil
+  "Non-nil to force managing all X windows in tiling layout.
+You can still make the X windows floating afterwards."
+  :type 'boolean)
+
+(defcustom exwm-manage-ping-timeout 3
+  "Seconds to wait before killing a client."
+  :type 'integer)
+
+(defcustom exwm-manage-configurations nil
+  "Per-application configurations.
+
+Configuration options allow to override various default behaviors of EXWM
+and only take effect when they are present.  Note for certain options
+specifying nil is not exactly the same as leaving them out.  Currently
+possible choices:
+* floating: Force floating (non-nil) or tiling (nil) on startup.
+* x/y/width/height: Override the initial geometry (floating X window only).
+* border-width: Override the border width (only visible when floating).
+* fullscreen: Force full screen (non-nil) on startup.
+* floating-mode-line: `mode-line-format' used when floating.
+* tiling-mode-line: `mode-line-format' used when tiling.
+* floating-header-line: `header-line-format' used when floating.
+* tiling-header-line: `header-line-format' used when tiling.
+* char-mode: Force char-mode (non-nil) on startup.
+* prefix-keys: `exwm-input-prefix-keys' local to this X window.
+* simulation-keys: `exwm-input-simulation-keys' local to this X window.
+* workspace: The initial workspace.
+* managed: Force to manage (non-nil) or not manage (nil) the X window.
+
+For each X window managed for the first time, matching criteria (sexps) are
+evaluated sequentially and the first configuration with a non-nil matching
+criterion would be applied.  Apart from generic forms, one would typically
+want to match against EXWM internal variables such as `exwm-title',
+`exwm-class-name' and `exwm-instance-name'."
+  :type '(alist :key-type (sexp :tag "Matching criterion" nil)
+                :value-type
+                (plist :tag "Configurations"
+                       :options
+                       (((const :tag "Floating" floating) boolean)
+                        ((const :tag "X" x) number)
+                        ((const :tag "Y" y) number)
+                        ((const :tag "Width" width) number)
+                        ((const :tag "Height" height) number)
+                        ((const :tag "Border width" border-width) integer)
+                        ((const :tag "Fullscreen" fullscreen) boolean)
+                        ((const :tag "Floating mode-line" floating-mode-line)
+                         sexp)
+                        ((const :tag "Tiling mode-line" tiling-mode-line) sexp)
+                        ((const :tag "Floating header-line"
+                                floating-header-line)
+                         sexp)
+                        ((const :tag "Tiling header-line" tiling-header-line)
+                         sexp)
+                        ((const :tag "Char-mode" char-mode) boolean)
+                        ((const :tag "Prefix keys" prefix-keys)
+                         (repeat key-sequence))
+                        ((const :tag "Simulation keys" simulation-keys)
+                         (alist :key-type (key-sequence :tag "From")
+                                :value-type (key-sequence :tag "To")))
+                        ((const :tag "Workspace" workspace) integer)
+                        ((const :tag "Managed" managed) boolean)
+                        ;; For forward compatibility.
+                        ((other) sexp))))
+  ;; TODO: This is admittedly ugly.  We'd be better off with an event type.
+  :get (lambda (symbol)
+         (mapcar (lambda (pair)
+                   (let* ((match (car pair))
+                          (config (cdr pair))
+                          (prefix-keys (plist-get config 'prefix-keys)))
+                     (when prefix-keys
+                       (setq config (copy-tree config)
+                             config (plist-put config 'prefix-keys
+                                               (mapcar (lambda (i)
+                                                         (if (sequencep i)
+                                                             i
+                                                           (vector i)))
+                                                       prefix-keys))))
+                     (cons match config)))
+                 (default-value symbol)))
+  :set (lambda (symbol value)
+         (set symbol
+              (mapcar (lambda (pair)
+                        (let* ((match (car pair))
+                               (config (cdr pair))
+                               (prefix-keys (plist-get config 'prefix-keys)))
+                          (when prefix-keys
+                            (setq config (copy-tree config)
+                                  config (plist-put config 'prefix-keys
+                                                    (mapcar (lambda (i)
+                                                              (if (sequencep i)
+                                                                  (aref i 0)
+                                                                i))
+                                                            prefix-keys))))
+                          (cons match config)))
+                      value))))
+
+;; FIXME: Make the following values as small as possible.
+(defconst exwm-manage--height-delta-min 5)
+(defconst exwm-manage--width-delta-min 5)
+
+;; The _MOTIF_WM_HINTS atom (see <Xm/MwmUtil.h> for more details)
+;; It's currently only used in 'exwm-manage' module
+(defvar exwm-manage--_MOTIF_WM_HINTS nil "_MOTIF_WM_HINTS atom.")
+
+(defvar exwm-manage--desktop nil "The desktop X window.")
+
+(defvar exwm-manage--frame-outer-id-list nil
+  "List of window-outer-id's of all frames.")
+
+(defvar exwm-manage--ping-lock nil
+  "Non-nil indicates EXWM is pinging a window.")
+
+(defvar exwm-input-prefix-keys)
+(defvar exwm-workspace--current)
+(defvar exwm-workspace--id-struts-alist)
+(defvar exwm-workspace--list)
+(defvar exwm-workspace--switch-history-outdated)
+(defvar exwm-workspace--workareas)
+(defvar exwm-workspace-current-index)
+(declare-function exwm--update-class "exwm.el" (id &optional force))
+(declare-function exwm--update-hints "exwm.el" (id &optional force))
+(declare-function exwm--update-normal-hints "exwm.el" (id &optional force))
+(declare-function exwm--update-protocols "exwm.el" (id &optional force))
+(declare-function exwm--update-struts "exwm.el" (id))
+(declare-function exwm--update-title "exwm.el" (id))
+(declare-function exwm--update-transient-for "exwm.el" (id &optional force))
+(declare-function exwm--update-desktop "exwm.el" (id &optional force))
+(declare-function exwm--update-window-type "exwm.el" (id &optional force))
+(declare-function exwm-floating--set-floating "exwm-floating.el" (id))
+(declare-function exwm-floating--unset-floating "exwm-floating.el" (id))
+(declare-function exwm-input-grab-keyboard "exwm-input.el")
+(declare-function exwm-input-set-local-simulation-keys "exwm-input.el")
+(declare-function exwm-layout--fullscreen-p "exwm-layout.el" ())
+(declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id))
+(declare-function exwm-workspace--position "exwm-workspace.el" (frame))
+(declare-function exwm-workspace--set-fullscreen "exwm-workspace.el" (frame))
+(declare-function exwm-workspace--update-struts "exwm-workspace.el" ())
+(declare-function exwm-workspace--update-workareas "exwm-workspace.el" ())
+
+(defun exwm-manage--update-geometry (id &optional force)
+  "Update window geometry."
+  (exwm--log "id=#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and exwm--geometry (not force))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:GetGeometry :drawable id))))
+        (setq exwm--geometry
+              (or reply
+                  ;; Provide a reasonable fallback value.
+                  (make-instance 'xcb:RECTANGLE
+                                 :x 0
+                                 :y 0
+                                 :width (/ (x-display-pixel-width) 2)
+                                 :height (/ (x-display-pixel-height) 2))))))))
+
+(defun exwm-manage--update-ewmh-state (id)
+  "Update _NET_WM_STATE."
+  (exwm--log "id=#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless exwm--ewmh-state
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:ewmh:get-_NET_WM_STATE
+                                      :window id))))
+        (when reply
+          (setq exwm--ewmh-state (append (slot-value reply 'value) nil)))))))
+
+(defun exwm-manage--update-mwm-hints (id &optional force)
+  "Update _MOTIF_WM_HINTS."
+  (exwm--log "id=#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and (not exwm--mwm-hints-decorations) (not force))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:-GetProperty
+                                      :window id
+                                      :property exwm-manage--_MOTIF_WM_HINTS
+                                      :type exwm-manage--_MOTIF_WM_HINTS
+                                      :long-length 5))))
+        (when reply
+          ;; Check MotifWmHints.decorations.
+          (with-slots (value) reply
+            (setq value (append value nil))
+            (when (and value
+                       ;; See <Xm/MwmUtil.h> for fields definitions.
+                       (/= 0 (logand
+                              (elt value 0) ;MotifWmHints.flags
+                              2))           ;MWM_HINTS_DECORATIONS
+                       (= 0
+                          (elt value 2))) ;MotifWmHints.decorations
+              (setq exwm--mwm-hints-decorations nil))))))))
+
+(defun exwm-manage--set-client-list ()
+  "Set _NET_CLIENT_LIST."
+  (exwm--log)
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_CLIENT_LIST
+                     :window exwm--root
+                     :data (vconcat (mapcar #'car exwm--id-buffer-alist)))))
+
+(cl-defun exwm-manage--get-configurations ()
+  "Retrieve configurations for this buffer."
+  (exwm--log)
+  (when (derived-mode-p 'exwm-mode)
+    (dolist (i exwm-manage-configurations)
+      (save-current-buffer
+        (when (with-demoted-errors "Problematic configuration: %S"
+                (eval (car i) t))
+          (cl-return-from exwm-manage--get-configurations (cdr i)))))))
+
+(defun exwm-manage--manage-window (id)
+  "Manage window ID."
+  (exwm--log "Try to manage #x%x" id)
+  (catch 'return
+    ;; Ensure it's alive
+    (when (xcb:+request-checked+request-check exwm--connection
+              (make-instance 'xcb:ChangeWindowAttributes
+                             :window id :value-mask xcb:CW:EventMask
+                             :event-mask (exwm--get-client-event-mask)))
+      (throw 'return 'dead))
+    ;; Add this X window to save-set.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ChangeSaveSet
+                       :mode xcb:SetMode:Insert
+                       :window id))
+    (with-current-buffer (generate-new-buffer "*EXWM*")
+      ;; Keep the oldest X window first.
+      (setq exwm--id-buffer-alist
+            (nconc exwm--id-buffer-alist `((,id . ,(current-buffer)))))
+      (exwm-mode)
+      (setq exwm--id id
+            exwm--frame exwm-workspace--current)
+      (exwm--update-window-type id)
+      (exwm--update-class id)
+      (exwm--update-transient-for id)
+      (exwm--update-normal-hints id)
+      (exwm--update-hints id)
+      (exwm-manage--update-geometry id)
+      (exwm-manage--update-mwm-hints id)
+      (exwm--update-title id)
+      (exwm--update-protocols id)
+      (setq exwm--configurations (exwm-manage--get-configurations))
+      ;; OverrideRedirect is not checked here.
+      (when (and
+             ;; The user has specified to manage it.
+             (not (plist-get exwm--configurations 'managed))
+             (or
+              ;; The user has specified not to manage it.
+              (plist-member exwm--configurations 'managed)
+              ;; This is not a type of X window we can manage.
+              (and exwm-window-type
+                   (not (cl-intersection
+                         exwm-window-type
+                         (list xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY
+                               xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG
+                               xcb:Atom:_NET_WM_WINDOW_TYPE_NORMAL))))
+              ;; Check the _MOTIF_WM_HINTS property to not manage floating X
+              ;; windows without decoration.
+              (and (not exwm--mwm-hints-decorations)
+                   (not exwm--hints-input)
+                   ;; Floating windows only
+                   (or exwm-transient-for exwm--fixed-size
+                       (memq xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY
+                             exwm-window-type)
+                       (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG
+                             exwm-window-type)))))
+        (exwm--log "No need to manage #x%x" id)
+        ;; Update struts.
+        (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK exwm-window-type)
+          (exwm--update-struts id))
+        ;; Remove all events
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ChangeWindowAttributes
+                           :window id :value-mask xcb:CW:EventMask
+                           :event-mask
+                           (if (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK
+                                     exwm-window-type)
+                               ;; Listen for PropertyChange (struts) and
+                               ;; UnmapNotify/DestroyNotify event of the dock.
+                               (exwm--get-client-event-mask)
+                             xcb:EventMask:NoEvent)))
+        ;; The window needs to be mapped
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:MapWindow :window id))
+        (with-slots (x y width height) exwm--geometry
+          ;; Center window of type _NET_WM_WINDOW_TYPE_SPLASH
+          (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_SPLASH exwm-window-type)
+            (let* ((workarea (elt exwm-workspace--workareas
+                                  (exwm-workspace--position exwm--frame)))
+                   (x* (aref workarea 0))
+                   (y* (aref workarea 1))
+                   (width* (aref workarea 2))
+                   (height* (aref workarea 3)))
+              (exwm--set-geometry id
+                                  (+ x* (/ (- width* width) 2))
+                                  (+ y* (/ (- height* height) 2))
+                                  nil
+                                  nil))))
+        ;; Check for desktop.
+        (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DESKTOP exwm-window-type)
+          ;; There should be only one desktop X window.
+          (setq exwm-manage--desktop id)
+          ;; Put it at bottom.
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:ConfigureWindow
+                             :window id
+                             :value-mask xcb:ConfigWindow:StackMode
+                             :stack-mode xcb:StackMode:Below)))
+        (xcb:flush exwm--connection)
+        (setq exwm--id-buffer-alist (assq-delete-all id exwm--id-buffer-alist))
+        (let ((kill-buffer-query-functions nil))
+          (kill-buffer (current-buffer)))
+        (throw 'return 'ignored))
+      (let ((index (plist-get exwm--configurations 'workspace)))
+        (when (and index (< index (length exwm-workspace--list)))
+          (setq exwm--frame (elt exwm-workspace--list index))))
+      ;; Manage the window
+      (exwm--log "Manage #x%x" id)
+      (xcb:+request exwm--connection    ;remove border
+          (make-instance 'xcb:ConfigureWindow
+                         :window id :value-mask xcb:ConfigWindow:BorderWidth
+                         :border-width 0))
+      (dolist (button       ;grab buttons to set focus / move / resize
+               (list xcb:ButtonIndex:1 xcb:ButtonIndex:2 xcb:ButtonIndex:3))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:GrabButton
+                           :owner-events 0 :grab-window id
+                           :event-mask xcb:EventMask:ButtonPress
+                           :pointer-mode xcb:GrabMode:Sync
+                           :keyboard-mode xcb:GrabMode:Async
+                           :confine-to xcb:Window:None :cursor xcb:Cursor:None
+                           :button button :modifiers xcb:ModMask:Any)))
+      (exwm-manage--set-client-list)
+      (xcb:flush exwm--connection)
+      (if (plist-member exwm--configurations 'floating)
+          ;; User has specified whether it should be floating.
+          (if (plist-get exwm--configurations 'floating)
+              (exwm-floating--set-floating id)
+            (with-selected-window (frame-selected-window exwm--frame)
+              (exwm-floating--unset-floating id)))
+        ;; Try to determine if it should be floating.
+        (if (and (not exwm-manage-force-tiling)
+                 (or exwm-transient-for exwm--fixed-size
+                     (memq xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY
+                           exwm-window-type)
+                     (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG
+                           exwm-window-type)))
+            (exwm-floating--set-floating id)
+          (with-selected-window (frame-selected-window exwm--frame)
+            (exwm-floating--unset-floating id))))
+      (if (plist-get exwm--configurations 'char-mode)
+          (exwm-input-release-keyboard id)
+        (exwm-input-grab-keyboard id))
+      (let ((simulation-keys (plist-get exwm--configurations 'simulation-keys))
+            (prefix-keys (plist-get exwm--configurations 'prefix-keys)))
+        (with-current-buffer (exwm--id->buffer id)
+          (when simulation-keys
+            (exwm-input-set-local-simulation-keys simulation-keys))
+          (when prefix-keys
+            (setq-local exwm-input-prefix-keys prefix-keys))))
+      (setq exwm-workspace--switch-history-outdated t)
+      (exwm--update-desktop id)
+      (exwm-manage--update-ewmh-state id)
+      (with-current-buffer (exwm--id->buffer id)
+        (when (or (plist-get exwm--configurations 'fullscreen)
+                  (exwm-layout--fullscreen-p))
+          (setq exwm--ewmh-state (delq xcb:Atom:_NET_WM_STATE_FULLSCREEN
+                                       exwm--ewmh-state))
+          (exwm-layout-set-fullscreen id))
+        (run-hooks 'exwm-manage-finish-hook)))))
+
+(defun exwm-manage--unmanage-window (id &optional withdraw-only)
+  "Unmanage window ID.
+
+If WITHDRAW-ONLY is non-nil, the X window will be properly placed back to the
+root window.  Set WITHDRAW-ONLY to 'quit if this functions is used when window
+manager is shutting down."
+  (let ((buffer (exwm--id->buffer id)))
+    (exwm--log "Unmanage #x%x (buffer: %s, widthdraw: %s)"
+               id buffer withdraw-only)
+    (setq exwm--id-buffer-alist (assq-delete-all id exwm--id-buffer-alist))
+    ;; Update workspaces when a dock is destroyed.
+    (when (and (null withdraw-only)
+               (assq id exwm-workspace--id-struts-alist))
+      (setq exwm-workspace--id-struts-alist
+            (assq-delete-all id exwm-workspace--id-struts-alist))
+      (exwm-workspace--update-struts)
+      (exwm-workspace--update-workareas)
+      (dolist (f exwm-workspace--list)
+        (exwm-workspace--set-fullscreen f)))
+    (when (buffer-live-p buffer)
+      (with-current-buffer buffer
+        ;; Unmap the X window.
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:UnmapWindow :window id))
+        ;;
+        (setq exwm-workspace--switch-history-outdated t)
+        ;;
+        (when withdraw-only
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:ChangeWindowAttributes
+                             :window id :value-mask xcb:CW:EventMask
+                             :event-mask xcb:EventMask:NoEvent))
+          ;; Delete WM_STATE property
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:DeleteProperty
+                             :window id :property xcb:Atom:WM_STATE))
+          (cond
+           ((eq withdraw-only 'quit)
+            ;; Remap the window when exiting.
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:MapWindow :window id)))
+           (t
+            ;; Remove _NET_WM_DESKTOP.
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:DeleteProperty
+                               :window id
+                               :property xcb:Atom:_NET_WM_DESKTOP)))))
+        (when exwm--floating-frame
+          ;; Unmap the floating frame before destroying its container.
+          (let ((window (frame-parameter exwm--floating-frame 'exwm-outer-id))
+                (container (frame-parameter exwm--floating-frame
+                                            'exwm-container)))
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:UnmapWindow :window window))
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:ReparentWindow
+                               :window window :parent exwm--root :x 0 :y 0))
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:DestroyWindow :window container))))
+        (when (exwm-layout--fullscreen-p)
+          (let ((window (get-buffer-window)))
+            (when window
+              (set-window-dedicated-p window nil))))
+        (exwm-manage--set-client-list)
+        (xcb:flush exwm--connection))
+      (let ((kill-buffer-func
+             (lambda (buffer)
+               (when (buffer-local-value 'exwm--floating-frame buffer)
+                 (select-window
+                  (frame-selected-window exwm-workspace--current)))
+               (with-current-buffer buffer
+                 (let ((kill-buffer-query-functions nil))
+                   (kill-buffer buffer))))))
+        (exwm--defer 0 kill-buffer-func buffer)
+        (when (active-minibuffer-window)
+          (exit-minibuffer))))))
+
+(defun exwm-manage--scan ()
+  "Search for existing windows and try to manage them."
+  (exwm--log)
+  (let* ((tree (xcb:+request-unchecked+reply exwm--connection
+                   (make-instance 'xcb:QueryTree
+                                  :window exwm--root)))
+         reply)
+    (dolist (i (slot-value tree 'children))
+      (setq reply (xcb:+request-unchecked+reply exwm--connection
+                      (make-instance 'xcb:GetWindowAttributes
+                                     :window i)))
+      ;; It's possible the X window has been destroyed.
+      (when reply
+        (with-slots (override-redirect map-state) reply
+          (when (and (= 0 override-redirect)
+                     (= xcb:MapState:Viewable map-state))
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:UnmapWindow
+                               :window i))
+            (xcb:flush exwm--connection)
+            (exwm-manage--manage-window i)))))))
+
+(defun exwm-manage--kill-buffer-query-function ()
+  "Run in `kill-buffer-query-functions'."
+  (exwm--log "id=#x%x; buffer=%s" exwm--id (current-buffer))
+  (catch 'return
+    (when (or (not exwm--id)
+              (xcb:+request-checked+request-check exwm--connection
+                  (make-instance 'xcb:ChangeWindowAttributes
+                                 :window exwm--id
+                                 :value-mask xcb:CW:EventMask
+                                 :event-mask (exwm--get-client-event-mask))))
+      ;; The X window is no longer alive so just close the buffer.
+      (when exwm--floating-frame
+        (let ((window (frame-parameter exwm--floating-frame 'exwm-outer-id))
+              (container (frame-parameter exwm--floating-frame
+                                          'exwm-container)))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:UnmapWindow :window window))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:ReparentWindow
+                             :window window
+                             :parent exwm--root
+                             :x 0 :y 0))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:DestroyWindow
+                             :window container))))
+      (xcb:flush exwm--connection)
+      (throw 'return t))
+    (unless (memq xcb:Atom:WM_DELETE_WINDOW exwm--protocols)
+      ;; The X window does not support WM_DELETE_WINDOW; destroy it.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:DestroyWindow :window exwm--id))
+      (xcb:flush exwm--connection)
+      ;; Wait for DestroyNotify event.
+      (throw 'return nil))
+    (let ((id exwm--id))
+      ;; Try to close the X window with WM_DELETE_WINDOW client message.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:icccm:SendEvent
+                         :destination id
+                         :event (xcb:marshal
+                                 (make-instance 'xcb:icccm:WM_DELETE_WINDOW
+                                                :window id)
+                                 exwm--connection)))
+      (xcb:flush exwm--connection)
+      ;;
+      (unless (memq xcb:Atom:_NET_WM_PING exwm--protocols)
+        ;; For X windows without _NET_WM_PING support, we'd better just
+        ;; wait for DestroyNotify events.
+        (throw 'return nil))
+      ;; Try to determine if the X window is dead with _NET_WM_PING.
+      (setq exwm-manage--ping-lock t)
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:SendEvent
+                         :propagate 0
+                         :destination id
+                         :event-mask xcb:EventMask:NoEvent
+                         :event (xcb:marshal
+                                 (make-instance 'xcb:ewmh:_NET_WM_PING
+                                                :window id
+                                                :timestamp 0
+                                                :client-window id)
+                                 exwm--connection)))
+      (xcb:flush exwm--connection)
+      (with-timeout (exwm-manage-ping-timeout
+                     (if (y-or-n-p (format "'%s' is not responding.  \
+Would you like to kill it? "
+                                              (buffer-name)))
+                         (progn (exwm-manage--kill-client id)
+                                ;; Kill the unresponsive X window and
+                                ;; wait for DestroyNotify event.
+                                (throw 'return nil))
+                       ;; Give up.
+                       (throw 'return nil)))
+        (while (and exwm-manage--ping-lock
+                    (exwm--id->buffer id)) ;may have been destroyed.
+          (accept-process-output nil 0.1))
+        ;; Give up.
+        (throw 'return nil)))))
+
+(defun exwm-manage--kill-client (&optional id)
+  "Kill an X client."
+  (unless id (setq id (exwm--buffer->id (current-buffer))))
+  (exwm--log "id=#x%x" id)
+  (let* ((response (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:ewmh:get-_NET_WM_PID :window id)))
+         (pid (and response (slot-value response 'value)))
+         (request (make-instance 'xcb:KillClient :resource id)))
+    (if (not pid)
+        (xcb:+request exwm--connection request)
+      ;; What if the PID is fake/wrong?
+      (signal-process pid 'SIGKILL)
+      ;; Ensure it's dead
+      (run-with-timer exwm-manage-ping-timeout nil
+                      (lambda ()
+                        (xcb:+request exwm--connection request))))
+    (xcb:flush exwm--connection)))
+
+(defun exwm-manage--add-frame (frame)
+  "Run in `after-make-frame-functions'."
+  (exwm--log "frame=%s" frame)
+  (when (display-graphic-p frame)
+    (push (string-to-number (frame-parameter frame 'outer-window-id))
+          exwm-manage--frame-outer-id-list)))
+
+(defun exwm-manage--remove-frame (frame)
+  "Run in `delete-frame-functions'."
+  (exwm--log "frame=%s" frame)
+  (when (display-graphic-p frame)
+    (setq exwm-manage--frame-outer-id-list
+          (delq (string-to-number (frame-parameter frame 'outer-window-id))
+                exwm-manage--frame-outer-id-list))))
+
+(defun exwm-manage--on-ConfigureRequest (data _synthetic)
+  "Handle ConfigureRequest event."
+  (exwm--log)
+  (let ((obj (make-instance 'xcb:ConfigureRequest))
+        buffer edges width-delta height-delta)
+    (xcb:unmarshal obj data)
+    (with-slots (window x y width height
+                        border-width sibling stack-mode value-mask)
+        obj
+      (exwm--log "#x%x (#x%x) @%dx%d%+d%+d; \
+border-width: %d; sibling: #x%x; stack-mode: %d"
+                 window value-mask width height x y
+                 border-width sibling stack-mode)
+      (if (and (setq buffer (exwm--id->buffer window))
+               (with-current-buffer buffer
+                 (or (exwm-layout--fullscreen-p)
+                     ;; Make sure it's a floating X window wanting to resize
+                     ;; itself.
+                     (or (not exwm--floating-frame)
+                         (progn
+                           (setq edges
+                                 (window-inside-pixel-edges
+                                  (get-buffer-window buffer t))
+                                 width-delta (- width (- (elt edges 2)
+                                                         (elt edges 0)))
+                                 height-delta (- height (- (elt edges 3)
+                                                           (elt edges 1))))
+                           ;; We cannot do resizing precisely for now.
+                           (and (if (= 0 (logand value-mask
+                                                 xcb:ConfigWindow:Width))
+                                    t
+                                  (< (abs width-delta)
+                                     exwm-manage--width-delta-min))
+                                (if (= 0 (logand value-mask
+                                                 xcb:ConfigWindow:Height))
+                                    t
+                                  (< (abs height-delta)
+                                     exwm-manage--height-delta-min))))))))
+          ;; Send client message for managed windows
+          (with-current-buffer buffer
+            (setq edges
+                  (if (exwm-layout--fullscreen-p)
+                      (with-slots (x y width height)
+                          (exwm-workspace--get-geometry exwm--frame)
+                        (list x y width height))
+                    (window-inside-absolute-pixel-edges
+                     (get-buffer-window buffer t))))
+            (exwm--log "Reply with ConfigureNotify (edges): %s" edges)
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:SendEvent
+                               :propagate 0 :destination window
+                               :event-mask xcb:EventMask:StructureNotify
+                               :event (xcb:marshal
+                                       (make-instance
+                                        'xcb:ConfigureNotify
+                                        :event window :window window
+                                        :above-sibling xcb:Window:None
+                                        :x (elt edges 0) :y (elt edges 1)
+                                        :width (- (elt edges 2) (elt edges 0))
+                                        :height (- (elt edges 3) (elt edges 1))
+                                        :border-width 0 :override-redirect 0)
+                                       exwm--connection))))
+        (if buffer
+            (with-current-buffer buffer
+              (exwm--log "ConfigureWindow (resize floating X window)")
+              (exwm--set-geometry (frame-parameter exwm--floating-frame
+                                                   'exwm-outer-id)
+                                  nil
+                                  nil
+                                  (+ (frame-pixel-width exwm--floating-frame)
+                                     width-delta)
+                                  (+ (frame-pixel-height exwm--floating-frame)
+                                     height-delta)))
+          (exwm--log "ConfigureWindow (preserve geometry)")
+          ;; Configure the unmanaged window.
+          ;; But Emacs frames should be excluded.  Generally we don't
+          ;; receive ConfigureRequest events from Emacs frames since we
+          ;; have set OverrideRedirect on them, but this is not true for
+          ;; Lucid build (as of 25.1).
+          (unless (memq window exwm-manage--frame-outer-id-list)
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:ConfigureWindow
+                               :window window
+                               :value-mask value-mask
+                               :x x :y y :width width :height height
+                               :border-width border-width
+                               :sibling sibling
+                               :stack-mode stack-mode)))))))
+  (xcb:flush exwm--connection))
+
+(defun exwm-manage--on-MapRequest (data _synthetic)
+  "Handle MapRequest event."
+  (let ((obj (make-instance 'xcb:MapRequest)))
+    (xcb:unmarshal obj data)
+    (with-slots (parent window) obj
+      (exwm--log "id=#x%x parent=#x%x" window parent)
+      (if (assoc window exwm--id-buffer-alist)
+          (with-current-buffer (exwm--id->buffer window)
+            (if (exwm-layout--iconic-state-p)
+                ;; State change: iconic => normal.
+                (when (eq exwm--frame exwm-workspace--current)
+                  (pop-to-buffer-same-window (current-buffer)))
+              (exwm--log "#x%x is already managed" window)))
+        (if (/= exwm--root parent)
+            (progn (xcb:+request exwm--connection
+                       (make-instance 'xcb:MapWindow :window window))
+                   (xcb:flush exwm--connection))
+          (exwm--log "#x%x" window)
+          (exwm-manage--manage-window window))))))
+
+(defun exwm-manage--on-UnmapNotify (data _synthetic)
+  "Handle UnmapNotify event."
+  (let ((obj (make-instance 'xcb:UnmapNotify)))
+    (xcb:unmarshal obj data)
+    (with-slots (window) obj
+      (exwm--log "id=#x%x" window)
+      (exwm-manage--unmanage-window window t))))
+
+(defun exwm-manage--on-MapNotify (data _synthetic)
+  "Handle MapNotify event."
+  (let ((obj (make-instance 'xcb:MapNotify)))
+    (xcb:unmarshal obj data)
+    (with-slots (window) obj
+      (when (assoc window exwm--id-buffer-alist)
+        (exwm--log "id=#x%x" window)
+        ;; With this we ensure that a "window hierarchy change" happens after
+        ;; mapping the window, as some servers (XQuartz) do not generate it.
+        (with-current-buffer (exwm--id->buffer window)
+          (if exwm--floating-frame
+              (xcb:+request exwm--connection
+                  (make-instance 'xcb:ConfigureWindow
+                                 :window window
+                                 :value-mask xcb:ConfigWindow:StackMode
+                                 :stack-mode xcb:StackMode:Above))
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:ConfigureWindow
+                               :window window
+                               :value-mask (logior xcb:ConfigWindow:Sibling
+                                                   xcb:ConfigWindow:StackMode)
+                               :sibling exwm--guide-window
+                               :stack-mode xcb:StackMode:Above))))
+        (xcb:flush exwm--connection)))))
+
+(defun exwm-manage--on-DestroyNotify (data synthetic)
+  "Handle DestroyNotify event."
+  (unless synthetic
+    (exwm--log)
+    (let ((obj (make-instance 'xcb:DestroyNotify)))
+      (xcb:unmarshal obj data)
+      (exwm--log "#x%x" (slot-value obj 'window))
+      (exwm-manage--unmanage-window (slot-value obj 'window)))))
+
+(defun exwm-manage--init ()
+  "Initialize manage module."
+  ;; Intern _MOTIF_WM_HINTS
+  (exwm--log)
+  (setq exwm-manage--_MOTIF_WM_HINTS (exwm--intern-atom "_MOTIF_WM_HINTS"))
+  (add-hook 'after-make-frame-functions #'exwm-manage--add-frame)
+  (add-hook 'delete-frame-functions #'exwm-manage--remove-frame)
+  (xcb:+event exwm--connection 'xcb:ConfigureRequest
+              #'exwm-manage--on-ConfigureRequest)
+  (xcb:+event exwm--connection 'xcb:MapRequest #'exwm-manage--on-MapRequest)
+  (xcb:+event exwm--connection 'xcb:UnmapNotify #'exwm-manage--on-UnmapNotify)
+  (xcb:+event exwm--connection 'xcb:MapNotify #'exwm-manage--on-MapNotify)
+  (xcb:+event exwm--connection 'xcb:DestroyNotify
+              #'exwm-manage--on-DestroyNotify))
+
+(defun exwm-manage--exit ()
+  "Exit the manage module."
+  (exwm--log)
+  (dolist (pair exwm--id-buffer-alist)
+    (exwm-manage--unmanage-window (car pair) 'quit))
+  (remove-hook 'after-make-frame-functions #'exwm-manage--add-frame)
+  (remove-hook 'delete-frame-functions #'exwm-manage--remove-frame)
+  (setq exwm-manage--_MOTIF_WM_HINTS nil))
+
+
+
+(provide 'exwm-manage)
+
+;;; exwm-manage.el ends here
diff --git a/third_party/exwm/exwm-randr.el b/third_party/exwm/exwm-randr.el
new file mode 100644
index 0000000000..68bfdd70e9
--- /dev/null
+++ b/third_party/exwm/exwm-randr.el
@@ -0,0 +1,375 @@
+;;; exwm-randr.el --- RandR Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module adds RandR support for EXWM.  Currently it requires external
+;; tools such as xrandr(1) to properly configure RandR first.  This
+;; dependency may be removed in the future, but more work is needed before
+;; that.
+
+;; To use this module, load, enable it and configure
+;; `exwm-randr-workspace-monitor-plist' and `exwm-randr-screen-change-hook'
+;; as follows:
+;;
+;;   (require 'exwm-randr)
+;;   (setq exwm-randr-workspace-monitor-plist '(0 "VGA1"))
+;;   (add-hook 'exwm-randr-screen-change-hook
+;;             (lambda ()
+;;               (start-process-shell-command
+;;                "xrandr" nil "xrandr --output VGA1 --left-of LVDS1 --auto")))
+;;   (exwm-randr-enable)
+;;
+;; With above lines, workspace 0 should be assigned to the output named "VGA1",
+;; staying at the left of other workspaces on the output "LVDS1".  Please refer
+;; to xrandr(1) for the configuration of RandR.
+
+;; References:
+;; + RandR (http://www.x.org/archive/X11R7.7/doc/randrproto/randrproto.txt)
+
+;;; Code:
+
+(require 'xcb-randr)
+
+(require 'exwm-core)
+(require 'exwm-workspace)
+
+(defgroup exwm-randr nil
+  "RandR."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-randr-refresh-hook nil
+  "Normal hook run when the RandR module just refreshed."
+  :type 'hook)
+
+(defcustom exwm-randr-screen-change-hook nil
+  "Normal hook run when screen changes."
+  :type 'hook)
+
+(defcustom exwm-randr-workspace-monitor-plist nil
+  "Plist mapping workspaces to monitors.
+
+In RandR 1.5 a monitor is a rectangle region decoupled from the physical
+size of screens, and can be identified with `xrandr --listmonitors' (name of
+the primary monitor is prefixed with an `*').  When no monitor is created it
+automatically fallback to RandR 1.2 output which represents the physical
+screen size.  RandR 1.5 monitors can be created with `xrandr --setmonitor'.
+For example, to split an output (`LVDS-1') of size 1280x800 into two
+side-by-side monitors one could invoke (the digits after `/' are size in mm)
+
+    xrandr --setmonitor *LVDS-1-L 640/135x800/163+0+0 LVDS-1
+    xrandr --setmonitor LVDS-1-R 640/135x800/163+640+0 none
+
+If a monitor is not active, the workspaces mapped to it are displayed on the
+primary monitor until it becomes active (if ever).  Unspecified workspaces
+are all mapped to the primary monitor.  For example, with the following
+setting workspace other than 1 and 3 would always be displayed on the
+primary monitor where workspace 1 and 3 would be displayed on their
+corresponding monitors whenever the monitors are active.
+
+  \\='(1 \"HDMI-1\" 3 \"DP-1\")"
+  :type '(plist :key-type integer :value-type string))
+
+(with-no-warnings
+  (define-obsolete-variable-alias 'exwm-randr-workspace-output-plist
+    'exwm-randr-workspace-monitor-plist "27.1"))
+
+(defvar exwm-randr--last-timestamp 0 "Used for debouncing events.")
+
+(defvar exwm-randr--prev-screen-change-seqnum nil
+  "The most recent ScreenChangeNotify sequence number.")
+
+(defvar exwm-randr--compatibility-mode nil
+  "Non-nil when the server does not support RandR 1.5 protocol.")
+
+(defun exwm-randr--get-monitors ()
+  "Get RandR 1.5 monitors."
+  (exwm--log)
+  (let (monitor-name geometry monitor-geometry-alist primary-monitor)
+    (with-slots (timestamp monitors)
+        (xcb:+request-unchecked+reply exwm--connection
+            (make-instance 'xcb:randr:GetMonitors
+                           :window exwm--root
+                           :get-active 1))
+      (when (> timestamp exwm-randr--last-timestamp)
+        (setq exwm-randr--last-timestamp timestamp))
+      (dolist (monitor monitors)
+        (with-slots (name primary x y width height) monitor
+          (setq monitor-name (x-get-atom-name name)
+                geometry (make-instance 'xcb:RECTANGLE
+                                        :x x
+                                        :y y
+                                        :width width
+                                        :height height)
+                monitor-geometry-alist (cons (cons monitor-name geometry)
+                                             monitor-geometry-alist))
+          (exwm--log "%s: %sx%s+%s+%s" monitor-name x y width height)
+          ;; Save primary monitor when available (fallback to the first one).
+          (when (or (/= 0 primary)
+                    (not primary-monitor))
+            (setq primary-monitor monitor-name)))))
+    (exwm--log "Primary monitor: %s" primary-monitor)
+    (list primary-monitor monitor-geometry-alist
+          (exwm-randr--get-monitor-alias primary-monitor
+                                         monitor-geometry-alist))))
+
+(defun exwm-randr--get-outputs ()
+  "Get RandR 1.2 outputs.
+
+Only used when RandR 1.5 is not supported by the server."
+  (exwm--log)
+  (let (output-name geometry output-geometry-alist primary-output)
+    (with-slots (config-timestamp outputs)
+        (xcb:+request-unchecked+reply exwm--connection
+            (make-instance 'xcb:randr:GetScreenResourcesCurrent
+                           :window exwm--root))
+      (when (> config-timestamp exwm-randr--last-timestamp)
+        (setq exwm-randr--last-timestamp config-timestamp))
+      (dolist (output outputs)
+        (with-slots (crtc connection name)
+            (xcb:+request-unchecked+reply exwm--connection
+                (make-instance 'xcb:randr:GetOutputInfo
+                               :output output
+                               :config-timestamp config-timestamp))
+          (when (and (= connection xcb:randr:Connection:Connected)
+                     (/= crtc 0))
+            (with-slots (x y width height)
+                (xcb:+request-unchecked+reply exwm--connection
+                    (make-instance 'xcb:randr:GetCrtcInfo
+                                   :crtc crtc
+                                   :config-timestamp config-timestamp))
+              (setq output-name (decode-coding-string
+                                 (apply #'unibyte-string name) 'utf-8)
+                    geometry (make-instance 'xcb:RECTANGLE
+                                            :x x
+                                            :y y
+                                            :width width
+                                            :height height)
+                    output-geometry-alist (cons (cons output-name geometry)
+                                                output-geometry-alist))
+              (exwm--log "%s: %sx%s+%s+%s" output-name x y width height)
+              ;; The primary output is the first one.
+              (unless primary-output
+                (setq primary-output output-name)))))))
+    (exwm--log "Primary output: %s" primary-output)
+    (list primary-output output-geometry-alist
+          (exwm-randr--get-monitor-alias primary-output
+                                         output-geometry-alist))))
+
+(defun exwm-randr--get-monitor-alias (primary-monitor monitor-geometry-alist)
+  "Generate monitor aliases using PRIMARY-MONITOR MONITOR-GEOMETRY-ALIST.
+
+In a mirroring setup some monitors overlap and should be treated as one."
+  (let (monitor-position-alist monitor-alias-alist monitor-name geometry)
+    (setq monitor-position-alist (with-slots (x y)
+                                     (cdr (assoc primary-monitor
+                                                 monitor-geometry-alist))
+                                   (list (cons primary-monitor (vector x y)))))
+    (setq monitor-alias-alist (list (cons primary-monitor primary-monitor)))
+    (dolist (pair monitor-geometry-alist)
+      (setq monitor-name (car pair)
+            geometry (cdr pair))
+      (unless (assoc monitor-name monitor-alias-alist)
+        (let* ((position (vector (slot-value geometry 'x)
+                                 (slot-value geometry 'y)))
+               (alias (car (rassoc position monitor-position-alist))))
+          (if alias
+              (setq monitor-alias-alist (cons (cons monitor-name alias)
+                                              monitor-alias-alist))
+            (setq monitor-position-alist (cons (cons monitor-name position)
+                                               monitor-position-alist)
+                  monitor-alias-alist (cons (cons monitor-name monitor-name)
+                                            monitor-alias-alist))))))
+    monitor-alias-alist))
+
+;;;###autoload
+(defun exwm-randr-refresh ()
+  "Refresh workspaces according to the updated RandR info."
+  (interactive)
+  (exwm--log)
+  (let* ((result (if exwm-randr--compatibility-mode
+                     (exwm-randr--get-outputs)
+                   (exwm-randr--get-monitors)))
+         (primary-monitor (elt result 0))
+         (monitor-geometry-alist (elt result 1))
+         (monitor-alias-alist (elt result 2))
+         container-monitor-alist container-frame-alist)
+    (when (and primary-monitor monitor-geometry-alist)
+      (when exwm-workspace--fullscreen-frame-count
+        ;; Not all workspaces are fullscreen; reset this counter.
+        (setq exwm-workspace--fullscreen-frame-count 0))
+      (dotimes (i (exwm-workspace--count))
+        (let* ((monitor (plist-get exwm-randr-workspace-monitor-plist i))
+               (geometry (cdr (assoc monitor monitor-geometry-alist)))
+               (frame (elt exwm-workspace--list i))
+               (container (frame-parameter frame 'exwm-container)))
+          (if geometry
+              ;; Unify monitor names in case it's a mirroring setup.
+              (setq monitor (cdr (assoc monitor monitor-alias-alist)))
+            ;; Missing monitors fallback to the primary one.
+            (setq monitor primary-monitor
+                  geometry (cdr (assoc primary-monitor
+                                       monitor-geometry-alist))))
+          (setq container-monitor-alist (nconc
+                                         `((,container . ,(intern monitor)))
+                                         container-monitor-alist)
+                container-frame-alist (nconc `((,container . ,frame))
+                                             container-frame-alist))
+          (set-frame-parameter frame 'exwm-randr-monitor monitor)
+          (set-frame-parameter frame 'exwm-geometry geometry)))
+      ;; Update workareas.
+      (exwm-workspace--update-workareas)
+      ;; Resize workspace.
+      (dolist (f exwm-workspace--list)
+        (exwm-workspace--set-fullscreen f))
+      (xcb:flush exwm--connection)
+      ;; Raise the minibuffer if it's active.
+      (when (and (active-minibuffer-window)
+                 (exwm-workspace--minibuffer-own-frame-p))
+        (exwm-workspace--show-minibuffer))
+      ;; Set _NET_DESKTOP_GEOMETRY.
+      (exwm-workspace--set-desktop-geometry)
+      ;; Update active/inactive workspaces.
+      (dolist (w exwm-workspace--list)
+        (exwm-workspace--set-active w nil))
+      ;; Mark the workspace on the top of each monitor as active.
+      (dolist (xwin
+               (reverse
+                (slot-value (xcb:+request-unchecked+reply exwm--connection
+                                (make-instance 'xcb:QueryTree
+                                               :window exwm--root))
+                            'children)))
+        (let ((monitor (cdr (assq xwin container-monitor-alist))))
+          (when monitor
+            (setq container-monitor-alist
+                  (rassq-delete-all monitor container-monitor-alist))
+            (exwm-workspace--set-active (cdr (assq xwin container-frame-alist))
+                                        t))))
+      (xcb:flush exwm--connection)
+      (run-hooks 'exwm-randr-refresh-hook))))
+
+(define-obsolete-function-alias 'exwm-randr--refresh #'exwm-randr-refresh
+  "27.1")
+
+(defun exwm-randr--on-ScreenChangeNotify (data _synthetic)
+  "Handle `ScreenChangeNotify' event.
+
+Run `exwm-randr-screen-change-hook' (usually user scripts to configure RandR)."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:randr:ScreenChangeNotify)))
+    (xcb:unmarshal evt data)
+    (let ((seqnum (slot-value evt '~sequence)))
+      (unless (equal seqnum exwm-randr--prev-screen-change-seqnum)
+        (setq exwm-randr--prev-screen-change-seqnum seqnum)
+        (run-hooks 'exwm-randr-screen-change-hook)))))
+
+(defun exwm-randr--on-Notify (data _synthetic)
+  "Handle `CrtcChangeNotify' and `OutputChangeNotify' events.
+
+Refresh when any CRTC/output changes."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:randr:Notify))
+        notify)
+    (xcb:unmarshal evt data)
+    (with-slots (subCode u) evt
+      (cl-case subCode
+        (xcb:randr:Notify:CrtcChange
+         (setq notify (slot-value u 'cc)))
+        (xcb:randr:Notify:OutputChange
+         (setq notify (slot-value u 'oc))))
+      (when notify
+        (with-slots (timestamp) notify
+          (when (> timestamp exwm-randr--last-timestamp)
+            (exwm-randr-refresh)
+            (setq exwm-randr--last-timestamp timestamp)))))))
+
+(defun exwm-randr--on-ConfigureNotify (data _synthetic)
+  "Handle `ConfigureNotify' event.
+
+Refresh when any RandR 1.5 monitor changes."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:ConfigureNotify)))
+    (xcb:unmarshal evt data)
+    (with-slots (window) evt
+      (when (eq window exwm--root)
+        (exwm-randr-refresh)))))
+
+(defun exwm-randr--init ()
+  "Initialize RandR extension and EXWM RandR module."
+  (exwm--log)
+  (when (= 0 (slot-value (xcb:get-extension-data exwm--connection 'xcb:randr)
+                         'present))
+    (error "[EXWM] RandR extension is not supported by the server"))
+  (with-slots (major-version minor-version)
+      (xcb:+request-unchecked+reply exwm--connection
+          (make-instance 'xcb:randr:QueryVersion
+                         :major-version 1 :minor-version 5))
+    (cond ((and (= major-version 1) (= minor-version 5))
+           (setq exwm-randr--compatibility-mode nil))
+          ((and (= major-version 1) (>= minor-version 2))
+           (setq exwm-randr--compatibility-mode t))
+          (t
+           (error "[EXWM] The server only support RandR version up to %d.%d"
+                  major-version minor-version)))
+    ;; External monitor(s) may already be connected.
+    (run-hooks 'exwm-randr-screen-change-hook)
+    (exwm-randr-refresh)
+    ;; Listen for `ScreenChangeNotify' to notify external tools to
+    ;; configure RandR and `CrtcChangeNotify/OutputChangeNotify' to
+    ;; refresh the workspace layout.
+    (xcb:+event exwm--connection 'xcb:randr:ScreenChangeNotify
+                #'exwm-randr--on-ScreenChangeNotify)
+    (xcb:+event exwm--connection 'xcb:randr:Notify
+                #'exwm-randr--on-Notify)
+    (xcb:+event exwm--connection 'xcb:ConfigureNotify
+                #'exwm-randr--on-ConfigureNotify)
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:randr:SelectInput
+                       :window exwm--root
+                       :enable (logior
+                                xcb:randr:NotifyMask:ScreenChange
+                                xcb:randr:NotifyMask:CrtcChange
+                                xcb:randr:NotifyMask:OutputChange)))
+    (xcb:flush exwm--connection)
+    (add-hook 'exwm-workspace-list-change-hook #'exwm-randr-refresh))
+  ;; Prevent frame parameters introduced by this module from being
+  ;; saved/restored.
+  (dolist (i '(exwm-randr-monitor))
+    (unless (assq i frameset-filter-alist)
+      (push (cons i :never) frameset-filter-alist))))
+
+(defun exwm-randr--exit ()
+  "Exit the RandR module."
+  (exwm--log)
+  (remove-hook 'exwm-workspace-list-change-hook #'exwm-randr-refresh))
+
+(defun exwm-randr-enable ()
+  "Enable RandR support for EXWM."
+  (exwm--log)
+  (add-hook 'exwm-init-hook #'exwm-randr--init)
+  (add-hook 'exwm-exit-hook #'exwm-randr--exit))
+
+
+
+(provide 'exwm-randr)
+
+;;; exwm-randr.el ends here
diff --git a/third_party/exwm/exwm-systemtray.el b/third_party/exwm/exwm-systemtray.el
new file mode 100644
index 0000000000..43b3e1eaef
--- /dev/null
+++ b/third_party/exwm/exwm-systemtray.el
@@ -0,0 +1,587 @@
+;;; exwm-systemtray.el --- System Tray Module for  -*- lexical-binding: t -*-
+;;;                        EXWM
+
+;; Copyright (C) 2016-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module adds system tray support for EXWM.
+
+;; To use this module, load and enable it as follows:
+;;   (require 'exwm-systemtray)
+;;   (exwm-systemtray-enable)
+
+;;; Code:
+
+(require 'xcb-icccm)
+(require 'xcb-xembed)
+(require 'xcb-systemtray)
+
+(require 'exwm-core)
+(require 'exwm-workspace)
+
+(defclass exwm-systemtray--icon ()
+  ((width :initarg :width)
+   (height :initarg :height)
+   (visible :initarg :visible))
+  :documentation "Attributes of a system tray icon.")
+
+(defclass xcb:systemtray:-ClientMessage
+  (xcb:icccm:--ClientMessage xcb:ClientMessage)
+  ((format :initform 32)
+   (type :initform xcb:Atom:MANAGER)
+   (time :initarg :time :type xcb:TIMESTAMP)      ;new slot
+   (selection :initarg :selection :type xcb:ATOM) ;new slot
+   (owner :initarg :owner :type xcb:WINDOW))      ;new slot
+  :documentation "A systemtray client message.")
+
+(defgroup exwm-systemtray nil
+  "System tray."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-systemtray-height nil
+  "System tray height.
+
+You shall use the default value if using auto-hide minibuffer."
+  :type 'integer)
+
+(defcustom exwm-systemtray-icon-gap 2
+  "Gap between icons."
+  :type 'integer)
+
+(defvar exwm-systemtray--embedder-window nil "The embedder window.")
+
+(defcustom exwm-systemtray-background-color nil
+  "Background color of systemtray.
+
+This should be a color, or nil for transparent background."
+  :type '(choice (const :tag "Transparent" nil)
+                 (color))
+  :initialize #'custom-initialize-default
+  :set (lambda (symbol value)
+         (set-default symbol value)
+         ;; Change the background color for embedder.
+         (when (and exwm--connection
+                    exwm-systemtray--embedder-window)
+           (let ((background-pixel (exwm--color->pixel value)))
+             (xcb:+request exwm--connection
+                 (make-instance 'xcb:ChangeWindowAttributes
+                                :window exwm-systemtray--embedder-window
+                                :value-mask (logior xcb:CW:BackPixmap
+                                                    (if background-pixel
+                                                        xcb:CW:BackPixel 0))
+                                :background-pixmap
+                                xcb:BackPixmap:ParentRelative
+                                :background-pixel background-pixel))
+             ;; Unmap & map to take effect immediately.
+             (xcb:+request exwm--connection
+                 (make-instance 'xcb:UnmapWindow
+                                :window exwm-systemtray--embedder-window))
+             (xcb:+request exwm--connection
+                 (make-instance 'xcb:MapWindow
+                                :window exwm-systemtray--embedder-window))
+             (xcb:flush exwm--connection)))))
+
+;; GTK icons require at least 16 pixels to show normally.
+(defconst exwm-systemtray--icon-min-size 16 "Minimum icon size.")
+
+(defvar exwm-systemtray--connection nil "The X connection.")
+
+(defvar exwm-systemtray--list nil "The icon list.")
+
+(defvar exwm-systemtray--selection-owner-window nil
+  "The selection owner window.")
+
+(defvar xcb:Atom:_NET_SYSTEM_TRAY_S0)
+
+(defun exwm-systemtray--embed (icon)
+  "Embed an icon."
+  (exwm--log "Try to embed #x%x" icon)
+  (let ((info (xcb:+request-unchecked+reply exwm-systemtray--connection
+                  (make-instance 'xcb:xembed:get-_XEMBED_INFO
+                                 :window icon)))
+        width* height* visible)
+    (when info
+      (exwm--log "Embed #x%x" icon)
+      (with-slots (width height)
+          (xcb:+request-unchecked+reply exwm-systemtray--connection
+              (make-instance 'xcb:GetGeometry :drawable icon))
+        (setq height* exwm-systemtray-height
+              width* (round (* width (/ (float height*) height))))
+        (when (< width* exwm-systemtray--icon-min-size)
+          (setq width* exwm-systemtray--icon-min-size
+                height* (round (* height (/ (float width*) width)))))
+        (exwm--log "Resize from %dx%d to %dx%d"
+                   width height width* height*))
+      ;; Add this icon to save-set.
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:ChangeSaveSet
+                         :mode xcb:SetMode:Insert
+                         :window icon))
+      ;; Reparent to the embedder.
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:ReparentWindow
+                         :window icon
+                         :parent exwm-systemtray--embedder-window
+                         :x 0
+                         ;; Vertically centered.
+                         :y (/ (- exwm-systemtray-height height*) 2)))
+      ;; Resize the icon.
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window icon
+                         :value-mask (logior xcb:ConfigWindow:Width
+                                             xcb:ConfigWindow:Height
+                                             xcb:ConfigWindow:BorderWidth)
+                         :width width*
+                         :height height*
+                         :border-width 0))
+      ;; Set event mask.
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:ChangeWindowAttributes
+                         :window icon
+                         :value-mask xcb:CW:EventMask
+                         :event-mask (logior xcb:EventMask:ResizeRedirect
+                                             xcb:EventMask:KeyPress
+                                             xcb:EventMask:PropertyChange)))
+      ;; Grab all keys and forward them to Emacs frame.
+      (unless (exwm-workspace--minibuffer-own-frame-p)
+        (xcb:+request exwm-systemtray--connection
+            (make-instance 'xcb:GrabKey
+                           :owner-events 0
+                           :grab-window icon
+                           :modifiers xcb:ModMask:Any
+                           :key xcb:Grab:Any
+                           :pointer-mode xcb:GrabMode:Async
+                           :keyboard-mode xcb:GrabMode:Async)))
+      (setq visible (slot-value info 'flags))
+      (if visible
+          (setq visible
+                (/= 0 (logand (slot-value info 'flags) xcb:xembed:MAPPED)))
+        ;; Default to visible.
+        (setq visible t))
+      (when visible
+        (exwm--log "Map the window")
+        (xcb:+request exwm-systemtray--connection
+            (make-instance 'xcb:MapWindow :window icon)))
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:xembed:SendEvent
+                         :destination icon
+                         :event
+                         (xcb:marshal
+                          (make-instance 'xcb:xembed:EMBEDDED-NOTIFY
+                                         :window icon
+                                         :time xcb:Time:CurrentTime
+                                         :embedder
+                                         exwm-systemtray--embedder-window
+                                         :version 0)
+                          exwm-systemtray--connection)))
+      (push `(,icon . ,(make-instance 'exwm-systemtray--icon
+                                      :width width*
+                                      :height height*
+                                      :visible visible))
+            exwm-systemtray--list)
+      (exwm-systemtray--refresh))))
+
+(defun exwm-systemtray--unembed (icon)
+  "Unembed an icon."
+  (exwm--log "Unembed #x%x" icon)
+  (xcb:+request exwm-systemtray--connection
+      (make-instance 'xcb:UnmapWindow :window icon))
+  (xcb:+request exwm-systemtray--connection
+      (make-instance 'xcb:ReparentWindow
+                     :window icon
+                     :parent exwm--root
+                     :x 0 :y 0))
+  (setq exwm-systemtray--list
+        (assq-delete-all icon exwm-systemtray--list))
+  (exwm-systemtray--refresh))
+
+(defun exwm-systemtray--refresh ()
+  "Refresh the system tray."
+  (exwm--log)
+  ;; Make sure to redraw the embedder.
+  (xcb:+request exwm-systemtray--connection
+      (make-instance 'xcb:UnmapWindow
+                     :window exwm-systemtray--embedder-window))
+  (let ((x exwm-systemtray-icon-gap)
+        map)
+    (dolist (pair exwm-systemtray--list)
+      (when (slot-value (cdr pair) 'visible)
+        (xcb:+request exwm-systemtray--connection
+            (make-instance 'xcb:ConfigureWindow
+                           :window (car pair)
+                           :value-mask xcb:ConfigWindow:X
+                           :x x))
+        (setq x (+ x (slot-value (cdr pair) 'width)
+                   exwm-systemtray-icon-gap))
+        (setq map t)))
+    (let ((workarea (elt exwm-workspace--workareas
+                         exwm-workspace-current-index)))
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window exwm-systemtray--embedder-window
+                         :value-mask (logior xcb:ConfigWindow:X
+                                             xcb:ConfigWindow:Width)
+                         :x (- (aref workarea 2) x)
+                         :width x)))
+    (when map
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:MapWindow
+                         :window exwm-systemtray--embedder-window))))
+  (xcb:flush exwm-systemtray--connection))
+
+(defun exwm-systemtray--on-DestroyNotify (data _synthetic)
+  "Unembed icons on DestroyNotify."
+  (exwm--log)
+  (let ((obj (make-instance 'xcb:DestroyNotify)))
+    (xcb:unmarshal obj data)
+    (with-slots (window) obj
+      (when (assoc window exwm-systemtray--list)
+        (exwm-systemtray--unembed window)))))
+
+(defun exwm-systemtray--on-ReparentNotify (data _synthetic)
+  "Unembed icons on ReparentNotify."
+  (exwm--log)
+  (let ((obj (make-instance 'xcb:ReparentNotify)))
+    (xcb:unmarshal obj data)
+    (with-slots (window parent) obj
+      (when (and (/= parent exwm-systemtray--embedder-window)
+                 (assoc window exwm-systemtray--list))
+        (exwm-systemtray--unembed window)))))
+
+(defun exwm-systemtray--on-ResizeRequest (data _synthetic)
+  "Resize the tray icon on ResizeRequest."
+  (exwm--log)
+  (let ((obj (make-instance 'xcb:ResizeRequest))
+        attr)
+    (xcb:unmarshal obj data)
+    (with-slots (window width height) obj
+      (when (setq attr (cdr (assoc window exwm-systemtray--list)))
+        (with-slots ((width* width)
+                     (height* height))
+            attr
+          (setq height* exwm-systemtray-height
+                width* (round (* width (/ (float height*) height))))
+          (when (< width* exwm-systemtray--icon-min-size)
+            (setq width* exwm-systemtray--icon-min-size
+                  height* (round (* height (/ (float width*) width)))))
+          (xcb:+request exwm-systemtray--connection
+              (make-instance 'xcb:ConfigureWindow
+                             :window window
+                             :value-mask (logior xcb:ConfigWindow:Y
+                                                 xcb:ConfigWindow:Width
+                                                 xcb:ConfigWindow:Height)
+                             ;; Vertically centered.
+                             :y (/ (- exwm-systemtray-height height*) 2)
+                             :width width*
+                             :height height*)))
+        (exwm-systemtray--refresh)))))
+
+(defun exwm-systemtray--on-PropertyNotify (data _synthetic)
+  "Map/Unmap the tray icon on PropertyNotify."
+  (exwm--log)
+  (let ((obj (make-instance 'xcb:PropertyNotify))
+        attr info visible)
+    (xcb:unmarshal obj data)
+    (with-slots (window atom state) obj
+      (when (and (eq state xcb:Property:NewValue)
+                 (eq atom xcb:Atom:_XEMBED_INFO)
+                 (setq attr (cdr (assoc window exwm-systemtray--list))))
+        (setq info (xcb:+request-unchecked+reply exwm-systemtray--connection
+                       (make-instance 'xcb:xembed:get-_XEMBED_INFO
+                                      :window window)))
+        (when info
+          (setq visible (/= 0 (logand (slot-value info 'flags)
+                                      xcb:xembed:MAPPED)))
+          (exwm--log "#x%x visible? %s" window visible)
+          (if visible
+              (xcb:+request exwm-systemtray--connection
+                  (make-instance 'xcb:MapWindow :window window))
+            (xcb:+request exwm-systemtray--connection
+                (make-instance 'xcb:UnmapWindow :window window)))
+          (setf (slot-value attr 'visible) visible)
+          (exwm-systemtray--refresh))))))
+
+(defun exwm-systemtray--on-ClientMessage (data _synthetic)
+  "Handle client messages."
+  (let ((obj (make-instance 'xcb:ClientMessage))
+        opcode data32)
+    (xcb:unmarshal obj data)
+    (with-slots (window type data) obj
+      (when (eq type xcb:Atom:_NET_SYSTEM_TRAY_OPCODE)
+        (setq data32 (slot-value data 'data32)
+              opcode (elt data32 1))
+        (exwm--log "opcode: %s" opcode)
+        (cond ((= opcode xcb:systemtray:opcode:REQUEST-DOCK)
+               (unless (assoc (elt data32 2) exwm-systemtray--list)
+                 (exwm-systemtray--embed (elt data32 2))))
+              ;; Not implemented (rarely used nowadays).
+              ((or (= opcode xcb:systemtray:opcode:BEGIN-MESSAGE)
+                   (= opcode xcb:systemtray:opcode:CANCEL-MESSAGE)))
+              (t
+               (exwm--log "Unknown opcode message: %s" obj)))))))
+
+(defun exwm-systemtray--on-KeyPress (data _synthetic)
+  "Forward all KeyPress events to Emacs frame."
+  (exwm--log)
+  ;; This function is only executed when there's no autohide minibuffer,
+  ;; a workspace frame has the input focus and the pointer is over a
+  ;; tray icon.
+  (let ((dest (frame-parameter (selected-frame) 'exwm-outer-id))
+        (obj (make-instance 'xcb:KeyPress)))
+    (xcb:unmarshal obj data)
+    (setf (slot-value obj 'event) dest)
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:SendEvent
+                       :propagate 0
+                       :destination dest
+                       :event-mask xcb:EventMask:NoEvent
+                       :event (xcb:marshal obj exwm-systemtray--connection))))
+  (xcb:flush exwm-systemtray--connection))
+
+(defun exwm-systemtray--on-workspace-switch ()
+  "Reparent/Refresh the system tray in `exwm-workspace-switch-hook'."
+  (exwm--log)
+  (unless (exwm-workspace--minibuffer-own-frame-p)
+    (exwm-workspace--update-offsets)
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:ReparentWindow
+                       :window exwm-systemtray--embedder-window
+                       :parent (string-to-number
+                                (frame-parameter exwm-workspace--current
+                                                 'window-id))
+                       :x 0
+                       :y (- (elt (elt exwm-workspace--workareas
+                                       exwm-workspace-current-index)
+                                  3)
+                             exwm-workspace--frame-y-offset
+                             exwm-systemtray-height))))
+  (exwm-systemtray--refresh))
+
+(defun exwm-systemtray--refresh-all ()
+  "Reposition/Refresh the system tray."
+  (exwm--log)
+  (unless (exwm-workspace--minibuffer-own-frame-p)
+    (exwm-workspace--update-offsets)
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:ConfigureWindow
+                       :window exwm-systemtray--embedder-window
+                       :value-mask xcb:ConfigWindow:Y
+                       :y (- (elt (elt exwm-workspace--workareas
+                                       exwm-workspace-current-index)
+                                  3)
+                             exwm-workspace--frame-y-offset
+                             exwm-systemtray-height))))
+  (exwm-systemtray--refresh))
+
+(cl-defun exwm-systemtray--init ()
+  "Initialize system tray module."
+  (exwm--log)
+  (cl-assert (not exwm-systemtray--connection))
+  (cl-assert (not exwm-systemtray--list))
+  (cl-assert (not exwm-systemtray--selection-owner-window))
+  (cl-assert (not exwm-systemtray--embedder-window))
+  (unless exwm-systemtray-height
+    (setq exwm-systemtray-height (max exwm-systemtray--icon-min-size
+                                      (line-pixel-height))))
+  ;; Create a new connection.
+  (setq exwm-systemtray--connection (xcb:connect))
+  (set-process-query-on-exit-flag (slot-value exwm-systemtray--connection
+                                              'process)
+                                  nil)
+  ;; Initialize XELB modules.
+  (xcb:xembed:init exwm-systemtray--connection t)
+  (xcb:systemtray:init exwm-systemtray--connection t)
+  ;; Acquire the manager selection _NET_SYSTEM_TRAY_S0.
+  (with-slots (owner)
+      (xcb:+request-unchecked+reply exwm-systemtray--connection
+          (make-instance 'xcb:GetSelectionOwner
+                         :selection xcb:Atom:_NET_SYSTEM_TRAY_S0))
+    (when (/= owner xcb:Window:None)
+      (xcb:disconnect exwm-systemtray--connection)
+      (setq exwm-systemtray--connection nil)
+      (warn "[EXWM] Other system tray detected")
+      (cl-return-from exwm-systemtray--init)))
+  (let ((id (xcb:generate-id exwm-systemtray--connection)))
+    (setq exwm-systemtray--selection-owner-window id)
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:CreateWindow
+                       :depth 0
+                       :wid id
+                       :parent exwm--root
+                       :x 0
+                       :y 0
+                       :width 1
+                       :height 1
+                       :border-width 0
+                       :class xcb:WindowClass:InputOnly
+                       :visual 0
+                       :value-mask xcb:CW:OverrideRedirect
+                       :override-redirect 1))
+    ;; Get the selection ownership.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:SetSelectionOwner
+                       :owner id
+                       :selection xcb:Atom:_NET_SYSTEM_TRAY_S0
+                       :time xcb:Time:CurrentTime))
+    ;; Send a client message to announce the selection.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:SendEvent
+                       :propagate 0
+                       :destination exwm--root
+                       :event-mask xcb:EventMask:StructureNotify
+                       :event (xcb:marshal
+                               (make-instance 'xcb:systemtray:-ClientMessage
+                                              :window exwm--root
+                                              :time xcb:Time:CurrentTime
+                                              :selection
+                                              xcb:Atom:_NET_SYSTEM_TRAY_S0
+                                              :owner id)
+                               exwm-systemtray--connection)))
+    ;; Set _NET_WM_NAME.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                       :window id
+                       :data "EXWM: exwm-systemtray--selection-owner-window"))
+    ;; Set the _NET_SYSTEM_TRAY_ORIENTATION property.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:xembed:set-_NET_SYSTEM_TRAY_ORIENTATION
+                       :window id
+                       :data xcb:systemtray:ORIENTATION:HORZ)))
+  ;; Create the embedder.
+  (let ((id (xcb:generate-id exwm-systemtray--connection))
+        (background-pixel (exwm--color->pixel exwm-systemtray-background-color))
+        frame parent depth y)
+    (setq exwm-systemtray--embedder-window id)
+    (if (exwm-workspace--minibuffer-own-frame-p)
+        (setq frame exwm-workspace--minibuffer
+              y (if (>= (line-pixel-height) exwm-systemtray-height)
+                    ;; Bottom aligned.
+                    (- (line-pixel-height) exwm-systemtray-height)
+                  ;; Vertically centered.
+                  (/ (- (line-pixel-height) exwm-systemtray-height) 2)))
+      (exwm-workspace--update-offsets)
+      (setq frame exwm-workspace--current
+            ;; Bottom aligned.
+            y (- (elt (elt exwm-workspace--workareas
+                           exwm-workspace-current-index)
+                      3)
+                 exwm-workspace--frame-y-offset
+                 exwm-systemtray-height)))
+    (setq parent (string-to-number (frame-parameter frame 'window-id))
+          depth (slot-value (xcb:+request-unchecked+reply
+                                exwm-systemtray--connection
+                                (make-instance 'xcb:GetGeometry
+                                               :drawable parent))
+                            'depth))
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:CreateWindow
+                       :depth depth
+                       :wid id
+                       :parent parent
+                       :x 0
+                       :y y
+                       :width 1
+                       :height exwm-systemtray-height
+                       :border-width 0
+                       :class xcb:WindowClass:InputOutput
+                       :visual 0
+                       :value-mask (logior xcb:CW:BackPixmap
+                                           (if background-pixel
+                                               xcb:CW:BackPixel 0)
+                                           xcb:CW:EventMask)
+                       :background-pixmap xcb:BackPixmap:ParentRelative
+                       :background-pixel background-pixel
+                       :event-mask xcb:EventMask:SubstructureNotify))
+    ;; Set _NET_WM_NAME.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                       :window id
+                       :data "EXWM: exwm-systemtray--embedder-window")))
+  (xcb:flush exwm-systemtray--connection)
+  ;; Attach event listeners.
+  (xcb:+event exwm-systemtray--connection 'xcb:DestroyNotify
+              #'exwm-systemtray--on-DestroyNotify)
+  (xcb:+event exwm-systemtray--connection 'xcb:ReparentNotify
+              #'exwm-systemtray--on-ReparentNotify)
+  (xcb:+event exwm-systemtray--connection 'xcb:ResizeRequest
+              #'exwm-systemtray--on-ResizeRequest)
+  (xcb:+event exwm-systemtray--connection 'xcb:PropertyNotify
+              #'exwm-systemtray--on-PropertyNotify)
+  (xcb:+event exwm-systemtray--connection 'xcb:ClientMessage
+              #'exwm-systemtray--on-ClientMessage)
+  (unless (exwm-workspace--minibuffer-own-frame-p)
+    (xcb:+event exwm-systemtray--connection 'xcb:KeyPress
+                #'exwm-systemtray--on-KeyPress))
+  ;; Add hook to move/reparent the embedder.
+  (add-hook 'exwm-workspace-switch-hook #'exwm-systemtray--on-workspace-switch)
+  (add-hook 'exwm-workspace--update-workareas-hook
+            #'exwm-systemtray--refresh-all)
+  (add-hook 'menu-bar-mode-hook #'exwm-systemtray--refresh-all)
+  (add-hook 'tool-bar-mode-hook #'exwm-systemtray--refresh-all)
+  (when (boundp 'exwm-randr-refresh-hook)
+    (add-hook 'exwm-randr-refresh-hook #'exwm-systemtray--refresh-all))
+  ;; The struts can be updated already.
+  (when exwm-workspace--workareas
+    (exwm-systemtray--refresh-all)))
+
+(defun exwm-systemtray--exit ()
+  "Exit the systemtray module."
+  (exwm--log)
+  (when exwm-systemtray--connection
+    ;; Hide & reparent out the embedder before disconnection to prevent
+    ;; embedded icons from being reparented to an Emacs frame (which is the
+    ;; parent of the embedder).
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:UnmapWindow
+                       :window exwm-systemtray--embedder-window))
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:ReparentWindow
+                       :window exwm-systemtray--embedder-window
+                       :parent exwm--root
+                       :x 0
+                       :y 0))
+    (xcb:disconnect exwm-systemtray--connection)
+    (setq exwm-systemtray--connection nil
+          exwm-systemtray--list nil
+          exwm-systemtray--selection-owner-window nil
+          exwm-systemtray--embedder-window nil)
+    (remove-hook 'exwm-workspace-switch-hook
+                 #'exwm-systemtray--on-workspace-switch)
+    (remove-hook 'exwm-workspace--update-workareas-hook
+                 #'exwm-systemtray--refresh-all)
+    (remove-hook 'menu-bar-mode-hook #'exwm-systemtray--refresh-all)
+    (remove-hook 'tool-bar-mode-hook #'exwm-systemtray--refresh-all)
+    (when (boundp 'exwm-randr-refresh-hook)
+      (remove-hook 'exwm-randr-refresh-hook #'exwm-systemtray--refresh-all))))
+
+(defun exwm-systemtray-enable ()
+  "Enable system tray support for EXWM."
+  (exwm--log)
+  (add-hook 'exwm-init-hook #'exwm-systemtray--init)
+  (add-hook 'exwm-exit-hook #'exwm-systemtray--exit))
+
+
+
+(provide 'exwm-systemtray)
+
+;;; exwm-systemtray.el ends here
diff --git a/third_party/exwm/exwm-workspace.el b/third_party/exwm/exwm-workspace.el
new file mode 100644
index 0000000000..fc68e1b070
--- /dev/null
+++ b/third_party/exwm/exwm-workspace.el
@@ -0,0 +1,1780 @@
+;;; exwm-workspace.el --- Workspace Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 1015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module adds workspace support for EXWM.
+
+;;; Code:
+
+(require 'server)
+
+(require 'exwm-core)
+
+(defgroup exwm-workspace nil
+  "Workspace."
+  :version "25.3"
+  :group 'exwm)
+
+(defcustom exwm-workspace-switch-hook nil
+  "Normal hook run after switching workspace."
+  :type 'hook)
+
+(defcustom exwm-workspace-list-change-hook nil
+  "Normal hook run when the workspace list is changed (workspace added,
+deleted, moved, etc)."
+  :type 'hook)
+
+(defcustom exwm-workspace-show-all-buffers nil
+  "Non-nil to show buffers on other workspaces."
+  :type 'boolean)
+
+(defcustom exwm-workspace-warp-cursor nil
+  "Non-nil to warp cursor automatically after workspace switch."
+  :type 'boolean)
+
+(defcustom exwm-workspace-number 1
+  "Initial number of workspaces."
+  :type 'integer)
+
+(defcustom exwm-workspace-index-map #'number-to-string
+  "Function for mapping a workspace index to a string for display.
+
+By default `number-to-string' is applied which yields 0 1 2 ... ."
+  :type 'function)
+
+(defcustom exwm-workspace-minibuffer-position nil
+  "Position of the minibuffer frame.
+
+A restart is required for this change to take effect."
+  :type '(choice (const :tag "Bottom (fixed)" nil)
+                 (const :tag "Bottom (auto-hide)" bottom)
+                 (const :tag "Top (auto-hide)" top)))
+
+(defcustom exwm-workspace-display-echo-area-timeout 1
+  "Timeout for displaying echo area."
+  :type 'integer)
+
+(defcustom exwm-workspace-switch-create-limit 10
+  "Number of workspaces `exwm-workspace-switch-create' allowed to create
+each time."
+  :type 'integer)
+
+(defvar exwm-workspace-current-index 0 "Index of current active workspace.")
+
+(defvar exwm-workspace--attached-minibuffer-height 0
+  "Height (in pixel) of the attached minibuffer.
+
+If the minibuffer is detached, this value is 0.")
+
+(defvar exwm-workspace--client nil
+  "The 'client' frame parameter of emacsclient frames.")
+
+(defvar exwm-workspace--create-silently nil
+  "When non-nil workspaces are created in the background (not switched to).
+
+Please manually run the hook `exwm-workspace-list-change-hook' afterwards.")
+
+(defvar exwm-workspace--current nil "Current active workspace.")
+
+(defvar exwm-workspace--display-echo-area-timer nil
+  "Timer for auto-hiding echo area.")
+
+(defvar exwm-workspace--id-struts-alist nil "Alist of X window and struts.")
+
+(defvar exwm-workspace--fullscreen-frame-count 0
+  "Count the fullscreen workspace frames.")
+
+(defvar exwm-workspace--list nil "List of all workspaces (Emacs frames).")
+
+(defvar exwm-workspace--minibuffer nil
+  "The minibuffer frame shared among all frames.")
+
+(defvar exwm-workspace--original-handle-focus-in
+  (symbol-function #'handle-focus-in))
+(defvar exwm-workspace--original-handle-focus-out
+  (symbol-function #'handle-focus-out))
+
+(defvar exwm-workspace--prompt-add-allowed nil
+  "Non-nil to allow adding workspace from the prompt.")
+
+(defvar exwm-workspace--prompt-delete-allowed nil
+  "Non-nil to allow deleting workspace from the prompt.")
+
+(defvar exwm-workspace--struts nil "Areas occupied by struts.")
+
+(defvar exwm-workspace--switch-history nil
+  "History for `read-from-minibuffer' to interactively switch workspace.")
+
+(defvar exwm-workspace--switch-history-outdated nil
+  "Non-nil to indicate `exwm-workspace--switch-history' is outdated.")
+
+(defvar exwm-workspace--timer nil "Timer used to track echo area changes.")
+
+(defvar exwm-workspace--update-workareas-hook nil
+  "Normal hook run when workareas get updated.")
+
+(defvar exwm-workspace--workareas nil "Workareas (struts excluded).")
+
+(defvar exwm-workspace--frame-y-offset 0
+  "Offset between Emacs inner & outer frame in Y.")
+(defvar exwm-workspace--window-y-offset 0
+  "Offset between Emacs first window & outer frame in Y.")
+
+(defvar exwm-input--during-command)
+(defvar exwm-input--event-hook)
+(defvar exwm-layout-show-all-buffers)
+(defvar exwm-manage--desktop)
+(declare-function exwm-input--on-buffer-list-update "exwm-input.el" ())
+(declare-function exwm-layout--fullscreen-p "exwm-layout.el" ())
+(declare-function exwm-layout--hide "exwm-layout.el" (id))
+(declare-function exwm-layout--other-buffer-predicate "exwm-layout.el"
+                  (buffer))
+(declare-function exwm-layout--refresh "exwm-layout.el")
+(declare-function exwm-layout--show "exwm-layout.el" (id &optional window))
+
+(defsubst exwm-workspace--position (frame)
+  "Retrieve index of given FRAME in workspace list.
+
+NIL if FRAME is not a workspace"
+  (cl-position frame exwm-workspace--list))
+
+(defsubst exwm-workspace--count ()
+  "Retrieve total number of workspaces."
+  (length exwm-workspace--list))
+
+(defsubst exwm-workspace--workspace-p (frame)
+  "Return t if FRAME is a workspace."
+  (memq frame exwm-workspace--list))
+
+(defvar exwm-workspace--client-p-hash-table
+  (make-hash-table :test 'eq :weakness 'key)
+  "Used to cache the results of calling ‘exwm-workspace--client-p’.")
+
+(defsubst exwm-workspace--client-p (&optional frame)
+  "Return non-nil if FRAME is an emacsclient frame."
+  (let* ((frame (or frame (selected-frame)))
+         (cached-value
+          (gethash frame exwm-workspace--client-p-hash-table 'absent)))
+    (if (eq cached-value 'absent)
+        (puthash frame
+                 (or (frame-parameter frame 'client)
+                     (not (display-graphic-p frame)))
+                 exwm-workspace--client-p-hash-table)
+        cached-value)))
+
+(defvar exwm-workspace--switch-map nil
+  "Keymap used for interactively selecting workspace.")
+
+(defun exwm-workspace--init-switch-map ()
+  (let ((map (make-sparse-keymap)))
+    (define-key map [t] (lambda () (interactive)))
+    (define-key map "+" #'exwm-workspace--prompt-add)
+    (define-key map "-" #'exwm-workspace--prompt-delete)
+    (dotimes (i 10)
+      (define-key map (int-to-string i)
+        #'exwm-workspace--switch-map-nth-prefix))
+    (unless (eq exwm-workspace-index-map #'number-to-string)
+      ;; Add extra (and possibly override) keys for selecting workspace.
+      (dotimes (i 10)
+        (let ((key (funcall exwm-workspace-index-map i)))
+          (when (and (stringp key)
+                     (= (length key) 1)
+                     (<= 0 (elt key 0) 127))
+            (define-key map key
+              (lambda ()
+                (interactive)
+                (exwm-workspace--switch-map-select-nth i)))))))
+    (define-key map "\C-a" (lambda () (interactive) (goto-history-element 1)))
+    (define-key map "\C-e" (lambda ()
+                             (interactive)
+                             (goto-history-element (exwm-workspace--count))))
+    (define-key map "\C-g" #'abort-recursive-edit)
+    (define-key map "\C-]" #'abort-recursive-edit)
+    (define-key map "\C-j" #'exit-minibuffer)
+    ;; (define-key map "\C-m" #'exit-minibuffer) ;not working
+    (define-key map [return] #'exit-minibuffer)
+    (define-key map " " #'exit-minibuffer)
+    (define-key map "\C-f" #'previous-history-element)
+    (define-key map "\C-b" #'next-history-element)
+    ;; Alternative keys
+    (define-key map [right] #'previous-history-element)
+    (define-key map [left] #'next-history-element)
+    (setq exwm-workspace--switch-map map)))
+
+(defun exwm-workspace--workspace-from-frame-or-index (frame-or-index)
+  "Retrieve the workspace frame from FRAME-OR-INDEX."
+  (cond
+   ((framep frame-or-index)
+    (unless (exwm-workspace--position frame-or-index)
+      (user-error "[EXWM] Frame is not a workspace %S" frame-or-index))
+    frame-or-index)
+   ((integerp frame-or-index)
+    (unless (and (<= 0 frame-or-index)
+                 (< frame-or-index (exwm-workspace--count)))
+      (user-error "[EXWM] Workspace index out of range: %d" frame-or-index))
+    (elt exwm-workspace--list frame-or-index))
+   (t (user-error "[EXWM] Invalid workspace: %s" frame-or-index))))
+
+(defun exwm-workspace--prompt-for-workspace (&optional prompt)
+  "Prompt for a workspace, returning the workspace frame."
+  (exwm-workspace--update-switch-history)
+  (let* ((current-idx (exwm-workspace--position exwm-workspace--current))
+         (history-add-new-input nil)  ;prevent modifying history
+         (history-idx (read-from-minibuffer
+                       (or prompt "Workspace: ")
+                       (elt exwm-workspace--switch-history current-idx)
+                       exwm-workspace--switch-map nil
+                       `(exwm-workspace--switch-history . ,(1+ current-idx))))
+         (workspace-idx (cl-position history-idx exwm-workspace--switch-history
+                                     :test #'equal)))
+    (elt exwm-workspace--list workspace-idx)))
+
+(defun exwm-workspace--prompt-add ()
+  "Add workspace from the prompt."
+  (interactive)
+  (when exwm-workspace--prompt-add-allowed
+    (let ((exwm-workspace--create-silently t))
+      (make-frame)
+      (run-hooks 'exwm-workspace-list-change-hook))
+    (exwm-workspace--update-switch-history)
+    (goto-history-element minibuffer-history-position)))
+
+(defun exwm-workspace--prompt-delete ()
+  "Delete workspace from the prompt."
+  (interactive)
+  (when (and exwm-workspace--prompt-delete-allowed
+             (< 1 (exwm-workspace--count)))
+    (let ((frame (elt exwm-workspace--list (1- minibuffer-history-position))))
+      (exwm-workspace--get-remove-frame-next-workspace frame)
+      (if (eq frame exwm-workspace--current)
+          ;; Abort the recursive minibuffer if deleting the current workspace.
+          (progn
+            (exwm--defer 0 #'delete-frame frame)
+            (abort-recursive-edit))
+        (delete-frame frame)
+        (exwm-workspace--update-switch-history)
+        (goto-history-element (min minibuffer-history-position
+                                   (exwm-workspace--count)))))))
+
+(defun exwm-workspace--update-switch-history ()
+  "Update the history for switching workspace to reflect the latest status."
+  (when exwm-workspace--switch-history-outdated
+    (setq exwm-workspace--switch-history-outdated nil)
+    (let* ((num (exwm-workspace--count))
+           (sequence (number-sequence 0 (1- num)))
+           (not-empty (make-vector num nil)))
+      (dolist (i exwm--id-buffer-alist)
+        (with-current-buffer (cdr i)
+          (when exwm--frame
+            (setf (aref not-empty
+                        (exwm-workspace--position exwm--frame))
+                  t))))
+      (setq exwm-workspace--switch-history
+            (mapcar
+             (lambda (i)
+               (mapconcat
+                (lambda (j)
+                  (format (if (= i j) "[%s]" " %s ")
+                          (propertize
+                           (apply exwm-workspace-index-map (list j))
+                           'face
+                           (cond ((frame-parameter (elt exwm-workspace--list j)
+                                                   'exwm-urgency)
+                                  '(:foreground "orange"))
+                                 ((aref not-empty j) '(:foreground "green"))
+                                 (t nil)))))
+                sequence ""))
+             sequence)))))
+
+;;;###autoload
+(defun exwm-workspace--get-geometry (frame)
+  "Return the geometry of frame FRAME."
+  (or (frame-parameter frame 'exwm-geometry)
+      (make-instance 'xcb:RECTANGLE
+                     :x 0
+                     :y 0
+                     :width (x-display-pixel-width)
+                     :height (x-display-pixel-height))))
+
+;;;###autoload
+(defun exwm-workspace--current-height ()
+  "Return the height of current workspace."
+  (let ((geometry (frame-parameter exwm-workspace--current 'exwm-geometry)))
+    (if geometry
+        (slot-value geometry 'height)
+      (x-display-pixel-height))))
+
+;;;###autoload
+(defun exwm-workspace--minibuffer-own-frame-p ()
+  "Reports whether the minibuffer is displayed in its own frame."
+  (memq exwm-workspace-minibuffer-position '(top bottom)))
+
+(defun exwm-workspace--update-struts ()
+  "Update `exwm-workspace--struts'."
+  (setq exwm-workspace--struts nil)
+  (let (struts struts*)
+    (dolist (pair exwm-workspace--id-struts-alist)
+      (setq struts (cdr pair))
+      (when struts
+        (dotimes (i 4)
+          (when (/= 0 (aref struts i))
+            (setq struts*
+                  (vector (aref [left right top bottom] i)
+                          (aref struts i)
+                          (when (= 12 (length struts))
+                            (substring struts (+ 4 (* i 2)) (+ 6 (* i 2))))))
+            (if (= 0 (mod i 2))
+                ;; Make left/top processed first.
+                (push struts* exwm-workspace--struts)
+              (setq exwm-workspace--struts
+                    (append exwm-workspace--struts (list struts*))))))))
+    (exwm--log "%s" exwm-workspace--struts)))
+
+(defun exwm-workspace--update-workareas ()
+  "Update `exwm-workspace--workareas'."
+  (let ((root-width (x-display-pixel-width))
+        (root-height (x-display-pixel-height))
+        workareas
+        edge width position
+        delta)
+    ;; Calculate workareas with no struts.
+    (if (frame-parameter (car exwm-workspace--list) 'exwm-geometry)
+        ;; Use the 'exwm-geometry' frame parameter if possible.
+        (dolist (f exwm-workspace--list)
+          (with-slots (x y width height) (frame-parameter f 'exwm-geometry)
+            (setq workareas (append workareas
+                                    (list (vector x y width height))))))
+      ;; Fall back to use the screen size.
+      (let ((workarea (vector 0 0 root-width root-height)))
+        (setq workareas (make-list (exwm-workspace--count) workarea))))
+    ;; Exclude areas occupied by struts.
+    (dolist (struts exwm-workspace--struts)
+      (setq edge (aref struts 0)
+            width (aref struts 1)
+            position (aref struts 2))
+      (dolist (w workareas)
+        (pcase edge
+          ;; Left and top are always processed first.
+          (`left
+           (setq delta (- (aref w 0) width))
+           (when (and (< delta 0)
+                      (or (not position)
+                          (< (max (aref position 0) (aref w 1))
+                             (min (aref position 1)
+                                  (+ (aref w 1) (aref w 3))))))
+             (cl-incf (aref w 2) delta)
+             (setf (aref w 0) width)))
+          (`right
+           (setq delta (- root-width (aref w 0) (aref w 2) width))
+           (when (and (< delta 0)
+                      (or (not position)
+                          (< (max (aref position 0) (aref w 1))
+                             (min (aref position 1)
+                                  (+ (aref w 1) (aref w 3))))))
+             (cl-incf (aref w 2) delta)))
+          (`top
+           (setq delta (- (aref w 1) width))
+           (when (and (< delta 0)
+                      (or (not position)
+                          (< (max (aref position 0) (aref w 0))
+                             (min (aref position 1)
+                                  (+ (aref w 0) (aref w 2))))))
+             (cl-incf (aref w 3) delta)
+             (setf (aref w 1) width)))
+          (`bottom
+           (setq delta (- root-height (aref w 1) (aref w 3) width))
+           (when (and (< delta 0)
+                      (or (not position)
+                          (< (max (aref position 0) (aref w 0))
+                             (min (aref position 1)
+                                  (+ (aref w 0) (aref w 2))))))
+             (cl-incf (aref w 3) delta))))))
+    ;; Save the result.
+    (setq exwm-workspace--workareas workareas)
+    (xcb:flush exwm--connection))
+  (exwm--log "%s" exwm-workspace--workareas)
+  (run-hooks 'exwm-workspace--update-workareas-hook))
+
+(defun exwm-workspace--update-offsets ()
+  "Update `exwm-workspace--frame-y-offset'/`exwm-workspace--window-y-offset'."
+  (exwm--log)
+  (if (not (and exwm-workspace--list
+                (or menu-bar-mode tool-bar-mode)))
+      (setq exwm-workspace--frame-y-offset 0
+            exwm-workspace--window-y-offset 0)
+    (redisplay t)
+    (let* ((frame (elt exwm-workspace--list 0))
+           (edges (window-inside-absolute-pixel-edges (frame-first-window
+                                                       frame))))
+      (with-slots (y)
+          (xcb:+request-unchecked+reply exwm--connection
+              (make-instance 'xcb:GetGeometry
+                             :drawable (frame-parameter frame
+                                                        'exwm-container)))
+        (with-slots ((y* y))
+            (xcb:+request-unchecked+reply exwm--connection
+                (make-instance 'xcb:GetGeometry
+                               :drawable (frame-parameter frame
+                                                          'exwm-outer-id)))
+          (with-slots ((y** y))
+              (xcb:+request-unchecked+reply exwm--connection
+                  (make-instance 'xcb:GetGeometry
+                                 :drawable (frame-parameter frame 'exwm-id)))
+            (setq exwm-workspace--frame-y-offset (- y** y*)
+                  exwm-workspace--window-y-offset (- (elt edges 1) y))))))))
+
+(defun exwm-workspace--set-active (frame active)
+  "Make frame FRAME active on its monitor."
+  (exwm--log "active=%s; frame=%s" frame active)
+  (set-frame-parameter frame 'exwm-active active)
+  (if active
+      (exwm-workspace--set-fullscreen frame)
+    (exwm--set-geometry (frame-parameter frame 'exwm-container) nil nil 1 1))
+  (exwm-layout--refresh frame)
+  (xcb:flush exwm--connection))
+
+(defun exwm-workspace--active-p (frame)
+  "Return non-nil if FRAME is active"
+  (frame-parameter frame 'exwm-active))
+
+(defun exwm-workspace--set-fullscreen (frame)
+  "Make frame FRAME fullscreen according to `exwm-workspace--workareas'."
+  (exwm--log "frame=%s" frame)
+  (let ((workarea (elt exwm-workspace--workareas
+                       (exwm-workspace--position frame)))
+        (id (frame-parameter frame 'exwm-outer-id))
+        (container (frame-parameter frame 'exwm-container))
+        x y width height)
+    (setq x (aref workarea 0)
+          y (aref workarea 1)
+          width (aref workarea 2)
+          height (aref workarea 3))
+    (exwm--log "x=%s; y=%s; w=%s; h=%s" x y width height)
+    (when (and (eq frame exwm-workspace--current)
+               (exwm-workspace--minibuffer-own-frame-p))
+      (exwm-workspace--resize-minibuffer-frame))
+    (if (exwm-workspace--active-p frame)
+        (exwm--set-geometry container x y width height)
+      (exwm--set-geometry container x y 1 1))
+    (exwm--set-geometry id nil nil width height)
+    (xcb:flush exwm--connection))
+  ;; This is only used for workspace initialization.
+  (when exwm-workspace--fullscreen-frame-count
+    (cl-incf exwm-workspace--fullscreen-frame-count)))
+
+(defun exwm-workspace--resize-minibuffer-frame ()
+  "Resize minibuffer (and its container) to fit the size of workspace."
+  (cl-assert (exwm-workspace--minibuffer-own-frame-p))
+  (let ((workarea (elt exwm-workspace--workareas exwm-workspace-current-index))
+        (container (frame-parameter exwm-workspace--minibuffer
+                                    'exwm-container))
+        y width)
+    (setq y (if (eq exwm-workspace-minibuffer-position 'top)
+                (- (aref workarea 1)
+                   exwm-workspace--attached-minibuffer-height)
+              ;; Reset the frame size.
+              (set-frame-height exwm-workspace--minibuffer 1)
+              (redisplay)               ;FIXME.
+              (+ (aref workarea 1) (aref workarea 3)
+                 (- (frame-pixel-height exwm-workspace--minibuffer))
+                 exwm-workspace--attached-minibuffer-height))
+          width (aref workarea 2))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ConfigureWindow
+                       :window container
+                       :value-mask (logior xcb:ConfigWindow:X
+                                           xcb:ConfigWindow:Y
+                                           xcb:ConfigWindow:Width
+                                           (if exwm-manage--desktop
+                                               xcb:ConfigWindow:Sibling
+                                             0)
+                                           xcb:ConfigWindow:StackMode)
+                       :x (aref workarea 0)
+                       :y y
+                       :width width
+                       :sibling exwm-manage--desktop
+                       :stack-mode (if exwm-manage--desktop
+                                       xcb:StackMode:Above
+                                     xcb:StackMode:Below)))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ConfigureWindow
+                       :window (frame-parameter exwm-workspace--minibuffer
+                                                'exwm-outer-id)
+                       :value-mask xcb:ConfigWindow:Width
+                       :width width))
+    (exwm--log "y: %s, width: %s" y width)))
+
+(defun exwm-workspace--switch-map-nth-prefix (&optional prefix-digits)
+  "Allow selecting a workspace by number.
+
+PREFIX-DIGITS is a list of the digits introduced so far."
+  (interactive)
+  (let* ((k (aref (substring (this-command-keys-vector) -1) 0))
+         (d (- k ?0))
+         ;; Convert prefix-digits to number.  For example, '(2 1) to 120.
+         (o 1)
+         (pn (apply #'+ (mapcar (lambda (x)
+                                  (setq o (* 10 o))
+                                  (* o x))
+                                prefix-digits)))
+         (n (+ pn d))
+         prefix-length index-max index-length)
+    (if (or (= n 0)
+            (> n
+               (setq index-max (1- (exwm-workspace--count))))
+            (>= (setq prefix-length (length prefix-digits))
+                (setq index-length (floor (log index-max 10))))
+            ;; Check if it's still possible to do a match.
+            (> (* n (expt 10 (- index-length prefix-length)))
+               index-max))
+        (exwm-workspace--switch-map-select-nth n)
+      ;; Go ahead if there are enough digits to select any workspace.
+      (set-transient-map
+       (let ((map (make-sparse-keymap))
+             (cmd (let ((digits (cons d prefix-digits)))
+                    (lambda ()
+                     (interactive)
+                     (exwm-workspace--switch-map-nth-prefix digits)))))
+         (dotimes (i 10)
+           (define-key map (int-to-string i) cmd))
+         ;; Accept
+         (define-key map [return]
+           (lambda ()
+             (interactive)
+             (exwm-workspace--switch-map-select-nth n)))
+         map)))))
+
+(defun exwm-workspace--switch-map-select-nth (n)
+  "Select Nth workspace."
+  (interactive)
+  (goto-history-element (1+ n))
+  (exit-minibuffer))
+
+;;;###autoload
+(defun exwm-workspace-switch (frame-or-index &optional force)
+  "Switch to workspace INDEX (0-based).
+
+Query for the index if not specified when called interactively.  Passing a
+workspace frame as the first option or making use of the rest options are
+for internal use only."
+  (interactive
+   (list
+    (cond
+     ((null current-prefix-arg)
+      (unless (and (derived-mode-p 'exwm-mode)
+                   ;; The prompt is invisible in fullscreen mode.
+                   (exwm-layout--fullscreen-p))
+        (let ((exwm-workspace--prompt-add-allowed t)
+              (exwm-workspace--prompt-delete-allowed t))
+          (exwm-workspace--prompt-for-workspace "Switch to [+/-]: "))))
+     ((and (integerp current-prefix-arg)
+           (<= 0 current-prefix-arg (exwm-workspace--count)))
+      current-prefix-arg)
+     (t 0))))
+  (exwm--log)
+  (let* ((frame (exwm-workspace--workspace-from-frame-or-index frame-or-index))
+         (old-frame exwm-workspace--current)
+         (index (exwm-workspace--position frame))
+         (window (frame-parameter frame 'exwm-selected-window)))
+    (when (or force (not (eq frame exwm-workspace--current)))
+      (unless (window-live-p window)
+        (setq window (frame-selected-window frame)))
+    (when (and (not (eq frame old-frame))
+               (frame-live-p old-frame))
+      (with-selected-frame old-frame
+        (funcall exwm-workspace--original-handle-focus-out
+                 (list 'focus-out frame))))
+      ;; Raise this frame.
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window (frame-parameter frame 'exwm-container)
+                         :value-mask (logior xcb:ConfigWindow:Sibling
+                                             xcb:ConfigWindow:StackMode)
+                         :sibling exwm--guide-window
+                         :stack-mode xcb:StackMode:Below))
+      (setq exwm-workspace--current frame
+            exwm-workspace-current-index index)
+      (unless (exwm-workspace--workspace-p (selected-frame))
+        ;; Save the floating frame window selected on the previous workspace.
+        (set-frame-parameter (buffer-local-value 'exwm--frame (window-buffer))
+                             'exwm-selected-window (selected-window)))
+      ;; Show/Hide X windows.
+      (let ((monitor-old (frame-parameter old-frame 'exwm-randr-monitor))
+            (monitor-new (frame-parameter frame 'exwm-randr-monitor))
+            (active-old (exwm-workspace--active-p old-frame))
+            (active-new (exwm-workspace--active-p frame))
+            workspaces-to-hide)
+        (cond
+         ((not active-old)
+          (exwm-workspace--set-active frame t))
+         ((equal monitor-old monitor-new)
+          (exwm-workspace--set-active frame t)
+          (unless (eq frame old-frame)
+            (exwm-workspace--set-active old-frame nil)
+            (setq workspaces-to-hide (list old-frame))))
+         (active-new)
+         (t
+          (dolist (w exwm-workspace--list)
+            (when (and (exwm-workspace--active-p w)
+                       (equal monitor-new
+                              (frame-parameter w 'exwm-randr-monitor)))
+              (exwm-workspace--set-active w nil)
+              (setq workspaces-to-hide (append workspaces-to-hide (list w)))))
+          (exwm-workspace--set-active frame t)))
+        (dolist (i exwm--id-buffer-alist)
+          (with-current-buffer (cdr i)
+            (if (memq exwm--frame workspaces-to-hide)
+                (exwm-layout--hide exwm--id)
+              (when (eq frame exwm--frame)
+                (let ((window (get-buffer-window nil t)))
+                  (when window
+                    (exwm-layout--show exwm--id window))))))))
+      (select-window window)
+      (x-focus-frame (window-frame window)) ;The real input focus.
+      (set-frame-parameter frame 'exwm-selected-window nil)
+      (if (exwm-workspace--minibuffer-own-frame-p)
+          ;; Resize the minibuffer frame.
+          (exwm-workspace--resize-minibuffer-frame)
+        ;; Set a default minibuffer frame.
+        (setq default-minibuffer-frame frame))
+      ;; Hide windows in other workspaces by preprending a space
+      (unless exwm-workspace-show-all-buffers
+        (dolist (i exwm--id-buffer-alist)
+          (with-current-buffer (cdr i)
+            (let ((name (replace-regexp-in-string "^\\s-*" ""
+                                                  (buffer-name))))
+              (exwm-workspace-rename-buffer (if (eq frame exwm--frame)
+                                                name
+                                              (concat " " name)))))))
+      ;; Update demands attention flag
+      (set-frame-parameter frame 'exwm-urgency nil)
+      ;; Update switch workspace history
+      (setq exwm-workspace--switch-history-outdated t)
+      ;; Set _NET_CURRENT_DESKTOP
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ewmh:set-_NET_CURRENT_DESKTOP
+                         :window exwm--root :data index))
+      (xcb:flush exwm--connection))
+    (when exwm-workspace-warp-cursor
+      (with-slots (win-x win-y)
+          (xcb:+request-unchecked+reply exwm--connection
+              (make-instance 'xcb:QueryPointer
+                             :window (frame-parameter frame
+                                                      'exwm-outer-id)))
+        (when (or (< win-x 0)
+                  (< win-y 0)
+                  (> win-x (frame-pixel-width frame))
+                  (> win-y (frame-pixel-height frame)))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:WarpPointer
+                             :src-window xcb:Window:None
+                             :dst-window (frame-parameter frame
+                                                          'exwm-outer-id)
+                             :src-x 0
+                             :src-y 0
+                             :src-width 0
+                             :src-height 0
+                             :dst-x (/ (frame-pixel-width frame) 2)
+                             :dst-y (/ (frame-pixel-height frame) 2)))
+          (xcb:flush exwm--connection))))
+    (funcall exwm-workspace--original-handle-focus-in (list 'focus-in frame))
+    (run-hooks 'exwm-workspace-switch-hook)))
+
+;;;###autoload
+(defun exwm-workspace-switch-create (frame-or-index)
+  "Switch to workspace INDEX or creating it first if it does not exist yet.
+
+Passing a workspace frame as the first option is for internal use only."
+  (interactive
+   (list
+    (cond
+     ((integerp current-prefix-arg)
+      current-prefix-arg)
+     (t 0))))
+  (unless frame-or-index
+    (setq frame-or-index 0))
+  (exwm--log "%s" frame-or-index)
+  (if (or (framep frame-or-index)
+          (< frame-or-index (exwm-workspace--count)))
+      (exwm-workspace-switch frame-or-index)
+    (let ((exwm-workspace--create-silently t))
+      (dotimes (_ (min exwm-workspace-switch-create-limit
+                       (1+ (- frame-or-index
+                              (exwm-workspace--count)))))
+        (make-frame))
+      (run-hooks 'exwm-workspace-list-change-hook))
+    (exwm-workspace-switch frame-or-index)))
+
+;;;###autoload
+(defun exwm-workspace-swap (workspace1 workspace2)
+  "Interchange position of WORKSPACE1 with that of WORKSPACE2."
+  (interactive
+   (unless (and (derived-mode-p 'exwm-mode)
+                ;; The prompt is invisible in fullscreen mode.
+                (exwm-layout--fullscreen-p))
+     (let (w1 w2)
+       (let ((exwm-workspace--prompt-add-allowed t)
+             (exwm-workspace--prompt-delete-allowed t))
+         (setq w1 (exwm-workspace--prompt-for-workspace
+                   "Pick a workspace [+/-]: ")))
+       (setq w2 (exwm-workspace--prompt-for-workspace
+                 (format "Swap workspace %d with: "
+                         (exwm-workspace--position w1))))
+       (list w1 w2))))
+  (exwm--log)
+  (let ((pos1 (exwm-workspace--position workspace1))
+        (pos2 (exwm-workspace--position workspace2)))
+    (if (or (not pos1) (not pos2) (= pos1 pos2))
+        (user-error "[EXWM] Cannot swap %s and %s" workspace1 workspace2)
+      (setf (elt exwm-workspace--list pos1) workspace2)
+      (setf (elt exwm-workspace--list pos2) workspace1)
+      ;; Update the _NET_WM_DESKTOP property of each X window affected.
+      (dolist (pair exwm--id-buffer-alist)
+        (when (memq (buffer-local-value 'exwm--frame (cdr pair))
+                    (list workspace1 workspace2))
+          (exwm-workspace--set-desktop (car pair))))
+      (xcb:flush exwm--connection)
+      (when (memq exwm-workspace--current (list workspace1 workspace2))
+        ;; With the current workspace involved, lots of stuffs need refresh.
+        (set-frame-parameter exwm-workspace--current 'exwm-selected-window
+                             (selected-window))
+        (exwm-workspace-switch exwm-workspace--current t))
+      (run-hooks 'exwm-workspace-list-change-hook))))
+
+;;;###autoload
+(defun exwm-workspace-move (workspace nth)
+  "Move WORKSPACE to the NTH position.
+
+When called interactively, prompt for a workspace and move current one just
+before it."
+  (interactive
+   (cond
+    ((null current-prefix-arg)
+     (unless (and (derived-mode-p 'exwm-mode)
+                  ;; The prompt is invisible in fullscreen mode.
+                  (exwm-layout--fullscreen-p))
+       (list exwm-workspace--current
+             (exwm-workspace--position
+              (exwm-workspace--prompt-for-workspace "Move workspace to: ")))))
+    ((and (integerp current-prefix-arg)
+          (<= 0 current-prefix-arg (exwm-workspace--count)))
+     (list exwm-workspace--current current-prefix-arg))
+    (t (list exwm-workspace--current 0))))
+  (exwm--log)
+  (let ((pos (exwm-workspace--position workspace))
+        flag start end index)
+    (if (= nth pos)
+        (user-error "[EXWM] Cannot move to same position")
+      ;; Set if the current workspace is involved.
+      (setq flag (or (eq workspace exwm-workspace--current)
+                     (eq (elt exwm-workspace--list nth)
+                         exwm-workspace--current)))
+      ;; Do the move.
+      (with-no-warnings                 ;For Emacs 24.
+        (pop (nthcdr pos exwm-workspace--list)))
+      (push workspace (nthcdr nth exwm-workspace--list))
+      ;; Update the _NET_WM_DESKTOP property of each X window affected.
+      (setq start (min pos nth)
+            end (max pos nth))
+      (dolist (pair exwm--id-buffer-alist)
+        (setq index (exwm-workspace--position
+                     (buffer-local-value 'exwm--frame (cdr pair))))
+        (unless (or (< index start) (> index end))
+          (exwm-workspace--set-desktop (car pair))))
+      (when flag
+        ;; With the current workspace involved, lots of stuffs need refresh.
+        (set-frame-parameter exwm-workspace--current 'exwm-selected-window
+                             (selected-window))
+        (exwm-workspace-switch exwm-workspace--current t))
+      (run-hooks 'exwm-workspace-list-change-hook))))
+
+;;;###autoload
+(defun exwm-workspace-add (&optional index)
+  "Add a workspace as the INDEX-th workspace, or the last one if INDEX is nil.
+
+INDEX must not exceed the current number of workspaces."
+  (interactive)
+  (exwm--log "%s" index)
+  (if (and index
+           ;; No need to move if it's the last one.
+           (< index (exwm-workspace--count)))
+      (exwm-workspace-move (make-frame) index)
+    (make-frame)))
+
+;;;###autoload
+(defun exwm-workspace-delete (&optional frame-or-index)
+  "Delete the workspace FRAME-OR-INDEX."
+  (interactive)
+  (exwm--log "%s" frame-or-index)
+  (when (< 1 (exwm-workspace--count))
+    (let ((frame (if frame-or-index
+                     (exwm-workspace--workspace-from-frame-or-index
+                      frame-or-index)
+                   exwm-workspace--current)))
+      (exwm-workspace--get-remove-frame-next-workspace frame)
+      (delete-frame frame))))
+
+(defun exwm-workspace--set-desktop (id)
+  "Set _NET_WM_DESKTOP for X window ID."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (let ((desktop (exwm-workspace--position exwm--frame)))
+      (setq exwm--desktop desktop)
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ewmh:set-_NET_WM_DESKTOP
+                         :window id
+                         :data desktop)))))
+
+;;;###autoload
+(cl-defun exwm-workspace-move-window (frame-or-index &optional id)
+  "Move window ID to workspace FRAME-OR-INDEX."
+  (interactive (list
+                (cond
+                 ((null current-prefix-arg)
+                  (let ((exwm-workspace--prompt-add-allowed t)
+                        (exwm-workspace--prompt-delete-allowed t))
+                    (exwm-workspace--prompt-for-workspace "Move to [+/-]: ")))
+                 ((and (integerp current-prefix-arg)
+                       (<= 0 current-prefix-arg (exwm-workspace--count)))
+                  current-prefix-arg)
+                 (t 0))))
+  (let ((frame (exwm-workspace--workspace-from-frame-or-index frame-or-index))
+        old-frame container)
+    (unless id (setq id (exwm--buffer->id (window-buffer))))
+    (unless id
+      (cl-return-from exwm-workspace-move-window))
+    (exwm--log "Moving #x%x to %s" id frame-or-index)
+    (with-current-buffer (exwm--id->buffer id)
+      (unless (eq exwm--frame frame)
+        (unless exwm-workspace-show-all-buffers
+          (let ((name (replace-regexp-in-string "^\\s-*" "" (buffer-name))))
+            (exwm-workspace-rename-buffer
+             (if (eq frame exwm-workspace--current)
+                 name
+               (concat " " name)))))
+        (setq old-frame exwm--frame
+              exwm--frame frame)
+        (if (not exwm--floating-frame)
+            ;; Tiling.
+            (if (get-buffer-window nil frame)
+                (when (eq frame exwm-workspace--current)
+                  (exwm-layout--refresh frame))
+              (set-window-buffer (get-buffer-window nil t)
+                                 (other-buffer nil t))
+              (unless (eq frame exwm-workspace--current)
+                ;; Clear the 'exwm-selected-window' frame parameter.
+                (set-frame-parameter frame 'exwm-selected-window nil))
+              (set-window-buffer (frame-selected-window frame)
+                                 (exwm--id->buffer id))
+              (if (eq frame exwm-workspace--current)
+                  (select-window (frame-selected-window frame))
+                (unless (exwm-workspace--active-p frame)
+                  (exwm-layout--hide id))))
+          ;; Floating.
+          (setq container (frame-parameter exwm--floating-frame
+                                           'exwm-container))
+          (unless (equal (frame-parameter old-frame 'exwm-randr-monitor)
+                         (frame-parameter frame 'exwm-randr-monitor))
+            (with-slots (x y)
+                (xcb:+request-unchecked+reply exwm--connection
+                    (make-instance 'xcb:GetGeometry
+                                   :drawable container))
+              (with-slots ((x1 x)
+                           (y1 y))
+                  (exwm-workspace--get-geometry old-frame)
+                (with-slots ((x2 x)
+                             (y2 y))
+                    (exwm-workspace--get-geometry frame)
+                  (setq x (+ x (- x2 x1))
+                        y (+ y (- y2 y1)))))
+              (exwm--set-geometry id x y nil nil)
+              (exwm--set-geometry container x y nil nil)))
+          (if (exwm-workspace--minibuffer-own-frame-p)
+              (if (eq frame exwm-workspace--current)
+                  (select-window (frame-root-window exwm--floating-frame))
+                (select-window (frame-selected-window exwm-workspace--current))
+                (unless (exwm-workspace--active-p frame)
+                  (exwm-layout--hide id)))
+            ;; The frame needs to be recreated since it won't use the
+            ;; minibuffer on the new workspace.
+            ;; The code is mostly copied from `exwm-floating--set-floating'.
+            (let* ((old-frame exwm--floating-frame)
+                   (new-frame
+                    (with-current-buffer
+                        (or (get-buffer "*scratch*")
+                            (progn
+                              (set-buffer-major-mode
+                               (get-buffer-create "*scratch*"))
+                              (get-buffer "*scratch*")))
+                      (make-frame
+                       `((minibuffer . ,(minibuffer-window frame))
+                         (left . ,(* window-min-width -100))
+                         (top . ,(* window-min-height -100))
+                         (width . ,window-min-width)
+                         (height . ,window-min-height)
+                         (unsplittable . t)))))
+                   (outer-id (string-to-number
+                              (frame-parameter new-frame
+                                               'outer-window-id)))
+                   (window-id (string-to-number
+                               (frame-parameter new-frame 'window-id)))
+                   (window (frame-root-window new-frame)))
+              (set-frame-parameter new-frame 'exwm-outer-id outer-id)
+              (set-frame-parameter new-frame 'exwm-id window-id)
+              (set-frame-parameter new-frame 'exwm-container container)
+              (make-frame-invisible new-frame)
+              (set-frame-size new-frame
+                              (frame-pixel-width old-frame)
+                              (frame-pixel-height old-frame)
+                              t)
+              (xcb:+request exwm--connection
+                  (make-instance 'xcb:ReparentWindow
+                                 :window outer-id
+                                 :parent container
+                                 :x 0 :y 0))
+              (xcb:flush exwm--connection)
+              (with-current-buffer (exwm--id->buffer id)
+                (setq window-size-fixed nil
+                      exwm--floating-frame new-frame)
+                (set-window-dedicated-p (frame-root-window old-frame) nil)
+                (remove-hook 'window-configuration-change-hook
+                             #'exwm-layout--refresh)
+                (set-window-buffer window (current-buffer))
+                (add-hook 'window-configuration-change-hook
+                          #'exwm-layout--refresh)
+                (set-window-dedicated-p window t))
+              ;; Select a tiling window and delete the old frame.
+              (select-window (frame-selected-window exwm-workspace--current))
+              (delete-frame old-frame)
+              ;; The rest is the same.
+              (make-frame-visible new-frame)
+              (exwm--set-geometry outer-id 0 0 nil nil)
+              (xcb:flush exwm--connection)
+              (redisplay)
+              (if (eq frame exwm-workspace--current)
+                  (with-current-buffer (exwm--id->buffer id)
+                    (select-window (frame-root-window exwm--floating-frame)))
+                (unless (exwm-workspace--active-p frame)
+                  (exwm-layout--hide id)))))
+          ;; Update the 'exwm-selected-window' frame parameter.
+          (when (not (eq frame exwm-workspace--current))
+            (with-current-buffer (exwm--id->buffer id)
+              (set-frame-parameter frame 'exwm-selected-window
+                                   (frame-root-window
+                                    exwm--floating-frame)))))
+        ;; Set _NET_WM_DESKTOP.
+        (exwm-workspace--set-desktop id)
+        (xcb:flush exwm--connection)))
+    (setq exwm-workspace--switch-history-outdated t)))
+
+;;;###autoload
+(defun exwm-workspace-switch-to-buffer (buffer-or-name)
+  "Make the current Emacs window display another buffer."
+  (interactive
+   (let ((inhibit-quit t))
+     ;; Show all buffers
+     (unless exwm-workspace-show-all-buffers
+       (dolist (pair exwm--id-buffer-alist)
+         (with-current-buffer (cdr pair)
+           (when (= ?\s (aref (buffer-name) 0))
+             (let ((buffer-list-update-hook
+                    (remq #'exwm-input--on-buffer-list-update
+                          buffer-list-update-hook)))
+               (rename-buffer (substring (buffer-name) 1)))))))
+     (prog1
+         (with-local-quit
+           (list (get-buffer (read-buffer-to-switch "Switch to buffer: "))))
+       ;; Hide buffers on other workspaces
+       (unless exwm-workspace-show-all-buffers
+         (dolist (pair exwm--id-buffer-alist)
+           (with-current-buffer (cdr pair)
+             (unless (or (eq exwm--frame exwm-workspace--current)
+                         (= ?\s (aref (buffer-name) 0)))
+               (let ((buffer-list-update-hook
+                      (remq #'exwm-input--on-buffer-list-update
+                            buffer-list-update-hook)))
+                 (rename-buffer (concat " " (buffer-name)))))))))))
+  (exwm--log)
+  (when buffer-or-name
+    (with-current-buffer buffer-or-name
+      (if (derived-mode-p 'exwm-mode)
+          ;; EXWM buffer.
+          (if (eq exwm--frame exwm-workspace--current)
+              ;; On the current workspace.
+              (if (not exwm--floating-frame)
+                  (switch-to-buffer buffer-or-name)
+                ;; Select the floating frame.
+                (select-frame-set-input-focus exwm--floating-frame)
+                (select-window (frame-root-window exwm--floating-frame)))
+            ;; On another workspace.
+            (if exwm-layout-show-all-buffers
+                (exwm-workspace-move-window exwm-workspace--current
+                                            exwm--id)
+              (let ((window (get-buffer-window buffer-or-name exwm--frame)))
+                (if window
+                    (set-frame-parameter exwm--frame
+                                         'exwm-selected-window window)
+                  (set-window-buffer (frame-selected-window exwm--frame)
+                                     buffer-or-name)))
+              (exwm-workspace-switch exwm--frame)))
+        ;; Ordinary buffer.
+        (switch-to-buffer buffer-or-name)))))
+
+(defun exwm-workspace-rename-buffer (newname)
+  "Rename a buffer."
+  (let ((hidden (= ?\s (aref newname 0)))
+        (basename (replace-regexp-in-string "<[0-9]+>$" "" newname))
+        (counter 1)
+        tmp)
+    (when hidden (setq basename (substring basename 1)))
+    (setq newname basename)
+    (while (and (setq tmp (or (get-buffer newname)
+                              (get-buffer (concat " " newname))))
+                (not (eq tmp (current-buffer))))
+      (setq newname (format "%s<%d>" basename (cl-incf counter))))
+    (let ((buffer-list-update-hook
+           (remq #'exwm-input--on-buffer-list-update
+                 buffer-list-update-hook)))
+      (rename-buffer (concat (and hidden " ") newname)))))
+
+(defun exwm-workspace--x-create-frame (orig-fun params)
+  "Set override-redirect on the frame created by `x-create-frame'."
+  (exwm--log)
+  (let ((frame (funcall orig-fun params)))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ChangeWindowAttributes
+                       :window (string-to-number
+                                (frame-parameter frame 'outer-window-id))
+                       :value-mask xcb:CW:OverrideRedirect
+                       :override-redirect 1))
+    (xcb:flush exwm--connection)
+    frame))
+
+(defsubst exwm-workspace--minibuffer-attached-p ()
+  "Return non-nil if the minibuffer is attached.
+
+Please check `exwm-workspace--minibuffer-own-frame-p' first."
+  (assq (frame-parameter exwm-workspace--minibuffer 'exwm-container)
+        exwm-workspace--id-struts-alist))
+
+;;;###autoload
+(defun exwm-workspace-attach-minibuffer ()
+  "Attach the minibuffer so that it always shows."
+  (interactive)
+  (exwm--log)
+  (when (and (exwm-workspace--minibuffer-own-frame-p)
+             (not (exwm-workspace--minibuffer-attached-p)))
+    ;; Reset the frame size.
+    (set-frame-height exwm-workspace--minibuffer 1)
+    (redisplay)                       ;FIXME.
+    (setq exwm-workspace--attached-minibuffer-height
+          (frame-pixel-height exwm-workspace--minibuffer))
+    (exwm-workspace--show-minibuffer)
+    (let ((container (frame-parameter exwm-workspace--minibuffer
+                                      'exwm-container)))
+      (push (cons container
+                  (if (eq exwm-workspace-minibuffer-position 'top)
+                      (vector 0 0 exwm-workspace--attached-minibuffer-height 0)
+                    (vector 0 0 0 exwm-workspace--attached-minibuffer-height)))
+            exwm-workspace--id-struts-alist)
+      (exwm-workspace--update-struts)
+      (exwm-workspace--update-workareas)
+      (dolist (f exwm-workspace--list)
+        (exwm-workspace--set-fullscreen f)))))
+
+;;;###autoload
+(defun exwm-workspace-detach-minibuffer ()
+  "Detach the minibuffer so that it automatically hides."
+  (interactive)
+  (exwm--log)
+  (when (and (exwm-workspace--minibuffer-own-frame-p)
+             (exwm-workspace--minibuffer-attached-p))
+    (setq exwm-workspace--attached-minibuffer-height 0)
+    (let ((container (frame-parameter exwm-workspace--minibuffer
+                                      'exwm-container)))
+      (setq exwm-workspace--id-struts-alist
+            (assq-delete-all container exwm-workspace--id-struts-alist))
+      (exwm-workspace--update-struts)
+      (exwm-workspace--update-workareas)
+      (dolist (f exwm-workspace--list)
+        (exwm-workspace--set-fullscreen f))
+      (exwm-workspace--hide-minibuffer))))
+
+;;;###autoload
+(defun exwm-workspace-toggle-minibuffer ()
+  "Attach the minibuffer if it's detached, or detach it if it's attached."
+  (interactive)
+  (exwm--log)
+  (when (exwm-workspace--minibuffer-own-frame-p)
+    (if (exwm-workspace--minibuffer-attached-p)
+        (exwm-workspace-detach-minibuffer)
+      (exwm-workspace-attach-minibuffer))))
+
+(defun exwm-workspace--update-minibuffer-height (&optional echo-area)
+  "Update the minibuffer frame height."
+  (unless (exwm-workspace--client-p)
+    (let ((height
+           (with-current-buffer
+               (window-buffer (minibuffer-window exwm-workspace--minibuffer))
+             (max 1
+                  (if echo-area
+                      (let ((width (frame-width exwm-workspace--minibuffer))
+                            (result 0))
+                        (mapc (lambda (i)
+                                (setq result
+                                      (+ result
+                                         (ceiling (1+ (length i)) width))))
+                              (split-string (or (current-message) "") "\n"))
+                        result)
+                    (count-screen-lines))))))
+      (when (and (integerp max-mini-window-height)
+                 (> height max-mini-window-height))
+        (setq height max-mini-window-height))
+      (exwm--log "%s" height)
+      (set-frame-height exwm-workspace--minibuffer height))))
+
+(defun exwm-workspace--on-ConfigureNotify (data _synthetic)
+  "Adjust the container to fit the minibuffer frame."
+  (let ((obj (make-instance 'xcb:ConfigureNotify))
+        workarea y)
+    (xcb:unmarshal obj data)
+    (with-slots (window height) obj
+      (when (eq (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id)
+                window)
+        (exwm--log)
+        (when (and (floatp max-mini-window-height)
+                   (> height (* max-mini-window-height
+                                (exwm-workspace--current-height))))
+          (setq height (floor
+                        (* max-mini-window-height
+                           (exwm-workspace--current-height))))
+          (xcb:+request exwm--connection
+              (make-instance 'xcb:ConfigureWindow
+                             :window window
+                             :value-mask xcb:ConfigWindow:Height
+                             :height height)))
+        (when (/= (exwm-workspace--count) (length exwm-workspace--workareas))
+          ;; There is a chance the workareas are not updated timely.
+          (exwm-workspace--update-workareas))
+        (setq workarea (elt exwm-workspace--workareas
+                            exwm-workspace-current-index)
+              y (if (eq exwm-workspace-minibuffer-position 'top)
+                    (- (aref workarea 1)
+                       exwm-workspace--attached-minibuffer-height)
+                  (+ (aref workarea 1) (aref workarea 3) (- height)
+                     exwm-workspace--attached-minibuffer-height)))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ConfigureWindow
+                           :window (frame-parameter exwm-workspace--minibuffer
+                                                    'exwm-container)
+                           :value-mask (logior xcb:ConfigWindow:Y
+                                               xcb:ConfigWindow:Height)
+                           :y y
+                           :height height))
+        (xcb:flush exwm--connection)))))
+
+(defun exwm-workspace--display-buffer (buffer alist)
+  "Display BUFFER as if the current workspace is selected."
+  ;; Only when the floating minibuffer frame is selected.
+  ;; This also protect this functions from being recursively called.
+  (when (eq (selected-frame) exwm-workspace--minibuffer)
+    (with-selected-frame exwm-workspace--current
+      (display-buffer buffer alist))))
+
+(defun exwm-workspace--show-minibuffer ()
+  "Show the minibuffer frame."
+  (exwm--log)
+  ;; Cancel pending timer.
+  (when exwm-workspace--display-echo-area-timer
+    (cancel-timer exwm-workspace--display-echo-area-timer)
+    (setq exwm-workspace--display-echo-area-timer nil))
+  ;; Show the minibuffer frame.
+  (unless (exwm-workspace--minibuffer-attached-p)
+    (exwm--set-geometry (frame-parameter exwm-workspace--minibuffer
+                                         'exwm-container)
+                        nil nil
+                        (frame-pixel-width exwm-workspace--minibuffer)
+                        (frame-pixel-height exwm-workspace--minibuffer)))
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ConfigureWindow
+                     :window (frame-parameter exwm-workspace--minibuffer
+                                              'exwm-container)
+                     :value-mask xcb:ConfigWindow:StackMode
+                     :stack-mode xcb:StackMode:Above))
+  (xcb:flush exwm--connection))
+
+(defun exwm-workspace--hide-minibuffer ()
+  "Hide the minibuffer frame."
+  (exwm--log)
+  ;; Hide the minibuffer frame.
+  (if (exwm-workspace--minibuffer-attached-p)
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ConfigureWindow
+                         :window (frame-parameter exwm-workspace--minibuffer
+                                                  'exwm-container)
+                         :value-mask (logior (if exwm-manage--desktop
+                                                 xcb:ConfigWindow:Sibling
+                                               0)
+                                             xcb:ConfigWindow:StackMode)
+                         :sibling exwm-manage--desktop
+                         :stack-mode (if exwm-manage--desktop
+                                         xcb:StackMode:Above
+                                       xcb:StackMode:Below)))
+    (exwm--set-geometry (frame-parameter exwm-workspace--minibuffer
+                                         'exwm-container)
+                        nil nil 1 1))
+  (xcb:flush exwm--connection))
+
+(defun exwm-workspace--on-minibuffer-setup ()
+  "Run in minibuffer-setup-hook to show the minibuffer and its container."
+  (exwm--log)
+  (when (and (= 1 (minibuffer-depth))
+             (not (exwm-workspace--client-p)))
+    (add-hook 'post-command-hook #'exwm-workspace--update-minibuffer-height)
+    (exwm-workspace--show-minibuffer))
+  ;; FIXME: This is a temporary fix for the *Completions* buffer not
+  ;;        being correctly fitted by its displaying window.  As with
+  ;;        `exwm-workspace--display-buffer', the problem is caused by
+  ;;        the fact that the minibuffer (rather than the workspace)
+  ;;        frame is the 'selected frame'.  `get-buffer-window' will
+  ;;        fail to retrieve the correct window.  It's likely there are
+  ;;        other related issues.
+  ;; This is not required by Emacs 24.
+  (when (fboundp 'window-preserve-size)
+    (let ((window (get-buffer-window "*Completions*"
+                                     exwm-workspace--current)))
+      (when window
+        (fit-window-to-buffer window)
+        (window-preserve-size window)))))
+
+(defun exwm-workspace--on-minibuffer-exit ()
+  "Run in minibuffer-exit-hook to hide the minibuffer container."
+  (exwm--log)
+  (when (and (= 1 (minibuffer-depth))
+             (not (exwm-workspace--client-p)))
+    (remove-hook 'post-command-hook #'exwm-workspace--update-minibuffer-height)
+    (exwm-workspace--hide-minibuffer)))
+
+(defun exwm-workspace--on-echo-area-dirty ()
+  "Run when new message arrives to show the echo area and its container."
+  (when (and (not (active-minibuffer-window))
+             (not (exwm-workspace--client-p))
+             (or (current-message)
+                 cursor-in-echo-area))
+    (exwm-workspace--update-minibuffer-height t)
+    (exwm-workspace--show-minibuffer)
+    (unless (or (not exwm-workspace-display-echo-area-timeout)
+                exwm-input--during-command ;e.g. read-event
+                input-method-use-echo-area)
+      (setq exwm-workspace--display-echo-area-timer
+            (run-with-timer exwm-workspace-display-echo-area-timeout nil
+                            #'exwm-workspace--echo-area-maybe-clear)))))
+
+(defun exwm-workspace--echo-area-maybe-clear ()
+  "Eventually clear the echo area container."
+  (exwm--log)
+  (if (not (current-message))
+      (exwm-workspace--on-echo-area-clear)
+    ;; Reschedule.
+    (cancel-timer exwm-workspace--display-echo-area-timer)
+    (setq exwm-workspace--display-echo-area-timer
+          (run-with-timer exwm-workspace-display-echo-area-timeout nil
+                          #'exwm-workspace--echo-area-maybe-clear))))
+
+(defun exwm-workspace--on-echo-area-clear ()
+  "Run in echo-area-clear-hook to hide echo area container."
+  (unless (exwm-workspace--client-p)
+    (unless (active-minibuffer-window)
+      (exwm-workspace--hide-minibuffer))
+    (when exwm-workspace--display-echo-area-timer
+      (cancel-timer exwm-workspace--display-echo-area-timer)
+      (setq exwm-workspace--display-echo-area-timer nil))))
+
+(defun exwm-workspace--set-desktop-geometry ()
+  "Set _NET_DESKTOP_GEOMETRY."
+  (exwm--log)
+  ;; We don't support large desktop so it's the same with screen size.
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_DESKTOP_GEOMETRY
+                     :window exwm--root
+                     :width (x-display-pixel-width)
+                     :height (x-display-pixel-height))))
+
+(defun exwm-workspace--add-frame-as-workspace (frame)
+  "Configure frame FRAME to be treated as a workspace."
+  (exwm--log "%s" frame)
+  (setq exwm-workspace--list (nconc exwm-workspace--list (list frame)))
+  (let ((outer-id (string-to-number (frame-parameter frame
+                                                     'outer-window-id)))
+        (window-id (string-to-number (frame-parameter frame 'window-id)))
+        (container (xcb:generate-id exwm--connection))
+        frame-colormap frame-visual frame-depth)
+    ;; Save window IDs
+    (set-frame-parameter frame 'exwm-outer-id outer-id)
+    (set-frame-parameter frame 'exwm-id window-id)
+    (set-frame-parameter frame 'exwm-container container)
+    ;; In case it's created by emacsclient.
+    (set-frame-parameter frame 'client nil)
+    ;; Copy RandR frame parameters from the first workspace to
+    ;; prevent potential problems.  The values do not matter here as
+    ;; they'll be updated by the RandR module later.
+    (let ((w (car exwm-workspace--list)))
+      (dolist (param '(exwm-randr-monitor
+                       exwm-geometry))
+        (set-frame-parameter frame param (frame-parameter w param))))
+    ;; Support transparency on the container X window when the Emacs frame
+    ;; does.  Note that in addition to setting the visual, colormap and depth
+    ;; we must also reset the `:border-pixmap', as its default value is
+    ;; relative to the parent window, which might have a different depth.
+    (let* ((vdc (exwm--get-visual-depth-colormap exwm--connection outer-id)))
+      (setq frame-visual (car vdc))
+      (setq frame-depth (cadr vdc))
+      (setq frame-colormap (caddr vdc)))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:CreateWindow
+                       :depth frame-depth
+                       :wid container
+                       :parent exwm--root
+                       :x -1
+                       :y -1
+                       :width 1
+                       :height 1
+                       :border-width 0
+                       :class xcb:WindowClass:InputOutput
+                       :visual frame-visual
+                       :value-mask (logior xcb:CW:BackPixmap
+                                           xcb:CW:BorderPixel
+                                           xcb:CW:Colormap
+                                           xcb:CW:OverrideRedirect)
+                       :background-pixmap xcb:BackPixmap:None
+                       :border-pixel 0
+                       :colormap frame-colormap
+                       :override-redirect 1))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ConfigureWindow
+                       :window container
+                       :value-mask xcb:ConfigWindow:StackMode
+                       :stack-mode xcb:StackMode:Below))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                       :window container
+                       :data
+                       (format "EXWM workspace %d frame container"
+                               (exwm-workspace--position frame))))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ReparentWindow
+                       :window outer-id :parent container :x 0 :y 0))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:icccm:set-WM_STATE
+                       :window outer-id
+                       :state xcb:icccm:WM_STATE:NormalState
+                       :icon xcb:Window:None))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:MapWindow :window container)))
+  (xcb:flush exwm--connection)
+  ;; Delay making the workspace fullscreen until Emacs becomes idle
+  (exwm--defer 0 #'set-frame-parameter frame 'fullscreen 'fullboth)
+  ;; Update EWMH properties.
+  (exwm-workspace--update-ewmh-props)
+  (if exwm-workspace--create-silently
+      (setq exwm-workspace--switch-history-outdated t)
+    (let ((original-index exwm-workspace-current-index))
+      (exwm-workspace-switch frame t)
+      (message "Created %s as workspace %d; switched from %d"
+               frame exwm-workspace-current-index original-index))
+    (run-hooks 'exwm-workspace-list-change-hook)))
+
+(defun exwm-workspace--get-remove-frame-next-workspace (frame)
+  "Return the next workspace if workspace FRAME is removed.
+
+All X windows currently on workspace FRAME will be automatically moved to
+the next workspace."
+  (let* ((index (exwm-workspace--position frame))
+         (lastp (= index (1- (exwm-workspace--count))))
+         (nextw (elt exwm-workspace--list (+ index (if lastp -1 +1)))))
+    ;; Clients need to be moved to some other workspace before this being
+    ;; removed.
+    (dolist (pair exwm--id-buffer-alist)
+      (with-current-buffer (cdr pair)
+        (when (eq exwm--frame frame)
+          (exwm-workspace-move-window nextw exwm--id))))
+    nextw))
+
+(defun exwm-workspace--remove-frame-as-workspace (frame)
+  "Stop treating frame FRAME as a workspace."
+  ;; TODO: restore all frame parameters (e.g. exwm-workspace, buffer-predicate,
+  ;; etc)
+  (exwm--log "Removing frame `%s' as workspace" frame)
+  (let* ((index (exwm-workspace--position frame))
+         (nextw (exwm-workspace--get-remove-frame-next-workspace frame)))
+    ;; Need to remove the workspace from the list in order for
+    ;; the correct calculation of indexes.
+    (setq exwm-workspace--list (delete frame exwm-workspace--list))
+    ;; Update the _NET_WM_DESKTOP property of each X window affected.
+    (dolist (pair exwm--id-buffer-alist)
+      (when (<= (1- index)
+                (exwm-workspace--position (buffer-local-value 'exwm--frame
+                                                              (cdr pair))))
+        (exwm-workspace--set-desktop (car pair))))
+    ;; If the current workspace is deleted, switch to next one.
+    (when (eq frame exwm-workspace--current)
+      (exwm-workspace-switch nextw)))
+  ;; Reparent out the frame.
+  (let ((outer-id (frame-parameter frame 'exwm-outer-id)))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:UnmapWindow
+                       :window outer-id))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ReparentWindow
+                       :window outer-id
+                       :parent exwm--root
+                       :x 0
+                       :y 0))
+    ;; Reset the override-redirect.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ChangeWindowAttributes
+                       :window outer-id
+                       :value-mask xcb:CW:OverrideRedirect
+                       :override-redirect 0))
+    ;; Remove fullscreen state.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_STATE
+                       :window outer-id
+                       :data nil))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:MapWindow
+                       :window outer-id)))
+  ;; Destroy the container.
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:DestroyWindow
+                     :window (frame-parameter frame 'exwm-container)))
+  (xcb:flush exwm--connection)
+  ;; Update EWMH properties.
+  (exwm-workspace--update-ewmh-props)
+  ;; Update switch history.
+  (setq exwm-workspace--switch-history-outdated t)
+  (run-hooks 'exwm-workspace-list-change-hook))
+
+(defun exwm-workspace--on-delete-frame (frame)
+  "Hook run upon `delete-frame' that tears down FRAME's configuration as a workspace."
+  (cond
+   ((not (exwm-workspace--workspace-p frame))
+    (exwm--log "Frame `%s' is not a workspace" frame))
+   (t
+    (when (= 1 (exwm-workspace--count))
+      ;; The user managed to delete the last workspace, so create a new one.
+      (exwm--log "Last workspace deleted; create a new one")
+      ;; TODO: this makes sense in the hook.  But we need a function that takes
+      ;; care of converting a workspace into a regular unmanaged frame.
+      (let ((exwm-workspace--create-silently t))
+        (make-frame)))
+    (exwm-workspace--remove-frame-as-workspace frame)
+    (remhash frame exwm-workspace--client-p-hash-table))))
+
+(defun exwm-workspace--on-after-make-frame (frame)
+  "Hook run upon `make-frame' that configures FRAME as a workspace."
+  (cond
+   ((exwm-workspace--workspace-p frame)
+    (exwm--log "Frame `%s' is already a workspace" frame))
+   ((not (display-graphic-p frame))
+    (exwm--log "Frame `%s' is not graphical" frame))
+   ((not (string-equal
+          (replace-regexp-in-string "\\.0$" ""
+                                    (slot-value exwm--connection 'display))
+          (replace-regexp-in-string "\\.0$" ""
+                                    (frame-parameter frame 'display))))
+    (exwm--log "Frame `%s' is on a different DISPLAY (%S instead of %S)"
+               frame
+               (frame-parameter frame 'display)
+               (slot-value exwm--connection 'display)))
+   ((frame-parameter frame 'unsplittable)
+    ;; We create floating frames with the "unsplittable" parameter set.
+    ;; Though it may not be a floating frame, we won't treat an
+    ;; unsplittable frame as a workspace anyway.
+    (exwm--log "Frame `%s' is floating" frame))
+   (t
+    (exwm--log "Adding frame `%s' as workspace" frame)
+    (exwm-workspace--add-frame-as-workspace frame))))
+
+(defun exwm-workspace--update-ewmh-props ()
+  "Update EWMH properties to match the workspace list."
+  (exwm--log)
+  (let ((num-workspaces (exwm-workspace--count)))
+    ;; Avoid setting 0 desktops.
+    (when (= 0 num-workspaces)
+      (setq num-workspaces 1))
+    ;; Set _NET_NUMBER_OF_DESKTOPS.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_NUMBER_OF_DESKTOPS
+                       :window exwm--root :data num-workspaces))
+    ;; Set _NET_DESKTOP_GEOMETRY.
+    (exwm-workspace--set-desktop-geometry)
+    ;; Update workareas.
+    (exwm-workspace--update-workareas))
+  (xcb:flush exwm--connection))
+
+(defun exwm-workspace--modify-all-x-frames-parameters (new-x-parameters)
+  "Modifies `window-system-default-frame-alist' for the X Window System.
+NEW-X-PARAMETERS is an alist of frame parameters, merged into current
+`window-system-default-frame-alist' for the X Window System.  The parameters are
+applied to all subsequently created X frames."
+  (exwm--log)
+  ;; The parameters are modified in place; take current
+  ;; ones or insert a new X-specific list.
+  (let ((x-parameters (or (assq 'x window-system-default-frame-alist)
+                          (let ((new-x-parameters '(x)))
+                            (push new-x-parameters
+                                  window-system-default-frame-alist)
+                            new-x-parameters))))
+    (setf (cdr x-parameters)
+          (append new-x-parameters (cdr x-parameters)))))
+
+(defun exwm-workspace--handle-focus-in (_orig-func _event)
+  "Replacement for `handle-focus-in'."
+  (interactive "e"))
+
+(defun exwm-workspace--handle-focus-out (_orig-func _event)
+  "Replacement for `handle-focus-out'."
+  (interactive "e"))
+
+(defun exwm-workspace--init-minibuffer-frame ()
+  (exwm--log)
+  ;; Initialize workspaces without minibuffers.
+  (setq exwm-workspace--minibuffer
+        (make-frame '((window-system . x) (minibuffer . only)
+                      (left . 10000) (right . 10000)
+                      (width . 1) (height . 1)
+                      (client . nil))))
+  ;; This is the only usable minibuffer frame.
+  (setq default-minibuffer-frame exwm-workspace--minibuffer)
+  (exwm-workspace--modify-all-x-frames-parameters
+   '((minibuffer . nil)))
+  (let ((outer-id (string-to-number
+                   (frame-parameter exwm-workspace--minibuffer
+                                    'outer-window-id)))
+        (window-id (string-to-number
+                    (frame-parameter exwm-workspace--minibuffer
+                                     'window-id)))
+        (container (xcb:generate-id exwm--connection)))
+    (set-frame-parameter exwm-workspace--minibuffer
+                         'exwm-outer-id outer-id)
+    (set-frame-parameter exwm-workspace--minibuffer 'exwm-id window-id)
+    (set-frame-parameter exwm-workspace--minibuffer 'exwm-container
+                         container)
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:CreateWindow
+                       :depth 0
+                       :wid container
+                       :parent exwm--root
+                       :x 0
+                       :y 0
+                       :width 1
+                       :height 1
+                       :border-width 0
+                       :class xcb:WindowClass:InputOutput
+                       :visual 0
+                       :value-mask (logior xcb:CW:BackPixmap
+                                           xcb:CW:OverrideRedirect)
+                       :background-pixmap xcb:BackPixmap:ParentRelative
+                       :override-redirect 1))
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                       :window container
+                       :data "EXWM minibuffer container"))
+    ;; Reparent the minibuffer frame to the container.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ReparentWindow
+                       :window outer-id :parent container :x 0 :y 0))
+    ;; Map the container.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:MapWindow
+                       :window container))
+    ;; Attach event listener for monitoring the frame
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ChangeWindowAttributes
+                       :window outer-id
+                       :value-mask xcb:CW:EventMask
+                       :event-mask xcb:EventMask:StructureNotify))
+    (xcb:+event exwm--connection 'xcb:ConfigureNotify
+                #'exwm-workspace--on-ConfigureNotify))
+  ;; Show/hide minibuffer / echo area when they're active/inactive.
+  (add-hook 'minibuffer-setup-hook #'exwm-workspace--on-minibuffer-setup)
+  (add-hook 'minibuffer-exit-hook #'exwm-workspace--on-minibuffer-exit)
+  (setq exwm-workspace--timer
+        (run-with-idle-timer 0 t #'exwm-workspace--on-echo-area-dirty))
+  (add-hook 'echo-area-clear-hook #'exwm-workspace--on-echo-area-clear)
+  ;; The default behavior of `display-buffer' (indirectly called by
+  ;; `minibuffer-completion-help') is not correct here.
+  (cl-pushnew '(exwm-workspace--display-buffer) display-buffer-alist
+              :test #'equal))
+
+(defun exwm-workspace--exit-minibuffer-frame ()
+  (exwm--log)
+  ;; Only on minibuffer-frame.
+  (remove-hook 'minibuffer-setup-hook #'exwm-workspace--on-minibuffer-setup)
+  (remove-hook 'minibuffer-exit-hook #'exwm-workspace--on-minibuffer-exit)
+  (remove-hook 'echo-area-clear-hook #'exwm-workspace--on-echo-area-clear)
+  (when exwm-workspace--timer
+    (cancel-timer exwm-workspace--timer)
+    (setq exwm-workspace--timer nil))
+  (setq display-buffer-alist
+        (cl-delete '(exwm-workspace--display-buffer) display-buffer-alist
+                   :test #'equal))
+  (setq default-minibuffer-frame nil)
+  (let ((id (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id)))
+    (when (and exwm-workspace--minibuffer id)
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ReparentWindow
+                         :window id
+                         :parent exwm--root
+                         :x 0
+                         :y 0)))
+    (setq exwm-workspace--minibuffer nil)))
+
+(defun exwm-workspace--init ()
+  "Initialize workspace module."
+  (exwm--log)
+  (exwm-workspace--init-switch-map)
+  ;; Prevent unexpected exit
+  (setq exwm-workspace--fullscreen-frame-count 0)
+  (exwm-workspace--modify-all-x-frames-parameters
+   '((internal-border-width . 0)))
+  (let ((initial-workspaces (frame-list)))
+    (if (not (exwm-workspace--minibuffer-own-frame-p))
+        ;; Initialize workspaces with minibuffers.
+        (when (< 1 (length initial-workspaces))
+          ;; Exclude the initial frame.
+          (dolist (i initial-workspaces)
+            (unless (frame-parameter i 'window-id)
+              (setq initial-workspaces (delq i initial-workspaces))))
+          (setq exwm-workspace--client
+                (frame-parameter (car initial-workspaces) 'client))
+          (let ((f (car initial-workspaces)))
+            ;; Remove the possible internal border.
+            (set-frame-parameter f 'internal-border-width 0)
+            ;; Prevent user from deleting the first frame by accident.
+            (set-frame-parameter f 'client nil)))
+      (exwm-workspace--init-minibuffer-frame)
+      ;; Remove/hide existing frames.
+      (dolist (f initial-workspaces)
+        (if (frame-parameter f 'client)
+            (progn
+              (unless exwm-workspace--client
+                (setq exwm-workspace--client (frame-parameter f 'client)))
+              (make-frame-invisible f))
+          (when (eq 'x (framep f))   ;do not delete the initial frame.
+            (delete-frame f))))
+      ;; Recreate one frame with the external minibuffer set.
+      (setq initial-workspaces (list (make-frame '((window-system . x)
+                                                   (client . nil))))))
+    ;; Prevent `other-buffer' from selecting already displayed EXWM buffers.
+    (modify-all-frames-parameters
+     '((buffer-predicate . exwm-layout--other-buffer-predicate)))
+    ;; Create remaining workspaces.
+    (dotimes (_ (- exwm-workspace-number (length initial-workspaces)))
+      (nconc initial-workspaces (list (make-frame '((window-system . x)
+                                                    (client . nil))))))
+    ;; Configure workspaces
+    (let ((exwm-workspace--create-silently t))
+      (dolist (i initial-workspaces)
+        (exwm-workspace--add-frame-as-workspace i))))
+  (xcb:flush exwm--connection)
+  ;; We have to advice `x-create-frame' or every call to it would hang EXWM
+  (advice-add 'x-create-frame :around #'exwm-workspace--x-create-frame)
+  ;; We have to manually handle focus-in and focus-out events for Emacs
+  ;; frames.
+  (advice-add 'handle-focus-in :around #'exwm-workspace--handle-focus-in)
+  (advice-add 'handle-focus-out :around #'exwm-workspace--handle-focus-out)
+  ;; Make new frames create new workspaces.
+  (add-hook 'after-make-frame-functions
+            #'exwm-workspace--on-after-make-frame)
+  (add-hook 'delete-frame-functions #'exwm-workspace--on-delete-frame)
+  (when (exwm-workspace--minibuffer-own-frame-p)
+    (add-hook 'exwm-input--event-hook
+              #'exwm-workspace--on-echo-area-clear))
+  ;; Switch to the first workspace
+  (exwm-workspace-switch 0 t)
+  ;; Prevent frame parameters introduced by this module from being
+  ;; saved/restored.
+  (dolist (i '(exwm-active exwm-outer-id exwm-id exwm-container exwm-geometry
+                           exwm-selected-window exwm-urgency fullscreen))
+    (unless (assq i frameset-filter-alist)
+      (push (cons i :never) frameset-filter-alist))))
+
+(defun exwm-workspace--exit ()
+  "Exit the workspace module."
+  (exwm--log)
+  (when (exwm-workspace--minibuffer-own-frame-p)
+    (exwm-workspace--exit-minibuffer-frame))
+  (advice-remove 'x-create-frame #'exwm-workspace--x-create-frame)
+  (advice-remove 'handle-focus-in #'exwm-workspace--handle-focus-in)
+  (advice-remove 'handle-focus-out #'exwm-workspace--handle-focus-out)
+  (remove-hook 'after-make-frame-functions
+               #'exwm-workspace--on-after-make-frame)
+  (remove-hook 'delete-frame-functions
+               #'exwm-workspace--on-delete-frame)
+  (when (exwm-workspace--minibuffer-own-frame-p)
+    (remove-hook 'exwm-input--event-hook
+                 #'exwm-workspace--on-echo-area-clear))
+  ;; Hide & reparent out all frames (save-set can't be used here since
+  ;; X windows will be re-mapped).
+  (setq exwm-workspace--current nil)
+  (dolist (i exwm-workspace--list)
+    (exwm-workspace--remove-frame-as-workspace i)
+    (modify-frame-parameters i '((exwm-selected-window . nil)
+                                 (exwm-urgency . nil)
+                                 (exwm-outer-id . nil)
+                                 (exwm-id . nil)
+                                 (exwm-container . nil)
+                                 ;; (internal-border-width . nil) ; integerp
+                                 ;; (client . nil)
+                                 (fullscreen . nil)
+                                 (buffer-predicate . nil))))
+  ;; Restore the 'client' frame parameter (before `exwm-exit').
+  (when exwm-workspace--client
+    (dolist (f exwm-workspace--list)
+      (set-frame-parameter f 'client exwm-workspace--client))
+    (when (exwm-workspace--minibuffer-own-frame-p)
+      (set-frame-parameter exwm-workspace--minibuffer 'client
+                           exwm-workspace--client))
+    (setq exwm-workspace--client nil)))
+
+(defun exwm-workspace--post-init ()
+  "The second stage in the initialization of the workspace module."
+  (exwm--log)
+  (when exwm-workspace--client
+    ;; Reset the 'fullscreen' frame parameter to make emacsclinet frames
+    ;; fullscreen (even without the RandR module enabled).
+    (dolist (i exwm-workspace--list)
+      (set-frame-parameter i 'fullscreen nil)
+      (set-frame-parameter i 'fullscreen 'fullboth)))
+  ;; Wait until all workspace frames are resized.
+  (with-timeout (1)
+    (while (< exwm-workspace--fullscreen-frame-count (exwm-workspace--count))
+      (accept-process-output nil 0.1)))
+  (setq exwm-workspace--fullscreen-frame-count nil))
+
+
+
+(provide 'exwm-workspace)
+
+;;; exwm-workspace.el ends here
diff --git a/third_party/exwm/exwm-xim.el b/third_party/exwm/exwm-xim.el
new file mode 100644
index 0000000000..9589648d22
--- /dev/null
+++ b/third_party/exwm/exwm-xim.el
@@ -0,0 +1,807 @@
+;;; exwm-xim.el --- XIM Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2019-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module adds XIM support for EXWM and allows sending characters
+;; generated by any Emacs's builtin input method (info node `Input Methods')
+;; to X windows.
+
+;; This module is essentially an X input method server utilizing Emacs as
+;; its backend.  It talks with X windows through the XIM protocol.  The XIM
+;; protocol is quite flexible by itself, stating that an implementation can
+;; create network connections of various types as well as make use of an
+;; existing X connection for communication, and that an IM server may
+;; support multiple transport versions, various input styles and several
+;; event flow modals, etc.  Here we only make choices that are most popular
+;; among other IM servers and more importantly, practical for Emacs to act
+;; as an IM server:
+;;
+;; + Packets are transported on top of an X connection like most IMEs.
+;; + Only transport version 0.0 (i.e. only-CM & Property-with-CM) is
+;;   supported (same as "IM Server Developers Kit", adopted by most IMEs).
+;; + Only support static event flow, on-demand-synchronous method.
+;; + Only "root-window" input style is supported.
+
+;; To use this module, first load and enable it as follows:
+;;
+;;    (require 'exwm-xim)
+;;    (exwm-xim-enable)
+;;
+;; A keybinding for `toggle-input-method' is probably required to turn on &
+;; off an input method (default to `default-input-method').  It's bound to
+;; 'C-\' by default and can be made reachable when working with X windows:
+;;
+;;    (push ?\C-\\ exwm-input-prefix-keys)
+;;
+;; It's also required (and error-prone) to setup environment variables to
+;; make applications actually use this input method.  Typically the
+;; following lines should be inserted into '~/.xinitrc'.
+;;
+;;    export XMODIFIERS=@im=exwm-xim
+;;    export GTK_IM_MODULE=xim
+;;    export QT_IM_MODULE=xim
+;;    export CLUTTER_IM_MODULE=xim
+
+;; References:
+;; + XIM (http://www.x.org/releases/X11R7.6/doc/libX11/specs/XIM/xim.html)
+;; + IMdkit (http://xorg.freedesktop.org/archive/unsupported/lib/IMdkit/)
+;; + UIM (https://github.com/uim/uim)
+
+;;; Code:
+
+(eval-when-compile (require 'cl-lib))
+
+(require 'xcb-keysyms)
+(require 'xcb-xim)
+
+(require 'exwm-core)
+(require 'exwm-input)
+
+(defconst exwm-xim--locales
+  "@locale=\
+aa,af,ak,am,an,anp,ar,as,ast,ayc,az,be,bem,ber,bg,bhb,bho,bn,bo,br,brx,bs,byn,\
+ca,ce,cmn,crh,cs,csb,cv,cy,da,de,doi,dv,dz,el,en,es,et,eu,fa,ff,fi,fil,fo,fr,\
+fur,fy,ga,gd,gez,gl,gu,gv,ha,hak,he,hi,hne,hr,hsb,ht,hu,hy,ia,id,ig,ik,is,it,\
+iu,iw,ja,ka,kk,kl,km,kn,ko,kok,ks,ku,kw,ky,lb,lg,li,li,lij,lo,lt,lv,lzh,mag,\
+mai,mg,mhr,mi,mk,ml,mn,mni,mr,ms,mt,my,nan,nb,nds,ne,nhn,niu,nl,nn,nr,nso,oc,\
+om,or,os,pa,pa,pap,pl,ps,pt,quz,raj,ro,ru,rw,sa,sat,sc,sd,se,shs,si,sid,sk,sl,\
+so,sq,sr,ss,st,sv,sw,szl,ta,tcy,te,tg,th,the,ti,tig,tk,tl,tn,tr,ts,tt,ug,uk,\
+unm,ur,uz,ve,vi,wa,wae,wal,wo,xh,yi,yo,yue,zh,zu,\
+C,no"
+  "All supported locales (stolen from glibc).")
+
+(defconst exwm-xim--default-error
+  (make-instance 'xim:error
+                 :im-id 0
+                 :ic-id 0
+                 :flag xim:error-flag:invalid-both
+                 :error-code xim:error-code:bad-something
+                 :length 0
+                 :type 0
+                 :detail nil)
+  "Default error returned to clients.")
+
+(defconst exwm-xim--default-im-attrs
+  (list (make-instance 'xim:XIMATTR
+                       :id 0
+                       :type xim:ATTRIBUTE-VALUE-TYPE:xim-styles
+                       :length (length xlib:XNQueryInputStyle)
+                       :attribute xlib:XNQueryInputStyle))
+  "Default IM attrs returned to clients.")
+
+(defconst exwm-xim--default-ic-attrs
+  (list (make-instance 'xim:XICATTR
+                       :id 0
+                       :type xim:ATTRIBUTE-VALUE-TYPE:long-data
+                       :length (length xlib:XNInputStyle)
+                       :attribute xlib:XNInputStyle)
+        (make-instance 'xim:XICATTR
+                       :id 1
+                       :type xim:ATTRIBUTE-VALUE-TYPE:window
+                       :length (length xlib:XNClientWindow)
+                       :attribute xlib:XNClientWindow)
+        ;; Required by e.g. xterm.
+        (make-instance 'xim:XICATTR
+                       :id 2
+                       :type xim:ATTRIBUTE-VALUE-TYPE:window
+                       :length (length xlib:XNFocusWindow)
+                       :attribute xlib:XNFocusWindow))
+  "Default IC attrs returned to clients.")
+
+(defconst exwm-xim--default-styles
+  (make-instance 'xim:XIMStyles
+                 :number nil
+                 :styles (list (logior xlib:XIMPreeditNothing
+                                       xlib:XIMStatusNothing)))
+  "Default styles: root-window, i.e. no preediting or status display support.")
+
+(defconst exwm-xim--default-attributes
+  (list (make-instance 'xim:XIMATTRIBUTE
+                       :id 0
+                       :length nil
+                       :value exwm-xim--default-styles))
+  "Default IM/IC attributes returned to clients.")
+
+(defvar exwm-xim--conn nil
+  "The X connection for initiating other XIM connections.")
+(defvar exwm-xim--event-xwin nil
+  "X window for initiating new XIM connections.")
+(defvar exwm-xim--server-client-plist '(nil nil)
+  "Plist mapping server window to [X connection, client window, byte-order].")
+(defvar exwm-xim--client-server-plist '(nil nil)
+  "Plist mapping client window to server window.")
+(defvar exwm-xim--property-index 0 "For generating a unique property name.")
+(defvar exwm-xim--im-id 0 "Last IM ID.")
+(defvar exwm-xim--ic-id 0 "Last IC ID.")
+
+;; X11 atoms.
+(defvar exwm-xim--@server nil)
+(defvar exwm-xim--LOCALES nil)
+(defvar exwm-xim--TRANSPORT nil)
+(defvar exwm-xim--XIM_SERVERS nil)
+(defvar exwm-xim--_XIM_PROTOCOL nil)
+(defvar exwm-xim--_XIM_XCONNECT nil)
+
+(defvar exwm-xim-buffer-p nil
+  "Whether current buffer is used by exwm-xim.")
+(make-variable-buffer-local 'exwm-xim-buffer-p)
+
+(defun exwm-xim--on-SelectionRequest (data _synthetic)
+  "Handle SelectionRequest events on IMS window.
+
+Such events would be received when clients query for LOCALES or TRANSPORT."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:SelectionRequest))
+        value fake-event)
+    (xcb:unmarshal evt data)
+    (with-slots (time requestor selection target property) evt
+      (setq value (cond ((= target exwm-xim--LOCALES)
+                         ;; Return supported locales.
+                         exwm-xim--locales)
+                        ((= target exwm-xim--TRANSPORT)
+                         ;; Use XIM over an X connection.
+                         "@transport=X/")))
+      (when value
+        ;; Change the property.
+        (xcb:+request exwm-xim--conn
+            (make-instance 'xcb:ChangeProperty
+                           :mode xcb:PropMode:Replace
+                           :window requestor
+                           :property property
+                           :type target
+                           :format 8
+                           :data-len (length value)
+                           :data value))
+        ;; Send a SelectionNotify event.
+        (setq fake-event (make-instance 'xcb:SelectionNotify
+                                        :time time
+                                        :requestor requestor
+                                        :selection selection
+                                        :target target
+                                        :property property))
+        (xcb:+request exwm-xim--conn
+            (make-instance 'xcb:SendEvent
+                           :propagate 0
+                           :destination requestor
+                           :event-mask xcb:EventMask:NoEvent
+                           :event (xcb:marshal fake-event exwm-xim--conn)))
+        (xcb:flush exwm-xim--conn)))))
+
+(cl-defun exwm-xim--on-ClientMessage-0 (data _synthetic)
+  "Handle ClientMessage event on IMS window (new connection).
+
+Such events would be received when clients request for _XIM_XCONNECT.
+A new X connection and server window would be created to communicate with
+this client."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:ClientMessage))
+        conn client-xwin server-xwin)
+    (xcb:unmarshal evt data)
+    (with-slots (window type data) evt
+      (unless (= type exwm-xim--_XIM_XCONNECT)
+        ;; Only handle _XIM_XCONNECT.
+        (exwm--log "Ignore ClientMessage %s" type)
+        (cl-return-from exwm-xim--on-ClientMessage-0))
+      (setq client-xwin (elt (slot-value data 'data32) 0)
+            ;; Create a new X connection and a new server window.
+            conn (xcb:connect)
+            server-xwin (xcb:generate-id conn))
+      (set-process-query-on-exit-flag (slot-value conn 'process) nil)
+      ;; Store this client.
+      (plist-put exwm-xim--server-client-plist server-xwin
+                 `[,conn ,client-xwin nil])
+      (plist-put exwm-xim--client-server-plist client-xwin server-xwin)
+      ;; Select DestroyNotify events on this client window.
+      (xcb:+request exwm-xim--conn
+          (make-instance 'xcb:ChangeWindowAttributes
+                         :window client-xwin
+                         :value-mask xcb:CW:EventMask
+                         :event-mask xcb:EventMask:StructureNotify))
+      (xcb:flush exwm-xim--conn)
+      ;; Handle ClientMessage events from this new connection.
+      (xcb:+event conn 'xcb:ClientMessage #'exwm-xim--on-ClientMessage)
+      ;; Create a communication window.
+      (xcb:+request conn
+          (make-instance 'xcb:CreateWindow
+                         :depth 0
+                         :wid server-xwin
+                         :parent exwm--root
+                         :x 0
+                         :y 0
+                         :width 1
+                         :height 1
+                         :border-width 0
+                         :class xcb:WindowClass:InputOutput
+                         :visual 0
+                         :value-mask xcb:CW:OverrideRedirect
+                         :override-redirect 1))
+      (xcb:flush conn)
+      ;; Send connection establishment ClientMessage.
+      (setf window client-xwin
+            (slot-value data 'data32) `(,server-xwin 0 0 0 0))
+      (slot-makeunbound data 'data8)
+      (slot-makeunbound data 'data16)
+      (xcb:+request exwm-xim--conn
+          (make-instance 'xcb:SendEvent
+                         :propagate 0
+                         :destination client-xwin
+                         :event-mask xcb:EventMask:NoEvent
+                         :event (xcb:marshal evt exwm-xim--conn)))
+      (xcb:flush exwm-xim--conn))))
+
+(cl-defun exwm-xim--on-ClientMessage (data _synthetic)
+  "Handle ClientMessage event on IMS communication window (request).
+
+Such events would be received when clients request for _XIM_PROTOCOL.
+The actual XIM request is in client message data or a property."
+  (exwm--log)
+  (let ((evt (make-instance 'xcb:ClientMessage))
+        conn client-xwin server-xwin)
+    (xcb:unmarshal evt data)
+    (with-slots (format window type data) evt
+      (unless (= type exwm-xim--_XIM_PROTOCOL)
+        (exwm--log "Ignore ClientMessage %s" type)
+        (cl-return-from exwm-xim--on-ClientMessage))
+      (setq server-xwin window
+            conn (plist-get exwm-xim--server-client-plist server-xwin)
+            client-xwin (elt conn 1)
+            conn (elt conn 0))
+      (cond ((= format 8)
+             ;; Data.
+             (exwm-xim--on-request (vconcat (slot-value data 'data8))
+                                   conn client-xwin server-xwin))
+            ((= format 32)
+             ;; Atom.
+             (with-slots (data32) data
+               (with-slots (value)
+                   (xcb:+request-unchecked+reply conn
+                       (make-instance 'xcb:GetProperty
+                                      :delete 1
+                                      :window server-xwin
+                                      :property (elt data32 1)
+                                      :type xcb:GetPropertyType:Any
+                                      :long-offset 0
+                                      :long-length (elt data32 0)))
+                 (when (> (length value) 0)
+                   (exwm-xim--on-request value conn client-xwin
+                                         server-xwin)))))))))
+
+(defun exwm-xim--on-request (data conn client-xwin server-xwin)
+  "Handle an XIM reuqest."
+  (exwm--log)
+  (let ((opcode (elt data 0))
+        ;; Let-bind `xim:lsb' to make pack/unpack functions work correctly.
+        (xim:lsb (elt (plist-get exwm-xim--server-client-plist server-xwin) 2))
+        req replies)
+    (cond ((= opcode xim:opcode:error)
+           (exwm--log "ERROR: %s" data))
+          ((= opcode xim:opcode:connect)
+           (exwm--log "CONNECT")
+           (setq xim:lsb (= (elt data 4) xim:connect-byte-order:lsb-first))
+           ;; Store byte-order.
+           (setf (elt (plist-get exwm-xim--server-client-plist server-xwin) 2)
+                 xim:lsb)
+           (setq req (make-instance 'xim:connect))
+           (xcb:unmarshal req data)
+           (if (and (= (slot-value req 'major-version) 1)
+                    (= (slot-value req 'minor-version) 0)
+                    ;; Do not support authentication.
+                    (= (slot-value req 'number) 0))
+               ;; Accept the connection.
+               (push (make-instance 'xim:connect-reply) replies)
+             ;; Deny it.
+             (push exwm-xim--default-error replies)))
+          ((memq opcode (list xim:opcode:auth-required
+                              xim:opcode:auth-reply
+                              xim:opcode:auth-next
+                              xim:opcode:auth-ng))
+           (exwm--log "AUTH: %d" opcode)
+           ;; Deny any attempt to make authentication.
+           (push exwm-xim--default-error replies))
+          ((= opcode xim:opcode:disconnect)
+           (exwm--log "DISCONNECT")
+           ;; Gracefully disconnect from the client.
+           (exwm-xim--make-request (make-instance 'xim:disconnect-reply)
+                                   conn client-xwin)
+           ;; Destroy the communication window & connection.
+           (xcb:+request conn
+               (make-instance 'xcb:DestroyWindow
+                              :window server-xwin))
+           (xcb:disconnect conn)
+           ;; Clean up cache.
+           (cl-remf exwm-xim--server-client-plist server-xwin)
+           (cl-remf exwm-xim--client-server-plist client-xwin))
+          ((= opcode xim:opcode:open)
+           (exwm--log "OPEN")
+           ;; Note: We make no check here.
+           (setq exwm-xim--im-id (if (< exwm-xim--im-id #xffff)
+                                     (1+ exwm-xim--im-id)
+                                   1))
+           (setq replies
+                 (list
+                  (make-instance 'xim:open-reply
+                                 :im-id exwm-xim--im-id
+                                 :im-attrs-length nil
+                                 :im-attrs exwm-xim--default-im-attrs
+                                 :ic-attrs-length nil
+                                 :ic-attrs exwm-xim--default-ic-attrs)
+                  (make-instance 'xim:set-event-mask
+                                 :im-id exwm-xim--im-id
+                                 :ic-id 0
+                                 ;; Static event flow.
+                                 :forward-event-mask xcb:EventMask:KeyPress
+                                 ;; on-demand-synchronous method.
+                                 :synchronous-event-mask
+                                 xcb:EventMask:NoEvent))))
+          ((= opcode xim:opcode:close)
+           (exwm--log "CLOSE")
+           (setq req (make-instance 'xim:close))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:close-reply
+                                :im-id (slot-value req 'im-id))
+                 replies))
+          ((= opcode xim:opcode:trigger-notify)
+           (exwm--log "TRIGGER-NOTIFY")
+           ;; Only static event flow modal is supported.
+           (push exwm-xim--default-error replies))
+          ((= opcode xim:opcode:encoding-negotiation)
+           (exwm--log "ENCODING-NEGOTIATION")
+           (setq req (make-instance 'xim:encoding-negotiation))
+           (xcb:unmarshal req data)
+           (let ((index (cl-position "COMPOUND_TEXT"
+                                     (mapcar (lambda (i) (slot-value i 'name))
+                                             (slot-value req 'names))
+                                     :test #'equal)))
+             (unless index
+               ;; Fallback to portable character encoding (a subset of ASCII).
+               (setq index -1))
+             (push (make-instance 'xim:encoding-negotiation-reply
+                                  :im-id (slot-value req 'im-id)
+                                  :category
+                                  xim:encoding-negotiation-reply-category:name
+                                  :index index)
+                   replies)))
+          ((= opcode xim:opcode:query-extension)
+           (exwm--log "QUERY-EXTENSION")
+           (setq req (make-instance 'xim:query-extension))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:query-extension-reply
+                                :im-id (slot-value req 'im-id)
+                                ;; No extension support.
+                                :length 0
+                                :extensions nil)
+                 replies))
+          ((= opcode xim:opcode:set-im-values)
+           (exwm--log "SET-IM-VALUES")
+           ;; There's only one possible input method attribute.
+           (setq req (make-instance 'xim:set-im-values))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:set-im-values-reply
+                                :im-id (slot-value req 'im-id))
+                 replies))
+          ((= opcode xim:opcode:get-im-values)
+           (exwm--log "GET-IM-VALUES")
+           (setq req (make-instance 'xim:get-im-values))
+           (let (im-attributes-id)
+             (xcb:unmarshal req data)
+             (setq im-attributes-id (slot-value req 'im-attributes-id))
+             (if (cl-notevery (lambda (i) (= i 0)) im-attributes-id)
+                 ;; Only support one IM attributes.
+                 (push (make-instance 'xim:error
+                                      :im-id (slot-value req 'im-id)
+                                      :ic-id 0
+                                      :flag xim:error-flag:invalid-ic-id
+                                      :error-code xim:error-code:bad-something
+                                      :length 0
+                                      :type 0
+                                      :detail nil)
+                       replies)
+               (push
+                (make-instance 'xim:get-im-values-reply
+                               :im-id (slot-value req 'im-id)
+                               :length nil
+                               :im-attributes exwm-xim--default-attributes)
+                replies))))
+          ((= opcode xim:opcode:create-ic)
+           (exwm--log "CREATE-IC")
+           (setq req (make-instance 'xim:create-ic))
+           (xcb:unmarshal req data)
+           ;; Note: The ic-attributes slot is ignored.
+           (setq exwm-xim--ic-id (if (< exwm-xim--ic-id #xffff)
+                                     (1+ exwm-xim--ic-id)
+                                   1))
+           (push (make-instance 'xim:create-ic-reply
+                                :im-id (slot-value req 'im-id)
+                                :ic-id exwm-xim--ic-id)
+                 replies))
+          ((= opcode xim:opcode:destroy-ic)
+           (exwm--log "DESTROY-IC")
+           (setq req (make-instance 'xim:destroy-ic))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:destroy-ic-reply
+                                :im-id (slot-value req 'im-id)
+                                :ic-id (slot-value req 'ic-id))
+                 replies))
+          ((= opcode xim:opcode:set-ic-values)
+           (exwm--log "SET-IC-VALUES")
+           (setq req (make-instance 'xim:set-ic-values))
+           (xcb:unmarshal req data)
+           ;; We don't distinguish between input contexts.
+           (push (make-instance 'xim:set-ic-values-reply
+                                :im-id (slot-value req 'im-id)
+                                :ic-id (slot-value req 'ic-id))
+                 replies))
+          ((= opcode xim:opcode:get-ic-values)
+           (exwm--log "GET-IC-VALUES")
+           (setq req (make-instance 'xim:get-ic-values))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:get-ic-values-reply
+                                :im-id (slot-value req 'im-id)
+                                :ic-id (slot-value req 'ic-id)
+                                :length nil
+                                :ic-attributes exwm-xim--default-attributes)
+                 replies))
+          ((= opcode xim:opcode:set-ic-focus)
+           (exwm--log "SET-IC-FOCUS")
+           ;; All input contexts are the same.
+           )
+          ((= opcode xim:opcode:unset-ic-focus)
+           (exwm--log "UNSET-IC-FOCUS")
+           ;; All input contexts are the same.
+           )
+          ((= opcode xim:opcode:forward-event)
+           (exwm--log "FORWARD-EVENT")
+           (setq req (make-instance 'xim:forward-event))
+           (xcb:unmarshal req data)
+           (exwm-xim--handle-forward-event-request req xim:lsb conn
+                                                   client-xwin))
+          ((= opcode xim:opcode:sync)
+           (exwm--log "SYNC")
+           (setq req (make-instance 'xim:sync))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:sync-reply
+                                :im-id (slot-value req 'im-id)
+                                :ic-id (slot-value req 'ic-id))
+                 replies))
+          ((= opcode xim:opcode:sync-reply)
+           (exwm--log "SYNC-REPLY"))
+          ((= opcode xim:opcode:reset-ic)
+           (exwm--log "RESET-IC")
+           ;; No context-specific data saved.
+           (setq req (make-instance 'xim:reset-ic))
+           (xcb:unmarshal req data)
+           (push (make-instance 'xim:reset-ic-reply
+                                :im-id (slot-value req 'im-id)
+                                :ic-id (slot-value req 'ic-id)
+                                :length 0
+                                :string "")
+                 replies))
+          ((memq opcode (list xim:opcode:str-conversion-reply
+                              xim:opcode:preedit-start-reply
+                              xim:opcode:preedit-caret-reply))
+           (exwm--log "PREEDIT: %d" opcode)
+           ;; No preedit support.
+           (push exwm-xim--default-error replies))
+          (t
+           (exwm--log "Bad protocol")
+           (push exwm-xim--default-error replies)))
+    ;; Actually send the replies.
+    (when replies
+      (mapc (lambda (reply)
+              (exwm-xim--make-request reply conn client-xwin))
+            replies)
+      (xcb:flush conn))))
+
+(defun exwm-xim--handle-forward-event-request (req lsb conn client-xwin)
+  (let ((im-func (with-current-buffer (window-buffer)
+                   input-method-function))
+        key-event keysym keysyms event result)
+    ;; Note: The flag slot is ignored.
+    ;; Do conversion in client's byte-order.
+    (let ((xcb:lsb lsb))
+      (setq key-event (make-instance 'xcb:KeyPress))
+      (xcb:unmarshal key-event (slot-value req 'event)))
+    (with-slots (detail state) key-event
+      (setq keysym (xcb:keysyms:keycode->keysym exwm-xim--conn detail
+                                                state))
+      (when (/= (car keysym) 0)
+        (setq event (xcb:keysyms:keysym->event
+                     exwm-xim--conn
+                     (car keysym)
+                     (logand state (lognot (cdr keysym)))))))
+    (while (or (slot-value req 'event) unread-command-events)
+      (unless (slot-value req 'event)
+        (setq event (pop unread-command-events))
+        ;; Handle events in (t . EVENT) format.
+        (when (and (consp event)
+                   (eq (car event) t))
+          (setq event (cdr event))))
+      (if (or (not im-func)
+              ;; `list' is the default method.
+              (eq im-func #'list)
+              (not event)
+              ;; Select only printable keys.
+              (not (integerp event)) (> #x20 event) (< #x7e event))
+          ;; Either there is no active input method, or invalid key
+          ;; is detected.
+          (with-slots ((raw-event event)
+                       im-id ic-id serial-number)
+              req
+            (if raw-event
+                (setq event raw-event)
+              (setq keysyms (xcb:keysyms:event->keysyms exwm-xim--conn event))
+              (with-slots (detail state) key-event
+                (setf detail (xcb:keysyms:keysym->keycode exwm-xim--conn
+                                                          (caar keysyms))
+                      state (cdar keysyms)))
+              (setq event (let ((xcb:lsb lsb))
+                            (xcb:marshal key-event conn))))
+            (when event
+              (exwm-xim--make-request
+               (make-instance 'xim:forward-event
+                              :im-id im-id
+                              :ic-id ic-id
+                              :flag xim:commit-flag:synchronous
+                              :serial-number serial-number
+                              :event event)
+               conn client-xwin)))
+        (when (eq exwm--selected-input-mode 'char-mode)
+          ;; Grab keyboard temporarily for char-mode.
+          (exwm-input--grab-keyboard))
+        (unwind-protect
+            (with-temp-buffer
+              ;; This variable is used to test whether exwm-xim is enabled.
+              ;; Used by e.g. pyim-probe.
+              (setq-local exwm-xim-buffer-p t)
+              ;; Always show key strokes.
+              (let ((input-method-use-echo-area t)
+                    (exwm-input-line-mode-passthrough t))
+                (setq result (funcall im-func event))
+                ;; Clear echo area for the input method.
+                (message nil)
+                ;; This also works for portable character encoding.
+                (setq result
+                      (encode-coding-string (concat result)
+                                            'compound-text-with-extensions))
+                (exwm-xim--make-request
+                 (make-instance 'xim:commit-x-lookup-chars
+                                :im-id (slot-value req 'im-id)
+                                :ic-id (slot-value req 'ic-id)
+                                :flag (logior xim:commit-flag:synchronous
+                                              xim:commit-flag:x-lookup-chars)
+                                :length (length result)
+                                :string result)
+                 conn client-xwin)))
+          (when (eq exwm--selected-input-mode 'char-mode)
+            (exwm-input--release-keyboard))))
+      (xcb:flush conn)
+      (setf event nil
+            (slot-value req 'event) nil))))
+
+(defun exwm-xim--make-request (req conn client-xwin)
+  "Make an XIM request REQ via connection CONN.
+
+CLIENT-XWIN would receive a ClientMessage event either telling the client
+the request data or where to fetch the data."
+  (exwm--log)
+  (let ((data (xcb:marshal req))
+        property format client-message-data client-message)
+    (if (<= (length data) 20)
+        ;; Send short requests directly with client messages.
+        (setq format 8
+              ;; Pad to 20 bytes.
+              data (append data (make-list (- 20 (length data)) 0))
+              client-message-data (make-instance 'xcb:ClientMessageData
+                                                 :data8 data))
+      ;; Send long requests with properties.
+      (setq property (exwm--intern-atom (format "_EXWM_XIM_%x"
+                                                exwm-xim--property-index)))
+      (cl-incf exwm-xim--property-index)
+      (xcb:+request conn
+          (make-instance 'xcb:ChangeProperty
+                         :mode xcb:PropMode:Append
+                         :window client-xwin
+                         :property property
+                         :type xcb:Atom:STRING
+                         :format 8
+                         :data-len (length data)
+                         :data data))
+      ;; Also send a client message to notify the client about this property.
+      (setq format 32
+            client-message-data (make-instance 'xcb:ClientMessageData
+                                               :data32 `(,(length data)
+                                                         ,property
+                                                         ;; Pad to 20 bytes.
+                                                         0 0 0))))
+    ;; Send the client message.
+    (setq client-message (make-instance 'xcb:ClientMessage
+                                        :format format
+                                        :window client-xwin
+                                        :type exwm-xim--_XIM_PROTOCOL
+                                        :data client-message-data))
+    (xcb:+request conn
+        (make-instance 'xcb:SendEvent
+                       :propagate 0
+                       :destination client-xwin
+                       :event-mask xcb:EventMask:NoEvent
+                       :event (xcb:marshal client-message conn)))))
+
+(defun exwm-xim--on-DestroyNotify (data synthetic)
+  "Do cleanups on receiving DestroyNotify event.
+
+Such event would be received when the client window is destroyed."
+  (exwm--log)
+  (unless synthetic
+    (let ((evt (make-instance 'xcb:DestroyNotify))
+          conn client-xwin server-xwin)
+      (xcb:unmarshal evt data)
+      (setq client-xwin (slot-value evt 'window)
+            server-xwin (plist-get exwm-xim--client-server-plist client-xwin))
+      (when server-xwin
+        (setq conn (aref (plist-get exwm-xim--server-client-plist server-xwin)
+                         0))
+        (cl-remf exwm-xim--server-client-plist server-xwin)
+        (cl-remf exwm-xim--client-server-plist client-xwin)
+        ;; Destroy the communication window & connection.
+        (xcb:+request conn
+            (make-instance 'xcb:DestroyWindow
+                           :window server-xwin))
+        (xcb:disconnect conn)))))
+
+(cl-defun exwm-xim--init ()
+  "Initialize the XIM module."
+  (exwm--log)
+  (when exwm-xim--conn
+    (cl-return-from exwm-xim--init))
+  ;; Initialize atoms.
+  (setq exwm-xim--@server (exwm--intern-atom "@server=exwm-xim")
+        exwm-xim--LOCALES (exwm--intern-atom "LOCALES")
+        exwm-xim--TRANSPORT (exwm--intern-atom "TRANSPORT")
+        exwm-xim--XIM_SERVERS (exwm--intern-atom "XIM_SERVERS")
+        exwm-xim--_XIM_PROTOCOL (exwm--intern-atom "_XIM_PROTOCOL")
+        exwm-xim--_XIM_XCONNECT (exwm--intern-atom "_XIM_XCONNECT"))
+  ;; Create a new connection and event window.
+  (setq exwm-xim--conn (xcb:connect)
+        exwm-xim--event-xwin (xcb:generate-id exwm-xim--conn))
+  (set-process-query-on-exit-flag (slot-value exwm-xim--conn 'process) nil)
+  ;; Initialize xcb:keysyms module.
+  (xcb:keysyms:init exwm-xim--conn)
+  ;; Listen to SelectionRequest event for connection establishment.
+  (xcb:+event exwm-xim--conn 'xcb:SelectionRequest
+              #'exwm-xim--on-SelectionRequest)
+  ;; Listen to ClientMessage event on IMS window for new XIM connection.
+  (xcb:+event exwm-xim--conn 'xcb:ClientMessage #'exwm-xim--on-ClientMessage-0)
+  ;; Listen to DestroyNotify event to do cleanups.
+  (xcb:+event exwm-xim--conn 'xcb:DestroyNotify #'exwm-xim--on-DestroyNotify)
+  ;; Create the event window.
+  (xcb:+request exwm-xim--conn
+      (make-instance 'xcb:CreateWindow
+                     :depth 0
+                     :wid exwm-xim--event-xwin
+                     :parent exwm--root
+                     :x 0
+                     :y 0
+                     :width 1
+                     :height 1
+                     :border-width 0
+                     :class xcb:WindowClass:InputOutput
+                     :visual 0
+                     :value-mask xcb:CW:OverrideRedirect
+                     :override-redirect 1))
+  ;; Set the selection owner.
+  (xcb:+request exwm-xim--conn
+      (make-instance 'xcb:SetSelectionOwner
+                     :owner exwm-xim--event-xwin
+                     :selection exwm-xim--@server
+                     :time xcb:Time:CurrentTime))
+  ;; Set XIM_SERVERS property on the root window.
+  (xcb:+request exwm-xim--conn
+      (make-instance 'xcb:ChangeProperty
+                     :mode xcb:PropMode:Prepend
+                     :window exwm--root
+                     :property exwm-xim--XIM_SERVERS
+                     :type xcb:Atom:ATOM
+                     :format 32
+                     :data-len 1
+                     :data (funcall (if xcb:lsb
+                                        #'xcb:-pack-u4-lsb
+                                      #'xcb:-pack-u4)
+                                    exwm-xim--@server)))
+  (xcb:flush exwm-xim--conn))
+
+(cl-defun exwm-xim--exit ()
+  "Exit the XIM module."
+  (exwm--log)
+  ;; Close IMS communication connections.
+  (mapc (lambda (i)
+          (when (vectorp i)
+            (xcb:disconnect (elt i 0))))
+        exwm-xim--server-client-plist)
+  ;; Close the IMS connection.
+  (unless exwm-xim--conn
+    (cl-return-from exwm-xim--exit))
+  ;; Remove exwm-xim from XIM_SERVERS.
+  (let ((reply (xcb:+request-unchecked+reply exwm-xim--conn
+                   (make-instance 'xcb:GetProperty
+                                  :delete 1
+                                  :window exwm--root
+                                  :property exwm-xim--XIM_SERVERS
+                                  :type xcb:Atom:ATOM
+                                  :long-offset 0
+                                  :long-length 1000)))
+        unpacked-reply pack unpack)
+    (unless reply
+      (cl-return-from exwm-xim--exit))
+    (setq reply (slot-value reply 'value))
+    (unless (> (length reply) 4)
+      (cl-return-from exwm-xim--exit))
+    (setq reply (vconcat reply)
+          pack (if xcb:lsb #'xcb:-pack-u4-lsb #'xcb:-pack-u4)
+          unpack (if xcb:lsb #'xcb:-unpack-u4-lsb #'xcb:-unpack-u4))
+    (dotimes (i (/ (length reply) 4))
+      (push (funcall unpack reply (* i 4)) unpacked-reply))
+    (setq unpacked-reply (delq exwm-xim--@server unpacked-reply)
+          reply (mapcar pack unpacked-reply))
+    (xcb:+request exwm-xim--conn
+        (make-instance 'xcb:ChangeProperty
+                       :mode xcb:PropMode:Replace
+                       :window exwm--root
+                       :property exwm-xim--XIM_SERVERS
+                       :type xcb:Atom:ATOM
+                       :format 32
+                       :data-len (length reply)
+                       :data reply))
+    (xcb:flush exwm-xim--conn))
+  (xcb:disconnect exwm-xim--conn)
+  (setq exwm-xim--conn nil))
+
+(defun exwm-xim-enable ()
+  "Enable XIM support for EXWM."
+  (exwm--log)
+  (add-hook 'exwm-init-hook #'exwm-xim--init)
+  (add-hook 'exwm-exit-hook #'exwm-xim--exit))
+
+
+
+(provide 'exwm-xim)
+
+;;; exwm-xim.el ends here
diff --git a/third_party/exwm/exwm.el b/third_party/exwm/exwm.el
new file mode 100644
index 0000000000..b025f6b49a
--- /dev/null
+++ b/third_party/exwm/exwm.el
@@ -0,0 +1,1019 @@
+;;; exwm.el --- Emacs X Window Manager  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+;; Author: Chris Feng <chris.w.feng@gmail.com>
+;; Maintainer: Adrián Medraño Calvo <adrian@medranocalvo.com>
+;; Version: 0.26
+;; Package-Requires: ((xelb "0.18"))
+;; Keywords: unix
+;; URL: https://github.com/ch11ng/exwm
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Overview
+;; --------
+;; EXWM (Emacs X Window Manager) is a full-featured tiling X window manager
+;; for Emacs built on top of [XELB](https://github.com/ch11ng/xelb).
+;; It features:
+;; + Fully keyboard-driven operations
+;; + Hybrid layout modes (tiling & stacking)
+;; + Dynamic workspace support
+;; + ICCCM/EWMH compliance
+;; + (Optional) RandR (multi-monitor) support
+;; + (Optional) Built-in system tray
+
+;; Installation & configuration
+;; ----------------------------
+;; Here are the minimal steps to get EXWM working:
+;; 1. Install XELB and EXWM, and make sure they are in `load-path'.
+;; 2. In '~/.emacs', add following lines (please modify accordingly):
+;;
+;;    (require 'exwm)
+;;    (require 'exwm-config)
+;;    (exwm-config-example)
+;;
+;; 3. Link or copy the file 'xinitrc' to '~/.xinitrc'.
+;; 4. Launch EXWM in a console (e.g. tty1) with
+;;
+;;    xinit -- vt01
+;;
+;; You should additionally hide the menu-bar, tool-bar, etc to increase the
+;; usable space.  Please check the wiki (https://github.com/ch11ng/exwm/wiki)
+;; for more detailed instructions on installation, configuration, usage, etc.
+
+;; References:
+;; + dwm (http://dwm.suckless.org/)
+;; + i3 wm (https://i3wm.org/)
+;; + Also see references within each required library.
+
+;;; Code:
+
+(require 'server)
+(require 'exwm-core)
+(require 'exwm-workspace)
+(require 'exwm-layout)
+(require 'exwm-floating)
+(require 'exwm-manage)
+(require 'exwm-input)
+
+(defgroup exwm nil
+  "Emacs X Window Manager."
+  :tag "EXWM"
+  :version "25.3"
+  :group 'applications
+  :prefix "exwm-")
+
+(defcustom exwm-init-hook nil
+  "Normal hook run when EXWM has just finished initialization."
+  :type 'hook)
+
+(defcustom exwm-exit-hook nil
+  "Normal hook run just before EXWM exits."
+  :type 'hook)
+
+(defcustom exwm-update-class-hook nil
+  "Normal hook run when window class is updated."
+  :type 'hook)
+
+(defcustom exwm-update-title-hook nil
+  "Normal hook run when window title is updated."
+  :type 'hook)
+
+(defcustom exwm-blocking-subrs '(x-file-dialog x-popup-dialog x-select-font)
+  "Subrs (primitives) that would normally block EXWM."
+  :type '(repeat function))
+
+(defcustom exwm-replace 'ask
+  "Whether to replace existing window manager."
+  :type '(radio (const :tag "Ask" ask)
+                (const :tag "Replace by default" t)
+                (const :tag "Do not replace" nil)))
+
+(defconst exwm--server-name "server-exwm"
+  "Name of the subordinate Emacs server.")
+
+(defvar exwm--server-process nil "Process of the subordinate Emacs server.")
+
+(defun exwm-reset ()
+  "Reset the state of the selected window (non-fullscreen, line-mode, etc)."
+  (interactive)
+  (exwm--log)
+  (with-current-buffer (window-buffer)
+    (when (derived-mode-p 'exwm-mode)
+      (when (exwm-layout--fullscreen-p)
+        (exwm-layout-unset-fullscreen))
+      ;; Force refresh
+      (exwm-layout--refresh)
+      (call-interactively #'exwm-input-grab-keyboard))))
+
+;;;###autoload
+(defun exwm-restart ()
+  "Restart EXWM."
+  (interactive)
+  (exwm--log)
+  (when (exwm--confirm-kill-emacs "[EXWM] Restart? " 'no-check)
+    (let* ((attr (process-attributes (emacs-pid)))
+           (args (cdr (assq 'args attr)))
+           (ppid (cdr (assq 'ppid attr)))
+           (pargs (cdr (assq 'args (process-attributes ppid)))))
+      (cond
+       ((= ppid 1)
+        ;; The parent is the init process.  This probably means this
+        ;; instance is an emacsclient.  Anyway, start a control instance
+        ;; to manage the subsequent ones.
+        (call-process (car command-line-args))
+        (kill-emacs))
+       ((string= args pargs)
+        ;; This is a subordinate instance.  Return a magic number to
+        ;; inform the parent (control instance) to start another one.
+        (kill-emacs ?R))
+       (t
+        ;; This is the control instance.  Keep starting subordinate
+        ;; instances until told to exit.
+        ;; Run `server-force-stop' if it exists.
+        (run-hooks 'kill-emacs-hook)
+        (with-temp-buffer
+          (while (= ?R (shell-command-on-region (point) (point) args))))
+        (kill-emacs))))))
+
+(defun exwm--update-desktop (xwin)
+  "Update _NET_WM_DESKTOP."
+  (exwm--log "#x%x" xwin)
+  (with-current-buffer (exwm--id->buffer xwin)
+    (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                     (make-instance 'xcb:ewmh:get-_NET_WM_DESKTOP
+                                    :window xwin)))
+          desktop)
+      (when reply
+        (setq desktop (slot-value reply 'value))
+        (cond
+         ((eq desktop 4294967295.)
+          (unless (or (not exwm--floating-frame)
+                      (eq exwm--frame exwm-workspace--current)
+                      (and exwm--desktop
+                           (= desktop exwm--desktop)))
+            (exwm-layout--show xwin (frame-root-window exwm--floating-frame)))
+          (setq exwm--desktop desktop))
+         ((and desktop
+               (< desktop (exwm-workspace--count))
+               (if exwm--desktop
+                   (/= desktop exwm--desktop)
+                 (/= desktop (exwm-workspace--position exwm--frame))))
+          (exwm-workspace-move-window desktop xwin))
+         (t
+          (exwm-workspace--set-desktop xwin)))))))
+
+(defun exwm--update-window-type (id &optional force)
+  "Update _NET_WM_WINDOW_TYPE."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and exwm-window-type (not force))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:ewmh:get-_NET_WM_WINDOW_TYPE
+                                      :window id))))
+        (when reply                     ;nil when destroyed
+          (setq exwm-window-type (append (slot-value reply 'value) nil)))))))
+
+(defun exwm--update-class (id &optional force)
+  "Update WM_CLASS."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and exwm-instance-name exwm-class-name (not force))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:get-WM_CLASS :window id))))
+        (when reply                     ;nil when destroyed
+          (setq exwm-instance-name (slot-value reply 'instance-name)
+                exwm-class-name (slot-value reply 'class-name))
+          (when (and exwm-instance-name exwm-class-name)
+            (run-hooks 'exwm-update-class-hook)))))))
+
+(defun exwm--update-utf8-title (id &optional force)
+  "Update _NET_WM_NAME."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (when (or force (not exwm-title))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:ewmh:get-_NET_WM_NAME :window id))))
+        (when reply                     ;nil when destroyed
+          (setq exwm-title (slot-value reply 'value))
+          (when exwm-title
+            (setq exwm--title-is-utf8 t)
+            (run-hooks 'exwm-update-title-hook)))))))
+
+(defun exwm--update-ctext-title (id &optional force)
+  "Update WM_NAME."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (or exwm--title-is-utf8
+                (and exwm-title (not force)))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:get-WM_NAME :window id))))
+        (when reply                     ;nil when destroyed
+          (setq exwm-title (slot-value reply 'value))
+          (when exwm-title
+            (run-hooks 'exwm-update-title-hook)))))))
+
+(defun exwm--update-title (id)
+  "Update _NET_WM_NAME or WM_NAME."
+  (exwm--log "#x%x" id)
+  (exwm--update-utf8-title id)
+  (exwm--update-ctext-title id))
+
+(defun exwm--update-transient-for (id &optional force)
+  "Update WM_TRANSIENT_FOR."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and exwm-transient-for (not force))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:get-WM_TRANSIENT_FOR
+                                      :window id))))
+        (when reply                     ;nil when destroyed
+          (setq exwm-transient-for (slot-value reply 'value)))))))
+
+(defun exwm--update-normal-hints (id &optional force)
+  "Update WM_NORMAL_HINTS."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and (not force)
+                 (or exwm--normal-hints-x exwm--normal-hints-y
+                     exwm--normal-hints-width exwm--normal-hints-height
+                     exwm--normal-hints-min-width exwm--normal-hints-min-height
+                     exwm--normal-hints-max-width exwm--normal-hints-max-height
+                     ;; FIXME: other fields
+                     ))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:get-WM_NORMAL_HINTS
+                                      :window id))))
+        (when (and reply (slot-value reply 'flags)) ;nil when destroyed
+          (with-slots (flags x y width height min-width min-height max-width
+                             max-height base-width base-height ;; win-gravity
+                             )
+              reply
+            (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:USPosition))
+              (setq exwm--normal-hints-x x exwm--normal-hints-y y))
+            (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:USSize))
+              (setq exwm--normal-hints-width width
+                    exwm--normal-hints-height height))
+            (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PMinSize))
+              (setq exwm--normal-hints-min-width min-width
+                    exwm--normal-hints-min-height min-height))
+            (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PMaxSize))
+              (setq exwm--normal-hints-max-width max-width
+                    exwm--normal-hints-max-height max-height))
+            (unless (or exwm--normal-hints-min-width
+                        (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PBaseSize)))
+              (setq exwm--normal-hints-min-width base-width
+                    exwm--normal-hints-min-height base-height))
+            ;; (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PWinGravity))
+            ;;   (setq exwm--normal-hints-win-gravity win-gravity))
+            (setq exwm--fixed-size
+                  (and exwm--normal-hints-min-width
+                       exwm--normal-hints-min-height
+                       exwm--normal-hints-max-width
+                       exwm--normal-hints-max-height
+                       (/= 0 exwm--normal-hints-min-width)
+                       (/= 0 exwm--normal-hints-min-height)
+                       (= exwm--normal-hints-min-width
+                          exwm--normal-hints-max-width)
+                       (= exwm--normal-hints-min-height
+                          exwm--normal-hints-max-height)))))))))
+
+(defun exwm--update-hints (id &optional force)
+  "Update WM_HINTS."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and (not force) exwm--hints-input exwm--hints-urgency)
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:get-WM_HINTS :window id))))
+        (when (and reply (slot-value reply 'flags)) ;nil when destroyed
+          (with-slots (flags input initial-state) reply
+            (when flags
+              (unless (= 0 (logand flags xcb:icccm:WM_HINTS:InputHint))
+                (setq exwm--hints-input (when input (= 1 input))))
+              (unless (= 0 (logand flags xcb:icccm:WM_HINTS:StateHint))
+                (setq exwm-state initial-state))
+              (unless (= 0 (logand flags xcb:icccm:WM_HINTS:UrgencyHint))
+                (setq exwm--hints-urgency t))))
+          (when (and exwm--hints-urgency
+                     (not (eq exwm--frame exwm-workspace--current)))
+            (unless (frame-parameter exwm--frame 'exwm-urgency)
+              (set-frame-parameter exwm--frame 'exwm-urgency t)
+              (setq exwm-workspace--switch-history-outdated t))))))))
+
+(defun exwm--update-protocols (id &optional force)
+  "Update WM_PROTOCOLS."
+  (exwm--log "#x%x" id)
+  (with-current-buffer (exwm--id->buffer id)
+    (unless (and exwm--protocols (not force))
+      (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                       (make-instance 'xcb:icccm:get-WM_PROTOCOLS
+                                      :window id))))
+        (when reply                     ;nil when destroyed
+          (setq exwm--protocols (append (slot-value reply 'value) nil)))))))
+
+(defun exwm--update-struts-legacy (id)
+  "Update _NET_WM_STRUT."
+  (exwm--log "#x%x" id)
+  (let ((pair (assq id exwm-workspace--id-struts-alist))
+        reply struts)
+    (unless (and pair (< 4 (length (cdr pair))))
+      (setq reply (xcb:+request-unchecked+reply exwm--connection
+                      (make-instance 'xcb:ewmh:get-_NET_WM_STRUT
+                                     :window id)))
+      (when reply
+        (setq struts (slot-value reply 'value))
+        (if pair
+            (setcdr pair struts)
+          (push (cons id struts) exwm-workspace--id-struts-alist))
+        (exwm-workspace--update-struts))
+      ;; Update workareas.
+      (exwm-workspace--update-workareas)
+      ;; Update workspaces.
+      (dolist (f exwm-workspace--list)
+        (exwm-workspace--set-fullscreen f)))))
+
+(defun exwm--update-struts-partial (id)
+  "Update _NET_WM_STRUT_PARTIAL."
+  (exwm--log "#x%x" id)
+  (let ((reply (xcb:+request-unchecked+reply exwm--connection
+                   (make-instance 'xcb:ewmh:get-_NET_WM_STRUT_PARTIAL
+                                  :window id)))
+        struts pair)
+    (when reply
+      (setq struts (slot-value reply 'value)
+            pair (assq id exwm-workspace--id-struts-alist))
+      (if pair
+          (setcdr pair struts)
+        (push (cons id struts) exwm-workspace--id-struts-alist))
+      (exwm-workspace--update-struts))
+    ;; Update workareas.
+    (exwm-workspace--update-workareas)
+    ;; Update workspaces.
+    (dolist (f exwm-workspace--list)
+      (exwm-workspace--set-fullscreen f))))
+
+(defun exwm--update-struts (id)
+  "Update _NET_WM_STRUT_PARTIAL or _NET_WM_STRUT."
+  (exwm--log "#x%x" id)
+  (exwm--update-struts-partial id)
+  (exwm--update-struts-legacy id))
+
+(defun exwm--on-PropertyNotify (data _synthetic)
+  "Handle PropertyNotify event."
+  (let ((obj (make-instance 'xcb:PropertyNotify))
+        atom id buffer)
+    (xcb:unmarshal obj data)
+    (setq id (slot-value obj 'window)
+          atom (slot-value obj 'atom))
+    (exwm--log "atom=%s(%s)" (x-get-atom-name atom exwm-workspace--current) atom)
+    (setq buffer (exwm--id->buffer id))
+    (if (not (buffer-live-p buffer))
+        ;; Properties of unmanaged X windows.
+        (cond ((= atom xcb:Atom:_NET_WM_STRUT)
+               (exwm--update-struts-legacy id))
+              ((= atom xcb:Atom:_NET_WM_STRUT_PARTIAL)
+               (exwm--update-struts-partial id)))
+      (with-current-buffer buffer
+        (cond ((= atom xcb:Atom:_NET_WM_WINDOW_TYPE)
+               (exwm--update-window-type id t))
+              ((= atom xcb:Atom:WM_CLASS)
+               (exwm--update-class id t))
+              ((= atom xcb:Atom:_NET_WM_NAME)
+               (exwm--update-utf8-title id t))
+              ((= atom xcb:Atom:WM_NAME)
+               (exwm--update-ctext-title id t))
+              ((= atom xcb:Atom:WM_TRANSIENT_FOR)
+               (exwm--update-transient-for id t))
+              ((= atom xcb:Atom:WM_NORMAL_HINTS)
+               (exwm--update-normal-hints id t))
+              ((= atom xcb:Atom:WM_HINTS)
+               (exwm--update-hints id t))
+              ((= atom xcb:Atom:WM_PROTOCOLS)
+               (exwm--update-protocols id t))
+              ((= atom xcb:Atom:_NET_WM_USER_TIME)) ;ignored
+              (t
+               (exwm--log "Unhandled: %s(%d)"
+                          (x-get-atom-name atom exwm-workspace--current)
+                          atom)))))))
+
+(defun exwm--on-ClientMessage (raw-data _synthetic)
+  "Handle ClientMessage event."
+  (let ((obj (make-instance 'xcb:ClientMessage))
+        type id data)
+    (xcb:unmarshal obj raw-data)
+    (setq type (slot-value obj 'type)
+          id (slot-value obj 'window)
+          data (slot-value (slot-value obj 'data) 'data32))
+    (exwm--log "atom=%s(%s)" (x-get-atom-name type exwm-workspace--current)
+               type)
+    (cond
+     ;; _NET_NUMBER_OF_DESKTOPS.
+     ((= type xcb:Atom:_NET_NUMBER_OF_DESKTOPS)
+      (let ((current (exwm-workspace--count))
+            (requested (elt data 0)))
+        ;; Only allow increasing/decreasing the workspace number by 1.
+        (cond
+         ((< current requested)
+          (make-frame))
+         ((and (> current requested)
+               (> current 1))
+          (let ((frame (car (last exwm-workspace--list))))
+            (exwm-workspace--get-remove-frame-next-workspace frame)
+            (delete-frame frame))))))
+     ;; _NET_CURRENT_DESKTOP.
+     ((= type xcb:Atom:_NET_CURRENT_DESKTOP)
+      (exwm-workspace-switch (elt data 0)))
+     ;; _NET_ACTIVE_WINDOW.
+     ((= type xcb:Atom:_NET_ACTIVE_WINDOW)
+      (let ((buffer (exwm--id->buffer id))
+            iconic window)
+        (when (buffer-live-p buffer)
+          (with-current-buffer buffer
+            (when (eq exwm--frame exwm-workspace--current)
+              (if exwm--floating-frame
+                  (select-frame exwm--floating-frame)
+                (setq iconic (exwm-layout--iconic-state-p))
+                (when iconic
+                  ;; State change: iconic => normal.
+                  (set-window-buffer (frame-selected-window exwm--frame)
+                                     (current-buffer)))
+                ;; Focus transfer.
+                (setq window (get-buffer-window nil t))
+                (when (or iconic
+                          (not (eq window (selected-window))))
+                  (select-window window))))))))
+     ;; _NET_CLOSE_WINDOW.
+     ((= type xcb:Atom:_NET_CLOSE_WINDOW)
+      (let ((buffer (exwm--id->buffer id)))
+        (when (buffer-live-p buffer)
+          (exwm--defer 0 #'kill-buffer buffer))))
+     ;; _NET_WM_MOVERESIZE
+     ((= type xcb:Atom:_NET_WM_MOVERESIZE)
+      (let ((direction (elt data 2))
+            (buffer (exwm--id->buffer id)))
+        (unless (and buffer
+                     (not (buffer-local-value 'exwm--floating-frame buffer)))
+          (cond ((= direction
+                    xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_KEYBOARD)
+                 ;; FIXME
+                 )
+                ((= direction
+                    xcb:ewmh:_NET_WM_MOVERESIZE_MOVE_KEYBOARD)
+                 ;; FIXME
+                 )
+                ((= direction xcb:ewmh:_NET_WM_MOVERESIZE_CANCEL)
+                 (exwm-floating--stop-moveresize))
+                ;; In case it's a workspace frame.
+                ((and (not buffer)
+                      (catch 'break
+                        (dolist (f exwm-workspace--list)
+                          (when (or (eq id (frame-parameter f 'exwm-outer-id))
+                                    (eq id (frame-parameter f 'exwm-id)))
+                            (throw 'break t)))
+                        nil)))
+                (t
+                 ;; In case it's a floating frame,
+                 ;; move the corresponding X window instead.
+                 (unless buffer
+                   (catch 'break
+                     (dolist (pair exwm--id-buffer-alist)
+                       (with-current-buffer (cdr pair)
+                         (when
+                             (and exwm--floating-frame
+                                  (or (eq id
+                                          (frame-parameter exwm--floating-frame
+                                                           'exwm-outer-id))
+                                      (eq id
+                                          (frame-parameter exwm--floating-frame
+                                                           'exwm-id))))
+                           (setq id exwm--id)
+                           (throw 'break nil))))))
+                 ;; Start to move it.
+                 (exwm-floating--start-moveresize id direction))))))
+     ;; _NET_REQUEST_FRAME_EXTENTS
+     ((= type xcb:Atom:_NET_REQUEST_FRAME_EXTENTS)
+      (let ((buffer (exwm--id->buffer id))
+            top btm)
+        (if (or (not buffer)
+                (not (buffer-local-value 'exwm--floating-frame buffer)))
+            (setq top 0
+                  btm 0)
+          (setq top (window-header-line-height)
+                btm (window-mode-line-height)))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ewmh:set-_NET_FRAME_EXTENTS
+                           :window id
+                           :left 0
+                           :right 0
+                           :top top
+                           :bottom btm)))
+      (xcb:flush exwm--connection))
+     ;; _NET_WM_DESKTOP.
+     ((= type xcb:Atom:_NET_WM_DESKTOP)
+      (let ((buffer (exwm--id->buffer id)))
+        (when (buffer-live-p buffer)
+          (exwm-workspace-move-window (elt data 0) id))))
+     ;; _NET_WM_STATE
+     ((= type xcb:Atom:_NET_WM_STATE)
+      (let ((action (elt data 0))
+            (props (list (elt data 1) (elt data 2)))
+            (buffer (exwm--id->buffer id))
+            props-new)
+        ;; only support _NET_WM_STATE_FULLSCREEN / _NET_WM_STATE_ADD for frames
+        (when (and (not buffer)
+                   (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props)
+                   (= action xcb:ewmh:_NET_WM_STATE_ADD))
+          (xcb:+request
+              exwm--connection
+              (make-instance 'xcb:ewmh:set-_NET_WM_STATE
+                             :window id
+                             :data (vector xcb:Atom:_NET_WM_STATE_FULLSCREEN)))
+          (xcb:flush exwm--connection))
+        (when buffer                    ;ensure it's managed
+          (with-current-buffer buffer
+            ;; _NET_WM_STATE_FULLSCREEN
+            (when (or (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props)
+                      (memq xcb:Atom:_NET_WM_STATE_ABOVE props))
+              (cond ((= action xcb:ewmh:_NET_WM_STATE_ADD)
+                     (unless (exwm-layout--fullscreen-p)
+                       (exwm-layout-set-fullscreen id))
+                     (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new))
+                    ((= action xcb:ewmh:_NET_WM_STATE_REMOVE)
+                     (when (exwm-layout--fullscreen-p)
+                       (exwm-layout-unset-fullscreen id)))
+                    ((= action xcb:ewmh:_NET_WM_STATE_TOGGLE)
+                     (if (exwm-layout--fullscreen-p)
+                         (exwm-layout-unset-fullscreen id)
+                       (exwm-layout-set-fullscreen id)
+                       (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new)))))
+            ;; _NET_WM_STATE_DEMANDS_ATTENTION
+            ;; FIXME: check (may require other properties set)
+            (when (memq xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION props)
+              (when (= action xcb:ewmh:_NET_WM_STATE_ADD)
+                (unless (eq exwm--frame exwm-workspace--current)
+                  (set-frame-parameter exwm--frame 'exwm-urgency t)
+                  (setq exwm-workspace--switch-history-outdated t)))
+              ;; xcb:ewmh:_NET_WM_STATE_REMOVE?
+              ;; xcb:ewmh:_NET_WM_STATE_TOGGLE?
+              )
+            (xcb:+request exwm--connection
+                (make-instance 'xcb:ewmh:set-_NET_WM_STATE
+                               :window id :data (vconcat props-new)))
+            (xcb:flush exwm--connection)))))
+     ((= type xcb:Atom:WM_PROTOCOLS)
+      (let ((type (elt data 0)))
+        (cond ((= type xcb:Atom:_NET_WM_PING)
+               (setq exwm-manage--ping-lock nil))
+              (t (exwm--log "Unhandled WM_PROTOCOLS of type: %d" type)))))
+     ((= type xcb:Atom:WM_CHANGE_STATE)
+      (let ((buffer (exwm--id->buffer id)))
+        (when (and (buffer-live-p buffer)
+                   (= (elt data 0) xcb:icccm:WM_STATE:IconicState))
+          (with-current-buffer buffer
+            (if exwm--floating-frame
+                (call-interactively #'exwm-floating-hide)
+              (bury-buffer))))))
+     (t
+      (exwm--log "Unhandled: %s(%d)"
+                 (x-get-atom-name type exwm-workspace--current) type)))))
+
+(defun exwm--on-SelectionClear (data _synthetic)
+  "Handle SelectionClear events."
+  (exwm--log)
+  (let ((obj (make-instance 'xcb:SelectionClear))
+        owner selection)
+    (xcb:unmarshal obj data)
+    (setq owner (slot-value obj 'owner)
+          selection (slot-value obj 'selection))
+    (when (and (eq owner exwm--wmsn-window)
+               (eq selection xcb:Atom:WM_S0))
+      (exwm-exit))))
+
+(defun exwm--init-icccm-ewmh ()
+  "Initialize ICCCM/EWMH support."
+  (exwm--log)
+  ;; Handle PropertyNotify event
+  (xcb:+event exwm--connection 'xcb:PropertyNotify #'exwm--on-PropertyNotify)
+  ;; Handle relevant client messages
+  (xcb:+event exwm--connection 'xcb:ClientMessage #'exwm--on-ClientMessage)
+  ;; Handle SelectionClear
+  (xcb:+event exwm--connection 'xcb:SelectionClear #'exwm--on-SelectionClear)
+  ;; Set _NET_SUPPORTED
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_SUPPORTED
+                     :window exwm--root
+                     :data (vector
+                            ;; Root windows properties.
+                            xcb:Atom:_NET_SUPPORTED
+                            xcb:Atom:_NET_CLIENT_LIST
+                            xcb:Atom:_NET_CLIENT_LIST_STACKING
+                            xcb:Atom:_NET_NUMBER_OF_DESKTOPS
+                            xcb:Atom:_NET_DESKTOP_GEOMETRY
+                            xcb:Atom:_NET_DESKTOP_VIEWPORT
+                            xcb:Atom:_NET_CURRENT_DESKTOP
+                            ;; xcb:Atom:_NET_DESKTOP_NAMES
+                            xcb:Atom:_NET_ACTIVE_WINDOW
+                            ;; xcb:Atom:_NET_WORKAREA
+                            xcb:Atom:_NET_SUPPORTING_WM_CHECK
+                            ;; xcb:Atom:_NET_VIRTUAL_ROOTS
+                            ;; xcb:Atom:_NET_DESKTOP_LAYOUT
+                            ;; xcb:Atom:_NET_SHOWING_DESKTOP
+
+                            ;; Other root window messages.
+                            xcb:Atom:_NET_CLOSE_WINDOW
+                            ;; xcb:Atom:_NET_MOVERESIZE_WINDOW
+                            xcb:Atom:_NET_WM_MOVERESIZE
+                            ;; xcb:Atom:_NET_RESTACK_WINDOW
+                            xcb:Atom:_NET_REQUEST_FRAME_EXTENTS
+
+                            ;; Application window properties.
+                            xcb:Atom:_NET_WM_NAME
+                            ;; xcb:Atom:_NET_WM_VISIBLE_NAME
+                            ;; xcb:Atom:_NET_WM_ICON_NAME
+                            ;; xcb:Atom:_NET_WM_VISIBLE_ICON_NAME
+                            xcb:Atom:_NET_WM_DESKTOP
+                            ;;
+                            xcb:Atom:_NET_WM_WINDOW_TYPE
+                            ;; xcb:Atom:_NET_WM_WINDOW_TYPE_DESKTOP
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_TOOLBAR
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_MENU
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_SPLASH
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_DROPDOWN_MENU
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_POPUP_MENU
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_TOOLTIP
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_NOTIFICATION
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_COMBO
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_DND
+                            xcb:Atom:_NET_WM_WINDOW_TYPE_NORMAL
+                            ;;
+                            xcb:Atom:_NET_WM_STATE
+                            ;; xcb:Atom:_NET_WM_STATE_MODAL
+                            ;; xcb:Atom:_NET_WM_STATE_STICKY
+                            ;; xcb:Atom:_NET_WM_STATE_MAXIMIZED_VERT
+                            ;; xcb:Atom:_NET_WM_STATE_MAXIMIZED_HORZ
+                            ;; xcb:Atom:_NET_WM_STATE_SHADED
+                            ;; xcb:Atom:_NET_WM_STATE_SKIP_TASKBAR
+                            ;; xcb:Atom:_NET_WM_STATE_SKIP_PAGER
+                            xcb:Atom:_NET_WM_STATE_HIDDEN
+                            xcb:Atom:_NET_WM_STATE_FULLSCREEN
+                            ;; xcb:Atom:_NET_WM_STATE_ABOVE
+                            ;; xcb:Atom:_NET_WM_STATE_BELOW
+                            xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION
+                            ;; xcb:Atom:_NET_WM_STATE_FOCUSED
+                            ;;
+                            xcb:Atom:_NET_WM_ALLOWED_ACTIONS
+                            xcb:Atom:_NET_WM_ACTION_MOVE
+                            xcb:Atom:_NET_WM_ACTION_RESIZE
+                            xcb:Atom:_NET_WM_ACTION_MINIMIZE
+                            ;; xcb:Atom:_NET_WM_ACTION_SHADE
+                            ;; xcb:Atom:_NET_WM_ACTION_STICK
+                            ;; xcb:Atom:_NET_WM_ACTION_MAXIMIZE_HORZ
+                            ;; xcb:Atom:_NET_WM_ACTION_MAXIMIZE_VERT
+                            xcb:Atom:_NET_WM_ACTION_FULLSCREEN
+                            xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP
+                            xcb:Atom:_NET_WM_ACTION_CLOSE
+                            ;; xcb:Atom:_NET_WM_ACTION_ABOVE
+                            ;; xcb:Atom:_NET_WM_ACTION_BELOW
+                            ;;
+                            xcb:Atom:_NET_WM_STRUT
+                            xcb:Atom:_NET_WM_STRUT_PARTIAL
+                            ;; xcb:Atom:_NET_WM_ICON_GEOMETRY
+                            ;; xcb:Atom:_NET_WM_ICON
+                            xcb:Atom:_NET_WM_PID
+                            ;; xcb:Atom:_NET_WM_HANDLED_ICONS
+                            ;; xcb:Atom:_NET_WM_USER_TIME
+                            ;; xcb:Atom:_NET_WM_USER_TIME_WINDOW
+                            xcb:Atom:_NET_FRAME_EXTENTS
+                            ;; xcb:Atom:_NET_WM_OPAQUE_REGION
+                            ;; xcb:Atom:_NET_WM_BYPASS_COMPOSITOR
+
+                            ;; Window manager protocols.
+                            xcb:Atom:_NET_WM_PING
+                            ;; xcb:Atom:_NET_WM_SYNC_REQUEST
+                            ;; xcb:Atom:_NET_WM_FULLSCREEN_MONITORS
+
+                            ;; Other properties.
+                            xcb:Atom:_NET_WM_FULL_PLACEMENT)))
+  ;; Create a child window for setting _NET_SUPPORTING_WM_CHECK
+  (let ((new-id (xcb:generate-id exwm--connection)))
+    (setq exwm--guide-window new-id)
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:CreateWindow
+                       :depth 0
+                       :wid new-id
+                       :parent exwm--root
+                       :x -1
+                       :y -1
+                       :width 1
+                       :height 1
+                       :border-width 0
+                       :class xcb:WindowClass:InputOnly
+                       :visual 0
+                       :value-mask xcb:CW:OverrideRedirect
+                       :override-redirect 1))
+    ;; Set _NET_WM_NAME.  Must be set to the name of the window manager, as
+    ;; required by wm-spec.
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                       :window new-id :data "EXWM"))
+    (dolist (i (list exwm--root new-id))
+      ;; Set _NET_SUPPORTING_WM_CHECK
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ewmh:set-_NET_SUPPORTING_WM_CHECK
+                         :window i :data new-id))))
+  ;; Set _NET_DESKTOP_VIEWPORT (we don't support large desktop).
+  (xcb:+request exwm--connection
+      (make-instance 'xcb:ewmh:set-_NET_DESKTOP_VIEWPORT
+                     :window exwm--root
+                     :data [0 0]))
+  (xcb:flush exwm--connection))
+
+(defun exwm--wmsn-acquire (replace)
+  "Acquire the WM_Sn selection.
+
+REPLACE specifies what to do in case there already is a window
+manager.  If t, replace it, if nil, abort and ask the user if `ask'."
+  (exwm--log "%s" replace)
+  (with-slots (owner)
+      (xcb:+request-unchecked+reply exwm--connection
+          (make-instance 'xcb:GetSelectionOwner
+                         :selection xcb:Atom:WM_S0))
+    (when (/= owner xcb:Window:None)
+      (when (eq replace 'ask)
+        (setq replace (yes-or-no-p "Replace existing window manager? ")))
+      (when (not replace)
+        (user-error "Other window manager detected")))
+    (let ((new-owner (xcb:generate-id exwm--connection)))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:CreateWindow
+                         :depth 0
+                         :wid new-owner
+                         :parent exwm--root
+                         :x -1
+                         :y -1
+                         :width 1
+                         :height 1
+                         :border-width 0
+                         :class xcb:WindowClass:CopyFromParent
+                         :visual 0
+                         :value-mask 0
+                         :override-redirect 0))
+      (xcb:+request exwm--connection
+          (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                         :window new-owner :data "EXWM: exwm--wmsn-window"))
+      (xcb:+request-checked+request-check exwm--connection
+          (make-instance 'xcb:SetSelectionOwner
+                         :selection xcb:Atom:WM_S0
+                         :owner new-owner
+                         :time xcb:Time:CurrentTime))
+      (with-slots (owner)
+          (xcb:+request-unchecked+reply exwm--connection
+              (make-instance 'xcb:GetSelectionOwner
+                             :selection xcb:Atom:WM_S0))
+        (unless (eq owner new-owner)
+          (error "Could not acquire ownership of WM selection")))
+      ;; Wait for the other window manager to terminate.
+      (when (/= owner xcb:Window:None)
+        (let (reply)
+          (cl-dotimes (i exwm--wmsn-acquire-timeout)
+            (setq reply (xcb:+request-unchecked+reply exwm--connection
+                            (make-instance 'xcb:GetGeometry :drawable owner)))
+            (when (not reply)
+              (cl-return))
+            (message "Waiting for other window manager to quit... %ds" i)
+            (sleep-for 1))
+          (when reply
+            (error "Other window manager did not release selection in time"))))
+      ;; announce
+      (let* ((cmd (make-instance 'xcb:ClientMessageData
+                                 :data32 (vector xcb:Time:CurrentTime
+                                                 xcb:Atom:WM_S0
+                                                 new-owner
+                                                 0
+                                                 0)))
+             (cm (make-instance 'xcb:ClientMessage
+                                               :window exwm--root
+                                               :format 32
+                                               :type xcb:Atom:MANAGER
+                                               :data cmd))
+             (se (make-instance 'xcb:SendEvent
+                         :propagate 0
+                         :destination exwm--root
+                         :event-mask xcb:EventMask:NoEvent
+                         :event (xcb:marshal cm exwm--connection))))
+        (xcb:+request exwm--connection se))
+      (setq exwm--wmsn-window new-owner))))
+
+;;;###autoload
+(cl-defun exwm-init (&optional frame)
+  "Initialize EXWM."
+  (interactive)
+  (exwm--log "%s" frame)
+  (if frame
+      ;; The frame might not be selected if it's created by emacslicnet.
+      (select-frame-set-input-focus frame)
+    (setq frame (selected-frame)))
+  (when (not (eq 'x (framep frame)))
+    (message "[EXWM] Not running under X environment")
+    (cl-return-from exwm-init))
+  (when exwm--connection
+    (exwm--log "EXWM already running")
+    (cl-return-from exwm-init))
+  (condition-case err
+      (progn
+        (exwm-enable 'undo)               ;never initialize again
+        (setq exwm--connection (xcb:connect))
+        (set-process-query-on-exit-flag (slot-value exwm--connection 'process)
+                                        nil) ;prevent query message on exit
+        (setq exwm--root
+              (slot-value (car (slot-value
+                                (xcb:get-setup exwm--connection) 'roots))
+                          'root))
+        ;; Initialize ICCCM/EWMH support
+        (xcb:icccm:init exwm--connection t)
+        (xcb:ewmh:init exwm--connection t)
+        ;; Try to register window manager selection.
+        (exwm--wmsn-acquire exwm-replace)
+        (when (xcb:+request-checked+request-check exwm--connection
+                  (make-instance 'xcb:ChangeWindowAttributes
+                                 :window exwm--root
+                                 :value-mask xcb:CW:EventMask
+                                 :event-mask
+                                 xcb:EventMask:SubstructureRedirect))
+          (error "Other window manager is running"))
+        ;; Disable some features not working well with EXWM
+        (setq use-dialog-box nil
+              confirm-kill-emacs #'exwm--confirm-kill-emacs)
+        (exwm--lock)
+        (exwm--init-icccm-ewmh)
+        (exwm-layout--init)
+        (exwm-floating--init)
+        (exwm-manage--init)
+        (exwm-workspace--init)
+        (exwm-input--init)
+        (exwm--unlock)
+        (exwm-workspace--post-init)
+        (exwm-input--post-init)
+        (run-hooks 'exwm-init-hook)
+        ;; Manage existing windows
+        (exwm-manage--scan))
+    (user-error)
+    ((quit error)
+     (exwm-exit)
+     ;; Rethrow error
+     (warn "[EXWM] EXWM fails to start (%s: %s)" (car err) (cdr err)))))
+
+
+;;;###autoload
+(defun exwm-exit ()
+  "Exit EXWM."
+  (interactive)
+  (exwm--log)
+  (run-hooks 'exwm-exit-hook)
+  (setq confirm-kill-emacs nil)
+  ;; Exit modules.
+  (exwm-input--exit)
+  (exwm-manage--exit)
+  (exwm-workspace--exit)
+  (exwm-floating--exit)
+  (exwm-layout--exit)
+  (when exwm--connection
+    (xcb:flush exwm--connection)
+    (xcb:disconnect exwm--connection))
+  (setq exwm--connection nil))
+
+;;;###autoload
+(defun exwm-enable (&optional undo)
+  "Enable/Disable EXWM."
+  (exwm--log "%s" undo)
+  (pcase undo
+    (`undo                              ;prevent reinitialization
+     (remove-hook 'window-setup-hook #'exwm-init)
+     (remove-hook 'after-make-frame-functions #'exwm-init))
+    (`undo-all                          ;attempt to revert everything
+     (remove-hook 'window-setup-hook #'exwm-init)
+     (remove-hook 'after-make-frame-functions #'exwm-init)
+     (remove-hook 'kill-emacs-hook #'exwm--server-stop)
+     (dolist (i exwm-blocking-subrs)
+       (advice-remove i #'exwm--server-eval-at)))
+    (_                                  ;enable EXWM
+     (setq frame-resize-pixelwise t     ;mandatory; before init
+           window-resize-pixelwise t)
+     ;; Ignore unrecognized command line arguments.  This can be helpful
+     ;; when EXWM is launched by some session manager.
+     (push #'vector command-line-functions)
+     ;; In case EXWM is to be started from a graphical Emacs instance.
+     (add-hook 'window-setup-hook #'exwm-init t)
+     ;; In case EXWM is to be started with emacsclient.
+     (add-hook 'after-make-frame-functions #'exwm-init t)
+     ;; Manage the subordinate Emacs server.
+     (add-hook 'kill-emacs-hook #'exwm--server-stop)
+     (dolist (i exwm-blocking-subrs)
+       (advice-add i :around #'exwm--server-eval-at)))))
+
+(defun exwm--server-stop ()
+  "Stop the subordinate Emacs server."
+  (exwm--log)
+  (server-force-delete exwm--server-name)
+  (when exwm--server-process
+    (delete-process exwm--server-process)
+    (setq exwm--server-process nil)))
+
+(defun exwm--server-eval-at (&rest args)
+  "Wrapper of `server-eval-at' used to advice subrs."
+  ;; Start the subordinate Emacs server if it's not alive
+  (exwm--log "%s" args)
+  (unless (server-running-p exwm--server-name)
+    (when exwm--server-process (delete-process exwm--server-process))
+    (setq exwm--server-process
+          (start-process exwm--server-name
+                         nil
+                         (car command-line-args) ;The executable file
+                         "-d" (frame-parameter nil 'display)
+                         "-Q"
+                         (concat "--daemon=" exwm--server-name)
+                         "--eval"
+                         ;; Create an invisible frame
+                         "(make-frame '((window-system . x) (visibility)))"))
+    (while (not (server-running-p exwm--server-name))
+      (sit-for 0.001)))
+  (server-eval-at
+   exwm--server-name
+   `(progn (select-frame (car (frame-list)))
+           (let ((result ,(nconc (list (make-symbol (subr-name (car args))))
+                                 (cdr args))))
+             (pcase (type-of result)
+               ;; Return the name of a buffer
+               (`buffer (buffer-name result))
+               ;; We blindly convert all font objects to their XLFD names. This
+               ;; might cause problems of course, but it still has a chance to
+               ;; work (whereas directly passing font objects would merely
+               ;; raise errors).
+               ((or `font-entity `font-object `font-spec)
+                (font-xlfd-name result))
+               ;; Passing following types makes little sense
+               ((or `compiled-function `finalizer `frame `hash-table `marker
+                    `overlay `process `window `window-configuration))
+               ;; Passing the name of a subr
+               (`subr (make-symbol (subr-name result)))
+               ;; For other types, return the value as-is.
+               (t result))))))
+
+(defun exwm--confirm-kill-emacs (prompt &optional force)
+  "Confirm before exiting Emacs."
+  (exwm--log)
+  (when (cond
+         ((and force (not (eq force 'no-check)))
+          ;; Force killing Emacs.
+          t)
+         ((or (eq force 'no-check) (not exwm--id-buffer-alist))
+          ;; Check if there's any unsaved file.
+          (pcase (catch 'break
+                   (let ((kill-emacs-query-functions
+                          (append kill-emacs-query-functions
+                                  (list (lambda ()
+                                          (throw 'break 'break))))))
+                     (save-buffers-kill-emacs)))
+            (`break (y-or-n-p prompt))
+            (x x)))
+         (t
+          (yes-or-no-p (format "[EXWM] %d window(s) will be destroyed.  %s"
+                               (length exwm--id-buffer-alist) prompt))))
+    ;; Run `kill-emacs-hook' (`server-force-stop' excluded) before Emacs
+    ;; frames are unmapped so that errors (if any) can be visible.
+    (if (memq #'server-force-stop kill-emacs-hook)
+        (progn
+          (setq kill-emacs-hook (delq #'server-force-stop kill-emacs-hook))
+          (run-hooks 'kill-emacs-hook)
+          (setq kill-emacs-hook (list #'server-force-stop)))
+      (run-hooks 'kill-emacs-hook)
+      (setq kill-emacs-hook nil))
+    ;; Exit each module, destroying all resources created by this connection.
+    (exwm-exit)
+    ;; Set the return value.
+    t))
+
+
+
+(provide 'exwm)
+
+;;; exwm.el ends here
diff --git a/third_party/exwm/xinitrc b/third_party/exwm/xinitrc
new file mode 100644
index 0000000000..591e419914
--- /dev/null
+++ b/third_party/exwm/xinitrc
@@ -0,0 +1,20 @@
+# Disable access control for the current user.
+xhost +SI:localuser:$USER
+
+# Make Java applications aware this is a non-reparenting window manager.
+export _JAVA_AWT_WM_NONREPARENTING=1
+
+# Set default cursor.
+xsetroot -cursor_name left_ptr
+
+# Set keyboard repeat rate.
+xset r rate 200 60
+
+# Uncomment the following block to use the exwm-xim module.
+#export XMODIFIERS=@im=exwm-xim
+#export GTK_IM_MODULE=xim
+#export QT_IM_MODULE=xim
+#export CLUTTER_IM_MODULE=xim
+
+# Finally start Emacs
+exec emacs
diff --git a/third_party/gerrit-queue/.buildkite/build.sh b/third_party/gerrit-queue/.buildkite/build.sh
new file mode 100755
index 0000000000..0a218c817e
--- /dev/null
+++ b/third_party/gerrit-queue/.buildkite/build.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+export GOPATH=~/go
+go generate
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -a -ldflags '-extldflags \"-static\"' -o gerrit-queue
diff --git a/third_party/gerrit-queue/.buildkite/pipeline.yml b/third_party/gerrit-queue/.buildkite/pipeline.yml
new file mode 100644
index 0000000000..0885cb9694
--- /dev/null
+++ b/third_party/gerrit-queue/.buildkite/pipeline.yml
@@ -0,0 +1,13 @@
+steps:
+  - command: |
+      . /var/lib/buildkite-agent/.nix-profile/etc/profile.d/nix.sh
+      # produces a ./gerrit-queue
+      nix-shell --run ./.buildkite/build.sh
+
+      mkdir -p out
+      mv ./gerrit-queue out/gerrit-queue-$(git describe --tags)
+
+    label: "Build (linux/amd64)"
+    timeout: 30
+    artifact_paths:
+      - "out/*"
diff --git a/third_party/gerrit-queue/.gitignore b/third_party/gerrit-queue/.gitignore
new file mode 100644
index 0000000000..f2ec770e3c
--- /dev/null
+++ b/third_party/gerrit-queue/.gitignore
@@ -0,0 +1,4 @@
+/.vscode
+/statik
+/.envrc.private
+/gerrit-queue
diff --git a/third_party/gerrit-queue/LICENSE b/third_party/gerrit-queue/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/third_party/gerrit-queue/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/third_party/gerrit-queue/README.md b/third_party/gerrit-queue/README.md
new file mode 100644
index 0000000000..9ffb81b8d2
--- /dev/null
+++ b/third_party/gerrit-queue/README.md
@@ -0,0 +1,80 @@
+# gerrit-queue
+
+This daemon automatically rebases and submits changesets from a Gerrit
+instance, ensuring they still pass CI.
+
+In a usual gerrit setup with a linear master history, different developers
+await CI feedback on a rebased changeset, then one clicks submit, and
+effectively makes everybody else rebase again. `gerrit-queue` is meant to
+remove these races to master.
+
+Developers can set the `Autosubmit` label to `+1` on all changesets in a series,
+and if all preconditions on are met ("submittable" in gerrit speech, this
+usually means passing CI and passing Code Review), `gerrit-queue` takes care of
+rebasing and submitting it to master
+
+## How it works
+Gerrit only knows about Changesets (and some relations to other changesets),
+but usually developers think in terms of multiple changesets.
+
+### Fetching changesets
+`gerrit-queue` fetches all changesets from gerrit, and tries to identify these
+chains of changesets. We call them `Series`. All changesets need to have strict
+parent/child relationships to be detected (so if only half of the stack gets
+rebased by the Gerrit Web interface, these are considered individual series.
+
+Series are sorted by the number of changesets in them. This ensures longer
+series are merged faster, and less rebases are triggered. In the future, this
+might be extended to other metrics.
+
+### Submitting changesets
+The submitqueue has a Trigger() function, which gets periodically executed.
+
+It can keep a reference to one single serie across multiple runs. This is
+necessary if it previously rebased one serie to current HEAD and needs to wait
+some time until CI feedback is there. If it wouldn't keep that state, it would
+pick another series (with +1 from CI) and trigger a rebase on that one, so
+depending on CI run times and trigger intervals, if not keepig this information
+it'd end up rebasing all unrebased changesets on the same HEAD, and then just
+pick one, instead of waiting for the one to finish.
+
+The Trigger() function first instructs the gerrit client to fetch changesets
+and assemble series.
+If there is a `wipSerie` from a previous run, we check if it can still be found
+in the newly assembled list of series (it still needs to contain the same
+number of series. Commit IDs may differ, because the code doesn't reassemble a
+`wipSerie` after scheduling a rebase.
+If the `wipSerie` could be refreshed, we update the pointer with the newly
+assembled series. If we couldn't find it, we drop it.
+
+Now, we enter the main for loop. The first half of the loop checks various
+conditions of the current `wipSerie`, and if successful, does the submit
+("Submit phase"), the second half will pick a suitable new `wipSerie`, and
+potentially do a rebase ("Pick phase").
+
+#### Submit phase
+We check if there is an existing `wipSerie`. If there isn't, we immediately go to
+the "pick" phase.
+
+The `wipSerie` still needs to be rebased on `HEAD` (otherwise, the submit queue
+advanced outside of gerrit), and should not fail CI (logical merge conflict) -
+otherwise we discard it, and continue with the picking phase.
+
+If the `wipSerie` still contains a changeset awaiting CI feedback, we `return`
+from the `Trigger()` function (and go back to sleep).
+
+If the changeset is "submittable" in gerrit speech, and has the necessary
+submit queue tag set, we submit it.
+
+#### Pick phase
+The pick phase finds a new `wipSerie`. It'll first try to find one that already
+is rebased on the current `HEAD` (so the loop can just continue, and the next
+submit phase simply submit), and otherwise fall back to a not-yet-rebased
+serie. Because the rebase mandates waiting for CI, the code `return`s the
+`Trigger()` function, so it'll be called again after waiting some time.
+
+## Compile and Run
+```sh
+go generate
+GERRIT_PASSWORD=mypassword go run main.go --url https://gerrit.mydomain.com --username myuser --project myproject
+```
diff --git a/third_party/gerrit-queue/default.nix b/third_party/gerrit-queue/default.nix
new file mode 100644
index 0000000000..427e312183
--- /dev/null
+++ b/third_party/gerrit-queue/default.nix
@@ -0,0 +1,14 @@
+{ pkgs, lib, ... }:
+
+pkgs.buildGoModule {
+  pname = "gerrit-queue";
+  version = "master";
+  vendorSha256 = "0n5h7j416yb2mwic9c3rhqza64jlvl7iw507r9mkw3jadn4whm7a";
+  src = ./.;
+
+  meta = with lib; {
+    description = "Gerrit submit bot";
+    homepage = "https://github.com/tweag/gerrit-queue";
+    license = licenses.asl20;
+  };
+}
diff --git a/third_party/gerrit-queue/frontend/frontend.go b/third_party/gerrit-queue/frontend/frontend.go
new file mode 100644
index 0000000000..2cc65423f0
--- /dev/null
+++ b/third_party/gerrit-queue/frontend/frontend.go
@@ -0,0 +1,113 @@
+package frontend
+
+import (
+	"embed"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+
+	"html/template"
+
+	"github.com/apex/log"
+
+	"github.com/tweag/gerrit-queue/gerrit"
+	"github.com/tweag/gerrit-queue/misc"
+	"github.com/tweag/gerrit-queue/submitqueue"
+)
+
+//go:embed templates
+var templates embed.FS
+
+//loadTemplate loads a list of templates, relative to the templates root, and a
+//FuncMap, and returns a template object
+func loadTemplate(templateNames []string, funcMap template.FuncMap) (*template.Template, error) {
+	if len(templateNames) == 0 {
+		return nil, fmt.Errorf("templateNames can't be empty")
+	}
+	tmpl := template.New(templateNames[0]).Funcs(funcMap)
+
+	for _, templateName := range templateNames {
+		r, err := templates.Open("/" + templateName)
+		if err != nil {
+			return nil, err
+		}
+		defer r.Close()
+		contents, err := ioutil.ReadAll(r)
+		if err != nil {
+			return nil, err
+		}
+		tmpl, err = tmpl.Parse(string(contents))
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return tmpl, nil
+}
+
+// MakeFrontend returns a http.Handler
+func MakeFrontend(rotatingLogHandler *misc.RotatingLogHandler, gerritClient *gerrit.Client, runner *submitqueue.Runner) http.Handler {
+	projectName := gerritClient.GetProjectName()
+	branchName := gerritClient.GetBranchName()
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
+		var wipSerie *gerrit.Serie = nil
+		HEAD := ""
+		currentlyRunning := runner.IsCurrentlyRunning()
+
+		// don't trigger operations requiring a lock
+		if !currentlyRunning {
+			wipSerie = runner.GetWIPSerie()
+			HEAD = gerritClient.GetHEAD()
+		}
+
+		funcMap := template.FuncMap{
+			"changesetURL": func(changeset *gerrit.Changeset) string {
+				return gerritClient.GetChangesetURL(changeset)
+			},
+			"levelToClasses": func(level log.Level) string {
+				switch level {
+				case log.DebugLevel:
+					return "text-muted"
+				case log.InfoLevel:
+					return "text-info"
+				case log.WarnLevel:
+					return "text-warning"
+				case log.ErrorLevel:
+					return "text-danger"
+				case log.FatalLevel:
+					return "text-danger"
+				default:
+					return "text-white"
+				}
+			},
+			"fieldsToJSON": func(fields log.Fields) string {
+				jsonData, _ := json.Marshal(fields)
+				return string(jsonData)
+			},
+		}
+
+		tmpl := template.Must(loadTemplate([]string{
+			"index.tmpl.html",
+			"serie.tmpl.html",
+			"changeset.tmpl.html",
+		}, funcMap))
+
+		tmpl.ExecuteTemplate(w, "index.tmpl.html", map[string]interface{}{
+			// Config
+			"projectName": projectName,
+			"branchName":  branchName,
+
+			// State
+			"currentlyRunning": currentlyRunning,
+			"wipSerie":         wipSerie,
+			"HEAD":             HEAD,
+
+			// History
+			"memory": rotatingLogHandler,
+		})
+	})
+	return mux
+}
diff --git a/third_party/gerrit-queue/frontend/templates/changeset.tmpl.html b/third_party/gerrit-queue/frontend/templates/changeset.tmpl.html
new file mode 100644
index 0000000000..5d3997885c
--- /dev/null
+++ b/third_party/gerrit-queue/frontend/templates/changeset.tmpl.html
@@ -0,0 +1,15 @@
+{{ define "changeset" }}
+<tr>
+    <td>{{ .OwnerName }}</td>
+    <td>
+    <strong>{{ .Subject }}</strong> (<a href="{{ changesetURL . }}" target="_blank">#{{ .Number }}</a>)<br />
+    <small><code>{{ .CommitID }}</code></small>
+    </td>
+    <td>
+    <span>
+        {{ if .IsVerified }}<span class="badge badge-success badge-pill">+1 (CI)</span>{{ end }}
+        {{ if .IsCodeReviewed }}<span class="badge badge-info badge-pill">+2 (CR)</span>{{ end }}
+    </span>
+    </td>
+</tr>
+{{ end }}
\ No newline at end of file
diff --git a/third_party/gerrit-queue/frontend/templates/index.tmpl.html b/third_party/gerrit-queue/frontend/templates/index.tmpl.html
new file mode 100644
index 0000000000..e04c0a349d
--- /dev/null
+++ b/third_party/gerrit-queue/frontend/templates/index.tmpl.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Gerrit Submit Queue</title>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha256-CjSoeELFOcH0/uxWu6mC/Vlrc1AARqbm/jiiImDGV3s=" crossorigin="anonymous"></script>
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous" />
+</head>
+<body>
+  <nav class="navbar sticky-top navbar-expand-sm navbar-dark bg-dark">
+    <div class="container">
+      <a class="navbar-brand" href="#">Gerrit Submit Queue</a>
+      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+        <span class="navbar-toggler-icon"></span>
+      </button>
+      <div class="collapse navbar-collapse" id="navbarSupportedContent">
+        <ul class="navbar-nav mr-auto">
+          <li class="nav-item">
+            <a class="nav-link" href="#region-info">Info</a>
+          </li>
+          <li class="nav-item">
+            <a class="nav-link" href="#region-wipserie">WIP Serie</a>
+          </li>
+          <li class="nav-item">
+            <a class="nav-link" href="#region-log">Log</a>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </nav>
+  <div class="container">
+    <h2 id="region-info">Info</h2>
+    <table class="table">
+      <tbody>
+        <tr>
+          <th scope="row">Project Name:</th>
+          <td>{{ .projectName }}</td>
+        </tr>
+        <tr>
+          <th scope="row">Branch Name:</th>
+          <td>{{ .branchName }}</td>
+        </tr>
+        <tr>
+          <th scope="row">Currently running:</th>
+          <td>
+            {{ if .currentlyRunning }}yes{{ else }}no{{ end }}
+          </td>
+        </tr>
+        <tr>
+          <th scope="row">HEAD:</th>
+          <td>
+            {{ if .HEAD }}{{ .HEAD }}{{ else }}-{{ end }}
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <h2 id="region-wipserie">WIP Serie</h2>
+    {{ if .wipSerie }}
+    {{ block "serie" .wipSerie }}{{ end }}
+    {{ else }}
+    - 
+    {{ end }}
+
+    <h2 id="region-log">Log</h2>
+    {{ range $entry := .memory.Entries }}
+    <div class="d-flex flex-row bg-dark {{ levelToClasses $entry.Level }} text-monospace"> 
+      <div class="p-2"><small>{{ $entry.Timestamp.Format "2006-01-02 15:04:05 UTC"}}</small></div>
+      <div class="p-2 flex-grow-1"><small><strong>{{ $entry.Message }}</strong></small></div>
+    </div>
+    <div class="bg-dark {{ levelToClasses $entry.Level }} text-monospace text-break" style="padding-left: 4rem"> 
+    <small>{{ fieldsToJSON $entry.Fields }}</small>
+    </div>
+    {{ end }}
+</body>
+</html>
diff --git a/third_party/gerrit-queue/frontend/templates/serie.tmpl.html b/third_party/gerrit-queue/frontend/templates/serie.tmpl.html
new file mode 100644
index 0000000000..60f0c18113
--- /dev/null
+++ b/third_party/gerrit-queue/frontend/templates/serie.tmpl.html
@@ -0,0 +1,19 @@
+{{ define "serie" }}
+<table class="table table-sm table-hover">
+<thead class="thead-light">
+    <tr>
+    <th scope="col">Owner</th>
+    <th scope="col">Changeset</th>
+    <th scope="col">Flags</th>
+    </tr>
+</thead>
+<tbody>
+    <tr>
+        <td colspan="3" class="table-success">Serie with {{ len .ChangeSets }} changes</td>
+    </tr>
+    {{ range $changeset := .ChangeSets }}
+    {{ block "changeset" $changeset }}{{ end }}
+    {{ end }}
+</tbody>
+</table>
+{{ end }}
\ No newline at end of file
diff --git a/third_party/gerrit-queue/gerrit/changeset.go b/third_party/gerrit-queue/gerrit/changeset.go
new file mode 100644
index 0000000000..f71032a567
--- /dev/null
+++ b/third_party/gerrit-queue/gerrit/changeset.go
@@ -0,0 +1,117 @@
+package gerrit
+
+import (
+	"bytes"
+	"fmt"
+
+	goGerrit "github.com/andygrunwald/go-gerrit"
+	"github.com/apex/log"
+)
+
+// Changeset represents a single changeset
+// Relationships between different changesets are described in Series
+type Changeset struct {
+	changeInfo      *goGerrit.ChangeInfo
+	ChangeID        string
+	Number          int
+	Verified        int
+	CodeReviewed    int
+	Autosubmit      int
+	Submittable     bool
+	CommitID        string
+	ParentCommitIDs []string
+	OwnerName       string
+	Subject         string
+}
+
+// MakeChangeset creates a new Changeset object out of a goGerrit.ChangeInfo object
+func MakeChangeset(changeInfo *goGerrit.ChangeInfo) *Changeset {
+	return &Changeset{
+		changeInfo:      changeInfo,
+		ChangeID:        changeInfo.ChangeID,
+		Number:          changeInfo.Number,
+		Verified:        labelInfoToInt(changeInfo.Labels["Verified"]),
+		CodeReviewed:    labelInfoToInt(changeInfo.Labels["Code-Review"]),
+		Autosubmit:      labelInfoToInt(changeInfo.Labels["Autosubmit"]),
+		Submittable:     changeInfo.Submittable,
+		CommitID:        changeInfo.CurrentRevision, // yes, this IS the commit ID.
+		ParentCommitIDs: getParentCommitIDs(changeInfo),
+		OwnerName:       changeInfo.Owner.Name,
+		Subject:         changeInfo.Subject,
+	}
+}
+
+// IsAutosubmit returns true if the changeset is intended to be
+// automatically submitted by gerrit-queue.
+//
+// This is determined by the Change Owner setting +1 on the
+// "Autosubmit" label.
+func (c *Changeset) IsAutosubmit() bool {
+	return c.Autosubmit == 1
+}
+
+// IsVerified returns true if the changeset passed CI,
+// that's when somebody left the Approved (+1) on the "Verified" label
+func (c *Changeset) IsVerified() bool {
+	return c.Verified == 1
+}
+
+// IsCodeReviewed returns true if the changeset passed code review,
+// that's when somebody left the Recommended (+2) on the "Code-Review" label
+func (c *Changeset) IsCodeReviewed() bool {
+	return c.CodeReviewed == 2
+}
+
+func (c *Changeset) String() string {
+	var b bytes.Buffer
+	b.WriteString("Changeset")
+	b.WriteString(fmt.Sprintf("(commitID: %.7s, author: %s, subject: %s, submittable: %v)",
+		c.CommitID, c.OwnerName, c.Subject, c.Submittable))
+	return b.String()
+}
+
+// FilterChangesets filters a list of Changeset by a given filter function
+func FilterChangesets(changesets []*Changeset, f func(*Changeset) bool) []*Changeset {
+	newChangesets := make([]*Changeset, 0)
+	for _, changeset := range changesets {
+		if f(changeset) {
+			newChangesets = append(newChangesets, changeset)
+		} else {
+			log.WithField("changeset", changeset.String()).Debug("dropped by filter")
+		}
+	}
+	return newChangesets
+}
+
+// labelInfoToInt converts a goGerrit.LabelInfo to -2…+2 int
+func labelInfoToInt(labelInfo goGerrit.LabelInfo) int {
+	if labelInfo.Recommended.AccountID != 0 {
+		return 2
+	}
+	if labelInfo.Approved.AccountID != 0 {
+		return 1
+	}
+	if labelInfo.Disliked.AccountID != 0 {
+		return -1
+	}
+	if labelInfo.Rejected.AccountID != 0 {
+		return -2
+	}
+	return 0
+}
+
+// getParentCommitIDs returns the parent commit IDs of the goGerrit.ChangeInfo
+// There is usually only one parent commit ID, except for merge commits.
+func getParentCommitIDs(changeInfo *goGerrit.ChangeInfo) []string {
+	// obtain the RevisionInfo object
+	revisionInfo := changeInfo.Revisions[changeInfo.CurrentRevision]
+
+	// obtain the Commit object
+	commit := revisionInfo.Commit
+
+	commitIDs := make([]string, len(commit.Parents))
+	for i, commit := range commit.Parents {
+		commitIDs[i] = commit.Commit
+	}
+	return commitIDs
+}
diff --git a/third_party/gerrit-queue/gerrit/client.go b/third_party/gerrit-queue/gerrit/client.go
new file mode 100644
index 0000000000..314f97281c
--- /dev/null
+++ b/third_party/gerrit-queue/gerrit/client.go
@@ -0,0 +1,220 @@
+package gerrit
+
+import (
+	"fmt"
+
+	goGerrit "github.com/andygrunwald/go-gerrit"
+	"github.com/apex/log"
+
+	"net/url"
+)
+
+// passed to gerrit when retrieving changesets
+var additionalFields = []string{
+	"LABELS",
+	"CURRENT_REVISION",
+	"CURRENT_COMMIT",
+	"DETAILED_ACCOUNTS",
+	"SUBMITTABLE",
+}
+
+// IClient defines the gerrit.Client interface
+type IClient interface {
+	Refresh() error
+	GetHEAD() string
+	GetBaseURL() string
+	GetChangesetURL(changeset *Changeset) string
+	SubmitChangeset(changeset *Changeset) (*Changeset, error)
+	RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error)
+	ChangesetIsRebasedOnHEAD(changeset *Changeset) bool
+	SerieIsRebasedOnHEAD(serie *Serie) bool
+	FilterSeries(filter func(s *Serie) bool) []*Serie
+	FindSerie(filter func(s *Serie) bool) *Serie
+}
+
+var _ IClient = &Client{}
+
+// Client provides some ways to interact with a gerrit instance
+type Client struct {
+	client      *goGerrit.Client
+	logger      *log.Logger
+	baseURL     string
+	projectName string
+	branchName  string
+	series      []*Serie
+	head        string
+}
+
+// NewClient initializes a new gerrit client
+func NewClient(logger *log.Logger, URL, username, password, projectName, branchName string) (*Client, error) {
+	urlParsed, err := url.Parse(URL)
+	if err != nil {
+		return nil, err
+	}
+	urlParsed.User = url.UserPassword(username, password)
+
+	goGerritClient, err := goGerrit.NewClient(urlParsed.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+	return &Client{
+		client:      goGerritClient,
+		baseURL:     URL,
+		logger:      logger,
+		projectName: projectName,
+		branchName:  branchName,
+	}, nil
+}
+
+// refreshHEAD queries the commit ID of the selected project and branch
+func (c *Client) refreshHEAD() (string, error) {
+	branchInfo, _, err := c.client.Projects.GetBranch(c.projectName, c.branchName)
+	if err != nil {
+		return "", err
+	}
+	return branchInfo.Revision, nil
+}
+
+// GetHEAD returns the internally stored HEAD
+func (c *Client) GetHEAD() string {
+	return c.head
+}
+
+// Refresh causes the client to refresh internal view of gerrit
+func (c *Client) Refresh() error {
+	c.logger.Debug("refreshing from gerrit")
+	HEAD, err := c.refreshHEAD()
+	if err != nil {
+		return err
+	}
+	c.head = HEAD
+
+	var queryString = fmt.Sprintf("status:open project:%s branch:%s", c.projectName, c.branchName)
+	c.logger.Debugf("fetching changesets: %s", queryString)
+	changesets, err := c.fetchChangesets(queryString)
+	if err != nil {
+		return err
+	}
+
+	c.logger.Infof("assembling series…")
+	series, err := AssembleSeries(changesets, c.logger)
+	if err != nil {
+		return err
+	}
+	series = SortSeries(series)
+	c.series = series
+	return nil
+}
+
+// fetchChangesets fetches a list of changesets matching a passed query string
+func (c *Client) fetchChangesets(queryString string) (changesets []*Changeset, Error error) {
+	opt := &goGerrit.QueryChangeOptions{}
+	opt.Query = []string{
+		queryString,
+	}
+	opt.AdditionalFields = additionalFields
+	changes, _, err := c.client.Changes.QueryChanges(opt)
+	if err != nil {
+		return nil, err
+	}
+
+	changesets = make([]*Changeset, 0)
+	for _, change := range *changes {
+		changesets = append(changesets, MakeChangeset(&change))
+	}
+
+	return changesets, nil
+}
+
+// fetchChangeset downloads an existing Changeset from gerrit, by its ID
+// Gerrit's API is a bit sparse, and only returns what you explicitly ask it
+// This is used to refresh an existing changeset with more data.
+func (c *Client) fetchChangeset(changeID string) (*Changeset, error) {
+	opt := goGerrit.ChangeOptions{}
+	opt.AdditionalFields = []string{"LABELS", "DETAILED_ACCOUNTS"}
+	changeInfo, _, err := c.client.Changes.GetChange(changeID, &opt)
+	if err != nil {
+		return nil, err
+	}
+	return MakeChangeset(changeInfo), nil
+}
+
+// SubmitChangeset submits a given changeset, and returns a changeset afterwards.
+func (c *Client) SubmitChangeset(changeset *Changeset) (*Changeset, error) {
+	changeInfo, _, err := c.client.Changes.SubmitChange(changeset.ChangeID, &goGerrit.SubmitInput{})
+	if err != nil {
+		return nil, err
+	}
+	c.head = changeInfo.CurrentRevision
+	return c.fetchChangeset(changeInfo.ChangeID)
+}
+
+// RebaseChangeset rebases a given changeset on top of a given ref
+func (c *Client) RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error) {
+	changeInfo, _, err := c.client.Changes.RebaseChange(changeset.ChangeID, &goGerrit.RebaseInput{
+		Base: ref,
+	})
+	if err != nil {
+		return changeset, err
+	}
+	return c.fetchChangeset(changeInfo.ChangeID)
+}
+
+// GetBaseURL returns the gerrit base URL
+func (c *Client) GetBaseURL() string {
+	return c.baseURL
+}
+
+// GetProjectName returns the configured gerrit project name
+func (c *Client) GetProjectName() string {
+	return c.projectName
+}
+
+// GetBranchName returns the configured gerrit branch name
+func (c *Client) GetBranchName() string {
+	return c.branchName
+}
+
+// GetChangesetURL returns the URL to view a given changeset
+func (c *Client) GetChangesetURL(changeset *Changeset) string {
+	return fmt.Sprintf("%s/c/%s/+/%d", c.GetBaseURL(), c.projectName, changeset.Number)
+}
+
+// ChangesetIsRebasedOnHEAD returns true if the changeset is rebased on the current HEAD
+func (c *Client) ChangesetIsRebasedOnHEAD(changeset *Changeset) bool {
+	if len(changeset.ParentCommitIDs) != 1 {
+		return false
+	}
+	return changeset.ParentCommitIDs[0] == c.head
+}
+
+// SerieIsRebasedOnHEAD returns true if the whole series is rebased on the current HEAD
+// this is already the case if the first changeset in the series is rebased on the current HEAD
+func (c *Client) SerieIsRebasedOnHEAD(serie *Serie) bool {
+	// an empty serie should not exist
+	if len(serie.ChangeSets) == 0 {
+		return false
+	}
+	return c.ChangesetIsRebasedOnHEAD(serie.ChangeSets[0])
+}
+
+// FilterSeries returns a subset of all Series, passing the given filter function
+func (c *Client) FilterSeries(filter func(s *Serie) bool) []*Serie {
+	matchedSeries := []*Serie{}
+	for _, serie := range c.series {
+		if filter(serie) {
+			matchedSeries = append(matchedSeries, serie)
+		}
+	}
+	return matchedSeries
+}
+
+// FindSerie returns the first serie that matches the filter, or nil if none was found
+func (c *Client) FindSerie(filter func(s *Serie) bool) *Serie {
+	for _, serie := range c.series {
+		if filter(serie) {
+			return serie
+		}
+	}
+	return nil
+}
diff --git a/third_party/gerrit-queue/gerrit/serie.go b/third_party/gerrit-queue/gerrit/serie.go
new file mode 100644
index 0000000000..788cf46f4e
--- /dev/null
+++ b/third_party/gerrit-queue/gerrit/serie.go
@@ -0,0 +1,112 @@
+package gerrit
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/apex/log"
+)
+
+// Serie represents a list of successive changesets with an unbroken parent -> child relation,
+// starting from the parent.
+type Serie struct {
+	ChangeSets []*Changeset
+}
+
+// GetParentCommitIDs returns the parent commit IDs
+func (s *Serie) GetParentCommitIDs() ([]string, error) {
+	if len(s.ChangeSets) == 0 {
+		return nil, fmt.Errorf("Can't return parent on a serie with zero ChangeSets")
+	}
+	return s.ChangeSets[0].ParentCommitIDs, nil
+}
+
+// GetLeafCommitID returns the commit id of the last commit in ChangeSets
+func (s *Serie) GetLeafCommitID() (string, error) {
+	if len(s.ChangeSets) == 0 {
+		return "", fmt.Errorf("Can't return leaf on a serie with zero ChangeSets")
+	}
+	return s.ChangeSets[len(s.ChangeSets)-1].CommitID, nil
+}
+
+// CheckIntegrity checks that the series contains a properly ordered and connected chain of commits
+func (s *Serie) CheckIntegrity() error {
+	logger := log.WithField("serie", s)
+	// an empty serie is invalid
+	if len(s.ChangeSets) == 0 {
+		return fmt.Errorf("An empty serie is invalid")
+	}
+
+	previousCommitID := ""
+	for i, changeset := range s.ChangeSets {
+		// we can't really check the parent of the first commit
+		// so skip verifying that one
+		logger.WithFields(log.Fields{
+			"changeset":        changeset.String(),
+			"previousCommitID": fmt.Sprintf("%.7s", previousCommitID),
+		}).Debug(" - verifying changeset")
+
+		parentCommitIDs := changeset.ParentCommitIDs
+		if len(parentCommitIDs) == 0 {
+			return fmt.Errorf("Changesets without any parent are not supported")
+		}
+		// we don't check parents of the first changeset in a series
+		if i != 0 {
+			if len(parentCommitIDs) != 1 {
+				return fmt.Errorf("Merge commits in the middle of a series are not supported (only at the beginning)")
+			}
+			if parentCommitIDs[0] != previousCommitID {
+				return fmt.Errorf("changesets parent commit id doesn't match previous commit id")
+			}
+		}
+		// update previous commit id for the next loop iteration
+		previousCommitID = changeset.CommitID
+	}
+	return nil
+}
+
+// FilterAllChangesets applies a filter function on all of the changesets in the series.
+// returns true if it returns true for all changesets, false otherwise
+func (s *Serie) FilterAllChangesets(f func(c *Changeset) bool) bool {
+	for _, changeset := range s.ChangeSets {
+		if f(changeset) == false {
+			return false
+		}
+	}
+	return true
+}
+
+func (s *Serie) String() string {
+	var sb strings.Builder
+	sb.WriteString(fmt.Sprintf("Serie[%d]", len(s.ChangeSets)))
+	if len(s.ChangeSets) == 0 {
+		sb.WriteString("()\n")
+		return sb.String()
+	}
+	parentCommitIDs, err := s.GetParentCommitIDs()
+	if err == nil {
+		if len(parentCommitIDs) == 1 {
+			sb.WriteString(fmt.Sprintf("(parent: %.7s)", parentCommitIDs[0]))
+		} else {
+			sb.WriteString("(merge: ")
+
+			for i, parentCommitID := range parentCommitIDs {
+				sb.WriteString(fmt.Sprintf("%.7s", parentCommitID))
+				if i < len(parentCommitIDs) {
+					sb.WriteString(", ")
+				}
+			}
+
+			sb.WriteString(")")
+
+		}
+	}
+	sb.WriteString(fmt.Sprintf("(%.7s..%.7s)",
+		s.ChangeSets[0].CommitID,
+		s.ChangeSets[len(s.ChangeSets)-1].CommitID))
+	return sb.String()
+}
+
+func shortCommitID(commitID string) string {
+	return commitID[:6]
+}
diff --git a/third_party/gerrit-queue/gerrit/series.go b/third_party/gerrit-queue/gerrit/series.go
new file mode 100644
index 0000000000..295193ee95
--- /dev/null
+++ b/third_party/gerrit-queue/gerrit/series.go
@@ -0,0 +1,126 @@
+package gerrit
+
+import (
+	"sort"
+
+	"github.com/apex/log"
+)
+
+// AssembleSeries consumes a list of `Changeset`, and groups them together to series
+//
+// We initially put every Changeset in its own Serie
+//
+// As we have no control over the order of the passed changesets,
+// we maintain a lookup table, mapLeafToSerie,
+// which allows to lookup a serie by its leaf commit id
+// We concat series in a fixpoint approach
+// because both appending and prepending is much more complex.
+// Concatenation moves changesets of the later changeset in the previous one
+// in a cleanup phase, we remove orphaned series (those without any changesets inside)
+// afterwards, we do an integrity check, just to be on the safe side.
+func AssembleSeries(changesets []*Changeset, logger *log.Logger) ([]*Serie, error) {
+	series := make([]*Serie, 0)
+	mapLeafToSerie := make(map[string]*Serie, 0)
+
+	for _, changeset := range changesets {
+		l := logger.WithField("changeset", changeset.String())
+
+		l.Debug("creating initial serie")
+		serie := &Serie{
+			ChangeSets: []*Changeset{changeset},
+		}
+		series = append(series, serie)
+		mapLeafToSerie[changeset.CommitID] = serie
+	}
+
+	// Combine series using a fixpoint approach, with a max iteration count.
+	logger.Debug("glueing together phase")
+	for i := 1; i < 100; i++ {
+		didUpdate := false
+		logger.Debugf("at iteration %d", i)
+		for j, serie := range series {
+			l := logger.WithFields(log.Fields{
+				"i":     i,
+				"j":     j,
+				"serie": serie.String(),
+			})
+			parentCommitIDs, err := serie.GetParentCommitIDs()
+			if err != nil {
+				return series, err
+			}
+			if len(parentCommitIDs) != 1 {
+				// We can't append merge commits to other series
+				l.Infof("No single parent, skipping.")
+				continue
+			}
+			parentCommitID := parentCommitIDs[0]
+			l.Debug("Looking for a predecessor.")
+			// if there's another serie that has this parent as a leaf, glue together
+			if otherSerie, ok := mapLeafToSerie[parentCommitID]; ok {
+				if otherSerie == serie {
+					continue
+				}
+				l = l.WithField("otherSerie", otherSerie)
+
+				myLeafCommitID, err := serie.GetLeafCommitID()
+				if err != nil {
+					return series, err
+				}
+
+				// append our changesets to the other serie
+				l.Debug("Splicing together.")
+				otherSerie.ChangeSets = append(otherSerie.ChangeSets, serie.ChangeSets...)
+
+				delete(mapLeafToSerie, parentCommitID)
+				mapLeafToSerie[myLeafCommitID] = otherSerie
+
+				// orphan our serie
+				serie.ChangeSets = []*Changeset{}
+				// remove the orphaned serie from the lookup table
+				delete(mapLeafToSerie, myLeafCommitID)
+
+				didUpdate = true
+			} else {
+				l.Debug("Not found.")
+			}
+		}
+		series = removeOrphanedSeries(series)
+		if !didUpdate {
+			logger.Infof("converged after %d iterations", i)
+			break
+		}
+	}
+
+	// Check integrity, just to be on the safe side.
+	for _, serie := range series {
+		l := logger.WithField("serie", serie.String())
+		l.Debugf("checking integrity")
+		err := serie.CheckIntegrity()
+		if err != nil {
+			l.Errorf("checking integrity failed: %s", err)
+		}
+	}
+	return series, nil
+}
+
+// removeOrphanedSeries removes all empty series (that contain zero changesets)
+func removeOrphanedSeries(series []*Serie) []*Serie {
+	newSeries := []*Serie{}
+	for _, serie := range series {
+		if len(serie.ChangeSets) != 0 {
+			newSeries = append(newSeries, serie)
+		}
+	}
+	return newSeries
+}
+
+// SortSeries sorts a list of series by the number of changesets in each serie, descending
+func SortSeries(series []*Serie) []*Serie {
+	newSeries := make([]*Serie, len(series))
+	copy(newSeries, series)
+	sort.Slice(newSeries, func(i, j int) bool {
+		// the weight depends on the amount of changesets series changeset size
+		return len(series[i].ChangeSets) > len(series[j].ChangeSets)
+	})
+	return newSeries
+}
diff --git a/third_party/gerrit-queue/go.mod b/third_party/gerrit-queue/go.mod
new file mode 100644
index 0000000000..3929f8cf65
--- /dev/null
+++ b/third_party/gerrit-queue/go.mod
@@ -0,0 +1,10 @@
+module github.com/tweag/gerrit-queue
+
+go 1.16
+
+require (
+	github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8
+	github.com/apex/log v1.1.1
+	github.com/google/go-querystring v1.0.0 // indirect
+	github.com/urfave/cli v1.22.1
+)
diff --git a/third_party/gerrit-queue/go.sum b/third_party/gerrit-queue/go.sum
new file mode 100644
index 0000000000..d11545a6c9
--- /dev/null
+++ b/third_party/gerrit-queue/go.sum
@@ -0,0 +1,69 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8 h1:9PvNa6zH6gOW4VVfbAx5rjDLpxunG+RSaXQB+8TEv4w=
+github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8/go.mod h1:0iuRQp6WJ44ts+iihy5E/WlPqfg5RNeQxOmzRkxCdtk=
+github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA=
+github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA=
+github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
+github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
+github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
+github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
+github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
+github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
+github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
+github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
+github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/third_party/gerrit-queue/main.go b/third_party/gerrit-queue/main.go
new file mode 100644
index 0000000000..eaa792c958
--- /dev/null
+++ b/third_party/gerrit-queue/main.go
@@ -0,0 +1,137 @@
+package main
+
+import (
+	"os"
+	"time"
+
+	"net/http"
+
+	"github.com/tweag/gerrit-queue/frontend"
+	"github.com/tweag/gerrit-queue/gerrit"
+	"github.com/tweag/gerrit-queue/misc"
+	"github.com/tweag/gerrit-queue/submitqueue"
+
+	"github.com/urfave/cli"
+
+	"github.com/apex/log"
+	"github.com/apex/log/handlers/multi"
+	"github.com/apex/log/handlers/text"
+)
+
+func main() {
+	var URL, username, password, projectName, branchName string
+	var fetchOnly bool
+	var triggerInterval int
+
+	app := cli.NewApp()
+	app.Name = "gerrit-queue"
+
+	app.Flags = []cli.Flag{
+		cli.StringFlag{
+			Name:        "url",
+			Usage:       "URL to the gerrit instance",
+			EnvVar:      "GERRIT_URL",
+			Destination: &URL,
+			Required:    true,
+		},
+		cli.StringFlag{
+			Name:        "username",
+			Usage:       "Username to use to login to gerrit",
+			EnvVar:      "GERRIT_USERNAME",
+			Destination: &username,
+			Required:    true,
+		},
+		cli.StringFlag{
+			Name:        "password",
+			Usage:       "Password to use to login to gerrit",
+			EnvVar:      "GERRIT_PASSWORD",
+			Destination: &password,
+			Required:    true,
+		},
+		cli.StringFlag{
+			Name:        "project",
+			Usage:       "Gerrit project name to run the submit queue for",
+			EnvVar:      "GERRIT_PROJECT",
+			Destination: &projectName,
+			Required:    true,
+		},
+		cli.StringFlag{
+			Name:        "branch",
+			Usage:       "Destination branch",
+			EnvVar:      "GERRIT_BRANCH",
+			Destination: &branchName,
+			Value:       "master",
+		},
+		cli.IntFlag{
+			Name:        "trigger-interval",
+			Usage:       "How often we should trigger ourselves (interval in seconds)",
+			EnvVar:      "SUBMIT_QUEUE_TRIGGER_INTERVAL",
+			Destination: &triggerInterval,
+			Value:       600,
+		},
+		cli.BoolFlag{
+			Name:        "fetch-only",
+			Usage:       "Only fetch changes and assemble queue, but don't actually write",
+			EnvVar:      "SUBMIT_QUEUE_FETCH_ONLY",
+			Destination: &fetchOnly,
+		},
+	}
+
+	rotatingLogHandler := misc.NewRotatingLogHandler(10000)
+	l := &log.Logger{
+		Handler: multi.New(
+			text.New(os.Stderr),
+			rotatingLogHandler,
+		),
+		Level: log.DebugLevel,
+	}
+
+	app.Action = func(c *cli.Context) error {
+		gerrit, err := gerrit.NewClient(l, URL, username, password, projectName, branchName)
+		if err != nil {
+			return err
+		}
+		log.Infof("Successfully connected to gerrit at %s", URL)
+
+		runner := submitqueue.NewRunner(l, gerrit)
+
+		handler := frontend.MakeFrontend(rotatingLogHandler, gerrit, runner)
+
+		// fetch only on first run
+		err = runner.Trigger(fetchOnly)
+		if err != nil {
+			log.Error(err.Error())
+		}
+
+		// ticker
+		go func() {
+			for {
+				time.Sleep(time.Duration(triggerInterval) * time.Second)
+				err = runner.Trigger(fetchOnly)
+				if err != nil {
+					log.Error(err.Error())
+				}
+			}
+		}()
+
+		server := http.Server{
+			Addr:    ":8080",
+			Handler: handler,
+		}
+
+		server.ListenAndServe()
+		if err != nil {
+			log.Fatalf(err.Error())
+		}
+
+		return nil
+	}
+
+	err := app.Run(os.Args)
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+
+	// TODOS:
+	// - handle event log, either by accepting webhooks, or by streaming events?
+}
diff --git a/third_party/gerrit-queue/misc/rotatingloghandler.go b/third_party/gerrit-queue/misc/rotatingloghandler.go
new file mode 100644
index 0000000000..3d4c5f3a83
--- /dev/null
+++ b/third_party/gerrit-queue/misc/rotatingloghandler.go
@@ -0,0 +1,34 @@
+package misc
+
+import (
+	"sync"
+
+	"github.com/apex/log"
+)
+
+// RotatingLogHandler implementation.
+type RotatingLogHandler struct {
+	mu         sync.Mutex
+	Entries    []*log.Entry
+	maxEntries int
+}
+
+// NewRotatingLogHandler creates a new rotating log handler
+func NewRotatingLogHandler(maxEntries int) *RotatingLogHandler {
+	return &RotatingLogHandler{
+		maxEntries: maxEntries,
+	}
+}
+
+// HandleLog implements log.Handler.
+func (h *RotatingLogHandler) HandleLog(e *log.Entry) error {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+	// drop tail if we have more entries than maxEntries
+	if len(h.Entries) > h.maxEntries {
+		h.Entries = append([]*log.Entry{e}, h.Entries[:(h.maxEntries-2)]...)
+	} else {
+		h.Entries = append([]*log.Entry{e}, h.Entries...)
+	}
+	return nil
+}
diff --git a/third_party/gerrit-queue/submitqueue/runner.go b/third_party/gerrit-queue/submitqueue/runner.go
new file mode 100644
index 0000000000..0b4cbcd8dd
--- /dev/null
+++ b/third_party/gerrit-queue/submitqueue/runner.go
@@ -0,0 +1,220 @@
+package submitqueue
+
+import (
+	"fmt"
+	"sync"
+
+	"github.com/apex/log"
+
+	"github.com/tweag/gerrit-queue/gerrit"
+)
+
+// Runner is a struct existing across the lifetime of a single run of the submit queue
+// it contains a mutex to avoid being run multiple times.
+// In fact, it even cancels runs while another one is still in progress.
+// It contains a Gerrit object facilitating access, a log object, the configured submit queue tag
+// and a `wipSerie` (only populated if waiting for a rebase)
+type Runner struct {
+	mut              sync.Mutex
+	currentlyRunning bool
+	wipSerie         *gerrit.Serie
+	logger           *log.Logger
+	gerrit           *gerrit.Client
+}
+
+// NewRunner creates a new Runner struct
+func NewRunner(logger *log.Logger, gerrit *gerrit.Client) *Runner {
+	return &Runner{
+		logger: logger,
+		gerrit: gerrit,
+	}
+}
+
+// isAutoSubmittable determines if something could be autosubmitted, potentially requiring a rebase
+// for this, it needs to:
+//  * have the "Autosubmit" label set to +1
+//  * have gerrit's 'submittable' field set to true
+// it doesn't check if the series is rebased on HEAD
+func (r *Runner) isAutoSubmittable(s *gerrit.Serie) bool {
+	for _, c := range s.ChangeSets {
+		if c.Submittable != true || !c.IsAutosubmit() {
+			return false
+		}
+	}
+	return true
+}
+
+// IsCurrentlyRunning returns true if the runner is currently running
+func (r *Runner) IsCurrentlyRunning() bool {
+	return r.currentlyRunning
+}
+
+// GetWIPSerie returns the current wipSerie, if any, nil otherwiese
+// Acquires a lock, so check with IsCurrentlyRunning first
+func (r *Runner) GetWIPSerie() *gerrit.Serie {
+	r.mut.Lock()
+	defer func() {
+		r.mut.Unlock()
+	}()
+	return r.wipSerie
+}
+
+// Trigger gets triggered periodically
+func (r *Runner) Trigger(fetchOnly bool) error {
+	// TODO: If CI fails, remove the auto-submit labels => rules.pl
+	// Only one trigger can run at the same time
+	r.mut.Lock()
+	if r.currentlyRunning {
+		return fmt.Errorf("Already running, skipping")
+	}
+	r.currentlyRunning = true
+	r.mut.Unlock()
+	defer func() {
+		r.mut.Lock()
+		r.currentlyRunning = false
+		r.mut.Unlock()
+	}()
+
+	// Prepare the work by creating a local cache of gerrit state
+	err := r.gerrit.Refresh()
+	if err != nil {
+		return err
+	}
+
+	// early return if we only want to fetch
+	if fetchOnly {
+		return nil
+	}
+
+	if r.wipSerie != nil {
+		// refresh wipSerie with how it looks like in gerrit now
+		wipSerie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool {
+			// the new wipSerie needs to have the same number of changesets
+			if len(r.wipSerie.ChangeSets) != len(s.ChangeSets) {
+				return false
+			}
+			// … and the same ChangeIDs.
+			for idx, c := range s.ChangeSets {
+				if r.wipSerie.ChangeSets[idx].ChangeID != c.ChangeID {
+					return false
+				}
+			}
+			return true
+		})
+		if wipSerie == nil {
+			r.logger.WithField("wipSerie", r.wipSerie).Warn("wipSerie has disappeared")
+			r.wipSerie = nil
+		} else {
+			r.wipSerie = wipSerie
+		}
+	}
+
+	for {
+		// initialize logger
+		r.logger.Info("Running")
+		if r.wipSerie != nil {
+			// if we have a wipSerie
+			l := r.logger.WithField("wipSerie", r.wipSerie)
+			l.Info("Checking wipSerie")
+
+			// discard wipSerie not rebased on HEAD
+			// we rebase them at the end of the loop, so this means master advanced without going through the submit queue
+			if !r.gerrit.SerieIsRebasedOnHEAD(r.wipSerie) {
+				l.Warnf("HEAD has moved to %v while still waiting for wipSerie, discarding it", r.gerrit.GetHEAD())
+				r.wipSerie = nil
+				continue
+			}
+
+			// we now need to check CI feedback:
+			// wipSerie might have failed CI in the meantime
+			for _, c := range r.wipSerie.ChangeSets {
+				if c == nil {
+					l.Error("BUG: changeset is nil")
+					continue
+				}
+				if c.Verified < 0 {
+					l.WithField("failingChangeset", c).Warnf("wipSerie failed CI in the meantime, discarding.")
+					r.wipSerie = nil
+					continue
+				}
+			}
+
+			// it might still be waiting for CI
+			for _, c := range r.wipSerie.ChangeSets {
+				if c == nil {
+					l.Error("BUG: changeset is nil")
+					continue
+				}
+				if c.Verified == 0 {
+					l.WithField("pendingChangeset", c).Warnf("still waiting for CI feedback in wipSerie, going back to sleep.")
+					// break the loop, take a look at it at the next trigger.
+					return nil
+				}
+			}
+
+			// it might be autosubmittable
+			if r.isAutoSubmittable(r.wipSerie) {
+				l.Infof("submitting wipSerie")
+				// if the WIP changeset is ready (auto submittable and rebased on HEAD), submit
+				for _, changeset := range r.wipSerie.ChangeSets {
+					_, err := r.gerrit.SubmitChangeset(changeset)
+					if err != nil {
+						l.WithField("changeset", changeset).Error("error submitting changeset")
+						r.wipSerie = nil
+						return err
+					}
+				}
+				r.wipSerie = nil
+			} else {
+				l.Error("BUG: wipSerie is not autosubmittable")
+				r.wipSerie = nil
+			}
+		}
+
+		r.logger.Info("Looking for series ready to submit")
+		// Find serie, that:
+		//  * has the auto-submit label
+		//  * has +2 review
+		//  * has +1 CI
+		//  * is rebased on master
+		serie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool {
+			return r.isAutoSubmittable(s) && s.ChangeSets[0].ParentCommitIDs[0] == r.gerrit.GetHEAD()
+		})
+		if serie != nil {
+			r.logger.WithField("serie", serie).Info("Found serie to submit without necessary rebase")
+			r.wipSerie = serie
+			continue
+		}
+
+		// Find serie, that:
+		//  * has the auto-submit label
+		//  * has +2 review
+		//  * has +1 CI
+		//  * is NOT rebased on master
+		serie = r.gerrit.FindSerie(r.isAutoSubmittable)
+		if serie == nil {
+			r.logger.Info("no more submittable series found, going back to sleep.")
+			break
+		}
+
+		l := r.logger.WithField("serie", serie)
+		l.Info("found serie, which needs a rebase")
+		// TODO: move into Client.RebaseSeries function
+		head := r.gerrit.GetHEAD()
+		for _, changeset := range serie.ChangeSets {
+			changeset, err := r.gerrit.RebaseChangeset(changeset, head)
+			if err != nil {
+				l.Error(err.Error())
+				return err
+			}
+			head = changeset.CommitID
+		}
+		// we don't need to care about updating the rebased changesets or getting the updated HEAD,
+		// as we'll refetch it on the beginning of the next trigger anyways
+		r.wipSerie = serie
+		break
+	}
+
+	r.logger.Info("Run complete")
+	return nil
+}
diff --git a/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch b/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch
new file mode 100644
index 0000000000..7d197795b7
--- /dev/null
+++ b/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch
@@ -0,0 +1,25 @@
+From 621cadcc1dd71e9397c21cf8cf0f1aae4f6f7057 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:02:09 +0100
+Subject: [PATCH 1/7] Use detzip in download_bower.py
+
+---
+ tools/js/download_bower.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
+index d541b565a9..ffdae60f95 100755
+--- a/tools/js/download_bower.py
++++ b/tools/js/download_bower.py
+@@ -110,7 +110,7 @@ def main():
+                 args.b, '--quiet', 'install', '%s#%s' % (args.p, args.v)))
+         bc = os.path.join(cwd, 'bower_components')
+         subprocess.check_call(
+-            ['zip', '-q', '--exclude', '.bower.json', '-r', cached, args.n],
++            ['detzip', '--exclude', '.bower.json', cached, args.n],
+             cwd=bc)
+ 
+         if args.s:
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0002-Syntax-highlight-nix.patch b/third_party/gerrit/0002-Syntax-highlight-nix.patch
new file mode 100644
index 0000000000..256da0a3c9
--- /dev/null
+++ b/third_party/gerrit/0002-Syntax-highlight-nix.patch
@@ -0,0 +1,24 @@
+From 924647c354576ade0dc46fdf30596967f58bb4c6 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:02:32 +0100
+Subject: [PATCH 2/7] Syntax highlight nix
+
+---
+ .../app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts         | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+index 081d28d749..2762ccc625 100644
+--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
++++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+@@ -99,6 +99,7 @@ const LANGUAGE_MAP = new Map<string, string>([
+   ['text/x-vhdl', 'vhdl'],
+   ['text/x-yaml', 'yaml'],
+   ['text/vbscript', 'vbscript'],
++  ['application/x-mix-transfer', 'nix'],
+ ]);
+ const ASYNC_DELAY = 10;
+ 
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch b/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch
new file mode 100644
index 0000000000..02bb3397ea
--- /dev/null
+++ b/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch
@@ -0,0 +1,46 @@
+From be348f64eda257ae0af1f89552548d3e8eca3688 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:02:43 +0100
+Subject: [PATCH 3/7] Syntax highlight rules.pl
+
+---
+ .../diff/gr-syntax-layer/gr-syntax-layer.ts         | 13 ++++++++++++-
+ 1 file changed, 12 insertions(+), 1 deletion(-)
+
+diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+index 2762ccc625..598e14589f 100644
+--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
++++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+@@ -103,6 +103,10 @@ const LANGUAGE_MAP = new Map<string, string>([
+ ]);
+ const ASYNC_DELAY = 10;
+ 
++const FILENAME_OVERRIDES = new Map<string, string>([
++  ['rules.pl', 'prolog'],
++]);
++
+ const CLASS_SAFELIST = new Set<string>([
+   'gr-diff gr-syntax gr-syntax-attr',
+   'gr-diff gr-syntax gr-syntax-attribute',
+@@ -241,10 +245,17 @@ export class GrSyntaxLayer implements DiffLayer {
+     }
+   }
+ 
++  _basename(filename: string): string {
++    const pieces = filename.split(/\//);
++    return pieces[pieces.length-1];
++  }
++
+   _getLanguage(metaInfo: DiffFileMetaInfo) {
+     // The Gerrit API provides only content-type, but for other users of
+     // gr-diff it may be more convenient to specify the language directly.
+-    return metaInfo.language ?? LANGUAGE_MAP.get(metaInfo.content_type);
++    return metaInfo.language ??
++        FILENAME_OVERRIDES.get(this._basename(metaInfo.name)) ??
++        LANGUAGE_MAP.get(metaInfo.content_type);
+   }
+ 
+   /**
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch b/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch
new file mode 100644
index 0000000000..8e78e5f535
--- /dev/null
+++ b/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch
@@ -0,0 +1,217 @@
+From 32bf13d8316f93828d2ff47ccfca38d4e7a634b1 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:03:02 +0100
+Subject: [PATCH 4/7] Add titles to CLs over HTTP
+
+---
+ .../gerrit/httpd/raw/IndexHtmlUtil.java       | 13 +++-
+ .../google/gerrit/httpd/raw/IndexServlet.java |  8 ++-
+ .../google/gerrit/httpd/raw/StaticModule.java |  6 +-
+ .../gerrit/httpd/raw/TitleComputer.java       | 67 +++++++++++++++++++
+ .../gerrit/httpd/raw/PolyGerritIndexHtml.soy  |  4 +-
+ 5 files changed, 90 insertions(+), 8 deletions(-)
+ create mode 100644 java/com/google/gerrit/httpd/raw/TitleComputer.java
+
+diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+index 8d52f5ad50..a9cfceb3b6 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
++++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+@@ -39,6 +39,7 @@ import java.util.Arrays;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.Map;
++import java.util.Optional;
+ import java.util.Set;
+ import java.util.function.Function;
+ 
+@@ -60,13 +61,14 @@ public class IndexHtmlUtil {
+       String faviconPath,
+       Map<String, String[]> urlParameterMap,
+       Function<String, SanitizedContent> urlInScriptTagOrdainer,
+-      String requestedURL)
++      String requestedURL,
++      TitleComputer titleComputer)
+       throws URISyntaxException, RestApiException {
+     ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+     data.putAll(
+             staticTemplateData(
+                 canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
+-        .putAll(dynamicTemplateData(gerritApi, requestedURL));
++        .putAll(dynamicTemplateData(gerritApi, requestedURL, titleComputer));
+     Set<String> enabledExperiments = experimentFeatures.getEnabledExperimentFeatures();
+ 
+     if (!enabledExperiments.isEmpty()) {
+@@ -77,7 +79,9 @@ public class IndexHtmlUtil {
+ 
+   /** Returns dynamic parameters of {@code index.html}. */
+   public static ImmutableMap<String, Object> dynamicTemplateData(
+-      GerritApi gerritApi, String requestedURL) throws RestApiException, URISyntaxException {
++      GerritApi gerritApi,
++      String requestedURL,
++      TitleComputer titleComputer) throws RestApiException, URISyntaxException {
+     ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+     Map<String, SanitizedContent> initialData = new HashMap<>();
+     Server serverApi = gerritApi.config().server();
+@@ -128,6 +132,9 @@ public class IndexHtmlUtil {
+       // Don't render data
+     }
+ 
++    Optional<String> title = titleComputer.computeTitle(requestedURL);
++    title.ifPresent(s -> data.put("title", s));
++
+     data.put("gerritInitialData", initialData);
+     return data.build();
+   }
+diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
+index 3f2c2028ae..7861c007df 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
++++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
+@@ -46,13 +46,15 @@ public class IndexServlet extends HttpServlet {
+   private final ExperimentFeatures experimentFeatures;
+   private final SoySauce soySauce;
+   private final Function<String, SanitizedContent> urlOrdainer;
++  private TitleComputer titleComputer;
+ 
+   IndexServlet(
+       @Nullable String canonicalUrl,
+       @Nullable String cdnPath,
+       @Nullable String faviconPath,
+       GerritApi gerritApi,
+-      ExperimentFeatures experimentFeatures) {
++      ExperimentFeatures experimentFeatures,
++      TitleComputer titleComputer) {
+     this.canonicalUrl = canonicalUrl;
+     this.cdnPath = cdnPath;
+     this.faviconPath = faviconPath;
+@@ -67,6 +69,7 @@ public class IndexServlet extends HttpServlet {
+         (s) ->
+             UnsafeSanitizedContentOrdainer.ordainAsSafe(
+                 s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
++    this.titleComputer = titleComputer;
+   }
+ 
+   @Override
+@@ -85,7 +88,8 @@ public class IndexServlet extends HttpServlet {
+               faviconPath,
+               parameterMap,
+               urlOrdainer,
+-              requestUrl);
++              requestUrl,
++              titleComputer);
+       renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
+     } catch (URISyntaxException | RestApiException e) {
+       throw new IOException(e);
+diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
+index bb1eb92525..6b20c504d2 100644
+--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
++++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
+@@ -224,11 +224,13 @@ public class StaticModule extends ServletModule {
+         @CanonicalWebUrl @Nullable String canonicalUrl,
+         @GerritServerConfig Config cfg,
+         GerritApi gerritApi,
+-        ExperimentFeatures experimentFeatures) {
++        ExperimentFeatures experimentFeatures,
++        TitleComputer titleComputer) {
+       String cdnPath =
+           options.useDevCdn() ? options.devCdn() : cfg.getString("gerrit", null, "cdnPath");
+       String faviconPath = cfg.getString("gerrit", null, "faviconPath");
+-      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi, experimentFeatures);
++      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi,
++          experimentFeatures, titleComputer);
+     }
+ 
+     @Provides
+diff --git a/java/com/google/gerrit/httpd/raw/TitleComputer.java b/java/com/google/gerrit/httpd/raw/TitleComputer.java
+new file mode 100644
+index 0000000000..8fd2053ad0
+--- /dev/null
++++ b/java/com/google/gerrit/httpd/raw/TitleComputer.java
+@@ -0,0 +1,67 @@
++package com.google.gerrit.httpd.raw;
++
++import com.google.common.flogger.FluentLogger;
++import com.google.gerrit.entities.Change;
++import com.google.gerrit.extensions.restapi.ResourceConflictException;
++import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
++import com.google.gerrit.server.change.ChangeResource;
++import com.google.gerrit.server.permissions.PermissionBackendException;
++import com.google.gerrit.server.restapi.change.ChangesCollection;
++import com.google.inject.Inject;
++import com.google.inject.Provider;
++import com.google.inject.Singleton;
++
++import java.net.MalformedURLException;
++import java.net.URL;
++import java.util.Optional;
++import java.util.regex.Matcher;
++import java.util.regex.Pattern;
++
++@Singleton
++public class TitleComputer {
++  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
++
++  @Inject
++  public TitleComputer(Provider<ChangesCollection> changes) {
++    this.changes = changes;
++  }
++
++  public Optional<String> computeTitle(String requestedURI) {
++    URL url = null;
++    try {
++      url = new URL(requestedURI);
++    } catch (MalformedURLException e) {
++      logger.atWarning().log("Failed to turn %s into a URL.", requestedURI);
++      return Optional.empty();
++    }
++
++    // Try to turn this into a change.
++    Optional<Change.Id> changeId = tryExtractChange(url.getPath());
++    if (changeId.isPresent()) {
++      return titleFromChangeId(changeId.get());
++    }
++
++    return Optional.empty();
++  }
++
++  private static final Pattern extractChangeIdRegex = Pattern.compile("^/(?:c/.*/\\+/)?(?<changeId>[0-9]+)(?:/[0-9]+)?(?:/.*)?$");
++  private final Provider<ChangesCollection> changes;
++
++  private Optional<Change.Id> tryExtractChange(String path) {
++    Matcher m = extractChangeIdRegex.matcher(path);
++    if (!m.matches()) {
++      return Optional.empty();
++    }
++    return Change.Id.tryParse(m.group("changeId"));
++  }
++
++  private Optional<String> titleFromChangeId(Change.Id changeId) {
++    ChangesCollection changesCollection = changes.get();
++    try {
++      ChangeResource changeResource = changesCollection.parse(changeId);
++      return Optional.of(changeResource.getChange().getSubject());
++    } catch (ResourceConflictException | ResourceNotFoundException | PermissionBackendException e) {
++      return Optional.empty();
++    }
++  }
++}
+diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+index 11717fb8a4..1ae9046360 100644
+--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
++++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+@@ -33,10 +33,12 @@
+   {@param? defaultDashboardHex: ?}
+   {@param? dashboardQuery: ?}
+   {@param? userIsAuthenticated: ?}
++  {@param? title: ?}
+   <!DOCTYPE html>{\n}
+   <html lang="en">{\n}
+   <meta charset="utf-8">{\n}
+-  <meta name="description" content="Gerrit Code Review">{\n}
++  {if $title}<title>{$title} · Gerrit Code Review</title>{\n}{/if}
++  <meta name="description" content="{if $title}{$title} · {/if}Gerrit Code Review">{\n}
+   <meta name="referrer" content="never">{\n}
+   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
+ 
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch b/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
new file mode 100644
index 0000000000..b664ea0ea6
--- /dev/null
+++ b/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
@@ -0,0 +1,26 @@
+From bd7db44cabb6de64f03adbaf5e24c73e022a8932 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Sat, 11 Jul 2020 00:45:57 +0000
+Subject: [PATCH 5/7] When using local fonts, always assume Gerrit is mounted
+ at the root.
+
+---
+ polygerrit-ui/app/rollup.config.js | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
+index d93b5eab39..c862c9bbae 100644
+--- a/polygerrit-ui/app/rollup.config.js
++++ b/polygerrit-ui/app/rollup.config.js
+@@ -50,7 +50,7 @@ const importLocalFontMetaUrlResolver = function() {
+     name: 'import-meta-url-resolver',
+     resolveImportMeta: function (property, data) {
+       if(property === 'url' && data.moduleId.endsWith('/@polymer/font-roboto-local/roboto.js')) {
+-        return 'new URL("..", document.baseURI).href';
++        return 'new URL("/", document.baseURI).href';
+       }
+       return null;
+     }
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0006-Always-use-Google-Fonts.patch b/third_party/gerrit/0006-Always-use-Google-Fonts.patch
new file mode 100644
index 0000000000..5b817d0b55
--- /dev/null
+++ b/third_party/gerrit/0006-Always-use-Google-Fonts.patch
@@ -0,0 +1,28 @@
+From d71f51afe12a280b92831070a583b15c8b6bc2f4 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Sat, 11 Jul 2020 00:46:13 +0000
+Subject: [PATCH 6/7] Always use Google Fonts.
+
+We're not a corporate, and we're not behind the GFW. Always use Google Fonts,
+because even though we no longer get the caching benefits (boo, browsers),
+it is still a better geographically-distributed CDN.
+---
+ java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+index a9cfceb3b6..9c287c6e45 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
++++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+@@ -184,7 +184,7 @@ public class IndexHtmlUtil {
+     if (urlParameterMap.containsKey("ce")) {
+       data.put("polyfillCE", "true");
+     }
+-    if (urlParameterMap.containsKey("gf")) {
++    if (/* urlParameterMap.containsKey("gf") || */ true) {
+       data.put("useGoogleFonts", "true");
+     }
+ 
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/default.nix b/third_party/gerrit/default.nix
new file mode 100644
index 0000000000..4873cf09b9
--- /dev/null
+++ b/third_party/gerrit/default.nix
@@ -0,0 +1,150 @@
+{ depot, pkgs, ... }:
+
+let
+  detzip = depot.nix.buildGo.program {
+    name = "detzip";
+    srcs = [ ./detzip.go ];
+  };
+  bazelRunScript = pkgs.writeShellScriptBin "bazel-run" ''
+    yarn config set cache-folder "$bazelOut/external/yarn_cache"
+    export HOME="$bazelOut/external/home"
+    mkdir -p "$bazelOut/external/home"
+    exec /bin/bazel "$@"
+  '';
+  bazelTop = pkgs.buildFHSUserEnv {
+    name = "bazel";
+    targetPkgs = pkgs: [
+      (pkgs.bazel.override { enableNixHacks = true; })
+      detzip
+      pkgs.jdk11_headless
+      pkgs.zlib
+      pkgs.python
+      pkgs.curl
+      pkgs.nodejs
+      pkgs.yarn
+      pkgs.git
+      bazelRunScript
+    ];
+    runScript = "/bin/bazel-run";
+  };
+  bazel = bazelTop // { override = x: bazelTop; };
+  version = "3.4.0";
+in
+pkgs.lib.makeOverridable pkgs.buildBazelPackage {
+  pname = "gerrit";
+  inherit version;
+
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/gerrit";
+    rev = "471c1c15a7bc294d10e246df43812942b5ac8a13";
+    branchName = "v${version}";
+    sha256 = "sha256:0ayj0bcsxjln8qydkj9j7yiqibmjgd3bcpqvgsdzdx072wzx01c0";
+    fetchSubmodules = true;
+  };
+
+  patches = [
+    ./0001-Use-detzip-in-download_bower.py.patch
+    ./0002-Syntax-highlight-nix.patch
+    ./0003-Syntax-highlight-rules.pl.patch
+    ./0004-Add-titles-to-CLs-over-HTTP.patch
+    ./0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
+    ./0006-Always-use-Google-Fonts.patch
+  ];
+
+  bazelTarget = "release api-skip-javadoc";
+  inherit bazel;
+
+  bazelFlags = [
+    "--repository_cache="
+    "--disk_cache="
+  ];
+  removeRulesCC = false;
+  fetchConfigured = true;
+
+  fetchAttrs = {
+    sha256 = "sha256:1q4sclf18zzh8hsnccg1y7vqnhgavq62mqp4xx50zxfcnixfkpbx";
+    preBuild = ''
+      rm .bazelversion
+    '';
+
+    installPhase = ''
+      runHook preInstall
+
+      # Remove all built in external workspaces, Bazel will recreate them when building
+      rm -rf $bazelOut/external/{bazel_tools,\@bazel_tools.marker}
+      rm -rf $bazelOut/external/{embedded_jdk,\@embedded_jdk.marker}
+      rm -rf $bazelOut/external/{local_config_cc,\@local_config_cc.marker}
+      rm -rf $bazelOut/external/{local_*,\@local_*.marker}
+
+      # Clear markers
+      find $bazelOut/external -name '@*\.marker' -exec sh -c 'echo > {}' \;
+
+      # Remove all vcs files
+      rm -rf $(find $bazelOut/external -type d -name .git)
+      rm -rf $(find $bazelOut/external -type d -name .svn)
+      rm -rf $(find $bazelOut/external -type d -name .hg)
+
+      # Removing top-level symlinks along with their markers.
+      # This is needed because they sometimes point to temporary paths (?).
+      # For example, in Tensorflow-gpu build:
+      # platforms -> NIX_BUILD_TOP/tmp/install/35282f5123611afa742331368e9ae529/_embedded_binaries/platforms
+      find $bazelOut/external -maxdepth 1 -type l | while read symlink; do
+        name="$(basename "$symlink")"
+        rm -rf "$symlink" "$bazelOut/external/@$name.marker"
+      done
+
+      # Patching symlinks to remove build directory reference
+      find $bazelOut/external -type l | while read symlink; do
+        new_target="$(readlink "$symlink" | sed "s,$NIX_BUILD_TOP,NIX_BUILD_TOP,")"
+        rm "$symlink"
+        ln -sf "$new_target" "$symlink"
+      done
+
+      echo '${bazel.name}' > $bazelOut/external/.nix-bazel-version
+
+      # Gerrit fixups:
+      # Remove polymer-bridges and ba-linkify, they're in-repo
+      rm -rf $bazelOut/external/yarn_cache/v6/npm-polymer-bridges-*
+      rm -rf $bazelOut/external/yarn_cache/v6/npm-ba-linkify-*
+      # Normalize permissions on .yarn-{tarball,metadata} files
+      find $bazelOut/external/yarn_cache \( -name .yarn-tarball.tgz -or -name .yarn-metadata.json \) -exec chmod 644 {} +
+
+      (cd $bazelOut/ && tar czf $out --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner external/)
+
+      runHook postInstall
+    '';
+  };
+
+  buildAttrs = {
+    preConfigure = ''
+      rm .bazelversion
+    '';
+    installPhase = ''
+      mkdir -p "$out"/webapps/ "$out"/share/api/
+      cp bazel-bin/release.war "$out"/webapps/gerrit-${version}.war
+      unzip bazel-bin/api-skip-javadoc.zip -d "$out"/share/api
+    '';
+
+    nativeBuildInputs = with pkgs; [
+      unzip
+    ];
+  };
+
+  passthru = {
+    # A list of plugins that are part of the gerrit.war file.
+    # Use `java -jar gerrit.war ls | grep -Po '(?<=plugins/)[^.]+' | sed -e 's,^,",' -e 's,$,",' | sort` to generate that list.
+    plugins = [
+      "codemirror-editor"
+      "commit-message-length-validator"
+      "delete-project"
+      "download-commands"
+      "gitiles"
+      "hooks"
+      "plugin-manager"
+      "replication"
+      "reviewnotes"
+      "singleusergroup"
+      "webhooks"
+    ];
+  };
+}
diff --git a/third_party/gerrit/detzip.go b/third_party/gerrit/detzip.go
new file mode 100644
index 0000000000..511c18ecfe
--- /dev/null
+++ b/third_party/gerrit/detzip.go
@@ -0,0 +1,97 @@
+package main
+
+import (
+	"archive/zip"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+)
+
+var (
+	exclude = flag.String("exclude", "", "comma-separated list of filenames to exclude (in any directory)")
+)
+
+func init() {
+	flag.Usage = func() {
+		fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [zip file] [directory]:\n", os.Args[0])
+		flag.PrintDefaults()
+	}
+}
+
+func listToMap(ss []string) map[string]bool {
+	m := make(map[string]bool)
+	for _, s := range ss {
+		m[s] = true
+	}
+	return m
+}
+
+func main() {
+	flag.Parse()
+	if flag.NArg() != 2 {
+		flag.Usage()
+		os.Exit(1)
+	}
+
+	outPath := flag.Arg(0)
+	dirPath := flag.Arg(1)
+
+	excludeFiles := listToMap(strings.Split(*exclude, ","))
+
+	// Aggregate all files first.
+	var files []string
+	filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if info.IsDir() {
+			return nil
+		}
+		if excludeFiles[info.Name()] {
+			return nil
+		}
+		files = append(files, path)
+		return nil
+	})
+
+	// Create zip
+	outW, err := os.Create(outPath)
+	if err != nil {
+		log.Fatalf("Create(%q): %v", outPath, err)
+	}
+
+	zipW := zip.NewWriter(outW)
+
+	// Output files in alphabetical order
+	sort.Strings(files)
+	for _, f := range files {
+		fw, err := zipW.CreateHeader(&zip.FileHeader{
+			Name:   f,
+			Method: zip.Store,
+		})
+		if err != nil {
+			log.Fatalf("creating %q in zip: %v", f, err)
+		}
+
+		ff, err := os.Open(f)
+		if err != nil {
+			log.Fatalf("opening %q: %v", f, err)
+		}
+		if _, err := io.Copy(fw, ff); err != nil {
+			log.Fatalf("copying %q to zip: %v", f, err)
+		}
+		ff.Close()
+	}
+
+	if err := zipW.Close(); err != nil {
+		log.Fatalf("writing ZIP central directory: %v", err)
+	}
+	if err := outW.Close(); err != nil {
+		log.Fatalf("closing ZIP file: %v", err)
+	}
+}
diff --git a/third_party/gerrit_plugins/builder.nix b/third_party/gerrit_plugins/builder.nix
new file mode 100644
index 0000000000..0b6501801c
--- /dev/null
+++ b/third_party/gerrit_plugins/builder.nix
@@ -0,0 +1,35 @@
+{ depot, pkgs, ... }:
+{
+  buildGerritBazelPlugin =
+    { name
+    , src
+    , depsOutputHash
+    , overlayPluginCmd ? ''
+        cp -R "${src}" "$out/plugins/${name}"
+      ''
+    , postPatch ? ""
+    ,
+    }: ((depot.third_party.gerrit.override {
+      name = "${name}.jar";
+
+      src = pkgs.runCommandLocal "${name}-src" { } ''
+        cp -R "${depot.third_party.gerrit.src}" "$out"
+        chmod +w "$out/plugins"
+        ${overlayPluginCmd}
+      '';
+
+      bazelTarget = "//plugins/${name}";
+    }).overrideAttrs (super: {
+      deps = super.deps.overrideAttrs (superDeps: {
+        outputHash = depsOutputHash;
+      });
+      installPhase = ''
+        cp "bazel-bin/plugins/${name}/${name}.jar" "$out"
+      '';
+      postPatch =
+        if super ? postPatch then ''
+          ${super.postPatch}
+          ${postPatch}
+        '' else postPatch;
+    }));
+}
diff --git a/third_party/gerrit_plugins/default.nix b/third_party/gerrit_plugins/default.nix
new file mode 100644
index 0000000000..c08afb0f77
--- /dev/null
+++ b/third_party/gerrit_plugins/default.nix
@@ -0,0 +1,23 @@
+{ depot, pkgs, ... }@args:
+
+let
+  inherit (import ./builder.nix args) buildGerritBazelPlugin;
+in
+depot.nix.readTree.drvTargets {
+  # https://gerrit.googlesource.com/plugins/owners
+  owners = buildGerritBazelPlugin rec {
+    name = "owners";
+    depsOutputHash = "sha256:00nbqwr83wsqa6l67bv4ywv9795l1ibl0yv1kq5q811syrvk2xiz";
+    src = pkgs.fetchgit {
+      url = "https://gerrit.googlesource.com/plugins/owners";
+      rev = "99a9ab585532d172d141b4641dfc70081513dfc2";
+      sha256 = "sha256:1xn9qb7q94jxfx7yq0zjqjm16gfyzzif13sak9x6j4f9r68frcd4";
+    };
+    overlayPluginCmd = ''
+      chmod +w "$out" "$out/plugins/external_plugin_deps.bzl"
+      cp -R "${src}/owners" "$out/plugins/owners"
+      cp "${src}/external_plugin_deps.bzl" "$out/plugins/external_plugin_deps.bzl"
+      cp -R "${src}/owners-common" "$out/owners-common"
+    '';
+  };
+}
diff --git a/third_party/gerrit_plugins/oauth/cas-6x.patch b/third_party/gerrit_plugins/oauth/cas-6x.patch
new file mode 100644
index 0000000000..7494298b3f
--- /dev/null
+++ b/third_party/gerrit_plugins/oauth/cas-6x.patch
@@ -0,0 +1,69 @@
+diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
+index 450549f..27310cd 100644
+--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
++++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
+@@ -15,7 +15,7 @@
+ package com.googlesource.gerrit.plugins.oauth;
+ 
+ import com.github.scribejava.core.builder.api.DefaultApi20;
+-import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor;
++import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
+ import com.github.scribejava.core.extractors.TokenExtractor;
+ import com.github.scribejava.core.model.OAuth2AccessToken;
+ import com.github.scribejava.core.oauth2.bearersignature.BearerSignature;
+@@ -47,6 +47,6 @@ public class CasApi extends DefaultApi20 {
+ 
+   @Override
+   public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
+-    return OAuth2AccessTokenExtractor.instance();
++    return OAuth2AccessTokenJsonExtractor.instance();
+   }
+ }
+diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
+index 5f3e4a1..fc5bc50 100644
+--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
++++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
+@@ -106,36 +106,14 @@ class CasOAuthService implements OAuthServiceProvider {
+         throw new IOException(String.format("CAS response missing id: %s", response.getBody()));
+       }
+ 
+-      JsonElement attrListJson = jsonObject.get("attributes");
+-      if (attrListJson == null) {
+-        throw new IOException(
+-            String.format("CAS response missing attributes: %s", response.getBody()));
+-      }
+-
+       String email = null, name = null, login = null;
+-      if (attrListJson.isJsonArray()) {
+-        // It is possible for CAS to be configured to not return any attributes (email, name,
+-        // login),
+-        // in which case,
+-        // CAS returns an empty JSON object "attributes":{}, rather than "null" or an empty JSON
+-        // array
+-        // "attributes": []
+-
+-        JsonArray attrJson = attrListJson.getAsJsonArray();
+-        for (JsonElement elem : attrJson) {
+-          if (elem == null || !elem.isJsonObject()) {
+-            throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", elem));
+-          }
+-          JsonObject obj = elem.getAsJsonObject();
+-
+-          String property = getStringElement(obj, "email");
+-          if (property != null) email = property;
+-          property = getStringElement(obj, "name");
+-          if (property != null) name = property;
+-          property = getStringElement(obj, "login");
+-          if (property != null) login = property;
+-        }
+-      }
++
++      String property = getStringElement(jsonObject, "mail");
++      if (property != null) email = property;
++      property = getStringElement(jsonObject, "displayName");
++      if (property != null) name = property;
++      property = getStringElement(jsonObject, "uid");
++      if (property != null) login = property;
+ 
+       return new OAuthUserInfo(
+           CAS_PROVIDER_PREFIX + id.getAsString(),
diff --git a/third_party/gerrit_plugins/oauth/default.nix b/third_party/gerrit_plugins/oauth/default.nix
new file mode 100644
index 0000000000..01748ba842
--- /dev/null
+++ b/third_party/gerrit_plugins/oauth/default.nix
@@ -0,0 +1,27 @@
+{ depot, pkgs, ... }@args:
+
+let
+  inherit (import ../builder.nix args) buildGerritBazelPlugin;
+in
+buildGerritBazelPlugin rec {
+  name = "oauth";
+  depsOutputHash = "sha256:0j86amkw54y177s522hc988hqg034fsrkywbsb9a7h14zwcqbran";
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/plugins/oauth";
+    rev = "4aa7322db5ec221b2419e12a9ec7af5b8c66659c";
+    sha256 = "1szra3pjl0axf4a7k96flpk7rhfvp37rdxay4gbglh939gzbba88";
+  };
+  overlayPluginCmd = ''
+    chmod +w "$out" "$out/plugins/external_plugin_deps.bzl"
+    cp -R "${src}" "$out/plugins/${name}"
+    cp "${src}/external_plugin_deps.bzl" "$out/plugins/external_plugin_deps.bzl"
+  '';
+
+  # The code in the OAuth repo expects CAS to return oauth2 access tokens as urlencoded.
+  # Our version of CAS returns them as JSON instead.
+  postPatch = ''
+    pushd plugins/oauth
+    patch -p1 <${./cas-6x.patch}
+    popd
+  '';
+}
diff --git a/third_party/git/0001-feat-third_party-git-date-add-dottime-format.patch b/third_party/git/0001-feat-third_party-git-date-add-dottime-format.patch
new file mode 100644
index 0000000000..a249b1ce1b
--- /dev/null
+++ b/third_party/git/0001-feat-third_party-git-date-add-dottime-format.patch
@@ -0,0 +1,119 @@
+From 2fd675c5379dcfa7a2c3465e325cdea8faa2b95c Mon Sep 17 00:00:00 2001
+From: Vincent Ambo <tazjin@google.com>
+Date: Mon, 6 Jan 2020 16:00:52 +0000
+Subject: [PATCH] feat(third_party/git/date): add "dottime" format
+
+Adds dottime (as defined on https://dotti.me) as a timestamp format.
+
+This format is designed to simplify working with timestamps across
+many different timezones by keeping the timestamp format itself in
+UTC (and indicating this with a dot character), but appending the
+local offset.
+
+This is implemented as a new format because the timestamp needs to be
+rendered both as UTC and including the offset, an implementation using
+a strftime formatting string is not sufficient.
+---
+ Documentation/rev-list-options.txt |  3 +++
+ builtin/blame.c                    |  3 +++
+ date.c                             | 17 +++++++++++++++++
+ date.h                             |  3 ++-
+ t/t0006-date.sh                    |  2 ++
+ 5 files changed, 27 insertions(+), 1 deletion(-)
+
+diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt
+index fd4f4e26c9..1f7ab97865 100644
+--- a/Documentation/rev-list-options.txt
++++ b/Documentation/rev-list-options.txt
+@@ -1054,6 +1054,9 @@ omitted.
+ 1970).  As with `--raw`, this is always in UTC and therefore `-local`
+ has no effect.
+ 
++`--date=dottime` shows the date in dottime format (rendered as UTC,
++but suffixed with the local timezone offset if given)
++
+ `--date=format:...` feeds the format `...` to your system `strftime`,
+ except for %s, %z, and %Z, which are handled internally.
+ Use `--date=format:%c` to show the date in your system locale's
+diff --git a/builtin/blame.c b/builtin/blame.c
+index 8d15b68afc..e0cdf418f5 100644
+--- a/builtin/blame.c
++++ b/builtin/blame.c
+@@ -1009,6 +1009,9 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
+ 	case DATE_STRFTIME:
+ 		blame_date_width = strlen(show_date(0, 0, &blame_date_mode)) + 1; /* add the null */
+ 		break;
++	case DATE_DOTTIME:
++		blame_date_width = sizeof("2006-10-19T15·00-0700");
++		break;
+ 	}
+ 	blame_date_width -= 1; /* strip the null */
+ 
+diff --git a/date.c b/date.c
+index 68a260c214..1485e3808f 100644
+--- a/date.c
++++ b/date.c
+@@ -347,6 +347,21 @@ const char *show_date(timestamp_t time, int tz, const struct date_mode *mode)
+ 				tm->tm_mday,
+ 				tm->tm_hour, tm->tm_min, tm->tm_sec,
+ 				sign, tz / 100, tz % 100);
++	} else if (mode->type == DATE_DOTTIME) {
++		char sign = (tz >= 0) ? '+' : '-';
++		tz = abs(tz);
++
++		// Time is converted again without the timezone as the
++		// dottime format includes the zone only in offset
++		// position.
++		time_t t = gm_time_t(time, 0);
++		gmtime_r(&t, tm);
++		strbuf_addf(&timebuf, "%04d-%02d-%02dT%02d·%02d%c%02d%02d",
++				tm->tm_year + 1900,
++				tm->tm_mon + 1,
++				tm->tm_mday,
++				tm->tm_hour, tm->tm_min,
++				sign, tz / 100, tz % 100);
+ 	} else if (mode->type == DATE_RFC2822)
+ 		strbuf_addf(&timebuf, "%.3s, %d %.3s %d %02d:%02d:%02d %+05d",
+ 			weekday_names[tm->tm_wday], tm->tm_mday,
+@@ -955,6 +970,8 @@ static enum date_mode_type parse_date_type(const char *format, const char **end)
+ 		return DATE_UNIX;
+ 	if (skip_prefix(format, "format", end))
+ 		return DATE_STRFTIME;
++	if (skip_prefix(format, "dottime", end))
++		return DATE_DOTTIME;
+ 	/*
+ 	 * Please update $__git_log_date_formats in
+ 	 * git-completion.bash when you add new formats.
+diff --git a/date.h b/date.h
+index 5d4eaba0a9..ff8fdffdbf 100644
+--- a/date.h
++++ b/date.h
+@@ -17,7 +17,8 @@ enum date_mode_type {
+ 	DATE_RFC2822,
+ 	DATE_STRFTIME,
+ 	DATE_RAW,
+-	DATE_UNIX
++	DATE_UNIX,
++	DATE_DOTTIME
+ };
+ 
+ struct date_mode {
+diff --git a/t/t0006-date.sh b/t/t0006-date.sh
+index 2490162071..7ce4fe1927 100755
+--- a/t/t0006-date.sh
++++ b/t/t0006-date.sh
+@@ -51,9 +51,11 @@ check_show short "$TIME" '2016-06-15'
+ check_show default "$TIME" 'Wed Jun 15 16:13:20 2016 +0200'
+ check_show raw "$TIME" '1466000000 +0200'
+ check_show unix "$TIME" '1466000000'
++check_show dottime "$TIME" '2016-06-15T14·13+0200'
+ check_show iso-local "$TIME" '2016-06-15 14:13:20 +0000'
+ check_show raw-local "$TIME" '1466000000 +0000'
+ check_show unix-local "$TIME" '1466000000'
++check_show dottime-local "$TIME" '2016-06-15T14·13+0000'
+ 
+ check_show 'format:%z' "$TIME" '+0200'
+ check_show 'format-local:%z' "$TIME" '+0000'
+-- 
+2.35.3
+
diff --git a/third_party/git/default.nix b/third_party/git/default.nix
new file mode 100644
index 0000000000..eed07b5616
--- /dev/null
+++ b/third_party/git/default.nix
@@ -0,0 +1,9 @@
+# git with custom patches. This is also used by cgit via
+# `pkgs.srcOnly`.
+{ pkgs, ... }:
+
+pkgs.git.overrideAttrs (old: {
+  patches = (old.patches or [ ]) ++ [
+    ./0001-feat-third_party-git-date-add-dottime-format.patch
+  ];
+})
diff --git a/third_party/gitignoreSource/default.nix b/third_party/gitignoreSource/default.nix
new file mode 100644
index 0000000000..150de7c990
--- /dev/null
+++ b/third_party/gitignoreSource/default.nix
@@ -0,0 +1,21 @@
+{ pkgs, ... }:
+
+let
+  gitignoreNix = import
+    (pkgs.fetchFromGitHub {
+      owner = "hercules-ci";
+      repo = "gitignore";
+      rev = "f9e996052b5af4032fe6150bba4a6fe4f7b9d698";
+      sha256 = "0jrh5ghisaqdd0vldbywags20m2cxpkbbk5jjjmwaw0gr8nhsafv";
+    })
+    { inherit (pkgs) lib; };
+
+in
+{
+  __functor = _: gitignoreNix.gitignoreSource;
+
+  # expose extra functions here
+  inherit (gitignoreNix)
+    gitignoreFilter
+    ;
+}
diff --git a/third_party/gopkgs/cloud.google.com/go/default.nix b/third_party/gopkgs/cloud.google.com/go/default.nix
new file mode 100644
index 0000000000..015d3e6308
--- /dev/null
+++ b/third_party/gopkgs/cloud.google.com/go/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "cloud.google.com/go";
+
+  src = pkgs.fetchgit {
+    url = "https://code.googlesource.com/gocloud";
+    rev = "4f03f8e4ba168c636e1c218da7ab41a1c8c0d8cf";
+    hash = "sha256:1cgr8f9349r4ymp2k0x49lby47jgi40bblvl1dk6i99ij6faj93d";
+  };
+}
diff --git a/third_party/gopkgs/github.com/cenkalti/backoff/default.nix b/third_party/gopkgs/github.com/cenkalti/backoff/default.nix
new file mode 100644
index 0000000000..c0e0335de7
--- /dev/null
+++ b/third_party/gopkgs/github.com/cenkalti/backoff/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/cenkalti/backoff/v4";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "cenkalti";
+    repo = "backoff";
+    rev = "18fe4ce5a8550e0d0919b680ad3c080a5455bddf";
+    sha256 = "083617p066p77ik0js8wwgb5qzabgvl8wqpkjb8s9alpyqsq2mpg";
+  };
+}
diff --git a/third_party/gopkgs/github.com/charmbracelet/bubbles/default.nix b/third_party/gopkgs/github.com/charmbracelet/bubbles/default.nix
new file mode 100644
index 0000000000..e041edd4b6
--- /dev/null
+++ b/third_party/gopkgs/github.com/charmbracelet/bubbles/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/charmbracelet/bubbles";
+  src = pkgs.fetchFromGitHub {
+    owner = "charmbracelet";
+    repo = "bubbles";
+    # unreleased version required by bubbletea
+    rev = "v0.7.6";
+    sha256 = "1gd4k4f2mj2dnqcbpdrh9plziz0l29ls6mgyy4mfdcdfijfyd30n";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".charmbracelet.bubbletea
+  ];
+}
diff --git a/third_party/gopkgs/github.com/charmbracelet/bubbletea/default.nix b/third_party/gopkgs/github.com/charmbracelet/bubbletea/default.nix
new file mode 100644
index 0000000000..8dc25bd918
--- /dev/null
+++ b/third_party/gopkgs/github.com/charmbracelet/bubbletea/default.nix
@@ -0,0 +1,30 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/charmbracelet/bubbletea";
+  src =
+    let
+      gitSrc = pkgs.fetchFromGitHub {
+        owner = "charmbracelet";
+        repo = "bubbletea";
+        rev = "v0.13.1";
+        sha256 = "0yf2fjkvx8ym9n6f3qp2z7sxs0qsfpj148sfvbrp38k67s3h20cs";
+      };
+      # The examples/ directory is fairly extensive,
+      # but it also adds most of the dependencies.
+    in
+    pkgs.runCommand gitSrc.name { } ''
+      mkdir -p $out
+      ln -s "${gitSrc}"/* $out
+      rm -r $out/examples
+      rm -r $out/tutorials
+    '';
+  deps = with depot.third_party; [
+    gopkgs."github.com".containerd.console
+    gopkgs."github.com".mattn.go-isatty
+    gopkgs."github.com".muesli.reflow.truncate
+    gopkgs."github.com".muesli.termenv
+    gopkgs."golang.org".x.sys.unix
+    gopkgs."golang.org".x.crypto.ssh.terminal
+  ];
+}
diff --git a/third_party/gopkgs/github.com/charmbracelet/lipgloss/default.nix b/third_party/gopkgs/github.com/charmbracelet/lipgloss/default.nix
new file mode 100644
index 0000000000..d10332a1d5
--- /dev/null
+++ b/third_party/gopkgs/github.com/charmbracelet/lipgloss/default.nix
@@ -0,0 +1,21 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/charmbracelet/lipgloss";
+  src = pkgs.fetchFromGitHub {
+    owner = "charmbracelet";
+    repo = "lipgloss";
+    # unreleased version required by bubbletea
+    rev = "v0.1.0";
+    sha256 = "1chhs492rsq7i4mr6qpjv3d89rvsd23ri6psnmil3ah6i286vl06";
+  };
+
+  deps = with depot.third_party; [
+    # gopkgs."github.com".charmbracelet.bubbletea
+    gopkgs."github.com".lucasb-eyer.go-colorful
+    gopkgs."github.com".muesli.reflow.ansi
+    gopkgs."github.com".muesli.reflow.truncate
+    gopkgs."github.com".muesli.reflow.wordwrap
+    gopkgs."github.com".muesli.termenv
+  ];
+}
diff --git a/third_party/gopkgs/github.com/containerd/console/default.nix b/third_party/gopkgs/github.com/containerd/console/default.nix
new file mode 100644
index 0000000000..3f451019e0
--- /dev/null
+++ b/third_party/gopkgs/github.com/containerd/console/default.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/containerd/console";
+  src = pkgs.fetchFromGitHub {
+    owner = "containerd";
+    repo = "console";
+    rev = "v1.0.1";
+    sha256 = "0s837wj6h80fykk2pdmaji75rw9c3863by0gh0cq51hh0lgyjpvg";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.sys.unix
+  ];
+}
diff --git a/third_party/gopkgs/github.com/davecgh/go-spew/default.nix b/third_party/gopkgs/github.com/davecgh/go-spew/default.nix
new file mode 100644
index 0000000000..1395f3dce6
--- /dev/null
+++ b/third_party/gopkgs/github.com/davecgh/go-spew/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/davecgh/go-spew";
+  src = pkgs.fetchFromGitHub {
+    owner = "davecgh";
+    repo = "go-spew";
+    rev = "8991bc29aa16c548c550c7ff78260e27b9ab7c73";
+    sha256 = "0hka6hmyvp701adzag2g26cxdj47g21x6jz4sc6jjz1mn59d474y";
+  };
+}
diff --git a/third_party/gopkgs/github.com/emirpasic/gods/default.nix b/third_party/gopkgs/github.com/emirpasic/gods/default.nix
new file mode 100644
index 0000000000..a858660f43
--- /dev/null
+++ b/third_party/gopkgs/github.com/emirpasic/gods/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/emirpasic/gods";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "emirpasic";
+    repo = "gods";
+    rev = "4e23915b9a82f35f320a68a395a7a5045c826932";
+    sha256 = "00f8ch1rccakc62f9nj97hapvnx84z7wbcdmbmz7p802b9mxk5nl";
+  };
+}
diff --git a/third_party/gopkgs/github.com/golang/glog/default.nix b/third_party/gopkgs/github.com/golang/glog/default.nix
new file mode 100644
index 0000000000..c8426f2b1a
--- /dev/null
+++ b/third_party/gopkgs/github.com/golang/glog/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/golang/glog";
+  src = pkgs.fetchFromGitHub {
+    owner = "golang";
+    repo = "glog";
+    rev = "23def4e6c14b4da8ac2ed8007337bc5eb5007998";
+    sha256 = "0jb2834rw5sykfr937fxi8hxi2zy80sj2bdn9b3jb4b26ksqng30";
+  };
+}
diff --git a/third_party/gopkgs/github.com/golang/groupcache/default.nix b/third_party/gopkgs/github.com/golang/groupcache/default.nix
new file mode 100644
index 0000000000..c2fc341fea
--- /dev/null
+++ b/third_party/gopkgs/github.com/golang/groupcache/default.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/golang/groupcache";
+
+  src = pkgs.fetchgit {
+    url = "https://github.com/golang/groupcache";
+    rev = "611e8accdfc92c4187d399e95ce826046d4c8d73";
+    hash = "sha256:0ydaq1xn03h2arfdri0vcv0df19pk8dvq4ly5hm1kv18yjfv1v13";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".golang.protobuf.proto
+  ];
+}
diff --git a/third_party/gopkgs/github.com/golang/protobuf/default.nix b/third_party/gopkgs/github.com/golang/protobuf/default.nix
new file mode 100644
index 0000000000..119eafb42c
--- /dev/null
+++ b/third_party/gopkgs/github.com/golang/protobuf/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/golang/protobuf";
+
+  src = pkgs.fetchgit {
+    url = "https://github.com/golang/protobuf";
+    rev = "ed6926b37a637426117ccab59282c3839528a700";
+    hash = "sha256:0fynqrim022x9xi2bivkw19npbz4316v4yr7mb677s9s36z4dc4h";
+  };
+}
diff --git a/third_party/gopkgs/github.com/google/uuid/default.nix b/third_party/gopkgs/github.com/google/uuid/default.nix
new file mode 100644
index 0000000000..191863bf0e
--- /dev/null
+++ b/third_party/gopkgs/github.com/google/uuid/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/google/uuid";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "google";
+    repo = "uuid";
+    rev = "c2e93f3ae59f2904160ceaab466009f965df46d6";
+    sha256 = "0zw8fvl6jqg0fmv6kmvhss0g4gkrbvgyvl2zgy5wdbdlgp4fja0h";
+  };
+}
diff --git a/third_party/gopkgs/github.com/googleapis/gax-go/default.nix b/third_party/gopkgs/github.com/googleapis/gax-go/default.nix
new file mode 100644
index 0000000000..63c6f4b1d7
--- /dev/null
+++ b/third_party/gopkgs/github.com/googleapis/gax-go/default.nix
@@ -0,0 +1,19 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/googleapis/gax-go";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "googleapis";
+    repo = "gax-go";
+    rev = "b443e5a67ec8eeac76f5f384004931878cab24b3";
+    sha256 = "075s8b76l14c9vlchly38hsf28bnr7vzq9q57g2kg1025h004lzw";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.net.trace.gopkg
+    gopkgs."google.golang.org".grpc.gopkg
+    gopkgs."google.golang.org".grpc.codes.gopkg
+    gopkgs."google.golang.org".grpc.status.gopkg
+  ];
+}
diff --git a/third_party/gopkgs/github.com/hashicorp/golang-lru/default.nix b/third_party/gopkgs/github.com/hashicorp/golang-lru/default.nix
new file mode 100644
index 0000000000..8d540877d5
--- /dev/null
+++ b/third_party/gopkgs/github.com/hashicorp/golang-lru/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/hashicorp/golang-lru";
+
+  src = pkgs.fetchgit {
+    url = "https://github.com/hashicorp/golang-lru";
+    rev = "7f827b33c0f158ec5dfbba01bb0b14a4541fd81d";
+    hash = "sha256:1p2igd58xkm8yaj2c2wxiplkf2hj6kxwrg6ss7mx61s5rd71v5xb";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.net.context.ctxhttp
+    gopkgs."cloud.google.com".go.compute.metadata
+  ];
+}
diff --git a/third_party/gopkgs/github.com/jbenet/go-context/default.nix b/third_party/gopkgs/github.com/jbenet/go-context/default.nix
new file mode 100644
index 0000000000..401fc6eb40
--- /dev/null
+++ b/third_party/gopkgs/github.com/jbenet/go-context/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/jbenet/go-context";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "jbenet";
+    repo = "go-context";
+    rev = "d14ea06fba99483203c19d92cfcd13ebe73135f4";
+    sha256 = "0q91f5549n81w3z5927n4a1mdh220bdmgl42zi3h992dcc4ls0sl";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.net.context
+  ];
+}
diff --git a/third_party/gopkgs/github.com/kevinburke/ssh_config/default.nix b/third_party/gopkgs/github.com/kevinburke/ssh_config/default.nix
new file mode 100644
index 0000000000..6b4e8f5275
--- /dev/null
+++ b/third_party/gopkgs/github.com/kevinburke/ssh_config/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/kevinburke/ssh_config";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "kevinburke";
+    repo = "ssh_config";
+    rev = "01f96b0aa0cdcaa93f9495f89bbc6cb5a992ce6e";
+    sha256 = "1bxfjkjl3ibzdkwyvgdwawmd0skz30ah1ha10rg6fkxvj7lgg4jz";
+  };
+}
diff --git a/third_party/gopkgs/github.com/lucasb-eyer/go-colorful/default.nix b/third_party/gopkgs/github.com/lucasb-eyer/go-colorful/default.nix
new file mode 100644
index 0000000000..decb7f3db9
--- /dev/null
+++ b/third_party/gopkgs/github.com/lucasb-eyer/go-colorful/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/lucasb-eyer/go-colorful";
+  src = pkgs.fetchFromGitHub {
+    owner = "lucasb-eyer";
+    repo = "go-colorful";
+    # unreleased version required by bubbletea
+    rev = "v1.2.0";
+    sha256 = "08c3fkf27r16izjjd4w94xd1z7w1r4mdalbl53ms2ka2j465s3qs";
+  };
+}
diff --git a/third_party/gopkgs/github.com/mattn/go-isatty/default.nix b/third_party/gopkgs/github.com/mattn/go-isatty/default.nix
new file mode 100644
index 0000000000..6ba12afff7
--- /dev/null
+++ b/third_party/gopkgs/github.com/mattn/go-isatty/default.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/mattn/go-isatty";
+  src = pkgs.fetchFromGitHub {
+    owner = "mattn";
+    repo = "go-isatty";
+    rev = "v0.0.12";
+    sha256 = "1dfsh27d52wmz0nmmzm2382pfrs2fcijvh6cgir7jbb4pnigr5w4";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.sys.unix
+  ];
+}
diff --git a/third_party/gopkgs/github.com/mattn/go-runewidth/default.nix b/third_party/gopkgs/github.com/mattn/go-runewidth/default.nix
new file mode 100644
index 0000000000..3186a06629
--- /dev/null
+++ b/third_party/gopkgs/github.com/mattn/go-runewidth/default.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/mattn/go-runewidth";
+  src = pkgs.fetchFromGitHub {
+    owner = "mattn";
+    repo = "go-runewidth";
+    rev = "v0.0.10";
+    sha256 = "0jh9552ppqvkdfni7x623n0x5mbiaqqhjhmr0zkh28x56k4ysii4";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".rivo.uniseg
+  ];
+}
diff --git a/third_party/gopkgs/github.com/mitchellh/go-homedir/default.nix b/third_party/gopkgs/github.com/mitchellh/go-homedir/default.nix
new file mode 100644
index 0000000000..8c593eaae8
--- /dev/null
+++ b/third_party/gopkgs/github.com/mitchellh/go-homedir/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/mitchellh/go-homedir";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "mitchellh";
+    repo = "go-homedir";
+    rev = "af06845cf3004701891bf4fdb884bfe4920b3727";
+    sha256 = "0ydzkipf28hwj2bfxqmwlww47khyk6d152xax4bnyh60f4lq3nx1";
+  };
+}
diff --git a/third_party/gopkgs/github.com/muesli/reflow/default.nix b/third_party/gopkgs/github.com/muesli/reflow/default.nix
new file mode 100644
index 0000000000..c7c50795c0
--- /dev/null
+++ b/third_party/gopkgs/github.com/muesli/reflow/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/muesli/reflow";
+  src = pkgs.fetchFromGitHub {
+    owner = "muesli";
+    repo = "reflow";
+    # unreleased version required by bubbletea
+    rev = "9e1d0d53df68baf262851201166872afafd04e5d";
+    sha256 = "08bmkqdn7sb5laqc1mvgk4xj31f600n1y04s1ifppjvszbcsxhid";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".mattn.go-runewidth
+  ];
+}
diff --git a/third_party/gopkgs/github.com/muesli/termenv/default.nix b/third_party/gopkgs/github.com/muesli/termenv/default.nix
new file mode 100644
index 0000000000..504d535954
--- /dev/null
+++ b/third_party/gopkgs/github.com/muesli/termenv/default.nix
@@ -0,0 +1,19 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/muesli/termenv";
+  src = pkgs.fetchFromGitHub {
+    owner = "muesli";
+    repo = "termenv";
+    # unreleased version required by bubbletea
+    rev = "v0.8.1";
+    sha256 = "0m24ljq1nq7z933fcvg99fw0fhxj9rb5ll4rlay7z2f2p59mrbdp";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".lucasb-eyer.go-colorful
+    gopkgs."github.com".mattn.go-isatty
+    gopkgs."github.com".mattn.go-runewidth
+    gopkgs."golang.org".x.sys.unix
+  ];
+}
diff --git a/third_party/gopkgs/github.com/pkg/browser/default.nix b/third_party/gopkgs/github.com/pkg/browser/default.nix
new file mode 100644
index 0000000000..4588c1b589
--- /dev/null
+++ b/third_party/gopkgs/github.com/pkg/browser/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/pkg/browser";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "pkg";
+    repo = "browser";
+    rev = "0a3d74bf9ce488f035cf5bc36f753a711bc74334";
+    sha256 = "0lv6kwvm31n79mh14a63zslaf4l9bspi2q0i8i9im4njfl42iv1c";
+  };
+}
diff --git a/third_party/gopkgs/github.com/rivo/uniseg/default.nix b/third_party/gopkgs/github.com/rivo/uniseg/default.nix
new file mode 100644
index 0000000000..f37d70bbda
--- /dev/null
+++ b/third_party/gopkgs/github.com/rivo/uniseg/default.nix
@@ -0,0 +1,14 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/rivo/uniseg";
+  src = pkgs.fetchFromGitHub {
+    owner = "rivo";
+    repo = "uniseg";
+    rev = "v0.1.0";
+    sha256 = "0flpc1px1l6b1lxzhdxi0mvpkkjchppvgxshxxnlmm40s76i9ww5";
+  };
+
+  deps = with depot.third_party; [
+  ];
+}
diff --git a/third_party/gopkgs/github.com/sergi/go-diff/default.nix b/third_party/gopkgs/github.com/sergi/go-diff/default.nix
new file mode 100644
index 0000000000..72fb96d475
--- /dev/null
+++ b/third_party/gopkgs/github.com/sergi/go-diff/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/sergi/go-diff";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "sergi";
+    repo = "go-diff";
+    rev = "58c5cb1602ee9676b5d3590d782bedde80706fcc";
+    sha256 = "0ir8ali2vx0j7pipmlfd6k8c973akyy2nmbjrf008fm800zcp7z2";
+  };
+}
diff --git a/third_party/gopkgs/github.com/src-d/gcfg/default.nix b/third_party/gopkgs/github.com/src-d/gcfg/default.nix
new file mode 100644
index 0000000000..210ab1bc70
--- /dev/null
+++ b/third_party/gopkgs/github.com/src-d/gcfg/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/src-d/gcfg";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "src-d";
+    repo = "gcfg";
+    rev = "1ac3a1ac202429a54835fe8408a92880156b489d";
+    sha256 = "044j95skmyrwjw5fwjk6ka32rjgsg0ar0mfp9np19sh1acwv4x4r";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."gopkg.in".warnings
+  ];
+}
diff --git a/third_party/gopkgs/github.com/xanzy/ssh-agent/default.nix b/third_party/gopkgs/github.com/xanzy/ssh-agent/default.nix
new file mode 100644
index 0000000000..078592aa9d
--- /dev/null
+++ b/third_party/gopkgs/github.com/xanzy/ssh-agent/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "github.com/xanzy/ssh-agent";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "xanzy";
+    repo = "ssh-agent";
+    rev = "6a3e2ff9e7c564f36873c2e36413f634534f1c44";
+    sha256 = "1chjlnv5d6svpymxgsr62d992m2xi6jb5lybjc5zn1h3hv1m01av";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.crypto.ssh.agent
+  ];
+}
diff --git a/third_party/gopkgs/go.opencensus.io/default.nix b/third_party/gopkgs/go.opencensus.io/default.nix
new file mode 100644
index 0000000000..b1ee6da1fc
--- /dev/null
+++ b/third_party/gopkgs/go.opencensus.io/default.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "go.opencensus.io";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "census-instrumentation";
+    repo = "opencensus-go";
+    rev = "643eada29081047b355cfaa1ceb9bc307a10423c";
+    sha256 = "1acmv2f5wz06abphk0yvb9igp2j5sn1v21dg1p8n109rwanwd5v4";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".hashicorp.golang-lru.simplelru
+    gopkgs."github.com".golang.groupcache.lru
+  ];
+}
diff --git a/third_party/gopkgs/golang.org/x/crypto/default.nix b/third_party/gopkgs/golang.org/x/crypto/default.nix
new file mode 100644
index 0000000000..41c3b4e726
--- /dev/null
+++ b/third_party/gopkgs/golang.org/x/crypto/default.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "golang.org/x/crypto";
+
+  src = pkgs.fetchgit {
+    url = "https://go.googlesource.com/crypto";
+    rev = "e9b2fee46413994441b28dfca259d911d963dfed";
+    hash = "sha256:18sz5426h320l9gdll9n43lzzxg2dmqv0s5fjy6sbvbkkpjs1m28";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.sys.unix.gopkg
+  ];
+}
diff --git a/third_party/gopkgs/golang.org/x/net/default.nix b/third_party/gopkgs/golang.org/x/net/default.nix
new file mode 100644
index 0000000000..9a8fef6948
--- /dev/null
+++ b/third_party/gopkgs/golang.org/x/net/default.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "golang.org/x/net";
+
+  src = pkgs.fetchgit {
+    url = "https://go.googlesource.com/net";
+    rev = "c0dbc17a35534bf2e581d7a942408dc936316da4";
+    hash = "sha256:1f1xqh2cvr629fkg9n9k347vf6g91jkrsmgmy8hlqdrq163blb54";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.text.secure.bidirule.gopkg
+    gopkgs."golang.org".x.text.unicode.bidi.gopkg
+    gopkgs."golang.org".x.text.unicode.norm.gopkg
+  ];
+}
diff --git a/third_party/gopkgs/golang.org/x/oauth2/default.nix b/third_party/gopkgs/golang.org/x/oauth2/default.nix
new file mode 100644
index 0000000000..60864ffe4a
--- /dev/null
+++ b/third_party/gopkgs/golang.org/x/oauth2/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "golang.org/x/oauth2";
+
+  src = pkgs.fetchgit {
+    url = "https://go.googlesource.com/oauth2";
+    rev = "858c2ad4c8b6c5d10852cb89079f6ca1c7309787";
+    hash = "sha256:1dc7n8ddph8w6q0i3cwlgvjwpf2wlkx407va1ydnazasi1j5ixrw";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.net.context.ctxhttp
+    gopkgs."cloud.google.com".go.compute.metadata
+  ];
+}
diff --git a/third_party/gopkgs/golang.org/x/sys/default.nix b/third_party/gopkgs/golang.org/x/sys/default.nix
new file mode 100644
index 0000000000..8da07693ce
--- /dev/null
+++ b/third_party/gopkgs/golang.org/x/sys/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "golang.org/x/sys";
+
+  src = pkgs.fetchgit {
+    url = "https://go.googlesource.com/sys";
+    rev = "ac6580df4449443a05718fd7858c1f91ad5f8d20";
+    hash = "sha256:14gvx65w5lddi20s4wypbbvbg9ni3m8777jhp9nqxhixc61k3dyi";
+  };
+}
diff --git a/third_party/gopkgs/golang.org/x/text/default.nix b/third_party/gopkgs/golang.org/x/text/default.nix
new file mode 100644
index 0000000000..f5a900b958
--- /dev/null
+++ b/third_party/gopkgs/golang.org/x/text/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "golang.org/x/text";
+
+  src = pkgs.fetchgit {
+    url = "https://go.googlesource.com/text";
+    rev = "cbf43d21aaebfdfeb81d91a5f444d13a3046e686";
+    hash = "sha256:1h6z2x4ijzd1126zk3lf8f3bp98j1irs7xg6p8nwpymkqkw5laq8";
+  };
+}
diff --git a/third_party/gopkgs/golang.org/x/time/default.nix b/third_party/gopkgs/golang.org/x/time/default.nix
new file mode 100644
index 0000000000..1c03a8f0a9
--- /dev/null
+++ b/third_party/gopkgs/golang.org/x/time/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "golang.org/x/time";
+
+  src = pkgs.fetchgit {
+    url = "https://go.googlesource.com/time";
+    rev = "555d28b269f0569763d25dbe1a237ae74c6bcc82";
+    hash = "sha256:1rhl4lyz030kwfsg63yk83yd3ivryv1afmzdz9sxbhcj84ym6h4r";
+  };
+}
diff --git a/third_party/gopkgs/google.golang.org/api/default.nix b/third_party/gopkgs/google.golang.org/api/default.nix
new file mode 100644
index 0000000000..490d3a9d2b
--- /dev/null
+++ b/third_party/gopkgs/google.golang.org/api/default.nix
@@ -0,0 +1,22 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "google.golang.org/api";
+
+  src = pkgs.fetchgit {
+    url = "https://code.googlesource.com/google-api-go-client";
+    rev = "8b4e46d953bd748a9ff098644a42389b3d8dab41";
+    hash = "sha256:1vffav53qkksrhdqnp8013v90ks6d7jra0vh3sbybg0v0bka7n3p";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".googleapis.gax-go.v2
+    gopkgs."golang.org".x.oauth2.google
+    gopkgs."golang.org".x.oauth2
+    gopkgs."google.golang.org".grpc
+    gopkgs."google.golang.org".grpc.naming
+    gopkgs."go.opencensus.io".plugin.ochttp
+    gopkgs."go.opencensus.io".trace
+    gopkgs."go.opencensus.io".trace.propagation
+  ];
+}
diff --git a/third_party/gopkgs/google.golang.org/genproto/default.nix b/third_party/gopkgs/google.golang.org/genproto/default.nix
new file mode 100644
index 0000000000..cba54e5890
--- /dev/null
+++ b/third_party/gopkgs/google.golang.org/genproto/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "google.golang.org/genproto";
+
+  src = pkgs.fetchgit {
+    url = "https://github.com/google/go-genproto";
+    rev = "0243a4be9c8f1264d238fdc2895620b4d9baf9e1";
+    hash = "sha256:071672lk0pzns98ncbqk6np7l9flwh84hjjibhhm2s1fi941m6q3";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".golang.protobuf.proto.gopkg
+    gopkgs."github.com".golang.protobuf.ptypes.any.gopkg
+  ];
+}
diff --git a/third_party/gopkgs/google.golang.org/grpc/default.nix b/third_party/gopkgs/google.golang.org/grpc/default.nix
new file mode 100644
index 0000000000..522a27d602
--- /dev/null
+++ b/third_party/gopkgs/google.golang.org/grpc/default.nix
@@ -0,0 +1,23 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "google.golang.org/grpc";
+
+  src = pkgs.fetchgit {
+    url = "https://github.com/grpc/grpc-go";
+    rev = "085c980048876e2735d4aba8f0d5bca4d7acaaa5";
+    hash = "sha256:1vl089pv8qgxkbdg10kyd7203psn35wwjzxxbvi22628faqcpg61";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.net.trace
+    gopkgs."golang.org".x.net.http2
+    gopkgs."golang.org".x.net.http2.hpack
+    gopkgs."golang.org".x.sys.unix
+    gopkgs."github.com".golang.protobuf.proto
+    gopkgs."github.com".golang.protobuf.ptypes
+    gopkgs."github.com".golang.protobuf.ptypes.duration
+    gopkgs."github.com".golang.protobuf.ptypes.timestamp
+    gopkgs."google.golang.org".genproto.googleapis.rpc.status
+  ];
+}
diff --git a/third_party/gopkgs/googlemaps.github.io/maps.nix b/third_party/gopkgs/googlemaps.github.io/maps.nix
new file mode 100644
index 0000000000..4d29cc2f89
--- /dev/null
+++ b/third_party/gopkgs/googlemaps.github.io/maps.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "googlemaps.github.io/maps";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "googlemaps";
+    repo = "google-maps-services-go";
+    rev = "a46d9fca56ac82caa79408b2417ea93a75e3b986";
+    sha256 = "1zpl85yd3m417060isdlhxzakqkf4f59jgpz3kcjp2i0mkrskkjs";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".google.uuid
+    gopkgs."golang.org".x.time.rate
+  ];
+}
diff --git a/third_party/gopkgs/gopkg.in/irc.v3/default.nix b/third_party/gopkgs/gopkg.in/irc.v3/default.nix
new file mode 100644
index 0000000000..7bfe550023
--- /dev/null
+++ b/third_party/gopkgs/gopkg.in/irc.v3/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "gopkg.in/irc.v3";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "go-irc";
+    repo = "irc";
+    rev = "21a5301d6035ea204b2a7bb522a7b4598e5f6b28";
+    sha256 = "1pi5y73pr4prhw5bvmp4babiw02nndizgmpksdgrrg28l9f2wm0n";
+  };
+}
diff --git a/third_party/gopkgs/gopkg.in/src-d/go-billy/default.nix b/third_party/gopkgs/gopkg.in/src-d/go-billy/default.nix
new file mode 100644
index 0000000000..b2773d85d5
--- /dev/null
+++ b/third_party/gopkgs/gopkg.in/src-d/go-billy/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "gopkg.in/src-d/go-billy.v4";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "src-d";
+    repo = "go-billy";
+    rev = "fd409ff12f33d0d60af0ce0abeb8d93df360af49";
+    sha256 = "1j0pl6ggzmd2lrqj71vmsnl6cqm43145h7yg6sy3j5n7hhd592qv";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."golang.org".x.sys.unix
+  ];
+}
diff --git a/third_party/gopkgs/gopkg.in/src-d/go-git/default.nix b/third_party/gopkgs/gopkg.in/src-d/go-git/default.nix
new file mode 100644
index 0000000000..ce5fe1d240
--- /dev/null
+++ b/third_party/gopkgs/gopkg.in/src-d/go-git/default.nix
@@ -0,0 +1,31 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  # .v4 is used throughout the codebase and I can't be bothered to do
+  # anything else about it other than using that package path here.
+  path = "gopkg.in/src-d/go-git.v4";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "src-d";
+    repo = "go-git";
+    rev = "1a7db85bca7027d90afdb5ce711622aaac9feaed";
+    sha256 = "08jl4ljrzzil7c3qcl2y1859nhpgw9ixxy1g40ff7kmq989yhs6v";
+  };
+
+  deps = with depot.third_party; [
+    gopkgs."github.com".emirpasic.gods.trees.binaryheap
+    gopkgs."github.com".jbenet.go-context.io
+    gopkgs."github.com".kevinburke.ssh_config
+    gopkgs."github.com".mitchellh.go-homedir
+    gopkgs."github.com".sergi.go-diff.diffmatchpatch
+    gopkgs."github.com".src-d.gcfg
+    gopkgs."github.com".xanzy.ssh-agent
+    gopkgs."golang.org".x.crypto.openpgp
+    gopkgs."golang.org".x.crypto.ssh
+    gopkgs."golang.org".x.crypto.ssh.knownhosts
+    gopkgs."golang.org".x.net.proxy
+    gopkgs."gopkg.in".src-d.go-billy
+    gopkgs."gopkg.in".src-d.go-billy.osfs
+    gopkgs."gopkg.in".src-d.go-billy.util
+  ];
+}
diff --git a/third_party/gopkgs/gopkg.in/warnings/default.nix b/third_party/gopkgs/gopkg.in/warnings/default.nix
new file mode 100644
index 0000000000..1b4659d3d8
--- /dev/null
+++ b/third_party/gopkgs/gopkg.in/warnings/default.nix
@@ -0,0 +1,12 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildGo.external {
+  path = "gopkg.in/warnings.v0";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "go-warnings";
+    repo = "warnings";
+    rev = "27b9fabbdaf131d2169ec3ff7db8ffc4d839635e";
+    sha256 = "1y276jd9gwvjriz8yd98k3srgbnmbja8f7f7m6lvr0h5sbq3g3w9";
+  };
+}
diff --git a/third_party/hii/OWNERS b/third_party/hii/OWNERS
new file mode 100644
index 0000000000..a742d0d22b
--- /dev/null
+++ b/third_party/hii/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - Profpatsch
diff --git a/third_party/impermanence/default.nix b/third_party/impermanence/default.nix
new file mode 100644
index 0000000000..6ef72e4943
--- /dev/null
+++ b/third_party/impermanence/default.nix
@@ -0,0 +1,12 @@
+# NixOS modules for systems with ephemeral root disks.
+#
+# https://github.com/nix-community/impermanence
+
+{ pkgs, ... }:
+
+pkgs.fetchFromGitHub {
+  owner = "nix-community";
+  repo = "impermanence";
+  rev = "58558845bc68dcf2bb32caa80564f7fe3f6cbc61";
+  sha256 = "10z3g4knkvq838zbfq71pkfyl8cffrpavna448wf5mjscycp0gnv";
+}
diff --git a/third_party/irccat/default.nix b/third_party/irccat/default.nix
new file mode 100644
index 0000000000..3181fd2067
--- /dev/null
+++ b/third_party/irccat/default.nix
@@ -0,0 +1,16 @@
+# https://github.com/irccloud/irccat
+{ lib, pkgs, ... }:
+
+pkgs.buildGoModule rec {
+  pname = "irccat";
+  version = "20201108";
+  meta.license = lib.licenses.gpl3;
+  vendorSha256 = "06a985y4alw1rsghgmhfyczns6klz7bbkfn5mnqc9fdfclgg4s3r";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "irccloud";
+    repo = "irccat";
+    rev = "17451e7e267f099e9614ec945541b624520f607e";
+    sha256 = "0l99mycxymyslwi8mmyfdcqa8pdp79wcyb04s5j5y4grmlsxw1wx";
+  };
+}
diff --git a/third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch b/third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch
new file mode 100644
index 0000000000..d3a2c0e998
--- /dev/null
+++ b/third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch
@@ -0,0 +1,43 @@
+From a82ccf1fab187969544b638f6977d698a55dbb2f Mon Sep 17 00:00:00 2001
+From: Vincent Ambo <mail@tazj.in>
+Date: Fri, 11 Feb 2022 13:14:02 +0300
+Subject: [PATCH] josh-proxy: Always require authentication when pushing
+
+This supports the use-case where josh serves a public repo without
+auth, but requires auth for pushing back.
+---
+ josh-proxy/src/auth.rs           | 4 ++--
+ josh-proxy/src/bin/josh-proxy.rs | 2 +-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/josh-proxy/src/auth.rs b/josh-proxy/src/auth.rs
+index 96a8241..0a007f3 100644
+--- a/josh-proxy/src/auth.rs
++++ b/josh-proxy/src/auth.rs
+@@ -54,8 +54,8 @@ impl Handle {
+     }
+ }
+ 
+-pub async fn check_auth(url: &str, auth: &Handle, required: bool) -> josh::JoshResult<bool> {
+-    if required && auth.hash.is_empty() {
++pub async fn check_auth(url: &str, pathinfo: &str, auth: &Handle, required: bool) -> josh::JoshResult<bool> {
++    if auth.hash.is_empty() && (required || pathinfo == "/git-receive-pack") {
+         return Ok(false);
+     }
+ 
+diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs
+index 700f2da..a96da1c 100644
+--- a/josh-proxy/src/bin/josh-proxy.rs
++++ b/josh-proxy/src/bin/josh-proxy.rs
+@@ -449,7 +449,7 @@ async fn call_service(
+     ]
+     .join("");
+ 
+-    if !josh_proxy::auth::check_auth(&remote_url, &auth, ARGS.is_present("require-auth"))
++    if !josh_proxy::auth::check_auth(&remote_url, &parsed_url.pathinfo, &auth, ARGS.is_present("require-auth"))
+         .in_current_span()
+         .await?
+     {
+-- 
+2.34.1
+
diff --git a/third_party/josh/default.nix b/third_party/josh/default.nix
new file mode 100644
index 0000000000..c82f91f80c
--- /dev/null
+++ b/third_party/josh/default.nix
@@ -0,0 +1,38 @@
+# https://github.com/esrlabs/josh
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.fetchFromGitHub {
+    owner = "esrlabs";
+    repo = "josh";
+    rev = "effe6290559136faba5591a115e56c2b30210329";
+    hash = "sha256:0kam9rqjk96brvh15wj3h3vm2sqnr5pckz91az2ida5617d5gp9v";
+  };
+in
+depot.third_party.naersk.buildPackage {
+  inherit src;
+
+  buildInputs = with pkgs; [
+    libgit2
+    openssl
+    pkgconfig
+  ];
+
+  cargoBuildOptions = x: x ++ [
+    "-p"
+    "josh"
+    "-p"
+    "josh-proxy"
+    "-p"
+    "josh-ui"
+  ];
+
+  overrideMain = x: {
+    patches = [ ./0001-josh-proxy-Always-require-authentication-when-pushin.patch ];
+
+    nativeBuildInputs = (x.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ];
+    postInstall = ''
+      wrapProgram $out/bin/josh-proxy --prefix PATH : "${pkgs.git}/bin"
+    '';
+  };
+}
diff --git a/third_party/kernelPatches/trx40_usb_audio/default.nix b/third_party/kernelPatches/trx40_usb_audio/default.nix
new file mode 100644
index 0000000000..f753878f7c
--- /dev/null
+++ b/third_party/kernelPatches/trx40_usb_audio/default.nix
@@ -0,0 +1,9 @@
+# This patch adds the ASUS TRX40 Prime Pro Whatever Edition
+# motherboard to the list of boards for which the USB Audio connector
+# map has been fixed.
+{ ... }:
+
+{
+  name = "trx40_usb_audio";
+  patch = ./trx40_usb_audio.patch;
+}
diff --git a/third_party/kernelPatches/trx40_usb_audio/trx40_usb_audio.patch b/third_party/kernelPatches/trx40_usb_audio/trx40_usb_audio.patch
new file mode 100644
index 0000000000..d55b7bc362
--- /dev/null
+++ b/third_party/kernelPatches/trx40_usb_audio/trx40_usb_audio.patch
@@ -0,0 +1,16 @@
+diff --git a/sound/usb/mixer_maps.c b/sound/usb/mixer_maps.c
+index 0260c750e156..5ee82872e31b 100644
+--- a/sound/usb/mixer_maps.c
++++ b/sound/usb/mixer_maps.c
+@@ -539,6 +539,11 @@ static const struct usbmix_ctl_map usbmix_ctl_maps[] = {
+ 		.id = USB_ID(0x0b05, 0x1917),
+ 		.map = asus_rog_map,
+ 	},
++	{	/* ASUS TRX40 Prime */
++		.id = USB_ID(0x0b05, 0x1918),
++		.map = trx40_mobo_map,
++		.connector_map = trx40_mobo_connector_map,
++	},
+ 	{	/* MSI TRX40 Creator */
+ 		.id = USB_ID(0x0db0, 0x0d64),
+ 		.map = trx40_mobo_map,
diff --git a/third_party/lisp/OWNERS b/third_party/lisp/OWNERS
new file mode 100644
index 0000000000..2d7f7e237b
--- /dev/null
+++ b/third_party/lisp/OWNERS
@@ -0,0 +1,5 @@
+# -*- mode: yaml; -*-
+inherited: true
+owners:
+  - eta
+  - grfn
diff --git a/third_party/lisp/alexandria.nix b/third_party/lisp/alexandria.nix
new file mode 100644
index 0000000000..b522e2d142
--- /dev/null
+++ b/third_party/lisp/alexandria.nix
@@ -0,0 +1,28 @@
+# Alexandria is one of the foundational Common Lisp libraries that
+# pretty much everything depends on.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.alexandria;
+in depot.nix.buildLisp.library {
+  name = "alexandria";
+
+  srcs = map (f: src + ("/alexandria-1/" + f)) [
+    "package.lisp"
+    "definitions.lisp"
+    "binding.lisp"
+    "strings.lisp"
+    "conditions.lisp"
+    "symbols.lisp"
+    "macros.lisp"
+    "functions.lisp"
+    "io.lisp"
+    "hash-tables.lisp"
+    "control-flow.lisp"
+    "lists.lisp"
+    "types.lisp"
+    "arrays.lisp"
+    "sequences.lisp"
+    "numbers.lisp"
+    "features.lisp"
+  ];
+}
diff --git a/third_party/lisp/anaphora.nix b/third_party/lisp/anaphora.nix
new file mode 100644
index 0000000000..c079943e67
--- /dev/null
+++ b/third_party/lisp/anaphora.nix
@@ -0,0 +1,13 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.anaphora;
+in depot.nix.buildLisp.library {
+  name = "anaphora";
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+    "early.lisp"
+    "symbolic.lisp"
+    "anaphora.lisp"
+  ];
+}
diff --git a/third_party/lisp/asdf-flv/.gitattributes b/third_party/lisp/asdf-flv/.gitattributes
new file mode 100644
index 0000000000..2b45716e47
--- /dev/null
+++ b/third_party/lisp/asdf-flv/.gitattributes
@@ -0,0 +1,2 @@
+.gitignore	export-ignore
+.gitattributes	export-ignore
diff --git a/third_party/lisp/asdf-flv/.gitignore b/third_party/lisp/asdf-flv/.gitignore
new file mode 100644
index 0000000000..bdf4ad2ae6
--- /dev/null
+++ b/third_party/lisp/asdf-flv/.gitignore
@@ -0,0 +1,3 @@
+sbcl-*/
+cmu-*/
+openmcl-*/
diff --git a/third_party/lisp/asdf-flv/Makefile b/third_party/lisp/asdf-flv/Makefile
new file mode 100644
index 0000000000..b4c74feefe
--- /dev/null
+++ b/third_party/lisp/asdf-flv/Makefile
@@ -0,0 +1,77 @@
+### Makefile --- Toplevel directory
+
+## Copyright (C) 2011, 2015 Didier Verna
+
+## Author: Didier Verna <didier@didierverna.net>
+
+## This file is part of ASDF-FLV.
+
+## Copying and distribution of this file, with or without modification,
+## are permitted in any medium without royalty provided the copyright
+## notice and this notice are preserved.  This file is offered as-is,
+## without any warranty.
+
+
+### Commentary:
+
+## Contents management by FCM version 0.1.
+
+
+### Code:
+
+PROJECT := asdf-flv
+VERSION := 2.1
+
+W3DIR := $(HOME)/www/software/lisp/$(PROJECT)
+
+DIST_NAME := $(PROJECT)-$(VERSION)
+TARBALL   := $(DIST_NAME).tar.gz
+SIGNATURE := $(TARBALL).asc
+
+
+all:
+
+clean:
+	-rm *~
+
+distclean: clean
+	-rm *.tar.gz *.tar.gz.asc
+
+tag:
+	git tag -a -m 'Version $(VERSION)' 'version-$(VERSION)'
+
+tar: $(TARBALL)
+gpg: $(SIGNATURE)
+dist: tar gpg
+
+install-www: dist
+	-install -m 644 $(TARBALL)   "$(W3DIR)/attic/"
+	-install -m 644 $(SIGNATURE) "$(W3DIR)/attic/"
+	echo "\
+<? lref (\"$(PROJECT)/attic/$(PROJECT)-$(VERSION).tar.gz\", \
+	 contents (\"Dernire version\", \"Latest version\")); ?> \
+| \
+<? lref (\"$(PROJECT)/attic/$(PROJECT)-$(VERSION).tar.gz.asc\", \
+	 contents (\"Signature GPG\", \"GPG Signature\")); ?>" \
+	  > "$(W3DIR)/latest.txt"
+	chmod 644 "$(W3DIR)/latest.txt"
+	cd "$(W3DIR)"					\
+	  && ln -fs attic/$(TARBALL) latest.tar.gz	\
+	  && ln -fs attic/$(SIGNATURE) latest.tar.gz.asc
+
+update-version:
+	perl -pi -e 's/:version ".*"/:version "$(VERSION)"/' \
+	  net.didierverna.$(PROJECT).asd
+
+$(TARBALL):
+	git archive --format=tar --prefix=$(DIST_NAME)/ \
+	    --worktree-attributes HEAD			\
+	  | gzip -c > $@
+
+$(SIGNATURE): $(TARBALL)
+	gpg -b -a $<
+
+
+.PHONY: all clean distclean tag tar gpg dist install-www update-version
+
+### Makefile ends here
diff --git a/third_party/lisp/asdf-flv/README.md b/third_party/lisp/asdf-flv/README.md
new file mode 100644
index 0000000000..7ccdd18881
--- /dev/null
+++ b/third_party/lisp/asdf-flv/README.md
@@ -0,0 +1,7 @@
+ASDF-FLV provides support for file-local variables through ASDF. A file-local
+variable behaves like `*PACKAGE*` and `*READTABLE*` with respect to `LOAD` and
+`COMPILE-FILE`: a new dynamic binding is created before processing the file,
+so that any modification to the variable essentially becomes file-local.
+
+In order to make one or several variables file-local, use the macros
+`SET-FILE-LOCAL-VARIABLE(S)`.
diff --git a/third_party/lisp/asdf-flv/asdf-flv.lisp b/third_party/lisp/asdf-flv/asdf-flv.lisp
new file mode 100644
index 0000000000..76c6845b82
--- /dev/null
+++ b/third_party/lisp/asdf-flv/asdf-flv.lisp
@@ -0,0 +1,64 @@
+;;; asdf-flv.lisp --- Implementation
+
+;; Copyright (C) 2011, 2015 Didier Verna
+
+;; Author: Didier Verna <didier@didierverna.net>
+
+;; This file is part of ASDF-FLV.
+
+;; Copying and distribution of this file, with or without modification,
+;; are permitted in any medium without royalty provided the copyright
+;; notice and this notice are preserved.  This file is offered as-is,
+;; without any warranty.
+
+
+;;; Commentary:
+
+;; Contents management by FCM version 0.1.
+
+
+;;; Code:
+
+(in-package :net.didierverna.asdf-flv)
+
+
+(defvar *file-local-variables* ()
+  "List of file-local special variables.")
+
+
+(defun make-variable-file-local (symbol)
+  "Make special variable named by SYMBOL have a file-local value."
+  (pushnew symbol *file-local-variables*))
+
+(defmacro set-file-local-variable (symbol)
+  "Set special variable named by SYMBOL as file-local.
+SYMBOL need not be quoted."
+  `(make-variable-file-local ',symbol))
+
+(defun make-variables-file-local (&rest symbols)
+  "Make special variables named by SYMBOLS have a file-local value."
+  (dolist (symbol symbols)
+    (pushnew symbol *file-local-variables*)))
+
+(defmacro set-file-local-variables (&rest symbols)
+  "Set special variables named by SYMBOLS as file-local.
+SYMBOLS need not be quoted."
+  `(make-variables-file-local ,@(mapcar (lambda (symbol) (list 'quote symbol))
+					symbols)))
+
+
+(defmethod asdf:perform :around
+    ((operation asdf:load-op) (file asdf:cl-source-file))
+  "Establish new dynamic bindings for file-local variables."
+  (progv *file-local-variables*
+      (mapcar #'symbol-value *file-local-variables*)
+    (call-next-method)))
+
+(defmethod asdf:perform :around
+    ((operation asdf:compile-op) (file asdf:cl-source-file))
+  "Establish new dynamic bindings for file-local variables."
+  (progv *file-local-variables*
+      (mapcar #'symbol-value *file-local-variables*)
+    (call-next-method)))
+
+;;; asdf-flv.lisp ends here
diff --git a/third_party/lisp/asdf-flv/default.nix b/third_party/lisp/asdf-flv/default.nix
new file mode 100644
index 0000000000..e8ec4aa8f8
--- /dev/null
+++ b/third_party/lisp/asdf-flv/default.nix
@@ -0,0 +1,13 @@
+# Imported from https://github.com/didierverna/asdf-flv
+{ depot, ... }:
+
+with depot.nix;
+buildLisp.library {
+  name = "asdf-flv";
+  deps = [ (buildLisp.bundled "asdf") ];
+
+  srcs = [
+    ./package.lisp
+    ./asdf-flv.lisp
+  ];
+}
diff --git a/third_party/lisp/asdf-flv/net.didierverna.asdf-flv.asd b/third_party/lisp/asdf-flv/net.didierverna.asdf-flv.asd
new file mode 100644
index 0000000000..41202746d0
--- /dev/null
+++ b/third_party/lisp/asdf-flv/net.didierverna.asdf-flv.asd
@@ -0,0 +1,43 @@
+;;; net.didierverna.asdf-flv.asd --- ASDF system definition
+
+;; Copyright (C) 2011, 2015 Didier Verna
+
+;; Author: Didier Verna <didier@didierverna.net>
+
+;; This file is part of ASDF-FLV.
+
+;; Copying and distribution of this file, with or without modification,
+;; are permitted in any medium without royalty provided the copyright
+;; notice and this notice are preserved.  This file is offered as-is,
+;; without any warranty.
+
+
+;;; Commentary:
+
+;; Contents management by FCM version 0.1.
+
+
+;;; Code:
+
+(asdf:defsystem :net.didierverna.asdf-flv
+  :long-name "ASDF File Local Variables"
+  :description "ASDF extension to provide support for file-local variables."
+  :long-description "\
+ASDF-FLV provides support for file-local variables through ASDF. A file-local
+variable behaves like *PACKAGE* and *READTABLE* with respect to LOAD and
+COMPILE-FILE: a new dynamic binding is created before processing the file, so
+that any modification to the variable becomes essentially file-local.
+
+In order to make one or several variables file-local, use the macros
+SET-FILE-LOCAL-VARIABLE(S)."
+  :author "Didier Verna"
+  :mailto "didier@didierverna.net"
+  :homepage "http://www.lrde.epita.fr/~didier/software/lisp/misc.php#asdf-flv"
+  :source-control "https://github.com/didierverna/asdf-flv"
+  :license "GNU All Permissive"
+  :version "2.1"
+  :serial t
+  :components ((:file "package")
+	       (:file "asdf-flv")))
+
+;;; net.didierverna.asdf-flv.asd ends here
diff --git a/third_party/lisp/asdf-flv/package.lisp b/third_party/lisp/asdf-flv/package.lisp
new file mode 100644
index 0000000000..1d7fb2bab4
--- /dev/null
+++ b/third_party/lisp/asdf-flv/package.lisp
@@ -0,0 +1,28 @@
+;;; package.lisp --- Package definition
+
+;; Copyright (C) 2011, 2015 Didier Verna
+
+;; Author: Didier Verna <didier@didierverna.net>
+
+;; This file is part of ASDF-FLV.
+
+;; Copying and distribution of this file, with or without modification,
+;; are permitted in any medium without royalty provided the copyright
+;; notice and this notice are preserved.  This file is offered as-is,
+;; without any warranty.
+
+
+;;; Commentary:
+
+;; Contents management by FCM version 0.1.
+
+
+;;; Code:
+
+(in-package :cl-user)
+
+(defpackage :net.didierverna.asdf-flv
+  (:use :cl)
+  (:export :set-file-local-variable :set-file-local-variables))
+
+;;; package.lisp ends here
diff --git a/third_party/lisp/babel.nix b/third_party/lisp/babel.nix
new file mode 100644
index 0000000000..ae7c5dd23d
--- /dev/null
+++ b/third_party/lisp/babel.nix
@@ -0,0 +1,31 @@
+# Babel is an encoding conversion library for Common Lisp.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.babel;
+in depot.nix.buildLisp.library {
+  name = "babel";
+  deps = [
+    depot.third_party.lisp.alexandria
+    depot.third_party.lisp.trivial-features
+  ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "packages.lisp"
+    "encodings.lisp"
+    "enc-ascii.lisp"
+    "enc-ebcdic.lisp"
+    "enc-ebcdic-int.lisp"
+    "enc-iso-8859.lisp"
+    "enc-unicode.lisp"
+    "enc-cp1251.lisp"
+    "enc-cp1252.lisp"
+    "jpn-table.lisp"
+    "enc-jpn.lisp"
+    "enc-gbk.lisp"
+    "enc-koi8.lisp"
+    "external-format.lisp"
+    "strings.lisp"
+    "gbk-map.lisp"
+    "sharp-backslash.lisp"
+  ];
+}
diff --git a/third_party/lisp/bordeaux-threads.nix b/third_party/lisp/bordeaux-threads.nix
new file mode 100644
index 0000000000..8a2e099508
--- /dev/null
+++ b/third_party/lisp/bordeaux-threads.nix
@@ -0,0 +1,24 @@
+# This library is meant to make writing portable multi-threaded apps
+# in Common Lisp simple.
+{ depot, pkgs, ... }:
+
+let
+  src = with pkgs; srcOnly lispPackages.bordeaux-threads;
+  getSrc = f: "${src}/src/${f}";
+in
+depot.nix.buildLisp.library {
+  name = "bordeaux-threads";
+  deps = [ depot.third_party.lisp.alexandria ];
+
+  srcs = map getSrc [
+    "pkgdcl.lisp"
+    "bordeaux-threads.lisp"
+  ] ++ [
+    {
+      sbcl = getSrc "impl-sbcl.lisp";
+      ecl = getSrc "impl-ecl.lisp";
+    }
+  ] ++ map getSrc [
+    "default-implementations.lisp"
+  ];
+}
diff --git a/third_party/lisp/cffi.nix b/third_party/lisp/cffi.nix
new file mode 100644
index 0000000000..de1d0c2e8e
--- /dev/null
+++ b/third_party/lisp/cffi.nix
@@ -0,0 +1,34 @@
+# CFFI purports to be the Common Foreign Function Interface.
+{ depot, pkgs, ... }:
+
+with depot.nix;
+let src = with pkgs; srcOnly lispPackages.cffi;
+in buildLisp.library {
+  name = "cffi";
+  deps = with depot.third_party.lisp; [
+    alexandria
+    babel
+    trivial-features
+    (buildLisp.bundled "asdf")
+  ];
+
+  srcs = [
+    {
+      ecl = src + "/src/cffi-ecl.lisp";
+      sbcl = src + "/src/cffi-sbcl.lisp";
+      ccl = src + "/src/cffi-openmcl.lisp";
+    }
+  ] ++ map (f: src + ("/src/" + f)) [
+    "package.lisp"
+    "utils.lisp"
+    "libraries.lisp"
+    "early-types.lisp"
+    "types.lisp"
+    "enum.lisp"
+    "strings.lisp"
+    "structures.lisp"
+    "functions.lisp"
+    "foreign-vars.lisp"
+    "features.lisp"
+  ];
+}
diff --git a/third_party/lisp/chipz.nix b/third_party/lisp/chipz.nix
new file mode 100644
index 0000000000..59e9914ee1
--- /dev/null
+++ b/third_party/lisp/chipz.nix
@@ -0,0 +1,26 @@
+# Common Lisp library for decompressing deflate, zlib, gzip, and bzip2 data
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.chipz;
+in depot.nix.buildLisp.library {
+  name = "chipz";
+  deps = [ (depot.nix.buildLisp.bundled "asdf") ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "chipz.asd"
+    "package.lisp"
+    "constants.lisp"
+    "conditions.lisp"
+    "dstate.lisp"
+    "types-and-tables.lisp"
+    "crc32.lisp"
+    "adler32.lisp"
+    "inflate-state.lisp"
+    "gzip.lisp"
+    "zlib.lisp"
+    "inflate.lisp"
+    "bzip2.lisp"
+    "decompress.lisp"
+    "stream.lisp"
+  ];
+}
diff --git a/third_party/lisp/chunga.nix b/third_party/lisp/chunga.nix
new file mode 100644
index 0000000000..d3f50bcb1a
--- /dev/null
+++ b/third_party/lisp/chunga.nix
@@ -0,0 +1,22 @@
+# Portable chunked streams for Common Lisp
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.chunga;
+in depot.nix.buildLisp.library {
+  name = "chunga";
+  deps = with depot.third_party.lisp; [
+    trivial-gray-streams
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+    "specials.lisp"
+    "util.lisp"
+    "known-words.lisp"
+    "conditions.lisp"
+    "read.lisp"
+    "streams.lisp"
+    "input.lisp"
+    "output.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-ansi-text.nix b/third_party/lisp/cl-ansi-text.nix
new file mode 100644
index 0000000000..0e34015247
--- /dev/null
+++ b/third_party/lisp/cl-ansi-text.nix
@@ -0,0 +1,16 @@
+# Enables ANSI colors for printing.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-ansi-text;
+in depot.nix.buildLisp.library {
+  name = "cl-ansi-text";
+  deps = with depot.third_party.lisp; [
+    alexandria
+    cl-colors2
+  ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "cl-ansi-text.lisp"
+    "define-colors.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-base64.nix b/third_party/lisp/cl-base64.nix
new file mode 100644
index 0000000000..08055a0471
--- /dev/null
+++ b/third_party/lisp/cl-base64.nix
@@ -0,0 +1,14 @@
+# Base64 encoding for Common Lisp
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-base64;
+in depot.nix.buildLisp.library {
+  name = "cl-base64";
+  srcs = [
+    (src + "/package.lisp")
+    (src + "/encode.lisp")
+    (src + "/decode.lisp")
+  ];
+}
+
+
diff --git a/third_party/lisp/cl-colors.nix b/third_party/lisp/cl-colors.nix
new file mode 100644
index 0000000000..b51e4d46a7
--- /dev/null
+++ b/third_party/lisp/cl-colors.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-colors;
+in depot.nix.buildLisp.library {
+  name = "cl-colors";
+  deps = [
+    depot.third_party.lisp.alexandria
+    depot.third_party.lisp.let-plus
+  ];
+  srcs = [
+    "${src}/package.lisp"
+    "${src}/colors.lisp"
+    "${src}/colornames.lisp"
+    "${src}/hexcolors.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-colors2.nix b/third_party/lisp/cl-colors2.nix
new file mode 100644
index 0000000000..34201bc2fa
--- /dev/null
+++ b/third_party/lisp/cl-colors2.nix
@@ -0,0 +1,18 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-colors2;
+in depot.nix.buildLisp.library {
+  name = "cl-colors2";
+  deps = with depot.third_party.lisp; [
+    alexandria
+    cl-ppcre
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "colors.lisp"
+    "colornames-x11.lisp"
+    "colornames-svg.lisp"
+    "hexcolors.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-date-time-parser.nix b/third_party/lisp/cl-date-time-parser.nix
new file mode 100644
index 0000000000..e53cb2dfce
--- /dev/null
+++ b/third_party/lisp/cl-date-time-parser.nix
@@ -0,0 +1,21 @@
+{ depot, pkgs, ... }:
+
+depot.nix.buildLisp.library {
+  name = "cl-date-time-parser";
+
+  srcs = [
+    (pkgs.fetchurl {
+      url = "https://raw.githubusercontent.com/tkych/cl-date-time-parser/00d6fc70b599f460fdf13cf0cf7e6bf843312410/date-time-parser.lisp";
+      sha256 = "0zrkv1q3sx5ksijxhw45ixf1hy5b9biii6i6v41h12q6pbkfqz69";
+    })
+  ];
+
+  deps = [
+    depot.third_party.lisp.alexandria
+    depot.third_party.lisp.anaphora
+    depot.third_party.lisp.split-sequence
+    depot.third_party.lisp.cl-ppcre
+    depot.third_party.lisp.local-time
+    depot.third_party.lisp.parse-float
+  ];
+}
diff --git a/third_party/lisp/cl-fad.nix b/third_party/lisp/cl-fad.nix
new file mode 100644
index 0000000000..9350abe2e3
--- /dev/null
+++ b/third_party/lisp/cl-fad.nix
@@ -0,0 +1,27 @@
+# Portable pathname library
+{ depot, pkgs, ... }:
+
+with depot.nix;
+
+let src = with pkgs; srcOnly lispPackages.cl-fad;
+in buildLisp.library {
+  name = "cl-fad";
+
+  deps = with depot.third_party.lisp; [
+    alexandria
+    bordeaux-threads
+    {
+      sbcl = buildLisp.bundled "sb-posix";
+    }
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+  ] ++ [
+    { ccl = "${src}/openmcl.lisp"; }
+  ] ++ map (f: src + ("/" + f)) [
+    "fad.lisp"
+    "path.lisp"
+    "temporary-files.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-json.nix b/third_party/lisp/cl-json.nix
new file mode 100644
index 0000000000..0230f274af
--- /dev/null
+++ b/third_party/lisp/cl-json.nix
@@ -0,0 +1,29 @@
+# JSON encoder & decoder
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+
+  src = pkgs.fetchFromGitHub {
+    owner = "hankhero";
+    repo = "cl-json";
+    rev = "6dfebb9540bfc3cc33582d0c03c9ec27cb913e79";
+    sha256 = "0fx3m3x3s5ji950yzpazz4s0img3l6b3d6l3jrfjv0lr702496lh";
+  };
+in
+buildLisp.library {
+  name = "cl-json";
+  deps = [ (buildLisp.bundled "asdf") ];
+
+  srcs = [ "${src}/cl-json.asd" ] ++
+    (map (f: src + ("/src/" + f)) [
+      "package.lisp"
+      "common.lisp"
+      "objects.lisp"
+      "camel-case.lisp"
+      "decoder.lisp"
+      "encoder.lisp"
+      "utils.lisp"
+      "json-rpc.lisp"
+    ]);
+}
diff --git a/third_party/lisp/cl-plus-ssl.nix b/third_party/lisp/cl-plus-ssl.nix
new file mode 100644
index 0000000000..dc0a95944f
--- /dev/null
+++ b/third_party/lisp/cl-plus-ssl.nix
@@ -0,0 +1,50 @@
+# Common Lisp bindings to OpenSSL
+{ depot, pkgs, ... }:
+
+with depot.nix;
+
+let
+  src = pkgs.fetchgit {
+    url = "https://github.com/cl-plus-ssl/cl-plus-ssl.git";
+    rev = "29081992f6d7b4e3aa2c5eeece4cd92b745071f4";
+    hash = "sha256:16lyrixl98b7vy29dbbzkbq0xaz789350dajrr1gdny5i55rkjq0";
+  };
+in
+buildLisp.library {
+  name = "cl-plus-ssl";
+  deps = with depot.third_party.lisp; [
+    alexandria
+    bordeaux-threads
+    cffi
+    flexi-streams
+    trivial-features
+    trivial-garbage
+    trivial-gray-streams
+    {
+      scbl = buildLisp.bundled "uiop";
+      default = buildLisp.bundled "asdf";
+    }
+    { sbcl = buildLisp.bundled "sb-posix"; }
+  ];
+
+  native = [ pkgs.openssl ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "package.lisp"
+    "reload.lisp"
+    "conditions.lisp"
+    "ffi.lisp"
+    "x509.lisp"
+    "ffi-buffer-all.lisp"
+    "ffi-buffer.lisp"
+    "streams.lisp"
+    "bio.lisp"
+    "random.lisp"
+    "context.lisp"
+    "verify-hostname.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/cl-ppcre.nix b/third_party/lisp/cl-ppcre.nix
new file mode 100644
index 0000000000..561e306191
--- /dev/null
+++ b/third_party/lisp/cl-ppcre.nix
@@ -0,0 +1,27 @@
+# cl-ppcre is a Common Lisp regular expression library.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-ppcre;
+in depot.nix.buildLisp.library {
+  name = "cl-ppcre";
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+    "specials.lisp"
+    "util.lisp"
+    "errors.lisp"
+    "charset.lisp"
+    "charmap.lisp"
+    "chartest.lisp"
+    "lexer.lisp"
+    "parser.lisp"
+    "regex-class.lisp"
+    "regex-class-util.lisp"
+    "convert.lisp"
+    "optimize.lisp"
+    "closures.lisp"
+    "repetition-closures.lisp"
+    "scanner.lisp"
+    "api.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-prevalence.nix b/third_party/lisp/cl-prevalence.nix
new file mode 100644
index 0000000000..188cbc686d
--- /dev/null
+++ b/third_party/lisp/cl-prevalence.nix
@@ -0,0 +1,25 @@
+# cl-prevalence is an implementation of object prevalence for CL (i.e.
+# an in-memory database)
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-prevalence;
+in depot.nix.buildLisp.library {
+  name = "cl-prevalence";
+
+  deps = with depot.third_party.lisp; [
+    bordeaux-threads
+    s-xml
+    s-sysdeps
+  ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "package.lisp"
+    "serialization/serialization.lisp"
+    "serialization/xml.lisp"
+    "serialization/sexp.lisp"
+    "prevalence.lisp"
+    "managed-prevalence.lisp"
+    "master-slave.lisp"
+    "blob.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-smtp.nix b/third_party/lisp/cl-smtp.nix
new file mode 100644
index 0000000000..7ab9bea59f
--- /dev/null
+++ b/third_party/lisp/cl-smtp.nix
@@ -0,0 +1,24 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-smtp;
+in depot.nix.buildLisp.library {
+  name = "cl-smtp";
+  deps = with depot.third_party.lisp; [
+    usocket
+    trivial-gray-streams
+    flexi-streams
+    cl-base64
+    cl-plus-ssl
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "attachments.lisp"
+    "cl-smtp.lisp"
+    "mime-types.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/cl-unicode.nix b/third_party/lisp/cl-unicode.nix
new file mode 100644
index 0000000000..815d99c2dc
--- /dev/null
+++ b/third_party/lisp/cl-unicode.nix
@@ -0,0 +1,80 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs) sbcl runCommand writeText;
+  inherit (depot.nix.buildLisp) bundled;
+
+  src = pkgs.fetchFromGitHub {
+    owner = "edicl";
+    repo = "cl-unicode";
+    rev = "8073fc5634c9d4802888ac03abf11dfe383e16fa";
+    sha256 = "0ykx2s9lqfl74p1px0ik3l2izd1fc9jd1b4ra68s5x34rvjy0hza";
+  };
+
+  cl-unicode-base = depot.nix.buildLisp.library {
+    name = "cl-unicode-base";
+    deps = with depot.third_party.lisp; [
+      cl-ppcre
+    ];
+
+    srcs = map (f: src + ("/" + f)) [
+      "packages.lisp"
+      "specials.lisp"
+      "util.lisp"
+    ];
+  };
+
+  cl-unicode-build = depot.nix.buildLisp.program {
+    name = "cl-unicode-build";
+    deps = with depot.third_party.lisp; [
+      cl-unicode-base
+      flexi-streams
+      {
+        ecl = bundled "asdf";
+        default = bundled "uiop";
+      }
+    ];
+
+    srcs = (map (f: src + ("/build/" + f)) [
+      "util.lisp"
+      "char-info.lisp"
+      "read.lisp"
+    ]) ++ [
+      (runCommand "dump.lisp" { } ''
+        substitute ${src}/build/dump.lisp $out \
+          --replace ':defaults *this-file*' ":defaults (uiop:getcwd)"
+      '')
+
+      (writeText "export-create-source-files.lisp" ''
+        (in-package :cl-unicode)
+        (export 'create-source-files)
+      '')
+    ];
+
+    main = "cl-unicode:create-source-files";
+  };
+
+
+  generated = runCommand "cl-unicode-generated" { } ''
+    mkdir -p $out/build
+    mkdir -p $out/test
+    cd $out/build
+    pwd
+    ${cl-unicode-build}/bin/cl-unicode-build
+  '';
+
+in
+depot.nix.buildLisp.library {
+  name = "cl-unicode";
+  deps = [ cl-unicode-base ];
+  srcs = [
+    "${src}/conditions.lisp"
+    "${generated}/lists.lisp"
+    "${generated}/hash-tables.lisp"
+    "${src}/api.lisp"
+    "${generated}/methods.lisp"
+    "${src}/test-functions.lisp"
+    "${src}/derived.lisp"
+    "${src}/alias.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-who.nix b/third_party/lisp/cl-who.nix
new file mode 100644
index 0000000000..601b09f118
--- /dev/null
+++ b/third_party/lisp/cl-who.nix
@@ -0,0 +1,13 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-who;
+in depot.nix.buildLisp.library {
+  name = "cl-who";
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+    "specials.lisp"
+    "util.lisp"
+    "who.lisp"
+  ];
+}
diff --git a/third_party/lisp/cl-yacc.nix b/third_party/lisp/cl-yacc.nix
new file mode 100644
index 0000000000..b40d5d0601
--- /dev/null
+++ b/third_party/lisp/cl-yacc.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.fetchFromGitHub {
+    owner = "jech";
+    repo = "cl-yacc";
+    rev = "1334f5469251ffb3f8738a682dc8ee646cb26635";
+    sha256 = "16946pzf8vvadnyfayvj8rbh4zjzw90h0azz2qk1mxrvhh5wklib";
+  };
+in
+depot.nix.buildLisp.library {
+  name = "cl-yacc";
+
+  srcs = map (f: src + ("/" + f)) [
+    "yacc.lisp"
+  ];
+}
diff --git a/third_party/lisp/closer-mop.nix b/third_party/lisp/closer-mop.nix
new file mode 100644
index 0000000000..145b9cfd43
--- /dev/null
+++ b/third_party/lisp/closer-mop.nix
@@ -0,0 +1,19 @@
+# Closer to MOP is a compatibility layer that rectifies many of the
+# absent or incorrect CLOS MOP features across a broad range of Common
+# Lisp implementations
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.closer-mop;
+in depot.nix.buildLisp.library {
+  name = "closer-mop";
+
+  srcs = [
+    "${src}/closer-mop-packages.lisp"
+    "${src}/closer-mop-shared.lisp"
+    {
+      sbcl = "${src}/closer-sbcl.lisp";
+      ecl = "${src}/closer-ecl.lisp";
+      ccl = "${src}/closer-clozure.lisp";
+    }
+  ];
+}
diff --git a/third_party/lisp/closure-common.nix b/third_party/lisp/closure-common.nix
new file mode 100644
index 0000000000..7f7f79f855
--- /dev/null
+++ b/third_party/lisp/closure-common.nix
@@ -0,0 +1,36 @@
+{ depot, pkgs, ... }:
+
+let
+  src = with pkgs; srcOnly lispPackages.closure-common;
+  getSrcs = builtins.map (p: "${src}/${p}");
+in
+depot.nix.buildLisp.library {
+  name = "closure-common";
+
+  # closure-common.asd surpresses some warnings otherwise breaking
+  # compilation. Feature macros across implementations:
+  #
+  # ECL  #+rune-is-character #-rune-is-integer #-x&y-streams-are-stream
+  # CCL  #+rune-is-character #-rune-is-integer #-x&y-streams-are-stream
+  # SBCL #+rune-is-character #-rune-is-integer #-x&y-streams-are-stream
+  #
+  # Since all implementations agree, the alternative files aren't encoded here.
+  srcs = getSrcs [
+    "closure-common.asd"
+    "package.lisp"
+    "definline.lisp"
+    "characters.lisp" #+rune-is-character
+    "syntax.lisp"
+    "encodings.lisp" #-x&y-streams-are-stream
+    "encodings-data.lisp" #-x&y-streams-are-stream
+    "xstream.lisp" #-x&y-streams-are-stream
+    "ystream.lisp" #-x&y-streams-are-stream
+    "hax.lisp"
+  ];
+
+  deps = [
+    (depot.nix.buildLisp.bundled "asdf")
+    depot.third_party.lisp.trivial-gray-streams
+    depot.third_party.lisp.babel #+rune-is-character
+  ];
+}
diff --git a/third_party/lisp/closure-html/default.nix b/third_party/lisp/closure-html/default.nix
new file mode 100644
index 0000000000..1886ea2ec9
--- /dev/null
+++ b/third_party/lisp/closure-html/default.nix
@@ -0,0 +1,65 @@
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.applyPatches {
+    name = "closure-html-source";
+    src = pkgs.lispPackages.closure-html.src;
+
+    patches = [
+      # delete unexported and unused double defun in sgml-dtd.lisp
+      # which reference undefined CL-USER:*HTML-DTD* (!) which
+      # unlike CLOSURE-HTML:*HTML-DTD* is not involved in the
+      # packages operation.
+      ./no-double-defun.patch
+      # Patches html-parser.lisp to look for the distributed
+      # dtd files and catalog in this source derivations out
+      # path in the nix store instead of the same directory
+      # relatively to the (built) system.
+      ./dtds-from-store.patch
+    ];
+
+    postPatch = ''
+      # Inject file which defines CLOSURE-HTML:*HTML-DTD*
+      # early in the package's build since SBCL otherwise
+      # fails due to the undefined variable. Need to inject
+      # this via postPatch since using a nix file results
+      # in failure to look up the file's true name which
+      # is done for … reasons, apparently.
+      cat > src/define-html-dtd.lisp << EOF
+      (in-package :closure-html)
+      (defvar *html-dtd*)
+      EOF
+
+      # Substitute reference to @out@ of this source
+      # directory in this patched file.
+      substituteAllInPlace src/parse/html-parser.lisp
+    '';
+  };
+
+  getSrcs = builtins.map (p: "${src}/${p}");
+in
+
+depot.nix.buildLisp.library {
+  name = "closure-html";
+
+  srcs = getSrcs [
+    "src/defpack.lisp"
+    "src/define-html-dtd.lisp"
+    "src/glisp/util.lisp"
+    "src/util/clex.lisp"
+    "src/util/lalr.lisp"
+    "src/net/mime.lisp"
+    "src/parse/pt.lisp"
+    "src/parse/sgml-dtd.lisp"
+    "src/parse/sgml-parse.lisp"
+    "src/parse/html-parser.lisp"
+    "src/parse/lhtml.lisp"
+    "src/parse/unparse.lisp"
+    "src/parse/documentation.lisp"
+  ];
+
+  deps = [
+    depot.third_party.lisp.flexi-streams
+    depot.third_party.lisp.closure-common
+  ];
+}
diff --git a/third_party/lisp/closure-html/dtds-from-store.patch b/third_party/lisp/closure-html/dtds-from-store.patch
new file mode 100644
index 0000000000..a9ffd8085e
--- /dev/null
+++ b/third_party/lisp/closure-html/dtds-from-store.patch
@@ -0,0 +1,16 @@
+diff --git a/src/parse/html-parser.lisp b/src/parse/html-parser.lisp
+index 4e45b81..5025a26 100644
+--- a/src/parse/html-parser.lisp
++++ b/src/parse/html-parser.lisp
+@@ -36,10 +36,7 @@
+         (make-pathname
+ 	 :name nil
+ 	 :type nil
+-	 :defaults (merge-pathnames
+-		    "resources/"
+-		    (asdf:component-relative-pathname
+-		     (asdf:find-system :closure-html))))))
++	 :defaults "@out@/resources/")))
+     (loop
+        :for (name . filename)
+        :in '(("-//W3O//DTD W3 HTML 3.0//EN" . "dtd/HTML-3.0")
diff --git a/third_party/lisp/closure-html/no-double-defun.patch b/third_party/lisp/closure-html/no-double-defun.patch
new file mode 100644
index 0000000000..ce7fb33abf
--- /dev/null
+++ b/third_party/lisp/closure-html/no-double-defun.patch
@@ -0,0 +1,78 @@
+diff --git a/src/parse/sgml-dtd.lisp b/src/parse/sgml-dtd.lisp
+index de774c0..dbee852 100644
+--- a/src/parse/sgml-dtd.lisp
++++ b/src/parse/sgml-dtd.lisp
+@@ -624,73 +624,6 @@
+           (return))))
+     classes))
+ 
+-;;;; ----------------------------------------------------------------------------------------------------
+-;;;;  Compiled DTDs
+-;;;;
+-
+-;; Since parsing and 'compiling' DTDs is slow, I'll provide for a way
+-;; to (un)dump compiled DTD to stream.
+-
+-(defun dump-dtd (dtd sink)
+-  (let ((*print-pretty* nil)
+-        (*print-readably* t)
+-        (*print-circle* t))
+-    (princ "#." sink)
+-    (prin1
+-     `(MAKE-DTD :NAME ',(dtd-name dtd)
+-                :ELEMENTS (LET ((R (MAKE-HASH-TABLE :TEST #'EQ)))
+-                               (SETF ,@(let ((q nil))
+-                                         (maphash (lambda (key value)
+-                                                    (push `',value q)
+-                                                    (push `(GETHASH ',key R) q))
+-                                                  (dtd-elements dtd))
+-                                         q))
+-                               R)
+-                :ENTITIES ',(dtd-entities dtd)
+-                :RESOLVE-INFO (LET ((R (MAKE-HASH-TABLE :TEST #'EQUAL))) 
+-                                   (SETF ,@(let ((q nil))
+-                                             (maphash (lambda (key value)
+-                                                        (push `',value q)
+-                                                        (push `(GETHASH ',key R) q))
+-                                                      (dtd-resolve-info dtd))
+-                                             q))
+-                                   R)
+-                ;; XXX surclusion-cache fehlt
+-                )
+-     sink)))
+-
+-;;XXX
+-(defun save-html-dtd ()
+-  (with-open-file (sink "html-dtd.lisp" :direction :output :if-exists :new-version)
+-    (print `(in-package :sgml) sink)
+-    (let ((*package* (find-package :sgml)))
+-      (princ "(SETQ " sink)
+-      (prin1 'cl-user::*html-dtd* sink)
+-      (princ " '" sink)
+-      (dump-dtd cl-user::*html-dtd* sink)
+-      (princ ")" sink))))
+-
+-;;; --------------------------------------------------------------------------------
+-;;;  dumping DTDs
+-
+-
+-(defun dump-dtd (dtd filename)
+-  (let ((*foo* dtd))
+-    (declare (special *foo*))
+-    (with-open-file (sink (merge-pathnames filename "*.lisp")
+-                     :direction :output
+-                     :if-exists :new-version)
+-      (format sink "(in-package :sgml)(locally (declare (special *foo*))(setq *foo* '#.*foo*))"))
+-    (compile-file (merge-pathnames filename "*.lisp"))))
+-
+-(defun undump-dtd (filename)
+-  (let (*foo*)
+-    (declare (special *foo*))
+-    (load (compile-file-pathname (merge-pathnames filename "*.lisp"))
+-          :verbose nil
+-          :print nil)
+-    *foo*))
+-
+ (defmethod make-load-form ((self dtd) &optional env)
+   (declare (ignore env))
+   `(make-dtd :name                  ',(dtd-name self)
diff --git a/third_party/lisp/defclass-std.nix b/third_party/lisp/defclass-std.nix
new file mode 100644
index 0000000000..c31ddb3c5b
--- /dev/null
+++ b/third_party/lisp/defclass-std.nix
@@ -0,0 +1,16 @@
+# A shortcut macro to write DEFCLASS forms quickly
+# Seems to be unmaintained (since early 2021)
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.defclass-std;
+in depot.nix.buildLisp.library {
+  name = "defclass-std";
+  deps = with depot.third_party.lisp; [
+    alexandria
+    anaphora
+  ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "defclass-std.lisp"
+  ];
+}
diff --git a/third_party/lisp/drakma.nix b/third_party/lisp/drakma.nix
new file mode 100644
index 0000000000..607f438d7e
--- /dev/null
+++ b/third_party/lisp/drakma.nix
@@ -0,0 +1,34 @@
+# Drakma is an HTTP client for Common Lisp.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.drakma;
+in depot.nix.buildLisp.library {
+  name = "drakma";
+  deps = with depot.third_party.lisp; [
+    chipz
+    chunga
+    cl-base64
+    cl-plus-ssl
+    cl-ppcre
+    flexi-streams
+    puri
+    usocket
+    (depot.nix.buildLisp.bundled "asdf")
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "drakma.asd" # Required because the system definition is used
+    "packages.lisp"
+    "specials.lisp"
+    "conditions.lisp"
+    "util.lisp"
+    "read.lisp"
+    "cookies.lisp"
+    "encoding.lisp"
+    "request.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/easy-routes.nix b/third_party/lisp/easy-routes.nix
new file mode 100644
index 0000000000..5caf8261fa
--- /dev/null
+++ b/third_party/lisp/easy-routes.nix
@@ -0,0 +1,30 @@
+{ depot, pkgs, ... }:
+
+let
+
+  src = pkgs.fetchFromGitHub {
+    owner = "mmontone";
+    repo = "easy-routes";
+    rev = "dab613ff419a655036a00beecee026ab6e0ba430";
+    sha256 = "06lnipwc6mmg0v5gybcnr7wn5xmn5xfd1gs19vbima777245bfka";
+  };
+
+in
+depot.nix.buildLisp.library {
+  name = "easy-routes";
+  deps = with depot.third_party.lisp; [
+    hunchentoot
+    routes
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "util.lisp"
+    "easy-routes.lisp"
+    "routes-map-printer.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/fiveam.nix b/third_party/lisp/fiveam.nix
new file mode 100644
index 0000000000..500e980a81
--- /dev/null
+++ b/third_party/lisp/fiveam.nix
@@ -0,0 +1,29 @@
+# FiveAM is a Common Lisp testing framework.
+#
+# Imported from https://github.com/sionescu/fiveam.git
+
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.fiveam;
+in depot.nix.buildLisp.library {
+  name = "fiveam";
+
+  deps = with depot.third_party.lisp; [
+    alexandria
+    asdf-flv
+    trivial-backtrace
+  ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "package.lisp"
+    "utils.lisp"
+    "check.lisp"
+    "fixture.lisp"
+    "classes.lisp"
+    "random.lisp"
+    "test.lisp"
+    "explain.lisp"
+    "suite.lisp"
+    "run.lisp"
+  ];
+}
diff --git a/third_party/lisp/flexi-streams.nix b/third_party/lisp/flexi-streams.nix
new file mode 100644
index 0000000000..a6a06d4ad0
--- /dev/null
+++ b/third_party/lisp/flexi-streams.nix
@@ -0,0 +1,33 @@
+# Flexible bivalent streams for Common Lisp
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.flexi-streams;
+in depot.nix.buildLisp.library {
+  name = "flexi-streams";
+  deps = [ depot.third_party.lisp.trivial-gray-streams ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+    "mapping.lisp"
+    "ascii.lisp"
+    "koi8-r.lisp"
+    "mac.lisp"
+    "iso-8859.lisp"
+    "enc-cn-tbl.lisp"
+    "code-pages.lisp"
+    "specials.lisp"
+    "util.lisp"
+    "conditions.lisp"
+    "external-format.lisp"
+    "length.lisp"
+    "encode.lisp"
+    "decode.lisp"
+    "in-memory.lisp"
+    "stream.lisp"
+    "output.lisp"
+    "input.lisp"
+    "io.lisp"
+    "strings.lisp"
+  ];
+}
+
diff --git a/third_party/lisp/global-vars.nix b/third_party/lisp/global-vars.nix
new file mode 100644
index 0000000000..a3d27a09b6
--- /dev/null
+++ b/third_party/lisp/global-vars.nix
@@ -0,0 +1,7 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.global-vars;
+in depot.nix.buildLisp.library {
+  name = "global-vars";
+  srcs = [ "${src}/global-vars.lisp" ];
+}
diff --git a/third_party/lisp/hunchentoot.nix b/third_party/lisp/hunchentoot.nix
new file mode 100644
index 0000000000..e2480cd349
--- /dev/null
+++ b/third_party/lisp/hunchentoot.nix
@@ -0,0 +1,62 @@
+# Hunchentoot is a web framework for Common Lisp.
+{ depot, pkgs, ... }:
+
+let
+  src = with pkgs; srcOnly lispPackages.hunchentoot;
+
+  url-rewrite = depot.nix.buildLisp.library {
+    name = "url-rewrite";
+
+    srcs = map (f: src + ("/url-rewrite/" + f)) [
+      "packages.lisp"
+      "specials.lisp"
+      "primitives.lisp"
+      "util.lisp"
+      "url-rewrite.lisp"
+    ];
+  };
+in
+depot.nix.buildLisp.library {
+  name = "hunchentoot";
+
+  deps = with depot.third_party.lisp; [
+    alexandria
+    bordeaux-threads
+    chunga
+    cl-base64
+    cl-fad
+    rfc2388
+    cl-plus-ssl
+    cl-ppcre
+    flexi-streams
+    md5
+    trivial-backtrace
+    usocket
+    url-rewrite
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "hunchentoot.asd"
+    "packages.lisp"
+    "compat.lisp"
+    "specials.lisp"
+    "conditions.lisp"
+    "mime-types.lisp"
+    "util.lisp"
+    "log.lisp"
+    "cookie.lisp"
+    "reply.lisp"
+    "request.lisp"
+    "session.lisp"
+    "misc.lisp"
+    "headers.lisp"
+    "set-timeouts.lisp"
+    "taskmaster.lisp"
+    "acceptor.lisp"
+    "easy-handlers.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/ironclad.nix b/third_party/lisp/ironclad.nix
new file mode 100644
index 0000000000..324c5da265
--- /dev/null
+++ b/third_party/lisp/ironclad.nix
@@ -0,0 +1,163 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs) runCommand;
+  inherit (depot.nix.buildLisp) bundled;
+  src = with pkgs; srcOnly lispPackages.ironclad;
+  getSrc = f: "${src}/src/${f}";
+
+in
+depot.nix.buildLisp.library {
+  name = "ironclad";
+
+  deps = with depot.third_party.lisp; [
+    (bundled "asdf")
+    { sbcl = bundled "sb-rotate-byte"; }
+    { sbcl = bundled "sb-posix"; }
+    alexandria
+    bordeaux-threads
+    nibbles
+  ];
+
+  srcs = map getSrc [
+    # {
+    #   # TODO(grfn): Figure out how to get this compiling with the assembly
+    #   # optimization eventually - see https://cl.tvl.fyi/c/depot/+/1333
+    #   sbcl = runCommand "package.lisp" {} ''
+    #     substitute ${src}/src/package.lisp $out \
+    #       --replace \#-ecl-bytecmp "" \
+    #       --replace '(pushnew :ironclad-assembly *features*)' ""
+    #   '';
+    #   default = getSrc "package.lisp";
+    # }
+    "package.lisp"
+    "conditions.lisp"
+    "generic.lisp"
+    "macro-utils.lisp"
+    "util.lisp"
+  ] ++ [
+    { sbcl = getSrc "opt/sbcl/fndb.lisp"; }
+    { sbcl = getSrc "opt/sbcl/cpu-features.lisp"; }
+    { sbcl = getSrc "opt/sbcl/x86oid-vm.lisp"; }
+
+    { ecl = getSrc "opt/ecl/c-functions.lisp"; }
+
+    { ccl = getSrc "opt/ccl/x86oid-vm.lisp"; }
+  ] ++ map getSrc [
+    "common.lisp"
+
+    "ciphers/cipher.lisp"
+    "ciphers/padding.lisp"
+    "ciphers/make-cipher.lisp"
+    "ciphers/modes.lisp"
+
+    # subsystem def ironclad/ciphers
+    "ciphers/aes.lisp"
+    "ciphers/arcfour.lisp"
+    "ciphers/aria.lisp"
+    "ciphers/blowfish.lisp"
+    "ciphers/camellia.lisp"
+    "ciphers/cast5.lisp"
+    "ciphers/chacha.lisp"
+    "ciphers/des.lisp"
+    "ciphers/idea.lisp"
+    "ciphers/kalyna.lisp"
+    "ciphers/kuznyechik.lisp"
+    "ciphers/misty1.lisp"
+    "ciphers/rc2.lisp"
+    "ciphers/rc5.lisp"
+    "ciphers/rc6.lisp"
+    "ciphers/salsa20.lisp"
+    "ciphers/keystream.lisp"
+    "ciphers/seed.lisp"
+    "ciphers/serpent.lisp"
+    "ciphers/sm4.lisp"
+    "ciphers/sosemanuk.lisp"
+    "ciphers/square.lisp"
+    "ciphers/tea.lisp"
+    "ciphers/threefish.lisp"
+    "ciphers/twofish.lisp"
+    "ciphers/xchacha.lisp"
+    "ciphers/xor.lisp"
+    "ciphers/xsalsa20.lisp"
+    "ciphers/xtea.lisp"
+
+    "digests/digest.lisp"
+    # subsystem def ironclad/digests
+    "digests/adler32.lisp"
+    "digests/blake2.lisp"
+    "digests/blake2s.lisp"
+    "digests/crc24.lisp"
+    "digests/crc32.lisp"
+    "digests/groestl.lisp"
+    "digests/jh.lisp"
+    "digests/kupyna.lisp"
+    "digests/md2.lisp"
+    "digests/md4.lisp"
+    "digests/md5.lisp"
+    "digests/md5-lispworks-int32.lisp"
+    "digests/ripemd-128.lisp"
+    "digests/ripemd-160.lisp"
+    "digests/sha1.lisp"
+    "digests/sha256.lisp"
+    "digests/sha3.lisp"
+    "digests/sha512.lisp"
+    "digests/skein.lisp"
+    "digests/sm3.lisp"
+    "digests/streebog.lisp"
+    "digests/tiger.lisp"
+    "digests/tree-hash.lisp"
+    "digests/whirlpool.lisp"
+
+    "macs/mac.lisp"
+    # subsystem def ironclad/macs
+    "macs/blake2-mac.lisp"
+    "macs/blake2s-mac.lisp"
+    "macs/cmac.lisp"
+    "macs/hmac.lisp"
+    "macs/gmac.lisp"
+    "macs/poly1305.lisp"
+    "macs/siphash.lisp"
+    "macs/skein-mac.lisp"
+
+    "prng/prng.lisp"
+    "prng/os-prng.lisp"
+    "prng/generator.lisp"
+    "prng/fortuna.lisp"
+
+    "math.lisp"
+
+    "octet-stream.lisp"
+
+    "aead/aead.lisp"
+    # subsystem def ironclad/aead
+    "aead/eax.lisp"
+    "aead/etm.lisp"
+    "aead/gcm.lisp"
+
+    "kdf/kdf.lisp"
+    # subsystem def ironclad/kdfs
+    "kdf/argon2.lisp"
+    "kdf/bcrypt.lisp"
+    "kdf/hmac.lisp"
+    "kdf/pkcs5.lisp"
+    "kdf/password-hash.lisp"
+    "kdf/scrypt.lisp"
+
+    "public-key/public-key.lisp"
+    "public-key/pkcs1.lisp"
+    "public-key/elliptic-curve.lisp"
+    # subsystem def ironclad/public-keys
+    "public-key/dsa.lisp"
+    "public-key/rsa.lisp"
+    "public-key/elgamal.lisp"
+    "public-key/curve25519.lisp"
+    "public-key/curve448.lisp"
+    "public-key/ed25519.lisp"
+    "public-key/ed448.lisp"
+    "public-key/secp256k1.lisp"
+    "public-key/secp256r1.lisp"
+    "public-key/secp384r1.lisp"
+    "public-key/secp521r1.lisp"
+  ];
+}
diff --git a/third_party/lisp/iterate.nix b/third_party/lisp/iterate.nix
new file mode 100644
index 0000000000..b7d60265ac
--- /dev/null
+++ b/third_party/lisp/iterate.nix
@@ -0,0 +1,12 @@
+# iterate is an iteration construct for Common Lisp, similar to the
+# LOOP macro.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.iterate;
+in depot.nix.buildLisp.library {
+  name = "iterate";
+  srcs = [
+    "${src}/package.lisp"
+    "${src}/iterate.lisp"
+  ];
+}
diff --git a/third_party/lisp/lass.nix b/third_party/lisp/lass.nix
new file mode 100644
index 0000000000..00f66c1fe3
--- /dev/null
+++ b/third_party/lisp/lass.nix
@@ -0,0 +1,35 @@
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.fetchFromGitHub {
+    owner = "Shinmera";
+    repo = "LASS";
+    rev = "f51b9e941ee0a2a1f76ba814dcef22f9fb5f69bf";
+    sha256 = "11mxzyx34ynsfsrs8pgrarqi9s442vkpmh7kdpzvarhj7i97g8yx";
+  };
+
+in
+depot.nix.buildLisp.library {
+  name = "lass";
+
+  deps = with depot.third_party.lisp; [
+    trivial-indent
+    trivial-mimes
+    physical-quantities
+    parse-float
+    cl-base64
+    (depot.nix.buildLisp.bundled "asdf")
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "readable-list.lisp"
+    "compiler.lisp"
+    "property-funcs.lisp"
+    "writer.lisp"
+    "lass.lisp"
+    "special.lisp"
+    "units.lisp"
+    "asdf.lisp"
+  ];
+}
diff --git a/third_party/lisp/let-plus.nix b/third_party/lisp/let-plus.nix
new file mode 100644
index 0000000000..bd7f31dfa0
--- /dev/null
+++ b/third_party/lisp/let-plus.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.let-plus;
+in depot.nix.buildLisp.library {
+  name = "let-plus";
+  deps = [
+    depot.third_party.lisp.alexandria
+    depot.third_party.lisp.anaphora
+  ];
+  srcs = [
+    "${src}/package.lisp"
+    "${src}/let-plus.lisp"
+    "${src}/extensions.lisp"
+  ];
+}
diff --git a/third_party/lisp/lisp-binary.nix b/third_party/lisp/lisp-binary.nix
new file mode 100644
index 0000000000..8deba4546f
--- /dev/null
+++ b/third_party/lisp/lisp-binary.nix
@@ -0,0 +1,37 @@
+# A library to easily read and write complex binary formats.
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.fetchFromGitHub {
+    owner = "j3pic";
+    repo = "lisp-binary";
+    rev = "052df578900dea59bf951e0a6749281fa73432e4";
+    sha256 = "1i1s5g01aimfq6lndcl1pnw7ly5hdh0wmjp2dj9cjjwbkz9lnwcf";
+  };
+in
+depot.nix.buildLisp.library {
+  name = "lisp-binary";
+
+  deps = with depot.third_party.lisp; [
+    cffi
+    quasiquote_2
+    moptilities
+    flexi-streams
+    closer-mop
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "utils.lisp"
+    "integer.lisp"
+    "float.lisp"
+    "simple-bit-stream.lisp"
+    "reverse-stream.lisp"
+    "binary-1.lisp"
+    "binary-2.lisp"
+    "types.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/local-time.nix b/third_party/lisp/local-time.nix
new file mode 100644
index 0000000000..1358408d38
--- /dev/null
+++ b/third_party/lisp/local-time.nix
@@ -0,0 +1,22 @@
+# Library for manipulating dates & times
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+  src = with pkgs; srcOnly lispPackages.local-time;
+in
+buildLisp.library {
+  name = "local-time";
+  deps = [
+    depot.third_party.lisp.cl-fad
+    {
+      scbl = buildLisp.bundled "uiop";
+      default = buildLisp.bundled "asdf";
+    }
+  ];
+
+  srcs = [
+    "${src}/src/package.lisp"
+    "${src}/src/local-time.lisp"
+  ];
+}
diff --git a/third_party/lisp/marshal.nix b/third_party/lisp/marshal.nix
new file mode 100644
index 0000000000..73a1664a01
--- /dev/null
+++ b/third_party/lisp/marshal.nix
@@ -0,0 +1,13 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.marshal;
+in depot.nix.buildLisp.library {
+  name = "marshal";
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "serialization-format.lisp"
+    "coding-idiom.lisp"
+    "marshal.lisp"
+    "unmarshal.lisp"
+  ];
+}
diff --git a/third_party/lisp/md5.nix b/third_party/lisp/md5.nix
new file mode 100644
index 0000000000..8c3e255f16
--- /dev/null
+++ b/third_party/lisp/md5.nix
@@ -0,0 +1,16 @@
+# MD5 hash implementation
+{ depot, pkgs, ... }:
+
+with depot.nix;
+
+let src = with pkgs; srcOnly lispPackages.md5;
+in buildLisp.library {
+  name = "md5";
+  deps = [
+    {
+      sbcl = buildLisp.bundled "sb-rotate-byte";
+      default = depot.third_party.lisp.flexi-streams;
+    }
+  ];
+  srcs = [ (src + "/md5.lisp") ];
+}
diff --git a/third_party/lisp/metabang-bind.nix b/third_party/lisp/metabang-bind.nix
new file mode 100644
index 0000000000..fc046d0895
--- /dev/null
+++ b/third_party/lisp/metabang-bind.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, ... }:
+
+let
+  getSrcs = builtins.map (p: "${pkgs.srcOnly pkgs.lispPackages.metabang-bind}/${p}");
+in
+
+depot.nix.buildLisp.library {
+  name = "metabang-bind";
+
+  srcs = getSrcs [
+    "dev/packages.lisp"
+    "dev/macros.lisp"
+    "dev/bind.lisp"
+    "dev/binding-forms.lisp"
+  ];
+}
diff --git a/third_party/lisp/mime4cl/.skip-subtree b/third_party/lisp/mime4cl/.skip-subtree
new file mode 100644
index 0000000000..5051f60d6b
--- /dev/null
+++ b/third_party/lisp/mime4cl/.skip-subtree
@@ -0,0 +1 @@
+prevent readTree from creating entries for subdirs that don't contain an .nix files
diff --git a/third_party/lisp/mime4cl/OWNERS b/third_party/lisp/mime4cl/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/third_party/lisp/mime4cl/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/third_party/lisp/mime4cl/README b/third_party/lisp/mime4cl/README
new file mode 100644
index 0000000000..73f0efbda9
--- /dev/null
+++ b/third_party/lisp/mime4cl/README
@@ -0,0 +1,7 @@
+MIME4CL is a Common Lisp library for dealing with MIME messages.
+It has originally been written by Walter C. Pelissero and vendored
+into depot as upstream has become inactive and provides no repo
+of any kind. Upstream and depot version may diverge.
+
+Upstream Website: http://wcp.sdf-eu.org/software/#mime4cl
+Vendored Tarball: http://wcp.sdf-eu.org/software/mime4cl-20150207T211851.tbz
diff --git a/third_party/lisp/mime4cl/address.lisp b/third_party/lisp/mime4cl/address.lisp
new file mode 100644
index 0000000000..944156916c
--- /dev/null
+++ b/third_party/lisp/mime4cl/address.lisp
@@ -0,0 +1,300 @@
+;;;  address.lisp --- e-mail address parser
+
+;;;  Copyright (C) 2007, 2008, 2009 by Walter C. Pelissero
+;;;  Copyright (C) 2022 The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+;;;  Although not MIME specific, this parser is often useful together
+;;;  with the MIME primitives.  It should be able to parse the address
+;;;  syntax described in RFC2822 excluding the obsolete syntax (see
+;;;  RFC822).  Have a look at the test suite to get an idea of what
+;;;  kind of addresses it can parse.
+
+(in-package :mime4cl)
+
+(defstruct (mailbox (:conc-name mbx-))
+  description
+  user
+  host
+  domain)
+
+(defstruct (mailbox-group (:conc-name mbxg-))
+  name
+  mailboxes)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun write-mailbox-domain-name (addr &optional (stream *standard-output*))
+  (when (eq :internet (mbx-domain addr))
+    (write-char #\[ stream))
+  (write-string (mbx-host addr) stream)
+  (when (eq :internet (mbx-domain addr))
+    (write-char #\] stream))
+  (when (stringp (mbx-domain addr))
+    (write-char #\. stream)
+    (write-string (mbx-domain addr) stream)))
+
+(defun write-mailbox-address (addr &optional (stream *standard-output*))
+  (write-string (mbx-user addr) stream)
+  (when (mbx-host addr)
+    (write-char #\@ stream)
+    (write-mailbox-domain-name addr stream)))
+
+(defmethod mbx-domain-name ((MBX mailbox))
+  "Return the complete domain name string of MBX, in the form
+\"host.domain\"."
+  (with-output-to-string (out)
+    (write-mailbox-domain-name mbx out)))
+
+(defmethod mbx-address ((mbx mailbox))
+  "Return the e-mail address string of MBX, in the form
+\"user@host.domain\"."
+  (with-output-to-string (out)
+    (write-mailbox-address mbx out)))
+
+(defun write-mailbox (addr &optional (stream *standard-output*))
+  (awhen (mbx-description addr)
+    (write it :stream stream :readably t)
+    (write-string " <" stream))
+  (write-mailbox-address addr stream)
+  (awhen (mbx-description addr)
+    (write-char #\> stream)))
+
+(defun write-mailbox-group (grp &optional (stream *standard-output*))
+  (write-string (mbxg-name grp) stream)
+  (write-string ": " stream)
+  (loop
+     for mailboxes on (mbxg-mailboxes grp)
+     for mailbox = (car mailboxes)
+     do (write-mailbox mailbox stream)
+     unless (endp (cdr mailboxes))
+     do (write-string ", " stream))
+  (write-char #\; stream))
+
+(defmethod print-object ((mbx mailbox) stream)
+  (if (or *print-readably* *print-escape*)
+      (call-next-method)
+      (write-mailbox mbx stream)))
+
+(defmethod print-object ((grp mailbox-group) stream)
+  (if (or *print-readably* *print-escape*)
+      (call-next-method)
+      (write-mailbox-group grp stream)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun parser-make-mailbox (description address-list)
+  (make-mailbox :description description
+                :user (car address-list)
+                :host (cadr address-list)
+                :domain (when (cddr address-list)
+                          (string-concat (cddr address-list) "."))))
+
+
+(defun populate-grammar ()
+  (defrule address-list
+      := (+ address ","))
+
+  (defrule address
+      := mailbox
+      := group)
+
+  (defrule mailbox
+      := display-name? angle-addr comment?
+      :reduce (parser-make-mailbox (or display-name comment) angle-addr)
+      := addr-spec comment?
+      :reduce (parser-make-mailbox comment addr-spec))
+
+  (defrule angle-addr
+      := "<" addr-spec ">")
+
+  (defrule group
+      := display-name ":" mailbox-list ";"
+      :reduce (make-mailbox-group :name display-name :mailboxes mailbox-list))
+
+  (defrule display-name
+      := phrase
+      :reduce (string-concat phrase " "))
+
+  (defrule phrase
+      := word+)
+
+  (defrule word
+      := atext
+      := string)
+
+  (defrule mailbox-list
+      := (+ mailbox ","))
+
+  (defrule addr-spec
+      := local-part "@" domain :reduce (cons local-part domain))
+
+  (defrule local-part
+      := dot-atom :reduce (string-concat dot-atom ".")
+      := string)
+
+  (defrule domain
+      := dot-atom
+      := domain-literal :reduce (list domain-literal :internet))
+
+  ;; actually, according to the RFC, dot-atoms don't allow spaces in
+  ;; between but these rules do
+  (defrule dot-atom
+      := (+ atom "."))
+
+  (defrule atom
+      := atext+
+      :reduce (apply #'concatenate 'string atext)))
+
+(deflazy define-grammar
+  (let ((*package* #.*package*)
+        (*compile-print* (when npg::*debug* t)))
+    (reset-grammar)
+    (format t "~&creating e-mail address grammar...~%")
+    (populate-grammar)
+    (let ((grammar (npg:generate-grammar #'string=)))
+      (reset-grammar)
+      (npg:print-grammar-figures grammar)
+      grammar)))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; The lexical analyser
+
+(defstruct cursor
+  stream
+  (position 0))
+
+(defun read-delimited-string (stream end-char &key nesting-start-char (escape-char #\\))
+  (labels ((collect ()
+             (with-output-to-string (out)
+               (loop
+                  for c = (read-char stream nil)
+                  while (and c (not (char= c end-char)))
+                  do (cond ((char= c escape-char)
+                            (awhen (read-char stream nil)
+                              (write-char it out)))
+                           ((and nesting-start-char
+                                 (char= c nesting-start-char))
+                            (write-char nesting-start-char out)
+                            (write-string (collect) out)
+                            (write-char end-char out))
+                           (t (write-char c out)))))))
+    (collect)))
+
+
+(defun read-string (cursor)
+  (make-token :type 'string
+              :value (read-delimited-string (cursor-stream cursor) #\")
+              :position (incf (cursor-position cursor))))
+
+(defun read-domain-literal (cursor)
+  (make-token :type 'domain-literal
+              :value (read-delimited-string (cursor-stream cursor) #\])
+              :position (incf (cursor-position cursor))))
+
+(defun read-comment (cursor)
+  (make-token :type 'comment
+              :value (read-delimited-string (cursor-stream cursor) #\) :nesting-start-char #\()
+              :position (incf (cursor-position cursor))))
+
+(declaim (inline atom-component-p))
+(defun atom-component-p (c)
+  (declare (type character c))
+  (not (find c " ()\"[]@.<>:;,")))
+
+(defun read-atext (first-character cursor)
+  (be string (with-output-to-string (out)
+               (write-char first-character out)
+               (loop
+                  for c = (read-char (cursor-stream cursor) nil)
+                  while (and c (atom-component-p c))
+                  do (write-char c out)
+                  finally (when c
+                            (unread-char c (cursor-stream cursor)))))
+    (make-token :type 'atext
+                :value string
+                :position (incf (cursor-position cursor)))))
+
+(defmethod read-next-tokens ((cursor cursor))
+  (flet ((make-keyword (c)
+           (make-token :type 'keyword
+                       :value (string c)
+                       :position (incf (cursor-position cursor)))))
+    (be in (cursor-stream cursor)
+      (loop
+         for c = (read-char in nil)
+         while c
+         unless (whitespace-p c)
+         return (list
+                 (cond ((char= #\( c)
+                        (read-comment cursor))
+                       ((char= #\" c)
+                        (read-string cursor))
+                       ((char= #\[ c)
+                        (read-domain-literal cursor))
+                       ((find c "@.<>:;,")
+                        (make-keyword c))
+                       (t
+                        ;; anything else is considered a text atom even
+                        ;; though it's just a single character
+                        (read-atext c cursor))))))))
+
+(defun analyse-string (string)
+  "Return the list of tokens produced by a lexical analysis of
+STRING.  These are the tokens that would be seen by the parser."
+  (with-input-from-string (stream string)
+    (be cursor (make-cursor :stream stream)
+      (loop
+         for tokens = (read-next-tokens cursor)
+         until (endp tokens)
+         append tokens))))
+
+(defun mailboxes-only (list-of-mailboxes-and-groups)
+  "Return a flat list of MAILBOX-ADDRESSes from
+LIST-OF-MAILBOXES-AND-GROUPS, which is the kind of list returned
+by PARSE-ADDRESSES.  This turns out to be useful when your
+program is not interested in mailbox groups and expects the user
+addresses only."
+  (mapcan #'(lambda (mbx)
+              (if (typep mbx 'mailbox-group)
+                  (mbxg-mailboxes mbx)
+                  (list mbx)))
+          list-of-mailboxes-and-groups))
+
+(defun parse-addresses (string &key no-groups)
+  "Parse STRING and return a list of MAILBOX-ADDRESSes or
+MAILBOX-GROUPs.  If STRING is unparsable return NIL.  If
+NO-GROUPS is true, return a flat list of mailboxes throwing away
+the group containers, if any."
+  (be grammar (force define-grammar)
+    (with-input-from-string (stream string)
+      (be* cursor (make-cursor :stream stream)
+           mailboxes (ignore-errors	; ignore parsing errors
+                       (parse grammar 'address-list cursor))
+        (if no-groups
+            (mailboxes-only mailboxes)
+            mailboxes)))))
+
+(defun debug-addresses (string)
+  "More or less like PARSE-ADDRESSES, but don't ignore parsing errors."
+  (be grammar (force define-grammar)
+    (with-input-from-string (stream string)
+      (be cursor (make-cursor :stream stream)
+        (parse grammar 'address-list cursor)))))
+
diff --git a/third_party/lisp/mime4cl/default.nix b/third_party/lisp/mime4cl/default.nix
new file mode 100644
index 0000000000..9d3d6253f4
--- /dev/null
+++ b/third_party/lisp/mime4cl/default.nix
@@ -0,0 +1,50 @@
+# Copyright (C) 2021 by the TVL Authors
+# SPDX-License-Identifier: LGPL-2.1-or-later
+{ depot, pkgs, ... }:
+
+depot.nix.buildLisp.library {
+  name = "mime4cl";
+
+  deps = [
+    depot.third_party.lisp.babel
+    depot.third_party.lisp.sclf
+    depot.third_party.lisp.npg
+    depot.third_party.lisp.trivial-gray-streams
+  ];
+
+  srcs = [
+    ./package.lisp
+    ./endec.lisp
+    ./streams.lisp
+    ./mime.lisp
+    ./address.lisp
+  ];
+
+  tests = {
+    name = "mime4cl-tests";
+
+    srcs = [
+      ./test/rt.lisp
+      ./test/package.lisp
+      (pkgs.writeText "nix-samples.lisp" ''
+        (in-package :mime4cl-tests)
+
+        ;; missing from the tarball completely
+        (defvar *samples-directory* (pathname "/this/does/not/exist"))
+        ;; override auto discovery which doesn't work in store
+        (defvar *sample1-file* (pathname "${./test/sample1.msg}"))
+      '')
+      ./test/endec.lisp
+      ./test/address.lisp
+      ./test/mime.lisp
+    ];
+
+    expression = "(rtest:do-tests)";
+  };
+
+  # limited by sclf
+  brokenOn = [
+    "ccl"
+    "ecl"
+  ];
+}
diff --git a/third_party/lisp/mime4cl/endec.lisp b/third_party/lisp/mime4cl/endec.lisp
new file mode 100644
index 0000000000..020c212e5e
--- /dev/null
+++ b/third_party/lisp/mime4cl/endec.lisp
@@ -0,0 +1,697 @@
+;;;  endec.lisp --- encoder/decoder functions
+
+;;;  Copyright (C) 2005-2008, 2010 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+
+(in-package :mime4cl)
+
+
+;; Thank you SBCL for rendering constants totally useless!
+(defparameter +base64-encode-table+
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
+
+(defparameter +base64-decode-table+
+  (let ((da (make-array 256 :element-type '(unsigned-byte 8) :initial-element 65)))
+    (dotimes (i 64)
+      (setf (aref da (char-code (char +base64-encode-table+ i))) i))
+    da))
+
+(declaim (type (simple-array (unsigned-byte 8)) +base64-decode-table+)
+         (type simple-string +base64-encode-table+))
+
+(defvar *base64-line-length* 76
+  "Maximum length of the encoded base64 line.  NIL means it can
+be of unlimited length \(no line breaks will be done by the
+encoding function).")
+
+(defvar *quoted-printable-line-length* 72
+  "Maximum length of the encoded quoted printable line.  NIL
+means it can be of unlimited length \(no line breaks will be done
+by the encoding function).")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass decoder ()
+  ((input-function :initarg :input-function
+                   :reader decoder-input-function
+                   :type function
+                   :documentation
+                   "Function is called repeatedly by the decoder methods to get the next character.
+It should return a character os NIL (indicating EOF)."))
+  (:documentation
+   "Abstract base class for decoders."))
+
+(defclass parsing-decoder (decoder)
+  ((parser-errors :initform nil
+                  :initarg :parser-errors
+                  :reader decoder-parser-errors
+                  :type boolean))
+  (:documentation
+   "Abstract base class for decoders that do parsing."))
+
+(defclass encoder ()
+  ((output-function :initarg :output-function
+                    :reader encoder-output-function
+                    :type function
+                    :documentation
+                    "Function is called repeatedly by the encoder methods to output a character.
+It should expect a character as its only argument."))
+  (:documentation
+   "Abstract base class for encoders."))
+
+(defclass line-encoder (encoder)
+  ((column :initform 0
+           :type fixnum)
+   (line-length :initarg :line-length
+                :initform nil
+                :reader encoder-line-length
+                :type (or fixnum null)))
+  (:documentation
+   "Abstract base class for line encoders."))
+
+(defclass 8bit-decoder (decoder)
+  ()
+  (:documentation
+   "Class for decoders that do nothing."))
+
+(defclass 8bit-encoder (encoder)
+  ()
+  (:documentation
+   "Class for encoders that do nothing."))
+
+(defclass 7bit-decoder (decoder)
+  ()
+  (:documentation
+   "Class for decoders that do nothing."))
+
+(defclass 7bit-encoder (encoder)
+  ()
+  (:documentation
+   "Class for encoders that do nothing."))
+
+(defclass byte-decoder (decoder)
+  ()
+  (:documentation
+   "Class for decoders that turns chars to bytes."))
+
+(defclass byte-encoder (encoder)
+  ()
+  (:documentation
+   "Class for encoders that turns bytes to chars."))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric encoder-write-byte (encoder byte))
+(defgeneric encoder-finish-output (encoder))
+(defgeneric decoder-read-byte (decoder))
+
+(defmethod encoder-finish-output ((encoder encoder))
+  (values))
+
+(defmethod encoder-write-byte ((encoder 8bit-encoder) byte)
+  (funcall (slot-value encoder 'output-function)
+           (code-char byte))
+  (values))
+
+(defmethod decoder-read-byte ((decoder 8bit-decoder))
+  (awhen (funcall (slot-value decoder 'input-function))
+    (char-code it)))
+
+(defmethod encoder-write-byte ((encoder 7bit-encoder) byte)
+  (funcall (slot-value encoder 'output-function)
+           (code-char (logand #x7F byte)))
+  (values))
+
+(defmethod decoder-read-byte ((decoder 7bit-decoder))
+  (awhen (funcall (slot-value decoder 'input-function))
+    (logand #x7F (char-code it))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun decoder-read-sequence (sequence decoder &key (start 0) (end (length sequence)))
+  (declare (optimize (speed 3) (safety 0) (debug 0))
+           (type fixnum start end)
+           (type vector sequence))
+  (loop
+     for i fixnum from start below end
+     for byte = (decoder-read-byte decoder)
+     while byte
+     do (setf (aref sequence i) byte)
+     finally (return i)))
+
+(defun decoder-read-line (decoder)
+  (with-output-to-string (str)
+    (loop
+       for byte = (decoder-read-byte decoder)
+       unless byte
+       do (return-from decoder-read-line nil)
+       do (be c (code-char byte)
+            (cond ((char= c #\return)
+                   ;; skip the newline
+                   (decoder-read-byte decoder)
+                   (return nil))
+                  ((char= c #\newline)
+                   ;; the #\return was missing
+                   (return nil))
+                  (t (write-char c str)))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(declaim (inline parse-hex))
+(defun parse-hex (c1 c2)
+  "Parse two characters as hexadecimal and return their combined
+value."
+  (declare (optimize (speed 3) (safety 0) (debug 0))
+           (type character c1 c2))
+  (flet ((digit-value (char)
+           (or (position char "0123456789ABCDEF")
+               (return-from parse-hex nil))))
+    (+ (* 16 (digit-value c1))
+       (digit-value c2))))
+
+(defclass quoted-printable-decoder (parsing-decoder)
+  ((saved-bytes :initform (make-queue))))
+
+(defmethod decoder-read-byte ((decoder quoted-printable-decoder))
+  (declare (optimize (speed 3) (safety 0) (debug 0)))
+  (with-slots (input-function saved-bytes parser-errors) decoder
+    (declare (type function input-function))
+    (labels ((saveb (b)
+               (queue-append saved-bytes b)
+               (values))
+             (save (c)
+               (saveb (char-code c)))
+             (push-next ()
+               (be c (funcall input-function)
+                 (declare (type (or null character) c))
+                 (cond ((not c))
+                       ((or (char= c #\space)
+                            (char= c #\tab))
+                        (save c)
+                        (push-next))
+                       ((char= c #\=)
+                        (be c1 (funcall input-function)
+                          (cond ((not c1)
+                                 (save #\=))
+                                ((char= c1 #\return)
+                                 ;; soft line break: skip the next
+                                 ;; character which we assume to be a
+                                 ;; newline (pity if it isn't)
+                                 (funcall input-function)
+                                 (push-next))
+                                ((char= c1 #\newline)
+                                 ;; soft line break: the #\return is
+                                 ;; missing, but we are tolerant
+                                 (push-next))
+                                (t
+                                 ;; hexadecimal sequence: get the 2nd digit
+                                 (be c2 (funcall input-function)
+                                   (if c2
+                                       (aif (parse-hex c1 c2)
+                                            (saveb it)
+                                            (if parser-errors
+                                                (error "invalid hex sequence ~A~A" c1 c2)
+                                                (progn
+                                                  (save #\=)
+                                                  (save c1)
+                                                  (save c2))))
+                                       (progn
+                                         (save c)
+                                         (save c1))))))))
+                       (t
+                        (save c))))))
+      (or (queue-pop saved-bytes)
+          (progn
+            (push-next)
+            (queue-pop saved-bytes))))))
+
+(defmacro make-encoder-loop (encoder-class input-form output-form)
+  (with-gensyms (encoder byte)
+    `(loop
+        with ,encoder = (make-instance ',encoder-class
+                                       :output-function #'(lambda (char) ,output-form))
+        for ,byte = ,input-form
+        while ,byte
+        do (encoder-write-byte ,encoder ,byte)
+        finally (encoder-finish-output ,encoder))))
+
+(defmacro make-decoder-loop (decoder-class input-form output-form &key parser-errors)
+  (with-gensyms (decoder)
+    `(loop
+        with ,decoder = (make-instance ',decoder-class
+                                       :input-function #'(lambda () ,input-form)
+                                       :parser-errors ,parser-errors)
+        for byte = (decoder-read-byte ,decoder)
+        while byte
+        do ,output-form)))
+
+(defun decode-quoted-printable-stream (in out &key parser-errors)
+  "Read from stream IN a quoted printable text and write to
+binary output OUT the decoded stream of bytes."
+  (make-decoder-loop quoted-printable-decoder
+                     (read-byte in nil) (write-byte byte out)
+                     :parser-errors parser-errors))
+
+(defmacro make-stream-to-sequence-decoder (decoder-class input-form &key parser-errors)
+  "Decode the character stream STREAM and return a sequence of bytes."
+  (with-gensyms (output-sequence)
+    `(be ,output-sequence (make-array 0
+                                      :element-type '(unsigned-byte 8)
+                                      :fill-pointer 0
+                                      :adjustable t)
+       (make-decoder-loop ,decoder-class ,input-form
+                          (vector-push-extend byte ,output-sequence)
+                          :parser-errors ,parser-errors)
+       ,output-sequence)))
+
+(defun decode-quoted-printable-stream-to-sequence (stream &key parser-errors)
+  "Read from STREAM a quoted printable text and return a vector of
+bytes."
+  (make-stream-to-sequence-decoder quoted-printable-decoder
+    (read-char stream nil)
+    :parser-errors parser-errors))
+
+(defun decode-quoted-printable-string (string &key (start 0) (end (length string)) parser-errors)
+  "Decode STRING as quoted printable sequence of characters and
+return a decoded sequence of bytes."
+  (with-input-from-string (in string :start start :end end)
+    (decode-quoted-printable-stream-to-sequence in :parser-errors parser-errors)))
+
+(defclass quoted-printable-encoder (line-encoder)
+  ((line-length :initform *quoted-printable-line-length*
+                :type (or fixnum null))
+   (pending-space :initform nil
+                  :type boolean)))
+
+(defmethod encoder-write-byte ((encoder quoted-printable-encoder) byte)
+  (declare (optimize (speed 3) (safety 0) (debug 0))
+           (type (unsigned-byte 8) byte))
+  (with-slots (output-function column pending-space line-length) encoder
+    (declare (type function output-function)
+             (type fixnum column)
+             (type (or fixnum null) line-length)
+             (type boolean pending-space))
+    (labels ((out (c)
+               (funcall output-function c)
+               (values))
+             (outs (str)
+               (declare (type simple-string str))
+               (loop
+                  for c across str
+                  do (out c))
+               (values))
+             (out2hex (x)
+               (declare (type fixnum x))
+               (multiple-value-bind (a b) (truncate x 16)
+                 (out (digit-char a 16))
+                 (out (digit-char b 16)))))
+      (cond ((= byte #.(char-code #\newline))
+             (when pending-space
+               (outs "=20")
+               (setf pending-space nil))
+             (out #\newline)
+             (setf column 0))
+            ((= byte #.(char-code #\space))
+             (if pending-space
+                 (progn
+                   (out #\space)
+                   (f++ column))
+                 (setf pending-space t)))
+            (t
+             (when pending-space
+               (out #\space)
+               (f++ column)
+               (setf pending-space nil))
+             (cond ((or (< byte 32)
+                        (= byte #.(char-code #\=))
+                        (> byte 126))
+                    (out #\=)
+                    (out2hex byte)
+                    (f++ column 3))
+                   (t
+                    (out (code-char byte))
+                    (f++ column)))))
+      (when (and line-length
+                 (>= column line-length))
+        ;; soft line break
+        (outs #.(coerce '(#\= #\newline) 'string))
+        (setf column 0)))))
+
+(defmethod encoder-finish-output ((encoder quoted-printable-encoder))
+  (declare (optimize (speed 3) (safety 0) (debug 0)))
+  (with-slots (pending-space output-function) encoder
+    (declare (type boolean pending-space)
+             (type function output-function))
+    (when pending-space
+      (flet ((outs (s)
+               (declare (type simple-string s))
+               (loop
+                  for c across s
+                  do (funcall output-function c))))
+        (setf pending-space nil)
+        (outs "=20")))))
+
+(defun encode-quoted-printable-stream (in out)
+  "Read from IN a stream of bytes and write to OUT a stream of
+characters quoted printables encoded."
+  (make-encoder-loop quoted-printable-encoder
+                     (read-byte in nil)
+                     (write-char char out)))
+
+(defun encode-quoted-printable-sequence-to-stream (sequence stream &key (start 0) (end (length sequence)))
+  "Encode the sequence of bytes SEQUENCE and write to STREAM a
+quoted printable sequence of characters."
+  (be i start
+    (make-encoder-loop quoted-printable-encoder
+     (when (< i end)
+       (prog1 (elt sequence i)
+         (f++ i)))
+     (write-char char stream))))
+
+(defun encode-quoted-printable-sequence (sequence &key (start 0) (end (length sequence)))
+  "Encode the sequence of bytes SEQUENCE into a quoted printable
+string and return it."
+  (with-output-to-string (out)
+    (encode-quoted-printable-sequence-to-stream sequence out :start start :end end)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass base64-encoder (line-encoder)
+  ((line-length :initform *base64-line-length*)
+   (bitstore :initform 0
+             :type fixnum)
+   (bytecount :initform 0
+              :type fixnum))
+  (:documentation
+   "Class for Base64 encoder output streams."))
+
+
+(eval-when (:load-toplevel :compile-toplevel)
+  (unless (> most-positive-fixnum (expt 2 (* 8 3)))))
+
+(macrolet ((with-encoder (encoder &body forms)
+             `(with-slots (bitstore line-length column bytecount output-function) ,encoder
+                (declare (type fixnum column)
+                         (type fixnum bitstore bytecount)
+                         (type (or fixnum null) line-length)
+                         (type function output-function))
+                (labels ((emitr (i b)
+                           (declare (type fixnum i b))
+                           (unless (zerop i)
+                             (emitr (1- i) (ash b -6)))
+                           (emitc
+                            (char +base64-encode-table+ (logand b #x3F)))
+                           (values))
+                         (out (c)
+                           (funcall output-function c))
+                         (eol ()
+                           (progn
+                             (out #\return)
+                             (out #\newline)))
+                         (emitc (char)
+                           (out char)
+                           (f++ column)
+                           (when (and line-length
+                                      (>= column line-length))
+                             (setf column 0)
+                             (eol))))
+                  (declare (inline out eol emitc)
+                           (ignorable (function emitr) (function out) (function eol) (function emitc)))
+                  ,@forms))))
+  ;; For this function to work correctly, the FIXNUM must be at least
+  ;; 24 bits.
+  (defmethod encoder-write-byte ((encoder base64-encoder) byte)
+    (declare (optimize (speed 3) (safety 0) (debug 0))
+             (type (unsigned-byte 8) byte))
+    (with-encoder encoder
+      (setf bitstore (logior byte (the fixnum (ash bitstore 8))))
+      (f++ bytecount)
+      (when (= 3 bytecount)
+        (emitr 3 bitstore)
+        (setf bitstore 0
+              bytecount 0)))
+    (values))
+
+  (defmethod encoder-finish-output ((encoder base64-encoder))
+    (with-encoder encoder
+      (unless (zerop bytecount)
+        (multiple-value-bind (saved6 rest) (truncate (* bytecount 8) 6)
+          (setf bitstore (ash bitstore (- 6 rest)))
+          (emitr saved6 bitstore)
+          (dotimes (x (- 3 saved6))
+            (emitc #\=))))
+      (when (and line-length
+                 (not (zerop column)))
+        (eol)))
+    (values)))
+
+(defun encode-base64-stream (in out)
+  "Read a byte stream from IN and write to OUT the encoded Base64
+character stream."
+  (make-encoder-loop base64-encoder (read-byte in nil)
+                     (write-char char out)))
+
+(defun encode-base64-sequence-to-stream (sequence stream &key (start 0) (end (length sequence)))
+  "Encode the sequence of bytes SEQUENCE and write to STREAM the
+Base64 character sequence."
+  (be i start
+    (make-encoder-loop base64-encoder
+                       (when (< i end)
+                         (prog1 (elt sequence i)
+                           (incf i)))
+                       (write-char char stream))))
+
+(defun encode-base64-sequence (sequence &key (start 0) (end (length sequence)))
+  "Encode the sequence of bytes SEQUENCE into a Base64 string and
+return it."
+  (with-output-to-string (out)
+    (encode-base64-sequence-to-stream sequence out :start start :end end)))
+
+(defclass base64-decoder (parsing-decoder)
+  ((bitstore :initform 0
+             :type fixnum)
+   (bytecount :initform 0 :type fixnum))
+  (:documentation
+   "Class for Base64 decoder input streams."))
+
+(defmethod decoder-read-byte ((decoder base64-decoder))
+  (declare (optimize (speed 3) (safety 0) (debug 0)))
+  (with-slots (bitstore bytecount input-function) decoder
+    (declare (type fixnum bitstore bytecount)
+             (type function input-function))
+    (labels ((in6 ()
+               (loop
+                  for c = (funcall input-function)
+                  when (or (not c) (char= #\= c))
+                  do (return-from decoder-read-byte nil)
+                  do (be sextet (aref +base64-decode-table+ (char-code c))
+                       (unless (= sextet 65) ; ignore unrecognised characters
+                         (return sextet)))))
+             (push6 (sextet)
+               (declare (type fixnum sextet))
+               (setf bitstore
+                     (logior sextet (the fixnum (ash bitstore 6))))))
+      (case bytecount
+        (0
+         (setf bitstore (in6))
+         (push6 (in6))
+         (setf bytecount 1)
+         (ash bitstore -4))
+        (1
+         (push6 (in6))
+         (setf bytecount 2)
+         (logand #xFF (ash bitstore -2)))
+        (2
+         (push6 (in6))
+         (setf bytecount 0)
+         (logand #xFF bitstore))))))
+
+(defun decode-base64-stream (in out &key parser-errors)
+  "Read from IN a stream of characters Base64 encoded and write
+to OUT a stream of decoded bytes."
+  (make-decoder-loop base64-decoder
+                     (read-byte in nil) (write-byte byte out)
+                     :parser-errors parser-errors))
+
+(defun decode-base64-stream-to-sequence (stream &key parser-errors)
+  (make-stream-to-sequence-decoder base64-decoder
+                                   (read-char stream nil)
+                                   :parser-errors parser-errors))
+
+(defun decode-base64-string (string &key (start 0) (end (length string)) parser-errors)
+  (with-input-from-string (in string :start start :end end)
+    (decode-base64-stream-to-sequence in :parser-errors parser-errors)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun dump-stream-binary (in out)
+  "Write content of IN character stream to OUT binary stream."
+  (loop
+     for c = (read-char in nil)
+     while c
+     do (write-byte (char-code c) out)))
+
+(defun decode-stream (in out encoding &key parser-errors-p)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (decode-quoted-printable-stream in out
+                                     :parser-errors parser-errors-p))
+    (:base64
+     (decode-base64-stream in out
+                           :parser-errors parser-errors-p))
+    (otherwise
+     (dump-stream-binary in out))))
+
+(defun decode-string (string encoding &key parser-errors-p)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (decode-quoted-printable-string string
+                                     :parser-errors parser-errors-p))
+    (:base64
+     (decode-base64-string string
+                           :parser-errors parser-errors-p))
+    (otherwise
+     (map '(vector (unsigned-byte 8)) #'char-code string))))
+
+(defun decode-stream-to-sequence (stream encoding &key parser-errors-p)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (decode-quoted-printable-stream-to-sequence stream
+                                                 :parser-errors parser-errors-p))
+    (:base64
+     (decode-base64-stream-to-sequence stream
+                                       :parser-errors parser-errors-p))
+    (otherwise
+     (loop
+        with output-sequence = (make-array 0 :fill-pointer 0
+                                           :element-type '(unsigned-byte 8)
+                                           :adjustable t)
+        for c = (read-char stream nil)
+        while c
+        do (vector-push-extend (char-code c) output-sequence)
+        finally (return output-sequence)))))
+
+(defun encode-stream (in out encoding)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (encode-quoted-printable-stream in out))
+    (:base64
+     (encode-base64-stream in out))
+    (otherwise
+     (loop
+        for byte = (read-byte in nil)
+        while byte
+        do (write-char (code-char byte) out)))))
+
+(defun encode-sequence-to-stream (sequence out encoding)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (encode-quoted-printable-sequence-to-stream sequence out))
+    (:base64
+     (encode-base64-sequence-to-stream sequence out))
+    (otherwise
+     (loop
+        for byte across sequence
+        do (write-char (code-char byte) out)))))
+
+(defun encode-sequence (sequence encoding)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (encode-quoted-printable-sequence sequence))
+    (:base64
+     (encode-base64-sequence sequence))
+    (otherwise
+     (map 'string #'code-char sequence))))
+
+;; This is similar to decode-quoted-printable-string but #\_ is used
+;; instead of space
+(defun decode-quoted-printable-RFC2047-string (string &key (start 0) (end (length string)))
+  "Decode a string encoded according to the quoted printable
+method of RFC2047 and return a sequence of bytes."
+  (declare (optimize (speed 3) (debug 0) (safety 0))
+           (type simple-string string))
+  (loop
+     with output-sequence = (make-array (length string)
+                                        :element-type '(unsigned-byte 8)
+                                        :fill-pointer 0)
+     for i fixnum from start by 1 below end
+     for c = (char string i)
+     do (case c
+          (#\=
+           (vector-push-extend (or (parse-hex (char string (1+ i)) (char string (+ 2 i)))
+                                   ;; the char code was malformed
+                                   #.(char-code #\?))
+                               output-sequence)
+           (f++ i 2))
+          (#\_ (vector-push-extend #.(char-code #\space) output-sequence))
+          (otherwise
+           (vector-push-extend (char-code c) output-sequence)))
+       finally (return output-sequence)))
+
+(defun decode-RFC2047-part (encoding string &key (start 0) (end (length string)))
+  "Decode STRING according to RFC2047 and return a sequence of
+bytes."
+  (gcase (encoding string-equal)
+    ("Q" (decode-quoted-printable-RFC2047-string string :start start :end end))
+    ("B" (decode-base64-string string :start start :end end))
+    (t string)))
+
+(defun parse-RFC2047-text (text)
+  "Parse the string TEXT according to RFC2047 rules and return a list
+of pairs and strings.  The strings are the bits interposed between the
+actually encoded text.  The pairs are composed of: a decoded byte
+sequence, a charset string indicating the original coding."
+  (loop
+     with result = '()
+     with previous-end = 0
+     for start = (search "=?" text :start2 previous-end)
+     while start
+     for first-? = (position #\? text :start (+ 2 start))
+     while first-?
+     for second-? = (position #\? text :start (1+ first-?))
+     while second-?
+     for end = (search "?=" text :start2 (1+ second-?))
+     while end
+     do (let ((charset (string-upcase (subseq text (+ 2 start) first-?)))
+              (encoding (subseq text (1+ first-?) second-?)))
+          (unless (= previous-end start)
+            (push (subseq text previous-end start)
+                  result))
+          (setf previous-end (+ end 2))
+          (push (cons (decode-RFC2047-part encoding text :start (1+ second-?) :end end)
+                      charset)
+                result))
+     finally (unless (= previous-end (length text))
+               (push (subseq text previous-end (length text))
+                     result))
+       (return (nreverse result))))
+
+(defun decode-RFC2047 (text)
+  "Decode TEXT into a fully decoded string. Whenever a non ASCII part is
+  encountered, try to decode it using babel, otherwise signal an error."
+  (flet ((decode-part (part)
+           (etypecase part
+             (cons (babel:octets-to-string
+                    (car part)
+                    :encoding (babel-encodings:get-character-encoding
+                               (intern (string-upcase (cdr part)) 'keyword))))
+             (string part))))
+    (apply #'concatenate
+           (cons 'string
+                 (mapcar #'decode-part (mime:parse-RFC2047-text text))))))
diff --git a/third_party/lisp/mime4cl/mime.lisp b/third_party/lisp/mime4cl/mime.lisp
new file mode 100644
index 0000000000..5639aab236
--- /dev/null
+++ b/third_party/lisp/mime4cl/mime.lisp
@@ -0,0 +1,1075 @@
+;;;  mime4cl.lisp --- MIME primitives for Common Lisp
+
+;;;  Copyright (C) 2005-2008, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2021 by the TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :mime4cl)
+
+(defclass mime-part ()
+  ((subtype
+    :type (or string null)
+    :initarg :subtype
+    :accessor mime-subtype
+    ;; some mime types don't require a subtype
+    :initform nil)
+   (type-parameters
+    :type list
+    :initarg :type-parameters
+    :initform '()
+    :accessor mime-type-parameters)
+   (version
+    :type (or string null)
+    :initarg :mime-version
+    :initform "1.0"
+    :accessor mime-version)
+   (id
+    :initform nil
+    :initarg :id
+    :reader mime-id)
+   (description
+    :initform nil
+    :initarg :description
+    :accessor mime-description)
+   (encoding
+    :initform :7bit
+    :initarg :encoding
+    :reader mime-encoding
+    :documentation
+    "It's supposed to be either:
+  :7BIT, :8BIT, :BINARY, :QUOTED-PRINTABLE, :BASE64, a
+  X-token or an ietf-token (whatever that means).")
+   (disposition
+    :type (or string null)
+    :initarg :disposition
+    :initform nil
+    :accessor mime-disposition)
+   (disposition-parameters
+    :type list
+    :initarg :disposition-parameters
+    :initform '()
+    :accessor mime-disposition-parameters))
+  (:documentation
+   "Abstract base class for all types of MIME parts."))
+
+(defclass mime-bodily-part (mime-part)
+  ((body
+    :initarg :body
+    :accessor mime-body))
+  (:documentation
+   "Abstract base class for MIME parts with a body."))
+
+(defclass mime-unknown-part (mime-bodily-part)
+  ((type
+    :initarg :type
+    :reader mime-type
+    :documentation
+    "The original type string from the MIME header."))
+  (:documentation
+   "MIME part unknown to this library.  Accepted but not handled."))
+
+(defclass mime-text (mime-bodily-part) ())
+
+;; This turns out to be handy when making methods specialised
+;; non-textual attachments.
+(defclass mime-binary (mime-bodily-part) ())
+
+(defclass mime-image (mime-binary) ())
+
+(defclass mime-audio (mime-binary) ())
+
+(defclass mime-video (mime-binary) ())
+
+(defclass mime-application (mime-binary) ())
+
+(defclass mime-multipart (mime-part)
+  ((parts :initarg :parts
+          :accessor mime-parts)))
+
+(defclass mime-message (mime-part)
+  ((headers :initarg :headers
+            :initform '()
+            :type list
+            :accessor mime-message-headers)
+   (real-message :initarg :body
+                 :accessor mime-body)))
+
+(defun mime-part-p (object)
+  (typep object 'mime-part))
+
+(defmethod initialize-instance ((part mime-multipart) &key &allow-other-keys)
+  (call-next-method)
+  ;; The initialization argument of the PARTS slot of a mime-multipart
+  ;; is expected to be a list of mime-parts.  Thus, we implicitly
+  ;; create the mime parts using the arguments found in this list.
+  (with-slots (parts) part
+    (when (slot-boundp part 'parts)
+      (setf parts
+            (mapcar #'(lambda (subpart)
+                        (if (mime-part-p subpart)
+                            subpart
+                            (apply #'make-instance subpart)))
+                    parts)))))
+
+(defmethod initialize-instance ((part mime-message) &key &allow-other-keys)
+  (call-next-method)
+  ;; Allow a list of mime parts to be specified as body of a
+  ;; mime-message.  In that case we implicitly create a mime-multipart
+  ;; and assign to the body slot.
+  (with-slots (real-message) part
+    (when (and (slot-boundp part 'real-message)
+               (consp real-message))
+      (setf real-message
+            (make-instance 'mime-multipart :parts real-message)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun alist= (alist1 alist2 &key (test #'eql))
+  (null
+   (set-difference alist1 alist2
+                   :test #'(lambda (x y)
+                             (and (funcall test (car x) (car y))
+                                  (funcall test (cdr x) (cdr y)))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric mime= (mime1 mime2)
+  (:documentation
+   "Return true if MIME1 and MIME2 have equivalent structure and identical bodies (as for EQ)."))
+
+(defmethod mime= ((part1 mime-part) (part2 mime-part))
+  (macrolet ((null-or (compare x y)
+               `(or (and (not ,x)
+                         (not ,y))
+                    (and ,x ,y
+                         (,compare ,x ,y))))
+             (cmp-slot (compare reader)
+               `(null-or ,compare (,reader part1) (,reader part2))))
+    (and (eq (class-of part1) (class-of part2))
+         (cmp-slot string-equal mime-subtype)
+         (alist= (mime-type-parameters part1)
+                 (mime-type-parameters part2)
+                 :test #'string-equal)
+         (cmp-slot string= mime-id)
+         (cmp-slot string= mime-description)
+         (cmp-slot eq mime-encoding)
+         (cmp-slot equal mime-disposition)
+         (alist= (mime-disposition-parameters part1)
+                 (mime-disposition-parameters part2)
+                 :test #'string-equal))))
+
+(defmethod mime= ((part1 mime-multipart) (part2 mime-multipart))
+  (and (call-next-method)
+       (every #'mime= (mime-parts part1) (mime-parts part2))))
+
+(defmethod mime= ((part1 mime-message) (part2 mime-message))
+  (and (call-next-method)
+       (alist= (mime-message-headers part1) (mime-message-headers part2)
+               :test #'string=)
+       (mime= (mime-body part1) (mime-body part2))))
+
+(defun mime-body-stream (mime-part &key (binary t))
+  (make-instance (if binary
+                     'binary-input-adapter-stream
+                     'character-input-adapter-stream)
+                 :source (mime-body mime-part)))
+
+(defun mime-body-length (mime-part)
+  (be body (mime-body mime-part)
+    ;; here the stream type is missing on purpose, because we may not
+    ;; be able to size the length of a stream
+    (etypecase body
+      (string
+       (length body))
+      (vector
+       (length body))
+      (pathname
+       (file-size body))
+      (file-portion
+       (with-open-stream (in (open-decoded-file-portion body))
+         (loop
+            for byte = (read-byte in nil)
+            while byte
+            count byte))))))
+
+(defmacro with-input-from-mime-body-stream ((stream part &key (binary t)) &body forms)
+  `(with-open-stream (,stream (mime-body-stream ,part :binary ,binary))
+     ,@forms))
+
+(defmethod mime= ((part1 mime-bodily-part) (part2 mime-bodily-part))
+  (and (call-next-method)
+       (with-input-from-mime-body-stream (in1 part1)
+         (with-input-from-mime-body-stream (in2 part2)
+           (loop
+              for b1 = (read-byte in1 nil)
+              for b2 = (read-byte in2 nil)
+              always (eq b1 b2)
+              while (and b1 b2))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric get-mime-type-parameter (part name)
+  (:documentation
+   "Return the MIME type parameter associated to NAME of PART."))
+
+(defgeneric (setf get-mime-type-parameter) (value part name)
+  (:documentation
+   "Set the MIME type parameter associated to NAME of PART."))
+
+(defmethod get-mime-type-parameter ((part mime-part) name)
+  (cdr (assoc name (mime-type-parameters part) :test #'string-equal)))
+
+(defmethod (setf get-mime-type-parameter) (value part name)
+  (aif (assoc name (mime-type-parameters part) :test #'string-equal)
+       (setf (cdr it) value)
+       (push (cons name value)
+             (mime-type-parameters part)))
+  value)
+
+(defgeneric get-mime-disposition-parameter (part name)
+  (:documentation
+   "Return the MIME disposition parameter associated to NAME of PART."))
+
+(defmethod get-mime-disposition-parameter ((part mime-part) name)
+  (cdr (assoc name (mime-disposition-parameters part) :test #'string-equal)))
+
+(defmethod (setf get-mime-disposition-parameter) (value part name)
+  (aif (assoc name (mime-disposition-parameters part) :test #'string-equal)
+       (setf (cdr it) value)
+       (push (cons name value)
+             (mime-disposition-parameters part))))
+
+(defmethod mime-part-file-name ((part mime-part))
+  "Return the filename associated to mime PART or NIL if the mime
+part doesn't have a file name."
+  (or (get-mime-disposition-parameter part :filename)
+      (get-mime-type-parameter part :name)))
+
+(defmethod (setf mime-part-file-name) (value (part mime-part))
+  "Set the filename associated to mime PART."
+  (setf (get-mime-disposition-parameter part :filename) value
+        (get-mime-type-parameter part :name) value))
+
+(defun mime-text-charset (part)
+  (get-mime-type-parameter part :charset))
+
+(defun split-header-parts (string)
+  "Split parts of a MIME headers.  These are divided by
+semi-colons not within strings or comments."
+  (labels ((skip-comment (pos)
+             (loop
+                while (< pos (length string))
+                do (case (elt string pos)
+                     (#\( (setf pos (skip-comment (1+ pos))))
+                     (#\\ (incf pos 2))
+                     (#\) (return (1+ pos)))
+                     (otherwise (incf pos)))
+                finally (return pos)))
+           (skip-string (pos)
+             (loop
+                while (< pos (length string))
+                do (case (elt string pos)
+                     (#\\ (incf pos 2))
+                     (#\" (return (1+ pos)))
+                     (otherwise (incf pos)))
+                finally (return pos))))
+    (loop
+       with start = 0 and i = 0 and parts = '()
+       while (< i (length string))
+       do (case (elt string i)
+            (#\; (push (subseq string start i) parts)
+                 (setf start (incf i)))
+            (#\" (setf i (skip-string i)))
+            (#\( (setf i (skip-comment (1+ i))))
+            (otherwise (incf i)))
+       finally (return (mapcar #'string-trim-whitespace (nreverse (cons (subseq string start) parts)))))))
+
+(defun parse-parameter (string)
+  "Given a string like \"foo=bar\" return a pair (\"foo\" .
+\"bar\").  Return NIL if string is not parsable."
+  (be equal-position (position #\= string)
+    (when equal-position
+      (be key (subseq string  0 equal-position)
+        (if (= equal-position (1- (length string)))
+            (cons key "")
+            (be value (string-trim-whitespace (subseq string (1+ equal-position)))
+              (cons key
+                    (if (and (> (length value) 1)
+                             (char= #\" (elt value 0)))
+                        ;; the syntax of a RFC822 string is more or
+                        ;; less the same as the Lisp one: use the Lisp
+                        ;; reader
+                        (or (ignore-errors (read-from-string value))
+                            (subseq value 1))
+                        (be end (or (position-if #'whitespace-p value)
+                                    (length value))
+                          (subseq value 0 end))))))))))
+
+(defun parse-content-type (string)
+  "Parse string as a Content-Type MIME header and return a list
+of three elements.  The first is the type, the second is the
+subtype and the third is an alist of parameters and their values.
+Example: (\"text\" \"plain\" ((\"charset\" . \"us-ascii\")...))."
+  (let* ((parts (split-header-parts string))
+         (content-type-string (car parts))
+         (slash (position #\/ content-type-string)))
+    ;; You'd be amazed to know how many MUA can't produce an RFC
+    ;; compliant message.
+    (when slash
+      (let ((type (subseq content-type-string 0 slash))
+            (subtype (subseq content-type-string (1+ slash))))
+        (list type subtype (remove nil (mapcar #'parse-parameter (cdr parts))))))))
+
+(defun parse-content-disposition (string)
+  "Parse string as a Content-Disposition MIME header and return a
+list.  The first element is the layout, the other elements are
+the optional parameters alist.
+Example: (\"inline\" (\"filename\" . \"doggy.jpg\"))."
+  (be parts (split-header-parts string)
+    (cons (car parts) (mapcan #'(lambda (parameter-string)
+                                  (awhen (parse-parameter parameter-string)
+                                    (list it)))
+                              (cdr parts)))))
+
+(defun parse-RFC822-header (string)
+  "Parse STRING which should be a valid RFC822 message header and
+return two values: a string of the header name and a string of
+the header value."
+  (be colon (position #\: string)
+    (when colon
+      (values (string-trim-whitespace (subseq string 0 colon))
+              (string-trim-whitespace (subseq string (1+ colon)))))))
+
+
+(defvar *default-type* '("text" "plain" (("charset" . "us-ascii")))
+  "Internal special variable that contains the default MIME type at
+any given time of the parsing phase.  There are MIME container parts
+that may change this.")
+
+(defvar *mime-types*
+  '((:text mime-text)
+    (:image mime-image)
+    (:audio mime-audio)
+    (:video mime-video)
+    (:application mime-application)
+    (:multipart mime-multipart)
+    (:message mime-message)))
+
+(defgeneric mime-part-size (part)
+  (:documentation
+   "Return the size in bytes of the body of a MIME part."))
+
+(defgeneric print-mime-part (part stream)
+  (:documentation
+   "Output to STREAM one of the possible human-readable representation
+of mime PART.  Binary parts are omitted.  This function can be used to
+quote messages, for instance."))
+
+(defun do-multipart-parts (body-stream part-boundary contents-function end-part-function)
+  "Read through BODY-STREAM.  Call CONTENTS-FUNCTION at
+each (non-boundary) line or END-PART-FUNCTION at each PART-BOUNDARY."
+  (let* ((boundary (s+ "--" part-boundary))
+         (boundary-length (length boundary)))
+    (labels ((output-line (line)
+               (funcall contents-function line))
+             (end-part ()
+               (funcall end-part-function))
+             (last-part ()
+               (end-part)
+               (return-from do-multipart-parts))
+             (process-line (line)
+               (cond ((not (string-starts-with boundary line))
+                      ;; normal line
+                      (output-line line))
+                     ((and (= (length (string-trim-whitespace line))
+                              (+ 2 boundary-length))
+                           (string= "--" line :start2 boundary-length))
+                      ;; end of the last part
+                      (last-part))
+                     ;; according to RFC2046 "the boundary may be followed
+                     ;; by zero or more characters of linear whitespace"
+                     ((= (length (string-trim-whitespace line)) boundary-length)
+                      ;; beginning of the next part
+                      (end-part))
+                     (t
+                      ;; the line boundary is followed by some
+                      ;; garbage; we treat it as a normal line
+                      (output-line line)))))
+      (loop
+         for line = (read-line body-stream nil)
+         ;; we should never reach the end of a proper multipart MIME
+         ;; stream, but we don't want to be fooled by corrupted ones,
+         ;; so we check for EOF
+         unless line
+         do (last-part)
+         do (process-line line)))))
+
+;; This awkward handling of newlines is due to RFC2046: "The CRLF
+;; preceding the boundary delimiter line is conceptually attached to
+;; the boundary so that it is possible to have a part that does not
+;; end with a CRLF (line break).  Body parts that must be considered
+;; to end with line breaks, therefore, must have two CRLFs preceding
+;; the boundary delimiter line, the first of which is part of the
+;; preceding body part, and the second of which is part of the
+;; encapsulation boundary".
+(defun split-multipart-parts (body-stream part-boundary)
+  "Read from BODY-STREAM and split MIME parts separated by
+PART-BOUNDARY.  Return a list of strings."
+  (let ((part (make-string-output-stream))
+        (parts '())
+        (beginning-of-part-p t))
+    (flet ((output-line (line)
+             (if beginning-of-part-p
+                 (setf beginning-of-part-p nil)
+                 (terpri part))
+             (write-string line part))
+           (end-part ()
+             (setf beginning-of-part-p t)
+             (push (get-output-stream-string part) parts)))
+      (do-multipart-parts body-stream part-boundary #'output-line #'end-part)
+      (close part)
+      ;; the first part is empty or contains all the junk
+      ;; to the first boundary
+      (cdr (nreverse parts)))))
+
+(defun index-multipart-parts (body-stream part-boundary)
+  "Read from BODY-STREAM and return the file offset of the MIME parts
+separated by PART-BOUNDARY."
+  (let ((parts '())
+        (start 0)
+        (len 0)
+        (beginning-of-part-p t))
+    (flet ((sum-chars (line)
+             (incf len (length line))
+             ;; account for the #\newline
+             (if beginning-of-part-p
+                 (setf beginning-of-part-p nil)
+                 (incf len)))
+           (end-part ()
+             (setf beginning-of-part-p t)
+             (push (cons start (+ start len)) parts)
+             (setf start (file-position body-stream)
+                   len 0)))
+      (do-multipart-parts body-stream part-boundary #'sum-chars #'end-part)
+      ;; the first part is all the stuff up to the first boundary;
+      ;; just junk
+      (cdr (nreverse parts)))))
+
+(defgeneric encode-mime-part (part stream))
+(defgeneric encode-mime-body (part stream))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun write-mime-header (part stream)
+  (when (mime-version part)
+    (format stream "~&MIME-Version: ~A~%" (mime-version part)))
+  (format stream "~&Content-Type: ~A~:{; ~A=~S~}~%" (mime-type-string part)
+          (mapcar #'(lambda (pair)
+                      (list (car pair) (cdr pair)))
+                  (mime-type-parameters part)))
+  (awhen (mime-encoding part)
+    (format stream "Content-Transfer-Encoding: ~A~%" it))
+  (awhen (mime-description part)
+    (format stream "Content-Description: ~A~%" it))
+  (when (mime-disposition part)
+    (format stream "Content-Disposition: ~A~:{; ~A=~S~}~%"
+            (mime-disposition part)
+            (mapcar #'(lambda (pair)
+                        (list (car pair) (cdr pair)))
+                    (mime-disposition-parameters part))))
+  (awhen (mime-id part)
+    (format stream "Content-ID: ~A~%" it))
+  (terpri stream))
+
+(defmethod encode-mime-part ((part mime-part) stream)
+  (write-mime-header part stream)
+  (encode-mime-body part stream))
+
+(defmethod encode-mime-part ((part mime-message) stream)
+  ;; tricky: we have to mix the MIME headers with the message headers
+  (dolist (h (mime-message-headers part))
+    (unless (stringp (car h))
+      (setf (car h)
+            (string-capitalize (car h))))
+    (unless (or (string-starts-with "content-" (car h) #'string-equal)
+                (string-equal "mime-version" (car h)))
+      (format stream "~A: ~A~%"
+              (car h) (cdr h))))
+  (encode-mime-part (mime-body part) stream))
+
+(defmethod encode-mime-part ((part mime-multipart) stream)
+  ;; choose a boundary if not already set
+  (let* ((original-boundary (get-mime-type-parameter part :boundary))
+         (boundary (choose-boundary (mime-parts part) original-boundary)))
+    (unless (and original-boundary
+                 (string= boundary original-boundary))
+      (setf (get-mime-type-parameter part :boundary) boundary))
+    (call-next-method)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmethod encode-mime-body ((part mime-part) stream)
+  (with-input-from-mime-body-stream (in part)
+    (encode-stream in stream (mime-encoding part))))
+
+(defmethod encode-mime-body ((part mime-message) stream)
+  (encode-mime-body (mime-body part) stream))
+
+(defmethod encode-mime-body ((part mime-multipart) stream)
+  (be boundary (or (get-mime-type-parameter part :boundary)
+                   (setf (get-mime-type-parameter part :boundary)
+                         (choose-boundary (mime-parts part))))
+    (dolist (p (mime-parts part))
+      (format stream "~%--~A~%" boundary)
+      (encode-mime-part p stream))
+    (format stream "~%--~A--~%" boundary)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun time-RFC822-string (&optional (epoch (get-universal-time)))
+  "Return a string describing the current time according to
+the RFC822."
+  (multiple-value-bind (ss mm hh day month year week-day dst tz) (decode-universal-time epoch)
+    (declare (ignore dst))
+    (format nil "~A, ~A ~A ~2,'0D ~2,'0D:~2,'0D:~2,'0D ~:[-~;+~]~2,'0D~2,'0D"
+            (subseq (week-day->string week-day) 0 3)
+            day (subseq (month->string month) 0 3) (mod year 100) hh mm ss
+            (plusp tz) (abs (truncate tz)) (mod (* 60 tz) 60))))
+
+(defun parse-RFC822-date (date-string)
+  "Parse a RFC822 compliant date string and return an universal
+time."
+  ;; if we can't parse it, just return NIL
+  (ignore-errors
+    ;; skip the optional DoW
+    (awhen (position #\, date-string)
+      (setf date-string (subseq date-string (1+ it))))
+    (destructuring-bind (day month year time &optional tz &rest rubbish)
+        (split-at '(#\space #\tab) date-string)
+      (declare (ignore rubbish))
+      (destructuring-bind (hh mm &optional ss) (split-string-at-char time #\:)
+        (encode-universal-time
+         (if ss
+             (read-from-string ss)
+             0)
+         (read-from-string mm)
+         (read-from-string hh)
+         (read-from-string day)
+         (1+ (position month
+                       '("Jan" "Feb" "Mar" "Apr" "May" "Jun"
+                         "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
+                       :test #'string-equal))
+         (read-from-string year)
+         (when (and tz (or (char= #\+ (elt tz 0))
+                           (char= #\- (elt tz 0))))
+           (/ (read-from-string tz) 100)))))))
+
+(defun read-RFC822-headers (stream &optional required-headers)
+  "Read RFC822 compliant headers from STREAM and return them in a
+alist of keyword and string pairs.  REQUIRED-HEADERS is a list of
+header names we are interested in; if NIL return all headers
+found in STREAM."
+  ;; the skip-header variable is to avoid the mistake of appending a
+  ;; continuation line of a header we don't want to a header we want
+  (loop
+     with headers = '() and skip-header = nil
+     for line = (be line (read-line stream nil)
+                  ;; skip the Unix "From " header if present
+                  (if (string-starts-with "From " line)
+                      (read-line stream nil)
+                      line))
+     then (read-line stream nil)
+     while (and line
+                (not (zerop (length line))))
+     do (if (whitespace-p (elt line 0))
+            (unless (or skip-header
+                        (null headers))
+              (setf (cdar headers) (s+ (cdar headers) '(#\newline) line)))
+            (multiple-value-bind (name value) (parse-RFC822-header line)
+              ;; the line contained rubbish instead of an header: we
+              ;; play nice and return as we were at the end of the
+              ;; headers
+              (unless name
+                (return (nreverse headers)))
+              (if (or (null required-headers)
+                      (member name required-headers :test #'string-equal))
+                  (progn
+                    (push (cons name value) headers)
+                    (setf skip-header nil))
+                  (setf skip-header t))))
+     finally (return (nreverse headers))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric mime-message (thing)
+  (:documentation
+   "Convert THING to a MIME-MESSAGE object."))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun mime-message-header-values (name message &key decode)
+  "Return all values of the header with NAME in MESSAGE, optionally decoding
+  it according to RFC2047 if :DECODE is T."
+  (loop ;; A header may occur multiple times
+        for header in (mime-message-headers message)
+        ;; MIME Headers should be case insensitive
+        ;; https://stackoverflow.com/a/6143644
+        when (string-equal (car header) name)
+        collect (if decode
+                    (decode-RFC2047 (cdr header))
+                    (cdr header))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar *lazy-mime-decode* t
+  "If true don't  decode mime bodies in memory.")
+
+(defgeneric decode-mime-body (part input-stream))
+
+(defmethod decode-mime-body ((part mime-part) (stream delimited-input-stream))
+ (be base (base-stream stream)
+   (if *lazy-mime-decode*
+       (setf (mime-body part)
+             (make-file-portion :data (etypecase base
+                                        (my-string-input-stream
+                                         (stream-string base))
+                                        (file-stream
+                                         (pathname base)))
+                                :encoding (mime-encoding part)
+                                :start (file-position stream)
+                                :end (stream-end stream)))
+       (call-next-method))))
+
+(defmethod decode-mime-body ((part mime-part) (stream file-stream))
+  (if *lazy-mime-decode*
+      (setf (mime-body part)
+            (make-file-portion :data (pathname stream)
+                               :encoding (mime-encoding part)
+                               :start (file-position stream)))
+      (call-next-method)))
+
+(defmethod decode-mime-body ((part mime-part) (stream my-string-input-stream))
+  (if *lazy-mime-decode*
+      (setf (mime-body part)
+            (make-file-portion :data (stream-string stream)
+                               :encoding (mime-encoding part)
+                               :start (file-position stream)))
+      (call-next-method)))
+
+(defmethod decode-mime-body ((part mime-part) stream)
+  (setf (mime-body part)
+        (decode-stream-to-sequence stream (mime-encoding part))))
+
+(defmethod decode-mime-body ((part mime-multipart) stream)
+  "Decode STREAM according to PART characteristics and return a
+list of MIME parts."
+  (save-file-excursion (stream)
+    (be offsets (index-multipart-parts stream (get-mime-type-parameter part :boundary))
+      (setf (mime-parts part)
+            (mapcar #'(lambda (p)
+                        (destructuring-bind (start . end) p
+                          (be *default-type* (if (eq :digest (mime-subtype part))
+                                                 '("message" "rfc822" ())
+                                                 '("text" "plain" (("charset" . "us-ascii"))))
+                              in (make-instance 'delimited-input-stream
+                                                :stream stream
+                                                :dont-close t
+                                                :start start
+                                                :end end)
+                              (read-mime-part in))))
+                    offsets)))))
+
+(defmethod decode-mime-body ((part mime-message) stream)
+  "Read from STREAM the body of PART.  Return the decoded MIME
+body."
+  (setf (mime-body part)
+        (read-mime-message stream)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst +known-encodings+ '(:7BIT :8BIT :BINARY :QUOTED-PRINTABLE :BASE64)
+  "List of known content encodings.")
+
+(defun keywordify-encoding (string)
+  "Return a keyword for a content transfer encoding string.
+Return STRING itself if STRING is an unkown encoding."
+  (aif (member string +known-encodings+ :test #'string-equal)
+       (car it)
+       string))
+
+(defun header (name headers)
+  (be elt (assoc name headers :test #'string-equal)
+    (values (cdr elt) (car elt))))
+
+(defun (setf header) (value name headers)
+  (be entry (assoc name headers :test #'string-equal)
+    (unless entry
+      (error "missing header ~A can't be set" name))
+    (setf (cdr entry) value)))
+
+(defun make-mime-part (headers stream)
+  "Create a MIME-PART object based on HEADERS and a body which
+has to be read from STREAM.  If the mime part type can't be
+guessed from the headers, use the *DEFAULT-TYPE*."
+  (flet ((hdr (what)
+           (header what headers)))
+    (destructuring-bind (type subtype parms)
+        (or 
+         (aand (hdr :content-type)
+               (parse-content-type it))
+         *default-type*)
+      (let* ((class (or (cadr (assoc type *mime-types* :test #'string-equal))
+                        'mime-unknown-part))
+             (disp (aif (hdr :content-disposition)
+                        (parse-content-disposition it)
+                        (values nil nil)))
+             (part (make-instance class
+                                  :type (hdr :content-type)
+                                  :subtype subtype
+                                  :type-parameters parms
+                                  :disposition (car disp)
+                                  :disposition-parameters (cdr disp)
+                                  :mime-version (hdr :mime-version)
+                                  :encoding (keywordify-encoding
+                                             (hdr :content-transfer-encoding))
+                                  :description (hdr :content-description)
+                                  :id (hdr :content-id)
+                                  :allow-other-keys t)))
+        (decode-mime-body part stream)
+        part))))
+
+(defun read-mime-part (stream)
+  "Read mime part from STREAM.  Return a MIME-PART object."
+  (be headers (read-rfc822-headers stream
+                                   '(:mime-version :content-transfer-encoding :content-type
+                                     :content-disposition :content-description :content-id))
+    (make-mime-part headers stream)))
+
+(defun read-mime-message (stream)
+  "Main function to read a MIME message from a stream.  It
+returns a MIME-MESSAGE object."
+  (be headers (read-rfc822-headers stream)
+      *default-type* '("text" "plain" (("charset" . "us-ascii")))
+    (flet ((hdr (what)
+             (header what headers)))
+      (destructuring-bind (type subtype parms)
+          (or (aand (hdr :content-type)
+                    (parse-content-type it))
+              *default-type*)
+        (declare (ignore type subtype))
+        (make-instance 'mime-message
+                       :headers headers
+                       ;; this is just for easy access
+                       :type-parameters parms
+                       :body (make-mime-part headers stream))))))
+
+(defmethod mime-message ((msg mime-message))
+  msg)
+
+(defmethod mime-message ((msg string))
+  (with-open-stream (in (make-instance 'my-string-input-stream :string msg))
+    (read-mime-message in)))
+
+(defmethod mime-message ((msg stream))
+  (read-mime-message msg))
+
+(defmethod mime-message ((msg pathname))
+  (let (#+sbcl(sb-impl::*default-external-format* :latin-1)
+        #+sbcl(sb-alien::*default-c-string-external-format* :latin-1))
+    (with-open-file (in msg)
+      (read-mime-message in))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric mime-part (object)
+  (:documentation
+   "Promote object, if necessary, to MIME-PART."))
+
+(defmethod mime-part ((object string))
+  (make-instance 'mime-text :subtype "plain" :body object))
+
+(defmethod mime-part ((object pathname))
+  (make-instance 'mime-application
+                 :subtype "octect-stream"
+                 :content-transfer-encoding :base64
+                 :body (read-file object :element-type '(unsigned-byte 8))))
+
+(defmethod mime-part ((object mime-part))
+  object)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmethod make-encoded-body-stream ((part mime-bodily-part))
+  (be body (mime-body part)
+    (make-instance (case (mime-encoding part)
+                     (:base64
+                      'base64-encoder-input-stream)
+                     (:quoted-printable
+                      'quoted-printable-encoder-input-stream)
+                     (t
+                      '8bit-encoder-input-stream))
+                   :stream (make-instance 'binary-input-adapter-stream :source body))))
+
+(defun choose-boundary (parts &optional default)
+  (labels ((match-in-parts (boundary parts)
+             (loop
+                for p in parts
+                thereis (typecase p
+                          (mime-multipart
+                           (match-in-parts boundary (mime-parts p)))
+                          (mime-bodily-part
+                           (match-in-body p boundary)))))
+           (match-in-body (part boundary)
+             (with-open-stream (in (make-encoded-body-stream part))
+               (loop
+                  for line = (read-line in nil)
+                  while line
+                  when (string= line boundary)
+                  return t
+                  finally (return nil)))))
+    (do ((boundary (if default
+                       (format nil "--~A" default)
+                       #1=(format nil "--~{~36R~}"
+                                  (loop
+                                     for i from 0 below 20
+                                     collect (random 36))))
+                   #1#))
+        ((not (match-in-parts boundary parts)) (subseq boundary 2)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; fall back method
+(defmethod mime-part-size ((part mime-part))
+  (be body (mime-body part)
+    (typecase body
+      (pathname
+       (file-size body))
+      (string
+       (length body))
+      (vector
+       (length body))
+      (t nil))))
+
+(defmethod mime-part-size ((part mime-multipart))
+  (loop
+     for p in (mime-parts part)
+     for size = (mime-part-size p)
+     unless size
+     return nil
+     sum size))
+
+(defmethod mime-part-size ((part mime-message))
+  (mime-part-size (mime-body part)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmethod print-mime-part ((part mime-multipart) (out stream))
+  (case (mime-subtype part)
+    (:alternative
+     ;; try to choose something simple to print or the first thing
+     (be parts (mime-parts part)
+       (print-mime-part (or (find-if #'(lambda (part)
+                                         (and (eq (class-of part) (find-class 'mime-text))
+                                              (eq (mime-subtype part) :plain)))
+                                     parts)
+                            (car parts)) out)))
+    (otherwise
+     (dolist (subpart (mime-parts part))
+       (print-mime-part subpart out)))))
+
+;; This is WRONG.  Here we don't use any special character encoding
+;; because we don't know which one we should use.  Messages written in
+;; anything but ASCII will likely be unreadable -wcp11/10/07.
+(defmethod print-mime-part ((part mime-text) (out stream))
+  (be body (mime-body part)
+    (etypecase body
+      (string
+       (write-string body out))
+      (vector
+       (loop
+          for byte across body
+          do (write-char (code-char byte) out)))
+      (pathname
+       (with-open-file (in body)
+         (loop
+            for c = (read-char in nil)
+            while c
+            do (write-char c out)))))))
+
+(defmethod print-mime-part ((part mime-message) (out stream))
+  (flet ((hdr (name)
+           (multiple-value-bind (value tag)
+               (header name (mime-message-headers part))
+             (cons tag value))))
+    (dolist (h (mapcar #'hdr '("from" "subject" "to" "date" "x-march-archive-id")))
+      (when h
+        (format out "~&~A: ~A" (car h) (cdr h))))
+    (format out "~2%")
+    (print-mime-part (mime-body part) out)))
+
+(defmethod print-mime-part ((part mime-part) (out stream))
+  (format out "~&[ ~A subtype=~A ~@[description=~S ~]~@[size=~A~] ]~%"
+          (type-of part) (mime-subtype part) (mime-description part) (mime-part-size part)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric find-mime-part-by-path (mime path)
+  (:documentation
+   "Return a subpart of MIME identified by PATH, which is a list of
+integers.  For example '(2 3 1) is the first part of the third of the
+second in MIME."))
+
+(defmethod find-mime-part-by-path ((part mime-part) path)
+  (if (null path)
+      part
+      (error "~S doesn't have subparts" part)))
+
+(defmethod find-mime-part-by-path ((part mime-message) path)
+  (if (null path)
+      part
+      (if (= 1 (car path))
+          (find-mime-part-by-path (mime-body part) (cdr path))
+          (error "~S may have just one subpart, but part ~D was requested (parts are enumerated base 1)."
+                 part (car path)))))
+
+(defmethod find-mime-part-by-path ((part mime-multipart) path)
+  (if (null path)
+      part
+      (be parts (mime-parts part)
+          part-number (car path)
+        (if (<= 1 part-number (length parts))
+            (find-mime-part-by-path (nth (1- (car path)) (mime-parts part)) (cdr path))
+            (error "~S has just ~D subparts, but part ~D was requested (parts are enumerated base 1)."
+                   part (length parts) part-number)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric find-mime-part-by-id (part id)
+  (:documentation
+   "Return a subpart of PAR, whose Content-ID is the same as ID, which
+is a string."))
+
+(defmethod find-mime-part-by-id ((part mime-part) id)
+  (when (string= id (mime-id part))
+    part))
+
+(defmethod find-mime-part-by-id ((part mime-message) id)
+  (find-mime-part-by-id (mime-body part) id))
+
+(defmethod find-mime-part-by-id ((part mime-multipart) id)
+  (or (call-next-method)
+      (some #'(lambda (p)
+                (find-mime-part-by-id p id))
+            (mime-parts part))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmethod find-mime-text-part (msg)
+  (:documentation
+   "Return message if it is a text message or first text part.
+   If no suitable text part is found, return NIL."))
+
+(defmethod find-mime-text-part ((part mime-text))
+  part) ; found our target
+
+(defmethod find-mime-text-part ((msg mime-message))
+  ;; mime-body is either a mime-part or mime-multipart
+  (find-mime-text-part (mime-body msg)))
+
+(defmethod find-mime-text-part ((parts mime-multipart))
+  ;; multipart messages may have a body, otherwise we
+  ;; search for the first text part
+  (or (call-next-method)
+      (find-if #'find-mime-text-part (mime-parts parts))))
+
+(defmethod find-mime-text-part ((part mime-part))
+  nil) ; default case
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric mime-type-string (mime-part)
+  (:documentation
+   "Return the string describing the MIME part."))
+
+(defmethod mime-type-string ((part mime-unknown-part))
+  (mime-type part))
+
+(defmethod mime-type-string ((part mime-text))
+  (format nil "text/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-image))
+  (format nil "image/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-audio))
+  (format nil "audio/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-video))
+  (format nil "video/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-application))
+  (format nil "application/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-multipart))
+  (format nil "multipart/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-message))
+  (format nil "message/~A" (mime-subtype part)))
+
+(defmethod mime-type-string ((part mime-unknown-part))
+  (mime-type part))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric map-parts (function mime-part)
+  (:documentation
+   "Recursively map FUNCTION to MIME-PART or its components."))
+
+;; Here we wrongly assume that we'll never want to replace messages
+;; and multiparts altogether.  If you need to do so you have to write
+;; your own mapping functions.
+
+(defmethod map-parts ((function function) (part mime-part))
+  (funcall function part))
+
+(defmethod map-parts ((function function) (part mime-message))
+  (setf (mime-body part) (map-parts function (mime-body part)))
+  part)
+
+(defmethod map-parts ((function function) (part mime-multipart))
+  (setf (mime-parts part) (mapcar #'(lambda (p)
+                                      (map-parts function p))
+                                  (mime-parts part)))
+  part)
+
+;; apply-on-parts is like map-parts but doesn't modify the parts (at least
+;; not implicitly)
+
+(defgeneric apply-on-parts (function part))
+
+(defmethod apply-on-parts ((function function) (part mime-part))
+  (funcall function part))
+
+(defmethod apply-on-parts ((function function) (part mime-multipart))
+  (dolist (p (mime-parts part))
+    (apply-on-parts function p)))
+
+(defmethod apply-on-parts ((function function) (part mime-message))
+  (apply-on-parts function (mime-body part)))
+
+(defmacro do-parts ((var mime-part) &body body)
+  `(apply-on-parts #'(lambda (,var) ,@body) ,mime-part))
diff --git a/third_party/lisp/mime4cl/mime4cl-tests.asd b/third_party/lisp/mime4cl/mime4cl-tests.asd
new file mode 100644
index 0000000000..f3b429eafb
--- /dev/null
+++ b/third_party/lisp/mime4cl/mime4cl-tests.asd
@@ -0,0 +1,55 @@
+;;;  mime4cl-tests.asd --- system description for the regression tests
+
+;;;  Copyright (C) 2006, 2007, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2022 by The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+#-(or sbcl)
+(warn "This code hasn't been tested on your Lisp system.")
+
+(defpackage :mime4cl-tests-system
+  (:use :common-lisp :asdf #+asdfa :asdfa)
+  (:export #:*base-directory*
+           #:*compilation-epoch*))
+
+(in-package :mime4cl-tests-system)
+
+(defsystem mime4cl-tests
+    :name "MIME4CL-tests"
+    :author "Walter C. Pelissero <walter@pelissero.de>"
+    :maintainer "Walter C. Pelissero <walter@pelissero.de>"
+    :description "Test suite for the MIME4CL library"
+    :long-description
+    "These regression tests require rt.lisp from MIT.  It is included."
+    :licence "LGPL"
+    :depends-on (:mime4cl)
+    :components
+    ((:module test
+              :components
+              ((:file "rt")
+               (:file "package" :depends-on ("rt"))
+               (:file "endec" :depends-on ("rt" "package"))
+               (:file "address" :depends-on ("rt" "package"))
+               (:file "mime" :depends-on ("rt" "package"))))))
+
+;; when loading this form the regression-test, the package is yet to
+;; be loaded so we cannot use rt:do-tests directly or we would get a
+;; reader error (unknown package)
+(defmethod perform ((o test-op) (c (eql (find-system :mime4cl-tests))))
+  (or (funcall (intern "DO-TESTS" "REGRESSION-TEST"))
+      (error "test-op failed")))
diff --git a/third_party/lisp/mime4cl/mime4cl.asd b/third_party/lisp/mime4cl/mime4cl.asd
new file mode 100644
index 0000000000..6528f115d4
--- /dev/null
+++ b/third_party/lisp/mime4cl/mime4cl.asd
@@ -0,0 +1,49 @@
+;;;  mime4cl.asd --- system definition
+
+;;;  Copyright (C) 2005-2007, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2022 by The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 2, or (at
+;;; your option) any later version.
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; General Public License for more details.
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program; see the file COPYING.  If not, write to
+;;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+;;; Boston, MA 02111-1307, USA.
+
+(in-package :cl-user)
+
+(defpackage :mime4cl-system
+  (:use :common-lisp :asdf))
+
+(in-package :mime4cl-system)
+
+(defsystem mime4cl
+    :name "MIME4CL"
+    :author "Walter C. Pelissero <walter@pelissero.de>"
+    :maintainer "Walter C. Pelissero <walter@pelissero.de>"
+    ;; :version "0.0"
+    :description "MIME primitives for Common Lisp"
+    :long-description
+    "A collection of Common Lisp primitives to forge and handle
+MIME mail contents."
+    :licence "LGPL"
+    :depends-on (:npg :sclf :trivial-gray-streams)
+    :components
+    ((:file "package")
+     (:file "mime" :depends-on ("package" "endec" "streams"))
+     (:file "endec" :depends-on ("package"))
+     (:file "streams" :depends-on ("package" "endec"))
+     (:file "address" :depends-on ("package"))))
+
+(defmethod perform ((o test-op) (c (eql (find-system 'mime4cl))))
+  (oos 'load-op 'mime4cl-tests)
+  (oos 'test-op 'mime4cl-tests :force t))
diff --git a/third_party/lisp/mime4cl/package.lisp b/third_party/lisp/mime4cl/package.lisp
new file mode 100644
index 0000000000..5586bdc390
--- /dev/null
+++ b/third_party/lisp/mime4cl/package.lisp
@@ -0,0 +1,110 @@
+;;;  package.lisp --- package declaration
+
+;;;  Copyright (C) 2005-2007, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2022 The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :cl-user)
+
+(defpackage :mime4cl
+  (:nicknames :mime)
+  (:use :common-lisp :npg :sclf :trivial-gray-streams)
+  ;; this is stuff that comes from SCLF and clashes with CMUCL's EXT
+  ;; package
+  (:shadowing-import-from :sclf
+                          #:process-wait
+                          #:process-alive-p
+                          #:run-program)
+  (:import-from :babel :octets-to-string)
+  (:import-from :babel-encodings :get-character-encoding)
+  (:export #:*lazy-mime-decode*
+           #:print-mime-part
+           #:read-mime-message
+           #:mime-part
+           #:mime-text
+           #:mime-binary
+           #:mime-id
+           #:mime-image
+           #:mime-message
+           #:mime-multipart
+           #:mime-audio
+           #:mime-unknown-part
+           #:get-mime-disposition-parameter
+           #:get-mime-type-parameter
+           #:mime-disposition
+           #:mime-disposition-parameters
+           #:mime-encoding
+           #:mime-application
+           #:mime-video
+           #:mime-description
+           #:mime-part-size
+           #:mime-subtype
+           #:mime-body
+           #:mime-body-stream
+           #:mime-body-length
+           #:mime-parts
+           #:mime-part-p
+           #:mime-type
+           #:mime-type-string
+           #:mime-type-parameters
+           #:mime-message-headers
+           #:mime-message-header-values
+           #:mime=
+           #:find-mime-part-by-path
+           #:find-mime-part-by-id
+           #:find-mime-text-part
+           #:encode-mime-part
+           #:encode-mime-body
+           #:decode-quoted-printable-stream
+           #:decode-quoted-printable-string
+           #:encode-quoted-printable-stream
+           #:encode-quoted-printable-sequence
+           #:decode-base64-stream
+           #:decode-base64-string
+           #:encode-base64-stream
+           #:encode-base64-sequence
+           #:parse-RFC2047-text
+           #:parse-RFC822-header
+           #:read-RFC822-headers
+           #:time-RFC822-string
+           #:parse-RFC822-date
+           #:map-parts
+           #:do-parts
+           #:apply-on-parts
+           #:mime-part-file-name
+           #:mime-text-charset
+           #:with-input-from-mime-body-stream
+           ;; endec.lisp
+           #:base64-encoder
+           #:base64-decoder
+           #:null-encoder
+           #:null-decoder
+           #:byte-encoder
+           #:byte-decoder
+           #:quoted-printable-encoder
+           #:quoted-printable-decoder
+           #:encoder-write-byte
+           #:encoder-finish-output
+           #:decoder-read-byte
+           #:decoder-read-sequence
+           #:*base64-line-length*
+           #:*quoted-printable-line-length*
+           ;; address.lisp
+           #:parse-addresses #:mailboxes-only
+           #:mailbox #:mbx-description #:mbx-user #:mbx-host #:mbx-domain #:mbx-domain-name #:mbx-address
+           #:mailbox-group #:mbxg-name #:mbxg-mailboxes))
diff --git a/third_party/lisp/mime4cl/streams.lisp b/third_party/lisp/mime4cl/streams.lisp
new file mode 100644
index 0000000000..dcac6ac341
--- /dev/null
+++ b/third_party/lisp/mime4cl/streams.lisp
@@ -0,0 +1,355 @@
+;;; streams.lisp --- En/De-coding Streams
+
+;;; Copyright (C) 2012 by Walter C. Pelissero
+;;; Copyright (C) 2021-2022 by the TVL Authors
+
+;;; Author: Walter C. Pelissero <walter@pelissero.de>
+;;; Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :mime4cl)
+
+(defclass coder-stream-mixin ()
+  ((real-stream :type stream
+                :initarg :stream
+                :reader real-stream)
+   (dont-close :initform nil
+               :initarg :dont-close)))
+
+(defmethod stream-file-position ((stream coder-stream-mixin))
+  (file-position (slot-value stream 'real-stream)))
+
+(defmethod (setf stream-file-position) (newval (stream coder-stream-mixin))
+  (file-position (slot-value stream 'real-stream) newval))
+
+(defclass coder-input-stream-mixin (fundamental-binary-input-stream coder-stream-mixin)
+  ())
+(defclass coder-output-stream-mixin (fundamental-binary-output-stream coder-stream-mixin)
+  ())
+
+
+(defclass quoted-printable-decoder-stream (coder-input-stream-mixin quoted-printable-decoder) ())
+(defclass base64-decoder-stream (coder-input-stream-mixin base64-decoder) ())
+(defclass 8bit-decoder-stream (coder-input-stream-mixin 8bit-decoder) ())
+
+(defclass quoted-printable-encoder-stream (coder-output-stream-mixin quoted-printable-encoder) ())
+(defclass base64-encoder-stream (coder-output-stream-mixin base64-encoder) ())
+(defclass 8bit-encoder-stream (coder-output-stream-mixin 8bit-encoder) ())
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmethod initialize-instance :after ((stream coder-stream-mixin) &key &allow-other-keys)
+  (unless (slot-boundp stream 'real-stream)
+    (error "REAL-STREAM is unbound.  Must provide a :STREAM argument.")))
+
+(defmethod initialize-instance ((stream coder-output-stream-mixin) &key &allow-other-keys)
+  (call-next-method)
+  (unless (slot-boundp stream 'output-function)
+    (setf (slot-value stream 'output-function)
+          #'(lambda (char)
+              (write-char char (slot-value stream 'real-stream))))))
+
+(defmethod initialize-instance ((stream coder-input-stream-mixin) &key &allow-other-keys)
+  (call-next-method)
+  (unless (slot-boundp stream 'input-function)
+    (setf (slot-value stream 'input-function)
+          #'(lambda ()
+              (read-char (slot-value stream 'real-stream) nil)))))
+
+(defmethod stream-read-byte ((stream coder-input-stream-mixin))
+  (or (decoder-read-byte stream)
+      :eof))
+
+(defmethod stream-write-byte ((stream coder-output-stream-mixin) byte)
+  (encoder-write-byte stream byte))
+
+(defmethod close ((stream coder-stream-mixin) &key abort)
+  (with-slots (real-stream dont-close) stream
+    (unless dont-close
+      (close real-stream :abort abort))))
+
+(defmethod close ((stream coder-output-stream-mixin) &key abort)
+  (unless abort
+    (encoder-finish-output stream))
+  (call-next-method))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass encoder-input-stream (fundamental-character-input-stream coder-stream-mixin)
+  ((encoder)
+   (buffer-queue :initform (make-queue)))
+  (:documentation
+   "This is the base class for encoders with the direction swapped. It
+reads from REAL-STREAM a stream of bytes, encodes it and returnes it
+in a stream of character."))
+
+(defclass quoted-printable-encoder-input-stream (encoder-input-stream) ())
+(defclass base64-encoder-input-stream (encoder-input-stream) ())
+(defclass 8bit-encoder-input-stream (fundamental-character-input-stream coder-stream-mixin) ())
+
+(defmethod initialize-instance ((stream quoted-printable-encoder-input-stream) &key &allow-other-keys)
+  (call-next-method)
+  (with-slots (encoder buffer-queue) stream
+    (setf encoder
+          (make-instance 'quoted-printable-encoder
+                         :output-function #'(lambda (char)
+                                              (queue-append buffer-queue char))))))
+
+(defmethod initialize-instance ((stream base64-encoder-input-stream) &key &allow-other-keys)
+  (call-next-method)
+  (with-slots (encoder buffer-queue) stream
+    (setf encoder
+          (make-instance 'base64-encoder
+                         :output-function #'(lambda (char)
+                                              (queue-append buffer-queue char))))))
+
+(defmethod stream-read-char ((stream encoder-input-stream))
+  (with-slots (encoder buffer-queue real-stream) stream
+    (loop
+       while (queue-empty-p buffer-queue)
+       do (be byte (read-byte real-stream nil)
+            (if byte
+                (encoder-write-byte encoder byte)
+                (progn
+                  (encoder-finish-output encoder)
+                  (queue-append buffer-queue :eof)))))
+    (queue-pop buffer-queue)))
+
+
+(defmethod stream-read-char ((stream 8bit-encoder-input-stream))
+  (with-slots (real-stream) stream
+    (aif (read-byte real-stream nil)
+         (code-char it)
+         :eof)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass input-adapter-stream ()
+  ((source :initarg :source)
+   (real-stream)
+   (input-function)))
+
+(defclass binary-input-adapter-stream (fundamental-binary-input-stream input-adapter-stream) ())
+
+(defclass character-input-adapter-stream (fundamental-character-input-stream input-adapter-stream) ())
+
+(defmethod stream-element-type ((stream binary-input-adapter-stream))
+  '(unsigned-byte 8))
+
+(defmethod initialize-instance ((stream input-adapter-stream) &key &allow-other-keys)
+  (call-next-method)
+  (assert (slot-boundp stream 'source)))
+
+(defmethod initialize-instance ((stream binary-input-adapter-stream) &key &allow-other-keys)
+  (call-next-method)
+  ;; REAL-STREAM slot is set only if we are going to close it later on
+  (with-slots (source real-stream input-function) stream
+    (etypecase source
+      (string
+       (setf real-stream (make-string-input-stream source)
+             input-function #'(lambda ()
+                                (awhen (read-char real-stream nil)
+                                  (char-code it)))))
+      ((vector (unsigned-byte 8))
+       (be i 0
+         (setf input-function #'(lambda ()
+                                  (when (< i (length source))
+                                    (prog1 (aref source i)
+                                      (incf i)))))))
+      (stream
+       (assert (input-stream-p source))
+       (setf input-function (if (subtypep (stream-element-type source) 'character)
+                                #'(lambda ()
+                                    (awhen (read-char source nil)
+                                      (char-code it)))
+                                #'(lambda ()
+                                    (read-byte source nil)))))
+      (pathname
+       (setf real-stream (open source :element-type '(unsigned-byte 8))
+             input-function #'(lambda ()
+                                (read-byte real-stream nil))))
+      (file-portion
+       (setf real-stream (open-decoded-file-portion source)
+             input-function #'(lambda ()
+                                (read-byte real-stream nil)))))))
+
+(defmethod initialize-instance ((stream character-input-adapter-stream) &key &allow-other-keys)
+  (call-next-method)
+  ;; REAL-STREAM slot is set only if we are going to close later on
+  (with-slots (source real-stream input-function) stream
+    (etypecase source
+      (string
+       (setf real-stream (make-string-input-stream source)
+             input-function #'(lambda ()
+                                (read-char real-stream nil))))
+      ((vector (unsigned-byte 8))
+       (be i 0
+         (setf input-function #'(lambda ()
+                                  (when (< i (length source))
+                                    (prog1 (code-char (aref source i))
+                                      (incf i)))))))
+      (stream
+       (assert (input-stream-p source))
+       (setf input-function (if (subtypep (stream-element-type source) 'character)
+                                #'(lambda ()
+                                    (read-char source nil))
+                                #'(lambda ()
+                                    (awhen (read-byte source nil)
+                                      (code-char it))))))
+      (pathname
+       (setf real-stream (open source :element-type 'character)
+             input-function #'(lambda ()
+                                (read-char real-stream nil))))
+      (file-portion
+       (setf real-stream (open-decoded-file-portion source)
+             input-function #'(lambda ()
+                                (awhen (read-byte real-stream nil)
+                                  (code-char it))))))))
+
+(defmethod close ((stream input-adapter-stream) &key abort)
+  (when (slot-boundp stream 'real-stream)
+    (with-slots (real-stream) stream
+      (close real-stream :abort abort))))
+
+(defmethod stream-read-byte ((stream binary-input-adapter-stream))
+  (with-slots (input-function) stream
+    (or (funcall input-function)
+        :eof)))
+
+(defmethod stream-read-char ((stream character-input-adapter-stream))
+  (with-slots (input-function) stream
+    (or (funcall input-function)
+        :eof)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass delimited-input-stream (fundamental-character-input-stream coder-stream-mixin)
+  ((start-offset :initarg :start
+                 :initform 0
+                 :reader stream-start
+                 :type integer)
+   (end-offset :initarg :end
+               :initform nil
+               :reader stream-end
+               :type (or null integer))
+   (current-offset :type integer)))
+
+(defmethod print-object ((object delimited-input-stream) stream)
+  (if *print-readably*
+      (call-next-method)
+      (with-slots (start-offset end-offset) object
+        (print-unreadable-object (object stream :type t :identity t)
+          (format stream "start=~A end=~A" start-offset end-offset)))))
+
+(defun base-stream (stream)
+  (if (typep stream 'delimited-input-stream)
+      (base-stream (real-stream stream))
+      stream))
+
+(defmethod initialize-instance ((stream delimited-input-stream) &key &allow-other-keys)
+  (call-next-method)
+  (unless (slot-boundp stream 'real-stream)
+    (error "REAL-STREAM is unbound.  Must provide a :STREAM argument."))
+  (with-slots (start-offset) stream
+    (file-position stream start-offset)))
+
+(defmethod (setf stream-file-position) (newval (stream delimited-input-stream))
+  (with-slots (current-offset real-stream) stream
+    (setf current-offset newval)
+    (call-next-method)))
+
+(defmethod stream-file-position ((stream delimited-input-stream))
+  (slot-value stream 'current-offset))
+
+;; Calling file-position with SBCL on every read is quite expensive, since
+;; it will invoke lseek each time. This is so expensive that it's faster to
+;; /compute/ the amount the stream got advanced by.
+;; file-position's behavior however, is quite flexible and it behaves differently
+;; not only for different implementation, but also different streams in SBCL.
+;; Thus, we should ideally go back to file-position and try to reduce the amount
+;; of calls by using read-sequence.
+;; TODO(sterni): make decoders use read-sequence and drop offset tracking code
+(macrolet ((def-stream-read (name read-fun update-offset-form)
+             `(defmethod ,name ((stream delimited-input-stream))
+               (with-slots (real-stream end-offset current-offset) stream
+                 (let ((el (if (or (not end-offset)
+                                   (< current-offset end-offset))
+                               (or (,read-fun real-stream nil)
+                                   :eof)
+                               :eof)))
+                   (setf current-offset ,update-offset-form)
+                   el)))))
+
+  ;; Assume we are using an encoding where < 128 is one byte, in all other cases
+  ;; it's hard to guess how much file-position will increase
+  (def-stream-read stream-read-char read-char
+    (if (or (eq el :eof) (< (char-code el) 128))
+        (1+ current-offset)
+        (file-position real-stream)))
+
+  (def-stream-read stream-read-byte read-byte (1+ current-offset)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass my-string-input-stream (fundamental-character-input-stream coder-stream-mixin)
+  ((string :initarg :string
+           :reader stream-string)))
+
+(defmethod initialize-instance ((stream my-string-input-stream) &key &allow-other-keys)
+  (call-next-method)
+  (assert (slot-boundp stream 'string))
+  (with-slots (string real-stream) stream
+    (setf real-stream (make-string-input-stream string))))
+
+(defmethod stream-read-char ((stream my-string-input-stream))
+  (with-slots (real-stream) stream
+    (or (read-char real-stream nil)
+        :eof)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defstruct file-portion
+  data					;  string or a pathname
+  encoding
+  start
+  end)
+
+(defun open-file-portion (file-portion)
+  (be data (file-portion-data file-portion)
+    (etypecase data
+      (pathname
+       (be stream (open data)
+         (make-instance 'delimited-input-stream
+                        :stream stream
+                        :start (file-portion-start file-portion)
+                        :end (file-portion-end file-portion))))
+      (string
+       (make-instance 'delimited-input-stream
+                      :stream (make-string-input-stream data)
+                      :start (file-portion-start file-portion)
+                      :end (file-portion-end file-portion)))
+      (stream
+       (make-instance 'delimited-input-stream
+                      :stream data
+                      :dont-close t
+                      :start (file-portion-start file-portion)
+                      :end (file-portion-end file-portion))))))
+
+(defun open-decoded-file-portion (file-portion)
+  (make-instance (case (file-portion-encoding file-portion)
+                   (:quoted-printable 'quoted-printable-decoder-stream)
+                   (:base64 'base64-decoder-stream)
+                   (t '8bit-decoder-stream))
+                 :stream (open-file-portion file-portion)))
diff --git a/third_party/lisp/mime4cl/test/address.lisp b/third_party/lisp/mime4cl/test/address.lisp
new file mode 100644
index 0000000000..a3653985c4
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/address.lisp
@@ -0,0 +1,123 @@
+;;;  address.lisp --- tests for the e-mail address parser
+
+;;;  Copyright (C) 2007, 2009 by Walter C. Pelissero
+;;;  Copyright (C) 2022 by The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :mime4cl-tests)
+
+(defun test-parsing (string)
+  (format nil "~{~A~^, ~}" (parse-addresses string)))
+
+(deftest address-parse-simple.1
+    (test-parsing "foo@bar")
+  "foo@bar")
+
+(deftest address-parse-simple.2
+    (test-parsing "foo@bar.com")
+  "foo@bar.com")
+
+(deftest address-parse-simple.3
+    (test-parsing "foo@bar.baz.com")
+  "foo@bar.baz.com")
+
+(deftest address-parse-simple.4
+    (test-parsing "foo.ooo@bar.baz.com")
+  "foo.ooo@bar.baz.com")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest address-parse-simple-commented.1
+    (test-parsing "foo@bar (Some Comment)")
+  "\"Some Comment\" <foo@bar>")
+
+(deftest address-parse-simple-commented.2
+    (test-parsing "foo@bar (Some, Comment)")
+  "\"Some, Comment\" <foo@bar>")
+
+(deftest address-parse-simple-commented.3
+    (test-parsing "foo@bar (Some Comment (yes, indeed))")
+  "\"Some Comment (yes, indeed)\" <foo@bar>")
+
+(deftest address-parse-simple-commented.4
+    (test-parsing "foo.bar@host.complicated.domain.net (Some Comment (yes, indeed))")
+  "\"Some Comment (yes, indeed)\" <foo.bar@host.complicated.domain.net>")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest address-parse-angle.1
+    (test-parsing "<foo@bar.baz.net>")
+  "foo@bar.baz.net")
+
+(deftest address-parse-angle.2
+    (test-parsing "My far far friend <foo@bar.baz.net>")
+  "\"My far far friend\" <foo@bar.baz.net>")
+
+(deftest address-parse-angle.3
+    (test-parsing "\"someone, I don't like\" <foo@bar.baz.net>")
+  "\"someone, I don't like\" <foo@bar.baz.net>")
+
+(deftest address-parse-angle.4
+    (test-parsing "\"this could (be a comment)\" <foo@bar.net>")
+  "\"this could (be a comment)\" <foo@bar.net>")
+
+(deftest address-parse-angle.5
+    (test-parsing "don't be fooled <foo@bar.net>")
+  "\"don't be fooled\" <foo@bar.net>")
+
+(deftest address-parse-angle.6
+    (test-parsing "<foo@bar>")
+  "foo@bar")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest address-parse-domain-literal.1
+    (test-parsing "<foo@[bar]>")
+  "foo@[bar]")
+
+(deftest address-parse-domain-literal.2
+    (test-parsing "<foo@[bar.net]>")
+  "foo@[bar.net]")
+
+(deftest address-parse-domain-literal.3
+    (test-parsing "<foo@[10.0.0.2]>")
+  "foo@[10.0.0.2]")
+
+(deftest address-parse-domain-literal.4
+    (test-parsing "<foo.bar@[10.0.0.2]>")
+  "foo.bar@[10.0.0.2]")
+
+(deftest address-parse-domain-literal.5
+    (test-parsing "somewhere unkown <foo.bar@[10.0.0.2]>")
+  "\"somewhere unkown\" <foo.bar@[10.0.0.2]>")
+
+(deftest address-parse-domain-literal.6
+    (test-parsing "\"Some--One\" <foo.bar@[10.0.0.23]>")
+  "\"Some--One\" <foo.bar@[10.0.0.23]>")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest address-parse-group.1
+    (test-parsing "friends:john@bar.in.soho, jack@pub.round.the.corner, jim@[10.0.1.2];")
+  "friends: john@bar.in.soho, jack@pub.round.the.corner, jim@[10.0.1.2];")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest address-parse-mixed.1
+    (test-parsing "Foo BAR <foo@bar.com>, \"John, Smith (that one!)\" <john.smith@host.domain.org>, friends:john@bar,jack@pub;, foo.bar.baz@wow.mail.mine, dont.bark@me (Fierce Dog)")
+  "\"Foo BAR\" <foo@bar.com>, \"John, Smith (that one!)\" <john.smith@host.domain.org>, friends: john@bar, jack@pub;, foo.bar.baz@wow.mail.mine, \"Fierce Dog\" <dont.bark@me>")
diff --git a/third_party/lisp/mime4cl/test/endec.lisp b/third_party/lisp/mime4cl/test/endec.lisp
new file mode 100644
index 0000000000..5e8d43a7d4
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/endec.lisp
@@ -0,0 +1,166 @@
+;;;  endec.lisp --- test suite for the MIME encoder/decoder functions
+
+;;;  Copyright (C) 2006, 2007, 2009, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2022 by The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :mime4cl-tests)
+
+(deftest quoted-printable.1
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "Français, Español, böse, skøl"))
+  "Fran=E7ais, Espa=F1ol, b=F6se, sk=F8l")
+
+(deftest quoted-printable.2
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "Français, Español, böse, skøl")
+                                      :start 10 :end 17)
+  "Espa=F1ol")
+
+(deftest quoted-printable.3
+    (map 'string #'code-char
+         (decode-quoted-printable-string "Fran=E7ais, Espa=F1ol, b=F6se, sk=F8l"))
+  "Français, Español, böse, skøl")
+
+(deftest quoted-printable.4
+    (map 'string #'code-char
+         (decode-quoted-printable-string "Fran=E7ais, Espa=F1ol, b=F6se, sk=F8l"
+                                         :start 12 :end 21))
+  "Español")
+
+(deftest quoted-printable.5
+    (map 'string #'code-char
+         (decode-quoted-printable-string "this = wrong"))
+  "this = wrong")
+
+(deftest quoted-printable.6
+    (map 'string #'code-char
+         (decode-quoted-printable-string "this is wrong="))
+  "this is wrong=")
+
+(deftest quoted-printable.7
+    (map 'string #'code-char
+         (decode-quoted-printable-string "this is wrong=1"))
+  "this is wrong=1")
+
+(deftest quoted-printable.8
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "x = x + 1"))
+  "x =3D x + 1")
+
+(deftest quoted-printable.9
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "x = x + 1   "))
+  "x =3D x + 1  =20")
+
+(deftest quoted-printable.10
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "this string is very very very very very very very very very very very very very very very very very very very very long"))
+  "this string is very very very very very very very very very very very ve=
+ry very very very very very very very very long")
+
+(deftest quoted-printable.11
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "this string is very very                                                                                  very very long"))
+  "this string is very very                                                =
+                                  very very long")
+
+(deftest quoted-printable.12
+    (encode-quoted-printable-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                           "please read the next   
+line"))
+  "please read the next  =20
+line")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest base64.1
+    (let ((*base64-line-length* nil))
+      (encode-base64-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                   "Some random string.")))
+  "U29tZSByYW5kb20gc3RyaW5nLg==")
+
+(deftest base64.2
+    (let ((*base64-line-length* nil))
+      (encode-base64-sequence (map '(vector (unsigned-byte 8)) #'char-code
+                                   "Some random string.") :start 5 :end 11))
+  "cmFuZG9t")
+
+(deftest base64.3
+    (map 'string #'code-char
+         (decode-base64-string "U29tZSByYW5kb20gc3RyaW5nLg=="))
+  "Some random string.")
+
+(deftest base64.4
+    (map 'string #'code-char
+         (decode-base64-string "some rubbish U29tZSByYW5kb20gc3RyaW5nLg== more rubbish"
+                               :start 13 :end 41))
+  "Some random string.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest RFC2047.1
+    (parse-RFC2047-text "foo bar")
+  ("foo bar"))
+
+(defun perftest-encoder (encoder-class &optional (megs 100))
+  (declare (optimize (speed 3) (debug 0) (safety 0))
+           (type fixnum megs))
+  (with-open-file (in #P"/dev/random" :element-type '(unsigned-byte 8))
+    (let* ((meg (* 1024 1024))
+           (buffer (make-sequence '(vector (unsigned-byte 8)) meg))
+           (encoder (make-instance encoder-class
+                                   :output-function #'(lambda (c) (declare (ignore c))))))
+      (declare (type fixnum meg))
+      (time
+       (progn
+         (dotimes (x megs)
+           (read-sequence buffer in)
+           (dotimes (i meg)
+             (mime4cl:encoder-write-byte encoder (aref buffer i))))
+         (mime4cl:encoder-finish-output encoder))))))
+
+(defun perftest-decoder (decoder-class &optional (megs 100))
+  (declare (optimize (speed 3) (debug 0) (safety 0))
+           (type fixnum megs))
+  (with-open-file (in #P"/dev/random" :element-type '(unsigned-byte 8))
+    (let ((sclf:*tmp-file-defaults* (make-pathname :defaults #.(or *load-pathname* *compile-file-pathname*)
+                                                   :type "encoded-data")))
+      (sclf:with-temp-file (tmp nil :direction :io)
+        (let* ((meg (* 1024 1024))
+               (buffer (make-sequence '(vector (unsigned-byte 8)) meg))
+               (encoder-class (ecase decoder-class
+                                (mime4cl:base64-decoder 'mime4cl:base64-encoder)
+                                (mime4cl:quoted-printable-decoder 'mime4cl:quoted-printable-encoder)))
+               (encoder (make-instance encoder-class
+                                       :output-function #'(lambda (c)
+                                                            (write-char c tmp))))
+               (decoder (make-instance decoder-class
+                                       :input-function #'(lambda ()
+                                                           (read-char tmp nil)))))
+          (declare (type fixnum meg))
+          (dotimes (x megs)
+            (read-sequence buffer in)
+            (dotimes (i meg)
+              (mime4cl:encoder-write-byte encoder (aref buffer i))))
+          (mime4cl:encoder-finish-output encoder)
+          (file-position tmp 0)
+          (time
+           (loop
+              for b = (mime4cl:decoder-read-byte decoder)
+              while b)))))))
diff --git a/third_party/lisp/mime4cl/test/mime.lisp b/third_party/lisp/mime4cl/test/mime.lisp
new file mode 100644
index 0000000000..8d93978599
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/mime.lisp
@@ -0,0 +1,54 @@
+;;; mime.lisp --- MIME regression tests
+
+;;; Copyright (C) 2012 by Walter C. Pelissero
+;;; Copyright (C) 2021-2022 by the TVL Authors
+
+;;; Author: Walter C. Pelissero <walter@pelissero.de>
+;;; Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :mime4cl-tests)
+
+(defvar *samples-directory*
+  (merge-pathnames (make-pathname :directory '(:relative "samples"))
+                   #.(or *compile-file-pathname*
+                         *load-pathname*
+                         #P"")))
+
+(defvar *sample1-file* (make-pathname :defaults #.(or *compile-file-pathname*
+                                                      *load-pathname*)
+                                      :name "sample1"
+                                      :type "msg"))
+
+(deftest mime.1
+    (let* ((orig (mime-message *sample1-file*))
+           (dup (mime-message (with-output-to-string (out) (encode-mime-part orig out)))))
+      (mime= orig dup))
+  t)
+
+(deftest mime.2
+    (loop
+       for f in (directory (make-pathname :defaults *samples-directory*
+                                          :name :wild
+                                          :type "txt"))
+       do
+         (format t "~A:~%" f)
+         (finish-output)
+         (let* ((orig (mime-message f))
+                (dup (mime-message (with-output-to-string (out) (encode-mime-part orig out)))))
+           (unless (mime= orig dup)
+             (return nil)))
+       finally (return t))
+  t)
diff --git a/third_party/lisp/mime4cl/test/package.lisp b/third_party/lisp/mime4cl/test/package.lisp
new file mode 100644
index 0000000000..6da1fc8fa2
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/package.lisp
@@ -0,0 +1,27 @@
+;;;  package.lisp --- package description for the regression tests
+
+;;;  Copyright (C) 2006, 2009 by Walter C. Pelissero
+;;;  Copyright (C) 2022 by The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(cl:in-package :common-lisp)
+
+(defpackage :mime4cl-tests
+  (:use :common-lisp
+        :rtest :mime4cl)
+  (:export))
diff --git a/third_party/lisp/mime4cl/test/rt.lisp b/third_party/lisp/mime4cl/test/rt.lisp
new file mode 100644
index 0000000000..06160debbe
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/rt.lisp
@@ -0,0 +1,254 @@
+#|----------------------------------------------------------------------------|
+ | Copyright 1990 by the Massachusetts Institute of Technology, Cambridge MA. |
+ |                                                                            |
+ | Permission  to  use,  copy, modify, and distribute this software  and  its |
+ | documentation for any purpose  and without fee is hereby granted, provided |
+ | that this copyright  and  permission  notice  appear  in  all  copies  and |
+ | supporting  documentation,  and  that  the  name  of M.I.T. not be used in |
+ | advertising or  publicity  pertaining  to  distribution  of  the  software |
+ | without   specific,   written   prior   permission.      M.I.T.  makes  no |
+ | representations  about  the  suitability of this software for any purpose. |
+ | It is provided "as is" without express or implied warranty.                |
+ |                                                                            |
+ |  M.I.T. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,  INCLUDING  |
+ |  ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL  |
+ |  M.I.T. BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL  DAMAGES  OR  |
+ |  ANY  DAMAGES  WHATSOEVER  RESULTING  FROM  LOSS OF USE, DATA OR PROFITS,  |
+ |  WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER  TORTIOUS  ACTION,  |
+ |  ARISING  OUT  OF  OR  IN  CONNECTION WITH THE USE OR PERFORMANCE OF THIS  |
+ |  SOFTWARE.                                                                 |
+ |----------------------------------------------------------------------------|#
+
+(defpackage #:regression-test
+  (:nicknames #:rtest #-lispworks #:rt) 
+  (:use #:cl)
+  (:export #:*do-tests-when-defined* #:*test* #:continue-testing
+           #:deftest #:do-test #:do-tests #:get-test #:pending-tests
+           #:rem-all-tests #:rem-test)
+  (:documentation "The MIT regression tester with pfdietz's modifications"))
+
+(in-package :regression-test)
+
+(defvar *test* nil "Current test name")
+(defvar *do-tests-when-defined* nil)
+(defvar *entries* '(nil) "Test database")
+(defvar *in-test* nil "Used by TEST")
+(defvar *debug* nil "For debugging")
+(defvar *catch-errors* t
+  "When true, causes errors in a test to be caught.")
+(defvar *print-circle-on-failure* nil
+  "Failure reports are printed with *PRINT-CIRCLE* bound to this value.")
+(defvar *compile-tests* nil
+  "When true, compile the tests before running them.")
+(defvar *optimization-settings* '((safety 3)))
+(defvar *expected-failures* nil
+  "A list of test names that are expected to fail.")
+
+(defstruct (entry (:conc-name nil)
+                  (:type list))
+  pend name form)
+
+(defmacro vals (entry) `(cdddr ,entry))
+
+(defmacro defn (entry) `(cdr ,entry))
+
+(defun pending-tests ()
+  (do ((l (cdr *entries*) (cdr l))
+       (r nil))
+      ((null l) (nreverse r))
+    (when (pend (car l))
+      (push (name (car l)) r))))
+
+(defun rem-all-tests ()
+  (setq *entries* (list nil))
+  nil)
+
+(defun rem-test (&optional (name *test*))
+  (do ((l *entries* (cdr l)))
+      ((null (cdr l)) nil)
+    (when (equal (name (cadr l)) name)
+      (setf (cdr l) (cddr l))
+      (return name))))
+
+(defun get-test (&optional (name *test*))
+  (defn (get-entry name)))
+
+(defun get-entry (name)
+  (let ((entry (find name (cdr *entries*)
+                     :key #'name
+                     :test #'equal)))
+    (when (null entry)
+      (report-error t
+        "~%No test with name ~:@(~S~)."
+        name))
+    entry))
+
+(defmacro deftest (name form &rest values)
+  `(add-entry '(t ,name ,form .,values)))
+
+(defun add-entry (entry)
+  (setq entry (copy-list entry))
+  (do ((l *entries* (cdr l))) (nil)
+    (when (null (cdr l))
+      (setf (cdr l) (list entry))
+      (return nil))
+    (when (equal (name (cadr l)) 
+                 (name entry))
+      (setf (cadr l) entry)
+      (report-error nil
+        "Redefining test ~:@(~S~)"
+        (name entry))
+      (return nil)))
+  (when *do-tests-when-defined*
+    (do-entry entry))
+  (setq *test* (name entry)))
+
+(defun report-error (error? &rest args)
+  (cond (*debug* 
+         (apply #'format t args)
+         (if error? (throw '*debug* nil)))
+        (error? (apply #'error args))
+        (t (apply #'warn args))))
+
+(defun do-test (&optional (name *test*))
+  (do-entry (get-entry name)))
+
+(defun equalp-with-case (x y)
+  "Like EQUALP, but doesn't do case conversion of characters."
+  (cond
+   ((eq x y) t)
+   ((consp x)
+    (and (consp y)
+         (equalp-with-case (car x) (car y))
+         (equalp-with-case (cdr x) (cdr y))))
+   ((and (typep x 'array)
+         (= (array-rank x) 0))
+    (equalp-with-case (aref x) (aref y)))
+   ((typep x 'vector)
+    (and (typep y 'vector)
+         (let ((x-len (length x))
+               (y-len (length y)))
+           (and (eql x-len y-len)
+                (loop
+                 for e1 across x
+                 for e2 across y
+                 always (equalp-with-case e1 e2))))))
+   ((and (typep x 'array)
+         (typep y 'array)
+         (not (equal (array-dimensions x)
+                     (array-dimensions y))))
+    nil)
+   ((typep x 'array)
+    (and (typep y 'array)
+         (let ((size (array-total-size x)))
+           (loop for i from 0 below size
+                 always (equalp-with-case (row-major-aref x i)
+                                          (row-major-aref y i))))))
+   (t (eql x y))))
+
+(defun do-entry (entry &optional
+                       (s *standard-output*))
+  (catch '*in-test*
+    (setq *test* (name entry))
+    (setf (pend entry) t)
+    (let* ((*in-test* t)
+           ;; (*break-on-warnings* t)
+           (aborted nil)
+           r)
+      ;; (declare (special *break-on-warnings*))
+
+      (block aborted
+        (setf r
+              (flet ((%do
+                      ()
+                      (if *compile-tests*
+                          (multiple-value-list
+                           (funcall (compile
+                                     nil
+                                     `(lambda ()
+                                        (declare
+                                         (optimize ,@*optimization-settings*))
+                                        ,(form entry)))))
+                        (multiple-value-list
+                         (eval (form entry))))))
+                (if *catch-errors*
+                    (handler-bind
+                        ((style-warning #'muffle-warning)
+                         (error #'(lambda (c)
+                                    (setf aborted t)
+                                    (setf r (list c))
+                                    (return-from aborted nil))))
+                      (%do))
+                  (%do)))))
+
+      (setf (pend entry)
+            (or aborted
+                (not (equalp-with-case r (vals entry)))))
+      
+      (when (pend entry)
+        (let ((*print-circle* *print-circle-on-failure*))
+          (format s "~&Test ~:@(~S~) failed~
+                   ~%Form: ~S~
+                   ~%Expected value~P: ~
+                      ~{~S~^~%~17t~}~%"
+                  *test* (form entry)
+                  (length (vals entry))
+                  (vals entry))
+          (format s "Actual value~P: ~
+                      ~{~S~^~%~15t~}.~%"
+                  (length r) r)))))
+  (when (not (pend entry)) *test*))
+
+(defun continue-testing ()
+  (if *in-test*
+      (throw '*in-test* nil)
+      (do-entries *standard-output*)))
+
+(defun do-tests (&optional
+                 (out *standard-output*))
+  (dolist (entry (cdr *entries*))
+    (setf (pend entry) t))
+  (if (streamp out)
+      (do-entries out)
+      (with-open-file 
+          (stream out :direction :output)
+        (do-entries stream))))
+
+(defun do-entries (s)
+  (format s "~&Doing ~A pending test~:P ~
+             of ~A tests total.~%"
+          (count t (cdr *entries*)
+                 :key #'pend)
+          (length (cdr *entries*)))
+  (dolist (entry (cdr *entries*))
+    (when (pend entry)
+      (format s "~@[~<~%~:; ~:@(~S~)~>~]"
+              (do-entry entry s))))
+  (let ((pending (pending-tests))
+        (expected-table (make-hash-table :test #'equal)))
+    (dolist (ex *expected-failures*)
+      (setf (gethash ex expected-table) t))
+    (let ((new-failures
+           (loop for pend in pending
+                 unless (gethash pend expected-table)
+                 collect pend)))
+      (if (null pending)
+          (format s "~&No tests failed.")
+        (progn
+          (format s "~&~A out of ~A ~
+                   total tests failed: ~
+                   ~:@(~{~<~%   ~1:;~S~>~
+                         ~^, ~}~)."
+                  (length pending)
+                  (length (cdr *entries*))
+                  pending)
+          (if (null new-failures)
+              (format s "~&No unexpected failures.")
+            (when *expected-failures*
+              (format s "~&~A unexpected failures: ~
+                   ~:@(~{~<~%   ~1:;~S~>~
+                         ~^, ~}~)."
+                    (length new-failures)
+                    new-failures)))
+          ))
+      (null pending))))
diff --git a/third_party/lisp/mime4cl/test/sample1.msg b/third_party/lisp/mime4cl/test/sample1.msg
new file mode 100644
index 0000000000..662a9fab34
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/sample1.msg
@@ -0,0 +1,86 @@
+From wcp@scylla.home.lan Fri Feb 17 11:02:28 2012
+Status: RO
+X-VM-v5-Data: ([nil nil nil nil nil nil nil nil nil]
+	["1133" "Friday" "17" "February" "2012" "11:02:27" "+0100" "Walter C. Pelissero" "walter@pelissero.de" nil "56" "test" "^From:" nil nil "2" nil nil nil nil nil nil nil nil nil nil]
+	nil)
+X-Clpmr-Processed: 2012-02-17T11:02:31
+X-Clpmr-Version: 2011-10-23T12:55:20, SBCL 1.0.49
+Received: from scylla.home.lan (localhost [127.0.0.1])
+	by scylla.home.lan (8.14.5/8.14.5) with ESMTP id q1HA2Sik004513
+	for <wcp@scylla.home.lan>; Fri, 17 Feb 2012 11:02:28 +0100 (CET)
+	(envelope-from wcp@scylla.home.lan)
+Received: (from wcp@localhost)
+	by scylla.home.lan (8.14.5/8.14.5/Submit) id q1HA2SqU004512;
+	Fri, 17 Feb 2012 11:02:28 +0100 (CET)
+	(envelope-from wcp)
+Message-ID: <20286.9651.890757.323027@scylla.home.lan>
+X-Mailer: VM 8.1.1 under 23.3.1 (amd64-portbld-freebsd8.2)
+Reply-To: walter@pelissero.de
+X-Attribution: WP
+X-For-Spammers: blacklistme@pelissero.de
+X-MArch-Processing-Time: 0.552s
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="615CiWUaGO"
+Content-Transfer-Encoding: 7BIT
+From: walter@pelissero.de (Walter C. Pelissero)
+To: wcp@scylla.home.lan
+Subject: test
+Date: Fri, 17 Feb 2012 11:02:27 +0100
+
+
+--615CiWUaGO
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7BIT
+Content-Description: message body text
+
+Hereafter three attachments.
+
+The first:
+
+--615CiWUaGO
+Content-Type: application/octet-stream; name="attach1"
+Content-Transfer-Encoding: BASE64
+Content-Disposition: attachment; filename="attach1"
+
+YXR0YWNoMQo=

+
+--615CiWUaGO
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7BIT
+Content-Description: message body text
+
+
+The second:
+
+--615CiWUaGO
+Content-Type: application/octet-stream; name="attach2"
+Content-Transfer-Encoding: BASE64
+Content-Disposition: attachment; filename="attach2"
+
+YXR0YWNoMgo=

+
+--615CiWUaGO
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7BIT
+Content-Description: message body text
+
+
+The third:
+
+--615CiWUaGO
+Content-Type: application/octet-stream; name="attach3"
+Content-Transfer-Encoding: BASE64
+Content-Disposition: attachment; filename="attach3"
+
+YXR0YWNoMwo=

+
+--615CiWUaGO
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7BIT
+Content-Description: .signature
+
+
+-- 
+http://pelissero.de
+--615CiWUaGO--
+
diff --git a/third_party/lisp/moptilities.nix b/third_party/lisp/moptilities.nix
new file mode 100644
index 0000000000..d38fbcb946
--- /dev/null
+++ b/third_party/lisp/moptilities.nix
@@ -0,0 +1,13 @@
+# Compatibility layer for minor MOP implementation differences
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.moptilities;
+in depot.nix.buildLisp.library {
+  name = "moptilities";
+  deps = [ depot.third_party.lisp.closer-mop ];
+  srcs = [ "${src}/dev/moptilities.lisp" ];
+
+  brokenOn = [
+    "ecl" # TODO(sterni): https://gitlab.com/embeddable-common-lisp/ecl/-/issues/651
+  ];
+}
diff --git a/third_party/lisp/nibbles.nix b/third_party/lisp/nibbles.nix
new file mode 100644
index 0000000000..b71f439c93
--- /dev/null
+++ b/third_party/lisp/nibbles.nix
@@ -0,0 +1,26 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix.buildLisp) bundled;
+  src = with pkgs; srcOnly lispPackages.nibbles;
+in
+depot.nix.buildLisp.library {
+  name = "nibbles";
+
+  deps = with depot.third_party.lisp; [
+    (bundled "asdf")
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "types.lisp"
+    "macro-utils.lisp"
+    "vectors.lisp"
+    "streams.lisp"
+  ] ++ [
+    { sbcl = "${src}/sbcl-opt/fndb.lisp"; }
+    { sbcl = "${src}/sbcl-opt/nib-tran.lisp"; }
+    { sbcl = "${src}/sbcl-opt/x86-vm.lisp"; }
+    { sbcl = "${src}/sbcl-opt/x86-64-vm.lisp"; }
+  ];
+}
diff --git a/third_party/lisp/npg/.project b/third_party/lisp/npg/.project
new file mode 100644
index 0000000000..82a8fe48bb
--- /dev/null
+++ b/third_party/lisp/npg/.project
@@ -0,0 +1 @@
+NPG a Naive Parser Generator
diff --git a/third_party/lisp/npg/.skip-subtree b/third_party/lisp/npg/.skip-subtree
new file mode 100644
index 0000000000..5051f60d6b
--- /dev/null
+++ b/third_party/lisp/npg/.skip-subtree
@@ -0,0 +1 @@
+prevent readTree from creating entries for subdirs that don't contain an .nix files
diff --git a/third_party/lisp/npg/COPYING b/third_party/lisp/npg/COPYING
new file mode 100644
index 0000000000..223ede7de3
--- /dev/null
+++ b/third_party/lisp/npg/COPYING
@@ -0,0 +1,504 @@
+		  GNU LESSER GENERAL PUBLIC LICENSE
+		       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+		  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+			    NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/third_party/lisp/npg/OWNERS b/third_party/lisp/npg/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/third_party/lisp/npg/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/third_party/lisp/npg/README b/third_party/lisp/npg/README
new file mode 100644
index 0000000000..a1661e744a
--- /dev/null
+++ b/third_party/lisp/npg/README
@@ -0,0 +1,48 @@
+
+		     NPG a Naive Parser Generator
+			   for Common Lisp
+
+	 Copyright (C) 2003-2006, 2010 by Walter C. Pelissero
+	 Copyright (C) 2021 by the TVL Authors
+
+Vendored into depot as it is a dependency of mime4cl and upstream has
+become inactive. Upstream and depot version may diverge.
+
+Upstream Website: http://wcp.sdf-eu.org/software/#npg
+Vendored Tarball: http://wcp.sdf-eu.org/software/npg-20150517T144652.tbz
+
+This library is  free software; you can redistribute  it and/or modify
+it  under  the terms  of  the GNU  Lesser  General  Public License  as
+published by the  Free Software Foundation; either version  2.1 of the
+License,  or (at  your option)  any  later version.   This library  is
+distributed  in the  hope  that it  will  be useful,  but WITHOUT  ANY
+WARRANTY;  without even  the  implied warranty  of MERCHANTABILITY  or
+FITNESS FOR A  PARTICULAR PURPOSE.  See the GNU  Lesser General Public
+License for more details.  You should  have received a copy of the GNU
+Lesser General Public  License along with this library;  if not, write
+to the  Free Software  Foundation, Inc., 59  Temple Place,  Suite 330,
+Boston, MA 02111-1307 USA
+
+
+This library generates on the fly (no external representation of the
+parser is produced) a recursive descent parser based on the grammar
+rules you have fed it with.  The parser object can then be used to
+scan tokenised input.  Although a facility to produce a lexical
+analiser is not provided, to write such a library is fairly easy for
+most languages.  NPG parsers require your lexer to adhere to a certain
+protocol to be able to communicate with them.  Examples are provided
+that explain these requirements.
+
+While quite possibly not producing the fastest parsers in town, it's
+fairly simple and hopefully easy to debug.  It accepts a lispy EBNF
+grammar description of arbitrary complexity with the exception of
+mutually left recursive rules (watch out, they produce undetected
+infinite recursion) and produces a backtracking recursive descent
+parser.  Immediate left recursive rules are properly simplified,
+though.
+
+Multiple concurrent parsers are supported.
+
+To compile, an ASDF and nix file are provided.
+
+See the examples directory for clues on how to use it.
diff --git a/third_party/lisp/npg/default.nix b/third_party/lisp/npg/default.nix
new file mode 100644
index 0000000000..af7ec53eaf
--- /dev/null
+++ b/third_party/lisp/npg/default.nix
@@ -0,0 +1,14 @@
+# Copyright (C) 2021 by the TVL Authors
+# SPDX-License-Identifier: LGPL-2.1-or-later
+{ depot, pkgs, ... }:
+
+depot.nix.buildLisp.library {
+  name = "npg";
+
+  srcs = [
+    ./src/package.lisp
+    ./src/common.lisp
+    ./src/define.lisp
+    ./src/parser.lisp
+  ];
+}
diff --git a/third_party/lisp/npg/examples/python.lisp b/third_party/lisp/npg/examples/python.lisp
new file mode 100644
index 0000000000..a45ac614f7
--- /dev/null
+++ b/third_party/lisp/npg/examples/python.lisp
@@ -0,0 +1,336 @@
+;;;  python.lisp --- sample grammar definition for the Python language
+
+;;;  Copyright (C) 2003 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+;;;  $Id: F-C1A8CD5961889C584B22F05E8B956006.lisp,v 1.3 2004/03/09 10:33:06 wcp Exp $
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+;;;  Commentary:
+;;;
+;;; This is far from being a complete Python grammar.  Actually I
+;;; haven't even read a Python book before starting to write this
+;;; stuff, so the code below comes mostly from wild guessing while
+;;; reading a Python source file.
+;;;
+;;; It's a design decision to avoid writing any transformation in this
+;;; module; only tagging is done at this level.  This improves the
+;;; separation between parsing and transformation, making the grammar
+;;; reusable for other purposes.
+
+
+#+cmu (ext:file-comment "$Id: F-C1A8CD5961889C584B22F05E8B956006.lisp,v 1.3 2004/03/09 10:33:06 wcp Exp $")
+
+(in-package :grammar)
+
+(deflazy define-grammar
+  (let ((*package* #.*package*)
+        (*compile-print* (and parser::*debug* t)))
+    (reset-grammar)
+    (format t "~&creating Python grammar...~%")
+    (populate-grammar)
+    (let ((grammar (parser:generate-grammar)))
+      (reset-grammar)
+      (parser:print-grammar-figures grammar)
+      grammar)))
+
+(defun populate-grammar ()
+
+(defrule program
+    := comment-string? statement+)
+
+(defrule comment-string
+    := string eol
+    :reduce string)
+
+;;; BOB = Beginning Of Block, EOB = End Of Block.  It's lexical
+;;; analyzer's task to find out where a statement or block starts/ends.
+
+(defrule suite
+    := statement-list eol
+    :reduce statement-list
+    := statement-block)
+
+(defrule commentable-suite
+    := statement-list eol
+    :reduce statement-list
+    := commented-statement-block)
+
+(defrule statement-block
+    := bob statement+ eob
+    :reduce $2)
+
+(defrule commented-statement-block
+    := bob comment-string? statement* eob
+    :reduce (cons comment-string statement))
+
+(defrule statement-list
+    := (+ simple-statement ";")
+    :reduce (if (cdr $1)
+                (cons :statement-list $1)
+                (car $1)))
+
+(defrule statement
+    := statement-list eol
+    :reduce statement-list
+    := compound-statement)
+
+(defrule simple-statement
+    := import-statement
+    := raise-statement
+    := assignment
+    := function-call
+    := return-statement
+    := assert-statement
+    := pass-statement
+    := break-statement
+    := continue-statement)
+
+(defrule compound-statement
+    := class-definition
+    := method-definition
+    := try-statement
+    := if-statement
+    := while-statement
+    := for-statement)
+
+(defrule import-statement
+    := "import" (+ package-name ",")
+    :tag :import
+    := "from" package-name "import" (+ symbol-name ",")
+    :tag :import-from)
+
+(defrule package-name := identifier)
+
+(defrule symbol-name
+    := identifier
+    := "*")
+
+(defrule try-statement
+    := "try" ":" suite try-except-part* try-finally-part?
+    :tag :try)
+
+(defrule try-except-part
+    := "except" exception-subject? ":" suite)
+
+(defrule try-finally-part
+    := "finally" ":" suite)
+
+(defrule exception-subject
+    := exception-name exception-variable?)
+
+(defrule exception-variable
+    := "," identifier)
+
+(defrule exception-name := class-name)
+
+(defrule class-name := identifier)
+
+(defrule raise-statement
+    := "raise"
+    :tag :raise-same
+    := "raise" exception-name
+    :tag :raise
+    := "raise" exception-name "," expression
+    :tag :raise
+    := "raise" exception-name "(" expression ")"
+    :tag :raise)
+
+(defrule assignment
+    := (+ variable-with-optional-subscript ",") "=" more-assignment
+    :tag :set)
+
+(defrule more-assignment
+    := expression
+    := assignment)
+
+(defrule variable-with-optional-subscript
+    := variable-name subscript
+    :tag :subscript
+    := variable-name)
+
+(defrule variable-name
+    := (+ identifier ".")
+    :tag :varef)
+
+(defrule expression
+    := expression "or" expression1
+    :tag :or
+    := expression1)
+
+(defrule expression1
+    := expression1 "and" expression2
+    :tag :and
+    := expression2)
+
+(defrule expression2
+    := expression2 "==" expression3
+    :tag :equal
+    := expression2 ">=" expression3
+    :tag :more-equal
+    := expression2 "<=" expression3
+    :tag :less-equal
+    := expression2 "!=" expression3
+    :tag :not-equal
+    := expression2 ">" expression3
+    :tag :more
+    := expression2 "<" expression3
+    :tag :less
+    := expression2 "is" expression3
+    :tag :equal
+    := expression2 "is" "not" expression3
+    :tag :not-equal
+    := expression3)
+
+(defrule expression3
+    := expression3 "+" expression4
+    :tag :plus
+    := expression3 "-" expression4
+    :tag :minus
+    := expression3 "|" expression4
+    :tag :bit-or
+    := expression4)
+
+;; high priority expression
+(defrule expression4
+    := expression4 "*" expression5
+    :tag :mult
+    := expression4 "/" expression5
+    :tag :div
+    := expression4 "%" expression5
+    :tag :modulo
+    := expression4 "&" expression5
+    :tag :bit-and
+    := expression4 "in" expression5
+    :tag :in
+    := expression5)
+
+(defrule expression5
+    := "~" expression5
+    :tag :bit-not
+    := "not" expression5
+    :tag :not
+    := "(" expression ")"
+    := expression6)
+
+(defrule expression6
+    := simple-expression subscript
+    :tag :subscript
+    := simple-expression)
+
+(defrule simple-expression
+    := function-call
+    := variable-name
+    := constant
+    := string-conversion
+    := list-constructor)
+
+(defrule subscript
+    := "[" expression "]"
+    := "[" expression ":" expression "]"
+    := "[" expression ":" "]"
+    :reduce (list expression nil)
+    := "[" ":" expression "]"
+    :reduce (list nil expression))
+
+(defrule string-conversion
+    := "`" expression "`"
+    :tag :to-string)
+
+(defrule constant
+    := number
+    := string
+    := lambda-expression)
+
+(defrule number
+    := float
+    := integer)
+
+(defrule list-constructor
+    := "[" (* expression ",") "]"
+    :tag :make-list)
+
+(defrule class-definition
+    := "class" class-name superclasses? ":" commentable-suite
+    :tag :defclass)
+
+(defrule superclasses
+    := "(" class-name+ ")")
+
+(defrule method-definition
+    := "def" method-name "(" method-arguments ")" ":" commentable-suite
+    :tag :defmethod)
+
+(defrule method-arguments
+    := (* method-argument ","))
+
+(defrule method-argument
+    := identifier argument-default?)
+
+(defrule argument-default
+    := "=" expression)
+
+(defrule method-name := identifier)
+
+(defrule if-statement
+    := "if" expression ":" suite elif-part* else-part?
+    :tag :if)
+
+(defrule else-part
+    :=  "else" ":" suite)
+
+(defrule elif-part
+    := "elif" expression ":" suite)
+
+(defrule lambda-expression
+    := "lambda" method-arguments ":" expression
+    :tag :lambda)
+
+(defrule function-call
+    := (+ identifier ".") "(" (* expression ",") ")"
+    :tag :funcall)
+
+(defrule for-statement
+    := "for" identifier "in" expression ":" suite
+    :tag :do-list
+    := "for" identifier "in" "range" "(" expression "," expression ")" ":" suite
+    :tag :do-range)
+
+(defrule while-statement
+    := "while" expression ":" suite
+    :tag :while)
+
+(defrule return-statement
+    := "return" expression?
+    :tag :return)
+
+(defrule assert-statement
+    := "assert" expression "," string
+    :tag :assert)
+
+(defrule pass-statement
+    := "pass"
+    :tag :pass)
+
+(defrule break-statement
+    := "break"
+    :tag :break)
+
+(defrule continue-statement
+    := "continue"
+    :tag :continue)
+
+)					; end of POPULATE-GRAMMAR
diff --git a/third_party/lisp/npg/examples/vs-cobol-ii.lisp b/third_party/lisp/npg/examples/vs-cobol-ii.lisp
new file mode 100644
index 0000000000..9ebd45a169
--- /dev/null
+++ b/third_party/lisp/npg/examples/vs-cobol-ii.lisp
@@ -0,0 +1,1901 @@
+;;;  vs-cobol-ii.lisp --- sample grammar for VS-Cobol II
+
+;;;  Copyright (C) 2003 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+;;;  $Id: F-1D03709AEB30BA7644C1CFA2DF60FE8C.lisp,v 1.2 2004/03/09 10:33:07 wcp Exp $
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+;;;  Commentary:
+;;;
+;;; A fairly incomplete VS-Cobol II grammar fro NPG.  It's probably
+;;; not very accurate either.
+
+#+cmu (ext:file-comment "$Id: F-1D03709AEB30BA7644C1CFA2DF60FE8C.lisp,v 1.2 2004/03/09 10:33:07 wcp Exp $")
+
+(in-package :grammar)
+
+(defun make-keyword (string)
+  "Create a keyword from STRING."
+  (intern (string-upcase string) :keyword))
+
+(defun flatten-list (list)
+  "Remove one depth level in LIST."
+  (mapcan #'identity list))
+
+(deflazy define-grammar
+  (let ((*package* #.*package*)
+        (*compile-print* (and parser::*debug* t)))
+    (reset-grammar)
+    (format t "creating Cobol grammar...~%")
+    (populate-grammar)
+    (let ((grammar (parser:generate-grammar)))
+      (reset-grammar)
+      (parser:print-grammar-figures grammar)
+      grammar)))
+
+(defun populate-grammar ()
+;;;
+;;; Hereafter PP means Partial Program
+;;;
+
+#+nil
+(defrule pp--declarations
+    := identification-division environment-division? data-division? "PROCEDURE" "DIVISION" using-phrase? "." :rest)
+
+;;; We need to split the parsing of the declarations from the rest
+;;; because the declarations may change the lexical rules (ie decimal
+;;; point)
+
+(defrule pp--declarations
+    := identification-division environment-division? data-division-head-or-procedure-division-head :rest)
+
+(defrule data-division-head-or-procedure-division-head
+    := data-division-head
+    :reduce :data-division
+    := procedure-division-head
+    :reduce (list :procedure-division $1))
+
+(defrule pp--data-division
+    := data-division-content procedure-division-head :rest)
+
+(defrule pp--sentence
+    := sentence :rest
+    := :eof)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; The real grammar
+;;;
+
+(defrule cobol-source-program
+    := identification-division environment-division? data-division procedure-division end-program?)
+
+(defrule identification-division
+    := identification "DIVISION" "." program-id-cobol-source-program identification-division-content
+    :reduce program-id-cobol-source-program)
+
+(defrule priority-number
+    := integer)
+
+(defrule level-number
+    := integer)
+
+(defrule to-id-or-lit
+    := "TO" id-or-lit)
+
+(defrule inspect-by-argument
+    := variable-identifier
+    := string
+    := figurative-constant-simple)
+
+(defrule figurative-constant-simple
+    := "ZERO"
+    :reduce :zero
+    := "ZEROS"
+    :reduce :zero
+    := "ZEROES"
+    :reduce :zero
+    := "SPACE"
+    :reduce :space
+    := "SPACES"
+    :reduce :space
+    := "HIGH-VALUE"
+    :reduce :high
+    := "HIGH-VALUES"
+    :reduce :high
+    := "LOW-VALUE"
+    :reduce :low
+    := "LOW-VALUES"
+    :reduce :low
+    := "QUOTE"
+    :reduce :quote
+    := "QUOTES"
+    :reduce :quote
+    := "NULL"
+    :reduce :null
+    := "NULLS"
+    :reduce :null)
+
+(defrule write-exceptions
+    := at-end-of-page-statement-list? not-at-end-of-page-statement-list? invalid-key-statement-list? not-invalid-key-statement-list?)
+
+(defrule set-statement-phrase
+    := variable-identifier+ set-oper set-src)
+
+(defrule set-src
+    := variable-identifier
+    := literal
+    := "TRUE"
+    := "ON"
+    := "OFF")
+
+(defrule set-oper
+    := "TO"
+    :reduce :to
+    := "UP" "BY"
+    :reduce :up
+    := "DOWN" "BY"
+    :reduce :down)
+
+(defrule fce-phrase
+    := reserve-clause
+    := fce-organization
+    := fce-access-mode
+    := record-key-clause
+    := password-clause
+    := alternate-record-key-clause
+    := file-status-clause
+    := padding-character-clause
+    := record-delimiter-clause)
+
+(defrule fce-organization
+    := organization-is? alt-indexed-relative-sequential
+    :reduce (list :organization (make-keyword alt-indexed-relative-sequential)))
+
+(defrule fce-access-mode
+    := "ACCESS" "MODE"? "IS"? alt-sequential-random-dynamic relative-key-clause?
+    :reduce (list :access-mode (make-keyword alt-sequential-random-dynamic)))
+
+(defrule alt-indexed-relative-sequential
+    := "INDEXED"
+    := "RELATIVE"
+    := "SEQUENTIAL")
+
+(defrule is-not
+    := "IS"? "NOT"?)
+
+(defrule all-procedures
+    := "ALL" "PROCEDURES")
+
+(defrule next-sentence
+    := "NEXT" "SENTENCE")
+
+(defrule no-rewind
+    := "NO" "REWIND")
+
+(defrule for-removal
+    := "FOR"? "REMOVAL")
+
+(defrule values
+    := "VALUE"
+    := "VALUES")
+
+(defrule records
+    := "RECORD"
+    := "RECORDS")
+
+(defrule end-program
+    := "END" "PROGRAM" program-name ".")
+
+(defrule environment-division
+    := "ENVIRONMENT" "DIVISION" "." environment-division-content)
+
+(defrule data-division-head
+    := "DATA" "DIVISION" ".")
+
+(defrule data-division
+    := data-division-head data-division-content
+    :reduce data-division-content)
+
+(defrule identification
+    := "IDENTIFICATION"
+    := "ID")
+
+(defrule identification-division-content
+    := identification-division-phrase*)
+
+(defrule author
+    := "AUTHOR" ".")
+
+(defrule installation
+    := "INSTALLATION" ".")
+
+(defrule date-written
+    := "DATE-WRITTEN" ".")
+
+(defrule date-compiled
+    := "DATE-COMPILED" ".")
+
+(defrule security
+    := "SECURITY" ".")
+
+(defrule remarks
+    := "REMARKS" ".")
+
+(defrule identification-division-phrase
+    := author
+    := installation
+    := date-written
+    := date-compiled
+    := security
+    := remarks)
+
+(defrule program-id-cobol-source-program
+    := "PROGRAM-ID" "."? program-name initial-program? "."
+    :reduce program-name)
+
+(defrule initial-program
+    := "IS"? "INITIAL" "PROGRAM"?)
+
+(defrule environment-division-content
+    := configuration-section? input-output-section?)
+
+(defrule input-output-section
+    := "INPUT-OUTPUT" "SECTION" "." file-control-paragraph? i-o-control-paragraph?
+    :reduce file-control-paragraph)
+
+(defrule file-control-paragraph
+    := "FILE-CONTROL" "." file-control-entry*)
+
+(defrule file-control-entry
+    := select-clause assign-clause fce-phrase* "."
+    :reduce (append select-clause
+                    assign-clause
+                    (flatten-list fce-phrase)))
+
+(defrule organization-is
+    := "ORGANIZATION" "IS"?)
+
+(defrule alt-sequential-random-dynamic
+    := "SEQUENTIAL"
+    := "RANDOM"
+    := "DYNAMIC")
+
+(defrule select-clause
+    := "SELECT" "OPTIONAL"? file-name
+    :reduce (list file-name :optional (and $2 t)))
+
+(defrule assign-clause
+    := "ASSIGN" "TO"? alt-assignment-name-literal+
+    :reduce (list :assign alt-assignment-name-literal))
+
+(defrule alt-assignment-name-literal
+    := assignment-name
+    := literal)
+
+(defrule reserve-clause
+    := "RESERVE" integer areas?)
+
+(defrule areas
+    := "AREA"
+    := "AREAS")
+
+(defrule padding-character-clause
+    := "PADDING" "CHARACTER"? "IS"? alt-qualified-data-name-literal)
+
+(defrule record-delimiter-clause
+    := "RECORD" "DELIMITER" "IS"? record-delimiter-name)
+
+(defrule record-delimiter-name
+    := "STANDARD-1"
+    := assignment-name)
+
+(defrule password-clause
+    := "PASSWORD" "IS"? data-name)
+
+(defrule file-status-clause
+    := "FILE"? "STATUS" "IS"? qualified-data-name qualified-data-name?
+    :reduce (list :file-status qualified-data-name))
+
+(defrule relative-key-clause
+    := "RELATIVE" "KEY"? "IS"? qualified-data-name
+    :reduce (list :relative-key qualified-data-name))
+
+(defrule record-key-clause
+    := "RECORD" "KEY"? "IS"? qualified-data-name
+    :reduce (list :key qualified-data-name))
+
+(defrule alternate-record-key-clause
+    := "ALTERNATE" "RECORD"? "KEY"? "IS"? qualified-data-name password-clause? with-duplicates?
+    :reduce (list :alternate-key qualified-data-name with-duplicates))
+
+(defrule with-duplicates
+    := "WITH"? "DUPLICATES")
+
+(defrule i-o-control-paragraph
+    := "I-O-CONTROL" "." i-o-sam? i-o-sort-merge?)
+
+(defrule i-o-sam
+    := qsam-or-sam-or-vsam-i-o-control-entries+ ".")
+
+(defrule i-o-sort-merge
+    := sort-merge-i-o-control-entries ".")
+
+(defrule qsam-or-sam-or-vsam-i-o-control-entries
+    := qsam-or-sam-or-vsam-i-o-control-entries-1
+    := qsam-or-sam-or-vsam-i-o-control-entries-2
+    := qsam-or-sam-or-vsam-i-o-control-entries-3
+    := qsam-or-sam-or-vsam-i-o-control-entries-4)
+
+(defrule qsam-or-sam-or-vsam-i-o-control-entries-1
+    := "RERUN" "ON" alt-assignment-name-file-name "EVERY"? every-phrase "OF"? file-name)
+
+(defrule every-phrase-1
+    := integer "RECORDS")
+
+(defrule every-phrase-2
+    := "END" "OF"? alt-reel-unit)
+
+(defrule every-phrase
+    := every-phrase-1
+    := every-phrase-2)
+
+(defrule alt-assignment-name-file-name
+    := assignment-name
+    := file-name)
+
+(defrule qsam-or-sam-or-vsam-i-o-control-entries-2
+    := "SAME" "RECORD"? "AREA"? "FOR"? file-name file-name+)
+
+(defrule qsam-or-sam-or-vsam-i-o-control-entries-3
+    := "MULTIPLE" "FILE" "TAPE"? "CONTAINS"? file-name-position+)
+
+(defrule position
+    := "POSITION" integer)
+
+(defrule file-name-position
+    := file-name position?)
+
+(defrule qsam-or-sam-or-vsam-i-o-control-entries-4
+    := "APPLY" "WRITE-ONLY" "ON"? file-name+)
+
+(defrule sort-merge-i-o-control-entries
+    := rerun-on? same-area+)
+
+(defrule rerun-on
+    := "RERUN" "ON" assignment-name)
+
+(defrule record-sort
+    := "RECORD"
+    := "SORT"
+    := "SORT-MERGE")
+
+(defrule same-area
+    := "SAME" record-sort "AREA"? "FOR"? file-name file-name+)
+
+(defrule configuration-section
+    := "CONFIGURATION" "SECTION" "." configuration-section-paragraph*
+    :reduce (flatten-list configuration-section-paragraph))
+
+(defrule configuration-section-paragraph
+    := source-computer-paragraph
+    := object-computer-paragraph
+    := special-names-paragraph)
+
+(defrule source-computer-paragraph
+    := "SOURCE-COMPUTER" "." source-computer-name
+    :reduce (list :source-computer source-computer-name))
+
+(defrule with-debugging-mode
+    := "WITH"? "DEBUGGING" "MODE")
+
+(defrule source-computer-name
+    := computer-name with-debugging-mode? "."
+    :reduce computer-name)
+
+(defrule object-computer-paragraph
+    := "OBJECT-COMPUTER" "." object-computer-name
+    :reduce (list :object-computer object-computer-name))
+
+(defrule memory-size-type
+    := "WORDS"
+    := "CHARACTERS"
+    := "MODULES")
+
+(defrule memory-size
+    := "MEMORY" "SIZE"? integer memory-size-type)
+
+(defrule object-computer-name
+    := computer-name memory-size? object-computer-paragraph-sequence-phrase "."
+    :reduce computer-name)
+
+(defrule object-computer-paragraph-sequence-phrase
+    := program-collating-sequence? segment-limit?)
+
+(defrule program-collating-sequence
+    := "PROGRAM"? "COLLATING"? "SEQUENCE" "IS"? alphabet-name)
+
+(defrule segment-limit
+    := "SEGMENT-LIMIT" "IS"? priority-number)
+
+(defrule special-names-paragraph
+    := "SPECIAL-NAMES" "." special-names-paragraph-phrase* special-names-paragraph-clause* "."
+    :reduce (flatten-list special-names-paragraph-clause))
+
+(defrule is-mnemonic-name
+    := "IS"? mnemonic-name special-names-paragraph-status-phrase?)
+
+(defrule special-names-paragraph-phrase-tail
+    := is-mnemonic-name
+    := special-names-paragraph-status-phrase)
+
+(defrule special-names-paragraph-phrase
+    := environment-name special-names-paragraph-phrase-tail)
+
+(defrule special-names-paragraph-status-phrase
+    := special-names-paragraph-status-phrase-1
+    := special-names-paragraph-status-phrase-2)
+
+(defrule special-names-paragraph-status-phrase-1
+    := "ON" "STATUS"? "IS"? condition off-status?)
+
+(defrule off-status
+    := "OFF" "STATUS"? "IS"? condition)
+
+(defrule special-names-paragraph-status-phrase-2
+    := "OFF" "STATUS"? "IS"? condition on-status?)
+
+(defrule on-status
+    := "ON" "STATUS"? "IS"? condition)
+
+(defrule special-names-paragraph-clause
+    ;; := alphabet-clause
+    ;; := symbolic-characters-clause
+    := currency-sign-clause
+    := decimal-point-clause)
+
+(defrule alphabet-clause
+    := "ALPHABET" alphabet-name "IS"? alphabet-type)
+
+(defrule alphabet-type-also
+    := "ALSO" literal)
+
+(defrule alphabet-type-alsos
+    := alphabet-type-also+)
+
+(defrule alphabet-type-also-through
+    := through-literal
+    := alphabet-type-alsos)
+
+(defrule alphabet-type-other
+    := literal alphabet-type-also-through?)
+
+(defrule alphabet-type-others
+    := alphabet-type-other+)
+
+(defrule alphabet-type
+    := "STANDARD-1"
+    := "STANDARD-2"
+    := "NATIVE"
+    := "EBCDIC"
+    := alphabet-type-others)
+
+(defrule symbolic-characters-clause
+    := "SYMBOLIC" "CHARACTERS"? symbolic-character-mapping+ in-alphabet-name?)
+
+(defrule are
+    := "ARE"
+    := "IS")
+
+(defrule symbolic-character-mapping
+    := symbolic-character+ are? integer+)
+
+(defrule in-alphabet-name
+    := "IN" alphabet-name)
+
+(defrule currency-sign-clause
+    := "CURRENCY" "SIGN"? "IS"? literal
+    :reduce (list :currency-sign literal))
+
+(defrule decimal-point-clause
+    := "DECIMAL-POINT" "IS"? "COMMA"
+    :reduce (list :decimal-point #\,))
+
+(defrule data-division-content
+    := file-section? working-storage-section? linkage-section?)
+
+(defrule file-section-entry
+    := file-and-sort-description-entry data-description-entry+
+    :reduce (cons file-and-sort-description-entry data-description-entry))
+
+(defrule file-section-head
+    := "FILE" "SECTION" ".")
+
+(defrule file-section
+    := file-section-head file-section-entry*
+    :reduce $2)
+
+(defrule working-storage-section-head
+    := "WORKING-STORAGE" "SECTION" ".")
+
+(defrule working-storage-section
+    := working-storage-section-head data-description-entry*
+    :reduce $2)
+
+(defrule linkage-section-head
+    := "LINKAGE" "SECTION" ".")
+
+(defrule linkage-section
+    := linkage-section-head data-description-entry*
+    :reduce $2)
+
+(defrule file-and-sort-description-entry
+    := alt-fd-sd file-name file-and-sort-description-entry-clause* "."
+    :reduce (list (make-keyword alt-fd-sd) file-name file-and-sort-description-entry-clause))
+
+(defrule alt-fd-sd
+    := "FD"
+    := "SD")
+
+(defrule file-and-sort-description-entry-clause
+    := external-clause
+    := global-clause
+    := block-contains-clause
+    := record-clause
+    := label-records-clause
+    := value-of-clause
+    := data-records-clause
+    := linage-clause
+    := recording-mode-clause
+    := code-set-clause)
+
+(defrule integer-to
+    := integer "TO")
+
+(defrule block-contains-clause
+    := "BLOCK" "CONTAINS"? integer-to? integer alt-characters-records?)
+
+(defrule alt-characters-records
+    := "CHARACTERS"
+    := "RECORDS"
+    := "RECORD")
+
+(defrule record-clause
+    := "RECORD" record-clause-tail)
+
+(defrule depending-on
+    := "DEPENDING" "ON"? data-name)
+
+(defrule record-clause-tail-1
+    := "CONTAINS"? integer "CHARACTERS"?)
+
+(defrule record-clause-tail-2
+    := "CONTAINS"? integer "TO" integer "CHARACTERS"?)
+
+(defrule record-clause-tail-3
+    := record-varying-phrase depending-on?)
+
+(defrule record-clause-tail
+    := record-clause-tail-2
+    := record-clause-tail-1
+    := record-clause-tail-3)
+
+(defrule record-varying-phrase
+    := "IS"? "VARYING" "IN"? "SIZE"? from-integer? to-integer? "CHARACTERS"?)
+
+(defrule from-integer
+    := "FROM"? integer)
+
+(defrule to-integer
+    := "TO" integer)
+
+(defrule label-records-clause
+    := "LABEL" records-are label-records-clause-tail
+    :reduce (list :label-record label-records-clause-tail))
+
+(defrule data-names
+    := data-name+)
+
+(defrule label-records-clause-tail
+    := "STANDARD" :reduce :standard
+    := "OMITTED" :reduce :omitted
+    := data-names)
+
+(defrule value-of-clause
+    := "VALUE" "OF" value-of-clause-tail+)
+
+(defrule alt-qualified-data-name-literal
+    := qualified-data-name
+    := literal)
+
+(defrule value-of-clause-tail
+    := variable-identifier "IS"? alt-qualified-data-name-literal)
+
+(defrule data-records-clause
+    := "DATA" records-are data-name+)
+
+(defrule records-are
+    := records are?)
+
+(defrule linage-clause
+    := "LINAGE" "IS"? alt-data-name-integer "LINES"? linage-footing-phrase)
+
+(defrule linage-footing-phrase
+    := footing? lines-top? lines-bottom?)
+
+(defrule alt-data-name-integer
+    := data-name
+    := integer)
+
+(defrule footing
+    := "WITH"? "FOOTING" "AT"? alt-data-name-integer)
+
+(defrule lines-top
+    := "LINES"? "AT"? "TOP" alt-data-name-integer)
+
+(defrule lines-bottom
+    := "LINES"? "AT"? "BOTTOM" alt-data-name-integer)
+
+(defrule recording-mode-clause
+    := "RECORDING" "MODE"? "IS"? variable-identifier)
+
+(defrule code-set-clause
+    := "CODE-SET" "IS"? alphabet-name)
+
+(defrule data-description-entry
+    := level-number alt-data-name-filler? data-description-entry-clause* "."
+    :reduce (append (list level-number alt-data-name-filler)
+                    (flatten-list data-description-entry-clause)))
+
+(defrule alt-data-name-filler
+    := data-name
+    := "FILLER"
+    :reduce (list))
+
+(defrule data-description-entry-clause
+    := picture-clause
+    := redefines-clause
+    := blank-when-zero-clause
+    := external-clause
+    := global-clause
+    := justified-clause
+    := occurs-clause
+    := sign-clause
+    := synchronized-clause
+    := usage-clause
+    := renames-clause
+    := value-clause)
+
+(defrule value-clause
+    := "VALUE" "IS"? literal
+    :reduce (list :value literal))
+
+(defrule redefines-clause
+    := "REDEFINES" data-name
+    :reduce `(:redefines ,data-name))
+
+(defrule blank-when-zero-clause
+    := "BLANK" "WHEN"? zeroes
+    :reduce '(:blank-when-zero t))
+
+(defrule zeroes
+    := "ZERO"
+    := "ZEROS"
+    := "ZEROES")
+
+(defrule external-clause
+    := "IS"? "EXTERNAL"
+    :reduce '(:external t))
+
+(defrule global-clause
+    := "IS"? "GLOBAL"
+    :reduce '(:global t))
+
+(defrule justified-clause
+    := justified "RIGHT"?
+    :reduce `(:justified ,(if $2 :right :left)))
+
+(defrule justified
+    := "JUSTIFIED"
+    := "JUST")
+
+(defrule occurs-clause
+    := "OCCURS" integer "TIMES"? occurs-clause-key* indexed-by?
+    ;; to be completed -wcp16/7/03.
+    :reduce `(:times ,integer)
+    := "OCCURS" integer "TO" integer "TIMES"? "DEPENDING" "ON"? qualified-data-name occurs-clause-key* indexed-by?
+    ;; to be completed -wcp16/7/03.
+    :reduce `(:times (,integer ,integer2 ,qualified-data-name)))
+
+(defrule occurs-clause-key
+    := alt-ascending-descending "KEY"? "IS"? qualified-data-name+)
+
+(defrule indexed-by
+    := "INDEXED" "BY"? index-name+)
+
+(defrule picture-clause
+    := picture "IS"? picture-string
+    :reduce `(:picture ,picture-string))
+
+(defrule picture
+    := "PICTURE"
+    := "PIC")
+
+(defrule sign-clause
+    := sign-is? alt-leading-trailing separate-character?
+    :reduce `(:separate-sign ,separate-character :sign-position ,alt-leading-trailing))
+
+(defrule sign-is
+    := "SIGN" "IS"?)
+
+(defrule separate-character
+    := "SEPARATE" "CHARACTER"?
+    :reduce t)
+
+(defrule alt-leading-trailing
+    := "LEADING"
+    :reduce :leading
+    := "TRAILING"
+    :reduce :trailing)
+
+(defrule synchronized-clause
+    := synchronized alt-left-right?
+    :reduce `(:synchronized ,(if alt-left-right
+                                 alt-left-right
+                                 t)))
+
+(defrule alt-left-right
+    := "LEFT"
+    :reduce :left
+    := "RIGHT"
+    :reduce :right)
+
+(defrule synchronized
+    := "SYNCHRONIZED"
+    := "SYNC")
+
+(defrule usage-clause
+    := usage-is? usage
+    :reduce (list :encoding usage))
+
+(defrule usage-is
+    := "USAGE" "IS"?)
+
+(defrule usage
+    := "BINARY"
+    :reduce :binary
+    := "COMP"
+    :reduce :comp
+    := "COMP-1"
+    :reduce :comp1
+    := "COMP-2"
+    :reduce :comp2
+    := "COMP-3"
+    :reduce :comp3
+    := "COMP-4"
+    :reduce :comp4
+    := "COMPUTATIONAL"
+    :reduce :comp
+    := "COMPUTATIONAL-1"
+    :reduce :comp1
+    := "COMPUTATIONAL-2"
+    :reduce :comp2
+    := "COMPUTATIONAL-3"
+    :reduce :comp3
+    := "COMPUTATIONAL-4"
+    :reduce :comp4
+    := "DISPLAY"
+    :reduce :display
+    := "DISPLAY-1"
+    :reduce :display1
+    := "INDEX"
+    :reduce :index
+    := "PACKED-DECIMAL"
+    :reduce :packed-decimal
+    := "POINTER"
+    :reduce :pointer)
+
+(defrule renames-clause
+    := "RENAMES" qualified-data-name through-qualified-data-name?
+    :reduce `(:renames ,qualified-data-name ,through-qualified-data-name))
+
+(defrule through-qualified-data-name
+    := through qualified-data-name
+    :reduce qualified-data-name)
+
+(defrule condition-value-clause
+    := values-are literal-through-literal+)
+
+(defrule through-literal
+    := through literal)
+
+(defrule literal-through-literal
+    := literal through-literal?)
+
+(defrule values-are
+    := values are?)
+
+(defrule procedure-division-head
+    := "PROCEDURE" "DIVISION" using-phrase? ".")
+
+(defrule procedure-division
+    := procedure-division-head sentence+)
+
+(defrule using-phrase
+    := "USING" data-name+)
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(defrule declaratives
+    := "DECLARATIVES" "." declaratives-content+ "END" "DECLARATIVES" ".")
+
+(defrule declaratives-content
+    := cobol-identifier "SECTION" "." use-statement "." sentence*)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(defrule paragraph-header
+    := cobol-identifier "SECTION"?
+    :reduce (list (if $2 :section :label) $1))
+
+(defrule sentence
+    := declaratives
+    := statement* "."
+    :reduce $1
+    := paragraph-header "."
+    :reduce $1)
+
+(defrule statement
+    := move-statement
+    := if-statement
+    := perform-statement
+    := go-to-statement
+    := accept-statement
+    := add-statement
+    := alter-statement
+    := call-statement
+    := cancel-statement
+    := close-statement
+    := compute-statement
+    := continue-statement
+    := delete-statement
+    := display-statement
+    := divide-statement
+    := entry-statement
+    := evaluate-statement
+    := exit-program-statement
+    := exit-statement
+    := goback-statement
+    := initialize-statement
+    := inspect-statement
+    := merge-statement
+    := multiply-statement
+    := open-statement
+    := read-statement
+    := release-statement
+    := return-statement
+    := rewrite-statement
+    := search-statement
+    := set-statement
+    := sort-statement
+    := start-statement
+    := stop-statement
+    := string-statement
+    := subtract-statement
+    := unstring-statement
+    := write-statement
+    := paragraph-header)
+
+(defrule accept-statement
+    := "ACCEPT" variable-identifier "FROM" date
+    := "ACCEPT" variable-identifier "AT" screen-coordinates
+    :reduce (apply #'list 'accept-at variable-identifier screen-coordinates)
+    := "ACCEPT" variable-identifier from-environment-name?)
+
+(defrule from-environment-name
+    := "FROM" cobol-identifier)
+
+
+(defrule date
+    := "DATE"
+    := "DAY"
+    := "DAY-OF-WEEK"
+    := "TIME")
+
+(defrule add-statement
+    := "ADD" id-or-lit+ to-id-or-lit? "GIVING" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-ADD"?
+    := "ADD" id-or-lit+ "TO" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-ADD"?
+    := "ADD" corresponding variable-identifier "TO" variable-identifier "ROUNDED"? on-size-error-statement-list? not-on-size-error-statement-list? "END-ADD"?)
+
+(defrule statement-list
+    := statement+)
+
+(defrule alter-statement
+    := "ALTER" procedure-to-procedure+)
+
+(defrule proceed-to
+    := "PROCEED" "TO")
+
+(defrule procedure-to-procedure
+    := procedure-name "TO" proceed-to? procedure-name)
+
+(defrule call-statement
+    := "CALL" id-or-lit using-parameters? call-rest-phrase "END-CALL"?
+    :reduce (list 'call id-or-lit (cons 'list using-parameters)))
+
+(defrule by-reference
+    := "BY"? "REFERENCE")
+
+(defrule content-parameter-value
+    := cobol-identifier
+    := literal)
+
+(defrule reference-parameter
+    := by-reference? variable-identifier)
+
+(defrule content-parameter
+    := "BY"? "CONTENT" content-parameter-value+)
+
+(defrule parameter
+    := reference-parameter
+    := content-parameter
+    := literal)
+
+(defrule using-parameters
+    := "USING" parameter+)
+
+(defrule call-rest-phrase
+    := on-exception-statement-list? not-on-exception-statement-list? on-overflow-statement-list?)
+
+(defrule on-exception-statement-list
+    := "ON"? "EXCEPTION" statement-list)
+
+(defrule not-on-exception-statement-list
+    := "NOT" "ON"? "EXCEPTION" statement-list)
+
+(defrule cancel-statement
+    := "CANCEL" id-or-lit+)
+
+(defrule close-statement
+    := "CLOSE" close-statement-file-name+
+    :reduce (list 'close close-statement-file-name))
+
+(defrule alt-removal-no-rewind
+    := for-removal
+    := with-no-rewind)
+
+(defrule alt-reel-unit
+    := "REEL"
+    := "UNIT")
+
+(defrule alt-no-rewind-lock
+    := no-rewind
+    := "LOCK")
+
+(defrule close-statement-options-1
+    := alt-reel-unit alt-removal-no-rewind?)
+
+(defrule close-statement-options-2
+    := "WITH"? alt-no-rewind-lock)
+
+(defrule close-statement-options
+    := close-statement-options-1
+    := close-statement-options-2)
+
+(defrule close-statement-file-name
+    := file-name close-statement-options?)
+
+(defrule compute-statement
+    := "COMPUTE" cobword-rounded+ equal arithmetic-expression on-size-error-statement-list? not-on-size-error-statement-list? "END-COMPUTE"?
+    :reduce (list 'compute cobword-rounded arithmetic-expression :on-size-error on-size-error-statement-list
+                  :not-on-size-error not-on-size-error-statement-list))
+
+(defrule equal
+    := "="
+    := "EQUAL")
+
+(defrule continue-statement
+    := "CONTINUE")
+
+(defrule delete-statement
+    := "DELETE" file-name "RECORD"? invalid-key-statement-list? not-invalid-key-statement-list? "END-DELETE"?
+    :reduce (list 'delete file-name :invalid invalid-key-statement-list :not-invalid not-invalid-key-statement-list))
+
+(defrule display-statement
+    := "DISPLAY" id-or-lit+ upon-environment-name? with-no-advancing?
+    :reduce (list 'display (cons 'list id-or-lit) :upon upon-environment-name :advance (not with-no-advancing))
+    := "DISPLAY" id-or-lit "AT" screen-coordinates
+    :reduce (apply #'list 'display-at id-or-lit screen-coordinates))
+
+(defrule screen-coordinates
+    := integer
+    :reduce (multiple-value-list (truncate integer 100)))
+
+(defrule upon-environment-name
+    := "UPON" cobol-identifier)
+
+(defrule with-no-advancing
+    := "WITH"? "NO" "ADVANCING")
+
+(defrule divide-statement
+    := "DIVIDE" id-or-lit "INTO" id-or-lit "GIVING" variable-identifier "ROUNDED"? "REMAINDER" variable-identifier on-size-error-statement-list? not-on-size-error-statement-list? "END-DIVIDE"?
+    := "DIVIDE" id-or-lit "BY" id-or-lit "GIVING" variable-identifier "ROUNDED"? "REMAINDER" variable-identifier on-size-error-statement-list? not-on-size-error-statement-list? "END-DIVIDE"?
+    := "DIVIDE" id-or-lit "INTO" id-or-lit "GIVING" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-DIVIDE"?
+    := "DIVIDE" id-or-lit "BY" id-or-lit "GIVING" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-DIVIDE"?
+    := "DIVIDE" id-or-lit "INTO" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-DIVIDE"?)
+
+(defrule entry-statement
+    := "ENTRY" literal using-phrase?)
+
+(defrule evaluate-statement
+    := "EVALUATE" evaluate-condition also-phrase* when-phrases+ when-other-phrase? "END-EVALUATE"?)
+
+(defrule evaluate-condition
+    := condition
+    := "TRUE"
+    := "FALSE")
+
+(defrule also-phrase
+    := "ALSO" evaluate-condition)
+
+(defrule when-phrase-also-phrase
+    := "ALSO" evaluate-phrase)
+
+(defrule when-phrase
+    := "WHEN" evaluate-phrase when-phrase-also-phrase*)
+
+(defrule when-phrases
+    := when-phrase+ statement-list)
+
+(defrule when-other-phrase
+    := "WHEN" "OTHER" statement-list)
+
+(defrule evaluate-phrase
+    := "ANY"
+    := condition
+    := "TRUE"
+    := "FALSE"
+    := evaluate-phrase-1)
+
+(defrule evaluate-phrase-1
+    := "NOT"? arithmetic-expression through-arithmetic-expression?)
+
+(defrule through-arithmetic-expression
+    := through arithmetic-expression)
+
+(defrule exit-statement
+    := "EXIT"
+    :reduce '(exit-paragraph))
+
+(defrule exit-program-statement
+    := "EXIT" "PROGRAM"
+    :reduce '(exit-program))
+
+(defrule goback-statement
+    := "GOBACK"
+    :reduce '(go-back))
+
+(defrule go-to-statement
+    := "GO" "TO"? procedure-name+ "DEPENDING" "ON"? variable-identifier
+    :reduce (list 'goto-depending variable-identifier procedure-name)
+    := "GO" "TO"? procedure-name
+    :reduce (list 'goto procedure-name))
+
+(defrule if-phrase
+    := "IF" condition "THEN"? alt-statement-list-next-sentence "ELSE" alt-statement-list-next-sentence
+    :reduce (list 'if condition
+                  (if (cdr alt-statement-list-next-sentence)
+                      (cons 'progn alt-statement-list-next-sentence)
+                      (car alt-statement-list-next-sentence))
+                  (if (cdr alt-statement-list-next-sentence2)
+                      (cons 'progn alt-statement-list-next-sentence2)
+                      (car alt-statement-list-next-sentence2)))
+    := "IF" condition "THEN"? alt-statement-list-next-sentence
+    :reduce (append (list 'when condition) alt-statement-list-next-sentence))
+
+(defrule if-statement
+    := if-phrase "END-IF"?
+    :reduce $1)
+
+(defrule initialize-statement
+    := "INITIALIZE" variable-identifier+ initialize-replacing-phrase?)
+
+(defrule initialize-replacing-type
+    := "ALPHABETIC"
+    := "ALPHANUMERIC"
+    := "NUMERIC"
+    := "ALPHANUMERIC-EDITED"
+    := "NUMERIC-EDITED"
+    := "DBCS"
+    := "EGCS")
+
+(defrule initialize-replacing-argument
+    := initialize-replacing-type "DATA"? "BY" id-or-lit)
+
+(defrule initialize-replacing-phrase
+    := "REPLACING" initialize-replacing-argument+)
+
+(defrule inspect-statement
+    := inspect-statement-1
+    := inspect-statement-2
+    := inspect-statement-3
+    := inspect-statement-4)
+
+(defrule inspect-statement-1
+    := "INSPECT" variable-identifier "TALLYING" tallying-argument+)
+
+(defrule inspect-statement-2
+    := "INSPECT" variable-identifier "CONVERTING" id-or-lit "TO" id-or-lit before-after-phrase*)
+
+(defrule inspect-statement-3
+    := "INSPECT" variable-identifier "TALLYING" tallying-argument+ "REPLACING" inspect-replacing-phrase+)
+
+(defrule tallying-for-id-or-lit
+    := id-or-lit before-after-phrase*)
+
+(defrule alt-all-leading
+    := "ALL"
+    := "LEADING")
+
+(defrule tallying-for-argument-1
+    := "CHARACTERS" before-after-phrase*)
+
+(defrule tallying-for-argument-2
+    := alt-all-leading tallying-for-id-or-lit+)
+
+(defrule tallying-for-argument
+    := tallying-for-argument-1
+    := tallying-for-argument-2)
+
+(defrule tallying-argument
+    := variable-identifier "FOR" tallying-for-argument+)
+
+(defrule inspect-statement-4
+    := "INSPECT" variable-identifier "REPLACING" inspect-replacing-phrase+)
+
+(defrule inspect-replacing-argument
+    := inspect-by-argument "BY" inspect-by-argument before-after-phrase*)
+
+(defrule alt-all-leading-first
+    := "ALL"
+    := "LEADING"
+    := "FIRST")
+
+(defrule inspect-replacing-phrase-1
+    := "CHARACTERS" "BY" id-or-lit before-after-phrase*)
+
+(defrule inspect-replacing-phrase-2
+    := alt-all-leading-first inspect-replacing-argument+)
+
+(defrule inspect-replacing-phrase
+    := inspect-replacing-phrase-1
+    := inspect-replacing-phrase-2)
+
+(defrule before-after-phrase
+    := alt-before-after "INITIAL"? id-or-lit)
+
+(defrule merge-statement
+    := "MERGE" file-name on-key-phrase+ collating-sequence? "USING" file-name file-name+ merge-statement-tail)
+
+(defrule on-key-phrase
+    := "ON"? alt-ascending-descending "KEY"? qualified-data-name+)
+
+(defrule merge-statement-tail
+    := output-procedure
+    := giving-file-names)
+
+(defrule move-statement
+    := "MOVE" id-or-lit "TO" variable-identifier+
+    :reduce (apply #'list 'move id-or-lit variable-identifier)
+    := "MOVE" corresponding variable-identifier "TO" variable-identifier+
+    :reduce (apply #'list 'move-corresponding variable-identifier variable-identifier2))
+
+(defrule multiply-statement
+    := "MULTIPLY" id-or-lit "BY" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-MULTIPLY"?
+    :reduce (list 'multiply id-or-lit cobword-rounded :on-size-error on-size-error-statement-list
+                  :not-on-size-error not-on-size-error-statement-list)
+    := "MULTIPLY" id-or-lit "BY" id-or-lit "GIVING" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-MULTIPLY"?
+    :reduce (list 'multiply id-or-lit id-or-lit2 :giving cobword-rounded
+                  :on-size-error on-size-error-statement-list
+                  :not-on-size-error not-on-size-error-statement-list))
+
+(defrule open-statement
+    := "OPEN" open-statement-phrase+
+    :reduce (list 'open open-statement-phrase))
+
+(defrule alt-reversed-with-no-rewind
+    := "REVERSED"
+    := with-no-rewind)
+
+(defrule open-statement-input-file-name
+    := file-name alt-reversed-with-no-rewind?)
+
+(defrule with-no-rewind
+    := "WITH"? "NO" "REWIND")
+
+(defrule open-statement-output-file-name
+    := file-name with-no-rewind?)
+
+(defrule open-statement-input
+    := "INPUT" open-statement-input-file-name+)
+
+(defrule open-statement-output
+    := "OUTPUT" open-statement-output-file-name+)
+
+(defrule open-statement-i-o
+    := "I-O" file-name+)
+
+(defrule open-statement-extend
+    := "EXTEND" file-name+)
+
+(defrule open-statement-phrase
+    := open-statement-input
+    := open-statement-output
+    := open-statement-i-o
+    := open-statement-extend)
+
+(defrule perform-statement
+    := "PERFORM" procedure-name through-procedure-name? perform-until-phrase
+    :reduce `(perform-until ,procedure-name ,through-procedure-name ,perform-until-phrase)
+    := "PERFORM" procedure-name through-procedure-name? perform-varying-phrase perform-after-phrase*
+    :reduce `(perform-varying ,perform-varying-phrase ,procedure-name ,through-procedure-name ,perform-after-phrase)
+    := "PERFORM" procedure-name through-procedure-name? cobword-int "TIMES"
+    :reduce `(perform-times ,cobword-int ,procedure-name ,through-procedure-name)
+    := "PERFORM" procedure-name through-procedure-name?
+    :reduce (append (list 'perform procedure-name) through-procedure-name))
+
+(defrule perform-varying-phrase
+    := with-test? "VARYING" variable-identifier "FROM" id-or-lit "BY" id-or-lit "UNTIL" condition)
+
+(defrule perform-after-phrase
+    := "AFTER" variable-identifier "FROM" id-or-lit "BY" id-or-lit "UNTIL" condition)
+
+(defrule perform-until-phrase
+    := with-test? "UNTIL" condition)
+
+(defrule with-test
+    := "WITH"? "TEST" alt-before-after
+    :reduce alt-before-after)
+
+(defrule read-statement
+    := "READ" file-name "NEXT"? "RECORD"? into-identifier? key-is-qualified-data-name? invalid-key-statement-list? not-invalid-key-statement-list? at-end-statement-list? not-at-end-statement-list? "END-READ"?)
+
+(defrule key-is-qualified-data-name
+    := "KEY" "IS"? qualified-data-name)
+
+(defrule release-statement
+    := "RELEASE" record-name from-identifier?)
+
+(defrule return-statement
+    := "RETURN" file-name "RECORD"? into-identifier? "AT"? "END" statement-list not-at-end-statement-list? "END-RETURN"?)
+
+(defrule into-identifier
+    := "INTO" variable-identifier)
+
+(defrule not-at-end-statement-list
+    := "NOT" "AT"? "END" statement-list)
+
+(defrule rewrite-statement
+    := "REWRITE" record-name from-identifier? invalid-key-statement-list? not-invalid-key-statement-list? "END-REWRITE"?)
+
+(defrule search-statement
+    := search-statement-1
+    := search-statement-2)
+
+(defrule search-statement-1
+    := "SEARCH" cobol-identifier varying-identifier? at-end-statement-list? when-condition-stats+ "END-SEARCH"?)
+
+(defrule varying-identifier
+    := "VARYING" variable-identifier)
+
+(defrule when-condition-stats
+    := "WHEN" condition alt-statement-list-next-sentence)
+
+(defrule search-statement-2
+    := "SEARCH" "ALL" variable-identifier at-end-statement-list? "WHEN" search-statement-condition search-statement-condition-tail* alt-statement-list-next-sentence "END-SEARCH"?)
+
+(defrule at-end-statement-list
+    := "AT"? "END" statement-list)
+
+(defrule search-statement-equal-expression
+    := variable-identifier "IS"? equal-to arithmetic-expression
+    :reduce (list '= variable-identifier arithmetic-expression))
+
+(defrule search-statement-condition
+    := search-statement-equal-expression
+    := condition-name-reference)
+
+(defrule search-statement-condition-tail
+    := "AND" search-statement-condition)
+
+(defrule alt-statement-list-next-sentence
+    := statement+
+    := next-sentence
+    :reduce :next-sentence)
+
+(defrule set-statement
+    := "SET" set-statement-phrase+)
+
+(defrule sort-statement
+    := "SORT" file-name on-key-is-phrase+ with-duplicates-in-order? collating-sequence? sort-statement-in sort-statement-out)
+
+(defrule key-is
+    := "KEY" "IS"?)
+
+(defrule alt-ascending-descending
+    := "ASCENDING"
+    := "DESCENDING")
+
+(defrule on-key-is-phrase
+    := "ON"? alt-ascending-descending key-is? qualified-data-name+)
+
+(defrule with-duplicates-in-order
+    := "WITH"? "DUPLICATES" "IN"? "ORDER"?)
+
+(defrule collating-sequence
+    := "COLLATING"? "SEQUENCE" "IS"? alphabet-name)
+
+(defrule through
+    := "THROUGH"
+    := "THRU")
+
+(defrule through-procedure-name
+    := through procedure-name
+    :reduce procedure-name)
+
+(defrule using-file-names
+    := "USING" file-name+)
+
+(defrule input-procedure
+    := "INPUT" "PROCEDURE" "IS"? procedure-name through-procedure-name?)
+
+(defrule giving-file-names
+    := "GIVING" file-name+)
+
+(defrule output-procedure
+    := "OUTPUT" "PROCEDURE" "IS"? procedure-name through-procedure-name?)
+
+(defrule sort-statement-in
+    := using-file-names
+    := input-procedure)
+
+(defrule sort-statement-out
+    := giving-file-names
+    := output-procedure)
+
+(defrule start-statement
+    := "START" file-name key-is-rel-op-qualified-data-name? invalid-key-statement-list? not-invalid-key-statement-list? "END-START"?)
+
+(defrule rel-op
+    := equal-to
+    :reduce '=
+    := greater-than
+    :reduce '>
+    := greater-equal
+    :reduce '>=)
+
+(defrule key-is-rel-op-qualified-data-name
+    := "KEY" "IS"? rel-op qualified-data-name
+    :reduce (list rel-op qualified-data-name))
+
+(defrule stop-statement
+    := "STOP" alt-run-literal
+    :reduce '(stop))
+
+(defrule alt-run-literal
+    := "RUN"
+    := literal)
+
+(defrule string-statement
+    := "STRING" delimited-by-phrase+ "INTO" variable-identifier with-pointer-identifier? on-overflow-statement-list? not-on-overflow-statement-list? "END-STRING"?
+    :reduce (list 'string-concat delimited-by-phrase variable-identifier :with-pointer with-pointer-identifier :on-overflow on-overflow-statement-list :not-on-overflow not-on-overflow-statement-list))
+
+(defrule id-or-lit-size
+    := literal
+    := variable-identifier
+    := "SIZE")
+
+(defrule delimited-by-phrase
+    := id-or-lit+ "DELIMITED" "BY"? id-or-lit-size
+    :reduce (list id-or-lit id-or-lit-size))
+
+(defrule subtract-statement
+    := "SUBTRACT" id-or-lit+ "FROM" id-or-lit "GIVING" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-SUBTRACT"?
+    :reduce (list 'subtract-giving id-or-lit id-or-lit2 cobword-rounded
+                  :on-size-error on-size-error-statement-list
+                  :not-on-size-error not-on-size-error-statement-list)
+    := "SUBTRACT" id-or-lit+ "FROM" cobword-rounded+ on-size-error-statement-list? not-on-size-error-statement-list? "END-SUBTRACT"?
+    :reduce (list 'subtract id-or-lit cobword-rounded
+                  :on-size-error on-size-error-statement-list
+                  :not-on-size-error not-on-size-error-statement-list)
+    := "SUBTRACT" corresponding variable-identifier "FROM" variable-identifier "ROUNDED"? on-size-error-statement-list? not-on-size-error-statement-list? "END-SUBTRACT"?
+    :reduce (list 'subtract-corr variable-identifier variable-identifier
+                  :rounded (and $5 t)
+                  :on-size-error on-size-error-statement-list
+                  :not-on-size-error not-on-size-error-statement-list))
+
+(defrule cobword-rounded
+    := variable-identifier "ROUNDED"?
+    :reduce (list variable-identifier (and $2 t)))
+
+(defrule on-size-error-statement-list
+    := "ON"? "SIZE" "ERROR" statement-list
+    :reduce statement-list)
+
+(defrule not-on-size-error-statement-list
+    := "NOT" "ON"? "SIZE" "ERROR" statement-list
+    :reduce statement-list)
+
+(defrule corresponding
+    := "CORRESPONDING"
+    := "CORR")
+
+(defrule unstring-statement
+    := "UNSTRING" variable-identifier delimited-by-all-phrase? "INTO" unstring-statement-dst+ with-pointer-identifier? tallying-in-identifier? on-overflow-statement-list? not-on-overflow-statement-list? "END-UNSTRING"?
+    :reduce (list 'unstring variable-identifier unstring-statement-dst
+                  :delimited-by-all delimited-by-all-phrase
+                  :with-pointer with-pointer-identifier
+                  :tallying tallying-in-identifier
+                  :on-overflow on-overflow-statement-list
+                  :not-on-overflow not-on-overflow-statement-list))
+
+(defrule id-or-lit
+    := literal
+    := variable-identifier)
+
+(defrule or-all-id-or-lit
+    := "OR" "ALL"? id-or-lit)
+
+(defrule delimited-by-all-phrase
+    := "DELIMITED" "BY"? "ALL"? id-or-lit or-all-id-or-lit*)
+
+(defrule delimiter-in-identifier
+    := "DELIMITER" "IN"? variable-identifier)
+
+(defrule count-in-identifier
+    := "COUNT" "IN"? variable-identifier)
+
+(defrule unstring-statement-dst
+    := variable-identifier delimiter-in-identifier? count-in-identifier?)
+
+(defrule with-pointer-identifier
+    := "WITH"? "POINTER" variable-identifier)
+
+(defrule tallying-in-identifier
+    := "TALLYING" "IN"? variable-identifier)
+
+(defrule on-overflow-statement-list
+    := "ON"? "OVERFLOW" statement-list)
+
+(defrule not-on-overflow-statement-list
+    := "NOT" "ON"? "OVERFLOW" statement-list)
+
+(defrule write-statement
+    := "WRITE" record-name from-identifier? advancing-phrase? write-exceptions "END-WRITE"?)
+
+(defrule lines
+    := "LINE"
+    := "LINES")
+
+(defrule cobword-int
+    := cobol-identifier
+    := integer)
+
+(defrule nr-lines-phrase
+    := cobword-int lines?)
+
+(defrule page-phrase
+    := nr-lines-phrase
+    := "PAGE")
+
+(defrule alt-before-after
+    := "BEFORE"
+    := "AFTER")
+
+(defrule advancing-phrase
+    := alt-before-after "ADVANCING"? page-phrase)
+
+(defrule from-identifier
+    := "FROM" variable-identifier)
+
+(defrule invalid-key-statement-list
+    := "INVALID" "KEY"? statement-list
+    :reduce statement-list)
+
+(defrule not-invalid-key-statement-list
+    := "NOT" "INVALID" "KEY"? statement-list
+    :reduce statement-list)
+
+(defrule end-of-page
+    := "END-OF-PAGE"
+    := "EOP")
+
+(defrule at-end-of-page-statement-list
+    := "AT"? end-of-page statement-list
+    :reduce statement-list)
+
+(defrule not-at-end-of-page-statement-list
+    := "NOT" "AT"? end-of-page statement-list
+    :reduce statement-list)
+
+;; This is left in the grammar but is not used.  COPYs are handled by
+;; the lexical scanner.
+(defrule copy-statement
+    := "COPY" alt-text-name-literal in-library? "SUPPRESS"? copy-statement-replacing-phrase?)
+
+(defrule in
+    := "OF"
+    := "IN")
+
+(defrule alt-library-name-literal
+    := library-name
+    := literal)
+
+(defrule in-library
+    := in alt-library-name-literal)
+
+(defrule copy-statement-by-phrase
+    := copy-operand "BY" copy-operand)
+
+(defrule copy-statement-replacing-phrase
+    := "REPLACING" copy-statement-by-phrase+)
+
+(defrule alt-text-name-literal
+    := text-name
+    := literal)
+
+(defrule copy-operand
+    := cobol-identifier
+    := literal)
+
+(defrule use-statement
+    := use-statement-1
+    := use-statement-2
+    := use-statement-3)
+
+(defrule use-statement-1
+    := "USE" "GLOBAL"? "AFTER" "STANDARD"? alt-exception-error "PROCEDURE" "ON"? alt-file-names-i-o)
+
+(defrule alt-exception-error
+    := "EXCEPTION"
+    := "ERROR")
+
+(defrule use-statement-2
+    := "USE" "GLOBAL"? "AFTER" "STANDARD"? alt-beginning-ending? alt-file-reel-unit? "LABEL" "PROCEDURE" "ON"? alt-file-names-i-o)
+
+(defrule alt-beginning-ending
+    := "BEGINNING"
+    := "ENDING")
+
+(defrule alt-file-reel-unit
+    := "FILE"
+    := "REEL"
+    := "UNIT")
+
+(defrule file-names
+    := file-name+)
+
+(defrule alt-file-names-i-o
+    := file-names
+    := "INPUT"
+    := "OUTPUT"
+    := "I-O"
+    := "EXTEND")
+
+(defrule use-statement-3
+    := "USE" "FOR"? "DEBUGGING" "ON"? alt-procedures-all-procedures)
+
+(defrule procedure-names
+    := procedure-name+)
+
+(defrule alt-procedures-all-procedures
+    := procedure-names
+    := all-procedures)
+
+(defrule condition
+    := combinable-condition
+    := combinable-condition "AND" condition
+    :reduce `(and ,combinable-condition ,condition)
+    := combinable-condition "OR" condition
+    :reduce `(or ,combinable-condition ,condition)
+    := combinable-condition "AND" id-or-lit
+    :reduce `(and ,combinable-condition (,(car combinable-condition) ,(cadr combinable-condition) ,id-or-lit))
+    := combinable-condition "OR" id-or-lit
+    :reduce `(or ,combinable-condition (,(car combinable-condition) ,(cadr combinable-condition) ,id-or-lit)))
+
+(defrule combinable-condition
+    := "NOT"? simple-condition
+    :reduce (if $1
+                (list 'not simple-condition)
+                simple-condition))
+
+(defrule simple-condition
+    := class-condition
+    := relation-condition
+    := sign-condition
+    := "(" condition ")"
+    ;; not sure if it's necessary -wcp15/7/03.
+    ;; := arithmetic-expression
+    )
+
+(defrule class-condition
+    := variable-identifier "IS"? "NOT"? class-type
+    :reduce (if $3
+                (list 'not (list 'type-of variable-identifier (make-keyword class-type)))
+                (list 'type-of variable-identifier (make-keyword class-type))))
+
+(defrule class-type
+    := "NUMERIC"
+    := "ALPHABETIC"
+    := "ALPHABETIC-LOWER"
+    := "ALPHABETIC-UPPER"
+    := "DBCS")
+
+(defun unfold-subrelations (main-relation subs)
+  (destructuring-bind (main-operator main-variable other-variable) main-relation
+    (declare (ignore other-variable))
+    (labels ((unfold (subs)
+               (if (null subs)
+                   main-relation
+                   (destructuring-bind (connection operator variable) (car subs)
+                     (list connection
+                           (list (or operator main-operator) main-variable variable)
+                           (unfold (cdr subs)))))))
+      (unfold subs))))
+
+(defrule relation-condition
+    ;; This is too complex
+    ;; := arithmetic-expression relational-operator simple-condition
+    := id-or-lit relational-operator id-or-lit subordinate-relation*
+    :reduce (unfold-subrelations (list relational-operator id-or-lit id-or-lit2) subordinate-relation))
+
+(defrule or-and
+    := "OR" :reduce 'or
+    := "AND" :reduce 'and)
+
+(defrule subordinate-relation
+    := or-and relational-operator? id-or-lit
+    :reduce (list or-and relational-operator id-or-lit))
+
+(defrule relational-operator
+    := "IS"? relational-operator-type
+    :reduce relational-operator-type)
+
+(defrule less-than
+    := "LESS" "THAN"?
+    := "<")
+
+(defrule greater-equal
+    := "GREATER" "THAN"? "OR" "EQUAL" "TO"?
+    := ">="
+    := ">" "="
+    := "NOT" "<"
+    := "NOT" "LESS" "THAN"?)
+
+(defrule less-equal
+    := "LESS" "THAN"? "OR" "EQUAL" "TO"?
+    := "<="
+    := "<" "="
+    := "NOT" ">"
+    := "NOT" "GREATER" "THAN"?)
+
+(defrule greater-than
+    := "GREATER" "THAN"?
+    := ">")
+
+(defrule equal-to
+    := "EQUAL" "TO"?
+    := "=")
+
+(defrule relational-operator-type
+    := greater-equal
+    :reduce 'cob>=
+    := less-equal
+    :reduce 'cob<=
+    := greater-than
+    :reduce 'cob>
+    := less-than
+    :reduce 'cob<
+    := equal-to
+    :reduce 'cob=
+    := "NOT" equal-to
+    :reduce 'cob-not=)
+
+(defrule sign-condition
+    := arithmetic-expression "IS"? "NOT"? sign-type
+    :reduce (if $3
+                `(not (,sign-type ,arithmetic-expression))
+                `(,sign-type ,arithmetic-expression)))
+
+(defrule sign-type
+    := "POSITIVE" :reduce '>
+    := "NEGATIVE" :reduce '<
+    := "ZERO" :reduce '=
+    := "ZEROES" :reduce '=
+    := "ZEROS" :reduce '=)
+
+(defrule procedure-name
+    := paragraph-or-section-name in-section-name
+    :reduce (list paragraph-or-section-name in-section-name)
+    := paragraph-or-section-name
+    :reduce paragraph-or-section-name)
+
+(defrule in-section-name
+    := in cobol-identifier
+    :reduce cobol-identifier)
+
+(defrule variable-identifier
+    := qualified-data-name subscript-parentheses* ;; reference-modification?
+    :reduce (if subscript-parentheses
+                (list :aref qualified-data-name subscript-parentheses)
+                qualified-data-name))
+
+(defrule reference-modification
+    := "(" leftmost-character-position ":" length? ")"
+    :reduce (if length
+                (list :range leftmost-character-position length)
+                leftmost-character-position))
+
+(defrule condition-name-reference
+    := condition-name in-data-or-file-or-mnemonic-name* subscript-parentheses*)
+
+(defrule in-data-or-file-or-mnemonic-name
+    := in data-or-file-or-mnemonic-name)
+
+(defrule subscript-parentheses
+    := "(" subscript ")")
+
+(defrule subscript
+    := subscript-expression+)
+
+(defrule plus-minus-integer
+    := plus-or-minus integer)
+
+(defrule subscript-expression-ambiguous
+    := qualified-data-name plus-minus-integer?)
+
+(defrule subscript-expression
+    := literal
+    := subscript-expression-ambiguous)
+
+(defrule qualified-data-name
+    := data-name in-data-or-file-name*
+    :reduce (if in-data-or-file-name
+                (list data-name in-data-or-file-name) ; incomplete -wcp15/7/03.
+                data-name)
+    := "ADDRESS" "OF" data-name
+    :reduce (list 'address-of data-name)
+    := "LENGTH" "OF" cobol-identifier
+    :reduce (list 'length-of cobol-identifier))
+
+(defrule in-data-or-file-name
+    := in data-or-file-name)
+
+(defrule leftmost-character-position
+    := arithmetic-expression)
+
+(defrule length
+    := arithmetic-expression)
+
+(defrule arithmetic-expression
+    := times-div
+    := times-div "+" arithmetic-expression
+    :reduce `(+ ,times-div ,arithmetic-expression)
+    := times-div "-" arithmetic-expression
+    :reduce `(- ,times-div ,arithmetic-expression))
+
+(defrule times-div
+    := power
+    := power "*" times-div
+    :reduce `(* ,power ,times-div)
+    := power "/" times-div
+    :reduce `(/ ,power ,times-div))
+
+(defrule power
+    := plus-or-minus? basis
+    := plus-or-minus? basis "**" power
+    :reduce (if plus-or-minus
+                `(plus-or-minus (expt basis basis2))
+                `(expt basis basis2)))
+
+(defrule plus-or-minus
+    := "+"
+    :reduce '+
+    := "-"
+    :reduce '-)
+
+;; (defrule power-tail
+;;     := "**" basis)
+
+(defrule basis
+    := literal
+    := variable-identifier
+    := "(" arithmetic-expression ")")
+
+(defrule alphabet-name
+    := cobol-identifier)
+
+(defrule condition-name
+    := cobol-identifier)
+
+(defrule data-name
+    := cobol-identifier)
+
+(defrule cobol-identifier
+    := identifier
+    :reduce (intern (string-upcase identifier)))
+
+(defrule file-name
+    := cobol-identifier)
+
+(defrule data-or-file-name
+    := cobol-identifier)
+
+(defrule index-name
+    := cobol-identifier)
+
+(defrule mnemonic-name
+    := cobol-identifier)
+
+(defrule data-or-file-or-mnemonic-name
+    := cobol-identifier)
+
+(defrule record-name
+    := qualified-data-name)
+
+(defrule symbolic-character
+    := cobol-identifier)
+
+(defrule library-name
+    := cobol-identifier)
+
+(defrule program-name
+    := cobol-identifier
+    := string)
+
+(defrule text-name
+    := cobol-identifier)
+
+(defrule paragraph-or-section-name
+    := cobol-identifier
+    := integer)
+
+(defrule computer-name
+    := identifier)
+
+(defrule environment-name
+    := cobol-identifier)
+
+(defrule assignment-name
+    := cobol-identifier)
+
+(defrule figurative-constant
+    := figurative-constant-simple
+    := figurative-constant-all)
+
+(defrule figurative-constant-all
+    := "ALL" literal)
+
+(defrule literal
+    := string
+    := float
+    := integer
+    := figurative-constant)
+
+)					; defun populate-grammar
diff --git a/third_party/lisp/npg/npg.asd b/third_party/lisp/npg/npg.asd
new file mode 100644
index 0000000000..1e35186d6c
--- /dev/null
+++ b/third_party/lisp/npg/npg.asd
@@ -0,0 +1,55 @@
+;;;  npg.asd --- declaration of this system
+
+;;;  Copyright (C) 2003, 2006 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+
+#+cmu (ext:file-comment "$Module: npg.asd, Time-stamp: <2006-01-03 17:20:21 wcp> $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(defpackage :npg-system
+  (:use :common-lisp :asdf))
+
+(in-package :npg-system)
+
+(defclass sample-file (doc-file) ())
+(defmethod source-file-type ((c sample-file) (s module))
+  "lisp")
+
+(defsystem npg
+  :name "NPG"
+  :author "Walter C. Pelissero <walter@pelissero.de>"
+  :maintainer "Walter C. Pelissero <walter@pelissero.de>"
+  :licence "Lesser General Public License"
+  :description "NPG a Naive Parser Generator"
+  :long-description
+  "NPG is a backtracking recursive descent parser generator for
+Common Lisp. It accepts rules in a Lispy EBNF syntax without indirect
+left recursive rules."
+  :components
+  ((:doc-file "README")
+   (:doc-file "COPYING")
+   (:doc-file ".project")
+   (:module :examples
+            :components
+            ((:sample-file "python")
+             (:sample-file "vs-cobol-ii")))
+   (:module :src
+            :components
+            ((:file "package")
+             (:file "common" :depends-on ("package"))
+             (:file "define" :depends-on ("package" "common"))
+             (:file "parser" :depends-on ("package" "common"))))))
diff --git a/third_party/lisp/npg/src/common.lisp b/third_party/lisp/npg/src/common.lisp
new file mode 100644
index 0000000000..8b64f5cc0a
--- /dev/null
+++ b/third_party/lisp/npg/src/common.lisp
@@ -0,0 +1,79 @@
+;;;  common.lisp --- common stuff
+
+;;;  Copyright (C) 2003-2006, 2009 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+
+#+cmu (ext:file-comment "$Module: common.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :naive-parser-generator)
+
+(eval-when (:compile-toplevel :load-toplevel)
+  (defstruct grammar
+    rules
+    keywords
+    equal-p)
+
+  (defstruct rule
+    name
+    productions)
+
+  (defstruct (production (:conc-name prod-))
+    tokens
+    (tokens-length 0 :type fixnum)
+    action)
+
+  (defstruct token
+    type		     ; type of token (identifier, number, ...)
+    value				; its actual value
+    position)			     ; line/column in the input stream
+  ) ; eval-when
+
+(defmethod print-object ((obj rule) stream)
+  (format stream "#R(~A)" (rule-name obj)))
+
+(defmethod print-object ((obj production) stream)
+  (format stream "#P(action: ~S)" (prod-action obj)))
+
+(defmethod print-object ((obj token) stream)
+  (format stream "#T:~A=~S" (token-type obj) (token-value obj)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(declaim (inline make-rules-table find-rule add-rule))
+
+(defun make-rules-table ()
+  (make-hash-table))
+
+(defun find-rule (rule-name rules)
+  (gethash rule-name rules))
+
+(defun add-rule (rule-name rule rules)
+  (setf (gethash rule-name rules) rule))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(declaim (inline make-keywords-table find-keyword add-keyword))
+
+(defun make-keywords-table ()
+   (make-hash-table :test 'equal))
+
+(defun find-keyword (keyword-name keywords)
+  (gethash keyword-name keywords))
+
+(defun add-keyword (keyword keywords)
+  (setf (gethash keyword keywords) t))
diff --git a/third_party/lisp/npg/src/define.lisp b/third_party/lisp/npg/src/define.lisp
new file mode 100644
index 0000000000..783f071fc5
--- /dev/null
+++ b/third_party/lisp/npg/src/define.lisp
@@ -0,0 +1,408 @@
+;;;  define.lisp --- grammar rules definition
+
+;;;  Copyright (C) 2003-2006, 2009 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+
+#+cmu (ext:file-comment "$Module: define.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :naive-parser-generator)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar *smart-default-reduction* t
+  "If true the default reductions take only the non-static tokens -
+those that are not declared as strings in the grammar.")
+
+;; These two are filled with DEFRULE.
+(defvar *rules* (make-rules-table))
+(defvar *keywords* (make-keywords-table))
+
+(defun make-action-arguments (tokens)
+  "Given a list of tokens making up a production, return three values:
+the list of variables for the function reducing this production, those
+that are non static and their unambiguous user-friendly names."
+  (flet ((unique (sym list)
+           (if (not (assoc sym list))
+               sym
+               (loop
+                  for i of-type fixnum from 2
+                  for x = (intern (format nil "~:@(~A~)~A" sym i))
+                  while (assoc x list)
+                  finally (return x)))))
+    (loop
+       for tok in tokens
+       for i of-type fixnum from 1
+       for arg = (intern (format nil "$~A" i) (find-package #.*package*))
+       collect arg into args
+       unless (const-terminal-p tok)
+         collect arg into vars
+         and when (symbolp tok)
+           collect (list (unique tok named-vars) arg) into named-vars
+       when (and (listp tok)
+                 (symbolp (cadr tok)))
+         collect (list (unique (cadr tok) named-vars) arg) into named-vars
+       finally
+       (return (values args vars named-vars)))))
+
+(defun make-action-function (name tokens action)
+  "Create a function with name NAME, arguments derived from TOKENS and
+body ACTION.  Return it's definition."
+  (let ((function
+         (multiple-value-bind (args vars named-vars)
+             (make-action-arguments tokens)
+           `(lambda ,args
+              (declare (ignorable ,@args))
+              (let (($vars (list ,@vars))
+                    ($all (list ,@args))
+                    ,@named-vars
+                    ($alist (list ,@(mapcar #'(lambda (v)
+                                                `(cons ',(intern (symbol-name (car v)))
+                                                       ,(cadr v)))
+                                            named-vars))))
+                (declare (ignorable $vars $all $alist ,@(mapcar #'car named-vars)))
+                (flet ((make-object (&optional type args)
+                         (apply #'make-instance (or type ',name)
+                                (append args $alist))))
+                  ,action))))))
+    (when *compile-print*
+      (if *compile-verbose*
+          (format t "; Compiling ~S:~%  ~S~%" name function)
+          (format t "; Compiling ~S~%" name)))
+    (compile name function)))
+
+(defun define-rule (name productions)
+  "Accept a rule in EBNF-like syntax, translate it into a sexp and a
+call to INSERT-RULE-IN-CURRENT-GRAMMAR."
+  (flet ((transform (productions)
+           (loop
+              for tok in productions
+              with prod = nil
+              with action = nil
+              with phase = nil
+              with new-prods = nil
+              while tok
+              do (cond ((eq tok :=)
+                        (push (list (nreverse prod) action) new-prods)
+                        (setf prod nil
+                              action nil
+                              phase :prod))
+                       ((eq tok :reduce)
+                        (setf phase :action))
+                       ((eq tok :tag)
+                        (setf phase :tag))
+                       ((eq phase :tag)
+                        (setf action `(cons ,tok $vars)))
+                       ((eq phase :action)
+                        (setf action tok))
+                       ((eq phase :prod)
+                        (push tok prod)))
+              finally
+                (return (cdr (nreverse (cons (list (nreverse prod) action) new-prods)))))))
+    (insert-rule-in-current-grammar name (transform productions))))
+
+(defmacro defrule (name &rest productions)
+  "Wrapper macro for DEFINE-RULE."
+  `(define-rule ',name ',productions))
+
+(defun make-optional-rule (token)
+  "Make a rule for a possibly missing (non)terminal (? syntax) and
+return it."
+  (insert-rule-in-current-grammar
+   (gensym (concatenate 'string "OPT-"
+                        (if (rule-p token)
+                            (symbol-name (rule-name token))
+                            (string-upcase token))))
+   `(((,token)) (()))))
+
+(defun make-alternative-rule (tokens)
+  "Make a rule for a list of alternatives (\"or\" syntax) and return it."
+  (insert-rule-in-current-grammar
+   (gensym "ALT")
+   (mapcar #'(lambda (alternative)
+               `((,alternative)))
+           tokens)))
+
+(defun make-nonempty-list-rule (token &optional separator)
+  "Make a rule for a non-empty list (+ syntax) and return it."
+  (let ((rule-name (gensym (concatenate 'string "NELST-"
+                                        (if (rule-p token)
+                                            (symbol-name (rule-name token))
+                                            (string-upcase token))))))
+    (insert-rule-in-current-grammar
+     rule-name
+     (if separator
+         `(((,token ,separator ,rule-name)
+            (cons $1 $3))
+           ((,token) ,#'list))
+         `(((,token ,rule-name)
+            (cons $1 $2))
+           ((,token) ,#'list))))))
+
+(defun make-list-rule (token &optional separator)
+  "Make a rule for a possibly empty list (* syntax) return it."
+  (make-optional-rule (make-nonempty-list-rule token separator)))
+
+(defun const-terminal-p (object)
+  (or (stringp object)
+      (keywordp object)))
+
+(defun expand-production-token (tok)
+  "Translate token of the type NAME? or NAME* or NAME+ into (? NAME)
+or (* NAME) or (+ NAME).  This is used by the DEFRULE macro."
+  (if (symbolp tok)
+      (let* ((name (symbol-name tok))
+             (last (char name (1- (length name))))
+             ;; this looks silly but we need to make sure that we
+             ;; return symbols interned in this package, no one else
+             (op (cadr (assoc last '((#\? ?) (#\+ +) (#\* *))))))
+        (if (and (> (length name) 1) op)
+            (list op
+                  (intern (subseq name 0 (1- (length name)))))
+            tok))
+      tok))
+
+(defun EBNF-to-SEBNF (tokens)
+  "Take a production as a list of TOKENS and expand it.  This turns a
+EBNF syntax into a sexp-based EBNF syntax or SEBNF."
+  (loop
+     for tok in tokens
+     for token = (expand-production-token tok)
+     with new-tokens = '()
+     do (cond ((member token '(* + ?))
+               (setf (car new-tokens)
+                     (list token (car new-tokens))))
+              (t
+               (push token new-tokens)))
+     finally (return (nreverse new-tokens))))
+
+(defun SEBNF-to-BNF (tokens)
+  "Take a production in SEBNF (Symbolic Extended BNF) syntax and turn
+it into BNF.  The production is simplified but the current grammar is
+populated with additional rules."
+  (flet ((make-complex-token-rule (tok)
+           (ecase (car tok)
+             (* (apply #'make-list-rule (cdr tok)))
+             (+ (apply #'make-nonempty-list-rule (cdr tok)))
+             (? (make-optional-rule (cadr tok)))
+             (or (make-alternative-rule (cdr tok))))))
+    (loop
+       for token in tokens
+       with new-tokens = '()
+       with keywords = '()
+       do (cond ((listp token)
+                 (push (make-complex-token-rule token) new-tokens))
+                (t
+                 (push token new-tokens)
+                 (when (const-terminal-p token)
+                   (push token keywords))))
+       finally (return (values (nreverse new-tokens) keywords)))))
+
+(defun make-default-action-function (name tokens)
+  "Create a sexp to be used as default action in case one is not
+supplied in the production.  This is usually a quite sensible
+one.  That is, only the non-constant tokens are returned in a
+list and in case only a variable token is available that one is
+returned (not included in a list).  If all the tokens are
+constant, then all of them are returned in a list."
+  (cond ((null tokens)
+         ;; if the production matched the empty list (no tokens) we
+         ;; return always nil, that is the function LIST applied to no
+         ;; arguments
+         #'list)
+        ((null (cdr tokens))
+         ;; if the production matches just one token we simply return
+         ;; that
+         #'identity)
+        (*smart-default-reduction*
+         ;; If we are required to be "smart" then create a function
+         ;; that simply returns the non static tokens of the
+         ;; production.  If the production doesn't have nonterminal,
+         ;; then return all the tokens.  If the production has only
+         ;; one argument then return that one only.
+         (make-action-function name tokens '(cond
+                                             ((null $vars) $all)
+                                             ((null (cdr $vars)) (car $vars))
+                                             (t $vars))))
+        (t
+         ;; in all the other cases we return all the token matching
+         ;; the production
+         #'list)))
+
+(defun make-production-from-descr (name production-description)
+  "Take a production NAME and its description in the form of a sexp
+and return a production structure object together with a list of used
+keywords."
+  (destructuring-bind (tokens &optional action) production-description
+    (let ((expanded-tokens (EBNF-to-SEBNF tokens)))
+      (multiple-value-bind (production-tokens keywords)
+          (sebnf-to-bnf expanded-tokens)
+      (let ((funct
+             (cond ((not action)
+                    (make-default-action-function name expanded-tokens))
+                   ((or (listp action)
+                        ;; the case when the action is simply to
+                        ;; return a token (ie $2) or a constant value
+                        (symbolp action))
+                    (make-action-function name expanded-tokens action))
+                   ((functionp action)
+                    action)
+                   (t			; action is a constant
+                    #'(lambda (&rest args)
+                        (declare (ignore args))
+                        action)))))
+        (values
+         ;; Make a promise instead of actually resolving the
+         ;; nonterminals.  This avoids endless recursion.
+         (make-production :tokens production-tokens
+                          :tokens-length (length production-tokens)
+                          :action funct)
+         keywords))))))
+
+(defun remove-immediate-left-recursivity (rule)
+  "Turn left recursive rules of the type
+    A -> A x | y
+into
+    A -> y A2
+    A2 -> x A2 | E
+where E is the empty production."
+  (let ((name (rule-name rule))
+        (productions (rule-productions rule)))
+    (loop
+       for prod in productions
+       for tokens = (prod-tokens prod)
+       ;; when immediately left recursive
+       when (eq (car tokens) rule)
+       collect prod into left-recursive
+       else
+       collect prod into non-left-recursive
+       finally
+         ;; found any left recursive production?
+         (when left-recursive
+           (warn "rule ~S is left recursive" name)
+           (let ((new-rule (make-rule :name (gensym "REWRITE"))))
+             ;; A -> y A2
+             (setf (rule-productions rule)
+                   (mapcar #'(lambda (p)
+                               (let ((tokens (prod-tokens p))
+                                     (action (prod-action p)))
+                                 (make-production :tokens (append tokens (list new-rule))
+                                                  :tokens-length (1+ (prod-tokens-length p))
+                                                  :action #'(lambda (&rest args)
+                                                              (let ((f-A2 (car (last args)))
+                                                                    (head (butlast args)))
+                                                                (funcall f-A2 (apply action head)))))))
+                           non-left-recursive))
+             ;; A2 -> x A2 | E
+             (setf (rule-productions new-rule)
+                   (append
+                    (mapcar #'(lambda (p)
+                                (let ((tokens (prod-tokens p))
+                                      (action (prod-action p)))
+                                  (make-production :tokens (append (cdr tokens) (list new-rule))
+                                                   :tokens-length (prod-tokens-length p)
+                                                   :action #'(lambda (&rest args)
+                                                               (let ((f-A2 (car (last args)))
+                                                                     (head (butlast args)))
+                                                                 #'(lambda (x)
+                                                                     (funcall f-A2 (apply action x head))))))))
+                            left-recursive)
+                    (list
+                     (make-production :tokens nil
+                                      :tokens-length 0
+                                      :action #'(lambda () #'(lambda (arg) arg)))))))))))
+
+(defun remove-left-recursivity-from-rules (rules)
+  (loop
+     for rule being each hash-value in rules
+     do
+     ;; More to be done here.  For now only the trivial immediate left
+     ;; recursivity is removed -wcp18/11/03.
+       (remove-immediate-left-recursivity rule)))
+
+(defun resolve-all-nonterminals (rules)
+  (loop
+     for rule being each hash-value in rules
+     do (loop
+           for production in (rule-productions rule)
+           do (setf (prod-tokens production)
+                    (resolve-nonterminals (prod-tokens production) rules)))))
+
+(defun make-rule-productions (rule-name production-descriptions)
+  "Return a production object that belongs to RULE-NAME made according
+to PRODUCTION-DESCRIPTIONS.  See also MAKE-PRODUCTION-FROM-DESCR."
+  (loop
+     for descr in production-descriptions
+     for i of-type fixnum from 1 by 1
+     for prod-name = (intern (format nil "~:@(~A~)-PROD~A" rule-name i))
+     with productions = '()
+     with keywords = '()
+     do (progn
+          (multiple-value-bind (production keyws)
+              (make-production-from-descr prod-name descr)
+            (push production productions)
+            (setf keywords (append keyws keywords))))
+     finally (return
+               (values (nreverse productions) keywords))))
+
+(defun create-rule (name production-descriptions)
+  "Return a new rule object together with a list of keywords making up
+the production definitions."
+  (multiple-value-bind (productions keywords)
+      (make-rule-productions name production-descriptions)
+    (values (make-rule :name name :productions productions)
+            keywords)))
+
+(defun insert-rule-in-current-grammar (name productions)
+  "Add rule to the current grammar and its keywords to the keywords
+hash table.  You don't want to use this directly.  See DEFRULE macro
+instead."
+  (when (find-rule name *rules*)
+    (error "redefining rule ~A" name))
+  (multiple-value-bind (rule keywords)
+      (create-rule name productions)
+    (add-rule name rule *rules*)
+    (dolist (term keywords)
+      (add-keyword term *keywords*))
+    rule))
+
+(defun resolve-nonterminals (tokens rules)
+  "Given a list of production tokens, try to expand the nonterminal
+ones with their respective rule from the the RULES pool."
+  (flet ((resolve-symbol (sym)
+           (or (find-rule sym rules)
+               sym)))
+    (mapcar #'(lambda (tok)
+                (if (symbolp tok)
+                    (resolve-symbol tok)
+                    tok))
+            tokens)))
+
+(defun reset-grammar ()
+  "Empty the current grammar from any existing rule."
+  (setf *rules* (make-rules-table)
+        *keywords* (make-keywords-table)))
+
+(defun generate-grammar (&optional (equal-p #'string-equal))
+  "Return a GRAMMAR structure suitable for the PARSE function, using
+the current rules.  EQUAL-P, if present, is a function to be used to
+match the input tokens; it defaults to STRING-EQUAL."
+  (resolve-all-nonterminals *rules*)
+  (remove-left-recursivity-from-rules *rules*)
+  (make-grammar :rules *rules*
+                :keywords *keywords*
+                :equal-p equal-p))
diff --git a/third_party/lisp/npg/src/package.lisp b/third_party/lisp/npg/src/package.lisp
new file mode 100644
index 0000000000..b405f7b5f1
--- /dev/null
+++ b/third_party/lisp/npg/src/package.lisp
@@ -0,0 +1,50 @@
+;;;  package.lisp --- backtracking parser package definition
+
+;;;  Copyright (C) 2003-2006, 2009 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+
+#+cmu (ext:file-comment "$Module: package.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :cl-user)
+
+(defpackage :naive-parser-generator
+  (:nicknames :npg)
+  (:use :common-lisp)
+  (:export
+   #:parse				; The Parser
+   #:reset-grammar
+   #:generate-grammar
+   #:print-grammar-figures
+   #:grammar-keyword-p
+   #:keyword
+   #:grammar
+   #:make-token
+   #:token-value
+   #:token-type
+   #:token-position
+   #:later-position
+   #:defrule				; to define grammars
+   #:deftoken				; to define a lexer
+   #:input-cursor-mixin
+   #:copy-input-cursor-slots
+   #:dup-input-cursor
+   #:read-next-tokens
+   #:end-of-input
+   #:? #:+ #:* #:or
+   #:$vars #:$all #:$alist
+   #:$1 #:$2 #:$3 #:$4 #:$5 #:$6 #:$7 #:$8 #:$9 #:$10))
diff --git a/third_party/lisp/npg/src/parser.lisp b/third_party/lisp/npg/src/parser.lisp
new file mode 100644
index 0000000000..c15d26fe39
--- /dev/null
+++ b/third_party/lisp/npg/src/parser.lisp
@@ -0,0 +1,234 @@
+;;;  parser.lisp --- runtime parser
+
+;;;  Copyright (C) 2003-2006, 2009 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: NPG a Naive Parser Generator
+
+#+cmu (ext:file-comment "$Module: parser.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+;;;  Commentary:
+;;;
+;;; This is the runtime part of the parser.  The code that is
+;;; responsible to execute the parser defined with the primitives
+;;; found in define.lisp.
+
+(in-package :naive-parser-generator)
+
+(defvar *debug* nil
+  "Either nil or a stream where to write the debug informations.")
+#+debug (declaim (fixnum *maximum-recursion-depth*))
+#+debug (defvar *maximum-recursion-depth* 1000
+  "Maximum depth the parser is allowed to recursively call itself.
+This is the only way for the parser to detect a loop in the grammar.
+Tune this if your grammar is unusually complex.")
+
+(declaim (inline reduce-production))
+(defun reduce-production (production arguments)
+  "Apply PRODUCTION's action on ARGUMENTS.  This has the effect of
+  \"reducing\" the production."
+  (when *debug*
+    (format *debug* "reducing ~S on ~S~%" production arguments))
+  (flet ((safe-token-value (token)
+           (if (token-p token)
+               (token-value token)
+               token)))
+    (apply (prod-action production) (mapcar #'safe-token-value arguments))))
+
+(defgeneric later-position (pos1 pos2)
+  (:documentation
+   "Compare two file postions and return true if POS1 is later than
+POS2 in the input stream."))
+
+;; This is meant to be overloaded in the lexer
+(defmethod later-position ((pos1 integer) (pos2 integer))
+  (> pos1 pos2))
+
+;; this looks silly but turns out to be useful (see below)
+(defmethod later-position (pos1 pos2)
+  (and (eq pos1 :eof) (not (eq pos2 :eof))))
+
+(defgeneric read-next-tokens (tokens-source)
+  (:documentation "Read next token from a lexical analysed stream.  The nature of
+TOKENS-SOURCE is implementation dependent and any lexical analyzer is
+supposed to specialise this method."))
+
+;; This is the actual parser.  the algorithm is pretty
+;; straightforward, the execution of the reductions a bit less.  Error
+;; recovery is rather clumsy.
+
+(defun parse (grammar start tokenizer)
+  "Match a GRAMMAR against the list of input tokens coming from TOKENIZER.
+Return the reduced values according to the nonterminal actions.  Raise
+an error on failure."
+  (declare (type grammar grammar)
+           (type symbol start))
+  (labels
+      ((match-token (expected token)
+         (when *debug*
+           (format *debug* "match-token ~S ~S -> " expected token))
+         (let ((res (cond ((symbolp expected)
+                           ;; non-costant terminal (like identifiers)
+                           (eq expected (token-type token)))
+                          ((and (stringp expected)
+                                (stringp (token-value token)))
+                           ;; string costant terminal
+                           (funcall (the function (grammar-equal-p grammar)) expected (token-value token)))
+                          ((functionp expected)
+                           ;; custom equality predicate (must be able
+                           ;; to deal with token objects)
+                           (funcall expected token))
+                          ;; all the rest
+                          (t (equal expected (token-value token))))))
+           (when *debug*
+             (format *debug* "~Amatched~%" (if res "" "not ")))
+           res))
+       (match (expected matched #+debug depth)
+         (declare (list expected matched)
+                  #+debug (fixnum depth))
+         (let ((first-expected (car expected)))
+           (cond #+debug ((> depth *maximum-recursion-depth*)
+                  (error "endless recursion on ~A ~A at ~A expecting ~S"
+                         (token-type (car matched)) (token-value (car matched))
+                         (token-position (car matched)) expected))
+                 ((eq first-expected :any)
+                  (match (cdr expected) (cdr matched) #+debug depth))
+                 ;; This is a trick to obtain partial parses.  When we
+                 ;; reach this expected token we assume we succeeded
+                 ;; the parsing and return the remaining tokens as
+                 ;; part of the match.
+                 ((eq first-expected :rest)
+                  ;; we could be at the end of input so we check this
+                  (unless (cdr matched)
+                    (setf (cdr matched) (list :rest)))
+                  (list nil nil))
+                 ((rule-p first-expected)
+                  ;; If it's a rule, then we try to match all its
+                  ;; productions.  We return the first that succeeds.
+                  (loop
+                     for production in (rule-productions first-expected)
+                     for production-tokens of-type list = (prod-tokens production)
+                     with last-error-position = nil
+                     with last-error = nil
+                     for (error-position error-descr) =
+                       (progn
+                         (when *debug*
+                           (format *debug* "trying to match ~A: ~S~%"
+                                   (rule-name first-expected) production-tokens))
+                         (match (append production-tokens (cdr expected)) matched #+debug (1+ depth)))
+                     do (cond ((not error-position)
+                               (return (let ((args-count (prod-tokens-length production)))
+                                         (setf (cdr matched)
+                                               (cons (reduce-production
+                                                      production
+                                                      (subseq (the list (cdr matched)) 0 args-count))
+                                                     (nthcdr (1+ args-count) matched)))
+                                         (list nil nil))))
+                              ((or (not last-error)
+                                   (later-position error-position last-error-position))
+                               (setf last-error-position error-position
+                                     last-error error-descr)))
+                     ;; if everything fails return the "best" error
+                     finally (return (list last-error-position
+                                           (if *debug*
+                                               #'(lambda ()
+                                                   (format nil "~A, trying to match ~A"
+                                                           (funcall (the function last-error))
+                                                           (rule-name first-expected)))
+                                               last-error)))))
+                 (t
+                  ;; if necessary load the next tokens
+                  (when (null (cdr matched))
+                    (setf (cdr matched) (read-next-tokens tokenizer)))
+                  (cond ((and (or (null expected) (eq first-expected :eof))
+                              (null (cdr matched)))
+                         ;; This point is reached only once for each complete
+                         ;; parsing.  The expected tokens and the input
+                         ;; tokens have been exhausted at the same time.
+                         ;; Hence we succeeded the parsing.
+                         (setf (cdr matched) (list :eof))
+                         (list nil nil))
+                        ((null expected)
+                         ;; Garbage at end of parsing.  This may mean that we
+                         ;; have considered a production completed too soon.
+                         (list (token-position (car matched))
+                               #'(lambda ()
+                                   "garbage at end of parsing")))
+                        ((null (cdr matched))
+                         ;; EOF error
+                         (list :eof
+                               #'(lambda ()
+                                   (format nil "end of input expecting ~S" expected))))
+                        (t ;; normal token
+                         (let ((first-token (cadr matched)))
+                           (if (match-token first-expected first-token)
+                               (match (cdr expected) (cdr matched) #+debug depth)
+                               ;; failed: we return the error
+                               (list (token-position first-token)
+                                     #'(lambda ()
+                                         (format nil "expected ~S but got ~S ~S"
+                                                 first-expected (token-type first-token)
+                                                 (token-value first-token)))))))))))))
+    (declare (inline match-token))
+    (let ((result (list :head)))
+      (destructuring-bind (error-position error)
+          (match (list (find-rule start (grammar-rules grammar))) result #+debug 0)
+        (when error-position
+          (error "~A at ~A~%" (funcall (the function error)) error-position))
+        (cadr result)))))
+
+(defgeneric terminals-in-grammar (grammar-or-hashtable)
+  (:documentation
+   "Find non constant terminal symbols in GRAMMAR."))
+
+(defmethod terminals-in-grammar ((grammar hash-table))
+  (loop
+     for rule being each hash-value of grammar
+     with terminals = '()
+     do (loop
+           for prod in (rule-productions rule)
+           do (loop
+                 for tok in (prod-tokens prod)
+                 when (symbolp tok)
+                 do (pushnew tok terminals)))
+     finally (return terminals)))
+
+(defmethod terminals-in-grammar ((grammar grammar))
+  (terminals-in-grammar (grammar-rules grammar)))
+
+(defun print-grammar-figures (grammar &optional (stream *standard-output*))
+  (format stream "rules: ~A~%constant terminals: ~A~%variable terminals: ~S~%"
+          (hash-table-count (grammar-rules grammar))
+          (hash-table-count (grammar-keywords grammar))
+          (terminals-in-grammar (grammar-rules grammar))))
+
+
+(defun grammar-keyword-p (keyword grammar)
+  "Check if KEYWORD is part of this grammar."
+  (find-keyword keyword (grammar-keywords grammar)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar *grammars* (make-hash-table))
+
+(defun find-grammar (name)
+  (gethash name *grammars*))
+
+(defun delete-grammar (name)
+  (remhash name *grammars*))
+
+(defun add-grammar (name grammar)
+  (setf (gethash name *grammars*) grammar))
diff --git a/third_party/lisp/parse-float.nix b/third_party/lisp/parse-float.nix
new file mode 100644
index 0000000000..e90824108e
--- /dev/null
+++ b/third_party/lisp/parse-float.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.parse-float;
+in depot.nix.buildLisp.library {
+  name = "parse-float";
+
+  deps = with depot.third_party.lisp; [
+    alexandria
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "parse-float.lisp"
+  ];
+}
diff --git a/third_party/lisp/parse-number.nix b/third_party/lisp/parse-number.nix
new file mode 100644
index 0000000000..61b0b1fddb
--- /dev/null
+++ b/third_party/lisp/parse-number.nix
@@ -0,0 +1,9 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.parse-number;
+in depot.nix.buildLisp.library {
+  name = "parse-number";
+  srcs = map (f: src + ("/" + f)) [
+    "parse-number.lisp"
+  ];
+}
diff --git a/third_party/lisp/parseq.nix b/third_party/lisp/parseq.nix
new file mode 100644
index 0000000000..23c67c2d9c
--- /dev/null
+++ b/third_party/lisp/parseq.nix
@@ -0,0 +1,13 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.parseq;
+in depot.nix.buildLisp.library {
+  name = "parseq";
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "conditions.lisp"
+    "utils.lisp"
+    "defrule.lisp"
+  ];
+}
diff --git a/third_party/lisp/physical-quantities.nix b/third_party/lisp/physical-quantities.nix
new file mode 100644
index 0000000000..d594ff1a1c
--- /dev/null
+++ b/third_party/lisp/physical-quantities.nix
@@ -0,0 +1,24 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.physical-quantities;
+in depot.nix.buildLisp.library {
+  name = "physical-quantities";
+
+  deps = with depot.third_party.lisp; [
+    parseq
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "utils.lisp"
+    "conditions.lisp"
+    "unit-factor.lisp"
+    "unit-database.lisp"
+    "units.lisp"
+    "quantity.lisp"
+    "numeric.lisp"
+    "parse-rules.lisp"
+    "read-macro.lisp"
+    "si-units.lisp"
+  ];
+}
diff --git a/third_party/lisp/postmodern.nix b/third_party/lisp/postmodern.nix
new file mode 100644
index 0000000000..25e0625c20
--- /dev/null
+++ b/third_party/lisp/postmodern.nix
@@ -0,0 +1,94 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix.buildLisp) bundled;
+  src = with pkgs; srcOnly lispPackages.postmodern;
+
+  cl-postgres = depot.nix.buildLisp.library {
+    name = "cl-postgres";
+    deps = with depot.third_party.lisp; [
+      md5
+      split-sequence
+      ironclad
+      cl-base64
+      uax-15
+      usocket
+    ];
+
+    srcs = map (f: src + ("/cl-postgres/" + f)) [
+      "package.lisp"
+      "features.lisp"
+      "config.lisp"
+      "oid.lisp"
+      "errors.lisp"
+      "data-types.lisp"
+      "sql-string.lisp"
+      "trivial-utf-8.lisp"
+      "strings-utf-8.lisp"
+      "communicate.lisp"
+      "messages.lisp"
+      "ieee-floats.lisp"
+      "interpret.lisp"
+      "saslprep.lisp"
+      "scram.lisp"
+      "protocol.lisp"
+      "public.lisp"
+      "bulk-copy.lisp"
+    ];
+  };
+
+  s-sql = depot.nix.buildLisp.library {
+    name = "s-sql";
+    deps = with depot.third_party.lisp; [
+      cl-postgres
+      alexandria
+    ];
+
+    srcs = map (f: src + ("/s-sql/" + f)) [
+      "package.lisp"
+      "config.lisp"
+      "s-sql.lisp"
+    ];
+  };
+
+  postmodern = depot.nix.buildLisp.library {
+    name = "postmodern";
+
+    deps = with depot.third_party.lisp; [
+      alexandria
+      cl-postgres
+      s-sql
+      global-vars
+      split-sequence
+      cl-unicode
+      closer-mop
+      bordeaux-threads
+    ];
+
+    srcs = [
+      "${src}/postmodern.asd"
+    ] ++ (map (f: src + ("/postmodern/" + f)) [
+      "package.lisp"
+      "config.lisp"
+      "connect.lisp"
+      "json-encoder.lisp"
+      "query.lisp"
+      "prepare.lisp"
+      "roles.lisp"
+      "util.lisp"
+      "transaction.lisp"
+      "namespace.lisp"
+      "execute-file.lisp"
+      "table.lisp"
+      "deftable.lisp"
+    ]);
+
+    brokenOn = [
+      "ecl" # TODO(sterni): https://gitlab.com/embeddable-common-lisp/ecl/-/issues/651
+    ];
+  };
+
+in
+postmodern // {
+  inherit s-sql cl-postgres;
+}
diff --git a/third_party/lisp/prove.nix b/third_party/lisp/prove.nix
new file mode 100644
index 0000000000..af48149920
--- /dev/null
+++ b/third_party/lisp/prove.nix
@@ -0,0 +1,29 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.prove;
+in depot.nix.buildLisp.library {
+  name = "prove";
+
+  deps = [
+    depot.third_party.lisp.alexandria
+    depot.third_party.lisp.cl-ansi-text
+    depot.third_party.lisp.cl-colors
+    depot.third_party.lisp.cl-ppcre
+    (depot.nix.buildLisp.bundled "asdf")
+  ];
+
+  srcs = [
+    "${src}/src/color.lisp"
+    "${src}/src/output.lisp"
+    "${src}/src/asdf.lisp"
+    "${src}/src/report.lisp"
+    "${src}/src/reporter.lisp"
+    "${src}/src/reporter/fiveam.lisp"
+    "${src}/src/reporter/list.lisp"
+    "${src}/src/reporter/dot.lisp"
+    "${src}/src/reporter/tap.lisp"
+    "${src}/src/suite.lisp"
+    "${src}/src/test.lisp"
+    "${src}/src/prove.lisp"
+  ];
+}
diff --git a/third_party/lisp/puri.nix b/third_party/lisp/puri.nix
new file mode 100644
index 0000000000..f7146ba93f
--- /dev/null
+++ b/third_party/lisp/puri.nix
@@ -0,0 +1,10 @@
+# Portable URI library
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.puri;
+in depot.nix.buildLisp.library {
+  name = "puri";
+  srcs = [
+    (src + "/src.lisp")
+  ];
+}
diff --git a/third_party/lisp/quasiquote_2/README.md b/third_party/lisp/quasiquote_2/README.md
new file mode 100644
index 0000000000..2d590a0564
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/README.md
@@ -0,0 +1,258 @@
+quasiquote-2.0
+==============
+
+Why should it be hard to write macros that write other macros?
+Well, it shouldn't!
+
+quasiquote-2.0 defines slightly different rules for quasiquotation,
+that make writing macro-writing macros very smooth experience.
+
+NOTE: quasiquote-2.0 does horrible things to shared structure!!!
+(it does a lot of COPY-TREE's, so shared-ness is destroyed).
+So, it's indeed a tool to construct code (where it does not matter much if the
+structure is shared or not) and not the data (or, at least, not the data with shared structure)
+
+
+```lisp
+(quasiquote-2.0:enable-quasiquote-2.0)
+
+(defmacro define-my-macro (name args &body body)
+  `(defmacro ,name ,args
+     `(sample-thing-to-expand-to
+        ,,@body))) ; note the difference from usual way
+
+(define-my-macro foo (x y)
+  ,x ; now here injections of quotation constructs work
+  ,y)
+
+(define-my-macro bar (&body body)
+  ,@body) ; splicing is also easy
+```
+
+The "injections" in macros FOO and BAR work as naively expected, as if I had written
+```lisp
+(defmacro foo (x y)
+  `(sample-thing-to-expand-to ,x ,y))
+
+(defmacro bar (&body body)
+  `(sample-thing-to-expand-to ,@body))
+
+(macroexpand-1 '(foo a b))
+
+  '(SAMPLE-THING-TO-EXPAND-TO A B)
+
+(macroexpand-1 '(bar a b c))
+
+  '(SAMPLE-THING-TO-EXPAND-TO A B C)
+```
+
+
+So, how is this effect achieved?
+
+
+DIG, INJECT and SPLICE
+-------------------------
+
+The transformations of backquote occur at macroexpansion-time and not at read-time.
+It is totally possible not to use any special reader syntax, but just
+underlying macros directly!
+
+At the core is a macro DIG, which expands to the code that generates the
+expression according to the rules, which are roughly these:
+  * each DIG increases "depth" by one (hence the name)
+  * each INJECT or SPLICE decreases "depth" by one
+  * if depth is 0, evaluation is turned on
+  * if depth if not zero (even if it's negative!) evaluation is off
+  * SPLICE splices the form, similarly to ordinary `,@`, INJECT simply injects, same as `,`
+
+```lisp
+;; The example using macros, without special reader syntax
+
+(dig ; depth is 1 here
+  (a b
+     (dig ; depth is 2 here
+       ((inject c) ; this inject is not evaluated, because depth is nonzero
+        (inject (d ;depth becomes 1 here again
+                (inject e) ; and this inject is evaluated, because depth becomes zero
+                ))
+        (inject 2 f) ; this inject with level specification is evaluated, because it
+                     ; decreases depth by 2
+        ))))
+
+
+;; the same example using ENABLE-QUASIQUOTE-2.0 syntax is written as
+`(a b `(,c ,(d ,e) ,,f)) ; note double comma acts different than usually
+```
+
+
+The ENABLE-QUASIQUOTE-2.0 macro just installs reader that reads
+`FORM as (DIG FORM), ,FORM as (INJECT FORM) and ,@FORM as (SPLICE FORM).
+You can just as well type DIG's, INJECT's and SPLICE's directly, 
+(in particular, when writing utility functions that generate macro-generating code)
+or roll your own convenient reader syntax (pull requests are welcome).
+
+So, these two lines (with ENABLE-QUASIQUOTE-2.0) read the same
+```lisp
+`(a (,b `,,c) d)
+
+(dig (a ((inject b) (dig (inject 2 c))) d))
+```
+
+You may notice the (INJECT 2 ...) form appearing, which is described below.
+
+
+At "level 1", i.e. when only \` , and ,@ are used, and not, say \`\` ,, ,', ,,@ ,',@
+this behaves exactly as usual quasiquotation.
+
+
+The optional N argument
+--------------
+
+All quasiquote-2.0 operators accept optional "depth" argument,
+which goes before the form for human readability.
+
+Namely, (DIG N FORM) increases depth by N instead of one and
+(INJECT N FORM) decreases depth by N instead of one.
+
+```lisp
+(DIG 2 (INJECT 2 A))
+
+; gives the same result as
+
+(DIG (INJECT A))
+```
+
+
+In fact, with ENABLE-QUASIQUOTE-2.0, say, ,,,,,FORM (5 quotes) reads as (INJECT 5 FORM)
+and ,,,,,@FORM as (SPLICE 5 FORM)
+
+
+More examples
+-------------
+
+For fairly complicated example, which uses ,,,@ and OINJECT (see below),
+ see DEFINE-BINOP-DEFINER macro
+in CG-LLVM (https://github.com/mabragor/cg-llvm/src/basics.lisp),
+desire to write which was the initial impulse for this project.
+
+
+For macro, that is not a macro-writing macro, yet benefits from
+ability to inject using `,` and `,@`, consider JOINING-WITH-COMMA-SPACE macro
+(also from CG-LLVM)
+
+```lisp
+(defmacro joining-with-comma-space (&body body)
+  ;; joinl just joins strings in the list with specified string
+  `(joinl ", " (mapcar #'emit-text-repr
+		       (remove-if-not #'identity  `(,,@body)))))
+
+;; the macro can be then used uniformly over strings and lists of strings
+(defun foo (x y &rest z)
+  (joining-with-comma-space ,x ,y ,@z))
+
+(foo "a" "b" "c" "d")
+  ;; produces
+  "a, b, c, d"
+```
+
+
+ODIG and OINJECT and OSPLICE
+----------------------------
+
+Sometimes you don't want DIG's macroexpansion to look further into the structure of
+some INJECT or SPLICE or DIG in its subform,
+if the depth does not match. In these cases you need "opaque" versions of
+DIG, INJECT and SPLICE, named, respectively, ODIG, OINJECT and OSPLICE.
+
+```lisp
+;; here injection of B would occur
+(defun foo (b)
+  (dig (dig (inject (a (inject b))))))
+
+;; and here not, because macroexpansion does not look into OINJECT form
+(defun bar (b)
+  (dig (dig (oinject (a (inject b))))))
+
+(foo 1)
+
+  '(DIG (INJECT (A 1)))
+
+(bar 1)
+
+  '(DIG (OINJECT (A (INJECT B))))
+```
+
+MACRO-INJECT and MACRO-SPLICE
+-----------------------------
+
+Sometimes you just want to abstract-out some common injection patterns...
+That is, you want macros, that expand into common injection patterns.
+However, you want this only sometimes, and only in special circumstances.
+So it won't do, if INJECT and SPLICE just expanded something, whenever it
+turned out to be macro. For that, use MACRO-INJECT and MACRO-SPLICE.
+
+```lisp
+;; with quasiquote-2.0 syntax turned on
+(defmacro inject-n-times (form n)
+  (make-list n :initial-element `(inject ,form)))
+
+(let (x 0)
+  `(dig (a (macro-inject (inject-n-times (incf x) 3)))))
+;; yields
+'(a (1 2 3))
+
+;;and same with MACRO-SPLICE
+(let (x 0)
+  `(dig (a (macro-splice (inject-n-times (incf x) 3)))))
+;; yields
+'(a 1 2 3)
+```
+
+OMACRO-INJECT and OMACRO-SPLICE are, as usual, opaque variants of MACRO-INJECT and MACRO-SPLICE.
+
+Both MACRO-INJECT and MACRO-SPLICE expand their subform exactly once (using MACROEXPAND-1),
+before plugging it into list.
+If you want to expand as much as it's possible, use MACRO-INJECT-ALL and MACRO-SPLICE-ALL,
+which expand using MACROEXPAND before injecting/splicing, respectively.
+That implies, that while subform of MACRO-INJECT and MACRO-SPLICE is checked to be
+macro-form, the subform of MACRO-INJECT-ALL is not.
+
+
+Terse syntax of the ENABLE-QUASIQUOTE-2.0
+-----------------------------------------
+
+Of course, typing all those MACRO-INJECT-ALL, or OMACRO-SPLICE-ALL or whatever explicitly
+every time you want this special things is kind of clumsy. For that, default reader
+of quasiquote-2.0 provides extended syntax
+
+```lisp
+',,,,!oma@x
+
+;; reads as
+'(OMACRO-SPLICE-ALL 4 X)
+```
+
+That is, the regexp of the syntax is
+[,]+![o][m][a][@]<whatever>
+
+As usual, number of commas determine the anti-depth of the injector, exclamation mark
+turns on the syntax, if `o` is present, opaque version of injector will be used,
+if `m` is present, macro-expanding version of injector will be used and if
+`a` is present, macro-all version of injector will be used.
+
+Note: it's possible to write ,!ax, which will read as (INJECT-ALL X), but
+this will not correspond to the actual macro name.
+
+Note: it was necessary to introduce special escape-char for extended syntax,
+since usual idioms like `,args` would otherwise be completely screwed.
+
+
+TODO
+----
+
+* WITH-QUASIQUOTE-2.0 read-macro-token for local enabling of ` and , overloading
+* wrappers for convenient definition of custom overloading schemes
+* some syntax for opaque operations
+
+P.S. Name "quasiquote-2.0" comes from "patronus 2.0" spell from www.hpmor.com
+     and has nothing to do with being "the 2.0" version of quasiquote.
\ No newline at end of file
diff --git a/third_party/lisp/quasiquote_2/default.nix b/third_party/lisp/quasiquote_2/default.nix
new file mode 100644
index 0000000000..521c384787
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/default.nix
@@ -0,0 +1,17 @@
+# Quasiquote more suitable for macros that define other macros
+{ depot, ... }:
+
+depot.nix.buildLisp.library {
+  name = "quasiquote-2.0";
+
+  deps = [
+    depot.third_party.lisp.iterate
+  ];
+
+  srcs = [
+    ./package.lisp
+    ./quasiquote-2.0.lisp
+    ./macros.lisp
+    ./readers.lisp
+  ];
+}
diff --git a/third_party/lisp/quasiquote_2/macros.lisp b/third_party/lisp/quasiquote_2/macros.lisp
new file mode 100644
index 0000000000..6ebeb47d08
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/macros.lisp
@@ -0,0 +1,15 @@
+
+(in-package #:quasiquote-2.0)
+
+(defmacro define-dig-like-macro (name)
+  `(defmacro ,name (n-or-form &optional (form nil form-p) &environment env)
+     (if (not form-p)
+	 `(,',name 1 ,n-or-form)
+	 (let ((*env* env))
+	   (transform-dig-form `(,',name ,n-or-form ,form))))))
+
+
+(define-dig-like-macro dig)
+(define-dig-like-macro odig)
+
+
diff --git a/third_party/lisp/quasiquote_2/package.lisp b/third_party/lisp/quasiquote_2/package.lisp
new file mode 100644
index 0000000000..9b140ef84c
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/package.lisp
@@ -0,0 +1,11 @@
+;;;; package.lisp
+
+(defpackage #:quasiquote-2.0
+  (:use #:cl #:iterate)
+  (:export #:%codewalk-dig-form #:transform-dig-form
+	   #:dig #:inject #:splice #:odig #:oinject #:osplice
+	   #:macro-inject #:omacro-inject #:macro-splice #:omacro-splice
+	   #:macro-inject-all #:omacro-inject-all #:macro-splice-all #:omacro-splice-all
+	   #:enable-quasiquote-2.0 #:disable-quasiquote-2.0))
+
+
diff --git a/third_party/lisp/quasiquote_2/quasiquote-2.0.asd b/third_party/lisp/quasiquote_2/quasiquote-2.0.asd
new file mode 100644
index 0000000000..3acfd32b80
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/quasiquote-2.0.asd
@@ -0,0 +1,30 @@
+;;;; quasiquote-2.0.asd
+
+(defpackage :quasiquote-2.0-system
+  (:use :cl :asdf))
+
+(in-package quasiquote-2.0-system)
+
+(asdf:defsystem #:quasiquote-2.0
+  :serial t
+  :description "Writing macros that write macros. Effortless."
+  :author "Alexandr Popolitov <popolit@gmail.com>"
+  :license "MIT"
+  :version "0.3"
+  :depends-on (#:iterate)
+  :components ((:file "package")
+               (:file "quasiquote-2.0")
+	       (:file "macros")
+	       (:file "readers")))
+
+(defsystem :quasiquote-2.0-tests
+  :description "Tests for QUASIQUOTE-2.0"
+  :licence "MIT"
+  :depends-on (:quasiquote-2.0 :fiveam)
+  :components ((:file "tests")
+	       (:file "tests-macro")
+	       ))
+
+(defmethod perform ((op test-op) (sys (eql (find-system :quasiquote-2.0))))
+  (load-system :quasiquote-2.0)
+  (funcall (intern "RUN-TESTS" :quasiquote-2.0)))
diff --git a/third_party/lisp/quasiquote_2/quasiquote-2.0.lisp b/third_party/lisp/quasiquote_2/quasiquote-2.0.lisp
new file mode 100644
index 0000000000..10043fe0ec
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/quasiquote-2.0.lisp
@@ -0,0 +1,340 @@
+;;;; quasiquote-2.0.lisp
+
+(in-package #:quasiquote-2.0)
+
+(defparameter *env* nil)
+
+(defmacro nonsense-error (str)
+  `(error ,(concatenate 'string
+			str
+			" appears as a bare, non DIG-enclosed form. "
+			"For now I don't know how to make sense of this.")))
+
+(defmacro define-nonsense-when-bare (name)
+  `(defmacro ,name (n-or-form &optional form)
+     (declare (ignore n-or-form form))
+     (nonsense-error ,(string name))))
+
+(define-nonsense-when-bare inject)
+(define-nonsense-when-bare oinject)
+(define-nonsense-when-bare splice)
+(define-nonsense-when-bare osplice)
+(define-nonsense-when-bare macro-inject)
+
+(defparameter *depth* 0)
+
+
+(defparameter *injectors* nil)
+
+(defparameter *void-elt* nil)
+(defparameter *void-filter-needed* nil)
+
+;; (defmacro with-injector-parsed (form)
+;;   `(let ((kwd (intern (string 
+
+(defun reset-injectors ()
+  (setf *injectors* nil))
+
+(defparameter *known-injectors* '(inject splice oinject osplice
+				  macro-inject omacro-inject
+				  macro-splice omacro-splice
+				  macro-inject-all omacro-inject-all
+				  macro-splice-all omacro-splice-all))
+
+(defun injector-form-p (form)
+  (and (consp form)
+       (find (car form) *known-injectors* :test #'eq)))
+
+(defun injector-level (form)
+  (if (equal 2 (length form))
+      1
+      (cadr form)))
+
+(defun injector-subform (form)
+  (if (equal 2 (length form))
+      (values (cdr form) '(cdr))
+      (values (cddr form) '(cddr))))
+
+(defparameter *opaque-injectors* '(odig oinject osplice omacro-inject))
+
+(defun transparent-p (form)
+  (not (find (car form) *opaque-injectors* :test #'eq)))
+
+(defun look-into-injector (form path)
+  (let ((*depth* (- *depth* (injector-level form))))
+    (multiple-value-bind (subform subpath) (injector-subform form)
+      (search-all-active-sites subform (append subpath path) nil))))
+
+(defparameter *known-diggers* '(dig odig))
+
+(defun dig-form-p (form)
+  (and (consp form)
+       (find (car form) *known-diggers* :test #'eq)))
+
+(defun look-into-dig (form path)
+  (let ((*depth* (+ *depth* (injector-level form))))
+    (multiple-value-bind (subform subpath) (injector-subform form)
+      (search-all-active-sites subform (append subpath path) nil))))
+
+(defun handle-macro-1 (form)
+  (if (atom form)
+      (error "Sorry, symbol-macros are not implemented for now")
+      (let ((fun (macro-function (car form) *env*)))
+	(if (not fun)
+	    (error "The subform of MACRO-1 injector is supposed to be macro, perhaps, something went wrong..."))
+	(macroexpand-1 form *env*))))
+
+(defun handle-macro-all (form)
+  (if (atom form)
+      (error "Sorry, symbol-macros are not implemented for now")
+      (macroexpand form *env*)))
+
+
+(defparameter *macro-handlers* `((macro-inject . ,#'handle-macro-1)
+				 (omacro-inject . ,#'handle-macro-1)
+				 (macro-splice . ,#'handle-macro-1)
+				 (omacro-splice . ,#'handle-macro-1)
+				 (macro-inject-all . ,#'handle-macro-all)
+				 (omacro-inject-all . ,#'handle-macro-all)
+				 (macro-splice-all . ,#'handle-macro-all)
+				 (omacro-splice-all . ,#'handle-macro-all)))
+
+(defun get-macro-handler (sym)
+  (or (cdr (assoc sym *macro-handlers*))
+      (error "Don't know how to handle this macro injector: ~a" sym)))
+
+	
+
+(defun macroexpand-macroinjector (place)
+  (if (not (splicing-injector (car place)))
+      (progn (setf (car place) (funcall (get-macro-handler (caar place))
+					(car (injector-subform (car place)))))
+	     nil)
+      (let ((new-forms (funcall (get-macro-handler (caar place))
+				(car (injector-subform (car place))))))
+	(cond ((not new-forms)
+	       (setf *void-filter-needed* t
+		     (car place) *void-elt*))
+	      ((atom new-forms) (error "We need to splice the macroexpansion, but got atom: ~a" new-forms))
+	      (t (setf (car place) (car new-forms))
+		 (let ((tail (cdr place)))
+		   (setf (cdr place) (cdr new-forms)
+			 (cdr (last new-forms)) tail))))
+	t)))
+	    
+
+(defun search-all-active-sites (form path toplevel-p)
+  ;; (format t "SEARCH-ALL-ACTIVE-SITES: got form ~a~%" form)
+  (if (not form)
+      nil
+      (if toplevel-p
+	  (cond ((atom (car form)) :just-quote-it!)
+		((injector-form-p (car form)) (if (equal *depth* (injector-level (car form)))
+						  :just-form-it!
+						  (if (transparent-p (car form))
+						      (look-into-injector (car form) (cons 'car path)))))
+		((dig-form-p (car form))
+		 ;; (format t "Got dig form ~a~%" form)
+		 (if (transparent-p (car form))
+		     (look-into-dig (car form) (cons 'car path))))
+		(t (search-all-active-sites (car form) (cons 'car path) nil)
+		   (search-all-active-sites (cdr form) (cons 'cdr path) nil)))
+	  (when (consp form)
+	    (cond ((dig-form-p (car form))
+		   ;; (format t "Got dig form ~a~%" form)
+		   (if (transparent-p (car form))
+		       (look-into-dig (car form) (cons 'car path))))
+		  ((injector-form-p (car form))
+		   ;; (format t "Got injector form ~a ~a ~a~%" form *depth* (injector-level (car form)))
+		   (if (equal *depth* (injector-level (car form)))
+		       (if (macro-injector-p (car form))
+			   (progn (macroexpand-macroinjector form)
+				  (return-from search-all-active-sites
+				    (search-all-active-sites form path nil)))
+			   (progn (push (cons form (cons 'car path)) *injectors*)
+				  nil))
+		       (if (transparent-p (car form))
+			   (look-into-injector (car form) (cons 'car path)))))
+		  (t (search-all-active-sites (car form) (cons 'car path) nil)))
+	    (search-all-active-sites (cdr form) (cons 'cdr path) nil)))))
+
+	  
+	      
+(defun codewalk-dig-form (form)
+  (reset-injectors)
+  (let ((it (search-all-active-sites form nil t)))
+    (values (nreverse *injectors*) it)))
+
+(defun %codewalk-dig-form (form)
+  (if (not (dig-form-p form))
+      (error "Supposed to be called on dig form")
+      (let ((*depth* (+ (injector-level form) *depth*)))
+	(codewalk-dig-form (injector-subform form)))))
+
+(defun path->setfable (path var)
+  (let ((res var))
+    ;; First element is artifact of extra CAR-ing
+    (dolist (spec (cdr (reverse path)))
+      (setf res (list spec res)))
+    res))
+
+(defun tree->cons-code (tree)
+  (if (atom tree)
+      `(quote ,tree)
+      `(cons ,(tree->cons-code (car tree))
+	     ,(tree->cons-code (cdr tree)))))
+
+(defparameter *known-splicers* '(splice osplice
+				 macro-splice omacro-splice
+				 macro-splice-all omacro-splice-all))
+
+(defun splicing-injector (form)
+  (and (consp form)
+       (find (car form) *known-splicers* :test #'eq)))
+
+(defparameter *known-macro-injectors* '(macro-inject omacro-inject
+					macro-splice omacro-splice
+					macro-inject-all omacro-inject-all
+					macro-splice-all omacro-splice-all
+					))
+
+(defun macro-injector-p (form)
+  (and (consp form)
+       (find (car form) *known-macro-injectors* :test #'eq)))
+
+(defun filter-out-voids (lst void-sym)
+  (let (caars cadrs cdars cddrs)
+    ;; search for all occurences of VOID
+    (labels ((rec (x)
+	       (if (consp x)
+		   (progn (cond ((consp (car x))
+				 (cond ((eq void-sym (caar x)) (push x caars))
+				       ((eq void-sym (cdar x)) (push x cdars))))
+				((consp (cdr x))
+				 (cond ((eq void-sym (cadr x)) (push x cadrs))
+				       ((eq void-sym (cddr x)) (push x cddrs)))))
+			  (rec (car x))
+			  (rec (cdr x))))))
+      (rec lst))
+    (if (or cdars cddrs)
+	(error "Void sym found on CDR position, which should not have happened"))
+    ;; destructively transform LST
+    (dolist (elt caars)
+      (setf (car elt) (cdar elt)))
+    (dolist (elt cadrs)
+      (setf (cdr elt) (cddr elt)))
+    ;; check that we indeed filtered-out all VOIDs
+    (labels ((rec (x)
+	       (if (not (atom x))
+		   (progn (rec (car x))
+			  (rec (cdr x)))
+		   (if (eq void-sym x)
+		       (error "Not all VOIDs were filtered")))))
+      (rec lst))
+    lst))
+
+(defun transform-dig-form (form)
+  (let ((the-form (copy-tree form)))
+    (let ((*void-filter-needed* nil)
+	  (*void-elt* (gensym "VOID")))
+      (multiple-value-bind (site-paths cmd) (%codewalk-dig-form the-form)
+	(cond ((eq cmd :just-quote-it!)
+	       `(quote ,(car (injector-subform the-form))))
+	      ((eq cmd :just-form-it!)
+	       (car (injector-subform (car (injector-subform the-form)))))
+	      (t (let ((cons-code (if (not site-paths)
+				      (tree->cons-code (car (injector-subform the-form)))
+				      (really-transform-dig-form the-form site-paths))))
+		   (if (not *void-filter-needed*)
+		       cons-code
+		       `(filter-out-voids ,cons-code ',*void-elt*)))))))))
+
+(defmacro make-list-form (o!-n form)
+  (let ((g!-n (gensym "N"))
+	(g!-i (gensym "I"))
+	(g!-res (gensym "RES")))
+    `(let ((,g!-n ,o!-n)
+	   (,g!-res nil))
+       (dotimes (,g!-i ,g!-n)
+	 (push ,form ,g!-res))
+       (nreverse ,g!-res))))
+
+(defun mk-splicing-injector-let (x)
+  `(let ((it ,(car (injector-subform x))))
+     (assert (listp it))
+     (copy-list it)))
+
+
+
+(defun mk-splicing-injector-setf (path g!-list g!-splicee)
+  (assert (eq 'car (car path)))
+  (let ((g!-rest (gensym "REST")))
+    `(let ((,g!-rest ,(path->setfable (cons 'cdr (cdr path)) g!-list)))
+       (assert (or (not ,g!-rest) (consp ,g!-rest)))
+       (if (not ,g!-splicee)
+	   (setf ,(path->setfable (cdr path) g!-list)
+		 ,g!-rest)
+	   (progn (setf ,(path->setfable (cdr path) g!-list) ,g!-splicee)
+		  (setf (cdr (last ,g!-splicee)) ,g!-rest))))))
+
+
+(defun really-transform-dig-form (the-form site-paths)
+  (let ((gensyms (make-list-form (length site-paths) (gensym "INJECTEE"))))
+    (let ((g!-list (gensym "LIST")))
+      (let ((lets nil)
+	    (splicing-setfs nil)
+	    (setfs nil))
+	(do ((site-path site-paths (cdr site-path))
+	     (gensym gensyms (cdr gensym)))
+	    ((not site-path))
+	  (destructuring-bind (site . path) (car site-path)
+	    (push `(,(car gensym) ,(if (not (splicing-injector (car site)))
+				       (car (injector-subform (car site)))
+				       (mk-splicing-injector-let (car site))))
+		  lets)
+	    (if (not (splicing-injector (car site)))
+		(push `(setf ,(path->setfable path g!-list) ,(car gensym)) setfs)
+		(push (mk-splicing-injector-setf path g!-list (car gensym)) splicing-setfs))
+	    (setf (car site) nil)))
+	`(let ,(nreverse lets)
+	   (let ((,g!-list ,(tree->cons-code (car (injector-subform the-form)))))
+	     ,@(nreverse setfs)
+	     ;; we apply splicing setf in reverse order for them not to bork the paths of each other
+	     ,@splicing-setfs
+	     ,g!-list))))))
+
+
+;; There are few types of recursive injection that may happen:
+;;   * compile-time injection:
+;;     (dig (inject (dig (inject a)))) -- this type will be handled automatically by subsequent macroexpansions
+;;   * run-time injection:
+;;     (dig (dig (inject 2 a)))
+;;     and A is '(dig (inject 3 'foo)) -- this one we guard against ? (probably, first we just ignore it
+;;     -- do not warn about it, and then it wont really happen.
+;;   * macroexpanded compile-time injection:
+;;     (dig (inject (my-macro a b c))),
+;;     where MY-MACRO expands into, say (splice (list 'a 'b 'c))
+;;     This is *not* handled automatically, and therefore we must do it by hand.
+
+      
+;; OK, now how to implement splicing ?
+;;   (dig (a (splice (list b c)) d))
+;; should transform into code that yields
+;;   (a b c d)
+;; what this code is?
+;;   (let ((#:a (copy-list (list b c))))
+;;     (let ((#:res (cons 'a nil 'd)))
+;;       ;; all non-splicing injects go here, as they do not spoil the path-structure
+;;       (setf (cdr #:res) #:a)
+;;       (setf (cdr (last #:a)) (cdr (cdr #:res)))
+;;       #:res)))
+
+
+;; How this macroexpansion should work in general?
+;;   * We go over the cons-tree, keeping track of the depth level, which is
+;;   controlled by DIG's
+;;   * Once we find the INJECT with matching level, we remember the place, where
+;;     this happens
+;;   * We have two special cases:
+;;     * cons-tree is an atom
+;;     * cons-tree is just a single INJECT
diff --git a/third_party/lisp/quasiquote_2/readers.lisp b/third_party/lisp/quasiquote_2/readers.lisp
new file mode 100644
index 0000000000..7c4c5a30c9
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/readers.lisp
@@ -0,0 +1,77 @@
+
+
+(in-package #:quasiquote-2.0)
+
+(defun read-n-chars (stream char)
+  (let (new-char
+	(n 0))
+    (loop
+       (setf new-char (read-char stream nil :eof t))
+       (if (not (char= new-char char))
+	   (progn (unread-char new-char stream)
+		  (return n))
+	   (incf n)))))
+
+(defmacro define-dig-reader (name symbol)
+  `(defun ,name (stream char)
+     (let ((depth (1+ (read-n-chars stream char))))
+       (if (equal 1 depth)
+	   (list ',symbol (read stream t nil t))
+	   (list ',symbol
+		 depth
+		 (read stream t nil t))))))
+
+(define-dig-reader dig-reader dig)
+(define-dig-reader odig-reader odig)
+
+(defun expect-char (char stream)
+  (let ((new-char (read-char stream t nil t)))
+    (if (char= char new-char)
+	t
+	(unread-char new-char stream))))
+
+(defun guess-injector-name (opaque-p macro-p all-p splicing-p)
+  (intern (concatenate 'string
+		       (if opaque-p "O" "")
+		       (if macro-p "MACRO-" "")
+		       (if splicing-p "SPLICE" "INJECT")
+		       (if all-p "-ALL" ""))
+	  "QUASIQUOTE-2.0"))
+
+(defun inject-reader (stream char)
+  (let ((anti-depth (1+ (read-n-chars stream char)))
+	(extended-syntax (expect-char #\! stream)))
+    (let ((injector-name (if (not extended-syntax)
+			     (guess-injector-name nil nil nil (expect-char #\@ stream))
+			     (guess-injector-name (expect-char #\o stream)
+						  (expect-char #\m stream)
+						  (expect-char #\a stream)
+						  (expect-char #\@ stream)))))
+      `(,injector-name ,@(if (not (equal 1 anti-depth)) `(,anti-depth))
+		       ,(read stream t nil t)))))
+
+
+
+(defvar *previous-readtables* nil)
+
+(defun %enable-quasiquote-2.0 ()
+  (push *readtable*
+        *previous-readtables*)
+  (setq *readtable* (copy-readtable))
+  (set-macro-character #\` #'dig-reader)
+  (set-macro-character #\, #'inject-reader)
+  (values))
+
+(defun %disable-quasiquote-2.0 ()
+  (if *previous-readtables*
+      (setf *readtable* (pop *previous-readtables*))
+      (setf *readtable* (copy-readtable nil)))
+  (values))
+
+(defmacro enable-quasiquote-2.0 ()
+  `(eval-when (:compile-toplevel :load-toplevel :execute)
+     (%enable-quasiquote-2.0)))
+(defmacro disable-quasiquote-2.0 ()
+  `(eval-when (:compile-toplevel :load-toplevel :execute)
+     (%disable-quasiquote-2.0)))
+  
diff --git a/third_party/lisp/quasiquote_2/tests-macro.lisp b/third_party/lisp/quasiquote_2/tests-macro.lisp
new file mode 100644
index 0000000000..df6c43e21d
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/tests-macro.lisp
@@ -0,0 +1,21 @@
+
+(in-package #:quasiquote-2.0-tests)
+
+(in-suite quasiquote-2.0)
+
+(enable-quasiquote-2.0)
+
+(defmacro define-sample-macro (name args &body body)
+  `(defmacro ,name ,args
+     `(sample-thing-to-macroexpand-to
+       ,,@body)))
+
+(define-sample-macro sample-macro-1 (x y)
+  ,x ,y)
+
+(define-sample-macro sample-macro-2 (&body body)
+  ,@body)
+
+(test macro-defined-macroexpansions
+  (is (equal '(sample-thing-to-macroexpand-to a b) (macroexpand-1 '(sample-macro-1 a b))))
+  (is (equal '(sample-thing-to-macroexpand-to a b c) (macroexpand-1 '(sample-macro-2 a b c)))))
\ No newline at end of file
diff --git a/third_party/lisp/quasiquote_2/tests.lisp b/third_party/lisp/quasiquote_2/tests.lisp
new file mode 100644
index 0000000000..6c8ab08cc1
--- /dev/null
+++ b/third_party/lisp/quasiquote_2/tests.lisp
@@ -0,0 +1,143 @@
+(in-package :cl-user)
+
+(defpackage :quasiquote-2.0-tests
+  (:use :cl :quasiquote-2.0 :fiveam)
+  (:export #:run-tests))
+
+(in-package :quasiquote-2.0-tests)
+
+(def-suite quasiquote-2.0)
+(in-suite quasiquote-2.0)
+
+(defun run-tests ()
+  (let ((results (run 'quasiquote-2.0)))
+    (fiveam:explain! results)
+    (unless (fiveam:results-status results)
+      (error "Tests failed."))))
+
+(test basic
+  (is (equal '(nil :just-quote-it!) (multiple-value-list (%codewalk-dig-form '(dig nil)))))
+  (is (equal '(nil :just-form-it!) (multiple-value-list (%codewalk-dig-form '(dig (inject a))))))
+  (is (equal '(nil :just-form-it!) (multiple-value-list (%codewalk-dig-form '(dig 2 (inject 2 a))))))
+  (is (equal '(((((inject b) c (inject d)) car cdr car) (((inject d)) car cdr cdr cdr car)) nil)
+	     (multiple-value-list (%codewalk-dig-form '(dig (a (inject b) c (inject d)))))))
+  (is (equal '(nil nil)
+	     (multiple-value-list (%codewalk-dig-form '(dig (dig (a (inject b) c (inject d))))))))
+  (is (equal '(((((inject 2 d)) car cdr cdr cdr car cdr car)) nil)
+	     (multiple-value-list (%codewalk-dig-form '(dig (dig (a (inject b) c (inject 2 d)))))))))
+  
+(test transform
+  (is (equal '(quote a) (transform-dig-form '(dig a))))
+  (is (equal '(quote a) (transform-dig-form '(dig 2 a))))
+  (is (equal 'a (transform-dig-form '(dig (inject a)))))
+  (is (equal 'a (transform-dig-form '(dig 2 (inject 2 a))))))
+
+(defun foo (b d)
+  (dig (a (inject b) c (inject d))))
+
+(defun foo1-transparent (x)
+  (declare (ignorable x))
+  (dig (dig (a (inject (b (inject x) c))))))
+
+(defun foo1-opaque (x)
+  (declare (ignorable x))
+  (dig (dig (a (oinject (b (inject x) c))))))
+
+(defun foo-recursive (x y)
+  (dig (a (inject (list x (dig (c (inject y))))))))
+  
+
+(test foos
+  (is (equal '(a 1 c 2) (foo 1 2)))
+  (is (equal '(a 100 c 200) (foo 100 200))))
+
+(test opaque-vs-transparent
+  (is (equal '(quote a) (transform-dig-form '(odig a))))
+  (is (equal '(quote a) (transform-dig-form '(odig 2 a))))
+  (is (equal 'a (transform-dig-form '(odig (inject a)))))
+  (is (equal 'a (transform-dig-form '(odig 2 (inject 2 a)))))
+  (is (equal '(odig (inject 2 a)) (eval (transform-dig-form '(dig (odig (inject 2 a)))))))
+  (is (equal '(dig (a (inject (b 3 c)))) (foo1-transparent 3)))
+  (is (equal '(dig (a (oinject (b (inject x) c)))) (foo1-opaque 3))))
+
+(test recursive-compile-time
+  (is (equal '(a (1 (c 2))) (foo-recursive 1 2))))
+	     
+
+(test splicing
+  (is (equal '(a b c d) (eval (transform-dig-form '(dig (a (splice '(b c)) d))))))
+  (is (equal '(b c d) (eval (transform-dig-form '(dig ((splice '(b c)) d))))))
+  (is (equal '(a b c) (eval (transform-dig-form '(dig (a (splice '(b c))))))))
+  (is (equal '(a b) (eval (transform-dig-form '(dig (a (splice nil) b))))))
+  (is (equal '(b) (eval (transform-dig-form '(dig ((splice nil) b))))))
+  (is (equal '(a) (eval (transform-dig-form '(dig (a (splice nil)))))))
+  (is (equal '() (eval (transform-dig-form '(dig ((splice nil)))))))
+  (is (equal '(a b) (eval (transform-dig-form '(dig ((splice '(a b)))))))))
+
+
+(test are-they-macro
+  (is (not (equal '(dig (a b)) (macroexpand-1 '(dig (a b))))))
+  (is (not (equal '(odig (a b)) (macroexpand-1 '(odig (a b)))))))
+
+
+(defmacro triple-var (x)
+  `((inject ,x) (inject ,x) (inject ,x)))
+
+(test correct-order-of-effects
+  (is (equal '(a 1 2 3) (let ((x 0))
+			  (dig (a (inject (incf x)) (inject (incf x)) (inject (incf x)))))))
+  (is (equal '(a (((1))) 2)
+	     (let ((x 0))
+	       (dig (a ((((inject (incf x))))) (inject (incf x))))))))
+
+(test macro-injects
+  (is (equal '(a (3 3 3)) (let ((x 3))
+			    (dig (a (macro-inject (triple-var x)))))))
+  (is (equal '(a (1 2 3)) (let ((x 0))
+			    (dig (a (macro-inject (triple-var (incf x))))))))
+  (macrolet ((frob (form n)
+	       (mapcar (lambda (x)
+			 `(inject ,x))
+		       (make-list n :initial-element form)))
+	     (frob1 (form)
+	       `(frob ,form 4)))
+    (is (equal '(a (1 2 3 4 5))
+	       (let ((x 0))
+		 (dig (a (macro-inject (frob (incf x) 5)))))))
+    (is (equal '(a 1 2 3 4 5)
+	       (let ((x 0))
+		 (dig (a (macro-splice (frob (incf x) 5)))))))
+    (is (equal '(a)
+	       (let ((x 0))
+		 (declare (ignorable x))
+		 (dig (a (macro-splice (frob (incf x) 0)))))))
+    (is (equal '(a frob (incf x) 4)
+	       (let ((x 0))
+		 (declare (ignorable x))
+		 (dig (a (macro-splice (frob1 (incf x))))))))
+    (is (equal '(a 1 2 3 4)
+	       (let ((x 0))
+		 (dig (a (macro-splice-all (frob1 (incf x))))))))))
+    
+	       
+(quasiquote-2.0:enable-quasiquote-2.0)
+
+(test reader
+  (is (equal '(inject x) ',x))
+  (is (equal '(inject 3 x) ',,,x))
+  (is (equal '(splice x) ',@x))
+  (is (equal '(splice 3 x) ',,,@x))
+  (is (equal '(omacro-splice-all 4 x) ',,,,!oma@x))
+  (is (equal '(inject 4 oma@x) ',,,,oma@x)))
+
+(test macro-splices
+  (macrolet ((splicer (x)
+	       ``(splice ,x)))
+    (is (equal '(a 1 2 3) (let ((x '(1 2 3)))
+			    `(a ,!m(splicer x)))))))
+
+(test repeated-splices
+  (is (equal '(a) `(a ,@nil ,@nil ,@nil ,@nil)))
+  (is (equal '(a b c d e f g) `(a ,@(list 'b 'c) ,@(list 'd 'e) ,@nil ,@(list 'f 'g)))))
+
+  
\ No newline at end of file
diff --git a/third_party/lisp/rfc2388.nix b/third_party/lisp/rfc2388.nix
new file mode 100644
index 0000000000..b82a490c9d
--- /dev/null
+++ b/third_party/lisp/rfc2388.nix
@@ -0,0 +1,12 @@
+# Implementation of RFC2388 (multipart/form-data)
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.rfc2388;
+in depot.nix.buildLisp.library {
+  name = "rfc2388";
+
+  srcs = map (f: src + ("/" + f)) [
+    "packages.lisp"
+    "rfc2388.lisp"
+  ];
+}
diff --git a/third_party/lisp/routes.nix b/third_party/lisp/routes.nix
new file mode 100644
index 0000000000..fc7d4e3067
--- /dev/null
+++ b/third_party/lisp/routes.nix
@@ -0,0 +1,39 @@
+{ depot, pkgs, ... }:
+
+let
+
+  src = pkgs.applyPatches {
+    name = "routes-source";
+    src = pkgs.fetchFromGitHub {
+      owner = "archimag";
+      repo = "cl-routes";
+      rev = "1b79e85aa653e1ec87e21ca745abe51547866fa9";
+      sha256 = "1zpk3cp2v8hm50ppjl10yxr437vv4552r8hylvizglzrq2ibsbr1";
+    };
+
+    patches = [
+      (pkgs.fetchpatch {
+        name = "fix-build-with-ccl.patch";
+        url = "https://github.com/archimag/cl-routes/commit/2296cdc316ef8e34310f2718b5d35a30040deee0.patch";
+        sha256 = "007c19kmymalam3v6l6y2qzch8xs3xnphrcclk1jrpggvigcmhax";
+      })
+    ];
+  };
+
+in
+depot.nix.buildLisp.library {
+  name = "routes";
+
+  deps = with depot.third_party.lisp; [
+    puri
+    iterate
+    split-sequence
+  ];
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "package.lisp"
+    "uri-template.lisp"
+    "route.lisp"
+    "mapper.lisp"
+  ];
+}
diff --git a/third_party/lisp/s-sysdeps.nix b/third_party/lisp/s-sysdeps.nix
new file mode 100644
index 0000000000..9c4da4a02b
--- /dev/null
+++ b/third_party/lisp/s-sysdeps.nix
@@ -0,0 +1,18 @@
+# A Common Lisp abstraction layer over platform dependent functionality.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.s-sysdeps;
+in depot.nix.buildLisp.library {
+  name = "s-sysdeps";
+
+  srcs = [
+    "${src}/src/package.lisp"
+    "${src}/src/sysdeps.lisp"
+  ];
+
+  deps = with depot.third_party.lisp; [
+    bordeaux-threads
+    usocket
+    usocket-server
+  ];
+}
diff --git a/third_party/lisp/s-xml/0001-fix-definition-order-in-xml.lisp.patch b/third_party/lisp/s-xml/0001-fix-definition-order-in-xml.lisp.patch
new file mode 100644
index 0000000000..9e5838c3c5
--- /dev/null
+++ b/third_party/lisp/s-xml/0001-fix-definition-order-in-xml.lisp.patch
@@ -0,0 +1,26 @@
+From 789dc38399f4039b114de28384c149721d66b030 Mon Sep 17 00:00:00 2001
+From: Vincent Ambo <mail@tazj.in>
+Date: Thu, 16 Dec 2021 00:48:04 +0300
+Subject: [PATCH] fix definition order in xml.lisp
+
+---
+ src/xml.lisp | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/src/xml.lisp b/src/xml.lisp
+index 39c9b63..3232491 100644
+--- a/src/xml.lisp
++++ b/src/xml.lisp
+@@ -19,6 +19,9 @@
+ 
+ ;;; error reporting
+ 
++(defvar *ignore-namespaces* nil
++  "When t, namespaces are ignored like in the old version of S-XML")
++
+ (define-condition xml-parser-error (error)
+   ((message :initarg :message :reader xml-parser-error-message)
+    (args :initarg :args :reader xml-parser-error-args)
+-- 
+2.34.0
+
diff --git a/third_party/lisp/s-xml/default.nix b/third_party/lisp/s-xml/default.nix
new file mode 100644
index 0000000000..486e1c1ac8
--- /dev/null
+++ b/third_party/lisp/s-xml/default.nix
@@ -0,0 +1,25 @@
+# XML serialiser for Common Lisp.
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.applyPatches {
+    name = "s-xml-source";
+    src = pkgs.lispPackages.s-xml.src;
+
+    patches = [
+      ./0001-fix-definition-order-in-xml.lisp.patch
+    ];
+  };
+in
+depot.nix.buildLisp.library {
+  name = "s-xml";
+
+  srcs = map (f: src + ("/src/" + f)) [
+    "package.lisp"
+    "xml.lisp"
+    "dom.lisp"
+    "lxml-dom.lisp"
+    "sxml-dom.lisp"
+    "xml-struct-dom.lisp"
+  ];
+}
diff --git a/third_party/lisp/sclf/.skip-subtree b/third_party/lisp/sclf/.skip-subtree
new file mode 100644
index 0000000000..5051f60d6b
--- /dev/null
+++ b/third_party/lisp/sclf/.skip-subtree
@@ -0,0 +1 @@
+prevent readTree from creating entries for subdirs that don't contain an .nix files
diff --git a/third_party/lisp/sclf/OWNERS b/third_party/lisp/sclf/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/third_party/lisp/sclf/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/third_party/lisp/sclf/README b/third_party/lisp/sclf/README
new file mode 100644
index 0000000000..2a1c2c3c5c
--- /dev/null
+++ b/third_party/lisp/sclf/README
@@ -0,0 +1,6 @@
+SCLF has originally been written by Walter C. Pelissero and vendored
+into depot since it is a dependency of mime4cl. Upstream and depot version
+may diverge.
+
+Upstream Website: http://wcp.sdf-eu.org/software/#sclf
+Vendored Tarball: http://wcp.sdf-eu.org/software/sclf-20150207T213551.tbz
diff --git a/third_party/lisp/sclf/default.nix b/third_party/lisp/sclf/default.nix
new file mode 100644
index 0000000000..fb07f8f764
--- /dev/null
+++ b/third_party/lisp/sclf/default.nix
@@ -0,0 +1,28 @@
+# Copyright (C) 2021 by the TVL Authors
+# SPDX-License-Identifier: LGPL-2.1-or-later
+{ depot, pkgs, ... }:
+
+depot.nix.buildLisp.library {
+  name = "sclf";
+
+  deps = [
+    (depot.nix.buildLisp.bundled "sb-posix")
+  ];
+
+  srcs = [
+    ./package.lisp
+    ./sclf.lisp
+    ./sysproc.lisp
+    ./lazy.lisp
+    ./time.lisp
+    ./directory.lisp
+    ./serial.lisp
+    ./mp/sbcl.lisp
+  ];
+
+  # TODO(sterni): implement OS interaction for ECL and CCL
+  brokenOn = [
+    "ecl"
+    "ccl"
+  ];
+}
diff --git a/third_party/lisp/sclf/directory.lisp b/third_party/lisp/sclf/directory.lisp
new file mode 100644
index 0000000000..3e479c4ac2
--- /dev/null
+++ b/third_party/lisp/sclf/directory.lisp
@@ -0,0 +1,404 @@
+;;;  directory.lisp --- filesystem directory access
+
+;;;  Copyright (C) 2006, 2007, 2008, 2009, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2021 by the TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: sclf
+
+#+cmu (ext:file-comment "$Module: directory.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+
+(cl:in-package :sclf)
+
+(defun pathname-as-directory (pathname)
+  "Converts PATHNAME to directory form and return it."
+  (setf pathname (pathname pathname))
+  (if (pathname-name pathname)
+      (make-pathname :directory (append (or (pathname-directory pathname)
+                                            '(:relative))
+                                        (list (file-namestring pathname)))
+                     :name nil
+                     :type nil
+                     :defaults pathname)
+      pathname))
+
+(defun d+ (path &rest rest)
+  "Concatenate directory pathname parts and return a pathname."
+  (make-pathname :defaults path
+                 :directory (append (pathname-directory path) rest)))
+
+(defun delete-directory (pathname)
+  "Remove directory PATHNAME.  Return PATHNAME."
+  #+cmu (multiple-value-bind (done errno)
+             (unix:unix-rmdir (namestring pathname))
+           (unless done
+             (error "Unable to delete directory ~A (errno=~A)"
+                    pathname errno)))
+  #+sbcl (sb-posix:rmdir pathname)
+  #+lispworks (lw:delete-directory pathname)
+  #-(or cmu sbcl)
+  (error "DELETE-DIRECTORY not implemented for you lisp system.")
+  pathname)
+
+(defun list-directory (pathname &key truenamep)
+  "List content of directory PATHNAME.  If TRUENAMEP is true don't try
+to follow symbolic links."
+  #-(or sbcl cmu) (declare (ignore truenamep))
+  (let (#+cmu (lisp::*ignore-wildcards* t))
+    (directory (make-pathname :defaults (pathname-as-directory pathname)
+                              :name :wild
+                              :type :wild
+                              :version :wild)
+               #+cmu :truenamep #+cmu truenamep
+               #+sbcl :resolve-symlinks #+sbcl truenamep)))
+
+(defun traverse-directory-tree (root-pathname proc &key truenamep test depth-first)
+  "Call PROC on all pathnames under ROOT-PATHNAME, both files and
+directories.  Unless TRUENAMEP is true, this function doesn't try
+to lookup the truename of files, as finding the truename may be a
+superfluous and noxious activity expecially when you expect
+broken symbolic links in your filesystem."
+  (check-type root-pathname pathname)
+  (check-type proc (or function symbol))
+  (check-type test (or function symbol null))
+  (labels ((ls (dir)
+             (declare (type pathname dir))
+             (list-directory dir :truenamep truenamep))
+           (traverse? (file)
+             (declare (type pathname file))
+             (and (not (pathname-name file))
+                  (or truenamep
+                      (not (symbolic-link-p file)))
+                  (or (not test)
+                      (funcall test file))))
+           (traverse-pre-order (dir)
+             (declare (type pathname dir))
+             (loop
+                for file in (ls dir)
+                do (funcall proc file)
+                when (traverse? file)
+                do (traverse-pre-order file)))
+           (traverse-post-order (dir)
+             (declare (type pathname dir))
+             (loop
+                for file in (ls dir)
+                when (traverse? file)
+                do (traverse-post-order file)
+                do (funcall proc file))))
+    (if depth-first
+        (traverse-post-order root-pathname)
+        (traverse-pre-order root-pathname))
+    (values)))
+
+(defmacro do-directory-tree ((file root-pathname &key truenamep test depth-first) &body body)
+  "Call TRAVERSE-DIRECTORY-TREE with BODY es procedure."
+  `(traverse-directory-tree ,root-pathname
+                            #'(lambda (,file)
+                                ,@body)
+                            :truenamep ,truenamep
+                            :test ,test
+                            :depth-first ,depth-first))
+
+(defun empty-directory-p (pathname)
+  (and (directory-p pathname)
+       (endp (list-directory pathname))))
+
+(defun remove-empty-directories (root)
+  (do-directory-tree (pathname root :depth-first t)
+    (when (empty-directory-p pathname)
+      (delete-directory pathname))))
+
+(defun map-directory-tree (pathname function)
+  "Apply FUNCTION to every file in a directory tree starting from
+PATHNAME.  Return the list of results."
+  (be return-list '()
+    (do-directory-tree (directory-entry pathname)
+      (push (funcall function directory-entry) return-list))
+    (nreverse return-list)))
+
+(defun find-files (root-pathname matcher-function &key truenamep)
+  "In the directory tree rooted at ROOT-PATHNAME, find files that
+when the pathname is applied to MATCHER-FUNCTION will return
+true.  Return the list of files found.  Unless TRUENAMEP is true
+this function doesn't try to lookup the truename of
+files. Finding the truename may be a superfluous and noxious
+activity expecially when you expect broken symbolic links in your
+filesystem.  (This may not apply to your particular lisp
+system.)"
+  (be files '()
+    (do-directory-tree (file root-pathname :truenamep truenamep)
+      (when (funcall matcher-function file)
+        (push file files)))
+    (nreverse files)))
+
+(defun delete-directory-tree (pathname)
+  "Recursively delete PATHNAME and all the directory structure below
+it.
+
+WARNING: depending on the way the DIRECTORY function is implemented on
+your Lisp system this function may follow Unix symbolic links and thus
+delete files outside the PATHNAME hierarchy.  Check this before using
+this function in your programs."
+  (if (pathname-name pathname)
+      (delete-file pathname)
+      (progn
+        (dolist (file (list-directory pathname))
+          (delete-directory-tree file))
+        (delete-directory pathname))))
+
+(defun make-directory (pathname &optional (mode #o777))
+  "Create a new directory in the filesystem.  Permissions MODE
+will be assigned to it.  Return PATHNAME."
+  #+cmu (multiple-value-bind (done errno)
+            (unix:unix-mkdir (native-namestring pathname) mode)
+          (unless done
+            (error "Unable to create directory ~A (errno=~A)." pathname errno)))
+  #+sbcl (sb-posix:mkdir pathname mode)
+  #-(or cmu sbcl)
+  (error "MAKE-DIRECTORY is not implemented for this Lisp system.")
+  pathname)
+
+;; At least on SBCL/CMUCL + Unix + NFS this function is faster than
+;; ENSURE-DIRECTORIES-EXIST, because it doesn't check all the pathname
+;; components starting from the root; it proceeds from the leaf and
+;; crawls the directory tree upward only if necessary."
+(defun ensure-directory (pathname &key verbose (mode #o777))
+  "Just like ENSURE-DIRECTORIES-EXIST but, in some situations,
+it's faster."
+  (labels ((ensure (path)
+             (unless (probe-file path)
+               (be* tail (last (pathname-directory path) 2)
+                    last (cdr tail)
+                 (setf (cdr tail) nil)
+                 (unwind-protect
+                      (ensure path)
+                   (setf (cdr tail) last))
+                 (make-directory path mode)
+                 (when verbose
+                   (format t "Created ~S~%" path))))))
+    (ensure (make-pathname :defaults pathname
+                           :name nil :type nil
+                           :version nil))))
+
+(defun make-temp-directory (&optional (default-pathname *tmp-file-defaults*) (mode #o777))
+  "Create a new directory and return its pathname.
+If DEFAULT-PATHNAME is specified and not NIL it's used as
+defaults to produce the pathname of the directory.  Return the
+pathname of the temporary directory."
+  (loop
+     for name = (pathname-as-directory (temp-file-name default-pathname))
+     when (ignore-errors (make-directory name mode))
+     return name))
+
+(defmacro with-temp-directory ((path &rest make-temp-directory-args) &body body)
+  "Execute BODY with PATH bound to the pathname of a new unique
+temporary directory.  On exit of BODY the directory tree starting from
+PATH will be automatically removed from the filesystem.  Return what
+BODY returns.  BODY is _not_ executed within the PATH directory; the
+working directory is never changed."
+  `(be ,path (make-temp-directory ,@make-temp-directory-args)
+     (unwind-protect
+          (progn ,@body)
+       (delete-directory-tree ,path))))
+
+(defun current-directory ()
+  "Return the pathname of the current directory."
+  (truename (make-pathname :directory '(:relative))))
+
+(defun ensure-home-translations ()
+  "Ensure that the logical pathname translations for the host \"home\"
+are defined."
+  ;; CMUCL already defines a HOME translation of its own and gets
+  ;; angry if we try to redefine it
+  #-cmu
+  (be home (user-homedir-pathname)
+    ;; we should discard and replace whatever has been defined in any
+    ;; rc file during compilation
+    (setf (logical-pathname-translations "home")
+          (list
+           (list "**;*.*.*"
+                 (make-pathname :defaults home
+                                :directory (append (pathname-directory home)
+                                                   '(:wild-inferiors))
+                                :name :wild
+                                :type :wild))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun parse-native-namestring (string &optional host (defaults *default-pathname-defaults*)
+                                &key (start 0) end junk-allowed)
+  #+sbcl (sb-ext:parse-native-namestring string host defaults
+                                         :start start
+                                         :end end
+                                         :junk-allowed junk-allowed)
+  #-sbcl (let (#+cmu(lisp::*ignore-wildcards* t))
+           (parse-namestring string host defaults
+                             :start start
+                             :end end
+                             :junk-allowed junk-allowed)))
+
+(defun native-namestring (pathname)
+  #+sbcl (sb-ext:native-namestring pathname)
+  #-sbcl (let (#+cmu (lisp::*ignore-wildcards* t))
+           (namestring pathname)))
+
+(defun native-file-namestring (pathname)
+  #+sbcl (sb-ext:native-namestring
+          (make-pathname :name (pathname-name pathname)
+                         :type (pathname-type pathname)))
+  #+cmu (be lisp::*ignore-wildcards* t
+          (file-namestring pathname)))
+
+(defun native-pathname (thing)
+  #+sbcl (sb-ext:native-pathname thing)
+  #+cmu (be lisp::*ignore-wildcards* t
+          (pathname thing)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun bits-set-p (x bits)
+  (= (logand x bits)
+     bits))
+
+(defun directory-p (pathname)
+  "Return true if PATHNAME names a directory on the filesystem."
+  #-clisp (awhen (unix-stat (native-namestring pathname))
+            (bits-set-p (stat-mode it)
+                        #+sbcl sb-posix:s-ifdir
+                        #+cmu unix:s-ifdir))
+  #+clisp (ext:probe-directory (pathname-as-directory pathname)))
+
+(defun regular-file-p (pathname)
+  "Return true if PATHNAME names a regular file on the filesystem."
+  #-(or sbcl cmu) (error "don't know how to check whether a file might be a regular file")
+  (awhen (unix-stat (native-namestring pathname))
+    (bits-set-p (stat-mode it)
+                #+sbcl sb-posix:s-ifreg
+                #+cmu unix:s-ifreg)))
+
+(defun file-readable-p (pathname)
+  #+sbcl (sb-unix:unix-access (native-namestring pathname) sb-unix:r_ok)
+  #+cmu (unix:unix-access (native-namestring pathname) unix:r_ok)
+  #-(or sbcl cmu) (error "don't know how to check whether a file might be readable"))
+
+(defun file-writable-p (pathname)
+  #+sbcl (sb-unix:unix-access (native-namestring pathname) sb-unix:w_ok)
+  #+cmu (unix:unix-access (native-namestring pathname) unix:w_ok)
+  #-(or sbcl cmu) (error "don't know how to check whether a file might be writable"))
+
+(defun file-executable-p (pathname)
+  #+sbcl (sb-unix:unix-access (native-namestring pathname) sb-unix:x_ok)
+  #+cmu (unix:unix-access (native-namestring pathname) unix:x_ok)
+  #-(or sbcl cmu) (error "don't know how to check whether a file might be executable"))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defstruct (unix-file-stat (:conc-name stat-))
+  device
+  inode
+  links
+  atime
+  mtime
+  ctime
+  size
+  blksize
+  blocks
+  uid
+  gid
+  mode)
+
+(defun unix-stat (pathname)
+  ;; this could be different depending on the unix systems
+  (multiple-value-bind (ok? device inode mode links uid gid rdev
+                            size atime mtime ctime
+                            blksize blocks)
+      (#+cmu unix:unix-lstat
+       #+sbcl sb-unix:unix-lstat
+       (if (stringp pathname)
+           pathname
+           (native-namestring pathname)))
+    (declare (ignore rdev))
+    (when ok?
+      (make-unix-file-stat :device device
+                           :inode inode
+                           :links links
+                           :atime atime
+                           :mtime mtime
+                           :ctime ctime
+                           :size size
+                           :blksize blksize
+                           :blocks blocks
+                           :uid uid
+                           :gid gid
+                           :mode mode))))
+
+(defun stat-modification-time (stat)
+  "Return the modification time of the STAT structure as Lisp
+Universal Time, which is not the same as the Unix time."
+  (unix->universal-time (stat-mtime stat)))
+
+(defun stat-creation-time (stat)
+  "Return the creation time of the STAT structure as Lisp
+Universal Time, which is not the same as the Unix time."
+  (unix->universal-time (stat-ctime stat)))
+
+(defun file-modification-time (file)
+  "Return the modification time of FILE as Lisp Universal Time, which
+is not the same as the Unix time."
+  (awhen (unix-stat file)
+    (stat-modification-time it)))
+
+(defun file-creation-time (file)
+  "Return the creation time of FILE as Lisp Universal Time, which
+is not the same as the Unix time."
+  (awhen (unix-stat file)
+    (stat-creation-time it)))
+
+(defun read-symbolic-link (symlink)
+  "Return the pathname the SYMLINK points to.  That is, it's
+contents."
+  #+sbcl (sb-posix:readlink (native-namestring symlink))
+  #+cmu (unix:unix-readlink (native-namestring symlink)))
+
+;; FILE-LENGTH is a bit idiosyncratic in this respect.  Besides, Unix
+;; allows to get to know the file size without being able to open a
+;; file; just ask politely.
+(defun file-size (pathname)
+  (stat-size (unix-stat pathname)))
+
+(defun symbolic-link-p (pathname)
+  #-(or sbcl cmu) (error "don't know hot to test for symbolic links.")
+  (aand (unix-stat pathname)
+        (bits-set-p (stat-mode it)
+                    #+sbcl sb-posix:s-iflnk
+                    #+cmu unix:s-iflnk)))
+
+(defun broken-link-p (pathname)
+ (when (symbolic-link-p pathname)
+   #+cmu (not (ignore-errors (truename pathname)))
+   ;; On a broken symlink SBCL returns the link path without resolving
+   ;; the link itself.  De gustibus non est disputandum.
+   #+sbcl (equalp pathname (probe-file pathname))))
+
+(defun move-file (old new)
+  "Just like RENAME-FILE, but doesn't carry on to NEW file the type of
+OLD file, if NEW doesn't specify one.  It does what most people would
+expect from a rename function, which RENAME-FILE doesn't do.
+So (MOVE-FILE \"foo.bar\" \"foo\") does rename foo.bar to foo, losing
+the \"bar\" type; RENAME-FILE wouldn't allow you that."
+  #+sbcl (sb-posix:rename (native-namestring old) (native-namestring new))
+  #+cmu (unix:unix-rename (native-namestring old) (native-namestring new)))
diff --git a/third_party/lisp/sclf/lazy.lisp b/third_party/lisp/sclf/lazy.lisp
new file mode 100644
index 0000000000..34bae82ebb
--- /dev/null
+++ b/third_party/lisp/sclf/lazy.lisp
@@ -0,0 +1,134 @@
+;;;  lazy.lisp --- lazy primitives
+
+;;;  Copyright (C) 2008, 2009, 2010 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: sclf
+
+#+cmu (ext:file-comment "$Module: lazy.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;
+;;; Lazy primitives
+;;;
+
+(in-package :sclf)
+
+(defstruct promise
+  procedure
+  value)
+
+(defmacro lazy (form)
+  `(make-promise :procedure #'(lambda () ,form)))
+
+(defun forced-p (promise)
+  (null (promise-procedure promise)))
+
+(defun force (promise)
+  (if (forced-p promise)
+      (promise-value promise)
+      (prog1 (setf (promise-value promise)
+                   (funcall (promise-procedure promise)))
+        (setf (promise-procedure promise) nil))))
+
+(defmacro deflazy (name value &optional documentation)
+  `(defparameter ,name (lazy ,value)
+     ,@(when documentation
+             (list documentation))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass lazy-metaclass (standard-class)
+  ()
+  (:documentation "Metaclass for object having lazy slots.  Lazy slots
+should be specified with the :LAZY keyword which must be a function of
+one argument.  If required this function will be called once to get
+the value to memoize in the slot.  Lazy slots can also be set/read as
+any other."))
+
+(defmethod validate-superclass ((class lazy-metaclass) (super standard-class))
+  "Lazy classes may inherit from ordinary classes."
+  (declare (ignore class super))
+  t)
+
+(defmethod validate-superclass ((class standard-class) (super lazy-metaclass))
+  "Ordinary classes may inherit from lazy classes."
+  (declare (ignore class super))
+  t)
+
+(defclass lazy-slot-mixin ()
+  ((lazy-function :initarg :lazy
+                   :reader lazy-slot-function
+                   :initform nil))
+  (:documentation
+   "Slot for LAZY-METACLASS classes.  Lazy slots must be declared with
+the argument :LAZY which must be a function accepting the object
+instance as argument."))
+
+(defclass lazy-direct-slot-definition (lazy-slot-mixin standard-direct-slot-definition)
+  ())
+
+(defclass lazy-effective-slot-definition (lazy-slot-mixin standard-effective-slot-definition)
+  ())
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmethod direct-slot-definition-class ((class lazy-metaclass) &rest initargs)
+  (if (getf initargs :lazy nil)
+      (find-class 'lazy-direct-slot-definition)
+      (call-next-method)))
+
+(defmethod effective-slot-definition-class ((class lazy-metaclass) &rest initargs)
+  (if (getf initargs :lazy nil)
+      (find-class 'lazy-effective-slot-definition)
+      (call-next-method)))
+
+(defmethod compute-effective-slot-definition-initargs ((class lazy-metaclass) direct-slots)
+  (let ((ds (car direct-slots)))
+    (if (typep ds 'lazy-direct-slot-definition)
+      (let ((form (lazy-slot-function ds))
+            (args (call-next-method)))
+        (when (or (getf args :initarg)
+                  (getf args :initform))
+          (error "Lazy slot ~S cannot have :INITARG nor :INITFORM arguments." ds))
+        (list* :lazy
+               (cond ((and (listp form)
+                           (eq 'lambda (car form)))
+                      (compile nil form))
+                     ((symbolp form)
+                      form)
+                     (t (compile nil `(lambda (self)
+                                        (declare (ignorable self))
+                                        ,form))))
+               args))
+      (call-next-method))))
+
+(defmethod slot-value-using-class ((class lazy-metaclass) instance (slot lazy-slot-mixin))
+  (declare (ignore class))
+  ;; If the slot is unbound, call the lazy function passing the
+  ;; instance and memoize the value in the slot.
+  (unless (slot-boundp-using-class class instance slot)
+    (setf (slot-value-using-class class instance slot)
+          (funcall (lazy-slot-function slot) instance)))
+  (call-next-method))
+
+(defun reset-lazy-slots (object)
+  "Unbind all the lazy slots in OBJECT so that they will be
+re-evaluated next time their value is requested again."
+  (be* class (class-of object)
+    (dolist (slot (class-slots class))
+      (when (typep slot 'lazy-effective-slot-definition)
+        (slot-makunbound object (slot-definition-name slot))))))
\ No newline at end of file
diff --git a/third_party/lisp/sclf/mp/README b/third_party/lisp/sclf/mp/README
new file mode 100644
index 0000000000..a0732c0294
--- /dev/null
+++ b/third_party/lisp/sclf/mp/README
@@ -0,0 +1,6 @@
+This directory contains an uniforming layer for multiprocessing in the
+style supported by Allegro Common Lisp and CMUCL.  Almost nothing of
+this has been written by me.  It's mostly the work of Gilbert Baumann
+(unk6@rz.uni-karlsruhe.de) and I've shamelessly lifted it from McCLIM.
+The copyright disclaimer in this code is compatible with the one of
+SCLF, so I believe there should be no legal issues.
diff --git a/third_party/lisp/sclf/mp/cmu.lisp b/third_party/lisp/sclf/mp/cmu.lisp
new file mode 100644
index 0000000000..1bdbba7989
--- /dev/null
+++ b/third_party/lisp/sclf/mp/cmu.lisp
@@ -0,0 +1,115 @@
+;;;
+;;; Code freely lifted from various places with compatible license
+;;; terms.  Most of this code is copyright Gilbert Baumann
+;;; <unk6@rz.uni-karlsruhe.de>.  The bugs are copyright Walter
+;;; C. Pelissero <walter@pelissero.de>.
+;;;
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Library General Public
+;;; License as published by the Free Software Foundation; either
+;;; version 2 of the License, or (at your option) any later version.
+;;;
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Library General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Library General Public
+;;; License along with this library; if not, write to the 
+;;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, 
+;;; Boston, MA  02111-1307  USA.
+
+(in-package :sclf)
+
+(defun make-lock (&optional name)
+  (mp:make-lock name))
+
+(defun make-recursive-lock (&optional name)
+  (mp:make-lock name :kind :recursive))
+
+(defmacro with-lock-held ((lock &key whostate (wait t) timeout) &body forms)
+  `(mp:with-lock-held (,lock ,(or whostate "Lock Wait")
+                             :wait wait
+                             ,@(when timeout (list :timeout timeout)))
+     ,@forms))
+
+(defmacro with-recursive-lock-held ((lock &key wait timeout) &body forms)
+  `(mp:with-lock-held (,lock
+                       ,@(when wait (list :wait wait))
+                       ,@(when timeout (list :timeout timeout)))
+     ,@forms))
+
+(defstruct condition-variable
+  (lock (make-lock "condition variable"))
+  (value nil)
+  (process-queue nil))
+
+(defun %release-lock (lock) ; copied from with-lock-held in multiproc.lisp
+  #+i486 (kernel:%instance-set-conditional
+          lock 2 mp:*current-process* nil)
+  #-i486 (when (eq (lock-process lock) mp:*current-process*)
+           (setf (lock-process lock) nil)))
+
+(defun condition-wait (cv lock &optional timeout)
+  (declare (ignore timeout))		;For now
+  (loop
+     (let ((cv-lock (condition-variable-lock cv)))
+       (with-lock-held (cv-lock)
+         (when (condition-variable-value cv)
+           (setf (condition-variable-value cv) nil)
+           (return-from condition-wait t))
+         (setf (condition-variable-process-queue cv)
+               (nconc (condition-variable-process-queue cv)
+                      (list mp:*current-process*)))
+         (%release-lock lock))
+       (mp:process-add-arrest-reason mp:*current-process* cv)
+       (let ((cv-val nil))
+         (with-lock-held (cv-lock)
+           (setq cv-val (condition-variable-value cv))
+           (when cv-val
+             (setf (condition-variable-value cv) nil)))
+         (when cv-val
+           (mp::lock-wait lock "waiting for condition variable lock")
+           (return-from condition-wait t))))))
+
+(defun condition-notify (cv)
+  (with-lock-held ((condition-variable-lock cv))
+    (let ((proc (pop (condition-variable-process-queue cv))))
+      ;; The waiting process may have released the CV lock but not
+      ;; suspended itself yet
+      (when proc
+        (loop
+         for activep = (mp:process-active-p proc)
+         while activep
+         do (mp:process-yield))
+        (setf (condition-variable-value cv) t)
+        (mp:process-revoke-arrest-reason proc cv))))
+  ;; Give the other process a chance
+  (mp:process-yield))
+
+(defun process-execute (process function)
+  (mp:process-preset process function)
+  ;; For some obscure reason process-preset doesn't make the process
+  ;; runnable.  I'm sure it's me who didn't understand how
+  ;; multiprocessing works under CMUCL, despite the vast documentation
+  ;; available.
+  (mp:enable-process process)
+  (mp:process-add-run-reason process :enable))
+
+(defun destroy-process (process)
+  ;; silnetly ignore a process that is trying to destroy itself
+  (unless (eq (mp:current-process)
+              process)
+    (mp:destroy-process process)))
+
+(defun restart-process (process)
+  (mp:restart-process process)
+  (mp:enable-process process)
+  (mp:process-add-run-reason process :enable))
+
+(defun process-alive-p (process)
+  (mp:process-alive-p process))
+
+(defun process-join (process)
+  (error "PROCESS-JOIN not support under CMUCL."))
diff --git a/third_party/lisp/sclf/mp/sbcl.lisp b/third_party/lisp/sclf/mp/sbcl.lisp
new file mode 100644
index 0000000000..a2cf497ff9
--- /dev/null
+++ b/third_party/lisp/sclf/mp/sbcl.lisp
@@ -0,0 +1,235 @@
+;;;
+;;; Code freely lifted from various places with compatible license
+;;; terms.  Most of this code is copyright Daniel Barlow
+;;; <dan@metacircles.com> or Gilbert Baumann
+;;; <unk6@rz.uni-karlsruhe.de>.  The bugs are copyright Walter
+;;; C. Pelissero <walter@pelissero.de>.
+;;;
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Library General Public
+;;; License as published by the Free Software Foundation; either
+;;; version 2 of the License, or (at your option) any later version.
+;;;
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Library General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Library General Public
+;;; License along with this library; if not, write to the 
+;;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, 
+;;; Boston, MA  02111-1307  USA.
+
+(in-package :sclf)
+
+(defstruct (process
+             (:constructor %make-process)
+             (:predicate processp))
+  name
+  state
+  whostate
+  function
+  thread)
+
+(defvar *current-process*
+  (%make-process
+   :name "initial process" :function nil
+   :thread
+   #+#.(cl:if (cl:find-symbol "THREAD-NAME" "SB-THREAD") '(and) '(or))
+   sb-thread:*current-thread*
+   #-#.(cl:if (cl:find-symbol "THREAD-NAME" "SB-THREAD") '(and) '(or))
+   (sb-thread:current-thread-id)))
+
+(defvar *all-processes* (list *current-process*))
+
+(defvar *all-processes-lock*
+  (sb-thread:make-mutex :name "Lock around *ALL-PROCESSES*"))
+
+;; we implement disable-process by making the disablee attempt to lock
+;; *permanent-queue*, which is already locked because we locked it
+;; here.  enable-process just interrupts the lock attempt.
+
+(defmacro get-mutex (mutex &optional (wait t))
+  `(
+    #+#.(cl:if (cl:find-symbol "GRAB-MUTEX" "SB-THREAD") '(and) '(or))
+        sb-thread:grab-mutex
+        #-#.(cl:if (cl:find-symbol "GRAB-MUTEX" "SB-THREAD") '(and) '(or))
+        sb-thread:get-mutex
+        ,mutex :waitp ,wait))
+
+(defvar *permanent-queue*
+  (sb-thread:make-mutex :name "Lock for disabled threads"))
+(unless (sb-thread:mutex-owner *permanent-queue*)
+  (get-mutex *permanent-queue* nil))
+
+(defun make-process (function &key name)
+  (let ((p (%make-process :name name
+                          :function function)))
+    (sb-thread:with-mutex (*all-processes-lock*)
+      (pushnew p *all-processes*))
+    (restart-process p)))
+
+(defun process-kill-thread (process)
+  (let ((thread (process-thread process)))
+    (when (and thread
+               (sb-thread:thread-alive-p thread))
+      (assert (not (eq thread sb-thread:*current-thread*)))
+      (sb-thread:terminate-thread thread)
+      ;; Wait until all the clean-up forms are done.
+      (sb-thread:join-thread thread :default nil))
+    (setf (process-thread process) nil)))
+
+(defun process-join (process)
+  (sb-thread:join-thread (process-thread process)))
+
+(defun restart-process (p)
+  (labels ((boing ()
+             (let ((*current-process* p)
+                   (function (process-function p)))
+               (when function
+                 (funcall function)))))
+    (process-kill-thread p)
+    (when (setf (process-thread p)
+                (sb-thread:make-thread #'boing :name (process-name p)))
+      p)))
+
+(defun destroy-process (process)
+  (sb-thread:with-mutex (*all-processes-lock*)
+    (setf *all-processes* (delete process *all-processes*)))
+  (process-kill-thread process))
+
+(defun current-process ()
+  *current-process*)
+
+(defun all-processes ()
+  ;; we're calling DELETE on *ALL-PROCESSES*.  If we look up the value
+  ;; while that delete is executing, we could end up with nonsense.
+  ;; Better use a lock (or call REMOVE instead in DESTROY-PROCESS).
+  (sb-thread:with-mutex (*all-processes-lock*)
+    *all-processes*))
+
+(defun process-yield ()
+  (sb-thread:thread-yield))
+
+(defun process-wait (reason predicate)
+  (let ((old-state (process-whostate *current-process*)))
+    (unwind-protect
+         (progn
+           (setf old-state (process-whostate *current-process*)
+                 (process-whostate *current-process*) reason)
+           (until (funcall predicate)
+             (process-yield)))
+      (setf (process-whostate *current-process*) old-state))))
+
+(defun process-wait-with-timeout (reason timeout predicate)
+  (let ((old-state (process-whostate *current-process*))
+        (end-time (+ (get-universal-time) timeout)))
+    (unwind-protect
+         (progn
+           (setf old-state (process-whostate *current-process*)
+                 (process-whostate *current-process*) reason)
+           (loop 
+              for result = (funcall predicate)
+              until (or result
+                        (> (get-universal-time) end-time))
+              do (process-yield)
+              finally (return result)))
+      (setf (process-whostate *current-process*) old-state))))
+
+(defun process-interrupt (process function)
+  (sb-thread:interrupt-thread (process-thread process) function))
+
+(defun disable-process (process)
+  (sb-thread:interrupt-thread
+   (process-thread process)
+   (lambda ()
+     (catch 'interrupted-wait (get-mutex *permanent-queue*)))))
+
+(defun enable-process (process)
+  (sb-thread:interrupt-thread
+   (process-thread process) (lambda () (throw 'interrupted-wait nil))))
+
+(defmacro without-scheduling (&body body)
+  (declare (ignore body))
+  (error "WITHOUT-SCHEDULING is not supported on this platform."))
+
+(defparameter *atomic-lock*
+  (sb-thread:make-mutex :name "atomic incf/decf"))
+
+(defmacro atomic-incf (place)
+  `(sb-thread:with-mutex (*atomic-lock*)
+    (incf ,place)))
+
+(defmacro atomic-decf (place) 
+  `(sb-thread:with-mutex (*atomic-lock*)
+    (decf ,place)))
+
+;;; 32.3 Locks
+
+(defun make-lock (&optional name)
+  (sb-thread:make-mutex :name name))
+
+(defmacro with-lock-held ((place &key state (wait t) timeout) &body body)
+  (declare (ignore timeout))
+  (let ((old-state (gensym "OLD-STATE")))
+    `(sb-thread:with-mutex (,place :wait-p ,wait)
+       (let (,old-state)
+         (unwind-protect
+              (progn
+                (when ,state
+                  (setf ,old-state (process-state *current-process*))
+                  (setf (process-state *current-process*) ,state))
+                ,@body)
+           (setf (process-state *current-process*) ,old-state))))))
+
+
+(defun make-recursive-lock (&optional name)
+  (sb-thread:make-mutex :name name))
+
+(defmacro with-recursive-lock-held ((place &optional state (wait t) timeout) &body body)
+  (declare (ignore wait timeout))
+  (let ((old-state (gensym "OLD-STATE")))
+  `(sb-thread:with-recursive-lock (,place)
+    (let (,old-state)
+      (unwind-protect
+           (progn
+             (when ,state
+               (setf ,old-state (process-state *current-process*))
+               (setf (process-state *current-process*) ,state))
+             ,@body)
+        (setf (process-state *current-process*) ,old-state))))))
+
+(defun make-condition-variable () (sb-thread:make-waitqueue))
+
+(defun condition-wait (cv lock &optional timeout)
+  (if timeout
+      (handler-case 
+          (sb-ext:with-timeout timeout
+            (sb-thread:condition-wait cv lock)
+            t)
+        (sb-ext:timeout (c)
+          (declare (ignore c))
+          nil))
+      (progn (sb-thread:condition-wait cv lock) t)))
+
+(defun condition-notify (cv)
+  (sb-thread:condition-notify cv))
+
+
+(defvar *process-plists* (make-hash-table)
+  "Hash table mapping processes to a property list.  This is used by
+PROCESS-PLIST.")
+
+(defun process-property-list (process)
+  (gethash process *process-plists*))
+
+(defun (setf process-property-list) (value process)
+  (setf (gethash process *process-plists*) value))
+
+(defun process-execute (process function)
+  (setf (process-function process) function)
+  (restart-process process))
+
+(defun process-alive-p (process)
+  (sb-thread:thread-alive-p (process-thread process)))
diff --git a/third_party/lisp/sclf/package.lisp b/third_party/lisp/sclf/package.lisp
new file mode 100644
index 0000000000..565ab301c7
--- /dev/null
+++ b/third_party/lisp/sclf/package.lisp
@@ -0,0 +1,258 @@
+;;;  package.lisp --- packages description
+
+;;;  Copyright (C) 2006, 2007, 2008, 2009, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2021 by the TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: sclf
+
+#+cmu (ext:file-comment "$Module: package.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :cl-user)
+
+(defpackage :sclf
+  (:use :common-lisp
+        ;; we need the MOP for lazy.lisp and serial.lisp
+        #+cmu :pcl
+        #+sbcl :sb-mop)
+  ;; Don't know why but compute-effective-slot-definition-initargs is
+  ;; internal in both CMUCL and SBCL
+  (:import-from #+cmu"PCL" #+sbcl"SB-PCL"
+                #-(or cmu sbcl) "CLOS"
+                "COMPUTE-EFFECTIVE-SLOT-DEFINITION-INITARGS")
+  #+cmu (:import-from :mp
+                      #:make-process
+                      #:current-process
+                      #:all-processes
+                      #:processp
+                      #:process-name
+                      #:process-state
+                      #:process-whostate
+                      #:process-wait
+                      #:process-wait-with-timeout
+                      #:process-yield
+                      #:process-interrupt
+                      #:disable-process
+                      #:enable-process
+                      #:without-scheduling
+                      #:atomic-incf
+                      #:atomic-decf
+                      #:process-property-list)
+  (:export #:be #:be*
+           #:defconst
+           #:with-gensyms
+           #:d+
+           #:s+
+           #:f++
+           #:list->string
+           #:string-starts-with #:string-ends-with
+           #:aif #:awhen #:acond #:aand #:acase #:it
+           #:+whitespace+
+           #:string-trim-whitespace
+           #:string-right-trim-whitespace
+           #:string-left-trim-whitespace
+           #:whitespace-p #:seq-whitespace-p
+           #:not-empty
+           #:position-any
+           #:+month-names+
+           #:find-any
+           #:split-at
+           #:split-string-at-char
+           #:week-day->string
+           #:month->string
+           #:month-string->number
+           #:add-months #:add-days
+           #:read-whole-stream
+           #:read-file #:write-file #:read-lines
+           #:read-from-file #:write-to-file
+           #:string-concat
+           #:gcase
+           #:string-truncate
+           #:promise #:force #:forced-p #:lazy #:deflazy #:lazy-metaclass #:self #:reset-lazy-slots
+           #:copy-stream #:copy-file
+           #:symlink-file
+           #:keywordify
+           #:until
+           #:year #:month #:day #:hour #:minute #:week-day #:week #:day-of-the-year
+           #:beginning-of-week #:end-of-week
+           #:next-week-day #:next-monday #:full-weeks-in-span
+           #:beginning-of-first-week #:end-of-last-week
+           #:beginning-of-month #:end-of-month
+           #:locate-system-program
+           #:*tmp-file-defaults*
+           #:temp-file-name
+           #:open-temp-file
+           #:with-temp-file
+           #:file-size
+           #:getenv
+           #:with-system-environment
+           #:time-string #:iso-time-string #:parse-iso-time-string
+           #:soundex
+           #:string-soundex=
+           #:lru-cache
+           #:getcache #:cached
+           #:print-time-span
+           #:double-linked-list #:limited-list #:sorted-list
+           #:insert #:size
+           #:heap #:heap-add #:heap-pop #:heap-empty-p
+           #:double-linked-element #:make-double-linked-element #:double-linked-element-p
+           #:dle-previous #:dle-next #:dle-value
+           #:cons-dle #:dle-remove #:dle-map #:do-dle :do-dle*
+           #:sl-map #:do-dll #:do-dll*
+           #:dll-find #:dll-find-cursor
+           #:push-first #:push-last #:dll-remove
+           #:pop-first #:pop-last
+           #:leap-year-p #:last-day-of-month
+           #:getuid #:setuid #:with-euid
+           #:get-logname #:get-user-name #:get-user-home #:find-uid
+           #:super-user-p
+           #:pathname-as-directory #:pathname-as-file
+           #:alist->plist #:plist->alist
+           #:byte-vector->string
+           #:string->byte-vector
+           #:outdated-p
+           #:with-hidden-temp-file
+           #:let-places #:let-slots
+           #:*decimal-point*
+           #:*thousands-comma*
+           #:format-amount #:parse-amount
+           #:with-package
+           #:make-directory #:ensure-directory
+           #:make-temp-directory
+           #:with-temp-directory
+           #:delete-directory
+           #:delete-directory-tree
+           #:do-directory-tree
+           #:traverse-directory-tree
+           #:empty-directory-p
+           #:remove-empty-directories
+           #:map-directory-tree
+           #:find-files
+           #:directory-p
+           #:regular-file-p
+           #:file-readable-p
+           #:file-writable-p
+           #:file-executable-p
+           #:current-directory
+           #:ensure-home-translations
+           #:list-directory
+           #:string-escape
+           #:string-substitute
+           #:bytes-simple-string
+           #:make-lock-files
+           #:with-lock-files
+           #:getpid
+           #:on-error
+           #:floor-to
+           #:round-to
+           #:ceiling-to
+           #:insert-in-order
+           #:forget-documentation
+           #:load-compiled
+           #:swap
+           #:queue #:make-queue #:queue-append #:queue-pop #:queue-empty-p
+           #:unix-stat #:unix-file-stat
+           #:stat-device
+           #:stat-inode
+           #:stat-links
+           #:stat-atime
+           #:stat-mtime
+           #:stat-ctime
+           #:stat-birthtime
+           #:stat-size
+           #:stat-blksize
+           #:stat-blocks
+           #:stat-uid
+           #:stat-gid
+           #:stat-mode
+           #:save-file-excursion
+           #:stat-modification-time
+           #:stat-creation-time
+           #:file-modification-time
+           #:file-creation-time
+           #:show
+           #:memoize-function
+           #:memoized
+           #:defun-memoized
+           #:parse-native-namestring
+           #:native-file-namestring
+           #:native-namestring
+           #:native-pathname
+           #:read-symbolic-link
+           #:symbolic-link-p
+           #:broken-link-p
+           #:circular-list
+           #:last-member
+           #:glob->regex
+           #:universal->unix-time #:unix->universal-time
+           #:get-unix-time
+           #:move-file
+
+           ;; sysproc.lisp
+           #:*run-verbose*
+           #:run-pipe
+           #:run-program
+           #:run-shell-command
+           #:run-async-shell-command
+           #:exit-code
+           #:with-open-pipe
+           #:*bourne-shell*
+           #:sysproc-kill
+           #:sysproc-input
+           #:sysproc-output
+           #:sysproc-alive-p
+           #:sysproc-pid
+           #:sysproc-p
+           #:sysproc-wait
+           #:sysproc-exit-code
+           #:sysproc-set-signal-callback
+
+           ;; MP
+           #:make-process
+           #:destroy-process
+           #:current-process
+           #:all-processes
+           #:processp
+           #:process-name
+           #:process-state
+           #:process-whostate
+           #:process-wait
+           #:process-wait-with-timeout
+           #:process-yield
+           #:process-interrupt
+           #:disable-process
+           #:enable-process
+           #:restart-process
+           #:without-scheduling
+           #:atomic-incf
+           #:atomic-decf
+           #:process-property-list
+           #:process-alive-p
+           #:process-join
+           ;;
+           #:make-lock
+           #:with-lock-held
+           #:make-recursive-lock
+           #:with-recursive-lock-held
+           ;;
+           #:make-condition-variable
+           #:condition-wait
+           #:condition-notify
+           #:process-property-list
+           #:process-execute
+           ;; mop.lisp
+           #:printable-object-mixin
+           ))
diff --git a/third_party/lisp/sclf/sclf.asd b/third_party/lisp/sclf/sclf.asd
new file mode 100644
index 0000000000..a9754b7569
--- /dev/null
+++ b/third_party/lisp/sclf/sclf.asd
@@ -0,0 +1,58 @@
+;;;  sclf.asd --- system definition
+
+;;;  Copyright (C) 2005, 2006, 2008, 2009 by Walter C. Pelissero
+;;;  Copyright (C) 2021 by the TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: SCLF
+
+#+cmu (ext:file-comment "$Module: sclf.asd, Time-stamp: <2013-06-17 15:32:29 wcp> $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :cl-user)
+
+(defpackage :sclf-system
+  (:use :common-lisp :asdf #+asdfa :asdfa))
+
+(in-package :sclf-system)
+
+(defsystem sclf
+    :name "SCLF"
+    :author "Walter C. Pelissero <walter@pelissero.de>"
+    :maintainer "Walter C. Pelissero <walter@pelissero.de>"
+    ;; :version "0.0"
+    :description "Stray Common Lisp Functions"
+    :long-description
+    "A collection of Common Lisp functions for the most disparate
+uses, too small to fit anywhere else."
+    :licence "LGPL"
+    :depends-on (#+sbcl :sb-posix)
+    :components
+    ((:doc-file "README")
+     (:file "package")
+     (:file "sclf" :depends-on ("package"))
+     (:file "sysproc" :depends-on ("package" "sclf"))
+     (:file "lazy" :depends-on ("package" "sclf"))
+     (:file "time" :depends-on ("package" "sclf"))
+     (:file "directory" :depends-on ("package" "sclf" "time"))
+     (:file "serial" :depends-on ("package" "sclf"))
+     (:module "mp"
+              :depends-on ("package" "sclf")
+              :components
+              ((:doc-file "README")
+               (:file #.(first
+                         (list #+cmu "cmu"
+                               #+sbcl "sbcl"
+                               "unknown")))))))
diff --git a/third_party/lisp/sclf/sclf.lisp b/third_party/lisp/sclf/sclf.lisp
new file mode 100644
index 0000000000..dfbc2078c8
--- /dev/null
+++ b/third_party/lisp/sclf/sclf.lisp
@@ -0,0 +1,1717 @@
+;;;  sclf.lisp --- miscellanea
+
+;;;  Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: SCLF
+
+#+cmu (ext:file-comment "$Module: sclf.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+;;;  Commentary:
+
+;;; This is a collection of Common Lisp functions of the most disparate
+;;; uses and purposes.  These functions are too small or too unrelated
+;;; to each other to deserve an own module.
+;;;
+;;; If you want to indent properly the following macros you should add
+;;; the following lines to your .emacs file:
+;;;
+;;; (defun cl-indent-be (path state indent-point sexp-column normal-indent)
+;;;   (let ((sexp-start (cadr state))
+;;; 	(i 0))
+;;;     (save-excursion
+;;;       (goto-char sexp-start)
+;;;       (forward-char)
+;;;       (+ sexp-column
+;;; 	 (block indentation
+;;; 	   (condition-case nil
+;;; 	       (while (< (point) indent-point)
+;;; 		 (setq i (1+ i))
+;;; 		 (when (and (= 0 (logand i 1))
+;;; 			    (looking-at "[\t\n ]*\\s("))
+;;; 		   (return-from indentation 2))
+;;; 		 (forward-sexp))
+;;; 	     (error nil))
+;;; 	   (if (= 1 (logand i 1))
+;;; 	       6 4))))))
+;;;
+;;; (put 'be 'common-lisp-indent-function 'cl-indent-be)
+;;; (put 'be* 'common-lisp-indent-function 'cl-indent-be)
+;;; (put 'awhen 'lisp-indent-function 1)
+;;; (put 'gcase 'lisp-indent-function 1)
+;;; (put 'acase 'lisp-indent-function 1)
+;;; (put 'acond 'lisp-indent-function 1)
+;;; (put 'until 'lisp-indent-function 1)
+
+
+
+(cl:in-package :sclf)
+
+(defmacro be (&rest bindings-and-body)
+  "Less-parenthetic let."
+  (let ((bindings
+         (loop
+            while (and (symbolp (car bindings-and-body))
+                       (cdr bindings-and-body))
+            collect (list (pop bindings-and-body)
+                          (pop bindings-and-body)))))
+    `(let ,bindings
+       ,@bindings-and-body)))
+
+(defmacro be* (&rest bindings-and-body)
+  "Less-parenthetic let*."
+  (let ((bindings
+         (loop
+            while (and (symbolp (car bindings-and-body))
+                       (cdr bindings-and-body))
+            collect (list (pop bindings-and-body)
+                          (pop bindings-and-body)))))
+    `(let* ,bindings
+       ,@bindings-and-body)))
+
+(defmacro defconst (name value &rest etc)
+  "For some reason SBCL, between usefulness and adherence to the ANSI
+standard, has chosen the latter, thus rendering the DEFCONSTANT pretty
+useless.  This macro works around that problem."
+  #+sbcl (list* 'defvar name value etc)
+  #-sbcl (list* 'defconstant name value etc))
+
+(defmacro with-gensyms ((&rest symbols) &body body)
+  "Gensym all SYMBOLS and make them available in BODY.
+See also LET-GENSYMS."
+  `(let ,(mapcar #'(lambda (s)
+                     (list s '(gensym))) symbols)
+     ,@body))
+
+(defun s+ (&rest strings)
+  "Return a string which is made of the concatenation of STRINGS."
+  (apply #'concatenate 'string strings))
+
+(defun string-starts-with (prefix string &optional (compare #'string=))
+  (be prefix-length (length prefix)
+    (and (>= (length string) prefix-length)
+         (funcall compare prefix string :end2 prefix-length))))
+
+(defun string-ends-with (postfix string &optional (compare #'string=))
+  "Return true if STRING's last characters are the same as POSTFIX."
+  (be postfix-length (length postfix)
+      string-length (length string)
+    (and (>= string-length postfix-length)
+         (funcall compare postfix string :start2 (- string-length postfix-length)))))
+
+(defun string-substitute (from to sequence &key (start 0) end (test #'eql))
+  "Replace in SEQUENCE occurrences of FROM with TO.  FROM and TO don't
+need to be the same length."
+  (be from-length (length from)
+    (with-output-to-string (out)
+      (write-string sequence out :start 0 :end start)
+      (loop
+         for position = (search from sequence :start2 start :end2 end :test test)
+         while position
+         do
+           (write-string sequence out :start start :end position)
+           (write-string to out)
+           (setf start (+ position from-length))
+         finally (write-string (subseq sequence start) out)))))
+
+(defun string-escape (string character &key (escape-character #\\) (escape-escape t))
+  "Prepend all occurences of CHARACTER in STRING with a
+ESCAPE-CHARACTER."
+  (with-output-to-string (stream)
+    (loop
+       for c across string
+       when (or (char= c character)
+                (and escape-escape
+                     (char= c escape-character)))
+       do (write-char escape-character stream)
+       do (write-char c stream))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmacro aif (test then &optional else)
+  `(be it ,test
+     (if it
+         ,then
+         ,else)))
+
+(defmacro awhen (test &body then)
+  `(be it ,test
+     (when it
+       ,@then)))
+
+(defmacro acond (&body forms)
+  (when forms
+    `(aif ,(caar forms)
+          (progn ,@(cdar forms))
+          (acond ,@(cdr forms)))))
+
+(defmacro aand (&rest args)
+  (cond ((null args) t)
+        ((null (cdr args)) (car args))
+        (t `(aif ,(car args) (aand ,@(cdr args))))))
+
+(defmacro acase (condition &body forms)
+  `(be it ,condition
+     (case it ,@forms)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst +whitespace+ '(#\return #\newline #\tab #\space #\page))
+
+(defun string-trim-whitespace (string)
+  (string-trim +whitespace+ string))
+
+(defun string-right-trim-whitespace (string)
+  (string-right-trim +whitespace+ string))
+
+(defun string-left-trim-whitespace (string)
+  (string-left-trim +whitespace+ string))
+
+(defun whitespace-p (char)
+  (member char +whitespace+))
+
+(defun seq-whitespace-p (sequence)
+  (every #'whitespace-p sequence))
+
+(defun not-empty (sequence)
+  "Return SEQUENCE if it's not empty, otherwise NIL.
+NIL is indeed empty."
+  (when (or (listp sequence)
+            (not (zerop (length sequence))))
+      sequence))
+
+(defun position-any (bag sequence &rest position-args)
+  "Find any element of bag in sequence and return its position.
+Accept any argument accepted by the POSITION function."
+  (apply #'position-if #'(lambda (element)
+                           (find element bag)) sequence position-args))
+
+(defun find-any (bag sequence &rest find-args)
+  "Find any element of bag in sequence.  Accept any argument
+accepted by the FIND function."
+  (apply #'find-if #'(lambda (element)
+                           (find element bag)) sequence find-args))
+
+(defun split-at (bag sequence &key (start 0) key)
+  "Split SEQUENCE at occurence of any element from BAG.
+Contiguous occurences of elements from BAG are considered atomic;
+so no empty sequence is returned."
+  (be len (length sequence)
+    (labels ((split-from (start)
+               (unless (>= start len)
+                 (be sep (position-any bag sequence :start start :key key)
+                   (cond ((not sep)
+                          (list (subseq sequence start)))
+                         ((> sep start)
+                          (cons (subseq sequence start sep)
+                                (split-from (1+ sep))))
+                         (t
+                          (split-from (1+ start))))))))
+      (split-from start))))
+
+(defun split-string-at-char (string separator &key escape skip-empty)
+  "Split STRING at SEPARATORs and return a list of the substrings.  If
+SKIP-EMPTY is true then filter out the empty substrings.  If ESCAPE is
+not nil then split at SEPARATOR only if it's not preceded by ESCAPE."
+  (declare (type string string) (type character separator))
+  (labels ((next-separator (beg)
+             (be pos (position separator string :start beg)
+               (if (and escape
+                        pos
+                        (plusp pos)
+                        (char= escape (char string (1- pos))))
+                   (next-separator (1+ pos))
+                   pos)))
+           (parse (beg)
+             (cond ((< beg (length string))
+                    (let* ((end (next-separator beg))
+                           (substring (subseq string beg end)))
+                      (cond ((and skip-empty (string= "" substring))
+                             (parse (1+ end)))
+                            ((not end)
+                             (list substring))
+                            (t
+                             (cons substring (parse (1+ end)))))))
+                   (skip-empty
+                    '())
+                   (t
+                    (list "")))))
+    (parse 0)))
+
+(defun copy-stream (in out)
+  (loop
+     for c = (read-char in nil)
+     while c
+     do (write-char c out)))
+
+(defun pathname-as-file (pathname)
+  "Converts PATHNAME to file form and return it."
+  (unless (pathnamep pathname)
+    (setf pathname (pathname pathname)))
+  (cond ((pathname-name pathname)
+         pathname)
+        ((stringp (car (last (pathname-directory pathname))))
+         (be name (parse-native-namestring (car (last (pathname-directory pathname))))
+           (make-pathname :directory (butlast (pathname-directory pathname))
+                          :name (pathname-name name)
+                          :type (pathname-type name)
+                          :defaults pathname)))
+        ;; it can't be done?
+        (t pathname)))
+
+(defun copy-file (file copy-file &key (if-exists :error))
+  (with-open-file (in file)
+    (with-open-file (out copy-file :direction :output :if-exists if-exists)
+      (copy-stream in out))))
+
+(defun symlink-file (src dst &key (if-exists :error))
+  (when (and (eq :supersede if-exists)
+             (probe-file dst))
+    (delete-file dst))
+  #+sbcl (sb-posix:symlink src dst)
+  #+cmu(unix:unix-symlink (native-namestring src) (native-namestring dst))
+  #-(or sbcl cmu) (error "don't know how to symlink files"))
+
+(defun read-whole-stream (stream)
+  "Read stream until the end and return it as a string."
+  (with-output-to-string (string)
+    (loop
+       for line = (read-line stream nil)
+       while line
+       do (write-line line string))))
+
+(defun read-lines (stream &optional n)
+  "Read N lines from stream and return them as a list of strings.  If
+N is NIL, read the whole stream til the end.  If the stream ends
+before N lines a read, this function will return those without
+signalling an error."
+  (loop
+     for line = (read-line stream nil)
+     for i from 0
+     while (and line
+                (or (not n)
+                    (< i n)))
+     collect line))
+
+(defun read-file (pathname &key (element-type 'character) (if-does-not-exist :error) default)
+  "Read the whole content of file and return it as a sequence which
+can be a string, a vector of bytes, or whatever you specify as
+ELEMENT-TYPE."
+  (with-open-file (in pathname
+                      :element-type element-type
+                      :if-does-not-exist (unless (eq :value if-does-not-exist)
+                                           :error))
+    (if in
+        (be seq (make-array (file-length in) :element-type element-type)
+          (read-sequence seq in)
+          seq)
+        default)))
+
+(defun write-file (pathname contents &key (if-exists :error))
+  "Read the whole content of file and return it as a sequence which
+can be a string, a vector of bytes, or whatever you specify as
+ELEMENT-TYPE."
+  (with-open-file (out pathname
+                       :element-type (if (stringp contents)
+                                         'character
+                                         (array-element-type contents))
+                       :if-exists if-exists)
+    (write-sequence contents out)))
+
+(defun read-from-file (pathname &key (on-error :error) default)
+  "Similar to READ-FROM-STRING but for files.  Read the first Lisp
+object in file and return it.  If file does not exist or does not
+contain a readable Lisp object, ON-ERROR tells what to do.  If
+ON-ERROR is :ERROR, an error is signalled.  If ON-ERROR is :VALUE,
+DEFAULT is returned."
+  (ecase on-error
+    (:error
+     (with-open-file (in pathname)
+       (read in)))
+    (:value
+     (handler-case (with-open-file (in pathname)
+                     (read in))
+       (t ()
+         default)))))
+
+(defun write-to-file (object pathname &key (if-exists :error) pretty)
+  "Similar to WRITE-TO-STRING but for files.  Write OBJECT to a file
+with pathname PATHNAME."
+  (with-open-file (out pathname :direction :output :if-exists if-exists)
+    (write object :stream out :escape t :readably t :pretty pretty)))
+
+(defun string-concat (list &optional (separator ""))
+  "Concatenate the strings in LIST interposing SEPARATOR (default
+nothing) between them."
+  (reduce #'(lambda (&rest args)
+              (if args
+                  (s+ (car args) separator (cadr args))
+                  ""))
+          list))
+
+;; to indent it properly: (put 'gcase 'lisp-indent-function 1)
+(defmacro gcase ((value &optional (test 'equalp)) &rest cases)
+  "Generic CASE macro.  Match VALUE to CASES as if by the normal CASE
+but use TEST as the comparison function, which defaults to EQUALP."
+  (with-gensyms (val)
+    `(be ,val ,value
+       ,(cons 'cond
+              (mapcar #'(lambda (case-desc)
+                          (destructuring-bind (vals &rest forms) case-desc
+                            `(,(cond ((consp vals)
+                                      (cons 'or (mapcar #'(lambda (v)
+                                                            (list test val v))
+                                                        vals)))
+                                     ((or (eq vals 'otherwise)
+                                          (eq vals t))
+                                      t)
+                                     (t (list test val vals)))
+                               ,@forms)))
+                      cases)))))
+
+(defun string-truncate (string max-length)
+  "If STRING is longer than MAX-LENGTH, return a shorter version.
+Otherwise return the same string unchanged."
+  (if (> (length string) max-length)
+      (subseq string 0 max-length)
+      string))
+
+;; to indent properly: (put 'until 'lisp-indent-function 1)
+(defmacro until (test &body body)
+  (with-gensyms (result)
+    `(loop
+        for ,result = ,test
+        until ,result
+        do (progn ,@body)
+        finally (return ,result))))
+
+(defun keywordify (string)
+  (intern (string-upcase string) :keyword))
+
+(defun locate-system-program (name)
+  "Given the NAME of a system program try to find it through the
+search of the environment variable PATH.  Return the full
+pathname."
+  (loop
+     for dir in (split-string-at-char (getenv "PATH") #\:)
+     for pathname = (merge-pathnames name (pathname-as-directory dir))
+     when (probe-file pathname)
+     return pathname))
+
+(defvar *tmp-file-defaults* #P"/tmp/")
+
+(defun temp-file-name (&optional (default *tmp-file-defaults*))
+  "Create a random pathname based on DEFAULT.  No effort is made
+to make sure that the returned pathname doesn't identify an
+already existing file.  If missing DEFAULT defaults to
+*TMP-FILE-DEFAULTS*."
+  (make-pathname :defaults default
+                 :name (format nil "~36R" (random #.(expt 36 10)))))
+
+(defun open-temp-file (&optional default-pathname &rest open-args)
+  "Open a new temporary file and return a stream to it.  This function
+makes sure the pathname of the temporary file is unique.  OPEN-ARGS
+are arguments passed verbatim to OPEN.  If OPEN-ARGS specify
+the :DIRECTION it should be either :OUTPUT (default) or :IO;
+any other value causes an error.  If DEFAULT-PATHNAME is specified and
+not NIL it's used as defaults to produce the pathname of the temporary
+file, otherwise *TMP-FILE-DEFAULTS* is used."
+  (unless default-pathname
+    (setf default-pathname *tmp-file-defaults*))
+  ;; if :DIRECTION is specified check that it's compatible with the
+  ;; purpose of this function, otherwise make it default to :OUTPUT
+  (aif (getf open-args :direction)
+       (unless (member it '(:output :io))
+         (error "Can't create temporary file with open direction ~A." it))
+       (setf open-args (append '(:direction :output)
+                               open-args)))
+  (do* ((name #1=(temp-file-name default-pathname) #1#)
+        (stream #2=(apply #'open  name
+                          :if-exists nil
+                          :if-does-not-exist :create
+                          open-args) #2#))
+       (stream stream)))
+
+(defmacro with-temp-file ((stream &rest open-temp-args) &body body)
+  "Execute BODY within a dynamic extent where STREAM is bound to
+a STREAM open on a unique temporary file name.  OPEN-TEMP-ARGS are
+passed verbatim to OPEN-TEMP-FILE."
+  `(be ,stream (open-temp-file ,@open-temp-args)
+     (unwind-protect
+          (progn ,@body)
+       (close ,stream)
+       ;; body may decide to rename the file so we must ignore the errors
+       (ignore-errors
+         (delete-file (pathname ,stream))))))
+
+(defmacro with-hidden-temp-file ((stream &rest open-args) &body body)
+  "Just like WITH-TEMP-FILE but unlink (delete) the temporary file
+before the execution of BODY.  As such BODY won't be able to
+manipulate the file but through STREAM, and no other program is able
+to see it.  Once STREAM is closed the temporary file blocks are
+automatically relinquished by the operating system.  This works at
+least on Unix filesystems.  I don't know about MS-OSs where the system
+may likely decide to crash, take all your data with it and, in the
+meanwhile, report you to the NSA as terrorist."
+  `(be ,stream (open-temp-file ,@open-args)
+     (unwind-protect
+          (progn (delete-file (pathname ,stream))
+                 ,@body)
+       (close ,stream))))
+
+(defun insert-in-order (item seq &key (test #'<) key)
+  "Destructively insert ITEM in LIST in order by TEST.  Return
+the new list.  This is a simple wrapper around MERGE."
+  (merge (if seq
+             (type-of seq)
+             'list)
+         (list item) seq test :key key))
+
+(defmacro f++ (x &optional (delta 1))
+  "Same as INCF but hopefully optimised for fixnums."
+  `(setf ,x (+ (the fixnum ,x) (the fixnum ,delta))))
+
+(defun soundex (word &optional (key-length 4))
+  "Knuth's Soundex algorithm.  Returns a string representing the
+sound of a certain word (English).  Different words will thus
+yield the same output string.  To compare two string by the
+sound, simply do:
+
+   (string= (soundex str1) (soundex str2))
+
+Examples:
+
+   (soundex \"Knuth\") => \"K530\"
+   (soundex \"Kant\") => \"K530\"
+   (soundex \"Lloyd\") => \"L300\"
+   (soundex \"Ladd\") => \"L300\""
+  (declare (type string word))
+  (flet ((translate-char (char)
+           (awhen (position char "BFPVCGJKQSXZDTLMNR")
+             (elt "111122222222334556" it))))
+    (let ((key (make-string key-length :initial-element #\0))
+          (word-length (length word)))
+      (setf (elt key 0) (elt word 0))
+      (loop
+         with previous-sound = (translate-char (char-upcase (elt word 0)))
+         with j = 1
+         for i from 1 by 1 below word-length
+         for c = (char-upcase (elt word i))
+         while (< j key-length)
+         do (be sound (translate-char c)
+              (cond ((not (eq sound previous-sound))
+                     (unless (member c '(#\H #\W))
+                       (setf previous-sound sound))
+                     (when sound
+                       (setf (elt key j) sound)
+                       (incf j))))))
+      key)))
+
+(defun string-soundex= (string1 string2)
+  (let ((l1 (split-at +whitespace+ string1))
+        (l2 (split-at +whitespace+ string2)))
+    (and (= (length l1) (length l2))
+         (every #'string= (mapcar #'soundex l1) (mapcar #'soundex l2)))))
+
+#+(OR)
+(defun soundex-test ()
+  (let* ((words1 '("Euler" "Gauss" "Hilbert" "Knuth" "Lloyd" "Lukasiewicz" "Wachs"))
+         (words2 '("Ellery" "Ghosh" "Heilbronn" "Kant" "Ladd" "Lissajous" "Waugh"))
+         (results '("E460" "G200" "H416" "K530" "L300" "L222" "W200")))
+    (mapc #'(lambda (w1 w2 r)
+              (let ((r1 (soundex w1))
+                    (r2 (soundex w2)))
+                (format t "~A = ~A, ~A = ~A => ~A~%" w1 r1 w2 r2
+                        (if (and (string= r1 r2)
+                                 (string= r r1))
+                            "OK"
+                            (format nil "ERROR (expected ~A)" r)))))
+          words1 words2 results)
+    (values)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; (defstruct cache-slot ()
+;;   ((previous :type (or cache-slot null)
+;; 	     :initarg :previous
+;; 	     :initform nil
+;; 	     :accessor cslot-previous)
+;;    (key :initarg :key
+;; 	:accessor cslot-key)
+;;    (value :initarg :value
+;; 	  :accessor cslot-value)
+;;    (next :type (or cache-slot null)
+;; 	 :initarg :next
+;; 	 :initform nil
+;; 	 :accessor cslot-next)))
+
+;; (defmethod print-object ((object cache-slot) stream)
+;;   (print-unreadable-object (object stream :type t)
+;;     (if (slot-boundp object 'key)
+;; 	(format stream "key=~S, value=~S" (cslot-key object) (cslot-value object))
+;; 	(format stream "NULL"))))
+
+
+(defstruct (double-linked-element (:conc-name dle-))
+  (previous nil :type (or double-linked-element null))
+  value
+  (next nil :type (or double-linked-element null)))
+
+(defmethod print-object ((object double-linked-element) stream)
+  (print-unreadable-object (object stream :type t)
+    (format stream "value=~S" (dle-value object))))
+
+(defun cons-dle (value previous next)
+  (declare (type (or double-linked-element null) previous next))
+  (be new-element (make-double-linked-element :previous previous :next next :value value)
+    (when previous
+      (setf (dle-next previous) new-element))
+    (when next
+      (setf (dle-previous next) new-element))
+    new-element))
+
+(defun dle-remove (dle-object)
+  "Remove the DLE-OBJECT from its current position in the list of
+elements agjusting the pointer of dle-objects before and after this
+one (if any)."
+  (declare (type double-linked-element dle-object))
+  (awhen (dle-next dle-object)
+    (setf (dle-previous it) (dle-previous dle-object)))
+  (awhen (dle-previous dle-object)
+    (setf (dle-next it) (dle-next dle-object))))
+
+(defun dle-map (function dle-object)
+  (when dle-object
+    (make-double-linked-element :value (funcall function (dle-value dle-object))
+                                :previous (dle-previous dle-object)
+                                :next (dle-map function (dle-next dle-object)))))
+
+(defmacro do-dle ((var dle &optional (result nil)) &body body)
+  "Iterate over a list of DOUBLE-LINKED-ELEMENTs and map body to
+each element's value.  Bind VAR to the value on each iteration."
+  (be cursor (gensym)
+    `(do ((,cursor ,dle (dle-next ,cursor)))
+         ((not ,cursor) ,result)
+       (be ,var (dle-value ,cursor)
+         ,@body))))
+
+(defmacro do-dle* ((var dle &optional (result nil)) &body body)
+  "Same as DO-DLE but VAR is a symbol macro, so that BODY can
+modify the element's value."
+  (be cursor (gensym)
+    `(symbol-macrolet ((,var (dle-value ,cursor)))
+       (do ((,cursor ,dle (dle-next ,cursor)))
+           ((not ,cursor) ,result)
+         ,@body))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass double-linked-list ()
+  ((elements :type double-linked-element
+             :documentation "The actual list of elements held by this object.")
+   (last-element :type double-linked-element))
+  (:documentation
+   "A double linked list where elements can be added or removed
+from either end."))
+
+(defmethod initialize-instance ((object double-linked-list) &rest rest)
+  (declare (ignorable rest))
+  (call-next-method)
+  (with-slots (last-element elements) object
+    (setf last-element (make-double-linked-element)
+          elements last-element)))
+
+(defmethod print-object ((object double-linked-list) stream)
+  (print-unreadable-object (object stream :type t)
+    (be elements '()
+      (do-dle (e (slot-value object 'elements))
+        (push e elements))
+      (format stream "elements=~S" (nreverse elements)))))
+
+(defgeneric pop-first (double-linked-list)
+  (:documentation
+   "Pop the first element of a double-linked-list."))
+(defgeneric pop-last (double-linked-list)
+  (:documentation
+   "Pop the last element of a double-linked-list."))
+(defgeneric push-first (item double-linked-list)
+  (:documentation
+   "Push an item in front of a double-linked-list."))
+(defgeneric push-last (item double-linked-list)
+  (:documentation
+   "Append an item to a double-linked-list."))
+(defgeneric list-map (function double-linked-list)
+  (:documentation
+   "Map a function to a double-linked-list."))
+(defgeneric dll-find-cursor (object dll &key test key))
+(defgeneric dll-find (object dll &key test key))
+(defgeneric dll-remove (cursor dll))
+
+(defmethod pop-last ((list double-linked-list))
+  "Drop the last element in the dl list."
+  (with-slots (last-element) list
+    (awhen (dle-previous last-element)
+      (dle-remove it)
+      (dle-value it))))
+
+(defmethod pop-first ((list double-linked-list))
+  "Drop the first element in the dl list."
+  (with-slots (elements) list
+    (when (dle-next elements)
+      (prog1 (dle-value elements)
+        (setf (dle-previous (dle-next elements)) nil
+              elements (dle-next elements))))))
+
+(defmethod push-first (value (list double-linked-list))
+  (with-slots (elements) list
+    (setf elements (cons-dle value nil elements)))
+  list)
+
+(defmethod push-last (value (list double-linked-list))
+  (with-slots (last-element) list
+    (cons-dle value (dle-previous last-element) last-element))
+  list)
+
+(defmethod list-map (function (list double-linked-list))
+  (labels ((map-dll (dle)
+             (when (dle-next dle)
+               (make-double-linked-element
+                :value (funcall function (dle-value dle))
+                :previous (dle-previous dle)
+                :next (map-dll (dle-next dle))))))
+    (map-dll (slot-value list 'elements))))
+
+(defmethod dll-find-cursor (object (list double-linked-list) &key (test #'eql) (key #'identity))
+  (do ((cursor (slot-value list 'elements) (dle-next cursor)))
+      ((not (dle-next cursor)))
+    (be value (dle-value cursor)
+      (when (funcall test (funcall key value) object)
+        (return cursor)))))
+
+(defmethod dll-find (object (list double-linked-list) &key (test #'eql) (key #'identity))
+  (awhen (dll-find-cursor object list :test test :key key)
+    (dle-value it)))
+
+(defmethod dll-remove ((cursor double-linked-element) (list double-linked-list))
+  (with-slots (elements) list
+    (if (dle-previous cursor)
+        (dle-remove cursor)
+        (setf (dle-previous (dle-next elements)) nil
+              elements (dle-next elements))))
+  list)
+
+(defmacro do-dll ((var list &optional (result nil)) &body body)
+  "Iterate over a dll and map body to each element's
+value.  Bind VAR to the value on each iteration."
+  (be cursor (gensym)
+    `(do ((,cursor (slot-value ,list 'elements) (dle-next ,cursor)))
+         ((not (dle-next ,cursor)) ,result)
+       (be ,var (dle-value ,cursor)
+         ,@body))))
+
+(defmacro do-dll* ((var list &optional (result nil)) &body body)
+  "Same as DO-DLL but VAR is a symbol macro, so that BODY can
+modify the element's value."
+  (be cursor (gensym)
+    `(symbol-macrolet ((,var (dle-value ,cursor)))
+       (do ((,cursor (slot-value ,list 'elements) (dle-next ,cursor)))
+           ((not (dle-next ,cursor)) ,result)
+         ,@body))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass limited-list (double-linked-list)
+  ((max-size :initform nil
+             :initarg :size
+             :reader max-size
+             :type (or integer null)
+             :documentation "Size limit to which the list is allowed to grow to.  NIL = no limit.")
+   (size :initform 0
+         :reader size
+         :type integer
+         :documentation "Current number of elements in the list."))
+  (:documentation
+   "A double linked list where the maximum number of elements can
+be limited."))
+
+(defun dll-member-p (dle list)
+  (with-slots (elements size) list
+    (do ((e elements (dle-next e)))
+        ((not e))
+      (when (eq e dle)
+        (return t)))))
+
+(defmethod dll-remove ((cursor double-linked-element) (list limited-list))
+  (with-slots (size) list
+    (unless (zerop size)
+      (decf size)
+      (call-next-method)))
+  list)
+
+(defmethod pop-first ((list limited-list))
+  (with-slots (size) list
+    (unless (zerop size)
+      (decf size)
+      (call-next-method))))
+
+(defmethod pop-last ((list limited-list))
+  (with-slots (size) list
+    (unless (zerop size)
+      (decf size)
+      (call-next-method))))
+
+(defmethod push-first (value (list limited-list))
+  "Add in front of the list and drop the last element if list is
+full."
+  (declare (ignore value))
+  (prog1 (call-next-method)
+    (with-slots (max-size size last-element) list
+      (if (or (not max-size)
+              (< size max-size))
+          (incf size)
+          (dle-remove (dle-previous last-element))))))
+
+(defmethod push-last (value (list limited-list))
+  "Add at the end of the list and drop the first element if list
+is full."
+  (declare (ignore value))
+  (prog1 (call-next-method)
+    (with-slots (max-size size elements) list
+      (if (or (not max-size)
+              (< size max-size))
+        (incf size)
+        (setf (dle-previous (dle-next elements)) nil
+              elements (dle-next elements))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass sorted-list (limited-list)
+  ((test :type function
+         :initarg :test))
+  (:documentation
+   "A double linked list where elements are inserted in a
+sorted order."))
+
+(defgeneric insert (item sorted-list)
+  (:documentation
+   "Insert an item in a sorted-list."))
+
+(defmethod insert (item (sl sorted-list))
+  "Insert ITEM in SL, which is a sorted double linked list,
+before the item for which TEST is true or at the end of the list.
+Returns two values, the modified list and the cursor to the new
+element."
+  (with-slots (max-size size elements test last-element) sl
+    (do ((cursor elements (dle-next cursor)))
+        ((or (not (dle-next cursor))
+             (funcall test item (dle-value cursor)))
+         (if (dle-previous cursor)
+             (cons-dle item (dle-previous cursor) cursor)
+             (setf elements (cons-dle item nil cursor)))
+         (if (or (not max-size)
+                  (< size max-size))
+             (incf size)
+             (dle-remove (dle-previous last-element)))
+         (values sl (dle-previous cursor))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass heap ()
+  ((less-than :type function
+              :initarg :test
+              :documentation "The heap invariant.")
+   (data :type array
+         :documentation "The heap tree representation.")))
+
+(defmethod initialize-instance ((heap heap) &rest args)
+  (declare (ignore args))
+  (call-next-method)
+  (with-slots (data) heap
+    (setf data (make-array 0 :fill-pointer 0 :adjustable t))))
+
+(defgeneric heap-add (heap item))
+
+(defun bubble-up (heap pos)
+  (with-slots (data less-than) heap
+    (loop
+       for current = pos then parent
+       for parent = (truncate (1- current) 2)
+       until (or (zerop current)
+                 (funcall less-than (aref data parent) (aref data current)))
+       do (rotatef (aref data current) (aref data parent)))))
+
+(defmethod heap-add ((heap heap) item)
+  (with-slots (data) heap
+    (vector-push-extend item data)
+    (bubble-up heap (1- (fill-pointer data)))))
+
+(defgeneric heap-size (heap))
+
+(defmethod heap-size ((heap heap))
+  (fill-pointer (slot-value heap 'data)))
+
+(defgeneric heap-empty-p (heap))
+
+(defmethod heap-empty-p ((heap heap))
+  (zerop (heap-size heap)))
+
+
+(defgeneric heap-pop (heap))
+
+(defun percolate-down (heap pos)
+  (with-slots (data less-than) heap
+    (loop
+       with end = (fill-pointer data)
+       for current = pos then child
+       for left-child = (+ 1 (* 2 current))
+       for right-child = (+ 2 (* 2 current))
+       for child = (cond ((>= left-child end)
+                          (return))
+                         ((>= right-child end)
+                          left-child)
+                         ((funcall less-than (aref data left-child) (aref data right-child))
+                          left-child)
+                         (t
+                          right-child))
+       while (funcall less-than (aref data child) (aref data current))
+       do (rotatef (aref data current) (aref data child)))))
+
+(defmethod heap-pop ((heap heap))
+  (assert (not (heap-empty-p heap)))
+  (with-slots (data) heap
+    (be root (aref data 0)
+      (setf (aref data 0) (vector-pop data))
+      (percolate-down heap 0)
+      root)))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defstruct (lru-cache-slot (:include double-linked-element)
+                           (:conc-name lruc-slot-))
+  key)
+
+(defmethod print-object ((object lru-cache-slot) stream)
+  (print-unreadable-object (object stream :type t)
+    (format stream "key=~S value=~S" (lruc-slot-key object) (lruc-slot-value object))))
+
+(defvar *default-cache-size* 100
+  "Default size of a LRU cache if it's not specified at instantiation
+time.")
+
+(defclass lru-cache ()
+  ((max-size :initform *default-cache-size*
+             :initarg :size
+             :reader max-size
+             :type (or integer null)
+             :documentation
+             "Maximum number of elements that the cache can fit.")
+   (elements-list :type lru-cache-slot
+                  :documentation "The list of elements held by the cache.")
+   (elements-hash :type hash-table
+                  :documentation "The hash table of the elements held bye the cache.")
+   (last-element :type lru-cache-slot)
+   (size :initform 0
+         :reader size
+         :type integer
+         :documentation "Current number of elements in the cache.")
+   (finalizer :initform nil
+              :initarg :finalizer
+              :documentation
+              "Procedure to call when elements are dropped from cache."))
+  (:documentation
+   "An objects cache that keeps the elements used more often and
+drops those that are used less often.  The usage is similar to an
+hash table.  Elements are added to the list up to MAX-SIZE, then
+any new element will drop the less used one in the cache.  Every
+time an element is set or retrieved it goes in front of a list.
+Those which get at the end of the list are dropped when more room
+is required."))
+
+(defmethod initialize-instance ((object lru-cache) &key test &allow-other-keys)
+  (call-next-method)
+  (with-slots (last-element elements-list elements-hash) object
+    (setf last-element (make-lru-cache-slot)
+          elements-list last-element
+          elements-hash (if test
+                            (make-hash-table :test test)
+                            (make-hash-table)))))
+
+(defgeneric getcache (key cache)
+  (:documentation
+   "Get an item with KEY from a CACHE."))
+
+(defgeneric (setf getcache) (value key cache)
+  (:documentation
+   "Set or add an item with KEY in a CACHE."))
+
+(defgeneric remcache (key cache)
+  (:documentation
+   "Remove an item with KEY from a CACHE."))
+
+(defun move-in-front-of-cache-list (slot cache)
+  "Relocate slot to the front of the elements list in cache.
+This will stretch its lifespan in the cache."
+  (declare (type lru-cache-slot slot)
+           (type lru-cache cache))
+  (with-slots (elements-list) cache
+    ;; unless it's already the first
+    (unless (eq slot elements-list)
+      ;; remove the slot from its original place...
+      (dle-remove slot)
+      ;; ... and add it in front of the list
+      (setf (lruc-slot-next slot) elements-list
+            (lruc-slot-previous slot) nil
+            (lruc-slot-previous elements-list) slot
+            elements-list slot))))
+
+(defun drop-last-cache-element (cache)
+  "Drop the last element in the list of the cache object."
+  (declare (type lru-cache cache))
+  (with-slots (last-element elements-hash finalizer) cache
+    (let ((second-last (lruc-slot-previous last-element)))
+      (assert second-last)
+      (when finalizer
+        (funcall finalizer (lruc-slot-value second-last)))
+      (dle-remove second-last)
+      (remhash (lruc-slot-key second-last) elements-hash))))
+
+(defun add-to-cache (slot cache)
+  (declare (type lru-cache-slot slot)
+           (type lru-cache cache))
+  (move-in-front-of-cache-list slot cache)
+  (with-slots (max-size size elements-hash) cache
+    (setf (gethash (lruc-slot-key slot) elements-hash) slot)
+    (if (and max-size
+             (< size max-size))
+        (incf size)
+        (drop-last-cache-element cache))))
+
+(defmethod getcache (key (cache lru-cache))
+  (multiple-value-bind (slot found?) (gethash key (slot-value cache 'elements-hash))
+    (when found?
+      (move-in-front-of-cache-list slot cache)
+      (values (lruc-slot-value slot) t))))
+
+(defmethod (setf getcache) (value key (cache lru-cache))
+  (with-slots (elements-hash elements-list) cache
+    (multiple-value-bind (slot found?) (gethash key elements-hash)
+      (if found?
+          (progn
+            (move-in-front-of-cache-list slot cache)
+            (setf (lruc-slot-value slot) value))
+          (add-to-cache (make-lru-cache-slot :key key :value value) cache))
+      value)))
+
+(defmethod remcache (key (cache lru-cache))
+  (with-slots (elements-hash size elements-list finalizer) cache
+    (multiple-value-bind (slot found?) (gethash key elements-hash)
+      (when found?
+        (remhash key elements-hash)
+        (when finalizer
+          (funcall finalizer (lruc-slot-value slot)))
+        (when (eq slot elements-list)
+          (setf elements-list (dle-next slot)))
+        (dle-remove slot)
+        (decf size)
+        t))))
+
+(defmacro cached (cache key value)
+  "If KEY is found in CACHE return the associated object.  Otherwise
+store VALUE for later re-use."
+  (with-gensyms (object my-cache my-key my-value found?)
+    `(let* ((,my-cache ,cache)
+            (,my-key ,key))
+       (multiple-value-bind (,object ,found?) (getcache ,my-key ,my-cache)
+         (if ,found?
+             ,object
+             (let ((,my-value ,value))
+               (setf (getcache ,my-key ,my-cache) ,my-value)
+               ,my-value))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(declaim (inline list->string))
+(defun list->string (list)
+  "Coerce a list of characters into a string."
+  (coerce list 'string))
+
+(defun setuid (id)
+  "Set the Unix real user id."
+  (when (stringp id)
+    (setf id (find-uid id)))
+  #+sbcl (sb-posix:setuid id)
+  #+cmu (unix:unix-setuid id)
+  #+clisp (posix::%setuid id)		; not verified -wcp26/8/09.
+  #-(or cmu sbcl clisp)
+  (error "setuid unsupported under this Lisp implementation"))
+
+(defun seteuid (id)
+  "Set the Unix effective user id."
+  (when (stringp id)
+    (setf id (find-uid id)))
+  #+sbcl (sb-posix:seteuid id)
+  #+cmu (unix:unix-setreuid -1 id)
+  #+clisp (posix::%seteuid id)		; not verified -wcp26/8/09.
+  #-(or cmu sbcl clisp)
+  (error "seteuid unsupported under this Lisp implementation"))
+
+(defun find-uid (name)
+  "Find the user id of NAME.  Return an integer."
+  #+sbcl (awhen (sb-posix:getpwnam name)
+           (sb-posix:passwd-uid it))
+  #+cmu (awhen (unix:unix-getpwnam name)
+          (unix:user-info-uid it))
+  #-(or cmu sbcl)
+  (error "Unable to find a UID on this Lisp system."))
+
+#+clisp (ffi:def-call-out %getuid
+            (:name "getuid")
+          (:arguments)
+          (:return-type ffi:int)
+          (:library "libc.so"))
+
+(defun getuid ()
+  "Return the Unix user id.  This is an integer."
+  #+sbcl (sb-unix:unix-getuid)
+  #+cmu (unix:unix-getuid)
+  #+clisp (%getuid)
+  #-(or cmu sbcl clisp)
+  (error "getuid unsupported under this Lisp implementation"))
+
+(defun super-user-p (&optional id)
+  "Return true if the user ID is zero.  ID defaults to the current
+user id."
+  (zerop (or id (getuid))))
+
+(defmacro with-euid (uid &body forms)
+  "Switch temporarely to Unix user id UID, while performing FORMS."
+  (with-gensyms (ruid)
+    `(be ,ruid (getuid)
+       (seteuid ,uid)
+       (unwind-protect (progn ,@forms)
+         (seteuid ,ruid)))))
+
+(defun get-logname (&optional uid)
+  "Return the login id of the user.  This is a string and it is not
+the Unix uid, which is a number."
+  (unless uid
+    (setf uid (getuid)))
+  (when (stringp uid)
+    (setf uid (find-uid uid)))
+  (when uid
+    #+sbcl (sb-unix:uid-username uid)
+    #+cmu (unix:user-info-name (unix:unix-getpwuid uid))
+    #+clisp (posix:user-info-login-id (posix:user-info uid))
+    #-(or cmu sbcl clisp)
+    (error "get-logname unsupported under this Lisp implementation")))
+
+(defun get-user-name (&optional uid)
+  "Return the user name, taken from the GCOS field of the /etc/passwd
+file."
+  (unless uid
+    (setf uid (getuid)))
+  (when (stringp uid)
+    (setf uid (find-uid uid)))
+  (when uid
+    (car (split-string-at-char #+cmu (unix:user-info-gecos (unix:unix-getpwuid uid))
+                               #+sbcl (sb-posix:passwd-gecos (sb-posix:getpwuid uid))
+                               #-(or cmu sbcl) (error "can't getpwuid() on this Lisp system.")
+                               #\,))))
+
+(defun get-user-home (&optional uid)
+  (unless uid
+    (setf uid (getuid)))
+  (when (stringp uid)
+    (setf uid (find-uid uid)))
+  (when uid
+    #+cmu (unix:user-info-dir (unix:unix-getpwuid uid))
+    #+sbcl (sb-posix:passwd-dir (sb-posix:getpwuid uid))))
+
+;; Rather stupid, but the mnemonic is worth it
+(declaim (inline alist->plist))
+(defun alist->plist (alist)
+  "Convert an association list into a property list.  The alist
+elements are assumed to be lists of just two elements: the key
+and the value.  If the element list is longer this function
+doesn't work."
+  (mapcan #'identity alist))
+
+(defun plist->alist (plist &optional pairs-p)
+  "Convert a property list into an association list.  The alist
+elements wiil be lists of just two elements: the key and the
+value.  If PAIRS-P is true the alist elements will be pairs."
+  (loop
+     for (key val) on plist by #'cddr
+     collect (if pairs-p
+                 (cons key val)
+                 (list key val))))
+
+(defun string->byte-vector (string &key start end)
+  "Convert a string of characters into a vector of (unsigned-byte
+8) elements."
+  (map '(vector (unsigned-byte 8)) #'char-code
+       (if (or start end)
+           (subseq string (or start 0) end)
+           string)))
+
+(defun byte-vector->string (vector &key start end)
+  "Convert a vector of (unsigned-byte 8) elements into a string
+of characters."
+  (map 'string #'code-char
+       (if (or start end)
+           (subseq vector (or start 0) end)
+           vector)))
+
+(defun outdated-p (file dependencies)
+  "Check if FILE has been modified before any of its
+DEPENDENCIES."
+  (be epoch (and (probe-file file)
+                 (file-write-date file))
+    ;; if file is missing altogether, we consider it outdated
+    (or (not epoch)
+        (loop
+           for dep in dependencies
+           thereis (aand (probe-file dep)
+                         (file-write-date dep)
+                         (> it epoch))))))
+
+(defmacro let-places (places-and-values &body body)
+  "Execute BODY binding temporarily some places to new values and
+restoring the original values of these places on exit of BODY.  The
+syntax of this macro is identical to LET.  The difference is that
+instead of new variable names this macro binds values to existing
+places (variables)."
+  (be tmp-variables (loop for x in places-and-values collect (gensym))
+    `(let ,(mapcar #'(lambda (tmp-var place-and-value)
+                       (list tmp-var (car place-and-value)))
+                   tmp-variables places-and-values)
+       (unwind-protect
+            (progn
+              ;; as some assignments could signal an error, we assign
+              ;; within the unwind-protect block so that we can always
+              ;; guarantee a consistent state on exit
+              ,@(mapcar #'(lambda (place-and-value)
+                            `(setf ,(car place-and-value) ,(cadr place-and-value)))
+                        places-and-values)
+              ,@body)
+         ,@(mapcar #'(lambda (tmp-var place-and-value)
+                       `(setf ,(car place-and-value) ,tmp-var))
+                   tmp-variables
+                   places-and-values)))))
+
+(defmacro let-slots (accessor/new-value-pairs object &body body)
+  "Execute BODY with some OBJECT's slots temporary sets to new
+values as described in ACCESSOR/NEW-VALUE-PAIRS.  The latter
+should be an alist of accessor names and the value to be assigned
+to that slot.  On exit from BODY, those slots are restored to
+their original value.  See LET-PLACES."
+  (with-gensyms (obj)
+    `(be ,obj ,object
+       (let-places ,(mapcar #'(lambda (av)
+                                `((,(car av) ,obj) ,(cadr av)))
+                            accessor/new-value-pairs)
+         ,@body))))
+
+(defvar *decimal-point* #\.)
+(defvar *thousands-comma* #\,)
+
+(defun format-amount (number &key (decimals 2) (rounder #'round)
+                      (comma *thousands-comma*) (comma-stance 3)
+                      (decimal-point *decimal-point*))
+  "Return a string formatted as fixed decimal point number of DECIMALS
+adding commas every COMMA-STANCE places before the decimal point."
+  (declare (type number number)
+           (type fixnum decimals comma-stance)
+           (type function rounder)
+           (type character comma decimal-point)
+           (optimize (speed 3) (safety 0) (debug 0)))
+  (let* ((int (funcall rounder (* number (expt 10 decimals))))
+         (negative (< int 0)))
+    (declare (integer int))
+    (when negative
+      (setf int (- int)))
+    (let* ((digits (max (1+ decimals)
+                        (1+ (if (zerop int)
+                                0
+                                (truncate (log int 10))))))
+           (string-length (+ digits
+                             ;; the minus sign
+                             (if negative 1 0)
+                             ;; the decimal point
+                             (if (zerop decimals) 0 1)
+                             ;; the thousands commas
+                             (1- (ceiling (- digits decimals) comma-stance))))
+           (string (make-string string-length))
+           (pos (1- string-length)))
+      (declare (type fixnum pos digits))
+      (labels ((add-char (char)
+                 (setf (schar string pos) char)
+                 (decf pos))
+               (add-digit ()
+                 (add-char (digit-char (mod int 10)))
+                 (setf int (truncate int 10))))
+        (unless (zerop decimals)
+          (loop
+             for i fixnum from 0 below decimals
+             do (add-digit))
+          (add-char decimal-point))
+        (loop
+           for i fixnum from 1
+           do (add-digit)
+           while (>= pos (if negative 1 0))
+           when (zerop (mod i comma-stance))
+           do (add-char comma))
+        (when negative
+          (add-char #\-)))
+      string)))
+
+(defun parse-amount (string &key (start 0) end)
+  "Parse STRING as if it was formatted with FORMAT-AMOUNT and return
+the parsed number.  Return NIL if STRING is malformed.  Leading or
+trailing spaces must be removed from the string in advance."
+  (loop
+     with amount = 0
+     with decimals = nil
+     with negative = (when (and (not (zerop (length string)))
+                                (char= #\- (char string 0)))
+                       (incf start)
+                       t)
+     for i from start below (or end (length string))
+     for c = (char string i)
+     do (cond ((char= c *decimal-point*)
+               (if decimals
+                   (return nil)
+                   (setf decimals 0)))
+              ((char= c *thousands-comma*))
+              (t
+               (be d (digit-char-p c)
+                 (cond ((not d)
+                        (return nil))
+                       (decimals
+                        (incf decimals)
+                        (incf amount (/ d (expt 10 decimals))))
+                       (t
+                        (setf amount (+ d (* amount 10))))))))
+     finally (return (if negative
+                         (- amount)
+                         amount))))
+
+(defmacro with-package (name &body body)
+  `(let ((*package* (find-package ,name)))
+     ,@body))
+
+(defun bytes-simple-string (n &optional imply-bytes)
+  "Return a string describing N using a unit of measure multiple
+of a byte that is most apporpriate for the magnitude of N.  A
+kilobyte is 1024 not 1000 bytes, everything follows."
+  (let* ((kilo 1024)
+         (mega (* kilo kilo))
+         (giga (* kilo mega))
+         (tera (* mega mega))
+         (peta (* kilo tera)))
+    (apply #'format nil "~,1F~A"
+           (cond ((> n (* 2 peta))
+                  (list (/ n peta) (if imply-bytes "P" "PB")))
+                 ((> n (* 2 tera))
+                  (list (/ n tera) (if imply-bytes "T" "TB")))
+                 ((> n (* 2 giga))
+                  (list (/ n giga) (if imply-bytes "G" "GB")))
+                 ((> n (* 2 mega))
+                  (list (/ n mega) (if imply-bytes "M" "MB")))
+                 ((> n (* 2 kilo))
+                  (list (/ n kilo) (if imply-bytes "K" "KB")))
+                 (t (list n (if imply-bytes "" " bytes")))))))
+
+;; WARNING: This function may or may not work on your Lisp system.  It
+;; all depends on how the OPEN function has been implemented regarding
+;; the :IF-EXISTS option.  This function requires that OPEN be
+;; implemented in a way so that the checking of the existence of file
+;; and its open attempt be atomic.  If the Lisp OPEN first checks that
+;; the file exists and then tries to open it, this function won't be
+;; reliable.  CMUCL seems to use the O_EXCL open() flag in the right
+;; way.  So at least on CMUCL this function will work.  Same goes for
+;; SBCL.
+(defun make-lock-files (pathnames &key (sleep-time 7) retries (suspend 13) expiration)
+  "Create semaphore files.  If it can't create all the specified
+files in the specified order, it waits SLEEP-TIME seconds and
+retries the last file that didn't succeed.  You can specify the
+number of RETRIES to do until failure is returned.  If the number
+of retries is NIL this function will retry forever.
+
+If it tries RETRIES times without success, this function signal
+an error and removes all the lock files it created until then.
+
+All files created by lock file will be read-only.
+
+If you specify a EXPIRATION then an existing lock file will be
+removed by force after EXPIRATION seconds have passed since the
+lock file was last modified/created (most likely by some other
+program that unexpectedly died without cleaning up its lock
+files).  After a lock file has been removed by force, a
+suspension of SUSPEND seconds is taken into account, in order to
+prevent the inadvertent immediate removal of any newly created
+lock file by another program."
+  (be locked '()
+    (flet ((lock (file)
+             (when (and expiration
+                        (> (get-universal-time)
+                           (+ (file-write-date file) expiration)))
+               (delete-file file)
+               (when suspend
+                 (sleep suspend)))
+             (do ((i 0 (1+ i))
+                  (done nil))
+                 (done)
+               (unless (or (not retries)
+                           (< i retries))
+                 (error "Can't create lock file ~S: tried ~A time~:P." file retries))
+               (with-open-file (out file :direction :output :if-exists nil)
+                 (cond (out
+                        (format out "Lock file created on ~A~%" (time-string (get-universal-time)))
+                        (setf done t))
+                       (sleep-time
+                        (sleep sleep-time)))))))
+      (unwind-protect
+           (progn
+             (dolist (file pathnames)
+               (lock file)
+               (push file locked))
+             (setf locked '()))
+        (mapc #'delete-file locked)))))
+
+(defmacro with-lock-files ((lock-files &rest lock-args) &body body)
+  "Execute BODY after creating LOCK-FILES.  Remove the lock files
+on exit.  LOCK-ARGS are passed to MAKE-LOCK-FILES."
+  (with-gensyms (files)
+    `(be ,files (list ,@lock-files)
+       (make-lock-files ,files ,@lock-args)
+       (unwind-protect (progn ,@body)
+         (mapc #'delete-file ,files)))))
+
+(defun getpid ()
+  #+cmu (unix:unix-getpid)
+  #+sbcl (sb-unix:unix-getpid)
+  #+clisp (ext:process-id)
+  #-(or cmu sbcl clisp)
+   (error "getpid unsupported under this Lisp implementation"))
+
+(defmacro on-error (form &body error-forms)
+  "Execute FORM and in case of error execute ERROR-FORMS too.
+This does _not_ stop the error from propagating."
+  (be done-p (gensym)
+    `(be ,done-p nil
+       (unwind-protect
+            (prog1
+                ,form
+              (setf ,done-p t))
+         (unless ,done-p
+           ,@error-forms)))))
+
+(defun floor-to (x aim)
+  "Round X down to the nearest multiple of AIM."
+  (* (floor x aim) aim))
+
+(defun round-to (x aim)
+  "Round X to the nearest multiple of AIM."
+  (* (round x aim) aim))
+
+(defun ceiling-to (x aim)
+  "Round X up to the nearest multiple of AIM."
+  (* (ceiling x aim) aim))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defstruct queue
+  first
+  last)
+
+(defgeneric queue-append (queue objects))
+(defgeneric queue-pop (queue))
+(defgeneric queue-empty-p (queue))
+
+(defmethod queue-append ((queue queue) (objects list))
+  (cond ((null (queue-first queue))
+         (setf (queue-first queue) objects
+               (queue-last queue) (last objects)))
+        (t
+         (setf (cdr (queue-last queue)) objects
+               (queue-last queue) (last objects))))
+  queue)
+
+(defmethod queue-append ((queue queue) object)
+  (queue-append queue (list object)))
+
+(defmethod queue-pop ((queue queue))
+  (prog1 (car (queue-first queue))
+    (setf (queue-first queue) (cdr (queue-first queue)))))
+
+(defmethod queue-empty-p ((queue queue))
+  (null (queue-first queue)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun package-locked-p (package)
+  #+sbcl (sb-ext:package-locked-p package)
+  #+cmu (ext:package-definition-lock package)
+  #+clisp (ext:package-lock package)
+  #-(or sbcl cmu clisp) (error "Don't know how to check whether a package might be locked."))
+
+(defun forget-documentation (packages)
+  "Remove documentation from all known symbols in PACKAGES.  If
+PACKAGES is NIL remove documentations from all packages.  This may not
+make sense if your Lisp image has been built so that existing objects
+don't get garbage collected.  It may work for your own code, though.
+Locked packages are left alone.  If you need to do those too, unlock
+them first."
+  (flet ((forget (symbol)
+           (dolist (type '(compiler-macro function method-combination setf structure type variable))
+             (when (ignore-errors (documentation symbol type))
+               (setf (documentation symbol type) nil)))))
+    (setf packages (mapcar #'(lambda (pkg)
+                               (if (packagep pkg)
+                                   (package-name pkg)
+                                   (package-name (find-package pkg))))
+                           packages))
+    (setf packages
+          ;; don't try to modify locked packages
+          (remove-if #'package-locked-p
+                     (mapcar #'find-package
+                             (or packages
+                                 (list-all-packages)))))
+    (dolist (package packages)
+      (with-package-iterator (next package :internal :external)
+        (loop
+           (multiple-value-bind (more? symbol) (next)
+             (unless more?
+               (return))
+             (forget symbol)))))
+    #+(OR) (do-all-symbols (symbol)
+             (when (member (symbol-package symbol) packages)
+               (forget symbol))))
+  (values))
+
+(defun load-compiled (pathname &optional compiled-pathname)
+  "Make sure to compile PATHNAME before loading it.  Don't compile if
+the compiled version is more recent than its source."
+  ;; be tolerant if we didn't get a type
+  (unless (probe-file pathname)
+    (setf pathname (merge-pathnames pathname (make-pathname :type "lisp"))))
+  (if (probe-file pathname)
+      (progn
+        (setf compiled-pathname (or compiled-pathname
+                                    (compile-file-pathname pathname)))
+        (when (or (not (probe-file compiled-pathname))
+                  (< (file-write-date compiled-pathname)
+                     (file-write-date pathname)))
+          (compile-file pathname))
+        (load compiled-pathname))
+      (error "Can't load ~A as it doesn't exist." pathname)))
+
+;; Just a silly mnemonic for those used to lesser languages
+(defmacro swap (x y)
+  "Swap values of places X and Y."
+  `(rotatef ,x ,y))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmacro show (&rest things)
+  "Debugging macro to show the name and content of variables.  You can
+also specify forms, not just variables."
+  (let ((*print-pretty* nil))
+    `(let ((*print-circle* t))
+       (format t ,(format nil "~~&~{~A=~~:W~~%~}" things)
+               ,@things)
+       (finish-output)
+       (values))))
+
+(defmacro memoize-function (name &key test)
+  "Make function NAME memoized.  TEST is passed to MAKE-HASH-TABLE."
+  `(setf (get ',name 'results-hash-table)
+         (make-hash-table ,@(when test (list :test test)))))
+
+(defmacro defun-memoized (name args &body forms)
+  "Define function NAME and make it memoizable.  Then the MEMOIZED
+macro can be used to call this function and memoize its results.  The
+function NAME must accept only one argument and return just one
+argument; more complicated cases are not considered.  The hash table
+test function is the default 'EQL."
+  `(eval-when (:load-toplevel :compile-toplevel)
+     (defun ,name ,args ,@forms)
+     (memoize-function ,name)))
+
+(defmacro memoized (function arg)
+  "If necessary call FUNCTION passing ARG so that its return value is
+memoized.  The next time this form is executed with the same argument
+value, the memoized result is returned instead of executing FUNCTION."
+  (with-gensyms (table key result not-found)
+    `(be* ,key ,arg
+          ,table (get ',function 'results-hash-table)
+          ,not-found (list nil)
+          ,result (gethash ,key ,table ,not-found)
+       (if (eq ,not-found ,result)
+           (setf (gethash ,key ,table)
+                 (,function ,key))
+           ,result))))
+
+
+(defmacro save-file-excursion ((stream &optional position) &body forms)
+  "Execute FORMS returning, on exit, STREAM to the position it was
+before FORMS.  Optionally POSITION can be set to the starting offset."
+  (unless position
+    (setf position (gensym)))
+  `(be ,position (file-position ,stream)
+     (unwind-protect (progn ,@forms)
+       (file-position ,stream ,position))))
+
+(defun circular-list (&rest elements)
+  "Return a circular list of ELEMENTS."
+  (setf (cdr (last elements)) elements))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun getenv (var)
+  "Return the string associate to VAR in the system environment."
+  #+cmu (cdr (assoc (if (symbolp var)
+                        var
+                        (intern var :keyword))
+                    ext:*environment-list*))
+  #+sbcl (sb-ext:posix-getenv (string var))
+  #+lispworks (hcl:getenv var)
+  #+clisp (ext:getenv (string var))
+  #-(or cmu sbcl lispworks clisp)
+  (error "GETENV not implemented for your Lisp system."))
+
+#+clisp (ffi:def-call-out %setenv
+            (:name "setenv")
+          (:arguments (name ffi:c-string) (value ffi:c-string) (overwrite ffi:int))
+          (:return-type ffi:int)
+          (:library "libc.so"))
+
+#+clisp (ffi:def-call-out %unsetenv
+            (:name "unsetenv")
+          (:arguments (name ffi:c-string))
+          (:return-type ffi:int)
+          (:library "libc.so"))
+
+(defun setenv (name value &optional (overwrite t))
+  (typecase value
+    (string)
+    (pathname
+     (setf value (native-namestring value)))
+    (t
+     (setf value (format nil "~A" value))))
+  #+sbcl (unless (zerop (sb-posix:setenv name value (if overwrite 1 0)))
+           (error "unable to setenv ~A: errno=~A." name
+                  (sb-alien:get-errno)))
+  #+cmu (be key (keywordify name)
+          (aif (assoc key
+                      ext:*environment-list*)
+               (when overwrite
+                 (setf (cdr it) value))
+               (setf ext:*environment-list*
+                     (cons (cons key value)
+                           ext:*environment-list*))))
+  #-(or cmu sbcl) (unless (zerop (%setenv name value (if overwrite 1 0)))
+                    (error "unable to setenv ~A." name)))
+
+(defun unsetenv (name)
+  #+sbcl (unless (zerop (sb-posix:unsetenv name))
+           (error "unable to unsetenv ~A: errno=~A." name
+                  (sb-alien:get-errno)))
+  #+cmu (be key (keywordify name)
+          (setf ext:*environment-list*
+                (delete-if #'(lambda (e)
+                               (eq (car e) key))
+                           ext:*environment-list*)))
+  #-(or cmu sbcl) (unless (zerop (%unsetenv name))
+                    (error "unable to unsetenv ~A." name)))
+
+(defun (setf getenv) (value name)
+  (if value
+      (setenv name value t)
+      (unsetenv name)))
+
+;; in CMUCL it's much easier (see below)
+#-cmu
+(defmacro with-system-environment ((&rest var-and-values) &body body)
+  (be gensym-alist (mapcar #'(lambda (vv)
+                               (list (gensym) (string (car vv)) (cadr vv)))
+                           var-and-values)
+      `(let ,(mapcar #'(lambda (vv)
+                         (destructuring-bind (varsym var value) vv
+                           (declare (ignore value))
+                           `(,varsym (getenv ,var))))
+                     gensym-alist)
+         (unwind-protect
+              (progn
+                ,@(mapcar #'(lambda (vv)
+                              (destructuring-bind (varsym var value) vv
+                                (declare (ignore varsym))
+                                `(setenv ,var ,value)))
+                          gensym-alist)
+                ,@body)
+           ,@(mapcar #'(lambda (vv)
+                         (destructuring-bind (varsym var value) vv
+                           (declare (ignore value))
+                           `(if ,varsym
+                                (setenv ,var ,varsym)
+                                (unsetenv ,var))))
+                     gensym-alist)))))
+
+#+cmu
+(defmacro with-system-environment ((&rest var-and-values) &body body)
+  `(let ((ext:*environment-list*
+          (append (list ,@(mapcar #'(lambda (vv)
+                                      (destructuring-bind (variable value) vv
+                                        `(cons ,(keywordify variable)
+                                               ,value)))
+                                  var-and-values))
+                  ext:*environment-list*)))
+     ,@body))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun last-member (item list &key key (test #'eq))
+  "Return the last sublist in LIST that is prefixed by ITEM."
+  (loop
+     with l = list and result = nil
+     for l2 = (member item l :key key :test test)
+     while l2
+     do (setf result l2
+              l (cdr l2))
+     finally (return result)))
+
+
+(defun glob->regex (string)
+  "Convert a shell glob expression into a regular expression string."
+  (with-output-to-string (out)
+    ;; globs are always anchored to beginning and end
+    (write-char #\^ out)
+    (loop
+       for i from 0 below (length string)
+       do (be c (char string i)
+            (cond ((char= c #\\)
+                   (setf c (char string (incf i))))
+                  ((find c  ".+()|^$")
+                   (write-char #\\ out))
+                  ((char= c #\*)
+                   (write-char #\. out))
+                  ((char= c #\?)
+                   (setf c #\.)))
+            (write-char c out)))
+    (write-char #\$ out)))
diff --git a/third_party/lisp/sclf/serial.lisp b/third_party/lisp/sclf/serial.lisp
new file mode 100644
index 0000000000..41d32e4c49
--- /dev/null
+++ b/third_party/lisp/sclf/serial.lisp
@@ -0,0 +1,62 @@
+ ;;; serial.lisp --- serialisation of CLOS objects
+
+ ;;; Copyright (C) 2009 by Walter C. Pelissero
+
+ ;;; Author: Walter C. Pelissero <walter@pelissero.de>
+ ;;; Project: sclf
+
+#+cmu (ext:file-comment "$Module: serial.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :sclf)
+
+(defclass printable-object-mixin () ())
+
+(defmacro reconstruct-object (class &rest args)
+  `(apply #'make-instance ',class ',args))
+
+(defun print-readable-instance (object &optional stream)
+  (unless stream
+    (setf stream *standard-output*))
+  (be class (class-of object)
+    (pprint-logical-block (stream (copy-list (class-slots class)) :prefix "#.(" :suffix ")")
+      (flet ((spc ()
+               (write-char #\space stream)))
+        (write 'reconstruct-object :stream stream)
+        (spc)
+        (write (class-name class) :stream stream :escape t :readably t :pretty t)
+        (pprint-exit-if-list-exhausted)
+        (spc)
+        (loop
+           (be* slot (pprint-pop)
+                slot-name (slot-definition-name slot)
+                initarg (car (slot-definition-initargs slot))
+             (when (and initarg
+                        (slot-boundp object slot-name))
+               (write initarg :stream stream)
+               (spc)
+               (when *print-pretty*
+                 (pprint-newline :miser stream))
+               (write (slot-value object slot-name)
+                      :stream stream)
+               (pprint-exit-if-list-exhausted)
+               (if *print-pretty*
+                   (pprint-newline :linear stream)
+                   (spc)))))))))
+
+(defmethod print-object ((object printable-object-mixin) stream)
+  (if *print-readably*
+      (print-readable-instance object stream)
+      (call-next-method)))
diff --git a/third_party/lisp/sclf/sysproc.lisp b/third_party/lisp/sclf/sysproc.lisp
new file mode 100644
index 0000000000..1dd559ebe3
--- /dev/null
+++ b/third_party/lisp/sclf/sysproc.lisp
@@ -0,0 +1,295 @@
+;;;  sysproc.lisp --- system processes
+
+;;;  Copyright (C) 2008, 2009, 2010 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: sclf
+
+#+cmu (ext:file-comment "$Module: sysproc.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :sclf)
+
+(defvar *bourne-shell* "/bin/sh")
+
+(defvar *run-verbose* nil
+  "If true system commands are displayed before execution and standard
+error is not discarded.")
+
+;;
+;; SIGINFO is missing in both CMUCL and SBCL
+;;
+
+#+cmu
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (defconstant unix::siginfo 29)
+  (defvar siginfo (unix::make-unix-signal :siginfo unix::siginfo "Information"))
+  (export '(unix::siginfo) "UNIX")
+  (pushnew siginfo unix::*unix-signals*))
+
+#+sbcl (in-package :sb-posix)
+#+sbcl
+(eval-when (:execute :compile-toplevel :load-toplevel)
+  (unless (find-symbol "SIGINFO" :sb-posix)
+    (sb-ext:with-unlocked-packages (:sb-posix)
+      (defvar siginfo 29)
+      (export '(SIGINFO)))))
+#+sbcl (in-package :sclf)
+
+(defun signal-number (signal-name)
+  (ecase signal-name
+    ((:abrt :abort)
+     #+cmu unix:sigabrt
+     #+sbcl sb-posix:sigabrt)
+    ((:alrm :alarm)
+     #+cmu unix:sigalrm
+     #+sbcl sb-posix:sigalrm)
+    ((:bus :bus-error)
+     #+cmu unix:sigbus
+     #+sbcl sb-posix:sigbus)
+    ((:chld :child)
+     #+cmu unix:sigchld
+     #+sbcl sb-posix:sigchld)
+    ((:cont :continue)
+     #+cmu unix:sigcont
+     #+sbcl sb-posix:sigcont)
+    #+freebsd((:emt :emulate-instruction)
+              #+cmu unix:sigemt
+              #+sbcl sb-posix:sigemt)
+    ((:fpe :floating-point-exception)
+     #+cmu unix:sigfpe
+     #+sbcl sb-posix:sigfpe)
+    ((:hup :hangup)
+     #+cmu unix:sighup
+     #+sbcl sb-posix:sighup)
+    ((:ill :illegal :illegal-instruction)
+     #+cmu unix:sigill
+     #+sbcl sb-posix:sigill)
+    ((:int :interrupt)
+     #+cmu unix:sigint
+     #+sbcl sb-posix:sigint)
+    ((:io :input-output)
+     #+cmu unix:sigio
+     #+sbcl sb-posix:sigio)
+    (:kill
+     #+cmu unix:sigkill
+     #+sbcl sb-posix:sigkill)
+    ((:pipe :broke-pipe)
+     #+cmu unix:sigpipe
+     #+sbcl sb-posix:sigpipe)
+    ((:prof :profiler)
+     #+cmu unix:sigprof
+     #+sbcl sb-posix:sigprof)
+    (:quit
+     #+cmu unix:sigquit
+     #+sbcl sb-posix:sigquit)
+    ((:segv :segmentation-violation)
+     #+cmu unix:sigsegv
+     #+sbcl sb-posix:sigsegv)
+    (:stop
+     #+cmu unix:sigstop
+     #+sbcl sb-posix:sigstop)
+    ((:sys :system-call)
+     #+cmu unix:sigsys
+     #+sbcl sb-posix:sigsys)
+    ((:term :terminate)
+     #+cmu unix:sigterm
+     #+sbcl sb-posix:sigterm)
+    ((:trap)
+     #+cmu unix:sigtrap
+     #+sbcl sb-posix:sigtrap)
+    ((:tstp :terminal-stop)
+     #+cmu unix:sigtstp
+     #+sbcl sb-posix:sigtstp)
+    ((:ttin :tty-input)
+     #+cmu unix:sigttin
+     #+sbcl sb-posix:sigttin)
+    ((:ttou :tty-output)
+     #+cmu unix:sigttou
+     #+sbcl sb-posix:sigttou)
+    ((:urg :urgent)
+     #+cmu unix:sigurg
+     #+sbcl sb-posix:sigurg)
+    ((:usr1 :user1)
+     #+cmu unix:sigusr1
+     #+sbcl sb-posix:sigusr1)
+    ((:usr2 :user2)
+     #+cmu unix:sigusr2
+     #+sbcl sb-posix:sigusr2)
+    ((:vtalrm :virtual-timer-alarm)
+     #+cmu unix:sigvtalrm
+     #+sbcl sb-posix:sigvtalrm)
+    ((:winch :window-change :window-size-change)
+     #+cmu unix:sigwinch
+     #+sbcl sb-posix:sigwinch)
+    ((:xcpu :exceeded-cpu)
+     #+cmu unix:sigxcpu
+     #+sbcl sb-posix:sigxcpu)
+    ((:xfsz :exceeded-file-size)
+     #+cmu unix:sigxfsz
+     #+sbcl sb-posix:sigxfsz)
+    ;; oddly this is not defined by neither CMUCL nor SBCL
+    (:info 29)))
+
+(defun sysproc-kill (process signal)
+  (when (keywordp signal)
+    (setf signal (signal-number signal)))
+  #+cmu (ext:process-kill process signal)
+  #+sbcl (sb-ext:process-kill process signal)
+  #-(or sbcl cmu) (error "Don't know how to kill a process"))
+
+(defun sysproc-exit-code (process)
+  #+cmu (ext:process-exit-code process)
+  #+sbcl (sb-ext:process-exit-code process)
+  #-(or sbcl cmu) (error "Don't know how to get a process exit code"))
+
+(defun sysproc-wait (process)
+  #+cmu (ext:process-wait process)
+  #+sbcl (sb-ext:process-wait process)
+  #-(or sbcl cmu) (error "Don't know how to wait a process"))
+
+(defun sysproc-input (process)
+  #+cmu (ext:process-input process)
+  #+sbcl (sb-ext:process-input process)
+  #-(or sbcl cmu) (error "Don't know how to get the process input"))
+
+(defun sysproc-output (process)
+  #+cmu (ext:process-output process)
+  #+sbcl (sb-ext:process-output process)
+  #-(or sbcl cmu) (error "Don't know how to get the process output"))
+
+(defun sysproc-alive-p (process)
+  #+cmu (ext:process-alive-p process)
+  #+sbcl (sb-ext:process-alive-p process)
+  #-(or sbcl cmu) (error "Don't know how to test wether a process might be running"))
+
+(defun sysproc-pid (process)
+  #+cmu (ext:process-pid process)
+  #+sbcl (sb-ext:process-pid process)
+  #-(or sbcl cmu) (error "Don't know how to get the id of a process"))
+
+(defun sysproc-p (thing)
+  #+sbcl (sb-ext:process-p thing)
+  #+cmu (ext:process-p thing)
+  #-(or sbcl cmu) (error "Don't know how to figure out whether something is a system process"))
+
+(defun run-program (program arguments &key (wait t) pty input output error)
+  "Run PROGRAM with ARGUMENTS (a list) and return a process object."
+  ;; convert arguments to strings
+  (setf arguments
+        (mapcar #'(lambda (item)
+                    (typecase item
+                      (string item)
+                      (pathname (native-namestring item))
+                      (t (format nil "~A" item))))
+                arguments))
+  (when *run-verbose*
+    (unless error
+      (setf error t))
+    (format t "~&; run-pipe ~A~{ ~S~}~%" program arguments))
+  #+cmu (ext:run-program program arguments
+                         :wait wait
+                         :pty pty
+                         :input input
+                         :output output
+                         :error (or error *run-verbose*))
+  #+sbcl (sb-ext:run-program program arguments
+                             :search t
+                             :wait wait
+                             :pty pty
+                             :input input
+                             :output output
+                             :error (or error *run-verbose*))
+  #-(or sbcl cmu)
+  (error "Unsupported Lisp system."))
+
+(defun run-pipe (direction program arguments &key error)
+  "Run PROGRAM with a list of ARGUMENTS and according to DIRECTION
+return the input and output streams and process object of that
+process."
+  (be process (run-program program arguments
+                           :wait nil
+                           :pty nil
+                           :input (when (member direction '(:output :input-output :io))
+                                    :stream)
+                           :output (when (member direction '(:input :input-output :io))
+                                     :stream)
+                           :error error)
+    (values (sysproc-output process)
+            (sysproc-input process)
+            process))
+  #-(or sbcl cmu)
+  (error "Unsupported Lisp system."))
+
+(defun exit-code (process)
+  (sysproc-wait process)
+  (sysproc-exit-code process))
+
+(defun run-shell-command (fmt &rest args)
+  "Run a Bourne Shell command.  Return the exit status of the command."
+  (run-program *bourne-shell* (list "-c" (apply #'format nil fmt args))))
+
+(defun run-async-shell-command (fmt &rest args)
+  "Run a Bourne Shell command asynchronously. Return a process
+object if provided by your Lisp implementation."
+  (run-program *bourne-shell* (list "-c" (apply #'format nil fmt args))
+               :wait nil))
+
+(defmacro with-open-pipe ((in out program arguments &key (process (gensym)) error pty) &body forms)
+  "Run BODY with IN and OUT bound respectively to an input and an
+output stream connected to a system process created by running PROGRAM
+with ARGUMENTS.  If IN or OUT are NIL, then don't create that stream."
+  (with-gensyms (prg args)
+    `(be* ,prg ,program
+          ,args ,arguments
+          ,process (run-program ,prg ,args
+                                :output ,(case in
+                                               ((t nil) in)
+                                               (t :stream))
+                                :input ,(case out
+                                              ((t nil) out)
+                                              (t :stream))
+                                :wait nil
+                                :pty ,pty
+                                ,@(when error `(:error ,error)))
+       (if ,process
+           (let (,@(case in
+                         ((t nil))
+                         (t `((,in (sysproc-output ,process)))))
+                 ,@(case out
+                         ((t nil))
+                         (t `((,out (sysproc-input ,process))))))
+             (unwind-protect
+                  (progn
+                    ,@forms)
+               ,@(case in
+                       ((t nil))
+                       (t `((close ,in))))
+               ,@(case out
+                       ((t nil))
+                       (t `((close ,out))))
+               (when (sysproc-alive-p ,process)
+                 (sysproc-kill ,process :term))))
+           (error "unable to run ~A~{ ~A~}." ,prg ,args)))))
+
+
+(defun sysproc-set-signal-callback (signal handler)
+  "Arrange HANDLER function to be called when receiving the system
+signal SIGNAL."
+  (when (keywordp signal)
+    (setf signal (signal-number signal)))
+  #+cmu (system:enable-interrupt signal handler)
+  #+sbcl (sb-sys:enable-interrupt signal handler)
+  #-(or cmu sbcl) (error "Don't know how to set a system signal callback."))
diff --git a/third_party/lisp/sclf/time.lisp b/third_party/lisp/sclf/time.lisp
new file mode 100644
index 0000000000..71b943aa43
--- /dev/null
+++ b/third_party/lisp/sclf/time.lisp
@@ -0,0 +1,311 @@
+;;;  time.lisp --- time primitives
+
+;;;  Copyright (C) 2006, 2007, 2009 by Walter C. Pelissero
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: sclf
+
+#+cmu (ext:file-comment "$Module: time.lisp $")
+
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 2.1
+;;; of the License, or (at your option) any later version.
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library; if not, write to the Free
+;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+;;; 02111-1307 USA
+
+(in-package :sclf)
+
+(defun year (epoch &optional time-zone)
+  "Return the year of EPOCH."
+  (sixth (multiple-value-list (decode-universal-time epoch time-zone))))
+
+(defun month (epoch &optional time-zone)
+  "Return the month of EPOCH."
+  (fifth (multiple-value-list (decode-universal-time epoch time-zone))))
+
+(defun day (epoch &optional time-zone)
+  "Return the day of EPOCH."
+  (fourth (multiple-value-list (decode-universal-time epoch time-zone))))
+
+(defun week-day (epoch &optional time-zone)
+  "Return the day of the week of EPOCH."
+  (seventh (multiple-value-list (decode-universal-time epoch time-zone))))
+
+(defun hour (epoch &optional time-zone)
+  "Return the hour of EPOCH."
+  (third (multiple-value-list (decode-universal-time epoch time-zone))))
+
+(defun minute (epoch &optional time-zone)
+  "Return the minute of EPOCH."
+  (second (multiple-value-list (decode-universal-time epoch time-zone))))
+
+(defun leap-year-p (year)
+  "Return true if YEAR is a leap year."
+  (and (zerop (mod year 4))
+       (or (not (zerop (mod year 100)))
+           (zerop (mod year 400)))))
+
+(defun last-day-of-month (month year)
+  "Return the last day of the month as integer."
+  (be last (elt #(31 28 31 30 31 30 31 31 30 31 30 31) (1- month))
+    (if (and (= last 28)
+             (leap-year-p year))
+        (1+ last)
+        last)))
+
+(defun add-months (months epoch &optional time-zone)
+  "Add MONTHS to EPOCH, which is a universal time.  MONTHS can be
+negative."
+  (multiple-value-bind (ss mm hh day month year) (decode-universal-time epoch time-zone)
+    (multiple-value-bind (y m) (floor (+ month months -1) 12)
+      (let ((new-month (1+ m))
+            (new-year (+ year y)))
+        (encode-universal-time ss mm hh
+                               (min day (last-day-of-month new-month (year epoch)))
+                               new-month
+                               new-year
+                               time-zone)))))
+
+(defun add-days (days epoch)
+  "Add DAYS to EPOCH, which is an universal time.  DAYS can be
+negative."
+  (+ (* 60 60 24 days) epoch))
+
+;; The following two functions are based on Thomas Russ <tar@isi.edu>
+;; code which didn't carry any copyright notice, so I assume it was in
+;; the public domain.
+
+(defun iso-time-string (time &key time-zone with-timezone-p basic)
+  "Return an ISO 8601 string representing TIME.  The time zone is
+included if WITH-TIMEZONE-P is true."
+  (flet ((format-timezone (zone)
+           (if (zerop zone)
+               "Z"
+               (multiple-value-bind (h m) (truncate (abs zone) 1.0)
+                 ;; Sign of time zone is reversed in ISO 8601 relative
+                 ;; to Common Lisp convention!
+                 (format nil "~:[+~;-~]~2,'0D:~2,'0D"
+                         (> zone 0) h (round m))))))
+    (multiple-value-bind (second minute hour day month year dow dst zone)
+        (decode-universal-time time time-zone)
+      (declare (ignore dow dst))
+      (if basic
+          (format nil "~4,'0D~2,'0D~2,'0DT~2,'0D~2,'0D~2,'0D~[~*~;~A~]"
+                  year month day hour minute second
+                  with-timezone-p (format-timezone zone))
+          (format nil "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D~:[~*~;~A~]"
+                  year month day hour minute second
+                  with-timezone-p (format-timezone zone))))))
+
+(defun parse-iso-time-string (time-string)
+  "Parse an ISO 8601 formated string and return the universal time.
+It can parse the basic and the extended format, but may not be able to
+cover all the cases."
+  (labels ((parse-delimited-string (string delimiter n)
+             ;; Parses a delimited string and returns a list of
+             ;; n integers found in that string.
+             (let ((answer (make-list n :initial-element 0)))
+               (loop
+                  for i upfrom 0
+                  for start = 0 then (1+ end)
+                  for end = (position delimiter string :start (1+ start))
+                  do (setf (nth i answer)
+                           (parse-integer (subseq string start end)))
+                  when (null end) return t)
+               (values-list answer)))
+           (parse-fixed-field-string (string field-sizes)
+             ;; Parses a string with fixed length fields and returns
+             ;; a list of integers found in that string.
+             (let ((answer (make-list (length field-sizes) :initial-element 0)))
+               (loop
+                  with len = (length string)
+                  for start = 0 then (+ start field-size)
+                  for field-size in field-sizes
+                  for i upfrom 0
+                  while (< start len)
+                  do (setf (nth i answer)
+                           (parse-integer (subseq string start (+ start field-size)))))
+               (values-list answer)))
+           (parse-iso8601-date (date-string)
+             (let ((hyphen-pos (position #\- date-string)))
+               (if hyphen-pos
+                   (parse-delimited-string date-string #\- 3)
+                   (parse-fixed-field-string date-string '(4 2 2)))))
+           (parse-iso8601-timeonly (time-string)
+             (let* ((colon-pos (position #\: time-string))
+                    (zone-pos (or (position #\- time-string)
+                                  (position #\+ time-string)))
+                    (timeonly-string (subseq time-string 0 zone-pos))
+                    (zone-string (when zone-pos (subseq time-string (1+ zone-pos))))
+                    (time-zone nil))
+               (when zone-pos
+                 (multiple-value-bind (zone-h zone-m)
+                     (parse-delimited-string zone-string #\: 2)
+                   (setq time-zone (+ zone-h (/ zone-m 60)))
+                   (when (char= (char time-string zone-pos) #\-)
+                     (setq time-zone (- time-zone)))))
+               (multiple-value-bind (hh mm ss)
+                   (if colon-pos
+                       (parse-delimited-string timeonly-string #\: 3)
+                       (parse-fixed-field-string timeonly-string '(2 2 2)))
+                 (values hh mm ss time-zone)))))
+    (let ((time-separator (position #\T time-string)))
+      (multiple-value-bind (year month date)
+          (parse-iso8601-date
+           (subseq time-string 0 time-separator))
+        (if time-separator
+            (multiple-value-bind (hh mm ss zone)
+                (parse-iso8601-timeonly
+                 (subseq time-string (1+ time-separator)))
+              (if zone
+                  ;; Sign of time zone is reversed in ISO 8601
+                  ;; relative to Common Lisp convention!
+                  (encode-universal-time ss mm hh date month year (- zone))
+                  (encode-universal-time ss mm hh date month year)))
+            (encode-universal-time 0 0 0 date month year))))))
+
+(defun time-string (time &optional time-zone)
+  "Return a string representing TIME in the form:
+  Tue Jan 25 12:55:40 2005"
+  (multiple-value-bind (ss mm hh day month year week-day)
+      (decode-universal-time time time-zone)
+    (format nil "~A ~A ~A ~D:~2,'0D:~2,'0D ~A"
+            (subseq (week-day->string week-day) 0 3)
+            (subseq (month->string month) 0 3)
+            day
+            hh mm ss
+            year)))
+
+(defun beginning-of-month (month year &optional time-zone)
+  (encode-universal-time 0 0 0 1 month year time-zone))
+
+(defun end-of-month (month year &optional time-zone)
+  (1- (add-months 1 (encode-universal-time 0 0 0 1 month year time-zone))))
+
+(defun beginning-of-first-week (year &optional time-zone)
+  "Return the epoch of the first week of YEAR.  As the first week
+of the year needs to have Thursday in this YEAR, the returned
+time can actually fall in the previous year."
+  (let* ((Jan-1st (encode-universal-time 0 0 0 1 1 year time-zone))
+         (start (- 4 (week-day (add-days 4 Jan-1st)))))
+    (add-days start Jan-1st)))
+
+(defun beginning-of-week (week year &optional time-zone)
+  "Return the epoch of the beginning of WEEK of YEAR."
+  (add-days (* (1- week) 7) (beginning-of-first-week year time-zone)))
+
+(defun end-of-week (week year &optional time-zone)
+  "Return the epoch of the beginning of WEEK of YEAR."
+  (1- (beginning-of-week (1+ week) year time-zone)))
+
+(defun end-of-last-week (year &optional time-zone)
+  "Return the epoch of the last week of YEAR.  As the last week
+of the year needs to have Thursday in this YEAR, the returned
+time can fall in the next year."
+  (1- (beginning-of-first-week (1+ year) time-zone)))
+
+(defun seconds-from-beginning-of-the-year (time &optional time-zone)
+  (- time (encode-universal-time 0 0 0 1 1 (year time) time-zone)))
+
+(defun day-of-the-year (time &optional time-zone)
+  "Return the day within the year of TIME starting from 1 up to
+365 (or 366)."
+  (1+ (truncate (seconds-from-beginning-of-the-year time time-zone)
+                (* 60 60 24))))
+
+(defun week (time &optional time-zone)
+  "Return the number of the week and the year TIME referes to.
+Week is an integer from 1 to 52.  Due to the way the first week
+of the year is calculated a day in one year could actually be in
+the last week of the previous or next year."
+  (let* ((year (year time))
+         (start (beginning-of-first-week year time-zone))
+         (days-from-start (truncate (- time start) (* 60 60 24)))
+         (weeks (truncate days-from-start 7))
+         (week-number (mod weeks 52)))
+    (values (1+ week-number)
+            (cond ((< weeks 0)
+                   (1- year))
+                  ((> weeks 51)
+                   (1+ year))
+                  (t year)))))
+
+(defun week-day->string (day &optional sunday-first)
+  "Return the weekday string corresponding to DAY number."
+  (elt (if sunday-first
+           #("Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday")
+           #("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday"))
+       day))
+
+(defconst +month-names+  #("January" "February" "March" "April" "May" "June" "July"
+                           "August" "September" "October" "November" "December"))
+
+(defun month->string (month)
+  "Return the month string corresponding to MONTH number."
+  (elt +month-names+ (1- month)))
+
+(defun month-string->number (month)
+  (1+ (position month +month-names+ :test #'string-equal)))
+
+(defun print-time-span (span &optional stream)
+  "Print in English the time SPAN expressed in seconds."
+  (let* ((minute 60)
+         (hour (* minute 60))
+         (day (* hour 24))
+         (seconds span))
+    (macrolet ((split (divisor)
+                 `(when (>= seconds ,divisor)
+                    (prog1 (truncate seconds ,divisor)
+                      (setf seconds (mod seconds ,divisor))))))
+      (let* ((days (split day))
+             (hours (split hour))
+             (minutes (split minute)))
+        (format stream "~{~A~^ ~}" (remove nil
+                                           (list
+                                            (when days
+                                              (format nil "~D day~:P" days))
+                                            (when hours
+                                              (format nil "~D hour~:P" hours))
+                                            (when minutes
+                                              (format nil "~D minute~:P" minutes))
+                                            (when (or (> seconds 0)
+                                                      (= span 0))
+                                              (format nil "~D second~:P" seconds)))))))))
+
+(defun next-week-day (epoch week-day &optional time-zone)
+  "Return the universal time of the next WEEK-DAY starting from epoch."
+  (add-days (mod (- week-day (week-day epoch time-zone)) 7)
+            epoch))
+
+(defun next-monday (epoch &optional time-zone)
+  "Return the universal time of the next Monday starting from
+EPOCH."
+  (next-week-day epoch 0 time-zone))
+
+(defun full-weeks-in-span (start end &optional time-zone)
+  "Return the number of full weeks in time span START to END.  A
+full week starts on Monday and ends on Sunday."
+  (be first-monday (next-monday start time-zone)
+    (truncate (- end first-monday) (* 7 24 60 60))))
+
+(defconst +unix-lisp-time-difference+
+  (encode-universal-time 0 0 0 1 1 1970 0)
+  "Time difference between Unix epoch and Common Lisp epoch.  The
+former is 1st January 1970, while the latter is the beginning of the
+XX century.")
+
+(defun universal->unix-time (time)
+  (- time +unix-lisp-time-difference+))
+
+(defun unix->universal-time (time)
+  (+ time +unix-lisp-time-difference+))
+
+(defun get-unix-time ()
+  (universal->unix-time (get-universal-time)))
diff --git a/third_party/lisp/split-sequence.nix b/third_party/lisp/split-sequence.nix
new file mode 100644
index 0000000000..4e8f723c31
--- /dev/null
+++ b/third_party/lisp/split-sequence.nix
@@ -0,0 +1,15 @@
+# split-sequence is a library for, well, splitting sequences apparently.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.split-sequence;
+in depot.nix.buildLisp.library {
+  name = "split-sequence";
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "vector.lisp"
+    "list.lisp"
+    "extended-sequence.lisp"
+    "api.lisp"
+    "documentation.lisp"
+  ];
+}
diff --git a/third_party/lisp/trivial-backtrace.nix b/third_party/lisp/trivial-backtrace.nix
new file mode 100644
index 0000000000..27949e8be1
--- /dev/null
+++ b/third_party/lisp/trivial-backtrace.nix
@@ -0,0 +1,15 @@
+# Imported from http://common-lisp.net/project/trivial-backtrace/trivial-backtrace.git
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.trivial-backtrace;
+in depot.nix.buildLisp.library {
+  name = "trivial-backtrace";
+
+  srcs = map (f: src + ("/dev/" + f)) [
+    "packages.lisp"
+    "utilities.lisp"
+    "backtrace.lisp"
+    "map-backtrace.lisp"
+    "fallback.lisp"
+  ];
+}
diff --git a/third_party/lisp/trivial-features.nix b/third_party/lisp/trivial-features.nix
new file mode 100644
index 0000000000..02abac54a8
--- /dev/null
+++ b/third_party/lisp/trivial-features.nix
@@ -0,0 +1,13 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.trivial-features;
+in depot.nix.buildLisp.library {
+  name = "trivial-features";
+  srcs = [
+    {
+      sbcl = src + "/src/tf-sbcl.lisp";
+      ecl = src + "/src/tf-ecl.lisp";
+      ccl = src + "/src/tf-openmcl.lisp";
+    }
+  ];
+}
diff --git a/third_party/lisp/trivial-garbage.nix b/third_party/lisp/trivial-garbage.nix
new file mode 100644
index 0000000000..74224df60d
--- /dev/null
+++ b/third_party/lisp/trivial-garbage.nix
@@ -0,0 +1,9 @@
+# trivial-garbage provides a portable API to finalizers, weak
+# hash-tables and weak pointers
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.trivial-garbage;
+in depot.nix.buildLisp.library {
+  name = "trivial-garbage";
+  srcs = [ (src + "/trivial-garbage.lisp") ];
+}
diff --git a/third_party/lisp/trivial-gray-streams.nix b/third_party/lisp/trivial-gray-streams.nix
new file mode 100644
index 0000000000..62a30f1e94
--- /dev/null
+++ b/third_party/lisp/trivial-gray-streams.nix
@@ -0,0 +1,13 @@
+# Portability library for CL gray streams.
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.trivial-gray-streams;
+in depot.nix.buildLisp.library {
+  name = "trivial-gray-streams";
+  srcs = [
+    (src + "/package.lisp")
+    (src + "/streams.lisp")
+  ];
+}
+
+
diff --git a/third_party/lisp/trivial-indent.nix b/third_party/lisp/trivial-indent.nix
new file mode 100644
index 0000000000..70a6e19d48
--- /dev/null
+++ b/third_party/lisp/trivial-indent.nix
@@ -0,0 +1,10 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.trivial-indent;
+in depot.nix.buildLisp.library {
+  name = "trivial-indent";
+
+  srcs = map (f: src + ("/" + f)) [
+    "indent.lisp"
+  ];
+}
diff --git a/third_party/lisp/trivial-ldap.nix b/third_party/lisp/trivial-ldap.nix
new file mode 100644
index 0000000000..c85fe2accb
--- /dev/null
+++ b/third_party/lisp/trivial-ldap.nix
@@ -0,0 +1,28 @@
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.fetchFromGitHub {
+    owner = "rwiker";
+    repo = "trivial-ldap";
+    rev = "3b8f1ff85f29ea63e6ab2d0d27029d68b046faf8";
+    sha256 = "1zaa4wnk5y5ff211pkg6dl27j4pjwh56hq0246slxsdxv6kvp1z9";
+  };
+in
+depot.nix.buildLisp.library {
+  name = "trivial-ldap";
+
+  deps = with depot.third_party.lisp; [
+    usocket
+    cl-plus-ssl
+    cl-yacc
+  ];
+
+  srcs = map (f: src + ("/" + f)) [
+    "package.lisp"
+    "trivial-ldap.lisp"
+  ];
+
+  brokenOn = [
+    "ecl" # dynamic cffi
+  ];
+}
diff --git a/third_party/lisp/trivial-mimes.nix b/third_party/lisp/trivial-mimes.nix
new file mode 100644
index 0000000000..b097a3d0ee
--- /dev/null
+++ b/third_party/lisp/trivial-mimes.nix
@@ -0,0 +1,26 @@
+{ depot, pkgs, ... }:
+
+let
+  src = with pkgs; srcOnly lispPackages.trivial-mimes;
+
+  mime-types = pkgs.runCommand "mime-types.lisp" { } ''
+    substitute ${src}/mime-types.lisp $out \
+      --replace /etc/mime.types ${src}/mime.types \
+      --replace "(asdf:system-source-directory :trivial-mimes)" '"/bogus-dir"'
+      # We want to prevent an ASDF lookup at build time since this will
+      # generally fail — we are not using ASDF after all.
+  '';
+
+in
+depot.nix.buildLisp.library {
+  name = "trivial-mimes";
+
+  deps = [
+    {
+      sbcl = depot.nix.buildLisp.bundled "uiop";
+      default = depot.nix.buildLisp.bundled "asdf";
+    }
+  ];
+
+  srcs = [ mime-types ];
+}
diff --git a/third_party/lisp/uax-15.nix b/third_party/lisp/uax-15.nix
new file mode 100644
index 0000000000..f98c029d36
--- /dev/null
+++ b/third_party/lisp/uax-15.nix
@@ -0,0 +1,43 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs) runCommand;
+  inherit (depot.nix.buildLisp) bundled;
+  src = with pkgs; srcOnly lispPackages.uax-15;
+in
+depot.nix.buildLisp.library {
+  name = "uax-15";
+
+  deps = with depot.third_party.lisp; [
+    split-sequence
+    cl-ppcre
+    (bundled "asdf")
+  ];
+
+  srcs = [
+    "${src}/src/package.lisp"
+    "${src}/src/utilities.lisp"
+    "${src}/src/trivial-utf-16.lisp"
+
+    # uax-15 has runtime data files that need to have their references
+    # replaced with store paths.
+    #
+    # additionally there are some wonky variable usages of variables
+    # that are never defined, for which we patch in defvar statements.
+    (runCommand "precomputed-tables.lisp" { } ''
+      substitute ${src}/src/precomputed-tables.lisp precomputed-tables.lisp \
+        --replace "(asdf:system-source-directory (asdf:find-system 'uax-15 nil))" \
+                  '"${src}/"'
+
+      sed -i precomputed-tables.lisp \
+        -e '10i(defvar *canonical-decomp-map*)' \
+        -e '10i(defvar *compatible-decomp-map*)' \
+        -e '10i(defvar *canonical-combining-class*)'
+
+      cp precomputed-tables.lisp $out
+    '')
+
+    "${src}/src/normalize-backend.lisp"
+    "${src}/src/uax-15.lisp"
+  ];
+}
diff --git a/third_party/lisp/unix-opts.nix b/third_party/lisp/unix-opts.nix
new file mode 100644
index 0000000000..2482961132
--- /dev/null
+++ b/third_party/lisp/unix-opts.nix
@@ -0,0 +1,12 @@
+# unix-opts is a portable command line argument parser
+{ depot, pkgs, ... }:
+
+
+let src = with pkgs; srcOnly lispPackages.unix-opts;
+in depot.nix.buildLisp.library {
+  name = "unix-opts";
+
+  srcs = [
+    "${src}/unix-opts.lisp"
+  ];
+}
diff --git a/third_party/lisp/usocket-server.nix b/third_party/lisp/usocket-server.nix
new file mode 100644
index 0000000000..5d6d04535f
--- /dev/null
+++ b/third_party/lisp/usocket-server.nix
@@ -0,0 +1,19 @@
+# Universal socket library for Common Lisp (server side)
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+  src = with pkgs; srcOnly lispPackages.usocket-server;
+in
+buildLisp.library {
+  name = "usocket-server";
+
+  deps = with depot.third_party.lisp; [
+    usocket
+    bordeaux-threads
+  ];
+
+  srcs = [
+    "${src}/server.lisp"
+  ];
+}
diff --git a/third_party/lisp/usocket.nix b/third_party/lisp/usocket.nix
new file mode 100644
index 0000000000..589a3a0cfc
--- /dev/null
+++ b/third_party/lisp/usocket.nix
@@ -0,0 +1,46 @@
+# Usocket is a portable socket library
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+  src = with pkgs; srcOnly lispPackages.usocket;
+in
+buildLisp.library {
+  name = "usocket";
+  deps = with depot.third_party.lisp; [
+    (buildLisp.bundled "asdf")
+    {
+      ecl = buildLisp.bundled "sb-bsd-sockets";
+      sbcl = buildLisp.bundled "sb-bsd-sockets";
+    }
+    split-sequence
+  ];
+
+  srcs = [
+    # usocket also reads its version from ASDF, but there's further
+    # shenanigans happening there that I don't intend to support right
+    # now. Behold:
+    (builtins.toFile "usocket.asd" ''
+      (in-package :asdf)
+      (defsystem usocket
+        :version "0.8.3")
+    '')
+  ] ++
+  # Now for the regularly scheduled programming:
+  (map (f: src + ("/" + f)) [
+    "package.lisp"
+    "usocket.lisp"
+    "condition.lisp"
+  ] ++ [
+    { sbcl = "${src}/backend/sbcl.lisp"; }
+
+    # ECL actually has two files, it supports the SBCL backend,
+    # but usocket also has some ECL specific code
+    { ecl = "${src}/backend/sbcl.lisp"; }
+    { ecl = "${src}/backend/ecl.lisp"; }
+
+    # Same for CCL
+    { ccl = "${src}/backend/openmcl.lisp"; }
+    { ccl = "${src}/backend/clozure.lisp"; }
+  ]);
+}
diff --git a/third_party/naersk/default.nix b/third_party/naersk/default.nix
new file mode 100644
index 0000000000..bf4c55fe55
--- /dev/null
+++ b/third_party/naersk/default.nix
@@ -0,0 +1,3 @@
+{ depot, pkgs, ... }:
+
+pkgs.callPackage depot.third_party.sources.naersk { }
diff --git a/third_party/nixpkgs/default.nix b/third_party/nixpkgs/default.nix
new file mode 100644
index 0000000000..3471468c4c
--- /dev/null
+++ b/third_party/nixpkgs/default.nix
@@ -0,0 +1,61 @@
+# This file imports the pinned nixpkgs sets and applies relevant
+# modifications, such as our overlays.
+#
+# The actual source pinning happens via niv in //third_party/sources
+#
+# Note that the attribute exposed by this (third_party.nixpkgs) is
+# "special" in that the fixpoint used as readTree's config parameter
+# in //default.nix passes this attribute as the `pkgs` argument to all
+# readTree derivations.
+
+{ depot ? { }, externalArgs ? { }, depotOverlays ? true, ... }:
+
+let
+  # import the nixos-unstable package set, or optionally use the
+  # source (e.g. a path) specified by the `nixpkgsBisectPath`
+  # argument. This is intended for use-cases where the depot is
+  # bisected against nixpkgs to find the root cause of an issue in a
+  # channel bump.
+  nixpkgsSrc = externalArgs.nixpkgsBisectPath or depot.third_party.sources.nixpkgs;
+
+  # Stable package set is imported, but not exposed, to overlay
+  # required packages into the unstable set.
+  stableNixpkgs = import depot.third_party.sources.nixpkgs-stable { };
+
+  # Overlay for packages that should come from the stable channel
+  # instead (e.g. because something is broken in unstable).
+  # Use `stableNixpkgs` from above.
+  stableOverlay = _unstableSelf: _unstableSuper: {
+    # bat syntaxes changed with syntect 5.0, but cheddar is still on 4.x
+    # TODO(tazjin): upgrade cheddar to syntect 5.0
+    bat = stableNixpkgs.bat;
+  };
+
+  # Overlay to expose the nixpkgs commits we are using to other Nix code.
+  commitsOverlay = _: _: {
+    nixpkgsCommits = {
+      unstable = depot.third_party.sources.nixpkgs.rev;
+      stable = depot.third_party.sources.nixpkgs-stable.rev;
+    };
+  };
+in
+import nixpkgsSrc {
+  # allow users to inject their config into builds (e.g. to test CA derivations)
+  config =
+    (if externalArgs ? nixpkgsConfig then externalArgs.nixpkgsConfig else { })
+    // {
+      allowUnfree = true;
+      allowBroken = true;
+    };
+
+  overlays = [
+    commitsOverlay
+    stableOverlay
+  ] ++ (if depotOverlays then [
+    depot.third_party.overlays.haskell
+    depot.third_party.overlays.emacs
+    depot.third_party.overlays.tvl
+    depot.third_party.overlays.ecl-static
+    depot.third_party.overlays.dhall
+  ] else [ ]);
+}
diff --git a/third_party/nsfv/default.nix b/third_party/nsfv/default.nix
new file mode 100644
index 0000000000..26d5488c42
--- /dev/null
+++ b/third_party/nsfv/default.nix
@@ -0,0 +1,23 @@
+# Real-time Noise Suppression Plugin (for PulseAudio).
+#
+# This should be invoked as a "ladspa" plugin for pulseaudio.
+#
+# https://github.com/werman/noise-suppression-for-voice
+{ pkgs, lib, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "noise-suppression-for-voice";
+
+  nativeBuildInputs = [ pkgs.cmake ];
+  src = pkgs.fetchFromGitHub {
+    owner = "werman";
+    repo = "noise-suppression-for-voice";
+    rev = "cd5c79378ab9819cd85f4fd108f3c77a40fd66ac";
+    sha256 = "10zh1al1bys60sjdd4p72qbp9jfb5wq1zaw33b595psgwmqpbckq";
+  };
+
+  meta = with lib; {
+    description = "Real-time noise suppression LADSPA plugin";
+    license = licenses.gpl3;
+  };
+}
diff --git a/third_party/overlays/dhall/OWNERS b/third_party/overlays/dhall/OWNERS
new file mode 100644
index 0000000000..a742d0d22b
--- /dev/null
+++ b/third_party/overlays/dhall/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - Profpatsch
diff --git a/third_party/overlays/dhall/default.nix b/third_party/overlays/dhall/default.nix
new file mode 100644
index 0000000000..74d2a999b9
--- /dev/null
+++ b/third_party/overlays/dhall/default.nix
@@ -0,0 +1,27 @@
+{ ... }:
+
+pkgs: _:
+
+let
+  # binary releases of dhall tools, since the build in nixpkgs is
+  # broken most of the time. The binaries are also fully static
+  # builds, instead of the half-static crap that nixpkgs produces.
+  easy-dhall-nix =
+    import
+      (builtins.fetchTarball {
+        url = "https://github.com/justinwoo/easy-dhall-nix/archive/eae7f64c4d6c70681e5a56c84198236930ba425e.tar.gz";
+        sha256 = "1y2x15v8a679vlpxazjpibfwajp6zph60f8wjcm4xflbvazk0dx7";
+      })
+      { inherit pkgs; };
+in
+{
+  dhall = easy-dhall-nix.dhall-simple;
+  dhall-bash = easy-dhall-nix.dhall-bash-simple;
+  dhall-docs = easy-dhall-nix.dhall-docs-simple;
+  dhall-json = easy-dhall-nix.dhall-json-simple;
+  dhall-lsp-server = easy-dhall-nix.dhall-lsp-simple;
+  dhall-nix = easy-dhall-nix.dhall-nix-simple;
+  # not yet in dhall-simple
+  # dhall-nixpkgs = easy-dhall-nix.dhall-nixpkgs-simple;
+  dhall-yaml = easy-dhall-nix.dhall-yaml-simple;
+}
diff --git a/third_party/overlays/ecl-static.nix b/third_party/overlays/ecl-static.nix
new file mode 100644
index 0000000000..66579c33ab
--- /dev/null
+++ b/third_party/overlays/ecl-static.nix
@@ -0,0 +1,37 @@
+{ ... }:
+
+self: super:
+
+{
+  # Statically linked ECL with statically linked dependencies.
+  # Works quite well, but solving this properly in a nixpkgs
+  # context will require figuring out cross compilation (for
+  # pkgsStatic), so we're gonna use this override for now.
+  #
+  # Note that ecl-static does mean that we have things
+  # statically linked against GMP and ECL which are LGPL.
+  # I believe this should be alright: The way ppl are gonna
+  # interact with the distributed binaries (i. e. the binary
+  # cache) is Nix in the depot monorepo, so the separability
+  # requirement should be satisfied: Source code or overriding
+  # would be available as ways to swap out the used GMP in the
+  # program.
+  # See https://www.gnu.org/licenses/gpl-faq.en.html#LGPLStaticVsDynamic
+  ecl-static = (super.pkgsMusl.ecl.override {
+    inherit (self.pkgsStatic) gmp libffi boehmgc;
+  }).overrideAttrs (drv: rec {
+    # version must not be changed as it indicates where to find the bundled libs,
+    # using ecl HEAD is necessary for us since it includes multiple fixes to do
+    # with bytecode compilation and allows to concatenate fasc files again.
+    src = self.fetchFromGitLab {
+      owner = "embeddable-common-lisp";
+      repo = "ecl";
+      rev = "1c989247c1b0bf1d38a76aec30b9ca5e41afe1e3";
+      sha256 = "0bzjqw6m1kk5z5b81yizic347k931msp5lf78x65dcw3fqfwv3xn";
+    };
+    configureFlags = drv.configureFlags ++ [
+      "--disable-shared"
+      "--with-dffi=no" # will fail at runtime anyways if statically linked
+    ];
+  });
+}
diff --git a/third_party/overlays/emacs.nix b/third_party/overlays/emacs.nix
new file mode 100644
index 0000000000..341feb5015
--- /dev/null
+++ b/third_party/overlays/emacs.nix
@@ -0,0 +1,4 @@
+# Emacs overlay from https://github.com/nix-community/emacs-overlay
+{ depot, ... }:
+
+import depot.third_party.sources.emacs-overlay
diff --git a/third_party/overlays/haskell/.skip-subtree b/third_party/overlays/haskell/.skip-subtree
new file mode 100644
index 0000000000..2a528eaa8a
--- /dev/null
+++ b/third_party/overlays/haskell/.skip-subtree
@@ -0,0 +1 @@
+extra-pkgs need to be callPackage-ed
diff --git a/third_party/overlays/haskell/default.nix b/third_party/overlays/haskell/default.nix
new file mode 100644
index 0000000000..0ed0196a28
--- /dev/null
+++ b/third_party/overlays/haskell/default.nix
@@ -0,0 +1,31 @@
+# Defines an overlay for overriding Haskell packages, for example to
+# avoid breakage currently present in nixpkgs or to modify package
+# versions.
+
+{ lib, ... }:
+
+self: super: # overlay parameters for the nixpkgs overlay
+
+let
+  overrides = hsSelf: hsSuper: with self.haskell.lib.compose; {
+    # No overrides for the default package set necessary at the moment
+  };
+in
+{
+  haskellPackages = super.haskellPackages.override {
+    inherit overrides;
+  };
+
+  haskell = lib.recursiveUpdate super.haskell {
+    packages.ghc8107 = super.haskell.packages.ghc8107.override {
+      overrides = lib.composeExtensions overrides (
+        hsSelf: hsSuper: with self.haskell.lib.compose; {
+          # TODO(sterni): TODO(grfn): patch xanthous to work with random-fu 0.3.*,
+          # so we can use GHC 9.0.2 and benefit from upstream binary cache.
+          random-fu = hsSelf.callPackage ./extra-pkgs/random-fu-0.2.nix { };
+          rvar = hsSelf.callPackage ./extra-pkgs/rvar-0.2.nix { };
+        }
+      );
+    };
+  };
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/random-fu-0.2.nix b/third_party/overlays/haskell/extra-pkgs/random-fu-0.2.nix
new file mode 100644
index 0000000000..1626eca7be
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/random-fu-0.2.nix
@@ -0,0 +1,41 @@
+{ mkDerivation
+, base
+, erf
+, lib
+, math-functions
+, monad-loops
+, mtl
+, random
+, random-shuffle
+, random-source
+, rvar
+, syb
+, template-haskell
+, transformers
+, vector
+}:
+mkDerivation {
+  pname = "random-fu";
+  version = "0.2.7.7";
+  sha256 = "8466bcfb5290bdc30a571c91e1eb526c419ea9773bc118996778b516cfc665ca";
+  revision = "1";
+  editedCabalFile = "16nhymfriygqr2by9v72vdzv93v6vhd9z07pgaji4zvv66jikv82";
+  libraryHaskellDepends = [
+    base
+    erf
+    math-functions
+    monad-loops
+    mtl
+    random
+    random-shuffle
+    random-source
+    rvar
+    syb
+    template-haskell
+    transformers
+    vector
+  ];
+  homepage = "https://github.com/mokus0/random-fu";
+  description = "Random number generation";
+  license = lib.licenses.publicDomain;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/rvar-0.2.nix b/third_party/overlays/haskell/extra-pkgs/rvar-0.2.nix
new file mode 100644
index 0000000000..c00f5a1a8d
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/rvar-0.2.nix
@@ -0,0 +1,25 @@
+{ mkDerivation
+, base
+, lib
+, MonadPrompt
+, mtl
+, random-source
+, transformers
+}:
+mkDerivation {
+  pname = "rvar";
+  version = "0.2.0.6";
+  sha256 = "01e18875ffde43f9591a8acd9f60c9c51704a026e51c1a6797faecd1c7ae8cd3";
+  revision = "1";
+  editedCabalFile = "1jn9ivlj3k65n8d9sfsp882m5lvni1ah79mk0cvkz91pgywvkiyq";
+  libraryHaskellDepends = [
+    base
+    MonadPrompt
+    mtl
+    random-source
+    transformers
+  ];
+  homepage = "https://github.com/mokus0/random-fu";
+  description = "Random Variables";
+  license = lib.licenses.publicDomain;
+}
diff --git a/third_party/overlays/patches/notmuch-dottime.patch b/third_party/overlays/patches/notmuch-dottime.patch
new file mode 100644
index 0000000000..7a9cfc6cc2
--- /dev/null
+++ b/third_party/overlays/patches/notmuch-dottime.patch
@@ -0,0 +1,81 @@
+From 569438172fa0e38129de4e61a72e06eff3330dca Mon Sep 17 00:00:00 2001
+From: Vincent Ambo <tazjin@google.com>
+Date: Thu, 10 Dec 2020 10:53:47 +0100
+Subject: [PATCH] time: Use dottime for formatting non-relative timestamps
+
+---
+ notmuch-time.c     | 10 +++++-----
+ util/gmime-extra.c |  7 +++++--
+ util/gmime-extra.h |  2 ++
+ 3 files changed, 12 insertions(+), 7 deletions(-)
+
+diff --git a/notmuch-time.c b/notmuch-time.c
+index cc7ffc23..3030a667 100644
+--- a/notmuch-time.c
++++ b/notmuch-time.c
+@@ -50,8 +50,8 @@ notmuch_time_relative_date (const void *ctx, time_t then)
+     time_t delta;
+     char *result;
+ 
+-    localtime_r (&now, &tm_now);
+-    localtime_r (&then, &tm_then);
++    gmtime_r (&now, &tm_now);
++    gmtime_r (&then, &tm_then);
+ 
+     result = talloc_zero_size (ctx, RELATIVE_DATE_MAX);
+     if (result == NULL)
+@@ -78,16 +78,16 @@ notmuch_time_relative_date (const void *ctx, time_t then)
+ 	if (tm_then.tm_wday == tm_now.tm_wday &&
+ 	    delta < DAY) {
+ 	    strftime (result, RELATIVE_DATE_MAX,
+-		      "Today %R", &tm_then);    /* Today 12:30 */
++		      "Today %k·%M", &tm_then); /* Today 12·30 */
+ 	    return result;
+ 	} else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
+ 	    strftime (result, RELATIVE_DATE_MAX,
+-		      "Yest. %R", &tm_then);    /* Yest. 12:30 */
++		      "Yest. %k·%M", &tm_then); /* Yest. 12·30 */
+ 	    return result;
+ 	} else {
+ 	    if (tm_then.tm_wday != tm_now.tm_wday) {
+ 		strftime (result, RELATIVE_DATE_MAX,
+-			  "%a. %R", &tm_then);  /* Mon. 12:30 */
++			  "%a. %k·%M", &tm_then); /* Mon. 12·30 */
+ 		return result;
+ 	    }
+ 	}
+diff --git a/util/gmime-extra.c b/util/gmime-extra.c
+index 04d8ed3d..868a2f69 100644
+--- a/util/gmime-extra.c
++++ b/util/gmime-extra.c
+@@ -131,10 +131,13 @@ g_mime_message_get_date_string (void *ctx, GMimeMessage *message)
+     GDateTime *parsed_date = g_mime_message_get_date (message);
+ 
+     if (parsed_date) {
+-	char *date = g_mime_utils_header_format_date (parsed_date);
++	char *date = g_date_time_format(
++		parsed_date,
++		"%a, %d %b %Y %H·%M%z"
++	);
+ 	return g_string_talloc_strdup (ctx, date);
+     } else {
+-	return talloc_strdup (ctx, "Thu, 01 Jan 1970 00:00:00 +0000");
++	return talloc_strdup (ctx, "Thu, 01 Jan 1970 00·00:00");
+     }
+ }
+ 
+diff --git a/util/gmime-extra.h b/util/gmime-extra.h
+index 094309ec..e6c98f8d 100644
+--- a/util/gmime-extra.h
++++ b/util/gmime-extra.h
+@@ -1,5 +1,7 @@
+ #ifndef _GMIME_EXTRA_H
+ #define _GMIME_EXTRA_H
++#include <glib.h>
++#include <glib/gprintf.h>
+ #include <gmime/gmime.h>
+ #include <talloc.h>
+ 
+-- 
+2.29.2.576.ga3fc446d84-goog
+
diff --git a/third_party/overlays/tvl.nix b/third_party/overlays/tvl.nix
new file mode 100644
index 0000000000..fbc48e1565
--- /dev/null
+++ b/third_party/overlays/tvl.nix
@@ -0,0 +1,114 @@
+# This overlay is used to make TVL-specific modifications in the
+# nixpkgs tree, where required.
+{ depot, ... }:
+
+self: super:
+let
+  # Rollback Nix to a stable version (2.3) with backports for
+  # build-user problems applied.
+  nixSrc = self.fetchFromGitHub
+    {
+      owner = "tvlfyi";
+      repo = "nix";
+      # branch 2.3-backport-await-users
+      rev = "880a62b08443a6baa55dab027b69bb8b1551a588";
+      hash = "sha256:0jnwrzxh04d0pyhx4n8fg4w1w6ak48byl5k2i8j7fk4h9vd9649k";
+    } // { revCount = 0; shortRev = "880a62b0"; };
+in
+{
+  nix = (import "${nixSrc}/release.nix" {
+    nix = nixSrc;
+    nixpkgs = super.path;
+    systems = [ builtins.currentSystem ];
+  }).build."${builtins.currentSystem}";
+
+  clang-tools_11 = self.clang-tools.override {
+    llvmPackages = self.llvmPackages_11;
+  };
+
+  # stdenv which uses clang, lld and libc++; full is a slight exaggeration,
+  # we for example don't use LLVM's libunwind
+  fullLlvm11Stdenv = self.overrideCC self.stdenv
+    (self.llvmPackages_11.libcxxStdenv.cc.override {
+      inherit (self.llvmPackages_11) bintools;
+    });
+
+  # Add our Emacs packages to the fixpoint
+  emacsPackagesFor = emacs: (
+    (super.emacsPackagesFor emacs).overrideScope' (eself: esuper: {
+      tvlPackages = depot.tools.emacs-pkgs // depot.third_party.emacs;
+
+      # Use the notmuch from nixpkgs instead of from the Emacs
+      # overlay, to avoid versions being out of sync.
+      notmuch = super.notmuch.emacs;
+
+      # Build EXWM with the depot sources instead.
+      exwm = esuper.exwm.overrideAttrs (_: {
+        src = depot.path.origSrc + "/third_party/exwm";
+      });
+
+      # Workaround for magit checking the git version at load time
+      magit = esuper.magit.overrideAttrs (_: {
+        propagatedNativeBuildInputs = [
+          self.git
+        ];
+      });
+    })
+  );
+
+  # dottime support for notmuch
+  notmuch = super.notmuch.overrideAttrs (old: {
+    passthru = old.passthru // {
+      patches = old.patches ++ [ ./patches/notmuch-dottime.patch ];
+    };
+  });
+
+  # nix-serve does not work with nix 2.4
+  # https://github.com/edolstra/nix-serve/issues/28
+  nix-serve = super.nix-serve.override { nix = super.nix_2_3; };
+
+  # Avoid builds of mkShell derivations in CI.
+  mkShell = super.lib.makeOverridable (args: (super.mkShell args).overrideAttrs (_: {
+    passthru = {
+      meta.ci.skip = true;
+    };
+  }));
+
+  # bump nixpkgs-fmt to a version that doesn't touch whitespace in
+  # strings
+  nixpkgs-fmt = super.nixpkgs-fmt.overrideAttrs (old: rec {
+    src = self.fetchFromGitHub {
+      owner = "nix-community";
+      repo = "nixpkgs-fmt";
+      rev = "5ae8532b82eb040ca6b21ae2d02d9e88f604e76a";
+      sha256 = "0hjkbcgz62793hzfzlaxyah8a2c1k79n1k891lg7kfw8mkbq0w4p";
+    };
+
+    cargoDeps = old.cargoDeps.overrideAttrs (_: {
+      inherit src;
+      outputHash = "10if2lmv8d95j3walq3ggx3y423yfy4yl9vplw3apd0s671bly8b";
+    });
+  });
+
+  # upgrade home-manager until the service-generation fix has landed upstream
+  # https://github.com/nix-community/home-manager/issues/2846
+  home-manager = super.home-manager.overrideAttrs (old: rec {
+    version = assert super.home-manager.version == "2021-12-25"; "2022-04-08";
+    src = self.fetchFromGitHub {
+      owner = "nix-community";
+      repo = "home-manager";
+      rev = "f911ebbec927e8e9b582f2e32e2b35f730074cfc";
+      sha256 = "07qa2qkbjczj3d0m03jpw85hfj35cbjm48xhifz3viy4khjw88vl";
+    };
+  });
+
+  python38 = super.python38.override {
+    packageOverrides = pySelf: pySuper: {
+      backports-zoneinfo = pySuper.backports-zoneinfo.overridePythonAttrs (_: {
+        # Outdated test-data, see https://github.com/pganssle/zoneinfo/pull/115
+        # Can be dropped when https://github.com/NixOS/nixpkgs/pull/170450 lands.
+        doCheck = false;
+      });
+    };
+  };
+}
diff --git a/third_party/prometheus-fail2ban-exporter/default.nix b/third_party/prometheus-fail2ban-exporter/default.nix
new file mode 100644
index 0000000000..818839e48c
--- /dev/null
+++ b/third_party/prometheus-fail2ban-exporter/default.nix
@@ -0,0 +1,18 @@
+{ pkgs, ... }:
+
+let
+  script = pkgs.fetchurl {
+    url = "https://raw.githubusercontent.com/jangrewe/prometheus-fail2ban-exporter/11066950b47bb2dbef96ea8544f76e46ed829e81/fail2ban-exporter.py";
+    sha256 = "049lsvw1nj65bbvp8ygyz3743ayzdawrbjixaxmpm03qbrcfmwc4";
+  };
+
+  python = pkgs.python3.withPackages (p: [
+    p.prometheus_client
+  ]);
+
+in
+pkgs.writeShellScriptBin "prometheus-fail2ban-exporter" ''
+  set -eo pipefail
+
+  exec "${python}/bin/python" "${script}"
+''
diff --git a/third_party/python/broadlink/.gitignore b/third_party/python/broadlink/.gitignore
new file mode 100644
index 0000000000..0d20b6487c
--- /dev/null
+++ b/third_party/python/broadlink/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/third_party/python/broadlink/LICENSE b/third_party/python/broadlink/LICENSE
new file mode 100644
index 0000000000..d8c801656b
--- /dev/null
+++ b/third_party/python/broadlink/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Mike Ryan
+Copyright (c) 2016 Matthew Garrett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/python/broadlink/README.md b/third_party/python/broadlink/README.md
new file mode 100644
index 0000000000..8faba2be75
--- /dev/null
+++ b/third_party/python/broadlink/README.md
@@ -0,0 +1,112 @@
+Python control for Broadlink RM2, RM3 and RM4 series controllers
+===============================================
+
+A simple Python API for controlling IR/RF controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, the following devices are currently supported:
+
+* RM Pro (referred to as RM2 in the codebase)
+* A1 sensor platform devices are supported
+* RM3 mini IR blaster
+* RM4 and RM4C mini blasters
+
+There is currently no support for the cloud API.
+
+Example use
+-----------
+
+Setup a new device on your local wireless network:
+
+1. Put the device into AP Mode
+  1. Long press the reset button until the blue LED is blinking quickly.
+  2. Long press again until blue LED is blinking slowly.
+  3. Manually connect to the WiFi SSID named BroadlinkProv.
+2. Run setup() and provide your ssid, network password (if secured), and set the security mode
+  1. Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2)
+```
+import broadlink
+
+broadlink.setup('myssid', 'mynetworkpass', 3)
+```
+
+Discover available devices on the local network:
+```
+import broadlink
+
+devices = broadlink.discover(timeout=5)
+```
+
+Obtain the authentication key required for further communication:
+```
+devices[0].auth()
+```
+
+Enter learning mode:
+```
+devices[0].enter_learning()
+```
+
+Sweep RF frequencies:
+```
+devices[0].sweep_frequency()
+```
+
+Cancel sweep RF frequencies:
+```
+devices[0].cancel_sweep_frequency()
+```
+Check whether a frequency has been found:
+```
+found = devices[0].check_frequency()
+```
+(This will return True if the RM has locked onto a frequency, False otherwise)
+
+Attempt to learn an RF packet:
+```
+found = devices[0].find_rf_packet()
+```
+(This will return True if a packet has been found, False otherwise)
+
+Obtain an IR or RF packet while in learning mode:
+```
+ir_packet = devices[0].check_data()
+```
+(This will return None if the device does not have a packet to return)
+
+Send an IR or RF packet:
+```
+devices[0].send_data(ir_packet)
+```
+
+Obtain temperature data from an RM2:
+```
+devices[0].check_temperature()
+```
+
+Obtain sensor data from an A1:
+```
+data = devices[0].check_sensors()
+```
+
+Set power state on a SmartPlug SP2/SP3:
+```
+devices[0].set_power(True)
+```
+
+Check power state on a SmartPlug:
+```
+state = devices[0].check_power()
+```
+
+Check energy consumption on a SmartPlug:
+```
+state = devices[0].get_energy()
+```
+
+Set power state for S1 on a SmartPowerStrip MP1:
+```
+devices[0].set_power(1, True)
+```
+
+Check power state on a SmartPowerStrip:
+```
+state = devices[0].check_power()
+```
diff --git a/third_party/python/broadlink/broadlink/__init__.py b/third_party/python/broadlink/broadlink/__init__.py
new file mode 100644
index 0000000000..0c70c4beae
--- /dev/null
+++ b/third_party/python/broadlink/broadlink/__init__.py
@@ -0,0 +1,1118 @@
+#!/usr/bin/python
+
+import codecs
+import json
+import random
+import socket
+import struct
+import threading
+import time
+from datetime import datetime
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+
+
+def gendevice(devtype, host, mac, name=None, cloud=None):
+    devices = {
+        sp1: [0],
+        sp2: [0x2711,  # SP2
+              0x2719, 0x7919, 0x271a, 0x791a,  # Honeywell SP2
+              0x2720,  # SPMini
+              0x753e,  # SP3
+              0x7D00,  # OEM branded SP3
+              0x947a, 0x9479,  # SP3S
+              0x2728,  # SPMini2
+              0x2733, 0x273e,  # OEM branded SPMini
+              0x7530, 0x7546, 0x7918,  # OEM branded SPMini2
+              0x7D0D,  # TMall OEM SPMini3
+              0x2736  # SPMiniPlus
+              ],
+        rm: [0x2712,  # RM2
+             0x2737,  # RM Mini
+             0x273d,  # RM Pro Phicomm
+             0x2783,  # RM2 Home Plus
+             0x277c,  # RM2 Home Plus GDT
+             0x272a,  # RM2 Pro Plus
+             0x2787,  # RM2 Pro Plus2
+             0x279d,  # RM2 Pro Plus3
+             0x27a9,  # RM2 Pro Plus_300
+             0x278b,  # RM2 Pro Plus BL
+             0x2797,  # RM2 Pro Plus HYC
+             0x27a1,  # RM2 Pro Plus R1
+             0x27a6,  # RM2 Pro PP
+             0x278f,  # RM Mini Shate
+             0x27c2,  # RM Mini 3
+             0x27d1,  # new RM Mini3
+             0x27de  # RM Mini 3 (C)
+             ],
+        rm4: [0x51da,  # RM4 Mini
+              0x5f36,  # RM Mini 3
+              0x6026,  # RM4 Pro
+              0x610e,  # RM4 Mini
+              0x610f,  # RM4c
+              0x62bc,  # RM4 Mini
+              0x62be  # RM4c
+              ],
+        a1: [0x2714],  # A1
+        mp1: [0x4EB5,  # MP1
+              0x4EF7  # Honyar oem mp1
+              ],
+        hysen: [0x4EAD],  # Hysen controller
+        S1C: [0x2722],  # S1 (SmartOne Alarm Kit)
+        dooya: [0x4E4D],  # Dooya DT360E (DOOYA_CURTAIN_V2)
+        bg1: [0x51E3], # BG Electrical Smart Power Socket
+        lb1 : [0x60c8]   # RGB Smart Bulb
+    }
+
+    # Look for the class associated to devtype in devices
+    [device_class] = [dev for dev in devices if devtype in devices[dev]] or [None]
+    if device_class is None:
+        return device(host, mac, devtype, name=name, cloud=cloud)
+    return device_class(host, mac, devtype, name=name, cloud=cloud)
+
+
+def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255', max_devices=100):
+    if local_ip_address is None:
+        local_ip_address = socket.gethostbyname(socket.gethostname())
+    if local_ip_address.startswith('127.'):
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        s.connect(('8.8.8.8', 53))  # connecting to a UDP address doesn't send packets
+        local_ip_address = s.getsockname()[0]
+    address = local_ip_address.split('.')
+    cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
+    cs.bind((local_ip_address, 0))
+    port = cs.getsockname()[1]
+    starttime = time.time()
+
+    devices = []
+
+    timezone = int(time.timezone / -3600)
+    packet = bytearray(0x30)
+
+    year = datetime.now().year
+
+    if timezone < 0:
+        packet[0x08] = 0xff + timezone - 1
+        packet[0x09] = 0xff
+        packet[0x0a] = 0xff
+        packet[0x0b] = 0xff
+    else:
+        packet[0x08] = timezone
+        packet[0x09] = 0
+        packet[0x0a] = 0
+        packet[0x0b] = 0
+    packet[0x0c] = year & 0xff
+    packet[0x0d] = year >> 8
+    packet[0x0e] = datetime.now().minute
+    packet[0x0f] = datetime.now().hour
+    subyear = str(year)[2:]
+    packet[0x10] = int(subyear)
+    packet[0x11] = datetime.now().isoweekday()
+    packet[0x12] = datetime.now().day
+    packet[0x13] = datetime.now().month
+    packet[0x18] = int(address[0])
+    packet[0x19] = int(address[1])
+    packet[0x1a] = int(address[2])
+    packet[0x1b] = int(address[3])
+    packet[0x1c] = port & 0xff
+    packet[0x1d] = port >> 8
+    packet[0x26] = 6
+    
+    checksum = 0xbeaf
+    for b in packet:
+        checksum = (checksum + b) & 0xffff
+
+    packet[0x20] = checksum & 0xff
+    packet[0x21] = checksum >> 8
+
+    cs.sendto(packet, (discover_ip_address, 80))
+    if timeout is None:
+        response = cs.recvfrom(1024)
+        responsepacket = bytearray(response[0])
+        host = response[1]
+        devtype = responsepacket[0x34] | responsepacket[0x35] << 8
+        mac = responsepacket[0x3a:0x40]
+        name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8')
+        cloud = bool(responsepacket[-1])
+        device = gendevice(devtype, host, mac, name=name, cloud=cloud)
+        return device
+
+    while ((time.time() - starttime) < timeout) and (len(devices) < max_devices):
+        cs.settimeout(timeout - (time.time() - starttime))
+        try:
+            response = cs.recvfrom(1024)
+        except socket.timeout:
+            return devices
+        responsepacket = bytearray(response[0])
+        host = response[1]
+        devtype = responsepacket[0x34] | responsepacket[0x35] << 8
+        mac = responsepacket[0x3a:0x40]
+        name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8')
+        cloud = bool(responsepacket[-1])
+        device = gendevice(devtype, host, mac, name=name, cloud=cloud)
+        devices.append(device)
+    return devices
+
+
+class device:
+    def __init__(self, host, mac, devtype, timeout=10, name=None, cloud=None):
+        self.host = host
+        self.mac = mac.encode() if isinstance(mac, str) else mac
+        self.devtype = devtype if devtype is not None else 0x272a
+        self.name = name
+        self.cloud = cloud
+        self.timeout = timeout
+        self.count = random.randrange(0xffff)
+        self.iv = bytearray(
+            [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58])
+        self.id = bytearray([0, 0, 0, 0])
+        self.cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
+        self.cs.bind(('', 0))
+        self.type = "Unknown"
+        self.lock = threading.Lock()
+
+        self.aes = None
+        key = bytearray(
+            [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02])
+        self.update_aes(key)
+
+    def update_aes(self, key):
+        self.aes = Cipher(algorithms.AES(key), modes.CBC(self.iv),
+                          backend=default_backend())
+
+    def encrypt(self, payload):
+        encryptor = self.aes.encryptor()
+        return encryptor.update(payload) + encryptor.finalize()
+
+    def decrypt(self, payload):
+        decryptor = self.aes.decryptor()
+        return decryptor.update(payload) + decryptor.finalize()
+
+    def auth(self):
+        payload = bytearray(0x50)
+        payload[0x04] = 0x31
+        payload[0x05] = 0x31
+        payload[0x06] = 0x31
+        payload[0x07] = 0x31
+        payload[0x08] = 0x31
+        payload[0x09] = 0x31
+        payload[0x0a] = 0x31
+        payload[0x0b] = 0x31
+        payload[0x0c] = 0x31
+        payload[0x0d] = 0x31
+        payload[0x0e] = 0x31
+        payload[0x0f] = 0x31
+        payload[0x10] = 0x31
+        payload[0x11] = 0x31
+        payload[0x12] = 0x31
+        payload[0x1e] = 0x01
+        payload[0x2d] = 0x01
+        payload[0x30] = ord('T')
+        payload[0x31] = ord('e')
+        payload[0x32] = ord('s')
+        payload[0x33] = ord('t')
+        payload[0x34] = ord(' ')
+        payload[0x35] = ord(' ')
+        payload[0x36] = ord('1')
+
+        response = self.send_packet(0x65, payload)
+        
+        if any(response[0x22:0x24]):
+            return False
+        
+        payload = self.decrypt(response[0x38:])
+
+        key = payload[0x04:0x14]
+        if len(key) % 16 != 0:
+            return False
+
+        self.id = payload[0x00:0x04]
+        self.update_aes(key)
+
+        return True
+
+    def get_type(self):
+        return self.type
+
+    def send_packet(self, command, payload):
+        self.count = (self.count + 1) & 0xffff
+        packet = bytearray(0x38)
+        packet[0x00] = 0x5a
+        packet[0x01] = 0xa5
+        packet[0x02] = 0xaa
+        packet[0x03] = 0x55
+        packet[0x04] = 0x5a
+        packet[0x05] = 0xa5
+        packet[0x06] = 0xaa
+        packet[0x07] = 0x55
+        packet[0x24] = self.devtype & 0xff
+        packet[0x25] = self.devtype >> 8
+        packet[0x26] = command
+        packet[0x28] = self.count & 0xff
+        packet[0x29] = self.count >> 8
+        packet[0x2a] = self.mac[0]
+        packet[0x2b] = self.mac[1]
+        packet[0x2c] = self.mac[2]
+        packet[0x2d] = self.mac[3]
+        packet[0x2e] = self.mac[4]
+        packet[0x2f] = self.mac[5]
+        packet[0x30] = self.id[0]
+        packet[0x31] = self.id[1]
+        packet[0x32] = self.id[2]
+        packet[0x33] = self.id[3]
+
+        # pad the payload for AES encryption
+        if payload:
+            payload += bytearray((16 - len(payload)) % 16)
+
+        checksum = 0xbeaf
+        for b in payload:
+            checksum = (checksum + b) & 0xffff
+
+        packet[0x34] = checksum & 0xff
+        packet[0x35] = checksum >> 8
+
+        payload = self.encrypt(payload)
+        for i in range(len(payload)):
+            packet.append(payload[i])
+
+        checksum = 0xbeaf
+        for b in packet:
+            checksum = (checksum + b) & 0xffff
+
+        packet[0x20] = checksum & 0xff
+        packet[0x21] = checksum >> 8
+
+        start_time = time.time()
+        with self.lock:
+            while True:
+                try:
+                    self.cs.sendto(packet, self.host)
+                    self.cs.settimeout(1)
+                    response = self.cs.recvfrom(2048)
+                    break
+                except socket.timeout:
+                    if (time.time() - start_time) > self.timeout:
+                        raise
+        return bytearray(response[0])
+
+
+class mp1(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "MP1"
+
+    def set_power_mask(self, sid_mask, state):
+        """Sets the power state of the smart power strip."""
+
+        packet = bytearray(16)
+        packet[0x00] = 0x0d
+        packet[0x02] = 0xa5
+        packet[0x03] = 0xa5
+        packet[0x04] = 0x5a
+        packet[0x05] = 0x5a
+        packet[0x06] = 0xb2 + ((sid_mask << 1) if state else sid_mask)
+        packet[0x07] = 0xc0
+        packet[0x08] = 0x02
+        packet[0x0a] = 0x03
+        packet[0x0d] = sid_mask
+        packet[0x0e] = sid_mask if state else 0
+
+        self.send_packet(0x6a, packet)
+
+    def set_power(self, sid, state):
+        """Sets the power state of the smart power strip."""
+        sid_mask = 0x01 << (sid - 1)
+        return self.set_power_mask(sid_mask, state)
+
+    def check_power_raw(self):
+        """Returns the power state of the smart power strip in raw format."""
+        packet = bytearray(16)
+        packet[0x00] = 0x0a
+        packet[0x02] = 0xa5
+        packet[0x03] = 0xa5
+        packet[0x04] = 0x5a
+        packet[0x05] = 0x5a
+        packet[0x06] = 0xae
+        packet[0x07] = 0xc0
+        packet[0x08] = 0x01
+
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+        if isinstance(payload[0x4], int):
+            state = payload[0x0e]
+        else:
+            state = ord(payload[0x0e])
+        return state
+
+    def check_power(self):
+        """Returns the power state of the smart power strip."""
+        state = self.check_power_raw()
+        if state is None:
+            return {'s1': None, 's2': None, 's3': None, 's4': None}
+        data = {}
+        data['s1'] = bool(state & 0x01)
+        data['s2'] = bool(state & 0x02)
+        data['s3'] = bool(state & 0x04)
+        data['s4'] = bool(state & 0x08)
+        return data
+
+
+class bg1(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "BG1"
+
+    def get_state(self):
+        """Get state of device.
+        
+        Returns:
+            dict: Dictionary of current state
+            eg. `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`"""
+        packet = self._encode(1, b'{}')
+        response = self.send_packet(0x6a, packet)
+        return self._decode(response)
+
+    def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None):
+        data = {}
+        if pwr is not None:
+            data['pwr'] = int(bool(pwr))
+        if pwr1 is not None:
+            data['pwr1'] = int(bool(pwr1))
+        if pwr2 is not None:
+            data['pwr2'] = int(bool(pwr2))
+        if maxworktime is not None:
+            data['maxworktime'] = maxworktime
+        if maxworktime1 is not None:
+            data['maxworktime1'] = maxworktime1
+        if maxworktime2 is not None:
+            data['maxworktime2'] = maxworktime2
+        if idcbrightness is not None:
+            data['idcbrightness'] = idcbrightness
+        js = json.dumps(data).encode('utf8')
+        packet = self._encode(2, js)
+        response = self.send_packet(0x6a, packet)
+        return self._decode(response)
+
+    def _encode(self, flag, js):
+        # packet format is:
+        # 0x00-0x01 length
+        # 0x02-0x05 header
+        # 0x06-0x07 00
+        # 0x08 flag (1 for read or 2 write?)
+        # 0x09 unknown (0xb)
+        # 0x0a-0x0d length of json
+        # 0x0e- json data
+        packet = bytearray(14)
+        length = 4 + 2 + 2 + 4 + len(js)
+        struct.pack_into('<HHHHBBI', packet, 0, length, 0xa5a5, 0x5a5a, 0x0000, flag, 0x0b, len(js))
+        for i in range(len(js)):
+            packet.append(js[i])
+
+        checksum = 0xc0ad
+        for b in packet[0x08:]:
+            checksum = (checksum + b) & 0xffff
+
+        packet[0x06] = checksum & 0xff
+        packet[0x07] = checksum >> 8
+
+        return packet
+
+    def _decode(self, response):
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+    
+        payload = self.decrypt(bytes(response[0x38:]))
+        js_len = struct.unpack_from('<I', payload, 0x0a)[0]
+        state = json.loads(payload[0x0e:0x0e+js_len])
+        return state
+
+class sp1(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "SP1"
+
+    def set_power(self, state):
+        packet = bytearray(4)
+        packet[0] = state
+        self.send_packet(0x66, packet)
+
+
+class sp2(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "SP2"
+
+    def set_power(self, state):
+        """Sets the power state of the smart plug."""
+        packet = bytearray(16)
+        packet[0] = 2
+        if self.check_nightlight():
+            packet[4] = 3 if state else 2
+        else:
+            packet[4] = 1 if state else 0
+        self.send_packet(0x6a, packet)
+
+    def set_nightlight(self, state):
+        """Sets the night light state of the smart plug"""
+        packet = bytearray(16)
+        packet[0] = 2
+        if self.check_power():
+            packet[4] = 3 if state else 1
+        else:
+            packet[4] = 2 if state else 0
+        self.send_packet(0x6a, packet)
+
+    def check_power(self):
+        """Returns the power state of the smart plug."""
+        packet = bytearray(16)
+        packet[0] = 1
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+        if isinstance(payload[0x4], int):
+            return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD)
+        return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD)
+
+    def check_nightlight(self):
+        """Returns the power state of the smart plug."""
+        packet = bytearray(16)
+        packet[0] = 1
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+        if isinstance(payload[0x4], int):
+            return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF)
+        return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF)
+
+    def get_energy(self):
+        packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45])
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+        if isinstance(payload[0x7], int):
+            energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0
+        else:
+            energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int(
+                hex(ord(payload[0x05]))[2:]) / 100.0
+        return energy
+
+
+class a1(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "A1"
+
+    def check_sensors(self):
+        packet = bytearray(16)
+        packet[0] = 1
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        data = {}
+        payload = self.decrypt(bytes(response[0x38:]))
+        if isinstance(payload[0x4], int):
+            data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0
+            data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0
+            light = payload[0x8]
+            air_quality = payload[0x0a]
+            noise = payload[0xc]
+        else:
+            data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0
+            data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0
+            light = ord(payload[0x8])
+            air_quality = ord(payload[0x0a])
+            noise = ord(payload[0xc])
+        if light == 0:
+            data['light'] = 'dark'
+        elif light == 1:
+            data['light'] = 'dim'
+        elif light == 2:
+            data['light'] = 'normal'
+        elif light == 3:
+            data['light'] = 'bright'
+        else:
+            data['light'] = 'unknown'
+        if air_quality == 0:
+            data['air_quality'] = 'excellent'
+        elif air_quality == 1:
+            data['air_quality'] = 'good'
+        elif air_quality == 2:
+            data['air_quality'] = 'normal'
+        elif air_quality == 3:
+            data['air_quality'] = 'bad'
+        else:
+            data['air_quality'] = 'unknown'
+        if noise == 0:
+            data['noise'] = 'quiet'
+        elif noise == 1:
+            data['noise'] = 'normal'
+        elif noise == 2:
+            data['noise'] = 'noisy'
+        else:
+            data['noise'] = 'unknown'
+        return data
+
+    def check_sensors_raw(self):
+        packet = bytearray(16)
+        packet[0] = 1
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        data = {}
+        payload = self.decrypt(bytes(response[0x38:]))
+        if isinstance(payload[0x4], int):
+            data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0
+            data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0
+            data['light'] = payload[0x8]
+            data['air_quality'] = payload[0x0a]
+            data['noise'] = payload[0xc]
+        else:
+            data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0
+            data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0
+            data['light'] = ord(payload[0x8])
+            data['air_quality'] = ord(payload[0x0a])
+            data['noise'] = ord(payload[0xc])
+        return data
+
+
+class rm(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "RM2"
+        self._request_header = bytes()
+        self._code_sending_header = bytes()
+
+    def check_data(self):
+        packet = bytearray(self._request_header)
+        packet.append(0x04)
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+        return payload[len(self._request_header) + 4:]
+
+    def send_data(self, data):
+        packet = bytearray(self._code_sending_header)
+        packet += bytes([0x02, 0x00, 0x00, 0x00])
+        packet += data
+        self.send_packet(0x6a, packet)
+
+    def enter_learning(self):
+        packet = bytearray(self._request_header)
+        packet.append(0x03)
+        self.send_packet(0x6a, packet)
+
+    def sweep_frequency(self):
+        packet = bytearray(self._request_header)
+        packet.append(0x19)
+        self.send_packet(0x6a, packet)
+
+    def cancel_sweep_frequency(self):
+        packet = bytearray(self._request_header)
+        packet.append(0x1e)
+        self.send_packet(0x6a, packet)
+
+    def check_frequency(self):
+        packet = bytearray(self._request_header)
+        packet.append(0x1a)
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return False
+        payload = self.decrypt(bytes(response[0x38:]))
+        if payload[len(self._request_header) + 4] == 1:
+            return True
+        return False
+
+    def find_rf_packet(self):
+        packet = bytearray(self._request_header)
+        packet.append(0x1b)
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return False
+        payload = self.decrypt(bytes(response[0x38:]))
+        if payload[len(self._request_header) + 4] == 1:
+            return True
+        return False
+
+    def _read_sensor(self, type, offset, divider):
+        packet = bytearray(self._request_header)
+        packet.append(type)
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return False
+        payload = self.decrypt(bytes(response[0x38:]))
+        value_pos = len(self._request_header) + offset
+        if isinstance(payload[value_pos], int):
+            value = (payload[value_pos] + payload[value_pos+1] / divider)
+        else:
+            value = (ord(payload[value_pos]) + ord(payload[value_pos+1]) / divider)
+        return value
+
+    def check_temperature(self):
+        return self._read_sensor( 0x01, 4, 10.0 )
+
+class rm4(rm):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "RM4"
+        self._request_header = b'\x04\x00'
+        self._code_sending_header = b'\xd0\x00'
+
+    def check_temperature(self):
+        return self._read_sensor( 0x24, 4, 100.0 )
+
+    def check_humidity(self):
+        return self._read_sensor( 0x24, 6, 100.0 )
+
+    def check_sensors(self):
+        return {
+            'temperature': self.check_temperature(),
+            'humidity': self.check_humidity()
+        }
+
+# For legacy compatibility - don't use this
+class rm2(rm):
+    def __init__(self):
+        device.__init__(self, None, None, None)
+
+    def discover(self):
+        dev = discover()
+        self.host = dev.host
+        self.mac = dev.mac
+
+
+class hysen(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "Hysen heating controller"
+
+    # Send a request
+    # input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00])
+    # Returns decrypted payload
+    # New behaviour: raises a ValueError if the device response indicates an error or CRC check fails
+    # The function prepends length (2 bytes) and appends CRC
+
+    def calculate_crc16(self, input_data):
+        from ctypes import c_ushort
+        crc16_tab = []
+        crc16_constant = 0xA001
+
+        for i in range(0, 256):
+            crc = c_ushort(i).value
+            for j in range(0, 8):
+                if (crc & 0x0001):
+                    crc = c_ushort(crc >> 1).value ^ crc16_constant
+                else:
+                    crc = c_ushort(crc >> 1).value
+            crc16_tab.append(hex(crc))
+
+        try:
+            is_string = isinstance(input_data, str)
+            is_bytes = isinstance(input_data, bytes)
+
+            if not is_string and not is_bytes:
+                raise Exception("Please provide a string or a byte sequence "
+                                "as argument for calculation.")
+
+            crcValue = 0xffff
+
+            for c in input_data:
+                d = ord(c) if is_string else c
+                tmp = crcValue ^ d
+                rotated = c_ushort(crcValue >> 8).value
+                crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0)
+
+            return crcValue
+        except Exception as e:
+            print("EXCEPTION(calculate): {}".format(e))
+
+    def send_request(self, input_payload):
+
+        crc = self.calculate_crc16(bytes(input_payload))
+
+        # first byte is length, +2 for CRC16
+        request_payload = bytearray([len(input_payload) + 2, 0x00])
+        request_payload.extend(input_payload)
+
+        # append CRC
+        request_payload.append(crc & 0xFF)
+        request_payload.append((crc >> 8) & 0xFF)
+
+        # send to device
+        response = self.send_packet(0x6a, request_payload)
+
+        # check for error
+        err = response[0x22] | (response[0x23] << 8)
+        if err:
+            raise ValueError('broadlink_response_error', err)
+
+        response_payload = bytearray(self.decrypt(bytes(response[0x38:])))
+
+        # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc)
+        response_payload_len = response_payload[0]
+        if response_payload_len + 2 > len(response_payload):
+            raise ValueError('hysen_response_error', 'first byte of response is not length')
+        crc = self.calculate_crc16(bytes(response_payload[2:response_payload_len]))
+        if (response_payload[response_payload_len] == crc & 0xFF) and (
+                response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF):
+            return response_payload[2:response_payload_len]
+        raise ValueError('hysen_response_error', 'CRC check on response failed')
+
+    # Get current room temperature in degrees celsius
+    def get_temp(self):
+        payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]))
+        return payload[0x05] / 2.0
+
+    # Get current external temperature in degrees celsius
+    def get_external_temp(self):
+        payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]))
+        return payload[18] / 2.0
+
+    # Get full status (including timer schedule)
+    def get_full_status(self):
+        payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16]))
+        data = {}
+        data['remote_lock'] = payload[3] & 1
+        data['power'] = payload[4] & 1
+        data['active'] = (payload[4] >> 4) & 1
+        data['temp_manual'] = (payload[4] >> 6) & 1
+        data['room_temp'] = (payload[5] & 255) / 2.0
+        data['thermostat_temp'] = (payload[6] & 255) / 2.0
+        data['auto_mode'] = payload[7] & 15
+        data['loop_mode'] = (payload[7] >> 4) & 15
+        data['sensor'] = payload[8]
+        data['osv'] = payload[9]
+        data['dif'] = payload[10]
+        data['svh'] = payload[11]
+        data['svl'] = payload[12]
+        data['room_temp_adj'] = ((payload[13] << 8) + payload[14]) / 2.0
+        if data['room_temp_adj'] > 32767:
+            data['room_temp_adj'] = 32767 - data['room_temp_adj']
+        data['fre'] = payload[15]
+        data['poweron'] = payload[16]
+        data['unknown'] = payload[17]
+        data['external_temp'] = (payload[18] & 255) / 2.0
+        data['hour'] = payload[19]
+        data['min'] = payload[20]
+        data['sec'] = payload[21]
+        data['dayofweek'] = payload[22]
+
+        weekday = []
+        for i in range(0, 6):
+            weekday.append(
+                {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0})
+
+        data['weekday'] = weekday
+        weekend = []
+        for i in range(6, 8):
+            weekend.append(
+                {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0})
+
+        data['weekend'] = weekend
+        return data
+
+    # Change controller mode
+    # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode.
+    # Manual mode will activate last used temperature.
+    # In typical usage call set_temp to activate manual control and set temp.
+    # loop_mode refers to index in [ "12345,67", "123456,7", "1234567" ]
+    # E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday follow the "weekend" schedule
+    # loop_mode = 2 ("1234567") means every day (including Saturday and Sunday) follows the "weekday" schedule
+    # The sensor command is currently experimental
+    def set_mode(self, auto_mode, loop_mode, sensor=0):
+        mode_byte = ((loop_mode + 1) << 4) + auto_mode
+        self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor]))
+
+    # Advanced settings
+    # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor,
+    # 2 for internal control temperature, external limit temperature. Factory default: 0.
+    # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C
+    # Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C
+    # Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C
+    # Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C
+    # Actual temperature calibration (AdJ) adj = -0.5. Prescision 0.1C
+    # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down,
+    #  1 for anti-freezing function open. Factory default: 0
+    # Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0
+    def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron):
+        input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl,
+                                   (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 0xff), fre, poweron])
+        self.send_request(input_payload)
+
+    # For backwards compatibility only.  Prefer calling set_mode directly.
+    # Note this function invokes loop_mode=0 and sensor=0.
+    def switch_to_auto(self):
+        self.set_mode(auto_mode=1, loop_mode=0)
+
+    def switch_to_manual(self):
+        self.set_mode(auto_mode=0, loop_mode=0)
+
+    # Set temperature for manual mode (also activates manual mode if currently in automatic)
+    def set_temp(self, temp):
+        self.send_request(bytearray([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)]))
+
+    # Set device on(1) or off(0), does not deactivate Wifi connectivity.
+    # Remote lock disables control by buttons on thermostat.
+    def set_power(self, power=1, remote_lock=0):
+        self.send_request(bytearray([0x01, 0x06, 0x00, 0x00, remote_lock, power]))
+
+    # set time on device
+    # n.b. day=1 is Monday, ..., day=7 is Sunday
+    def set_time(self, hour, minute, second, day):
+        self.send_request(bytearray([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day]))
+
+    # Set timer schedule
+    # Format is the same as you get from get_full_status.
+    # weekday is a list (ordered) of 6 dicts like:
+    # {'start_hour':17, 'start_minute':30, 'temp': 22 }
+    # Each one specifies the thermostat temp that will become effective at start_hour:start_minute
+    # weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon)
+    def set_schedule(self, weekday, weekend):
+        # Begin with some magic values ...
+        input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18])
+
+        # Now simply append times/temps
+        # weekday times
+        for i in range(0, 6):
+            input_payload.append(weekday[i]['start_hour'])
+            input_payload.append(weekday[i]['start_minute'])
+
+        # weekend times
+        for i in range(0, 2):
+            input_payload.append(weekend[i]['start_hour'])
+            input_payload.append(weekend[i]['start_minute'])
+
+        # weekday temperatures
+        for i in range(0, 6):
+            input_payload.append(int(weekday[i]['temp'] * 2))
+
+        # weekend temperatures
+        for i in range(0, 2):
+            input_payload.append(int(weekend[i]['temp'] * 2))
+
+        self.send_request(input_payload)
+
+
+S1C_SENSORS_TYPES = {
+    0x31: 'Door Sensor',  # 49 as hex
+    0x91: 'Key Fob',  # 145 as hex, as serial on fob corpse
+    0x21: 'Motion Sensor'  # 33 as hex
+}
+
+
+class S1C(device):
+    """
+    Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C
+    """
+
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = 'S1C'
+
+    def get_sensors_status(self):
+        packet = bytearray(16)
+        packet[0] = 0x06  # 0x06 - get sensors info, 0x07 - probably add sensors
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+
+        payload = self.decrypt(bytes(response[0x38:]))
+        if not payload:
+            return None
+        count = payload[0x4]
+        sensors = payload[0x6:]
+        sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)]
+
+        sens_res = []
+        for sens in sensors_a:
+            status = ord(chr(sens[0]))
+            _name = str(bytes(sens[4:26]).decode())
+            _order = ord(chr(sens[1]))
+            _type = ord(chr(sens[3]))
+            _serial = bytes(codecs.encode(sens[26:30], "hex")).decode()
+
+            type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown')
+
+            r = {
+                'status': status,
+                'name': _name.strip('\x00'),
+                'type': type_str,
+                'order': _order,
+                'serial': _serial,
+            }
+            if r['serial'] != '00000000':
+                sens_res.append(r)
+        result = {
+            'count': count,
+            'sensors': sens_res
+        }
+        return result
+
+
+class dooya(device):
+    def __init__(self, *args, **kwargs):
+        device.__init__(self, *args, **kwargs)
+        self.type = "Dooya DT360E"
+
+    def _send(self, magic1, magic2):
+        packet = bytearray(16)
+        packet[0] = 0x09
+        packet[2] = 0xbb
+        packet[3] = magic1
+        packet[4] = magic2
+        packet[9] = 0xfa
+        packet[10] = 0x44
+        response = self.send_packet(0x6a, packet)
+        err = response[0x22] | (response[0x23] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+        return ord(payload[4])
+
+    def open(self):
+        return self._send(0x01, 0x00)
+
+    def close(self):
+        return self._send(0x02, 0x00)
+
+    def stop(self):
+        return self._send(0x03, 0x00)
+
+    def get_percentage(self):
+        return self._send(0x06, 0x5d)
+
+    def set_percentage_and_wait(self, new_percentage):
+        current = self.get_percentage()
+        if current > new_percentage:
+            self.close()
+            while current is not None and current > new_percentage:
+                time.sleep(0.2)
+                current = self.get_percentage()
+
+        elif current < new_percentage:
+            self.open()
+            while current is not None and current < new_percentage:
+                time.sleep(0.2)
+                current = self.get_percentage()
+        self.stop()
+
+class lb1(device):
+    state_dict = []
+    effect_map_dict = { 'lovely color' : 0,
+                        'flashlight' : 1,
+                        'lightning' : 2,
+                        'color fading' : 3,
+                        'color breathing' : 4,
+                        'multicolor breathing' : 5,
+                        'color jumping' : 6,
+                        'multicolor jumping' : 7 }
+
+    def __init__(self, host, mac, devtype):
+        device.__init__(self, host, mac, devtype)
+        self.type = "SmartBulb"
+
+    def send_command(self,command, type = 'set'):
+        packet = bytearray(16+(int(len(command)/16) + 1)*16)
+        packet[0x02] = 0xa5
+        packet[0x03] = 0xa5
+        packet[0x04] = 0x5a
+        packet[0x05] = 0x5a
+        packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set
+        packet[0x09] = 0x0b
+        packet[0x0a] = len(command)
+        packet[0x0e:] = map(ord, command)
+
+        checksum = 0xbeaf
+        for b in packet:
+            checksum = (checksum + b) & 0xffff
+
+        packet[0x00] = (0x0c + len(command)) & 0xff
+        packet[0x06] = checksum & 0xff  # Checksum 1 position
+        packet[0x07] = checksum >> 8  # Checksum 2 position
+
+        response = self.send_packet(0x6a, packet)
+
+        err = response[0x36] | (response[0x37] << 8)
+        if err != 0:
+            return None
+        payload = self.decrypt(bytes(response[0x38:]))
+
+        responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8)
+        if responseLength > 0:
+            self.state_dict = json.loads(payload[0x0e:0x0e+responseLength])
+
+    def set_json(self, jsonstr):
+        reconvert = json.loads(jsonstr)
+        if 'bulb_sceneidx' in reconvert.keys():
+            reconvert['bulb_sceneidx'] = self.effect_map_dict.get(reconvert['bulb_sceneidx'], 255)
+
+        self.send_command(json.dumps(reconvert))
+        return json.dumps(self.state_dict)
+
+    def set_state(self, state):
+        cmd = '{"pwr":%d}' % (1 if state == "ON" or state == 1 else 0)
+        self.send_command(cmd)
+
+    def get_state(self):
+        cmd = "{}"
+        self.send_command(cmd)
+        return self.state_dict
+
+# Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode.
+# Only tested with Broadlink RM3 Mini (Blackbean)
+def setup(ssid, password, security_mode):
+    # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2)
+    payload = bytearray(0x88)
+    payload[0x26] = 0x14  # This seems to always be set to 14
+    # Add the SSID to the payload
+    ssid_start = 68
+    ssid_length = 0
+    for letter in ssid:
+        payload[(ssid_start + ssid_length)] = ord(letter)
+        ssid_length += 1
+    # Add the WiFi password to the payload
+    pass_start = 100
+    pass_length = 0
+    for letter in password:
+        payload[(pass_start + pass_length)] = ord(letter)
+        pass_length += 1
+
+    payload[0x84] = ssid_length  # Character length of SSID
+    payload[0x85] = pass_length  # Character length of password
+    payload[0x86] = security_mode  # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2)
+
+    checksum = 0xbeaf
+    for b in payload:
+        checksum = (checksum + b) & 0xffff
+
+    payload[0x20] = checksum & 0xff  # Checksum 1 position
+    payload[0x21] = checksum >> 8  # Checksum 2 position
+
+    sock = socket.socket(socket.AF_INET,  # Internet
+                         socket.SOCK_DGRAM)  # UDP
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    # sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
+    sock.sendto(payload, ('192.168.10.1', 80))
diff --git a/third_party/python/broadlink/cli/README.md b/third_party/python/broadlink/cli/README.md
new file mode 100644
index 0000000000..7e229e3eb5
--- /dev/null
+++ b/third_party/python/broadlink/cli/README.md
@@ -0,0 +1,85 @@
+Command line interface for python-broadlink
+===========================================
+
+This is a command line interface for broadlink python library
+
+Tested with BroadLink RMPRO / RM2
+
+
+Requirements
+------------
+You should have the broadlink python installed, this can be made in many linux distributions using :
+```
+sudo pip install broadlink
+```
+
+Installation
+-----------
+Just copy this files
+
+
+Programs
+--------
+
+
+* broadlink_discovery 
+used to run the discovery in the network
+this program withh show the command line parameters to be used with
+broadlink_cli to select broadlink device
+
+* broadlink_cli 
+used to send commands and query the broadlink device
+
+
+device specification formats
+----------------------------
+
+Using separate parameters for each information:
+```
+broadlink_cli --type 0x2712 --host 1.1.1.1 --mac aaaaaaaaaa --temp
+```
+
+Using all parameters as a single argument:
+```
+broadlink_cli --device "0x2712 1.1.1.1 aaaaaaaaaa" --temp
+```
+
+Using file with parameters:
+```
+broadlink_cli --device @BEDROOM.device --temp
+```
+This is prefered as the configuration is stored in file and you can change
+just a file to point to a different hardware 
+
+Sample usage
+------------
+
+Learn commands :
+```
+# Learn and save to file
+broadlink_cli --device @BEDROOM.device --learnfile LG-TV.power
+# LEard and show at console
+broadlink_cli --device @BEDROOM.device --learn 
+```
+
+
+Send command :
+```
+broadlink_cli --device @BEDROOM.device --send @LG-TV.power
+broadlink_cli --device @BEDROOM.device --send ....datafromlearncommand...
+```
+
+Get Temperature :
+```
+broadlink_cli --device @BEDROOM.device --temperature
+```
+
+Get Energy Consumption (For a SmartPlug) :
+```
+broadlink_cli --device @BEDROOM.device --energy
+```
+
+Once joined to the Broadlink provisioning Wi-Fi, configure it with your Wi-Fi details:
+```
+broadlink_cli --joinwifi MySSID MyWifiPassword
+```
diff --git a/third_party/python/broadlink/cli/broadlink_cli b/third_party/python/broadlink/cli/broadlink_cli
new file mode 100755
index 0000000000..5045c5c108
--- /dev/null
+++ b/third_party/python/broadlink/cli/broadlink_cli
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+
+import argparse
+import base64
+import codecs
+import time
+
+import broadlink
+
+TICK = 32.84
+IR_TOKEN = 0x26
+
+
+def auto_int(x):
+    return int(x, 0)
+
+
+def to_microseconds(bytes):
+    result = []
+    #  print bytes[0] # 0x26 = 38for IR
+    index = 4
+    while index < len(bytes):
+        chunk = bytes[index]
+        index += 1
+        if chunk == 0:
+            chunk = bytes[index]
+            chunk = 256 * chunk + bytes[index + 1]
+            index += 2
+        result.append(int(round(chunk * TICK)))
+        if chunk == 0x0d05:
+            break
+    return result
+
+
+def durations_to_broadlink(durations):
+    result = bytearray()
+    result.append(IR_TOKEN)
+    result.append(0)
+    result.append(len(durations) % 256)
+    result.append(len(durations) / 256)
+    for dur in durations:
+        num = int(round(dur / TICK))
+        if num > 255:
+            result.append(0)
+            result.append(num / 256)
+        result.append(num % 256)
+    return result
+
+
+def format_durations(data):
+    result = ''
+    for i in range(0, len(data)):
+        if len(result) > 0:
+            result += ' '
+        result += ('+' if i % 2 == 0 else '-') + str(data[i])
+    return result
+
+
+def parse_durations(str):
+    result = []
+    for s in str.split():
+        result.append(abs(int(s)))
+    return result
+
+
+parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+parser.add_argument("--device", help="device definition as 'type host mac'")
+parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device")
+parser.add_argument("--host", help="host address")
+parser.add_argument("--mac", help="mac address (hex reverse), as used by python-broadlink library")
+parser.add_argument("--temperature", action="store_true", help="request temperature from device")
+parser.add_argument("--energy", action="store_true", help="request energy consumption from device")
+parser.add_argument("--check", action="store_true", help="check current power state")
+parser.add_argument("--checknl", action="store_true", help="check current nightlight state")
+parser.add_argument("--turnon", action="store_true", help="turn on device")
+parser.add_argument("--turnoff", action="store_true", help="turn off device")
+parser.add_argument("--turnnlon", action="store_true", help="turn on nightlight on the device")
+parser.add_argument("--turnnloff", action="store_true", help="turn off nightlight on the device")
+parser.add_argument("--switch", action="store_true", help="switch state from on to off and off to on")
+parser.add_argument("--send", action="store_true", help="send command")
+parser.add_argument("--sensors", action="store_true", help="check all sensors")
+parser.add_argument("--learn", action="store_true", help="learn command")
+parser.add_argument("--rfscanlearn", action="store_true", help="rf scan learning")
+parser.add_argument("--learnfile", help="save learned command to a specified file")
+parser.add_argument("--durations", action="store_true",
+                    help="use durations in micro seconds instead of the Broadlink format")
+parser.add_argument("--convert", action="store_true", help="convert input data to durations")
+parser.add_argument("--joinwifi", nargs=2, help="Args are SSID PASSPHRASE to configure Broadlink device with");
+parser.add_argument("data", nargs='*', help="Data to send or convert")
+args = parser.parse_args()
+
+if args.device:
+    values = args.device.split()
+    type = int(values[0], 0)
+    host = values[1]
+    mac = bytearray.fromhex(values[2])
+elif args.mac:
+    type = args.type
+    host = args.host
+    mac = bytearray.fromhex(args.mac)
+
+if args.host or args.device:
+    dev = broadlink.gendevice(type, (host, 80), mac)
+    dev.auth()
+
+if args.joinwifi:
+    broadlink.setup(args.joinwifi[0], args.joinwifi[1], 4)
+
+if args.convert:
+    data = bytearray.fromhex(''.join(args.data))
+    durations = to_microseconds(data)
+    print(format_durations(durations))
+if args.temperature:
+    print(dev.check_temperature())
+if args.energy:
+    print(dev.get_energy())
+if args.sensors:
+    try:
+        data = dev.check_sensors()
+    except:
+        data = {}
+        data['temperature'] = dev.check_temperature()
+    for key in data:
+        print("{} {}".format(key, data[key]))
+if args.send:
+    data = durations_to_broadlink(parse_durations(' '.join(args.data))) \
+        if args.durations else bytearray.fromhex(''.join(args.data))
+    dev.send_data(data)
+if args.learn or args.learnfile:
+    dev.enter_learning()
+    data = None
+    print("Learning...")
+    timeout = 30
+    while (data is None) and (timeout > 0):
+        time.sleep(2)
+        timeout -= 2
+        data = dev.check_data()
+    if data:
+        learned = format_durations(to_microseconds(bytearray(data))) \
+            if args.durations \
+            else ''.join(format(x, '02x') for x in bytearray(data))
+        if args.learn:
+            print(learned)
+            decode_hex = codecs.getdecoder("hex_codec")
+            print("Base64: " + str(base64.b64encode(decode_hex(learned)[0])))
+        if args.learnfile:
+            print("Saving to {}".format(args.learnfile))
+            with open(args.learnfile, "w") as text_file:
+                text_file.write(learned)
+    else:
+        print("No data received...")
+if args.check:
+    if dev.check_power():
+        print('* ON *')
+    else:
+        print('* OFF *')
+if args.checknl:
+    if dev.check_nightlight():
+        print('* ON *')
+    else:
+        print('* OFF *')
+if args.turnon:
+    dev.set_power(True)
+    if dev.check_power():
+        print('== Turned * ON * ==')
+    else:
+        print('!! Still OFF !!')
+if args.turnoff:
+    dev.set_power(False)
+    if dev.check_power():
+        print('!! Still ON !!')
+    else:
+        print('== Turned * OFF * ==')
+if args.turnnlon:
+    dev.set_nightlight(True)
+    if dev.check_nightlight():
+        print('== Turned * ON * ==')
+    else:
+        print('!! Still OFF !!')
+if args.turnnloff:
+    dev.set_nightlight(False)
+    if dev.check_nightlight():
+        print('!! Still ON !!')
+    else:
+        print('== Turned * OFF * ==')
+if args.switch:
+    if dev.check_power():
+        dev.set_power(False)
+        print('* Switch to OFF *')
+    else:
+        dev.set_power(True)
+        print('* Switch to ON *')
+if args.rfscanlearn:
+    dev.sweep_frequency()
+    print("Learning RF Frequency, press and hold the button to learn...")
+
+    timeout = 20
+
+    while (not dev.check_frequency()) and (timeout > 0):
+        time.sleep(1)
+        timeout -= 1
+
+    if timeout <= 0:
+        print("RF Frequency not found")
+        dev.cancel_sweep_frequency()
+        exit(1)
+
+    print("Found RF Frequency - 1 of 2!")
+    print("You can now let go of the button")
+
+    input("Press enter to continue...")
+
+    print("To complete learning, single press the button you want to learn")
+
+    dev.find_rf_packet()
+
+    data = None
+    timeout = 20
+
+    while (data is None) and (timeout > 0):
+        time.sleep(1)
+        timeout -= 1
+        data = dev.check_data()
+
+    if data:
+        print("Found RF Frequency - 2 of 2!")
+        learned = format_durations(to_microseconds(bytearray(data))) \
+            if args.durations \
+            else ''.join(format(x, '02x') for x in bytearray(data))
+        if args.learnfile is None:
+            print(learned)
+            decode_hex = codecs.getdecoder("hex_codec")
+            print("Base64: {}".format(str(base64.b64encode(decode_hex(learned)[0]))))
+        if args.learnfile is not None:
+            print("Saving to {}".format(args.learnfile))
+            with open(args.learnfile, "w") as text_file:
+                text_file.write(learned)
+    else:
+        print("No data received...")
diff --git a/third_party/python/broadlink/cli/broadlink_discovery b/third_party/python/broadlink/cli/broadlink_discovery
new file mode 100755
index 0000000000..1c6b80b148
--- /dev/null
+++ b/third_party/python/broadlink/cli/broadlink_discovery
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+import argparse
+
+import broadlink
+
+parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+parser.add_argument("--timeout", type=int, default=5, help="timeout to wait for receiving discovery responses")
+parser.add_argument("--ip", default=None, help="ip address to use in the discovery")
+parser.add_argument("--dst-ip", default=None, help="destination ip address to use in the discovery")
+args = parser.parse_args()
+
+print("Discovering...")
+devices = broadlink.discover(timeout=args.timeout, local_ip_address=args.ip, discover_ip_address=args.dst_ip)
+for device in devices:
+    if device.auth():
+        print("###########################################")
+        print(device.type)
+        print("# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0],
+                                                                    ''.join(format(x, '02x') for x in device.mac)))
+        print("Device file data (to be used with --device @filename in broadlink_cli) : ")
+        print("{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac)))
+        if hasattr(device, 'check_temperature'):
+            print("temperature = {}".format(device.check_temperature()))
+        print("")
+    else:
+        print("Error authenticating with device : {}".format(device.host))
diff --git a/third_party/python/broadlink/default.nix b/third_party/python/broadlink/default.nix
new file mode 100644
index 0000000000..b1dcf30081
--- /dev/null
+++ b/third_party/python/broadlink/default.nix
@@ -0,0 +1,16 @@
+# Python package for controlling the Broadlink RM Pro Infrared
+# controller.
+#
+# https://github.com/mjg59/python-broadlink
+{ pkgs, lib, ... }:
+
+let
+  inherit (pkgs) fetchFromGitHub;
+  inherit (pkgs.python3Packages) buildPythonPackage cryptography;
+in
+buildPythonPackage (lib.fix (self: {
+  pname = "python-broadlink";
+  version = "0.13.2";
+  src = ./.;
+  propagatedBuildInputs = [ cryptography ];
+}))
diff --git a/third_party/python/broadlink/protocol.md b/third_party/python/broadlink/protocol.md
new file mode 100644
index 0000000000..2e388d7499
--- /dev/null
+++ b/third_party/python/broadlink/protocol.md
@@ -0,0 +1,202 @@
+Broadlink RM2 network protocol
+==============================
+
+Encryption
+----------
+
+Packets include AES-based encryption in CBC mode. The initial key is 0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02. The IV is 0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58.
+
+Checksum
+--------
+
+Construct the packet and set checksum bytes to zero. Add each byte to the starting value of 0xbeaf, wrapping after 0xffff.
+
+New device setup
+----------------
+
+To setup a new Broadlink device while in AP Mode a 136 byte packet needs to be sent to the device as follows:
+
+| Offset  | Contents |
+|---------|----------|
+|0x00-0x19|00|
+|0x20-0x21|Checksum as a little-endian 16 bit integer|
+|0x26|14 (Always 14)|
+|0x44-0x63|SSID Name (zero padding is appended)|
+|0x64-0x83|Password (zero padding is appended)|
+|0x84|Character length of SSID|
+|0x85|Character length of password|
+|0x86|Wireless security mode (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2)|
+|0x87-88|00|
+
+Send this packet as a UDP broadcast to 255.255.255.255 on port 80.
+
+Network discovery
+-----------------
+
+To discover Broadlink devices on the local network, send a 48 byte packet with the following contents:
+
+| Offset  | Contents |
+|---------|----------|
+|0x00-0x07|00|
+|0x08-0x0b|Current offset from GMT as a little-endian 32 bit integer|
+|0x0c-0x0d|Current year as a little-endian 16 bit integer|
+|0x0e|Current number of seconds past the minute|
+|0x0f|Current number of minutes past the hour|
+|0x10|Current number of hours past midnight|
+|0x11|Current day of the week (Monday = 1, Tuesday = 2, etc)|
+|0x12|Current day in month|
+|0x13|Current month|
+|0x14-0x17|00|
+|0x18-0x1b|Local IP address|
+|0x1c-0x1d|Source port as a little-endian 16 bit integer|
+|0x1e-0x1f|00|
+|0x20-0x21|Checksum as a little-endian 16 bit integer|
+|0x22-0x25|00|
+|0x26|06|
+|0x27-0x2f|00|
+
+Send this packet as a UDP broadcast to 255.255.255.255 on port 80.
+
+Response (any unicast response):
+
+| Offset  | Contents |
+|---------|----------|
+|0x34-0x35|Device type as a little-endian 16 bit integer (see device type mapping)|
+|0x3a-0x3f|MAC address of the target device|
+
+Device type mapping:
+
+| Device type in response packet | Device type | Treat as |
+|---------|----------|----------|
+|0|SP1|SP1|
+|0x2711|SP2|SP2|
+|0x2719 or 0x7919 or 0x271a or 0x791a|Honeywell SP2|SP2|
+|0x2720|SPMini|SP2|
+|0x753e|SP3|SP2|
+|0x2728|SPMini2|SP2
+|0x2733 or 0x273e|OEM branded SPMini|SP2|
+|>= 0x7530 and <= 0x7918|OEM branded SPMini2|SP2|
+|0x2736|SPMiniPlus|SP2|
+|0x2712|RM2|RM|
+|0x2737|RM Mini / RM3 Mini Blackbean|RM|
+|0x273d|RM Pro Phicomm|RM|
+|0x2783|RM2 Home Plus|RM|
+|0x277c|RM2 Home Plus GDT|RM|
+|0x272a|RM2 Pro Plus|RM|
+|0x2787|RM2 Pro Plus2|RM|
+|0x278b|RM2 Pro Plus BL|RM|
+|0x278f|RM Mini Shate|RM|
+|0x2714|A1|A1|
+|0x4EB5|MP1|MP1|
+
+
+Command packet format
+---------------------
+
+The command packet header is 56 bytes long with the following format:
+
+|Offset|Contents|
+|------|--------|
+|0x00|0x5a|
+|0x01|0xa5|
+|0x02|0xaa|
+|0x03|0x55|
+|0x04|0x5a|
+|0x05|0xa5|
+|0x06|0xaa|
+|0x07|0x55|
+|0x08-0x1f|00|
+|0x20-0x21|Checksum of full packet as a little-endian 16 bit integer|
+|0x22-0x23|00|
+|0x24-0x25|Device type as a little-endian 16 bit integer|
+|0x26-0x27|Command code as a little-endian 16 bit integer|
+|0x28-0x29|Packet count as a little-endian 16 bit integer|
+|0x2a-0x2f|Local MAC address|
+|0x30-0x33|Local device ID (obtained during authentication, 00 before authentication)|
+|0x34-0x35|Checksum of unencrypted payload as a little-endian 16 bit integer
+|0x36-0x37|00|
+
+The payload is appended immediately after this. The checksum at 0x20 is calculated *after* the payload is appended, and covers the entire packet (including the checksum at 0x34). Therefore:
+
+1. Generate packet header with checksum values set to 0
+2. Set the checksum initialisation value to 0xbeaf and calculate the checksum of the unencrypted payload. Set 0x34-0x35 to this value.
+3. Encrypt and append the payload
+4. Set the checksum initialisation value to 0xbeaf and calculate the checksum of the entire packet. Set 0x20-0x21 to this value.
+
+Authorisation
+-------------
+
+You must obtain an authorisation key from the device before you can communicate. To do so, generate an 80 byte packet with the following contents:
+
+|Offset|Contents|
+|------|--------|
+|0x00-0x03|00|
+|0x04-0x12|A 15-digit value that represents this device. Broadlink's implementation uses the IMEI.|
+|0x13|01|
+|0x14-0x2c|00|
+|0x2d|0x01|
+|0x30-0x7f|NULL-terminated ASCII string containing the device name|
+
+Send this payload with a command value of 0x0065. The response packet will contain an encrypted payload from byte 0x38 onwards. Decrypt this using the default key and IV. The format of the decrypted payload is:
+
+|Offset|Contents|
+|------|--------|
+|0x00-0x03|Device ID|
+|0x04-0x13|Device encryption key|
+
+All further command packets must use this encryption key and device ID.
+
+Entering learning mode
+----------------------
+
+Send the following 16 byte payload with a command value of 0x006a:
+
+|Offset|Contents|
+|------|--------|
+|0x00|0x03|
+|0x01-0x0f|0x00|
+
+Reading back data from learning mode
+------------------------------------
+
+Send the following 16 byte payload with a command value of 0x006a:
+
+|Offset|Contents|
+|------|--------|
+|0x00|0x04|
+|0x01-0x0f|0x00|
+
+Byte 0x22 of the response contains a little-endian 16 bit error code. If this is 0, a code has been obtained. Bytes 0x38 and onward of the response are encrypted. Decrypt them. Bytes 0x04 and onward of the decrypted payload contain the captured data.
+
+Sending data
+------------
+
+Send the following payload with a command byte of 0x006a
+
+|Offset|Contents|
+|------|--------|
+|0x00|0x02|
+|0x01-0x03|0x00|
+|0x04|0x26 = IR, 0xb2 for RF 433Mhz, 0xd7 for RF 315Mhz|
+|0x05|repeat count, (0 = no repeat, 1 send twice, .....)|
+|0x06-0x07|Length of the following data in little endian|
+|0x08 ....|Pulse lengths in 2^-15 s units (µs * 269 / 8192 works very well)|
+|....|0x0d 0x05 at the end for IR only|
+
+Each value is represented by one byte. If the length exceeds one byte
+then it is stored big endian with a leading 0.
+
+Example: The header for my Optoma projector is 8920 4450  
+8920 * 269 / 8192 = 0x124  
+4450 * 269 / 8192 = 0x92  
+
+So the data starts with `0x00 0x1 0x24 0x92 ....`
+
+
+Todo
+----
+
+* Support for other devices using the Broadlink protocol (various smart home devices)
+* Figure out what the format of the data packets actually is.
+* Deal with the response after AP Mode WiFi network setup.
+
diff --git a/third_party/python/broadlink/requirements.txt b/third_party/python/broadlink/requirements.txt
new file mode 100644
index 0000000000..09f445bfd8
--- /dev/null
+++ b/third_party/python/broadlink/requirements.txt
@@ -0,0 +1 @@
+cryptography==2.6.1
diff --git a/third_party/python/broadlink/setup.py b/third_party/python/broadlink/setup.py
new file mode 100644
index 0000000000..778f495fb5
--- /dev/null
+++ b/third_party/python/broadlink/setup.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+from setuptools import setup, find_packages
+
+
+version = '0.13.2'
+
+setup(
+    name='broadlink',
+    version=version,
+    author='Matthew Garrett',
+    author_email='mjg59@srcf.ucam.org',
+    url='http://github.com/mjg59/python-broadlink',
+    packages=find_packages(),
+    scripts=[],
+    install_requires=['cryptography>=2.1.1'],
+    description='Python API for controlling Broadlink IR controllers',
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+    ],
+    include_package_data=True,
+    zip_safe=False,
+)
diff --git a/third_party/rust-crates/OWNERS b/third_party/rust-crates/OWNERS
new file mode 100644
index 0000000000..e978e46150
--- /dev/null
+++ b/third_party/rust-crates/OWNERS
@@ -0,0 +1,5 @@
+inherited: true
+owners:
+  - sterni
+  - Profpatsch
+  - zseri
diff --git a/third_party/rust-crates/default.nix b/third_party/rust-crates/default.nix
new file mode 100644
index 0000000000..17e06a7049
--- /dev/null
+++ b/third_party/rust-crates/default.nix
@@ -0,0 +1,423 @@
+{ depot, pkgs, ... }:
+
+# TVL tool rust crate dependencies, where tools like carnix are not used.
+# Intended for manual updates, which keeps us honest with what we pull into our closure.
+
+let
+  buildRustCrate =
+    attrs@{ edition ? "2018"
+    , pname
+    , crateName ? pname
+    , ...
+    }: pkgs.buildRustCrate (attrs // {
+      inherit
+        crateName
+        edition
+        ;
+    });
+in
+
+rec {
+  cfg-if = buildRustCrate {
+    pname = "cfg-if";
+    version = "1.0.0";
+    sha256 = "1fzidq152hnxhg4lj6r2gv4jpnn8yivp27z6q6xy7w6v0dp6bai9";
+  };
+
+  cc = buildRustCrate {
+    pname = "cc";
+    version = "1.0.66";
+    sha256 = "12q71z6ck8wlqrwgi25x3lrryyks9djymswn9b1c6qq0i01jpc1p";
+  };
+
+  ascii = buildRustCrate {
+    pname = "ascii";
+    version = "1.0.0";
+    edition = "2015";
+    sha256 = "0gam8xsn981wfa40srsniivffjsfz1pg0xnigmczk9k7azb1ks1m";
+  };
+
+  regex-syntax = buildRustCrate {
+    pname = "regex-syntax";
+    version = "0.6.25";
+    edition = "2015";
+    sha256 = "0i211p26m97ii169g0f4gf2a99r8an4xc1fdqj0sf5wpn17qhs29";
+  };
+
+  regex = buildRustCrate {
+    pname = "regex";
+    version = "1.5.5";
+    features = [ "std" ];
+    dependencies = [ regex-syntax ];
+    edition = "2018";
+    sha256 = "0i7yrxsvxpx682vdbkvj7j4w3a3z2c1qwmaa795mm9a9prx4yzjk";
+  };
+
+  libloading = buildRustCrate {
+    pname = "libloading";
+    version = "0.6.7";
+    dependencies = [ cfg-if ];
+    edition = "2015";
+    sha256 = "111d8zsizswnxiqn43vcgnc2ym9spsx1i6pcfp35ca3yw2ixq95j";
+  };
+
+  tree-sitter = buildRustCrate {
+    pname = "tree-sitter";
+    # buildRustCrate isn’t really smart enough to detect the subdir
+    libPath = "binding_rust/lib.rs";
+    # and the build.rs is also not where buildRustCrate would find it
+    build = "binding_rust/build.rs";
+    version = "0.17.1";
+    dependencies = [ regex ];
+    buildDependencies = [ cc ];
+    sha256 = "0jwwbvs4icpra7m1ycvnyri5h3sbw4qrfvgnnvnk72h4w93qhzhr";
+  };
+
+  libc = buildRustCrate {
+    pname = "libc";
+    version = "0.2.82";
+    edition = "2015";
+    sha256 = "02zgn6c0xwh331hky417lbr29kmvrw3ylxs8822syyhjfjqszvsx";
+  };
+
+  bitflags = buildRustCrate {
+    pname = "bitflags";
+    version = "1.2.1";
+    sha256 = "0b77awhpn7yaqjjibm69ginfn996azx5vkzfjj39g3wbsqs7mkxg";
+  };
+
+  inotify-sys = buildRustCrate {
+    pname = "inotify-sys";
+    version = "0.1.5";
+    dependencies = [ libc ];
+    sha256 = "1yiy577xxhi0j90nbg9nkd8cqwc1xix62rz55jjngvxa5jl5613v";
+  };
+
+  inotify = buildRustCrate {
+    pname = "inotify";
+    version = "0.9.2";
+    dependencies = [ bitflags libc inotify-sys ];
+    sha256 = "0fcknyvknglwwk1pdzdlb4m0ry2dym1yx8r5prf2v00pxnjk0hv2";
+  };
+
+  httparse = buildRustCrate {
+    pname = "httparse";
+    version = "1.3.4";
+    edition = "2015";
+    sha256 = "0dggj4s0cq69bn63q9nqzzay5acmwl33nrbhjjsh5xys8sk2x4jw";
+  };
+
+  version-check = buildRustCrate {
+    pname = "version-check";
+    version = "0.9.2";
+    edition = "2015";
+    sha256 = "1vwvc1mzwv8ana9jv8z933p2xzgj1533qwwl5zr8mi89azyhq21v";
+  };
+
+  memchr = buildRustCrate {
+    pname = "memchr";
+    version = "2.3.3";
+    edition = "2015";
+    sha256 = "1ivxvlswglk6wd46gadkbbsknr94gwryk6y21v64ja7x4icrpihw";
+  };
+  nom = buildRustCrate {
+    pname = "nom";
+    version = "5.1.1";
+    sha256 = "1gb4r6mjwd645jqh02nhn60i7qkw8cgy3xq1r4clnmvz3cmkv1l0";
+    dependencies = [ memchr ];
+    buildDependencies = [ version-check ];
+    features = [ "std" "alloc" ];
+  };
+
+  base64 = buildRustCrate {
+    pname = "base64";
+    version = "0.13.0";
+    sha256 = "0i0jk5sgq37kc4c90d1g7dp7zvphbg0dbqc1ajnn0vffjxblgamg";
+    features = [ "alloc" "std" ];
+  };
+
+  bufstream = buildRustCrate {
+    pname = "bufstream";
+    version = "0.1.4";
+    sha256 = "10rqm7jly5jjx7wcc19q6q4m2zsrw3l2v3m1054wnbwvdh42xxf1";
+  };
+
+  autocfg = buildRustCrate {
+    pname = "autocfg";
+    version = "1.0.1";
+    edition = "2015";
+    sha256 = "1lsjz23jdcchcqbsmlzd4iksg3hc8bdvy677jy0938i2gp24frw1";
+  };
+
+  num-traits = buildRustCrate {
+    pname = "num-traits";
+    version = "0.2.14";
+    edition = "2015";
+    buildDependencies = [ autocfg ];
+    sha256 = "09ac9dcp6cr57vjzyiy213y7312jqcy84mkamp99zr40qd1gwnyk";
+  };
+
+  num-integer = buildRustCrate {
+    pname = "num-integer";
+    version = "0.1.44";
+    edition = "2015";
+    dependencies = [ num-traits ];
+    buildDependencies = [ autocfg ];
+    sha256 = "1gdbnfgnivp90h644wmqj4a20yfmdga2xxxacx53pjbcazvfvajc";
+  };
+
+  chrono = buildRustCrate {
+    pname = "chrono";
+    version = "0.4.19";
+    edition = "2015";
+    dependencies = [ num-traits num-integer ];
+    features = [ "alloc" "std" ];
+    sha256 = "0cjf5dnfbk99607vz6n5r6bhwykcypq5psihvk845sxrhnzadsar";
+  };
+
+  imap-proto = buildRustCrate {
+    pname = "imap-proto";
+    version = "0.10.2";
+    dependencies = [ nom ];
+    sha256 = "1bf5r4d0z7c8wxrvr7kjy26500wr7cd4sxz49ix3b3yzc6ayyqv1";
+  };
+
+  lazy_static = buildRustCrate {
+    pname = "lazy_static";
+    version = "1.4.0";
+    sha256 = "13h6sdghdcy7vcqsm2gasfw3qg7ssa0fl3sw7lq6pdkbk52wbyfr";
+  };
+
+  imap = buildRustCrate {
+    pname = "imap";
+    version = "2.4.0";
+    dependencies = [
+      base64
+      bufstream
+      chrono
+      imap-proto
+      lazy_static
+      nom
+      regex
+    ];
+    sha256 = "1nj6x45qnid98nv637623rrh7imcxk0kad89ry8j5dkkgccvjyc0";
+  };
+
+  epoll = buildRustCrate {
+    pname = "epoll";
+    version = "4.3.1";
+    dependencies = [ bitflags libc ];
+    sha256 = "0dgmgdmrfbjkpxn1w3xmmwsm2a623a9qdwn90s8yl78n4a36kbh9";
+  };
+
+  serde = buildRustCrate {
+    pname = "serde";
+    version = "1.0.123";
+    edition = "2015";
+    sha256 = "05xl2s1vpf3p7fi2yc9qlzw88d5ap0z3qmhmd7axa6pp9pn1s5xc";
+    features = [ "std" ];
+  };
+
+  ryu = buildRustCrate {
+    pname = "ryu";
+    version = "1.0.5";
+    sha256 = "060y2ln1csix593ingwxr2y3wl236ls0ly1ffkv39h5im7xydhrc";
+  };
+
+  itoa = buildRustCrate {
+    pname = "itoa";
+    version = "0.4.7";
+    sha256 = "0079jlkcmcaw37wljrvb6r3dqq15nfahkqnl5npvlpdvkg31k11x";
+  };
+
+  serde_json = buildRustCrate {
+    pname = "serde_json";
+    version = "1.0.62";
+    sha256 = "0sgc8dycigq0nxr4j613m4q733alfb2i10s6nz80lsbbqgrka21q";
+    dependencies = [ serde ryu itoa ];
+    features = [ "std" ];
+  };
+
+  log = buildRustCrate {
+    pname = "log";
+    version = "0.4.11";
+    sha256 = "0m6xhqxsps5mgd7r91g5mqkndbh8zbjd58p7w75r330zl4n40l07";
+    dependencies = [ cfg-if ];
+  };
+
+  mustache = buildRustCrate {
+    pname = "mustache";
+    version = "0.9.0";
+    edition = "2015";
+    sha256 = "1zgl8l15i19lzp90icgwyi6zqdd31b9vm8w129f41d1zd0hs7ayq";
+    dependencies = [ log serde ];
+  };
+
+  semver-parser = buildRustCrate {
+    pname = "semver-parser";
+    version = "0.7.0";
+    edition = "2015";
+    sha256 = "1da66c8413yakx0y15k8c055yna5lyb6fr0fw9318kdwkrk5k12h";
+  };
+
+  semver = buildRustCrate {
+    pname = "semver";
+    version = "0.10.0";
+    edition = "2015";
+    sha256 = "0pbkdwlpq4d0hgdrymm2rcw31plni2siwd882gbcbscjvyvrrrqa";
+    dependencies = [ semver-parser ];
+  };
+
+  toml = buildRustCrate {
+    pname = "toml";
+    version = "0.5.8";
+    sha256 = "1vwjwmwsy83pbgvvm11a6grbhb09zkcrv9v95wfwv48wjm01wdj4";
+    edition = "2018";
+    dependencies = [ serde ];
+  };
+
+  pkg-config = buildRustCrate {
+    pname = "pkg-config";
+    version = "0.3.19";
+    crateName = "pkg_config";
+    sha256 = "1kd047p8jv6mhmfzddjvfa2nwkfrb3l1wml6lfm51n1cr06cc9lz";
+  };
+
+  libz-sys = buildRustCrate {
+    pname = "libz-sys";
+    version = "1.1.2";
+    crateName = "libz-sys";
+    sha256 = "1y7v6bkwr4b6yaf951p1ns7mx47b29ziwdd5wziaic14gs1gwq30";
+    buildDependencies = [
+      cc
+      pkg-config
+    ];
+  };
+
+  libgit2-sys = buildRustCrate {
+    pname = "libgit2-sys";
+    version = "0.12.26+1.3.0";
+    crateName = "libgit2_sys";
+    sha256 = "15zg0yy7lk7464yf9i1kxh4gaxdyb8m96ayb7vkjgmz1s2rgq7s2";
+    dependencies = [
+      libc
+      libz-sys
+    ];
+    libPath = "lib.rs";
+    libName = "libgit2_sys";
+    # TODO: this should be available via `pkgs.defaultCrateOverrides`,
+    # I thought that was included by default?
+    nativeBuildInputs = [ pkg-config ];
+    buildInputs = [ pkgs.zlib pkgs.libgit2 ];
+    buildDependencies = [
+      cc
+      pkg-config
+    ];
+  };
+
+  matches = buildRustCrate {
+    pname = "matches";
+    version = "0.1.8";
+    crateName = "matches";
+    sha256 = "03hl636fg6xggy0a26200xs74amk3k9n0908rga2szn68agyz3cv";
+    libPath = "lib.rs";
+  };
+
+  percent-encoding = buildRustCrate {
+    pname = "percent_encoding";
+    version = "2.1.0";
+    crateName = "percent_encoding";
+    sha256 = "0i838f2nr81585ckmfymf8l1x1vdmx6n8xqvli0lgcy60yl2axy3";
+    libPath = "lib.rs";
+  };
+
+  form_urlencoded = buildRustCrate {
+    pname = "form_urlencoded";
+    version = "1.0.1";
+    crateName = "form_urlencoded";
+    sha256 = "0rhv2hfrzk2smdh27walkm66zlvccnnwrbd47fmf8jh6m420dhj8";
+    dependencies = [
+      matches
+      percent-encoding
+    ];
+  };
+
+  tinyvec_macros = buildRustCrate {
+    pname = "tinyvec_macros";
+    version = "0.1.0";
+    crateName = "tinyvec-macros";
+    sha256 = "0aim73hyq5g8b2hs9gjq2sv0xm4xzfbwp5fdyg1frljqzkapq682";
+  };
+
+  tinyvec = buildRustCrate {
+    pname = "tinyvec";
+    version = "1.2.0";
+    crateName = "tinyvec";
+    sha256 = "1c95nma20kiyrjwfsk7hzd5ir6yy4bm63fmfbfb4dm9ahnlvdp3y";
+    features = [ "alloc" ];
+    dependencies = [
+      tinyvec_macros
+    ];
+  };
+
+  unicode-normalization = buildRustCrate {
+    pname = "unicode-normalization";
+    version = "0.1.17";
+    crateName = "unicode_normalization";
+    sha256 = "0w4s0avzlf7pzcclhhih93aap613398sshm6jrxcwq0f9lhis11c";
+    dependencies = [
+      tinyvec
+    ];
+  };
+
+  unicode-bidi = buildRustCrate {
+    pname = "unicode-bidi";
+    version = "0.3.5";
+    crateName = "unicode_bidi";
+    sha256 = "193jzlxj1dfcms2381lyd45zh4ywlicj9lzcfpid1zbkmfarymkz";
+    dependencies = [
+      matches
+    ];
+  };
+
+  idna = buildRustCrate {
+    pname = "idna";
+    version = "0.2.3";
+    crateName = "idna";
+    sha256 = "0hwypd0fpym9lmd4bbqpwyr5lhrlvmvzhi1vy9asc5wxwkzrh299";
+    dependencies = [
+      matches
+      unicode-normalization
+      unicode-bidi
+    ];
+  };
+
+  url = buildRustCrate {
+    pname = "url";
+    version = "2.2.1";
+    crateName = "url";
+    sha256 = "1ci1djafh83qhpzbmxnr9w5gcrjs3ghf8rrxdy4vklqyji6fvn5v";
+    dependencies = [
+      form_urlencoded
+      idna
+      matches
+      percent-encoding
+    ];
+  };
+
+
+  git2 = buildRustCrate {
+    pname = "git2";
+    edition = "2018";
+    version = "0.13.25";
+    crateName = "git2";
+    sha256 = "181mw4kxsqrwpib9kf25fykc48wxhjla37vzis4j0b0w0yhyaqi3";
+    dependencies = [
+      bitflags
+      libc
+      libgit2-sys
+      log
+      url
+    ];
+  };
+}
diff --git a/third_party/rustsec-advisory-db/default.nix b/third_party/rustsec-advisory-db/default.nix
new file mode 100644
index 0000000000..e0ea2b080a
--- /dev/null
+++ b/third_party/rustsec-advisory-db/default.nix
@@ -0,0 +1,19 @@
+# RustSec's advisory db for crates
+{ pkgs, depot, ... }:
+
+let
+  inherit (depot.third_party.sources) rustsec-advisory-db;
+in
+
+pkgs.fetchFromGitHub {
+  inherit (rustsec-advisory-db)
+    owner
+    repo
+    sha256
+    rev
+    ;
+
+  passthru = {
+    inherit (rustsec-advisory-db) rev;
+  };
+}
diff --git a/third_party/smtprelay/default.nix b/third_party/smtprelay/default.nix
new file mode 100644
index 0000000000..b2d730a667
--- /dev/null
+++ b/third_party/smtprelay/default.nix
@@ -0,0 +1,21 @@
+# A simple SMTP relay without the kitchen sink.
+{ pkgs, lib, ... }:
+
+pkgs.buildGoModule rec {
+  pname = "smtprelay";
+  version = "1.7.0";
+  vendorSha256 = "00nb81hdg5pv5l0q7w5lv08dv4v72vml7jha351frani0gpg27pn";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "decke";
+    repo = "smtprelay";
+    rev = "v${version}";
+    sha256 = "0js18xhk64g0g82dx8ii8vhbbssj3pxf1hqv1zadnckdgwfwlj2r";
+  };
+
+  meta = with lib; {
+    description = "Simple Golang SMTP relay/proxy server";
+    homepage = https://github.com/decke/smtprelay;
+    license = licenses.mit;
+  };
+}
diff --git a/third_party/sources/default.nix b/third_party/sources/default.nix
new file mode 100644
index 0000000000..5894c92079
--- /dev/null
+++ b/third_party/sources/default.nix
@@ -0,0 +1,151 @@
+# This file has been generated by Niv.
+_:
+let
+
+  #
+  # The fetchers. fetch_<type> fetches specs of type <type>.
+  #
+
+  fetch_file = pkgs: spec:
+    if spec.builtin or true then
+      builtins_fetchurl { inherit (spec) url sha256; }
+    else
+      pkgs.fetchurl { inherit (spec) url sha256; };
+
+  fetch_tarball = pkgs: name: spec:
+    let
+      ok = str: ! builtins.isNull (builtins.match "[a-zA-Z0-9+-._?=]" str);
+      # sanitize the name, though nix will still fail if name starts with period
+      name' = stringAsChars (x: if ! ok x then "-" else x) "${name}-src";
+    in
+    if spec.builtin or true then
+      builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
+    else
+      pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
+
+  fetch_git = spec:
+    builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
+
+  fetch_local = spec: spec.path;
+
+  fetch_builtin-tarball = name: throw
+    ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
+        $ niv modify ${name} -a type=tarball -a builtin=true'';
+
+  fetch_builtin-url = name: throw
+    ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
+        $ niv modify ${name} -a type=file -a builtin=true'';
+
+  #
+  # Various helpers
+  #
+
+  # The set of packages used when specs are fetched using non-builtins.
+  mkPkgs = sources:
+    let
+      sourcesNixpkgs =
+        import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { };
+      hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
+      hasThisAsNixpkgsPath = <nixpkgs> == ./.;
+    in
+    if builtins.hasAttr "nixpkgs" sources
+    then sourcesNixpkgs
+    else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
+      import <nixpkgs> { }
+    else
+      abort
+        ''
+          Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
+          add a package called "nixpkgs" to your sources.json.
+        '';
+
+  # The actual fetching function.
+  fetch = pkgs: name: spec:
+
+    if ! builtins.hasAttr "type" spec then
+      abort "ERROR: niv spec ${name} does not have a 'type' attribute"
+    else if spec.type == "file" then fetch_file pkgs spec
+    else if spec.type == "tarball" then fetch_tarball pkgs name spec
+    else if spec.type == "git" then fetch_git spec
+    else if spec.type == "local" then fetch_local spec
+    else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
+    else if spec.type == "builtin-url" then fetch_builtin-url name
+    else
+      abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
+
+  # If the environment variable NIV_OVERRIDE_${name} is set, then use
+  # the path directly as opposed to the fetched source.
+  replace = name: drv:
+    let
+      saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
+      ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
+    in
+    if ersatz == "" then drv else ersatz;
+
+  # Ports of functions for older nix versions
+
+  # a Nix version of mapAttrs if the built-in doesn't exist
+  mapAttrs = builtins.mapAttrs or (
+    f: set: with builtins;
+    listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
+  );
+
+  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
+  range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1);
+
+  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
+  stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
+
+  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
+  stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
+  concatStrings = builtins.concatStringsSep "";
+
+  # fetchTarball version that is compatible between all the versions of Nix
+  builtins_fetchTarball = { url, name, sha256 }@attrs:
+    let
+      inherit (builtins) lessThan nixVersion fetchTarball;
+    in
+    if lessThan nixVersion "1.12" then
+      fetchTarball { inherit name url; }
+    else
+      fetchTarball attrs;
+
+  # fetchurl version that is compatible between all the versions of Nix
+  builtins_fetchurl = { url, sha256 }@attrs:
+    let
+      inherit (builtins) lessThan nixVersion fetchurl;
+    in
+    if lessThan nixVersion "1.12" then
+      fetchurl { inherit url; }
+    else
+      fetchurl attrs;
+
+  # Create the final "sources" from the config
+  mkSources = config:
+    mapAttrs
+      (
+        name: spec:
+          if builtins.hasAttr "outPath" spec
+          then
+            abort
+              "The values in sources.json should not have an 'outPath' attribute"
+          else
+            spec // { outPath = replace name (fetch config.pkgs name spec); }
+      )
+      config.sources;
+
+  # The "config" used by the fetchers
+  mkConfig =
+    { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
+    , sources ? if isNull sourcesFile then { } else builtins.fromJSON (builtins.readFile sourcesFile)
+    , pkgs ? mkPkgs sources
+    }: rec {
+      # The sources, i.e. the attribute set of spec name to spec
+      inherit sources;
+
+      # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
+      inherit pkgs;
+    };
+
+in
+mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); }
diff --git a/third_party/sources/sources.json b/third_party/sources/sources.json
new file mode 100644
index 0000000000..b63a5160c6
--- /dev/null
+++ b/third_party/sources/sources.json
@@ -0,0 +1,74 @@
+{
+    "agenix": {
+        "branch": "main",
+        "description": "age-encrypted secrets for NixOS",
+        "homepage": "https://matrix.to/#/#agenix:nixos.org",
+        "owner": "ryantm",
+        "repo": "agenix",
+        "rev": "7e5e58b98c3dcbf497543ff6f22591552ebfe65b",
+        "sha256": "1cfdd2ja56g8clllygf91il7dignr90ij1bl29g3kl7dl977dhl4",
+        "type": "tarball",
+        "url": "https://github.com/ryantm/agenix/archive/7e5e58b98c3dcbf497543ff6f22591552ebfe65b.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "emacs-overlay": {
+        "branch": "master",
+        "description": "Bleeding edge emacs overlay [maintainer=@adisbladis] ",
+        "homepage": "",
+        "owner": "nix-community",
+        "repo": "emacs-overlay",
+        "rev": "fd5baf065e1af4cbb9d40eba66971d31f61d6bd1",
+        "sha256": "0ca529wrx15svzkajxb9gpahnb5f43g336dp7flp04qac1jwnhlq",
+        "type": "tarball",
+        "url": "https://github.com/nix-community/emacs-overlay/archive/fd5baf065e1af4cbb9d40eba66971d31f61d6bd1.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "naersk": {
+        "branch": "master",
+        "description": "Build rust crates in Nix. No configuration, no code generation, no IFD. Sandbox friendly. [maintainer: @Patryk27]",
+        "homepage": "",
+        "owner": "nmattia",
+        "repo": "naersk",
+        "rev": "69daaceebe12c070cd5ae69ba38f277bbf033695",
+        "sha256": "1ip749p9g0prmrwiz0845x9n4j9b39sdrzpi60s4yygy6qgc7262",
+        "type": "tarball",
+        "url": "https://github.com/nmattia/naersk/archive/69daaceebe12c070cd5ae69ba38f277bbf033695.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "nixpkgs": {
+        "branch": "nixos-unstable",
+        "description": "Nix Packages collection",
+        "homepage": "",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "5ce6597eca7d7b518c03ecda57d45f9404b5e060",
+        "sha256": "1hs1lnnbp1dky3nfp7xlricpp5c63sr46jyrnvykci8bl8jnxnl3",
+        "type": "tarball",
+        "url": "https://github.com/NixOS/nixpkgs/archive/5ce6597eca7d7b518c03ecda57d45f9404b5e060.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "nixpkgs-stable": {
+        "branch": "nixos-21.11",
+        "description": "Nix Packages collection",
+        "homepage": "",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "06db2e2197401b74fcf82d4e84be15b0b5851c7b",
+        "sha256": "0p3c9gjjdr1gz55al2s5yhh59kx8fqbgzhw4cab3mqair9h84m4j",
+        "type": "tarball",
+        "url": "https://github.com/NixOS/nixpkgs/archive/06db2e2197401b74fcf82d4e84be15b0b5851c7b.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "rustsec-advisory-db": {
+        "branch": "main",
+        "description": "Security advisory database for Rust crates published through crates.io",
+        "homepage": "https://rustsec.org",
+        "owner": "RustSec",
+        "repo": "advisory-db",
+        "rev": "29281434b74516435cf1c3a79046bbec2cca9ee2",
+        "sha256": "0ma62bf9haacbqc37d11w9c89fi6a7rj0sndnjp9dsfqh25jxrk3",
+        "type": "tarball",
+        "url": "https://github.com/RustSec/advisory-db/archive/29281434b74516435cf1c3a79046bbec2cca9ee2.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    }
+}
diff --git a/third_party/terraform-provider-glesys/default.nix b/third_party/terraform-provider-glesys/default.nix
new file mode 100644
index 0000000000..85a9fd6480
--- /dev/null
+++ b/third_party/terraform-provider-glesys/default.nix
@@ -0,0 +1,19 @@
+# GleSYS Terraform provider
+#
+# Some TVL resources (DNS, object storage, ...) are hosted with them.
+{ pkgs, ... }:
+
+pkgs.terraform-providers.mkProvider rec {
+  version = "0.3.2";
+
+  owner = "glesys";
+  repo = "terraform-provider-glesys";
+  rev = "v${version}";
+  sha256 = "1hlqa4f9d44hq614ff8ivg8a6fwg48jwz11zsrlghjzky82cfraq";
+
+  vendorSha256 = "0g5g69absf0vmin0ff0anrxcgfq0bzx4iz3qci90p9xkvyph4nlw";
+
+  # This provider is not officially published in the TF registry, so
+  # we're giving it a fake source here.
+  provider-source-address = "registry.terraform.io/depot/glesys";
+}
diff --git a/tools/cheddar/.gitignore b/tools/cheddar/.gitignore
new file mode 100644
index 0000000000..2f7896d1d1
--- /dev/null
+++ b/tools/cheddar/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/tools/cheddar/.skip-subtree b/tools/cheddar/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tools/cheddar/.skip-subtree
diff --git a/tools/cheddar/Cargo.lock b/tools/cheddar/Cargo.lock
new file mode 100644
index 0000000000..5ffe05f7ed
--- /dev/null
+++ b/tools/cheddar/Cargo.lock
@@ -0,0 +1,1194 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ascii"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
+]
+
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cheddar"
+version = "0.2.0"
+dependencies = [
+ "clap",
+ "comrak",
+ "lazy_static",
+ "regex",
+ "rouille",
+ "serde",
+ "serde_json",
+ "syntect",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "comrak"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b423acba50d5016684beaf643f9991e622633a4c858be6885653071c2da2b0c6"
+dependencies = [
+ "clap",
+ "entities",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "regex",
+ "shell-words",
+ "twoway 0.2.2",
+ "typed-arena",
+ "unicode_categories",
+ "xdg",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "deflate"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70"
+dependencies = [
+ "adler32",
+ "gzip-header",
+]
+
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gzip-header"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand",
+ "safemem",
+ "tempfile",
+ "twoway 0.1.8",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "onig"
+version = "6.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ddfe2c93bb389eea6e6d713306880c7f6dcc99a75b659ce145d962c861b225"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "libc",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd3eee045c84695b53b20255bb7317063df090b68e18bfac0abb6c39cf7f33e"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+dependencies = [
+ "maplit",
+ "pest",
+ "sha-1",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "plist"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
+dependencies = [
+ "base64",
+ "indexmap",
+ "line-wrap",
+ "serde",
+ "time",
+ "xml-rs",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rouille"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18b2380c42510ef4a28b5f228a174c801e0dec590103e215e60812e2e2f34d05"
+dependencies = [
+ "base64",
+ "brotli",
+ "chrono",
+ "deflate",
+ "filetime",
+ "multipart",
+ "num_cpus",
+ "percent-encoding",
+ "rand",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "threadpool",
+ "time",
+ "tiny_http",
+ "url",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer",
+ "digest",
+ "fake-simd",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
+dependencies = [
+ "sha1_smol",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "syn"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syntect"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031"
+dependencies = [
+ "bincode",
+ "bitflags",
+ "flate2",
+ "fnv",
+ "lazy_static",
+ "lazycell",
+ "onig",
+ "plist",
+ "regex-syntax",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "walkdir",
+ "yaml-rust",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "tiny_http"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39"
+dependencies = [
+ "ascii",
+ "chrono",
+ "chunked_transfer",
+ "log",
+ "url",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "twoway"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
+dependencies = [
+ "memchr",
+ "unchecked-index",
+]
+
+[[package]]
+name = "typed-arena"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unchecked-index"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "xdg"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
+dependencies = [
+ "dirs",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
diff --git a/tools/cheddar/Cargo.toml b/tools/cheddar/Cargo.toml
new file mode 100644
index 0000000000..ee4cbb4d58
--- /dev/null
+++ b/tools/cheddar/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "cheddar"
+version = "0.2.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+edition = "2018"
+
+[dependencies]
+clap = "2.33"
+comrak = "0.10"
+lazy_static = "1.4"
+rouille = "3.5"
+syntect = "4.5.0"
+serde_json = "1.0"
+regex = "1.4"
+
+[dependencies.serde]
+version = "1.0"
+features = [ "derive" ]
diff --git a/tools/cheddar/README.md b/tools/cheddar/README.md
new file mode 100644
index 0000000000..706f3b62d5
--- /dev/null
+++ b/tools/cheddar/README.md
@@ -0,0 +1,21 @@
+cheddar
+=======
+
+Cheddar is a tiny Rust tool that uses [syntect][] to render source code to
+syntax-highlighted HTML.
+
+It's invocation is compatible with `cgit` filters, i.e. data is read from
+`stdin` and the filename is taken from `argv`:
+
+```shell
+cat README.md | cheddar README.md > README.html
+
+```
+
+In fact, if you are looking at this file on git.tazj.in chances are that it was
+rendered by cheddar.
+
+The name was chosen because I was eyeing a pack of cheddar-flavoured crisps
+while thinking about name selection.
+
+[syntect]: https://github.com/trishume/syntect
diff --git a/tools/cheddar/build.rs b/tools/cheddar/build.rs
new file mode 100644
index 0000000000..f70818d801
--- /dev/null
+++ b/tools/cheddar/build.rs
@@ -0,0 +1,55 @@
+//! Build script that can be used outside of Nix builds to inject the
+//! BAT_SYNTAXES variable when building in development mode.
+//!
+//! Note that this script assumes that cheddar is in a checkout of the
+//! TVL depot.
+
+use std::process::Command;
+
+static BAT_SYNTAXES: &str = "BAT_SYNTAXES";
+static ERROR_MESSAGE: &str = r#"Failed to build syntax set.
+
+When building during development, cheddar expects to be in a checkout
+of the TVL depot. This is required to automatically build the syntax
+highlighting files that are needed at compile time.
+
+As cheddar can not automatically detect the location of the syntax
+files, you must set the `BAT_SYNTAXES` environment variable to the
+right path.
+
+The expected syntax files are at //third_party/bat_syntaxes in the
+depot."#;
+
+fn main() {
+    // Do nothing if the variable is already set (e.g. via Nix)
+    if let Ok(_) = std::env::var(BAT_SYNTAXES) {
+        return;
+    }
+
+    // Otherwise ask Nix to build it and inject the result.
+    let output = Command::new("nix-build")
+        .arg("-A")
+        .arg("third_party.bat_syntaxes")
+        // ... assuming cheddar is at //tools/cheddar ...
+        .arg("../..")
+        .output()
+        .expect(ERROR_MESSAGE);
+
+    if !output.status.success() {
+        eprintln!(
+            "{}\nNix output: {}",
+            ERROR_MESSAGE,
+            String::from_utf8_lossy(&output.stderr)
+        );
+        return;
+    }
+
+    let out_path = String::from_utf8(output.stdout)
+        .expect("Nix returned invalid output after building syntax set");
+
+    // Return an instruction to Cargo that will set the environment
+    // variable during rustc calls.
+    //
+    // https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-envvarvalue
+    println!("cargo:rustc-env={}={}", BAT_SYNTAXES, out_path.trim());
+}
diff --git a/tools/cheddar/default.nix b/tools/cheddar/default.nix
new file mode 100644
index 0000000000..17efae91ff
--- /dev/null
+++ b/tools/cheddar/default.nix
@@ -0,0 +1,23 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  doDoc = false;
+
+  override = x: {
+    # Use our custom bat syntax set, which is everything from upstream,
+    # plus additional languages we care about.
+    BAT_SYNTAXES = "${depot.third_party.bat_syntaxes}";
+  };
+
+  passthru = {
+    # Wrapper for cgit which can't be told to pass arguments to a filter
+    about-filter = pkgs.writeShellScriptBin "cheddar-about" ''
+      exec ${depot.tools.cheddar}/bin/cheddar --about-filter $@
+    '';
+  };
+
+  meta.ci.targets = [
+    "about-filter"
+  ];
+}
diff --git a/tools/cheddar/src/bin/cheddar.rs b/tools/cheddar/src/bin/cheddar.rs
new file mode 100644
index 0000000000..48c504d535
--- /dev/null
+++ b/tools/cheddar/src/bin/cheddar.rs
@@ -0,0 +1,134 @@
+//! This file defines the binary for cheddar, which can be interacted
+//! with in two different ways:
+//!
+//! 1. As a CLI tool that acts as a cgit filter.
+//! 2. As a long-running HTTP server that handles rendering requests
+//!    (matching the SourceGraph protocol).
+use clap::{App, Arg};
+use rouille::{router, try_or_400, Response};
+use serde::Deserialize;
+use serde_json::json;
+use std::collections::HashMap;
+use std::io;
+
+use cheddar::{format_code, format_markdown, THEMES};
+
+// Server endpoint for rendering the syntax of source code. This
+// replaces the 'syntect_server' component of Sourcegraph.
+fn code_endpoint(request: &rouille::Request) -> rouille::Response {
+    #[derive(Deserialize)]
+    struct SourcegraphQuery {
+        filepath: String,
+        theme: String,
+        code: String,
+    }
+
+    let query: SourcegraphQuery = try_or_400!(rouille::input::json_input(request));
+    let mut buf: Vec<u8> = Vec::new();
+
+    // We don't use syntect with the sourcegraph themes bundled
+    // currently, so let's fall back to something that is kind of
+    // similar (tm).
+    let theme = &THEMES.themes[match query.theme.as_str() {
+        "Sourcegraph (light)" => "Solarized (light)",
+        _ => "Solarized (dark)",
+    }];
+
+    format_code(theme, &mut query.code.as_bytes(), &mut buf, &query.filepath);
+
+    Response::json(&json!({
+        "is_plaintext": false,
+        "data": String::from_utf8_lossy(&buf)
+    }))
+}
+
+// Server endpoint for rendering a Markdown file.
+fn markdown_endpoint(request: &rouille::Request) -> rouille::Response {
+    let mut texts: HashMap<String, String> = try_or_400!(rouille::input::json_input(request));
+
+    for text in texts.values_mut() {
+        let mut buf: Vec<u8> = Vec::new();
+        format_markdown(&mut text.as_bytes(), &mut buf);
+        *text = String::from_utf8_lossy(&buf).to_string();
+    }
+
+    Response::json(&texts)
+}
+
+fn highlighting_server(listen: &str) {
+    println!("Starting syntax highlighting server on '{}'", listen);
+
+    rouille::start_server(listen, move |request| {
+        router!(request,
+                // Markdown rendering route
+                (POST) (/markdown) => {
+                    markdown_endpoint(request)
+                },
+
+                // Code rendering route
+                (POST) (/) => {
+                    code_endpoint(request)
+                },
+
+                _ => {
+                    rouille::Response::empty_404()
+                },
+        )
+    });
+}
+
+fn main() {
+    // Parse the command-line flags passed to cheddar to determine
+    // whether it is running in about-filter mode (`--about-filter`)
+    // and what file extension has been supplied.
+    let matches = App::new("cheddar")
+        .about("TVL's syntax highlighter")
+        .arg(
+            Arg::with_name("about-filter")
+                .help("Run as a cgit about-filter (renders Markdown)")
+                .long("about-filter")
+                .takes_value(false),
+        )
+        .arg(
+            Arg::with_name("sourcegraph-server")
+                .help("Run as a Sourcegraph compatible web-server")
+                .long("sourcegraph-server")
+                .takes_value(false),
+        )
+        .arg(
+            Arg::with_name("listen")
+                .help("Address to listen on")
+                .long("listen")
+                .takes_value(true),
+        )
+        .arg(Arg::with_name("filename").help("File to render").index(1))
+        .get_matches();
+
+    if matches.is_present("sourcegraph-server") {
+        highlighting_server(
+            matches
+                .value_of("listen")
+                .expect("Listening address is required for server mode"),
+        );
+        return;
+    }
+
+    let filename = matches.value_of("filename").expect("filename is required");
+
+    let stdin = io::stdin();
+    let mut in_handle = stdin.lock();
+
+    let stdout = io::stdout();
+    let mut out_handle = stdout.lock();
+
+    if matches.is_present("about-filter") && filename.ends_with(".md") {
+        format_markdown(&mut in_handle, &mut out_handle);
+    } else {
+        format_code(
+            &THEMES.themes["InspiredGitHub"],
+            &mut in_handle,
+            &mut out_handle,
+            filename,
+        );
+    }
+}
diff --git a/tools/cheddar/src/lib.rs b/tools/cheddar/src/lib.rs
new file mode 100644
index 0000000000..851bd743db
--- /dev/null
+++ b/tools/cheddar/src/lib.rs
@@ -0,0 +1,339 @@
+//! This file implements the rendering logic of cheddar with public
+//! functions for syntax-highlighting code and for turning Markdown
+//! into HTML with TVL extensions.
+use comrak::arena_tree::Node;
+use comrak::nodes::{Ast, AstNode, NodeCodeBlock, NodeHtmlBlock, NodeValue};
+use comrak::{format_html, parse_document, Arena, ComrakOptions};
+use lazy_static::lazy_static;
+use regex::Regex;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::ffi::OsStr;
+use std::io::{BufRead, Write};
+use std::path::Path;
+use std::{env, io};
+use syntect::dumps::from_binary;
+use syntect::easy::HighlightLines;
+use syntect::highlighting::{Theme, ThemeSet};
+use syntect::parsing::{SyntaxReference, SyntaxSet};
+use syntect::util::LinesWithEndings;
+
+use syntect::html::{
+    append_highlighted_html_for_styled_line, start_highlighted_html_snippet, IncludeBackground,
+};
+
+#[cfg(test)]
+mod tests;
+
+lazy_static! {
+    // Load syntaxes lazily. Initialisation might not be required in
+    // the case of Markdown rendering (if there's no code blocks
+    // within the document).
+    //
+    // Note that the syntax set is included from the path pointed to
+    // by the BAT_SYNTAXES environment variable at compile time. This
+    // variable is populated by Nix and points to TVL's syntax set.
+    static ref SYNTAXES: SyntaxSet = from_binary(include_bytes!(env!("BAT_SYNTAXES")));
+    pub static ref THEMES: ThemeSet = ThemeSet::load_defaults();
+
+    // Configure Comrak's Markdown rendering with all the bells &
+    // whistles!
+    static ref MD_OPTS: ComrakOptions = {
+        let mut options = ComrakOptions::default();
+
+        // Enable non-standard Markdown features:
+        options.extension.strikethrough = true;
+        options.extension.tagfilter = true;
+        options.extension.table = true;
+        options.extension.autolink = true;
+        options.extension.tasklist = true;
+        options.extension.header_ids = Some(String::new()); // yyeeesss!
+        options.extension.footnotes = true;
+        options.extension.description_lists = true;
+        options.extension.front_matter_delimiter = Some("---".to_owned());
+
+        // Required for tagfilter
+        options.render.unsafe_ = true;
+
+        options
+    };
+
+    // Configures a map of specific filenames to languages, for cases
+    // where the detection by extension or other heuristics fails.
+    static ref FILENAME_OVERRIDES: HashMap<&'static str, &'static str> = {
+        let mut map = HashMap::new();
+        // rules.pl is the canonical name of the submit rule file in
+        // Gerrit, which is written in Prolog.
+        map.insert("rules.pl", "Prolog");
+        map
+    };
+
+    // Default shortlink set used in cheddar (i.e. TVL's shortlinks)
+    static ref TVL_LINKS: Vec<Shortlink> = vec![
+        // TVL shortlinks for bugs and changelists (e.g. b/123,
+        // cl/123). Coincidentally these have the same format, which
+        // makes the initial implementation easy.
+        Shortlink {
+            pattern: Regex::new(r#"\b(?P<type>b|cl)/(?P<dest>\d+)\b"#).unwrap(),
+            replacement: "[$type/$dest](https://$type.tvl.fyi/$dest)",
+        },
+        Shortlink {
+            pattern: Regex::new(r#"\br/(?P<dest>\d+)\b"#).unwrap(),
+            replacement: "[r/$dest](https://code.tvl.fyi/commit/?id=refs/r/$dest)",
+        }
+    ];
+}
+
+/// Structure that describes a single shortlink that should be
+/// automatically highlighted. Highlighting is performed as a string
+/// replacement over input Markdown.
+pub struct Shortlink {
+    /// Short link pattern to recognise. Make sure to anchor these
+    /// correctly.
+    pub pattern: Regex,
+
+    /// Replacement string, as per the documentation of
+    /// [`Regex::replace`].
+    pub replacement: &'static str,
+}
+
+// HTML fragment used when rendering inline blocks in Markdown documents.
+// Emulates the GitHub style (subtle background hue and padding).
+const BLOCK_PRE: &str = "<pre style=\"background-color:#f6f8fa;padding:16px;\">\n";
+
+fn should_continue(res: &io::Result<usize>) -> bool {
+    match *res {
+        Ok(n) => n > 0,
+        Err(_) => false,
+    }
+}
+
+// This function is taken from the Comrak documentation.
+fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F)
+where
+    F: Fn(&'a AstNode<'a>),
+{
+    f(node);
+    for c in node.children() {
+        iter_nodes(c, f);
+    }
+}
+
+// Many of the syntaxes in the syntax list have random capitalisations, which
+// means that name matching for the block info of a code block in HTML fails.
+//
+// Instead, try finding a syntax match by comparing case insensitively (for
+// ASCII characters, anyways).
+fn find_syntax_case_insensitive(info: &str) -> Option<&'static SyntaxReference> {
+    // TODO(tazjin): memoize this lookup
+    SYNTAXES
+        .syntaxes()
+        .iter()
+        .rev()
+        .find(|&s| info.eq_ignore_ascii_case(&s.name))
+}
+
+// Replaces code-block inside of a Markdown AST with HTML blocks rendered by
+// syntect. This enables static (i.e. no JavaScript) syntax highlighting, even
+// of complex languages.
+fn highlight_code_block(code_block: &NodeCodeBlock) -> NodeValue {
+    let theme = &THEMES.themes["InspiredGitHub"];
+    let info = String::from_utf8_lossy(&code_block.info);
+
+    let syntax = find_syntax_case_insensitive(&info)
+        .or_else(|| SYNTAXES.find_syntax_by_extension(&info))
+        .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
+
+    let code = String::from_utf8_lossy(&code_block.literal);
+
+    let rendered = {
+        // Write the block preamble manually to get exactly the
+        // desired layout:
+        let mut hl = HighlightLines::new(syntax, theme);
+        let mut buf = BLOCK_PRE.to_string();
+
+        for line in LinesWithEndings::from(&code) {
+            let regions = hl.highlight(line, &SYNTAXES);
+            append_highlighted_html_for_styled_line(&regions[..], IncludeBackground::No, &mut buf);
+        }
+
+        buf.push_str("</pre>");
+        buf
+    };
+
+    let mut block = NodeHtmlBlock::default();
+    block.literal = rendered.into_bytes();
+
+    NodeValue::HtmlBlock(block)
+}
+
+// Supported callout elements (which each have their own distinct rendering):
+enum Callout {
+    Todo,
+    Warning,
+    Question,
+    Tip,
+}
+
+// Determine whether the first child of the supplied node contains a text that
+// should cause a callout section to be rendered.
+fn has_callout<'a>(node: &Node<'a, RefCell<Ast>>) -> Option<Callout> {
+    match node.first_child().map(|c| c.data.borrow()) {
+        Some(child) => match &child.value {
+            NodeValue::Text(text) => {
+                if text.starts_with(b"TODO") {
+                    return Some(Callout::Todo);
+                } else if text.starts_with(b"WARNING") {
+                    return Some(Callout::Warning);
+                } else if text.starts_with(b"QUESTION") {
+                    return Some(Callout::Question);
+                } else if text.starts_with(b"TIP") {
+                    return Some(Callout::Tip);
+                }
+
+                None
+            }
+            _ => None,
+        },
+        _ => None,
+    }
+}
+
+// Replace instances of known shortlinks in the input document with
+// Markdown syntax for a highlighted link.
+fn linkify_shortlinks(mut text: String, shortlinks: &[Shortlink]) -> String {
+    for link in shortlinks {
+        text = link
+            .pattern
+            .replace_all(&text, link.replacement)
+            .to_string();
+    }
+
+    return text;
+}
+
+fn format_callout_paragraph(callout: Callout) -> NodeValue {
+    let class = match callout {
+        Callout::Todo => "cheddar-todo",
+        Callout::Warning => "cheddar-warning",
+        Callout::Question => "cheddar-question",
+        Callout::Tip => "cheddar-tip",
+    };
+
+    let mut block = NodeHtmlBlock::default();
+    block.literal = format!("<p class=\"cheddar-callout {}\">", class).into_bytes();
+    NodeValue::HtmlBlock(block)
+}
+
+pub fn format_markdown_with_shortlinks<R: BufRead, W: Write>(
+    reader: &mut R,
+    writer: &mut W,
+    shortlinks: &[Shortlink],
+) {
+    let document = {
+        let mut buffer = String::new();
+        reader
+            .read_to_string(&mut buffer)
+            .expect("reading should work");
+        buffer
+    };
+
+    let arena = Arena::new();
+    let root = parse_document(&arena, &linkify_shortlinks(document, shortlinks), &MD_OPTS);
+
+    // This node must exist with a lifetime greater than that of the parsed AST
+    // in case that callouts are encountered (otherwise insertion into the tree
+    // is not possible).
+    let mut p_close_value = NodeHtmlBlock::default();
+    p_close_value.literal = b"</p>".to_vec();
+
+    let p_close_node = Ast::new(NodeValue::HtmlBlock(p_close_value));
+    let p_close = Node::new(RefCell::new(p_close_node));
+
+    // Special features of Cheddar are implemented by traversing the
+    // arena and reacting on nodes that we might want to modify.
+    iter_nodes(root, &|node| {
+        let mut ast = node.data.borrow_mut();
+        let new = match &ast.value {
+            // Syntax highlighting is implemented by replacing the
+            // code block node with literal HTML.
+            NodeValue::CodeBlock(code) => Some(highlight_code_block(code)),
+
+            NodeValue::Paragraph => {
+                if let Some(callout) = has_callout(node) {
+                    node.insert_after(&p_close);
+                    Some(format_callout_paragraph(callout))
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        };
+
+        if let Some(new_value) = new {
+            ast.value = new_value
+        }
+    });
+
+    format_html(root, &MD_OPTS, writer).expect("Markdown rendering failed");
+}
+
+pub fn format_markdown<R: BufRead, W: Write>(reader: &mut R, writer: &mut W) {
+    format_markdown_with_shortlinks(reader, writer, &TVL_LINKS)
+}
+
+fn find_syntax_for_file(filename: &str) -> &'static SyntaxReference {
+    (*FILENAME_OVERRIDES)
+        .get(filename)
+        .and_then(|name| SYNTAXES.find_syntax_by_name(name))
+        .or_else(|| {
+            Path::new(filename)
+                .extension()
+                .and_then(OsStr::to_str)
+                .and_then(|s| SYNTAXES.find_syntax_by_extension(s))
+        })
+        .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text())
+}
+
+pub fn format_code<R: BufRead, W: Write>(
+    theme: &Theme,
+    reader: &mut R,
+    writer: &mut W,
+    filename: &str,
+) {
+    let mut linebuf = String::new();
+
+    // Get the first line, we might need it for syntax identification.
+    let mut read_result = reader.read_line(&mut linebuf);
+    let syntax = find_syntax_for_file(filename);
+
+    let mut hl = HighlightLines::new(syntax, theme);
+    let (mut outbuf, bg) = start_highlighted_html_snippet(theme);
+
+    // Rather than using the `lines` iterator, read each line manually
+    // and maintain buffer state.
+    //
+    // This is done because the syntax highlighter requires trailing
+    // newlines to be efficient, and those are stripped in the lines
+    // iterator.
+    while should_continue(&read_result) {
+        let regions = hl.highlight(&linebuf, &SYNTAXES);
+
+        append_highlighted_html_for_styled_line(
+            &regions[..],
+            IncludeBackground::IfDifferent(bg),
+            &mut outbuf,
+        );
+
+        // immediately output the current state to avoid keeping
+        // things in memory
+        write!(writer, "{}", outbuf).expect("write should not fail");
+
+        // merry go round again
+        linebuf.clear();
+        outbuf.clear();
+        read_result = reader.read_line(&mut linebuf);
+    }
+
+    writeln!(writer, "</pre>").expect("write should not fail");
+}
diff --git a/tools/cheddar/src/tests.rs b/tools/cheddar/src/tests.rs
new file mode 100644
index 0000000000..c82bba6767
--- /dev/null
+++ b/tools/cheddar/src/tests.rs
@@ -0,0 +1,105 @@
+use super::*;
+use std::io::BufReader;
+
+// Markdown rendering expectation, ignoring leading and trailing
+// whitespace in the input and output.
+fn expect_markdown(input: &str, expected: &str) {
+    let mut input_buf = BufReader::new(input.trim().as_bytes());
+    let mut out_buf: Vec<u8> = vec![];
+    format_markdown(&mut input_buf, &mut out_buf);
+
+    let out_string = String::from_utf8(out_buf).expect("output should be UTF8");
+    assert_eq!(out_string.trim(), expected.trim());
+}
+
+#[test]
+fn renders_simple_markdown() {
+    expect_markdown("hello", "<p>hello</p>\n");
+}
+
+#[test]
+fn renders_callouts() {
+    expect_markdown(
+        "TODO some task.",
+        r#"<p class="cheddar-callout cheddar-todo">
+TODO some task.
+</p>
+"#,
+    );
+
+    expect_markdown(
+        "WARNING: be careful",
+        r#"<p class="cheddar-callout cheddar-warning">
+WARNING: be careful
+</p>
+"#,
+    );
+
+    expect_markdown(
+        "TIP: note the thing",
+        r#"<p class="cheddar-callout cheddar-tip">
+TIP: note the thing
+</p>
+"#,
+    );
+}
+
+#[test]
+fn renders_code_snippets() {
+    expect_markdown(
+        r#"
+Code:
+```nix
+toString 42
+```
+"#,
+        r#"
+<p>Code:</p>
+<pre style="background-color:#f6f8fa;padding:16px;">
+<span style="color:#62a35c;">toString </span><span style="color:#0086b3;">42
+</span></pre>
+"#,
+    );
+}
+
+#[test]
+fn highlights_bug_link() {
+    expect_markdown(
+        "Please look at b/123.",
+        "<p>Please look at <a href=\"https://b.tvl.fyi/123\">b/123</a>.</p>",
+    );
+}
+
+#[test]
+fn highlights_cl_link() {
+    expect_markdown(
+        "Please look at cl/420.",
+        "<p>Please look at <a href=\"https://cl.tvl.fyi/420\">cl/420</a>.</p>",
+    );
+}
+
+#[test]
+fn highlights_r_link() {
+    expect_markdown(
+        "Fixed in r/3268.",
+        "<p>Fixed in <a href=\"https://code.tvl.fyi/commit/?id=refs/r/3268\">r/3268</a>.</p>",
+    );
+}
+
+#[test]
+fn highlights_multiple_shortlinks() {
+    expect_markdown(
+        "Please look at cl/420, b/123.",
+        "<p>Please look at <a href=\"https://cl.tvl.fyi/420\">cl/420</a>, <a href=\"https://b.tvl.fyi/123\">b/123</a>.</p>",
+    );
+
+    expect_markdown(
+        "b/213/cl/213 are different things",
+        "<p><a href=\"https://b.tvl.fyi/213\">b/213</a>/<a href=\"https://cl.tvl.fyi/213\">cl/213</a> are different things</p>",
+    );
+}
+
+#[test]
+fn ignores_invalid_shortlinks() {
+    expect_markdown("b/abc is not a real bug", "<p>b/abc is not a real bug</p>");
+}
diff --git a/tools/crfo-approve.nix b/tools/crfo-approve.nix
new file mode 100644
index 0000000000..d4cff9e1b2
--- /dev/null
+++ b/tools/crfo-approve.nix
@@ -0,0 +1,52 @@
+# Helper script to run a CRFO approval using depot-interventions.
+#
+# Use as 'crfo-approve $CL_ID $PATCHSET $REAL_USER $ON_BEHALF_OF'.
+#
+# Set credential in GERRIT_TOKEN envvar.
+{ pkgs, ... }:
+
+pkgs.writeShellScriptBin "crfo-approve" ''
+  set -ueo pipefail
+
+  if (($# != 4)) || [[ -z ''${GERRIT_TOKEN-} ]]; then
+    cat >&2 <<'EOF'
+  crfo-approve - Helper script to CRFO approve a TVL CL
+
+  Requires membership in depot-interventions to work.
+
+  Gerrit HTTP credential must be set in GERRIT_TOKEN envvar.
+
+  Usage:
+    crfo-approve $CL_ID $PATCHSET $REAL_USER $ON_BEHALF_OF
+  EOF
+    exit 1
+  fi
+
+  export PATH="${pkgs.lib.makeBinPath [ pkgs.httpie pkgs.jq ]}:''${PATH}"
+
+  readonly CL_ID="''${1}"
+  readonly PATCHSET="''${2}"
+  readonly REAL_USER="''${3}"
+  readonly TOKEN="''${GERRIT_TOKEN}"
+  readonly ON_BEHALF_OF="''${4}"
+  readonly URL="https://cl.tvl.fyi/a/changes/''${CL_ID}/revisions/''${PATCHSET}/review"
+
+  # First we need to find the account ID for the user
+  ACC_RESPONSE=$(http --check-status 'https://cl.tvl.fyi/accounts/' "q==name:''${ON_BEHALF_OF}" | tail -n +2)
+  ACC_LENGTH=$(echo "''${ACC_RESPONSE}" | jq 'length')
+
+  if [[ ''${ACC_LENGTH} -ne 1 ]]; then
+      echo "Did not find a unique account ID for ''${ON_BEHALF_OF}"
+      exit 1
+  fi
+
+  ACC_ID=$(jq -n --argjson response "''${ACC_RESPONSE}" '$response[0]._account_id')
+  echo "using account ID ''${ACC_ID} for ''${ON_BEHALF_OF}"
+
+  http --check-status -a "''${REAL_USER}:''${TOKEN}" POST "''${URL}" \
+    message="CRFO on behalf of ''${ON_BEHALF_OF}" \
+    'labels[Code-Review]=+2' \
+    on_behalf_of="''${ACC_ID}" \
+    "add_to_attention_set[0][user]=''${ACC_ID}" \
+    "add_to_attention_set[0][reason]=CRFO approval through depot-interventions"
+''
diff --git a/tools/depot-deps.nix b/tools/depot-deps.nix
new file mode 100644
index 0000000000..eabd6484c3
--- /dev/null
+++ b/tools/depot-deps.nix
@@ -0,0 +1,27 @@
+# Shell derivation to invoke //nix/lazy-deps with the dependencies
+# that should be lazily made available in depot.
+{ pkgs, depot, ... }:
+
+depot.nix.lazy-deps {
+  age-keygen.attr = "third_party.nixpkgs.age";
+  age.attr = "third_party.nixpkgs.age";
+  depotfmt.attr = "tools.depotfmt";
+  gerrit-update.attr = "tools.gerrit-update";
+  gerrit.attr = "tools.gerrit-cli";
+  hash-password.attr = "tools.hash-password";
+  mg.attr = "tools.magrathea";
+  nint.attr = "nix.nint";
+  niv.attr = "third_party.nixpkgs.niv";
+  rebuild-system.attr = "ops.nixos.rebuild-system";
+  rink.attr = "third_party.nixpkgs.rink";
+
+  tf-glesys = {
+    attr = "ops.glesys.terraform";
+    cmd = "terraform";
+  };
+
+  tf-keycloak = {
+    attr = "ops.keycloak.terraform";
+    cmd = "terraform";
+  };
+}
diff --git a/tools/depot-scanner/OWNERS b/tools/depot-scanner/OWNERS
new file mode 100644
index 0000000000..cefacea4d0
--- /dev/null
+++ b/tools/depot-scanner/OWNERS
@@ -0,0 +1,3 @@
+inherit: true
+owners:
+ - riking
diff --git a/tools/depot-scanner/default.nix b/tools/depot-scanner/default.nix
new file mode 100644
index 0000000000..59b6e53f70
--- /dev/null
+++ b/tools/depot-scanner/default.nix
@@ -0,0 +1,18 @@
+{ depot, pkgs, ... }:
+
+let
+  localProto = depot.nix.buildGo.grpc {
+    name = "code.tvl.fyi/tools/depot-scanner/proto";
+    proto = ./depot_scanner.proto;
+  };
+in
+depot.nix.buildGo.program
+  {
+    name = "depot-scanner";
+    srcs = [
+      ./main.go
+    ];
+    deps = [
+      localProto
+    ];
+  } // { inherit localProto; }
diff --git a/tools/depot-scanner/depot_scanner.proto b/tools/depot-scanner/depot_scanner.proto
new file mode 100644
index 0000000000..5249daebf4
--- /dev/null
+++ b/tools/depot-scanner/depot_scanner.proto
@@ -0,0 +1,46 @@
+// Copyright 2020 TVL
+// SPDX-License-Identifier: MIT
+
+syntax = "proto3";
+package tvl.tools.depot_scanner;
+option go_package = "code.tvl.fyi/tools/depot-scanner/proto";
+
+enum PathType {
+  UNKNOWN = 0;
+  DEPOT = 1;
+  STORE = 2;
+  CORE = 3;
+}
+
+message ScanRequest {
+  // Which revision of the depot
+  string revision = 1;
+  string attr = 2;
+  // Optionally, the attr to evaluate can be provided as a path to a folder or a
+  // .nix file.  This is used by the HTTP service.
+  string attrAsPath = 3;
+}
+
+message ScanResponse {
+  repeated string depotPath = 1;
+  repeated string nixStorePath = 2;
+  repeated string corePkgsPath = 4;
+  repeated string otherPath = 3;
+
+  bytes derivation = 5;
+}
+
+message ArchiveRequest {
+  repeated string depotPath = 1;
+}
+
+message ArchiveChunk {
+  bytes chunk = 1;
+}
+
+service DepotScanService {
+  rpc Scan(ScanRequest) returns (ScanResponse);
+
+  rpc MakeArchive(ArchiveRequest) returns (stream ArchiveChunk);
+}
+
diff --git a/tools/depot-scanner/go.mod b/tools/depot-scanner/go.mod
new file mode 100644
index 0000000000..bdd22fc1ef
--- /dev/null
+++ b/tools/depot-scanner/go.mod
@@ -0,0 +1,3 @@
+module code.tvl.fyi/tools/depot-scanner
+
+go 1.14
diff --git a/tools/depot-scanner/main.go b/tools/depot-scanner/main.go
new file mode 100644
index 0000000000..9171587be2
--- /dev/null
+++ b/tools/depot-scanner/main.go
@@ -0,0 +1,227 @@
+package main
+
+import (
+	"bufio"
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"strings"
+
+	pb "code.tvl.fyi/tools/depot-scanner/proto"
+)
+
+var nixInstantiatePath = flag.String("nix-bin", "/run/current-system/sw/bin/nix-instantiate", "path to nix-instantiate")
+var depotRoot = flag.String("depot", envOr("DEPOT_ROOT", "/depot/"), "path to tvl.fyi depot at current canon")
+var nixStoreRoot = flag.String("store-path", "/nix/store/", "prefix for all valid nix store paths")
+
+var modeFlag = flag.String("mode", modeArchive, "operation mode. valid values: tar, print")
+var onlyFlag = flag.String("only", "", "only enable the listed output types, comma separated. valid values: DEPOT, STORE, CORE, UNKNOWN")
+var relativeFlag = flag.Bool("relpath", false, "when printing paths, print them relative to the root of their path type")
+
+const (
+	modeArchive = "tar"
+	modePrint   = "print"
+)
+
+const (
+	// String that identifies a path as belonging to nix corepkgs.
+	corePkgsString = "/share/nix/corepkgs/"
+
+	depotTraceString = "trace: depot-scan: "
+)
+
+type fileScanType int
+
+const (
+	unknownPath fileScanType = iota
+	depotPath
+	nixStorePath
+	corePkgsPath
+)
+
+func launchNix(attr string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) {
+	cmd := exec.Command(*nixInstantiatePath, "--trace-file-access", "-A", attr)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		stdout.Close()
+		return nil, nil, nil, err
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		stdout.Close()
+		stderr.Close()
+		return nil, nil, nil, err
+	}
+
+	return cmd, stdout, stderr, nil
+}
+
+func categorizePath(path string) fileScanType {
+	if strings.HasPrefix(path, *nixStoreRoot) {
+		if strings.Contains(path, corePkgsString) {
+			return corePkgsPath
+		}
+		return nixStorePath
+	} else if strings.HasPrefix(path, *depotRoot) {
+		return depotPath
+	} else if strings.Contains(path, corePkgsString) {
+		return corePkgsPath
+	}
+	return unknownPath
+}
+
+func addPath(path string, out map[fileScanType]map[string]struct{}) {
+	cat := categorizePath(path)
+	if out[cat] == nil {
+		out[cat] = make(map[string]struct{})
+	}
+
+	out[cat][path] = struct{}{}
+}
+
+func consumeOutput(stdout, stderr io.ReadCloser) (map[fileScanType]map[string]struct{}, string, error) {
+	result := make(map[fileScanType]map[string]struct{})
+
+	scanner := bufio.NewScanner(stderr)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.HasPrefix(line, depotTraceString) {
+			addPath(strings.TrimPrefix(line, depotTraceString), result)
+		} else {
+			// print remaining stderr output of nix-instantiate
+			// to prevent silent swallowing of possible important
+			// error messages (e.g. about command line interface changes)
+			fmt.Fprintf(os.Stderr, "nix-inst> %s\n", line)
+		}
+	}
+	if scanner.Err() != nil {
+		return nil, "", scanner.Err()
+	}
+
+	// Get derivation path
+	derivPath := ""
+	scanner = bufio.NewScanner(stdout)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.HasPrefix(line, *nixStoreRoot) {
+			derivPath = line
+			// consume the rest of the output
+		}
+	}
+	if scanner.Err() != nil {
+		return nil, "", scanner.Err()
+	}
+
+	return result, derivPath, nil
+}
+
+func main() {
+	flag.Parse()
+
+	checkDepotRoot()
+
+	enabledPathTypes := make(map[pb.PathType]bool, 4)
+	if len(*onlyFlag) > 0 {
+		enabledOutputs := strings.Split(*onlyFlag, ",")
+		for _, v := range enabledOutputs {
+			i, ok := pb.PathType_value[strings.ToUpper(v)]
+			if !ok {
+				fmt.Fprintln(os.Stderr, "warning: unrecognized PathType name: ", v)
+				continue
+			}
+			enabledPathTypes[pb.PathType(i)] = true
+		}
+	} else {
+		// Default
+		enabledPathTypes = map[pb.PathType]bool{
+			pb.PathType_UNKNOWN: true,
+			pb.PathType_DEPOT:   true,
+			pb.PathType_STORE:   true,
+			pb.PathType_CORE:    true,
+		}
+	}
+
+	cmd, stdout, stderr, err := launchNix(flag.Arg(0))
+	if err != nil {
+		panic(fmt.Errorf("could not launch nix: %w", err))
+	}
+	results, derivPath, err := consumeOutput(stdout, stderr)
+	if err != nil {
+		err2 := cmd.Wait()
+		if err2 != nil {
+			panic(fmt.Errorf("nix-instantiate failed: %w\nadditionally, while reading output: %w", err2, err))
+		}
+		panic(fmt.Errorf("problem reading nix output: %w", err))
+	}
+	err = cmd.Wait()
+	if err != nil {
+		panic(fmt.Errorf("nix-instantiate failed: %w", err))
+	}
+
+	_ = derivPath
+
+	if *modeFlag == "print" {
+		if enabledPathTypes[pb.PathType_STORE] {
+			for k, _ := range results[nixStorePath] {
+				if *relativeFlag {
+					k = strings.TrimPrefix(k, *nixStoreRoot)
+					k = strings.TrimPrefix(k, "/")
+				}
+				fmt.Println(k)
+			}
+		}
+		if enabledPathTypes[pb.PathType_DEPOT] {
+			for k, _ := range results[depotPath] {
+				if *relativeFlag {
+					k = strings.TrimPrefix(k, *depotRoot)
+					k = strings.TrimPrefix(k, "/")
+				}
+				fmt.Println(k)
+			}
+		}
+		if enabledPathTypes[pb.PathType_CORE] {
+			for k, _ := range results[corePkgsPath] {
+				// TODO relativeFlag
+				fmt.Println(k)
+			}
+		}
+		if enabledPathTypes[pb.PathType_UNKNOWN] {
+			for k, _ := range results[unknownPath] {
+				fmt.Println(k)
+			}
+		}
+	} else {
+		panic("unimplemented")
+	}
+}
+
+func envOr(envVar, def string) string {
+	v := os.Getenv(envVar)
+	if v == "" {
+		return def
+	}
+	return v
+}
+
+func checkDepotRoot() {
+	if *depotRoot == "" {
+		fmt.Fprintln(os.Stderr, "error: DEPOT_ROOT / -depot not set")
+		os.Exit(2)
+	}
+	_, err := os.Stat(*depotRoot)
+	if os.IsNotExist(err) {
+		fmt.Fprintf(os.Stderr, "error: %q does not exist\ndid you forget to set DEPOT_ROOT / --depot ?\n", *depotRoot)
+		os.Exit(1)
+	} else if err != nil {
+		fmt.Fprintf(os.Stderr, "error: could not stat %q: %v\n", *depotRoot, err)
+		os.Exit(1)
+	}
+
+}
diff --git a/tools/depotfmt.nix b/tools/depotfmt.nix
new file mode 100644
index 0000000000..dbd3a31a0d
--- /dev/null
+++ b/tools/depotfmt.nix
@@ -0,0 +1,60 @@
+# Builds treefmt for depot, with a hardcoded configuration that
+# includes the right paths to formatters.
+{ depot, pkgs, ... }:
+
+let
+  # terraform fmt can't handle multiple paths at once, but treefmt
+  # expects this
+  terraformat = pkgs.writeShellScript "terraformat" ''
+    echo "$@" | xargs -n1 ${pkgs.terraform}/bin/terraform fmt
+  '';
+
+  config = pkgs.writeText "depot-treefmt-config" ''
+    [formatter.go]
+    command = "${pkgs.go}/bin/gofmt"
+    options = [ "-w" ]
+    includes = ["*.go"]
+
+    [formatter.tf]
+    command = "${terraformat}"
+    includes = [ "*.tf" ]
+
+    [formatter.nix]
+    command = "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt"
+    includes = [ "*.nix" ]
+    excludes = [
+      "third_party/nix/tests/*",
+      "third_party/nix/src/tests/*"
+    ]
+
+    [formatter.rust]
+    command = "${pkgs.rustfmt}/bin/rustfmt"
+    includes = [ "*.rs" ]
+    excludes = [
+      "users/tazjin/*",
+    ]
+  '';
+
+  # helper tool for formatting the depot interactively
+  depotfmt = pkgs.writeShellScriptBin "depotfmt" ''
+    exec ${pkgs.treefmt}/bin/treefmt ''${@} \
+      --config-file ${config} \
+      --tree-root $(${pkgs.git}/bin/git rev-parse --show-toplevel)
+  '';
+
+  # wrapper script for running formatting checks in CI
+  check = pkgs.writeShellScript "depotfmt-check" ''
+    ${pkgs.treefmt}/bin/treefmt \
+      --clear-cache \
+      --fail-on-change \
+      --config-file ${config} \
+      --tree-root .
+  '';
+in
+depotfmt.overrideAttrs (_: {
+  passthru.meta.ci.extraSteps.check = {
+    label = "depot formatting check";
+    command = check;
+    alwaysRun = true;
+  };
+})
diff --git a/tools/emacs-pkgs/buildEmacsPackage.nix b/tools/emacs-pkgs/buildEmacsPackage.nix
new file mode 100644
index 0000000000..990b53b763
--- /dev/null
+++ b/tools/emacs-pkgs/buildEmacsPackage.nix
@@ -0,0 +1,38 @@
+# Builder for depot-internal Emacs packages. Packages built using this
+# builder are added into the Emacs packages fixpoint under
+# `emacsPackages.tvlPackages`, which in turn makes it possible to use
+# them with special Emacs features like native compilation.
+#
+# Arguments passed to the builder are the same as
+# emacsPackages.trivialBuild, except:
+#
+# * packageRequires is not used
+#
+# * externalRequires takes a selection function for packages from
+#   emacsPackages
+#
+# * internalRequires takes other depot packages
+{ pkgs, ... }:
+
+buildArgs:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+
+  let
+    # Select external dependencies from the emacsPackages set
+    externalDeps = (buildArgs.externalRequires or (_: [ ])) emacsPackages;
+
+    # Override emacsPackages for depot-internal packages
+    internalDeps = map (p: p.override { inherit emacsPackages; })
+      (buildArgs.internalRequires or [ ]);
+
+    trivialBuildArgs = builtins.removeAttrs buildArgs [
+      "externalRequires"
+      "internalRequires"
+    ] // {
+      packageRequires = externalDeps ++ internalDeps;
+    };
+  in
+  emacsPackages.trivialBuild trivialBuildArgs)
+{ }
diff --git a/tools/emacs-pkgs/defzone/defzone.el b/tools/emacs-pkgs/defzone/defzone.el
new file mode 100644
index 0000000000..ffd359e5ff
--- /dev/null
+++ b/tools/emacs-pkgs/defzone/defzone.el
@@ -0,0 +1,60 @@
+;;; defzone.el --- Generate zone files from Elisp  -*- lexical-binding: t; -*-
+
+(require 'dash)
+(require 'dash-functional)
+(require 's)
+
+(defun record-to-record (zone record &optional subdomain)
+  "Evaluate a record definition and turn it into a zone file
+  record in ZONE, optionally prefixed with SUBDOMAIN."
+
+  (cl-labels ((plist->alist (plist)
+                            (when plist
+                              (cons
+                               (cons (car plist) (cadr plist))
+                               (plist->alist (cddr plist))))))
+    (let ((name (if subdomain (s-join "." (list subdomain zone)) zone)))
+      (pcase record
+        ;; SOA RDATA (RFC 1035; 3.3.13)
+        ((and `(SOA . (,ttl . ,keys))
+              (let (map (:mname mname) (:rname rname) (:serial serial)
+                        (:refresh refresh) (:retry retry) (:expire expire)
+                        (:minimum min))
+                (plist->alist keys)))
+         (if-let ((missing (-filter #'null (not (list mname rname serial
+                                                      refresh retry expire min)))))
+             (error "Missing fields in SOA record: %s" missing)
+             (format "%s %s IN SOA %s %s %s %s %s %s %s"
+                     name ttl mname rname serial refresh retry expire min)))
+
+        (`(NS . (,ttl . ,targets))
+         (->> targets
+              (-map (lambda (target) (format "%s %s IN NS %s" name ttl target)))
+              (s-join "\n")))
+
+        (`(MX . (,ttl . ,pairs))
+         (->> pairs
+              (-map (-lambda ((preference . exchange))
+                      (format "%s %s IN MX %s %s" name ttl preference exchange)))
+              (s-join "\n")))
+
+        (`(TXT ,ttl ,text) (format "%s %s IN TXT %s" name ttl (prin1-to-string text)))
+
+        (`(A . (,ttl . ,ips))
+         (->> ips
+              (-map (lambda (ip) (format "%s %s IN A %s" name ttl ip)))
+              (s-join "\n")))
+
+        (`(CNAME ,ttl ,target) (format "%s %s IN CNAME %s" name ttl target))
+
+        ((and `(,sub . ,records)
+              (guard (stringp sub)))
+         (s-join "\n" (-map (lambda (r) (record-to-record zone r sub)) records)))
+
+        (_ (error "Invalid record definition: %s" record))))))
+
+(defmacro defzone (fqdn &rest records)
+  "Generate zone file for the zone at FQDN from a simple DSL."
+  (declare (indent defun))
+
+  `(s-join "\n" (-map (lambda (r) (record-to-record ,fqdn r)) (quote ,records))))
diff --git a/tools/emacs-pkgs/defzone/example.el b/tools/emacs-pkgs/defzone/example.el
new file mode 100644
index 0000000000..e9c86d25ee
--- /dev/null
+++ b/tools/emacs-pkgs/defzone/example.el
@@ -0,0 +1,45 @@
+;;; example.el - usage example for defzone macro
+
+(defzone "tazj.in."
+  (SOA 21600
+       :mname "ns-cloud-a1.googledomains.com."
+       :rname "cloud-dns-hostmaster.google.com."
+       :serial 123
+       :refresh 21600
+       :retry 3600
+       :expire 1209600
+       :minimum 300)
+
+  (NS 21600
+      "ns-cloud-a1.googledomains.com."
+      "ns-cloud-a2.googledomains.com."
+      "ns-cloud-a3.googledomains.com."
+      "ns-cloud-a4.googledomains.com.")
+
+  (MX 300
+      (1  . "aspmx.l.google.com.")
+      (5  . "alt1.aspmx.l.google.com.")
+      (5  . "alt2.aspmx.l.google.com.")
+      (10 . "alt3.aspmx.l.google.com.")
+      (10 . "alt4.aspmx.l.google.com."))
+
+  (TXT 3600 "google-site-verification=d3_MI1OwD6q2OT42Vvh0I9w2u3Q5KFBu-PieNUE1Fig")
+
+  (A 300 "34.98.120.189")
+
+  ;; Nested record sets are indicated by a list that starts with a
+  ;; string (this is just joined, so you can nest multiple levels at
+  ;; once)
+  ("blog"
+   ;; Blog "storage engine" is in a separate DNS zone
+   (NS 21600
+       "ns-cloud-c1.googledomains.com."
+       "ns-cloud-c2.googledomains.com."
+       "ns-cloud-c3.googledomains.com."
+       "ns-cloud-c4.googledomains.com."))
+
+  ("git"
+   (A 300 "34.98.120.189")
+   (TXT 300 "<3 edef"))
+
+  ("files" (CNAME 300 "c.storage.googleapis.com.")))
diff --git a/tools/emacs-pkgs/dottime/default.nix b/tools/emacs-pkgs/dottime/default.nix
new file mode 100644
index 0000000000..b819e9c14d
--- /dev/null
+++ b/tools/emacs-pkgs/dottime/default.nix
@@ -0,0 +1,7 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage {
+  pname = "dottime";
+  version = "1.0";
+  src = ./dottime.el;
+}
diff --git a/tools/emacs-pkgs/dottime/dottime.el b/tools/emacs-pkgs/dottime/dottime.el
new file mode 100644
index 0000000000..2446f6488f
--- /dev/null
+++ b/tools/emacs-pkgs/dottime/dottime.el
@@ -0,0 +1,81 @@
+;;; dottime.el --- use dottime in the modeline
+;;
+;; Copyright (C) 2019 Google Inc.
+;;
+;; Author: Vincent Ambo <tazjin@google.com>
+;; Version: 1.0
+;; Package-Requires: (cl-lib)
+;;
+;;; Commentary:
+;;
+;; This package changes the display of time in the modeline to use
+;; dottime (see https://dotti.me/) instead of the standard time
+;; display.
+;;
+;; Modeline dottime display is enabled by calling
+;; `dottime-display-mode' and dottime can be used in Lisp code via
+;; `dottime-format'.
+
+(require 'cl-lib)
+(require 'time)
+
+(defun dottime--format-string (&optional offset prefix)
+  "Creates the dottime format string for `format-time-string'
+  based on the local timezone."
+
+  (let* ((offset-sec (or offset (car (current-time-zone))))
+         (offset-hours (/ offset-sec 60 60))
+         (base (concat prefix "%m-%dT%H·%M")))
+    (if (/= offset-hours 0)
+        (concat base (format "%0+3d" offset-hours))
+      base)))
+
+(defun dottime--display-time-update-advice (orig)
+  "Function used as advice to `display-time-update' with a
+  rebound definition of `format-time-string' that renders all
+  timestamps as dottime."
+
+  (cl-letf* ((format-orig (symbol-function 'format-time-string))
+             ((symbol-function 'format-time-string)
+              (lambda (&rest _)
+                (funcall format-orig (dottime--format-string) nil t))))
+    (funcall orig)))
+
+(defun dottime-format (&optional time offset prefix)
+  "Format the given TIME in dottime at OFFSET. If TIME is nil,
+  the current time will be used. PREFIX is prefixed to the format
+  string verbatim.
+
+  OFFSET can be an integer representing an offset in seconds, or
+  the argument can be elided in which case the system time zone
+  is used."
+
+  (format-time-string (dottime--format-string offset prefix) time t))
+
+(defun dottime-display-mode (arg)
+  "Enable time display as dottime. Disables dottime if called
+  with prefix 0 or nil."
+
+  (interactive "p")
+  (if (or (eq arg 0) (eq arg nil))
+      (advice-remove 'display-time-update #'dottime--display-time-update-advice)
+    (advice-add 'display-time-update :around #'dottime--display-time-update-advice))
+  (display-time-update)
+
+  ;; Amend the time display in telega.el to use dottime.
+  ;;
+  ;; This will never display offsets in the chat window, as those are
+  ;; always visible in the modeline anyways.
+  (when (featurep 'telega)
+    (defun telega-ins--dottime-advice (orig timestamp)
+      (let* ((dtime (decode-time timestamp t))
+             (current-ts (time-to-seconds (current-time)))
+             (ctime (decode-time current-ts))
+             (today00 (telega--time-at00 current-ts ctime)))
+        (if (> timestamp today00)
+            (telega-ins (format "%02d·%02d" (nth 2 dtime) (nth 1 dtime)))
+          (funcall orig timestamp))))
+
+    (advice-add 'telega-ins--date :around #'telega-ins--dottime-advice)))
+
+(provide 'dottime)
diff --git a/tools/emacs-pkgs/nix-util/default.nix b/tools/emacs-pkgs/nix-util/default.nix
new file mode 100644
index 0000000000..b167cb9642
--- /dev/null
+++ b/tools/emacs-pkgs/nix-util/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage {
+  pname = "nix-util";
+  version = "1.0";
+  src = ./nix-util.el;
+  externalRequires = epkgs: [ epkgs.s ];
+}
diff --git a/tools/emacs-pkgs/nix-util/nix-util.el b/tools/emacs-pkgs/nix-util/nix-util.el
new file mode 100644
index 0000000000..4ddc81f563
--- /dev/null
+++ b/tools/emacs-pkgs/nix-util/nix-util.el
@@ -0,0 +1,69 @@
+;;; nix-util.el --- Utilities for dealing with Nix code. -*- lexical-binding: t; -*-
+;;
+;; Copyright (C) 2019 Google Inc.
+;; Copyright (C) 2022 The TVL Authors
+;;
+;; Author: Vincent Ambo <tazjin@google.com>
+;; Version: 1.0
+;; Package-Requires: (json map s)
+;;
+;;; Commentary:
+;;
+;; This package adds some functionality that I find useful when
+;; working in Nix buffers or programs installed from Nix.
+
+(require 'json)
+(require 'map)
+(require 's)
+
+(defun nix/prefetch-github (owner repo) ; TODO(tazjin): support different branches
+  "Fetch the master branch of a GitHub repository and insert the
+  call to `fetchFromGitHub' at point."
+
+  (interactive "sOwner: \nsRepository: ")
+
+  (let* (;; Keep these vars around for output insertion
+         (point (point))
+         (buffer (current-buffer))
+         (name (concat "github-fetcher/" owner "/" repo))
+         (outbuf (format "*%s*" name))
+         (errbuf (get-buffer-create "*github-fetcher/errors*"))
+         (cleanup (lambda ()
+                    (kill-buffer outbuf)
+                    (kill-buffer errbuf)
+                    (with-current-buffer buffer
+                      (read-only-mode -1))))
+         (prefetch-handler
+          (lambda (_process event)
+            (unwind-protect
+                (pcase event
+                  ("finished\n"
+                   (let* ((json-string (with-current-buffer outbuf
+                                         (buffer-string)))
+                          (result (json-read-from-string json-string)))
+                     (with-current-buffer buffer
+                       (goto-char point)
+                       (map-let (("rev" rev) ("sha256" sha256)) result
+                         (read-only-mode -1)
+                         (insert (format "fetchFromGitHub {
+  owner = \"%s\";
+  repo = \"%s\";
+  rev = \"%s\";
+  sha256 = \"%s\";
+};" owner repo rev sha256))
+                         (indent-region point (point))))))
+                  (_ (with-current-buffer errbuf
+                       (error "Failed to prefetch %s/%s: %s"
+                              owner repo (buffer-string)))))
+              (funcall cleanup)))))
+
+    ;; Fetching happens asynchronously, but we'd like to make sure the
+    ;; point stays in place while that happens.
+    (read-only-mode)
+    (make-process :name name
+                  :buffer outbuf
+                  :command `("nix-prefetch-github" ,owner ,repo)
+                  :stderr errbuf
+                  :sentinel prefetch-handler)))
+
+(provide 'nix-util)
diff --git a/tools/emacs-pkgs/notable/OWNERS b/tools/emacs-pkgs/notable/OWNERS
new file mode 100644
index 0000000000..f7da62ecf7
--- /dev/null
+++ b/tools/emacs-pkgs/notable/OWNERS
@@ -0,0 +1,2 @@
+owners:
+  - tazjin
diff --git a/tools/emacs-pkgs/notable/default.nix b/tools/emacs-pkgs/notable/default.nix
new file mode 100644
index 0000000000..f57b1c66ae
--- /dev/null
+++ b/tools/emacs-pkgs/notable/default.nix
@@ -0,0 +1,17 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage rec {
+  pname = "notable";
+  version = "1.0";
+  src = ./notable.el;
+
+  externalRequires = epkgs: with epkgs; [
+    f
+    ht
+    s
+  ];
+
+  internalRequires = [
+    depot.tools.emacs-pkgs.dottime
+  ];
+}
diff --git a/tools/emacs-pkgs/notable/notable.el b/tools/emacs-pkgs/notable/notable.el
new file mode 100644
index 0000000000..4668dd333c
--- /dev/null
+++ b/tools/emacs-pkgs/notable/notable.el
@@ -0,0 +1,251 @@
+;;; notable.el --- a simple note-taking app -*- lexical-binding: t; -*-
+;;
+;; Copyright (C) 2020 The TVL Contributors
+;;
+;; Author: Vincent Ambo <mail@tazj.in>
+;; Version: 1.0
+;; Package-Requires: (cl-lib dash f rx s subr-x)
+;;
+;;; Commentary:
+;;
+;; This package provides a simple note-taking application which can be
+;; invoked from anywhere in Emacs, with several interactive
+;; note-taking functions included.
+;;
+;; As is tradition for my software, the idea here is to reduce
+;; friction which I see even with tools like `org-capture', because
+;; `org-mode' does a ton of things I don't care about.
+;;
+;; Notable stores its notes in simple JSON files in the folder
+;; specified by `notable-note-dir'.
+
+(require 'cl-lib)
+(require 'dottime)
+(require 'f)
+(require 'ht)
+(require 'rx)
+(require 's)
+(require 'subr-x)
+
+;; User-facing customisation options
+
+(defgroup notable nil
+  "Simple note-taking application."
+  :group 'applications)
+
+;; TODO(tazjin): Use whatever the XDG state dir thing is for these by
+;; default.
+(defcustom notable-note-dir (expand-file-name "~/.notable/")
+  "File path to the directory containing notable's notes."
+  :type 'string
+  :group 'notable)
+
+;; Package internal definitions
+
+(cl-defstruct (notable--note (:constructor notable--make-note))
+  "Structure containing the fields of a single notable note."
+  time    ;; UNIX timestamp at which the note was taken
+  content ;; Textual content of the note
+  )
+
+(defvar notable--note-lock (make-mutex "notable-notes")
+  "Exclusive lock for note operations with shared state.")
+
+(defvar notable--note-regexp
+  (rx "note-"
+      (group (one-or-more (any num)))
+      ".json")
+  "Regular expression to match note file names.")
+
+(defvar notable--next-note
+  (let ((next 0))
+    (dolist (file (f-entries notable-note-dir))
+      (when-let* ((match (string-match notable--note-regexp file))
+                  (id (string-to-number
+                       (match-string 1 file)))
+                  (larger (> id next)))
+        (setq next id)))
+    (+ 1 next))
+  "Next ID to use for notes. Initial value is determined based on
+  the existing notes files.")
+
+(defun notable--serialize-note (note)
+  "Serialise NOTE into JSON format."
+  (check-type note notable--note)
+  (json-serialize (ht ("time" (notable--note-time note))
+                      ("content" (notable--note-content note)))))
+
+(defun notable--deserialize-note (json)
+  "Deserialise JSON into a notable note."
+  (check-type json string)
+  (let ((parsed (json-parse-string json)))
+    (unless (and (ht-contains? parsed "time")
+                 (ht-contains-p parsed "content"))
+      (error "Missing required keys in note structure!"))
+    (notable--make-note :time (ht-get parsed "time")
+                        :content (ht-get parsed "content"))))
+
+(defun notable--next-id ()
+  "Return the next note ID and increment the counter."
+  (with-mutex notable--note-lock
+    (let ((id notable--next-note))
+      (setq notable--next-note (+ 1 id))
+      id)))
+
+(defun notable--note-path (id)
+  (check-type id integer)
+  (f-join notable-note-dir (format "note-%d.json" id)))
+
+(defun notable--archive-path (id)
+  (check-type id integer)
+  (f-join notable-note-dir (format "archive-%d.json" id)))
+
+(defun notable--add-note (content)
+  "Add a note with CONTENT to the note store."
+  (let* ((id (notable--next-id))
+         (note (notable--make-note :time (time-convert nil 'integer)
+                                   :content content))
+         (path (notable--note-path id)))
+    (when (f-exists? path) (error "Note file '%s' already exists!" path))
+    (f-write-text (notable--serialize-note note) 'utf-8 path)
+    (message "Saved note %d" id)))
+
+(defun notable--archive-note (id)
+  "Archive the note with ID."
+  (check-type id integer)
+
+  (unless (f-exists? (notable--note-path id))
+    (error "There is no note with ID %d." id))
+
+  (when (f-exists? (notable--archive-path id))
+    (error "Oh no, a note with ID %d has already been archived!" id))
+
+  (f-move (notable--note-path id) (notable--archive-path id))
+  (message "Archived note with ID %d." id))
+
+(defun notable--list-note-ids ()
+  "List all note IDs (not contents) from `notable-note-dir'"
+  (cl-loop for file in (f-entries notable-note-dir)
+           with res = nil
+           if (string-match notable--note-regexp file)
+           do (push (string-to-number (match-string 1 file)) res)
+           finally return res))
+
+(defun notable--get-note (id)
+  (let ((path (notable--note-path id)))
+    (unless (f-exists? path)
+      (error "No note with ID %s in note storage!" id))
+    (notable--deserialize-note (f-read-text path 'utf-8))))
+
+;; Note view buffer implementation
+
+(defvar-local notable--buffer-note nil "The note ID displayed by this buffer.")
+
+(define-derived-mode notable-note-mode fundamental-mode "notable-note"
+  "Major mode displaying a single Notable note."
+  (set (make-local-variable 'scroll-preserve-screen-position) t)
+  (setq truncate-lines t)
+  (setq buffer-read-only t)
+  (setq buffer-undo-list t))
+
+(setq notable-note-mode-map
+      (let ((map (make-sparse-keymap)))
+        (define-key map "q" 'kill-current-buffer)
+        map))
+
+(defun notable--show-note (id)
+  "Display a single note in a separate buffer."
+  (check-type id integer)
+
+  (let ((note (notable--get-note id))
+        (buffer (get-buffer-create (format "*notable: %d*" id)))
+        (inhibit-read-only t))
+    (with-current-buffer buffer
+      (notable-note-mode)
+      (erase-buffer)
+      (setq notable--buffer-note id)
+      (setq header-line-format
+            (format "Note from %s"
+                    (dottime-format
+                     (seconds-to-time (notable--note-time note))))))
+    (switch-to-buffer buffer)
+    (goto-char (point-min))
+    (insert (notable--note-content note))))
+
+(defun notable--show-note-at-point ()
+  (interactive)
+  (notable--show-note (get-text-property (point) 'notable-note-id)))
+
+(defun notable--archive-note-at-point ()
+  (interactive)
+  (notable--archive-note (get-text-property (point) 'notable-note-id)))
+
+;; Note list buffer implementation
+
+(define-derived-mode notable-list-mode fundamental-mode "notable"
+  "Major mode displaying the Notable note list."
+  ;; TODO(tazjin): `imenu' functions?
+
+  (set (make-local-variable 'scroll-preserve-screen-position) t)
+  (setq truncate-lines t)
+  (setq buffer-read-only t)
+  (setq buffer-undo-list t)
+  (hl-line-mode t))
+
+(setq notable-list-mode-map
+      (let ((map (make-sparse-keymap)))
+        (define-key map "a" 'notable--archive-note-at-point)
+        (define-key map "q" 'kill-current-buffer)
+        (define-key map "g" 'notable-list-notes)
+        (define-key map (kbd "RET") 'notable--show-note-at-point)
+        map))
+
+(defun notable--render-note (id note)
+  (check-type id integer)
+  (check-type note notable--note)
+
+  (let* ((start (point))
+         (date (dottime-format (seconds-to-time
+                                (notable--note-time note))))
+         (first-line (truncate-string-to-width
+                      (car (s-lines (notable--note-content note)))
+                      ;; Length of the window, minus the date prefix:
+                      (- (window-width) (+ 2 (length date)))
+                      nil nil 1)))
+    (insert (propertize (s-concat date "  " first-line)
+                        'notable-note-id id))
+    (insert "\n")))
+
+(defun notable--render-notes (notes)
+  "Retrieve each note in NOTES by ID and insert its contents into
+the list buffer.
+
+For larger notes only the first line is displayed."
+  (dolist (id notes)
+    (notable--render-note id (notable--get-note id))))
+
+;; User-facing functions
+
+(defun notable-take-note (content)
+  "Interactively prompt the user for a note that should be stored
+in Notable."
+  (interactive "sEnter note: ")
+  (check-type content string)
+  (notable--add-note content))
+
+(defun notable-list-notes ()
+  "Open a buffer listing all active notes."
+  (interactive)
+
+  (let ((buffer (get-buffer-create "*notable*"))
+        (notes (notable--list-note-ids))
+        (inhibit-read-only t))
+    (with-current-buffer buffer
+      (notable-list-mode)
+      (erase-buffer)
+      (setq header-line-format "Notable notes"))
+    (switch-to-buffer buffer)
+    (goto-char (point-min))
+    (notable--render-notes notes)))
+
+(provide 'notable)
diff --git a/tools/emacs-pkgs/passively/OWNERS b/tools/emacs-pkgs/passively/OWNERS
new file mode 100644
index 0000000000..56853aed59
--- /dev/null
+++ b/tools/emacs-pkgs/passively/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - tazjin
diff --git a/tools/emacs-pkgs/passively/README.md b/tools/emacs-pkgs/passively/README.md
new file mode 100644
index 0000000000..052c496b32
--- /dev/null
+++ b/tools/emacs-pkgs/passively/README.md
@@ -0,0 +1,76 @@
+<!-- SPDX-License-Identifier: MIT -->
+passively
+=========
+
+Passively is an Emacs Lisp library for passively learning new
+information in an Emacs instance.
+
+Passively works by displaying a random piece of information to be
+learned in the Emacs echoline whenever Emacs is idle for a set amount
+of time.
+
+It was designed to aid in language acquisition by passively displaying
+new vocabulary to learn.
+
+Passively is configured with a corpus of information (a hash table
+mapping string keys to string values) and maintains a set of terms
+that the user already learned in a file on disk.
+
+## Configuration & usage
+
+Configure passively like this:
+
+```lisp
+;; Configure the terms to learn. Each term should have a key and a
+;; string value which is displayed.
+(setq passively-learn-terms
+      (ht ("забыть" "забыть - to forget")
+          ("действительно" "действительно - indeed, really")))
+
+;; Configure a file in which passively should store its state
+;; (defaults to $user-emacs-directory/passively.el)
+(setq passively-store-state "/persist/tazjin/passively.el")
+
+;; Configure after how many seconds of idle time passively should
+;; display a new piece of information.
+;; (defaults to 4 seconds)
+(setq passively-show-after-idle-for 5)
+
+;; Once this configuration has been set up, start passively:
+(passively-enable)
+
+;; Or, if it annoys you, disable it again:
+(passively-disable)
+```
+
+These variables are registered with `customize` and may be customised
+through its interface.
+
+### Known terms
+
+Passively exposes the interactive function
+`passively-mark-last-as-known` which marks the previously displayed
+term as known. This means that it will not be included in the random
+selection anymore.
+
+### Last term
+
+Passively stores the key of the last known term in
+`passively-last-displayed`.
+
+## Installation
+
+Inside of the TVL depot, you can install passively from
+`pkgs.emacsPackages.tvlPackages.passively`. Outside of the depot, you
+can clone passively like this:
+
+    git clone https://code.tvl.fyi/depot.git:/tools/emacs-pkgs/passively.git
+
+Passively depends on `ht.el`.
+
+Feel free to contribute patches by emailing them to `depot@tazj.in`
+
+## Use-cases
+
+I'm using passively to learn Russian vocabulary. Once I've cleaned up
+my configuration for that, my Russian term list will be linked here.
diff --git a/tools/emacs-pkgs/passively/default.nix b/tools/emacs-pkgs/passively/default.nix
new file mode 100644
index 0000000000..ec59cc85fd
--- /dev/null
+++ b/tools/emacs-pkgs/passively/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage {
+  pname = "passively";
+  version = "1.0";
+  src = ./passively.el;
+  externalRequires = (epkgs: with epkgs; [ ht ]);
+}
diff --git a/tools/emacs-pkgs/passively/passively.el b/tools/emacs-pkgs/passively/passively.el
new file mode 100644
index 0000000000..0d871f26ad
--- /dev/null
+++ b/tools/emacs-pkgs/passively/passively.el
@@ -0,0 +1,121 @@
+;;; passively.el --- Passively learn new information -*- lexical-binding: t; -*-
+;;
+;; SPDX-License-Identifier: MIT
+;; Copyright (C) 2020 The TVL Contributors
+;;
+;; Author: Vincent Ambo <tazjin@tvl.su>
+;; Version: 1.0
+;; Package-Requires: (ht seq)
+;; URL: https://code.tvl.fyi/about/tools/emacs-pkgs/passively/
+;;
+;; This file is not part of GNU Emacs.
+
+(require 'ht)
+(require 'seq)
+
+;; Customisation options
+
+(defgroup passively nil
+  "Customisation options for passively"
+  :group 'applications)
+
+(defcustom passively-learn-terms nil
+  "Terms that passively should randomly display to the user. The
+format of this variable is a hash table with a string key that
+uniquely identifies the term, and a string value that is
+displayed to the user.
+
+For example, a possible value could be:
+
+   (ht (\"забыть\" \"забыть - to forget\")
+       (\"действительно\" \"действительно - indeed, really\")))
+"
+  ;; TODO(tazjin): No hash-table type in customization.el?
+  :type '(sexp)
+  :group 'passively)
+
+(defcustom passively-store-state (format "%spassively.el" user-emacs-directory)
+  "File in which passively should store its state (e.g. known terms)"
+  :type '(file)
+  :group 'passively)
+
+(defcustom passively-show-after-idle-for 4
+  "Number of seconds after Emacs goes idle that passively should
+wait before displaying a term."
+  :type '(integer)
+  :group 'passively)
+
+;; Implementation of state persistence
+(defvar passively-last-displayed nil
+  "Key of the last displayed passively term.")
+
+(defvar passively--known-terms (make-hash-table)
+  "Set of terms that are already known.")
+
+(defun passively--persist-known-terms ()
+  "Persist the set of known passively terms to disk."
+  (with-temp-file passively-store-state
+    (insert (prin1-to-string (ht-keys passively--known-terms)))))
+
+(defun passively--load-known-terms ()
+  "Load the set of known passively terms from disk."
+  (with-temp-buffer
+    (insert-file-contents passively-store-state)
+    (let ((keys (read (current-buffer))))
+      (setq passively--known-terms (make-hash-table))
+      (seq-do
+       (lambda (key) (ht-set passively--known-terms key t))
+       keys)))
+  (message "passively: loaded %d known words"
+           (seq-length (ht-keys passively--known-terms))))
+
+(defun passively-mark-last-as-known ()
+  "Mark the last term that passively displayed as known. It will
+not be displayed again."
+  (interactive)
+
+  (ht-set passively--known-terms passively-last-displayed t)
+  (passively--persist-known-terms)
+  (message "passively: Marked '%s' as known" passively-last-displayed))
+
+;; Implementation of main display logic
+(defvar passively--display-timer nil
+  "idle-timer used for displaying terms by passively")
+
+(defun passively--random-term (timeout)
+  ;; This is stupid, calculate set intersections instead.
+  (if (< 1000 timeout)
+      (error "It seems you already know all the terms?")
+    (seq-random-elt (ht-keys passively-learn-terms))))
+
+(defun passively--display-random-term ()
+  (let* ((timeout 1)
+         (term (passively--random-term timeout)))
+    (while (ht-contains? passively--known-terms term)
+      (setq timeout (+ 1 timeout))
+      (setq term (passively--random-term timeout)))
+    (setq passively-last-displayed term)
+    (message (ht-get passively-learn-terms term))))
+
+(defun passively-enable ()
+  "Enable automatic display of terms via passively."
+  (interactive)
+  (if passively--display-timer
+      (error "passively: Already running!")
+    (passively--load-known-terms)
+    (setq passively--display-timer
+          (run-with-idle-timer passively-show-after-idle-for t
+                               #'passively--display-random-term))
+    (message "passively: Now running after %s seconds of idle time"
+             passively-show-after-idle-for)))
+
+(defun passively-disable ()
+  "Turn off automatic display of terms via passively."
+  (interactive)
+  (unless passively--display-timer
+    (error "passively: Not running!"))
+  (cancel-timer passively--display-timer)
+  (setq passively--display-timer nil)
+  (message "passively: Now disabled"))
+
+(provide 'passively)
diff --git a/tools/emacs-pkgs/term-switcher/default.nix b/tools/emacs-pkgs/term-switcher/default.nix
new file mode 100644
index 0000000000..e775de5cdb
--- /dev/null
+++ b/tools/emacs-pkgs/term-switcher/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage {
+  pname = "term-switcher";
+  version = "1.0";
+  src = ./term-switcher.el;
+  externalRequires = epkgs: with epkgs; [ dash ivy s vterm ];
+}
diff --git a/tools/emacs-pkgs/term-switcher/term-switcher.el b/tools/emacs-pkgs/term-switcher/term-switcher.el
new file mode 100644
index 0000000000..0055f87fd6
--- /dev/null
+++ b/tools/emacs-pkgs/term-switcher/term-switcher.el
@@ -0,0 +1,57 @@
+;;; term-switcher.el --- Easily switch between open vterms
+;;
+;; Copyright (C) 2019 Google Inc.
+;;
+;; Author: Vincent Ambo <tazjin@google.com>
+;; Version: 1.1
+;; Package-Requires: (dash ivy s vterm)
+;;
+;;; Commentary:
+;;
+;; This package adds a function that lets users quickly switch between
+;; different open vterms via ivy.
+
+(require 'dash)
+(require 'ivy)
+(require 's)
+(require 'vterm)
+
+(defgroup term-switcher nil
+  "Customization options `term-switcher'.")
+
+(defcustom term-switcher-buffer-prefix "vterm<"
+  "String prefix for vterm terminal buffers. For example, if you
+  set your titles to match `vterm<...>' a useful prefix might be
+  `vterm<'."
+  :type '(string)
+  :group 'term-switcher)
+
+(defun ts/open-or-create-vterm (buffer-name)
+  "Switch to the buffer with BUFFER-NAME or create a new vterm
+  buffer."
+  (if (equal "New vterm" buffer-name)
+      (vterm)
+    (if-let ((buffer (get-buffer buffer-name)))
+        (switch-to-buffer buffer)
+      (error "Could not find vterm buffer: %s" buffer-name))))
+
+(defun ts/is-vterm-buffer (buffer)
+  "Determine whether BUFFER runs a vterm."
+  (equal 'vterm-mode (buffer-local-value 'major-mode buffer)))
+
+(defun ts/switch-to-terminal ()
+  "Switch to an existing vterm buffer or create a new one."
+
+  (interactive)
+  (let ((terms (-map #'buffer-name
+                     (-filter #'ts/is-vterm-buffer (buffer-list)))))
+    (if terms
+        (ivy-read "Switch to vterm: "
+                  (cons "New vterm" terms)
+                  :caller 'ts/switch-to-terminal
+                  :preselect (s-concat "^" term-switcher-buffer-prefix)
+                  :require-match t
+                  :action #'ts/open-or-create-vterm)
+      (vterm))))
+
+(provide 'term-switcher)
diff --git a/tools/emacs-pkgs/tvl/OWNERS b/tools/emacs-pkgs/tvl/OWNERS
new file mode 100644
index 0000000000..ce7e0e37ee
--- /dev/null
+++ b/tools/emacs-pkgs/tvl/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - grfn
diff --git a/tools/emacs-pkgs/tvl/default.nix b/tools/emacs-pkgs/tvl/default.nix
new file mode 100644
index 0000000000..5dcc184bb5
--- /dev/null
+++ b/tools/emacs-pkgs/tvl/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.tools.emacs-pkgs.buildEmacsPackage {
+  pname = "tvl";
+  version = "1.0";
+  src = ./tvl.el;
+  externalRequires = (epkgs: with epkgs; [ magit s ]);
+}
diff --git a/tools/emacs-pkgs/tvl/tvl.el b/tools/emacs-pkgs/tvl/tvl.el
new file mode 100644
index 0000000000..500ffa1653
--- /dev/null
+++ b/tools/emacs-pkgs/tvl/tvl.el
@@ -0,0 +1,222 @@
+;;; tvl.el --- description -*- lexical-binding: t; -*-
+;;
+;; Copyright (C) 2020 Griffin Smith
+;; Copyright (C) 2020 The TVL Contributors
+;;
+;; Author: Griffin Smith <grfn@gws.fyi>
+;; Version: 0.0.1
+;; Package-Requires: (cl s magit)
+;;
+;; This file is not part of GNU Emacs.
+;;
+;;; Commentary:
+;;
+;; This file provides shared utilities for interacting with the TVL monorepo
+;;
+;;; Code:
+
+(require 'magit)
+(require 's)
+(require 'cl) ; TODO(tazjin): replace lexical-let* with non-deprecated alternative
+
+(defgroup tvl nil
+  "Customisation options for TVL functionality.")
+
+(defcustom tvl-gerrit-remote "origin"
+  "Name of the git remote for gerrit"
+  :type '(string)
+  :group 'tvl)
+
+(defcustom tvl-depot-path "/depot"
+  "Location at which the TVL depot is checked out."
+  :type '(string)
+  :group 'tvl)
+
+(defcustom tvl-target-branch "canon"
+  "Branch to use to target CLs"
+  :group 'tvl
+  :type '(string)
+  :safe (lambda (_) t))
+
+(defun tvl--gerrit-ref (target-branch &optional flags)
+  (let ((flag-suffix (if flags (format "%%%s" (s-join "," flags))
+                       "")))
+    (format "HEAD:refs/for/%s%s" target-branch flag-suffix)))
+
+(transient-define-suffix magit-gerrit-push-for-review ()
+  "Push to Gerrit for review."
+  (interactive)
+  (magit-push-refspecs tvl-gerrit-remote
+                       (tvl--gerrit-ref tvl-target-branch)
+                       nil))
+
+(transient-append-suffix
+  #'magit-push ["r"]
+  (list "R" "push to Gerrit for review" #'magit-gerrit-push-for-review))
+
+(transient-define-suffix magit-gerrit-push-wip ()
+  "Push to Gerrit as a work-in-progress."
+  (interactive)
+  (magit-push-refspecs tvl-gerrit-remote
+                       (tvl--gerrit-ref tvl-target-branch '("wip"))
+                       nil))
+
+(transient-append-suffix
+  #'magit-push ["r"]
+  (list "W" "push to Gerrit as a work-in-progress" #'magit-gerrit-push-wip))
+
+(transient-define-suffix magit-gerrit-push-autosubmit ()
+  "Push to Gerrit with autosubmit enabled."
+  (interactive)
+  (magit-push-refspecs tvl-gerrit-remote
+                       (tvl--gerrit-ref tvl-target-branch '("l=Autosubmit+1"))
+                       nil))
+
+(transient-append-suffix
+  #'magit-push ["r"]
+  (list "A" "push to Gerrit with autosubmit enabled" #'magit-gerrit-push-autosubmit))
+
+(transient-define-suffix magit-gerrit-submit ()
+  "Push to Gerrit for review."
+  (interactive)
+  (magit-push-refspecs tvl-gerrit-remote
+                       (tvl--gerrit-ref tvl-target-branch '("submit"))
+                       nil))
+
+(transient-append-suffix
+  #'magit-push ["r"]
+  (list "S" "push to Gerrit to submit" #'magit-gerrit-submit))
+
+
+(transient-define-suffix magit-gerrit-rubberstamp ()
+  "Push, approve and autosubmit to Gerrit. CLs created via this
+rubberstamp method will automatically be submitted after CI
+passes. This is potentially dangerous, use with care."
+  (interactive)
+  (magit-push-refspecs tvl-gerrit-remote
+                       (tvl--gerrit-ref tvl-target-branch
+                                        '("l=Code-Review+2"
+                                          "l=Autosubmit+1"
+                                          "publish-comments"))
+                       nil))
+
+(transient-append-suffix
+  #'magit-push ["r"]
+  (list "P" "push & rubberstamp to Gerrit" #'magit-gerrit-rubberstamp))
+
+(defvar magit-cl-history nil)
+(defun magit-read-cl (prompt remote)
+  (let* ((refs (prog2 (message "Determining available refs...")
+                   (magit-remote-list-refs remote)
+                 (message "Determining available refs...done")))
+         (change-refs (-filter
+                       (apply-partially #'string-prefix-p "refs/changes/")
+                       refs))
+         (cl-number-to-refs
+          (-group-by
+           (lambda (change-ref)
+             ;; refs/changes/34/1234/1
+             ;; ^    ^       ^  ^    ^
+             ;; 1    2       3  4    5
+             ;;                 ^-- this one
+             (cadddr
+              (split-string change-ref (rx "/"))))
+           change-refs))
+         (cl-numbers
+          (-map
+           (lambda (cl-to-refs)
+             (let ((latest-patchset-ref
+                    (-max-by
+                     (-on #'> (lambda (ref)
+                                (string-to-number
+                                 (nth 4 (split-string ref (rx "/"))))))
+                     (-remove
+                      (apply-partially #'s-ends-with-p "meta")
+                      (cdr cl-to-refs)))))
+               (propertize (car cl-to-refs) 'ref latest-patchset-ref)))
+           cl-number-to-refs)))
+    (get-text-property
+     0
+     'ref
+     (magit-completing-read
+      prompt cl-numbers nil t nil 'magit-cl-history))))
+
+(transient-define-suffix magit-gerrit-checkout (remote cl-refspec)
+  "Prompt for a CL number and checkout the latest patchset of that CL with
+  detached HEAD"
+  (interactive
+   (let* ((remote tvl-gerrit-remote)
+          (cl (magit-read-cl "Checkout CL" remote)))
+     (list remote cl)))
+  (magit-fetch-refspec remote cl-refspec (magit-fetch-arguments))
+  ;; That runs async, so wait for it to finish (this is how magit does it)
+  (while (and magit-this-process
+              (eq (process-status magit-this-process) 'run))
+    (sleep-for 0.005))
+  (magit-checkout "FETCH_HEAD" (magit-branch-arguments))
+  (message "HEAD detached at %s" cl-refspec))
+
+
+(transient-append-suffix
+  #'magit-branch ["l"]
+  (list "g" "gerrit CL" #'magit-gerrit-checkout))
+
+(transient-define-suffix magit-gerrit-cherry-pick (remote cl-refspec)
+  "Prompt for a CL number and cherry-pick the latest patchset of that CL"
+  (interactive
+   (let* ((remote tvl-gerrit-remote)
+          (cl (magit-read-cl "Cherry-pick CL" remote)))
+     (list remote cl)))
+  (magit-fetch-refspec remote cl-refspec (magit-fetch-arguments))
+  ;; That runs async, so wait for it to finish (this is how magit does it)
+  (while (and magit-this-process
+              (eq (process-status magit-this-process) 'run))
+    (sleep-for 0.005))
+  (magit-cherry-copy (list "FETCH_HEAD"))
+  (message "HEAD detached at %s" cl-refspec))
+
+
+(transient-append-suffix
+  #'magit-cherry-pick ["m"]
+  (list "g" "Gerrit CL" #'magit-gerrit-cherry-pick))
+
+(defun tvl-depot-status ()
+  "Open the TVL monorepo in magit."
+  (interactive)
+  (magit-status-setup-buffer tvl-depot-path))
+
+(eval-after-load 'sly
+  '(defun tvl-sly-from-depot (attribute)
+     "Start a Sly REPL configured with a Lisp matching a derivation
+     from the depot.
+
+     The derivation invokes nix.buildLisp.sbclWith and is built
+     asynchronously. The build output is included in the error
+     thrown on build failures."
+
+     (interactive "sAttribute: ")
+     (lexical-let* ((outbuf (get-buffer-create (format "*depot-out/%s*" attribute)))
+                    (errbuf (get-buffer-create (format "*depot-errors/%s*" attribute)))
+                    (expression (format "(import <depot> {}).%s.repl" attribute))
+                    (command (list "nix-build" "--no-out-link" "-I" (format "depot=%s" tvl-depot-path) "-E" expression)))
+       (message "Acquiring Lisp for <depot>.%s" attribute)
+       (make-process :name (format "depot-nix-build/%s" attribute)
+                     :buffer outbuf
+                     :stderr errbuf
+                     :command command
+                     :sentinel
+                     (lambda (process event)
+                       (unwind-protect
+                           (pcase event
+                             ("finished\n"
+                              (let* ((outpath (s-trim (with-current-buffer outbuf (buffer-string))))
+                                     (lisp-path (s-concat outpath "/bin/sbcl")))
+                                (message "Acquired Lisp for <depot>.%s at %s" attribute lisp-path)
+                                (sly lisp-path)))
+                             (_ (with-current-buffer errbuf
+                                  (error "Failed to build '%s':\n%s" attribute (buffer-string)))))
+                         (kill-buffer outbuf)
+                         (kill-buffer errbuf)))))))
+
+(provide 'tvl)
+;;; tvl.el ends here
diff --git a/tools/eprintf.nix b/tools/eprintf.nix
new file mode 100644
index 0000000000..933d73ea71
--- /dev/null
+++ b/tools/eprintf.nix
@@ -0,0 +1,15 @@
+{ depot, pkgs, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.coreutils [ "printf" ];
+
+  # printf(1), but redirect to stderr
+in
+depot.nix.writeExecline "eprintf" { } [
+  "fdmove"
+  "-c"
+  "1"
+  "2"
+  bins.printf
+  "$@"
+]
diff --git a/tools/gerrit-cli.nix b/tools/gerrit-cli.nix
new file mode 100644
index 0000000000..1606155a80
--- /dev/null
+++ b/tools/gerrit-cli.nix
@@ -0,0 +1,13 @@
+# Utility script to run a gerrit command on the depot host via ssh.
+# Reads the username from TVL_USERNAME, or defaults to $(whoami)
+{ pkgs, ... }:
+
+pkgs.writeShellScriptBin "gerrit" ''
+  TVL_USERNAME=''${TVL_USERNAME:-$(whoami)}
+  if which ssh &>/dev/null; then
+    ssh=ssh
+  else
+    ssh="${pkgs.openssh}/bin/ssh"
+  fi
+  exec $ssh $TVL_USERNAME@code.tvl.fyi -p 29418 -- gerrit $@
+''
diff --git a/tools/gerrit-update.nix b/tools/gerrit-update.nix
new file mode 100644
index 0000000000..e4efd89ea5
--- /dev/null
+++ b/tools/gerrit-update.nix
@@ -0,0 +1,34 @@
+# Utility script to perform a Gerrit update.
+{ pkgs, ... }:
+
+pkgs.writeShellScriptBin "gerrit-update" ''
+  set -euo pipefail
+
+  if [[ $EUID -ne 0 ]]; then
+    echo "Oh no! Only root is allowed to update Gerrit!" >&2
+    exit 1
+  fi
+
+  gerrit_war="$(find "${pkgs.gerrit}/webapps" -name 'gerrit*.war')"
+  java="${pkgs.jdk}/bin/java"
+  backup_path="/root/gerrit_preupgrade-$(date +"%Y-%m-%d").tar.bz2"
+
+  # Take a safety backup of Gerrit into /root's homedir. Just in case.
+  echo "Backing up Gerrit to $backup_path"
+  tar -cjf "$backup_path" /var/lib/gerrit
+
+  # Stop Gerrit (and its activation socket).
+  echo "Stopping Gerrit"
+  systemctl stop gerrit.service gerrit.socket
+
+  # Ask Gerrit to do a schema upgrade...
+  echo "Performing schema upgrade"
+  "$java" -jar "$gerrit_war" \
+    init --no-auto-start --batch --skip-plugins --site-path "/var/lib/gerrit"
+
+  # Restart Gerrit.
+  echo "Restarting Gerrit"
+  systemctl start gerrit.socket gerrit.service
+
+  echo "...done"
+''
diff --git a/tools/hash-password.nix b/tools/hash-password.nix
new file mode 100644
index 0000000000..6192436c17
--- /dev/null
+++ b/tools/hash-password.nix
@@ -0,0 +1,15 @@
+# Utility for invoking slappasswd with the correct options for
+# creating an ARGON2 password hash.
+{ pkgs, ... }:
+
+let
+  script = pkgs.writeShellScriptBin "hash-password" ''
+    ${pkgs.openldap}/bin/slappasswd -o module-load=argon2 -h '{ARGON2}' "$@"
+  '';
+in
+script.overrideAttrs (old: {
+  doCheck = true;
+  checkPhase = ''
+    ${pkgs.stdenv.shell} $out/bin/hash-password -s example-password > /dev/null
+  '';
+})
diff --git a/tools/magrathea/default.nix b/tools/magrathea/default.nix
new file mode 100644
index 0000000000..fa0a5d89a1
--- /dev/null
+++ b/tools/magrathea/default.nix
@@ -0,0 +1,23 @@
+# magrathea helps you build planets
+#
+# it is a tool for working with monorepos in the style of tvl's depot
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "magrathea";
+  src = ./.;
+  dontInstall = true;
+
+  nativeBuildInputs = [ pkgs.chicken ];
+  buildInputs = with pkgs.chickenPackages.chickenEggs; [
+    matchable
+    srfi-13
+  ];
+
+  propagatedBuildInputs = [ pkgs.git ];
+
+  buildPhase = ''
+    mkdir -p $out/bin
+    csc -o $out/bin/mg -static ${./mg.scm}
+  '';
+}
diff --git a/tools/magrathea/mg.scm b/tools/magrathea/mg.scm
new file mode 100644
index 0000000000..cbbcfc69dd
--- /dev/null
+++ b/tools/magrathea/mg.scm
@@ -0,0 +1,359 @@
+;; magrathea helps you build planets
+;;
+;; it is a tiny tool designed to ease workflows in monorepos that are
+;; modeled after the tvl depot.
+;;
+;; users familiar with workflows from other, larger monorepos may be
+;; used to having a build tool that can work in any tree location.
+;; magrathea enables this, but with nix-y monorepos.
+
+(import (chicken base)
+        (chicken format)
+        (chicken irregex)
+        (chicken port)
+        (chicken file)
+        (chicken file posix)
+        (chicken process)
+        (chicken process-context)
+        (chicken string)
+        (matchable)
+        (only (chicken io) read-string))
+
+(define usage #<<USAGE
+usage: mg <command> [<target>]
+
+target:
+  a target specification with meaning inside of the repository. can
+  be absolute (starting with //) or relative to the current directory
+  (as long as said directory is inside of the repo). if no target is
+  specified, the current directory's physical target is built.
+
+  for example:
+
+    //tools/magrathea - absolute physical target
+    //foo/bar:baz     - absolute virtual target
+    magrathea         - relative physical target
+    :baz              - relative virtual target
+
+commands:
+  build - build a target
+  shell - enter a shell with the target's build dependencies
+  path  - print source folder for the target
+  run   - build a target and execute its output
+
+file all feedback on b.tvl.fyi
+USAGE
+)
+
+;; parse target definitions. trailing slashes on physical targets are
+;; allowed for shell autocompletion.
+;;
+;; component ::= any string without "/" or ":"
+;;
+;; physical-target ::= <component>
+;;                   | <component> "/"
+;;                   | <component> "/" <physical-target>
+;;
+;; virtual-target ::= ":" <component>
+;;
+;; relative-target ::= <physical-target>
+;;                   | <virtual-target>
+;;                   | <physical-target> <virtual-target>
+;;
+;; root-anchor ::= "//"
+;;
+;; target ::= <relative-target> | <root-anchor> <relative-target>
+
+;; read a path component until it looks like something else is coming
+(define (read-component first port)
+  (let ((keep-reading?
+         (lambda () (not (or (eq? #\/ (peek-char port))
+                             (eq? #\: (peek-char port))
+                             (eof-object? (peek-char port)))))))
+    (let reader ((acc (list first))
+                 (condition (keep-reading?)))
+      (if condition (reader (cons (read-char port) acc) (keep-reading?))
+          (list->string (reverse acc))))))
+
+;; read something that started with a slash. what will it be?
+(define (read-slash port)
+  (if (eq? #\/ (peek-char port))
+      (begin (read-char port)
+             'root-anchor)
+      'path-separator))
+
+;; read any target token and leave port sitting at the next one
+(define (read-token port)
+  (match (read-char port)
+         [#\/ (read-slash port)]
+         [#\: 'virtual-separator]
+         [other (read-component other port)]))
+
+;; read a target into a list of target tokens
+(define (read-target target-str)
+  (call-with-input-string
+   target-str
+   (lambda (port)
+     (let reader ((acc '()))
+       (if (eof-object? (peek-char port))
+           (reverse acc)
+           (reader (cons (read-token port) acc)))))))
+
+(define-record target absolute components virtual)
+(define (empty-target) (make-target #f '() #f))
+
+(define-record-printer (target t out)
+  (fprintf out (conc (if (target-absolute t) "//" "")
+                     (string-intersperse (target-components t) "/")
+                     (if (target-virtual t) ":" "")
+                     (or (target-virtual t) ""))))
+
+;; parse and validate a list of target tokens
+(define parse-tokens
+  (lambda (tokens #!optional (mode 'root) (acc (empty-target)))
+    (match (cons mode tokens)
+           ;; absolute target
+           [('root . ('root-anchor . rest))
+            (begin (target-absolute-set! acc #t)
+                   (parse-tokens rest 'root acc))]
+
+           ;; relative target minus potential garbage
+           [('root . (not ('path-separator . _)))
+            (parse-tokens tokens 'normal acc)]
+
+           ;; virtual target
+           [('normal . ('virtual-separator . rest))
+            (parse-tokens rest 'virtual acc)]
+
+           [('virtual . ((? string? v)))
+            (begin
+              (target-virtual-set! acc v)
+              acc)]
+
+           ;; chomp through all components and separators
+           [('normal . ('path-separator . rest)) (parse-tokens rest 'normal acc)]
+           [('normal . ((? string? component) . rest))
+            (begin (target-components-set!
+                    acc (append (target-components acc) (list component)))
+                   (parse-tokens rest 'normal acc ))]
+
+           ;; nothing more to parse and not in a weird state, all done, yay!
+           [('normal . ()) acc]
+
+           ;; oh no, we ran out of input too early :(
+           [(_ . ()) `(error . ,(format "unexpected end of input while parsing ~s target" mode))]
+
+           ;; something else was invalid :(
+           [_ `(error . ,(format "unexpected ~s while parsing ~s target" (car tokens) mode))])))
+
+(define (parse-target target)
+  (parse-tokens (read-target target)))
+
+;; turn relative targets into absolute targets based on the current
+;; directory
+(define (normalise-target t)
+  (when (not (target-absolute t))
+    (target-components-set! t (append (relative-repo-path)
+                                      (target-components t)))
+    (target-absolute-set! t #t))
+  t)
+
+;; nix doesn't care about the distinction between physical and virtual
+;; targets, normalise it away
+(define (normalised-components t)
+  (if (target-virtual t)
+      (append (target-components t) (list (target-virtual t)))
+      (target-components t)))
+
+;; return the current repository root as a string
+(define mg--repository-root #f)
+(define (repository-root)
+  (or mg--repository-root
+      (begin
+        (set! mg--repository-root
+              (or (get-environment-variable "MG_ROOT")
+                  (string-chomp
+                   (call-with-input-pipe "git rev-parse --show-toplevel"
+                                         (lambda (p) (read-string #f p))))))
+        mg--repository-root)))
+
+;; determine the current path relative to the root of the repository
+;; and return it as a list of path components.
+(define (relative-repo-path)
+  (string-split
+   (substring (current-directory) (string-length (repository-root))) "/"))
+
+;; escape a string for interpolation in nix code
+(define (nix-escape str)
+  (string-translate* str '(("\"" . "\\\"")
+                           ("${" . "\\${"))))
+
+;; create a nix expression to build the attribute at the specified
+;; components
+;;
+;; an empty target will build the current folder instead.
+;;
+;; this uses builtins.getAttr explicitly to avoid problems with
+;; escaping.
+(define (nix-expr-for target)
+  (let nest ((parts (normalised-components (normalise-target target)))
+             (acc (conc "(import " (repository-root) " {})")))
+    (match parts
+           [() (conc "with builtins; " acc)]
+           [_ (nest (cdr parts)
+                    (conc "(getAttr \""
+                          (nix-escape (car parts))
+                          "\" " acc ")"))])))
+
+;; exit and complain at the user if something went wrong
+(define (mg-error message)
+  (format (current-error-port) "[mg] error: ~A~%" message)
+  (exit 1))
+
+(define (guarantee-success value)
+  (match value
+         [('error . message) (mg-error message)]
+         [_ value]))
+
+(define-record build-args target passthru unknown)
+(define (execute-build args)
+  (let ((expr (nix-expr-for (build-args-target args))))
+    (fprintf (current-error-port) "[mg] building target ~A~%" (build-args-target args))
+    (process-execute "nix-build" (append (list "-E" expr "--show-trace")
+                                         (or (build-args-passthru args) '())))))
+
+;; split the arguments used for builds into target/unknown args/nix
+;; args, where the latter occur after '--'
+(define (parse-build-args acc args)
+  (match args
+         ;; no arguments remaining, return accumulator as is
+         [() acc]
+
+         ;; next argument is '--' separator, split off passthru and
+         ;; return
+         [("--" . passthru)
+          (begin
+            (build-args-passthru-set! acc passthru)
+            acc)]
+
+         [(arg . rest)
+          ;; set target if not already known (and if the first
+          ;; argument does not look like an accidental unknown
+          ;; parameter)
+          (if (and (not (build-args-target acc))
+                   (not (substring=? "-" arg)))
+              (begin
+                (build-args-target-set! acc (guarantee-success (parse-target arg)))
+                (parse-build-args acc rest))
+
+              ;; otherwise, collect unknown arguments
+              (begin
+                (build-args-unknown-set! acc (append (or (build-args-unknown acc) '())
+                                                     (list arg)))
+                (parse-build-args acc rest)))]))
+
+;; parse the passed build args, applying sanity checks and defaulting
+;; the target if necessary, then execute the build
+(define (build args)
+  (let ((parsed (parse-build-args (make-build-args #f #f #f) args)))
+    ;; fail if there are unknown arguments present
+    (when (build-args-unknown parsed)
+      (let ((unknown (string-intersperse (build-args-unknown parsed))))
+        (mg-error (sprintf "unknown arguments: ~a
+
+if you meant to pass these arguments to nix, please separate them with
+'--' like so:
+
+  mg build ~a -- ~a"
+                        unknown
+                        (or (build-args-target parsed) "")
+                        unknown))))
+
+    ;; default the target to the current folder's main target
+    (unless (build-args-target parsed)
+      (build-args-target-set! parsed (empty-target)))
+
+    (execute-build parsed)))
+
+(define (execute-shell t)
+  (let ((expr (nix-expr-for t))
+        (user-shell (or (get-environment-variable "SHELL") "bash")))
+    (fprintf (current-error-port) "[mg] entering shell for ~A~%" t)
+    (process-execute "nix-shell"
+                     (list "-E" expr "--command" user-shell))))
+
+(define (shell args)
+  (match args
+         [() (execute-shell (empty-target))]
+         [(arg) (execute-shell
+                 (guarantee-success (parse-target arg)))]
+         [other (print "not yet implemented")]))
+
+(define (execute-run t #!optional cmd-args)
+  (fprintf (current-error-port) "[mg] building target ~A~%" t)
+  (let* ((expr (nix-expr-for t))
+         (out (call-with-input-pipe
+               (apply string-append
+                      ;; TODO(sterni): temporary gc root
+                      (intersperse `("nix-build" "-E" ,(qs expr) "--no-out-link")
+                                   " "))
+               (lambda (p)
+                 (string-chomp (let ((s (read-string #f p)))
+                                 (if (eq? s #!eof) "" s)))))))
+
+    ;; TODO(sterni): can we get the exit code of nix-build somehow?
+    (when (= (string-length out) 0)
+      (mg-error (string-append "Couldn't build target " (format "~A" t)))
+      (exit 1))
+
+    (fprintf (current-error-port) "[mg] running target ~A~%" t)
+    (process-execute
+     ;; If the output is a file, we assume it's an executable à la writeExecline,
+     ;; otherwise we look in the bin subdirectory and pick the only executable.
+     ;; Handling multiple executables is not possible at the moment, the choice
+     ;; could be made via a command line flag in the future.
+     (if (regular-file? out)
+         out
+         (let* ((dir-path (string-append out "/bin"))
+                (dir-contents (if (directory-exists? dir-path)
+                                  (directory dir-path #f)
+                                  '())))
+           (case (length dir-contents)
+             ((0) (mg-error "no executables in build output")
+                  (exit 1))
+             ((1) (string-append dir-path "/" (car dir-contents)))
+             (else (mg-error "more than one executable in build output")
+                   (exit 1)))))
+     cmd-args)))
+
+(define (run args)
+  (match args
+         [() (execute-run (empty-target))]
+         ;; TODO(sterni): flag for selecting binary name
+         [other (execute-run (guarantee-success (parse-target (car args)))
+                             (cdr args))]))
+
+(define (path args)
+  (match args
+         [(arg)
+          (print (apply string-append
+                        (intersperse
+                         (cons (repository-root)
+                               (target-components
+                                (normalise-target
+                                 (guarantee-success (parse-target arg)))))
+                         "/")))]
+         [() (mg-error "path command needs a target")]
+         [other (mg-error (format "unknown arguments: ~a" other))]))
+
+(define (main args)
+  (match args
+         [() (print usage)]
+         [("build" . _) (build (cdr args))]
+         [("shell" . _) (shell (cdr args))]
+         [("path" . _) (path (cdr args))]
+         [("run" . _) (run (cdr args))]
+         [other (begin (print "unknown command: mg " args)
+                       (print usage))]))
+
+(main (command-line-arguments))
diff --git a/tools/nixery/.gitignore b/tools/nixery/.gitignore
new file mode 100644
index 0000000000..578eea3923
--- /dev/null
+++ b/tools/nixery/.gitignore
@@ -0,0 +1,12 @@
+result
+result-*
+.envrc
+debug/
+
+# Just to be sure, since we're occasionally handling test keys:
+*.pem
+*.p12
+*.json
+
+# Created by the integration test
+var-cache-nixery
diff --git a/tools/nixery/.skip-subtree b/tools/nixery/.skip-subtree
new file mode 100644
index 0000000000..4948dd56eb
--- /dev/null
+++ b/tools/nixery/.skip-subtree
@@ -0,0 +1 @@
+Imported subtree is not yet fully readTree-compatible.
diff --git a/tools/nixery/LICENSE b/tools/nixery/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/tools/nixery/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/tools/nixery/README.md b/tools/nixery/README.md
new file mode 100644
index 0000000000..03515939a9
--- /dev/null
+++ b/tools/nixery/README.md
@@ -0,0 +1,156 @@
+<div align="center">
+  <img src="docs/src/nixery-logo.png">
+</div>
+
+-----------------
+
+[![Build status](https://badge.buildkite.com/016bff4b8ae2704a3bbbb0a250784e6692007c582983b6dea7.svg?branch=refs/heads/canon)](https://buildkite.com/tvl/depot)
+
+**Nixery** is a Docker-compatible container registry that is capable of
+transparently building and serving container images using [Nix][].
+
+Images are built on-demand based on the *image name*. Every package that the
+user intends to include in the image is specified as a path component of the
+image name.
+
+The path components refer to top-level keys in `nixpkgs` and are used to build a
+container image using a [layering strategy][] that optimises for caching popular
+and/or large dependencies.
+
+A public instance as well as additional documentation is available at
+[nixery.dev][public].
+
+You can watch the NixCon 2019 [talk about
+Nixery](https://www.youtube.com/watch?v=pOI9H4oeXqA) for more information about
+the project and its use-cases.
+
+The canonical location of the Nixery source code is
+[`//tools/nixery`][depot-link] in the [TVL](https://tvl.fyi)
+monorepository. If cloning the entire repository is not desirable, the
+Nixery subtree can be cloned like this:
+
+    git clone https://code.tvl.fyi/depot.git:/tools/nixery.git
+
+The subtree is infrequently mirrored to `tazjin/nixery` on Github.
+
+## Demo
+
+Click the image to see an example in which an image containing an interactive
+shell and GNU `hello` is downloaded.
+
+[![asciicast](https://asciinema.org/a/262583.png)](https://asciinema.org/a/262583?autoplay=1)
+
+To try it yourself, head to [nixery.dev][public]!
+
+The special meta-package `shell` provides an image base with many core
+components (such as `bash` and `coreutils`) that users commonly expect in
+interactive images.
+
+## Feature overview
+
+* Serve container images on-demand using image names as content specifications
+
+  Specify package names as path components and Nixery will create images, using
+  the most efficient caching strategy it can to share data between different
+  images.
+
+* Use private package sets from various sources
+
+  In addition to building images from the publicly available Nix/NixOS channels,
+  a private Nixery instance can be configured to serve images built from a
+  package set hosted in a custom git repository or filesystem path.
+
+  When using this feature with custom git repositories, Nixery will forward the
+  specified image tags as git references.
+
+  For example, if a company used a custom repository overlaying their packages
+  on the Nix package set, images could be built from a git tag `release-v2`:
+
+  `docker pull nixery.thecompany.website/custom-service:release-v2`
+
+* Efficient serving of image layers from Google Cloud Storage
+
+  After building an image, Nixery stores all of its layers in a GCS bucket and
+  forwards requests to retrieve layers to the bucket. This enables efficient
+  serving of layers, as well as sharing of image layers between redundant
+  instances.
+
+## Configuration
+
+Nixery supports the following configuration options, provided via environment
+variables:
+
+* `PORT`: HTTP port on which Nixery should listen
+* `NIXERY_CHANNEL`: The name of a Nix/NixOS channel to use for building
+* `NIXERY_PKGS_REPO`: URL of a git repository containing a package set (uses
+  locally configured SSH/git credentials)
+* `NIXERY_PKGS_PATH`: A local filesystem path containing a Nix package set to
+  use for building
+* `NIXERY_STORAGE_BACKEND`: The type of backend storage to use, currently
+  supported values are `gcs` (Google Cloud Storage) and `filesystem`.
+
+  For each of these additional backend configuration is necessary, see the
+  [storage section](#storage) for details.
+* `NIX_TIMEOUT`: Number of seconds that any Nix builder is allowed to run
+  (defaults to 60)
+* `NIX_POPULARITY_URL`: URL to a file containing popularity data for
+  the package set (see `popcount/`)
+
+If the `GOOGLE_APPLICATION_CREDENTIALS` environment variable is set to a service
+account key, Nixery will also use this key to create [signed URLs][] for layers
+in the storage bucket. This makes it possible to serve layers from a bucket
+without having to make them publicly available.
+
+In case the `GOOGLE_APPLICATION_CREDENTIALS` environment variable is not set, a
+redirect to storage.googleapis.com is issued, which means the underlying bucket
+objects need to be publicly accessible.
+
+### Storage
+
+Nixery supports multiple different storage backends in which its build cache and
+image layers are kept, and from which they are served.
+
+Currently the available storage backends are Google Cloud Storage and the local
+file system.
+
+In the GCS case, images are served by redirecting clients to the storage bucket.
+Layers stored on the filesystem are served straight from the local disk.
+
+These extra configuration variables must be set to configure storage backends:
+
+* `GCS_BUCKET`: Name of the Google Cloud Storage bucket to use (**required** for
+  `gcs`)
+* `GOOGLE_APPLICATION_CREDENTIALS`: Path to a GCP service account JSON key
+  (**optional** for `gcs`)
+* `STORAGE_PATH`: Path to a folder in which to store and from which to serve
+  data (**required** for `filesystem`)
+
+### Background
+
+The project started out inspired by the [buildLayeredImage][] blog post with the
+intention of becoming a Kubernetes controller that can serve declarative image
+specifications specified in CRDs as container images. The design for this was
+outlined in [a public gist][gist].
+
+## Roadmap
+
+### Kubernetes integration
+
+It should be trivial to deploy Nixery inside of a Kubernetes cluster with
+correct caching behaviour, addressing and so on.
+
+See [issue #4](https://github.com/tazjin/nixery/issues/4).
+
+### Nix-native builder
+
+The image building and layering functionality of Nixery will be extracted into a
+separate Nix function, which will make it possible to build images directly in
+Nix builds.
+
+[Nix]: https://nixos.org/
+[layering strategy]: https://tazj.in/blog/nixery-layers
+[gist]: https://gist.github.com/tazjin/08f3d37073b3590aacac424303e6f745
+[buildLayeredImage]: https://grahamc.com/blog/nix-and-layered-docker-images
+[public]: https://nixery.dev
+[depot-link]: https://cs.tvl.fyi/depot/-/tree/tools/nixery
+[gcs]: https://cloud.google.com/storage/
diff --git a/tools/nixery/builder/archive.go b/tools/nixery/builder/archive.go
new file mode 100644
index 0000000000..8763e4cb85
--- /dev/null
+++ b/tools/nixery/builder/archive.go
@@ -0,0 +1,104 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+package builder
+
+// This file implements logic for walking through a directory and creating a
+// tarball of it.
+//
+// The tarball is written straight to the supplied reader, which makes it
+// possible to create an image layer from the specified store paths, hash it and
+// upload it in one reading pass.
+import (
+	"archive/tar"
+	"compress/gzip"
+	"crypto/sha256"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/google/nixery/layers"
+)
+
+// Create a new compressed tarball from each of the paths in the list
+// and write it to the supplied writer.
+//
+// The uncompressed tarball is hashed because image manifests must
+// contain both the hashes of compressed and uncompressed layers.
+func packStorePaths(l *layers.Layer, w io.Writer) (string, error) {
+	shasum := sha256.New()
+	gz := gzip.NewWriter(w)
+	multi := io.MultiWriter(shasum, gz)
+	t := tar.NewWriter(multi)
+
+	for _, path := range l.Contents {
+		err := filepath.Walk(path, tarStorePath(t))
+		if err != nil {
+			return "", err
+		}
+	}
+
+	if err := t.Close(); err != nil {
+		return "", err
+	}
+
+	if err := gz.Close(); err != nil {
+		return "", err
+	}
+
+	return fmt.Sprintf("sha256:%x", shasum.Sum([]byte{})), nil
+}
+
+func tarStorePath(w *tar.Writer) filepath.WalkFunc {
+	return func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// If the entry is not a symlink or regular file, skip it.
+		if info.Mode()&os.ModeSymlink == 0 && !info.Mode().IsRegular() {
+			return nil
+		}
+
+		// the symlink target is read if this entry is a symlink, as it
+		// is required when creating the file header
+		var link string
+		if info.Mode()&os.ModeSymlink != 0 {
+			link, err = os.Readlink(path)
+			if err != nil {
+				return err
+			}
+		}
+
+		header, err := tar.FileInfoHeader(info, link)
+		if err != nil {
+			return err
+		}
+
+		// The name retrieved from os.FileInfo only contains the file's
+		// basename, but the full path is required within the layer
+		// tarball.
+		header.Name = path
+		if err = w.WriteHeader(header); err != nil {
+			return err
+		}
+
+		// At this point, return if no file content needs to be written
+		if !info.Mode().IsRegular() {
+			return nil
+		}
+
+		f, err := os.Open(path)
+		if err != nil {
+			return err
+		}
+
+		if _, err := io.Copy(w, f); err != nil {
+			return err
+		}
+
+		f.Close()
+
+		return nil
+	}
+}
diff --git a/tools/nixery/builder/builder.go b/tools/nixery/builder/builder.go
new file mode 100644
index 0000000000..901373b57e
--- /dev/null
+++ b/tools/nixery/builder/builder.go
@@ -0,0 +1,519 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package builder implements the logic for assembling container
+// images. It shells out to Nix to retrieve all required Nix-packages
+// and assemble the symlink layer and then creates the required
+// tarballs in-process.
+package builder
+
+import (
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"context"
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"sort"
+	"strings"
+
+	"github.com/google/nixery/config"
+	"github.com/google/nixery/layers"
+	"github.com/google/nixery/manifest"
+	"github.com/google/nixery/storage"
+	log "github.com/sirupsen/logrus"
+)
+
+// The maximum number of layers in an image is 125. To allow for
+// extensibility, the actual number of layers Nixery is "allowed" to
+// use up is set at a lower point.
+const LayerBudget int = 94
+
+// State holds the runtime state that is carried around in Nixery and
+// passed to builder functions.
+type State struct {
+	Storage storage.Backend
+	Cache   *LocalCache
+	Cfg     config.Config
+	Pop     layers.Popularity
+}
+
+// Architecture represents the possible CPU architectures for which
+// container images can be built.
+//
+// The default architecture is amd64, but support for ARM platforms is
+// available within nixpkgs and can be toggled via meta-packages.
+type Architecture struct {
+	// Name of the system tuple to pass to Nix
+	nixSystem string
+
+	// Name of the architecture as used in the OCI manifests
+	imageArch string
+}
+
+var amd64 = Architecture{"x86_64-linux", "amd64"}
+var arm64 = Architecture{"aarch64-linux", "arm64"}
+
+// Image represents the information necessary for building a container image.
+// This can be either a list of package names (corresponding to keys in the
+// nixpkgs set) or a Nix expression that results in a *list* of derivations.
+type Image struct {
+	Name string
+	Tag  string
+
+	// Names of packages to include in the image. These must correspond
+	// directly to top-level names of Nix packages in the nixpkgs tree.
+	Packages []string
+
+	// Architecture for which to build the image. Nixery defaults
+	// this to amd64 if not specified via meta-packages.
+	Arch *Architecture
+}
+
+// BuildResult represents the data returned from the server to the
+// HTTP handlers. Error information is propagated straight from Nix
+// for errors inside of the build that should be fed back to the
+// client (such as missing packages).
+type BuildResult struct {
+	Error    string          `json:"error"`
+	Pkgs     []string        `json:"pkgs"`
+	Manifest json.RawMessage `json:"manifest"`
+}
+
+// ImageFromName parses an image name into the corresponding structure which can
+// be used to invoke Nix.
+//
+// It will expand convenience names under the hood (see the `convenienceNames`
+// function below) and append packages that are always included (cacert, iana-etc).
+//
+// Once assembled the image structure uses a sorted representation of
+// the name. This is to avoid unnecessarily cache-busting images if
+// only the order of requested packages has changed.
+func ImageFromName(name string, tag string) Image {
+	pkgs := strings.Split(name, "/")
+	arch, expanded := metaPackages(pkgs)
+	expanded = append(expanded, "cacert", "iana-etc")
+
+	sort.Strings(pkgs)
+	sort.Strings(expanded)
+
+	return Image{
+		Name:     strings.Join(pkgs, "/"),
+		Tag:      tag,
+		Packages: expanded,
+		Arch:     arch,
+	}
+}
+
+// ImageResult represents the output of calling the Nix derivation
+// responsible for preparing an image.
+type ImageResult struct {
+	// These fields are populated in case of an error
+	Error string   `json:"error"`
+	Pkgs  []string `json:"pkgs"`
+
+	// These fields are populated in case of success
+	Graph        layers.RuntimeGraph `json:"runtimeGraph"`
+	SymlinkLayer struct {
+		Size    int    `json:"size"`
+		TarHash string `json:"tarHash"`
+		Path    string `json:"path"`
+	} `json:"symlinkLayer"`
+}
+
+// metaPackages expands package names defined by Nixery which either
+// include sets of packages or trigger certain image-building
+// behaviour.
+//
+// Meta-packages must be specified as the first packages in an image
+// name.
+//
+// Currently defined meta-packages are:
+//
+// * `shell`: Includes bash, coreutils and other common command-line tools
+// * `arm64`: Causes Nixery to build images for the ARM64 architecture
+func metaPackages(packages []string) (*Architecture, []string) {
+	arch := &amd64
+
+	var metapkgs []string
+	lastMeta := 0
+	for idx, p := range packages {
+		if p == "shell" || p == "arm64" {
+			metapkgs = append(metapkgs, p)
+			lastMeta = idx + 1
+		} else {
+			break
+		}
+	}
+
+	// Chop off the meta-packages from the front of the package
+	// list
+	packages = packages[lastMeta:]
+
+	for _, p := range metapkgs {
+		switch p {
+		case "shell":
+			packages = append(packages, "bashInteractive", "coreutils", "moreutils", "nano")
+		case "arm64":
+			arch = &arm64
+		}
+	}
+
+	return arch, packages
+}
+
+// logNix logs each output line from Nix. It runs in a goroutine per
+// output channel that should be live-logged.
+func logNix(image, cmd string, r io.ReadCloser) {
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		log.WithFields(log.Fields{
+			"image": image,
+			"cmd":   cmd,
+		}).Info("[nix] " + scanner.Text())
+	}
+}
+
+func callNix(program, image string, args []string) ([]byte, error) {
+	cmd := exec.Command(program, args...)
+
+	outpipe, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+
+	errpipe, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, err
+	}
+	go logNix(image, program, errpipe)
+
+	if err = cmd.Start(); err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"image": image,
+			"cmd":   program,
+		}).Error("error invoking Nix")
+
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"cmd":   program,
+		"image": image,
+	}).Info("invoked Nix build")
+
+	stdout, _ := ioutil.ReadAll(outpipe)
+
+	if err = cmd.Wait(); err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"image":  image,
+			"cmd":    program,
+			"stdout": stdout,
+		}).Info("failed to invoke Nix")
+
+		return nil, err
+	}
+
+	resultFile := strings.TrimSpace(string(stdout))
+	buildOutput, err := ioutil.ReadFile(resultFile)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"image": image,
+			"file":  resultFile,
+		}).Info("failed to read Nix result file")
+
+		return nil, err
+	}
+
+	return buildOutput, nil
+}
+
+// Call out to Nix and request metadata for the image to be built. All
+// required store paths for the image will be realised, but layers
+// will not yet be created from them.
+//
+// This function is only invoked if the manifest is not found in any
+// cache.
+func prepareImage(s *State, image *Image) (*ImageResult, error) {
+	packages, err := json.Marshal(image.Packages)
+	if err != nil {
+		return nil, err
+	}
+
+	srcType, srcArgs := s.Cfg.Pkgs.Render(image.Tag)
+
+	args := []string{
+		"--timeout", s.Cfg.Timeout,
+		"--argstr", "packages", string(packages),
+		"--argstr", "srcType", srcType,
+		"--argstr", "srcArgs", srcArgs,
+		"--argstr", "system", image.Arch.nixSystem,
+	}
+
+	output, err := callNix("nixery-prepare-image", image.Name, args)
+	if err != nil {
+		// granular error logging is performed in callNix already
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"image": image.Name,
+		"tag":   image.Tag,
+	}).Info("finished image preparation via Nix")
+
+	var result ImageResult
+	err = json.Unmarshal(output, &result)
+	if err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// Groups layers and checks whether they are present in the cache
+// already, otherwise calls out to Nix to assemble layers.
+//
+// Newly built layers are uploaded to the bucket. Cache entries are
+// added only after successful uploads, which guarantees that entries
+// retrieved from the cache are present in the bucket.
+func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageResult) ([]manifest.Entry, error) {
+	grouped := layers.GroupLayers(&result.Graph, &s.Pop, LayerBudget)
+
+	var entries []manifest.Entry
+
+	// Splits the layers into those which are already present in
+	// the cache, and those that are missing.
+	//
+	// Missing layers are built and uploaded to the storage
+	// bucket.
+	for _, l := range grouped {
+		if entry, cached := layerFromCache(ctx, s, l.Hash()); cached {
+			entries = append(entries, *entry)
+		} else {
+			lh := l.Hash()
+
+			// While packing store paths, the SHA sum of
+			// the uncompressed layer is computed and
+			// written to `tarhash`.
+			//
+			// TODO(tazjin): Refactor this to make the
+			// flow of data cleaner.
+			var tarhash string
+			lw := func(w io.Writer) error {
+				var err error
+				tarhash, err = packStorePaths(&l, w)
+				return err
+			}
+
+			entry, err := uploadHashLayer(ctx, s, lh, lw)
+			if err != nil {
+				return nil, err
+			}
+			entry.MergeRating = l.MergeRating
+			entry.TarHash = tarhash
+
+			var pkgs []string
+			for _, p := range l.Contents {
+				pkgs = append(pkgs, layers.PackageFromPath(p))
+			}
+
+			log.WithFields(log.Fields{
+				"layer":    lh,
+				"packages": pkgs,
+				"tarhash":  tarhash,
+			}).Info("created image layer")
+
+			go cacheLayer(ctx, s, l.Hash(), *entry)
+			entries = append(entries, *entry)
+		}
+	}
+
+	// Symlink layer (built in the first Nix build) needs to be
+	// included here manually:
+	slkey := result.SymlinkLayer.TarHash
+	entry, err := uploadHashLayer(ctx, s, slkey, func(w io.Writer) error {
+		f, err := os.Open(result.SymlinkLayer.Path)
+		if err != nil {
+			log.WithError(err).WithFields(log.Fields{
+				"image": image.Name,
+				"tag":   image.Tag,
+				"layer": slkey,
+			}).Error("failed to open symlink layer")
+
+			return err
+		}
+		defer f.Close()
+
+		gz := gzip.NewWriter(w)
+		_, err = io.Copy(gz, f)
+		if err != nil {
+			log.WithError(err).WithFields(log.Fields{
+				"image": image.Name,
+				"tag":   image.Tag,
+				"layer": slkey,
+			}).Error("failed to upload symlink layer")
+
+			return err
+		}
+
+		return gz.Close()
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	entry.TarHash = "sha256:" + result.SymlinkLayer.TarHash
+	go cacheLayer(ctx, s, slkey, *entry)
+	entries = append(entries, *entry)
+
+	return entries, nil
+}
+
+// layerWriter is the type for functions that can write a layer to the
+// multiwriter used for uploading & hashing.
+//
+// This type exists to avoid duplication between the handling of
+// symlink layers and store path layers.
+type layerWriter func(w io.Writer) error
+
+// byteCounter is a special io.Writer that counts all bytes written to
+// it and does nothing else.
+//
+// This is required because the ad-hoc writing of tarballs leaves no
+// single place to count the final tarball size otherwise.
+type byteCounter struct {
+	count int64
+}
+
+func (b *byteCounter) Write(p []byte) (n int, err error) {
+	b.count += int64(len(p))
+	return len(p), nil
+}
+
+// Upload a layer tarball to the storage bucket, while hashing it at
+// the same time. The supplied function is expected to provide the
+// layer data to the writer.
+//
+// The initial upload is performed in a 'staging' folder, as the
+// SHA256-hash is not yet available when the upload is initiated.
+//
+// After a successful upload, the file is moved to its final location
+// in the bucket and the build cache is populated.
+//
+// The return value is the layer's SHA256 hash, which is used in the
+// image manifest.
+func uploadHashLayer(ctx context.Context, s *State, key string, lw layerWriter) (*manifest.Entry, error) {
+	path := "staging/" + key
+	sha256sum, size, err := s.Storage.Persist(ctx, path, manifest.LayerType, func(sw io.Writer) (string, int64, error) {
+		// Sets up a "multiwriter" that simultaneously runs both hash
+		// algorithms and uploads to the storage backend.
+		shasum := sha256.New()
+		counter := &byteCounter{}
+		multi := io.MultiWriter(sw, shasum, counter)
+
+		err := lw(multi)
+		sha256sum := fmt.Sprintf("%x", shasum.Sum([]byte{}))
+
+		return sha256sum, counter.count, err
+	})
+
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"layer":   key,
+			"backend": s.Storage.Name(),
+		}).Error("failed to create and store layer")
+
+		return nil, err
+	}
+
+	// Hashes are now known and the object is in the bucket, what
+	// remains is to move it to the correct location and cache it.
+	err = s.Storage.Move(ctx, "staging/"+key, "layers/"+sha256sum)
+	if err != nil {
+		log.WithError(err).WithField("layer", key).
+			Error("failed to move layer from staging")
+
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"layer":  key,
+		"sha256": sha256sum,
+		"size":   size,
+	}).Info("created and persisted layer")
+
+	entry := manifest.Entry{
+		Digest: "sha256:" + sha256sum,
+		Size:   size,
+	}
+
+	return &entry, nil
+}
+
+func BuildImage(ctx context.Context, s *State, image *Image) (*BuildResult, error) {
+	key := s.Cfg.Pkgs.CacheKey(image.Packages, image.Tag)
+	if key != "" {
+		if m, c := manifestFromCache(ctx, s, key); c {
+			return &BuildResult{
+				Manifest: m,
+			}, nil
+		}
+	}
+
+	imageResult, err := prepareImage(s, image)
+	if err != nil {
+		return nil, err
+	}
+
+	if imageResult.Error != "" {
+		return &BuildResult{
+			Error: imageResult.Error,
+			Pkgs:  imageResult.Pkgs,
+		}, nil
+	}
+
+	layers, err := prepareLayers(ctx, s, image, imageResult)
+	if err != nil {
+		return nil, err
+	}
+
+	// If the requested packages include a shell,
+	// set cmd accordingly.
+	cmd := ""
+	for _, pkg := range image.Packages {
+		if pkg == "bashInteractive" {
+			cmd = "bash"
+		}
+	}
+	m, c := manifest.Manifest(image.Arch.imageArch, layers, cmd)
+
+	lw := func(w io.Writer) error {
+		r := bytes.NewReader(c.Config)
+		_, err := io.Copy(w, r)
+		return err
+	}
+
+	if _, err = uploadHashLayer(ctx, s, c.SHA256, lw); err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"image": image.Name,
+			"tag":   image.Tag,
+		}).Error("failed to upload config")
+
+		return nil, err
+	}
+
+	if key != "" {
+		go cacheManifest(ctx, s, key, m)
+	}
+
+	result := BuildResult{
+		Manifest: m,
+	}
+	return &result, nil
+}
diff --git a/tools/nixery/builder/builder_test.go b/tools/nixery/builder/builder_test.go
new file mode 100644
index 0000000000..507f3eb15a
--- /dev/null
+++ b/tools/nixery/builder/builder_test.go
@@ -0,0 +1,112 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+package builder
+
+import (
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"testing"
+)
+
+var ignoreArch = cmpopts.IgnoreFields(Image{}, "Arch")
+
+func TestImageFromNameSimple(t *testing.T) {
+	image := ImageFromName("hello", "latest")
+	expected := Image{
+		Name: "hello",
+		Tag:  "latest",
+		Packages: []string{
+			"cacert",
+			"hello",
+			"iana-etc",
+		},
+	}
+
+	if diff := cmp.Diff(expected, image, ignoreArch); diff != "" {
+		t.Fatalf("Image(\"hello\", \"latest\") mismatch:\n%s", diff)
+	}
+}
+
+func TestImageFromNameMultiple(t *testing.T) {
+	image := ImageFromName("hello/git/htop", "latest")
+	expected := Image{
+		Name: "git/hello/htop",
+		Tag:  "latest",
+		Packages: []string{
+			"cacert",
+			"git",
+			"hello",
+			"htop",
+			"iana-etc",
+		},
+	}
+
+	if diff := cmp.Diff(expected, image, ignoreArch); diff != "" {
+		t.Fatalf("Image(\"hello/git/htop\", \"latest\") mismatch:\n%s", diff)
+	}
+}
+
+func TestImageFromNameShell(t *testing.T) {
+	image := ImageFromName("shell", "latest")
+	expected := Image{
+		Name: "shell",
+		Tag:  "latest",
+		Packages: []string{
+			"bashInteractive",
+			"cacert",
+			"coreutils",
+			"iana-etc",
+			"moreutils",
+			"nano",
+		},
+	}
+
+	if diff := cmp.Diff(expected, image, ignoreArch); diff != "" {
+		t.Fatalf("Image(\"shell\", \"latest\") mismatch:\n%s", diff)
+	}
+}
+
+func TestImageFromNameShellMultiple(t *testing.T) {
+	image := ImageFromName("shell/htop", "latest")
+	expected := Image{
+		Name: "htop/shell",
+		Tag:  "latest",
+		Packages: []string{
+			"bashInteractive",
+			"cacert",
+			"coreutils",
+			"htop",
+			"iana-etc",
+			"moreutils",
+			"nano",
+		},
+	}
+
+	if diff := cmp.Diff(expected, image, ignoreArch); diff != "" {
+		t.Fatalf("Image(\"shell/htop\", \"latest\") mismatch:\n%s", diff)
+	}
+}
+
+func TestImageFromNameShellArm64(t *testing.T) {
+	image := ImageFromName("shell/arm64", "latest")
+	expected := Image{
+		Name: "arm64/shell",
+		Tag:  "latest",
+		Packages: []string{
+			"bashInteractive",
+			"cacert",
+			"coreutils",
+			"iana-etc",
+			"moreutils",
+			"nano",
+		},
+	}
+
+	if diff := cmp.Diff(expected, image, ignoreArch); diff != "" {
+		t.Fatalf("Image(\"shell/arm64\", \"latest\") mismatch:\n%s", diff)
+	}
+
+	if image.Arch.imageArch != "arm64" {
+		t.Fatal("Image(\"shell/arm64\"): Expected arch arm64")
+	}
+}
diff --git a/tools/nixery/builder/cache.go b/tools/nixery/builder/cache.go
new file mode 100644
index 0000000000..9e4283c0e5
--- /dev/null
+++ b/tools/nixery/builder/cache.go
@@ -0,0 +1,225 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+package builder
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"os"
+	"sync"
+
+	"github.com/google/nixery/manifest"
+	log "github.com/sirupsen/logrus"
+)
+
+// LocalCache implements the structure used for local caching of
+// manifests and layer uploads.
+type LocalCache struct {
+	// Manifest cache
+	mmtx sync.RWMutex
+	mdir string
+
+	// Layer cache
+	lmtx   sync.RWMutex
+	lcache map[string]manifest.Entry
+}
+
+// Creates an in-memory cache and ensures that the local file path for
+// manifest caching exists.
+func NewCache() (LocalCache, error) {
+	path := os.TempDir() + "/nixery"
+	err := os.MkdirAll(path, 0755)
+	if err != nil {
+		return LocalCache{}, err
+	}
+
+	return LocalCache{
+		mdir:   path + "/",
+		lcache: make(map[string]manifest.Entry),
+	}, nil
+}
+
+// Retrieve a cached manifest if the build is cacheable and it exists.
+func (c *LocalCache) manifestFromLocalCache(key string) (json.RawMessage, bool) {
+	c.mmtx.RLock()
+	defer c.mmtx.RUnlock()
+
+	f, err := os.Open(c.mdir + key)
+	if err != nil {
+		// This is a debug log statement because failure to
+		// read the manifest key is currently expected if it
+		// is not cached.
+		log.WithError(err).WithField("manifest", key).
+			Debug("failed to read manifest from local cache")
+
+		return nil, false
+	}
+	defer f.Close()
+
+	m, err := ioutil.ReadAll(f)
+	if err != nil {
+		log.WithError(err).WithField("manifest", key).
+			Error("failed to read manifest from local cache")
+
+		return nil, false
+	}
+
+	return json.RawMessage(m), true
+}
+
+// Adds the result of a manifest build to the local cache, if the
+// manifest is considered cacheable.
+//
+// Manifests can be quite large and are cached on disk instead of in
+// memory.
+func (c *LocalCache) localCacheManifest(key string, m json.RawMessage) {
+	c.mmtx.Lock()
+	defer c.mmtx.Unlock()
+
+	err := ioutil.WriteFile(c.mdir+key, []byte(m), 0644)
+	if err != nil {
+		log.WithError(err).WithField("manifest", key).
+			Error("failed to locally cache manifest")
+	}
+}
+
+// Retrieve a layer build from the local cache.
+func (c *LocalCache) layerFromLocalCache(key string) (*manifest.Entry, bool) {
+	c.lmtx.RLock()
+	e, ok := c.lcache[key]
+	c.lmtx.RUnlock()
+
+	return &e, ok
+}
+
+// Add a layer build result to the local cache.
+func (c *LocalCache) localCacheLayer(key string, e manifest.Entry) {
+	c.lmtx.Lock()
+	c.lcache[key] = e
+	c.lmtx.Unlock()
+}
+
+// Retrieve a manifest from the cache(s). First the local cache is
+// checked, then the storage backend.
+func manifestFromCache(ctx context.Context, s *State, key string) (json.RawMessage, bool) {
+	if m, cached := s.Cache.manifestFromLocalCache(key); cached {
+		return m, true
+	}
+
+	r, err := s.Storage.Fetch(ctx, "manifests/"+key)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"manifest": key,
+			"backend":  s.Storage.Name(),
+		}).Error("failed to fetch manifest from cache")
+
+		return nil, false
+	}
+	defer r.Close()
+
+	m, err := ioutil.ReadAll(r)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"manifest": key,
+			"backend":  s.Storage.Name(),
+		}).Error("failed to read cached manifest from storage backend")
+
+		return nil, false
+	}
+
+	go s.Cache.localCacheManifest(key, m)
+	log.WithField("manifest", key).Info("retrieved manifest from GCS")
+
+	return json.RawMessage(m), true
+}
+
+// Add a manifest to the bucket & local caches
+func cacheManifest(ctx context.Context, s *State, key string, m json.RawMessage) {
+	go s.Cache.localCacheManifest(key, m)
+
+	path := "manifests/" + key
+	_, size, err := s.Storage.Persist(ctx, path, manifest.ManifestType, func(w io.Writer) (string, int64, error) {
+		size, err := io.Copy(w, bytes.NewReader([]byte(m)))
+		return "", size, err
+	})
+
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"manifest": key,
+			"backend":  s.Storage.Name(),
+		}).Error("failed to cache manifest to storage backend")
+
+		return
+	}
+
+	log.WithFields(log.Fields{
+		"manifest": key,
+		"size":     size,
+		"backend":  s.Storage.Name(),
+	}).Info("cached manifest to storage backend")
+}
+
+// Retrieve a layer build from the cache, first checking the local
+// cache followed by the bucket cache.
+func layerFromCache(ctx context.Context, s *State, key string) (*manifest.Entry, bool) {
+	if entry, cached := s.Cache.layerFromLocalCache(key); cached {
+		return entry, true
+	}
+
+	r, err := s.Storage.Fetch(ctx, "builds/"+key)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"layer":   key,
+			"backend": s.Storage.Name(),
+		}).Debug("failed to retrieve cached layer from storage backend")
+
+		return nil, false
+	}
+	defer r.Close()
+
+	jb := bytes.NewBuffer([]byte{})
+	_, err = io.Copy(jb, r)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"layer":   key,
+			"backend": s.Storage.Name(),
+		}).Error("failed to read cached layer from storage backend")
+
+		return nil, false
+	}
+
+	var entry manifest.Entry
+	err = json.Unmarshal(jb.Bytes(), &entry)
+	if err != nil {
+		log.WithError(err).WithField("layer", key).
+			Error("failed to unmarshal cached layer")
+
+		return nil, false
+	}
+
+	go s.Cache.localCacheLayer(key, entry)
+	return &entry, true
+}
+
+func cacheLayer(ctx context.Context, s *State, key string, entry manifest.Entry) {
+	s.Cache.localCacheLayer(key, entry)
+
+	j, _ := json.Marshal(&entry)
+	path := "builds/" + key
+	_, _, err := s.Storage.Persist(ctx, path, "", func(w io.Writer) (string, int64, error) {
+		size, err := io.Copy(w, bytes.NewReader(j))
+		return "", size, err
+	})
+
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"layer":   key,
+			"backend": s.Storage.Name(),
+		}).Error("failed to cache layer")
+	}
+
+	return
+}
diff --git a/tools/nixery/cmd/server/main.go b/tools/nixery/cmd/server/main.go
new file mode 100644
index 0000000000..8fe1679cfa
--- /dev/null
+++ b/tools/nixery/cmd/server/main.go
@@ -0,0 +1,281 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// The nixery server implements a container registry that transparently builds
+// container images based on Nix derivations.
+//
+// The Nix derivation used for image creation is responsible for creating
+// objects that are compatible with the registry API. The targeted registry
+// protocol is currently Docker's.
+//
+// When an image is requested, the required contents are parsed out of the
+// request and a Nix-build is initiated that eventually responds with the
+// manifest as well as information linking each layer digest to a local
+// filesystem path.
+package main
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"regexp"
+
+	"github.com/google/nixery/builder"
+	"github.com/google/nixery/config"
+	"github.com/google/nixery/layers"
+	"github.com/google/nixery/logs"
+	mf "github.com/google/nixery/manifest"
+	"github.com/google/nixery/storage"
+	log "github.com/sirupsen/logrus"
+)
+
+// ManifestMediaType is the Content-Type used for the manifest itself. This
+// corresponds to the "Image Manifest V2, Schema 2" described on this page:
+//
+// https://docs.docker.com/registry/spec/manifest-v2-2/
+const manifestMediaType string = "application/vnd.docker.distribution.manifest.v2+json"
+
+// This variable will be initialised during the build process and set
+// to the hash of the entire Nixery source tree.
+var version string = "devel"
+
+// Regexes matching the V2 Registry API routes. This only includes the
+// routes required for serving images, since pushing and other such
+// functionality is not available.
+var (
+	manifestRegex = regexp.MustCompile(`^/v2/([\w|\-|\.|\_|\/]+)/manifests/([\w|\-|\.|\_]+)$`)
+	blobRegex     = regexp.MustCompile(`^/v2/([\w|\-|\.|\_|\/]+)/(blobs|manifests)/sha256:(\w+)$`)
+)
+
+// Downloads the popularity information for the package set from the
+// URL specified in Nixery's configuration.
+func downloadPopularity(url string) (layers.Popularity, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("popularity download from '%s' returned status: %s\n", url, resp.Status)
+	}
+
+	j, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	var pop layers.Popularity
+	err = json.Unmarshal(j, &pop)
+	if err != nil {
+		return nil, err
+	}
+
+	return pop, nil
+}
+
+// Error format corresponding to the registry protocol V2 specification. This
+// allows feeding back errors to clients in a way that can be presented to
+// users.
+type registryError struct {
+	Code    string `json:"code"`
+	Message string `json:"message"`
+}
+
+type registryErrors struct {
+	Errors []registryError `json:"errors"`
+}
+
+func writeError(w http.ResponseWriter, status int, code, message string) {
+	err := registryErrors{
+		Errors: []registryError{
+			{code, message},
+		},
+	}
+	json, _ := json.Marshal(err)
+
+	w.WriteHeader(status)
+	w.Header().Add("Content-Type", "application/json")
+	w.Write(json)
+}
+
+type registryHandler struct {
+	state *builder.State
+}
+
+// Serve a manifest by tag, building it via Nix and populating caches
+// if necessary.
+func (h *registryHandler) serveManifestTag(w http.ResponseWriter, r *http.Request, name string, tag string) {
+	log.WithFields(log.Fields{
+		"image": name,
+		"tag":   tag,
+	}).Info("requesting image manifest")
+
+	image := builder.ImageFromName(name, tag)
+	buildResult, err := builder.BuildImage(r.Context(), h.state, &image)
+
+	if err != nil {
+		writeError(w, 500, "UNKNOWN", "image build failure")
+
+		log.WithError(err).WithFields(log.Fields{
+			"image": name,
+			"tag":   tag,
+		}).Error("failed to build image manifest")
+
+		return
+	}
+
+	// Some error types have special handling, which is applied
+	// here.
+	if buildResult.Error == "not_found" {
+		s := fmt.Sprintf("Could not find Nix packages: %v", buildResult.Pkgs)
+		writeError(w, 404, "MANIFEST_UNKNOWN", s)
+
+		log.WithFields(log.Fields{
+			"image":    name,
+			"tag":      tag,
+			"packages": buildResult.Pkgs,
+		}).Warn("could not find Nix packages")
+
+		return
+	}
+
+	// This marshaling error is ignored because we know that this
+	// field represents valid JSON data.
+	manifest, _ := json.Marshal(buildResult.Manifest)
+	w.Header().Add("Content-Type", manifestMediaType)
+
+	// The manifest needs to be persisted to the blob storage (to become
+	// available for clients that fetch manifests by their hash, e.g.
+	// containerd) and served to the client.
+	//
+	// Since we have no stable key to address this manifest (it may be
+	// uncacheable, yet still addressable by blob) we need to separate
+	// out the hashing, uploading and serving phases. The latter is
+	// especially important as clients may start to fetch it by digest
+	// as soon as they see a response.
+	sha256sum := fmt.Sprintf("%x", sha256.Sum256(manifest))
+	path := "layers/" + sha256sum
+	ctx := context.TODO()
+
+	_, _, err = h.state.Storage.Persist(ctx, path, mf.ManifestType, func(sw io.Writer) (string, int64, error) {
+		// We already know the hash, so no additional hash needs to be
+		// constructed here.
+		written, err := sw.Write(manifest)
+		return sha256sum, int64(written), err
+	})
+
+	if err != nil {
+		writeError(w, 500, "MANIFEST_UPLOAD", "could not upload manifest to blob store")
+
+		log.WithError(err).WithFields(log.Fields{
+			"image": name,
+			"tag":   tag,
+		}).Error("could not upload manifest")
+
+		return
+	}
+
+	w.Write(manifest)
+}
+
+// serveBlob serves a blob from storage by digest
+func (h *registryHandler) serveBlob(w http.ResponseWriter, r *http.Request, blobType, digest string) {
+	storage := h.state.Storage
+	err := storage.Serve(digest, r, w)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"type":    blobType,
+			"digest":  digest,
+			"backend": storage.Name(),
+		}).Error("failed to serve blob from storage backend")
+	}
+}
+
+// ServeHTTP dispatches HTTP requests to the matching handlers.
+func (h *registryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// Acknowledge that we speak V2 with an empty response
+	if r.RequestURI == "/v2/" {
+		return
+	}
+
+	// Build & serve a manifest by tag
+	manifestMatches := manifestRegex.FindStringSubmatch(r.RequestURI)
+	if len(manifestMatches) == 3 {
+		h.serveManifestTag(w, r, manifestMatches[1], manifestMatches[2])
+		return
+	}
+
+	// Serve a blob by digest
+	layerMatches := blobRegex.FindStringSubmatch(r.RequestURI)
+	if len(layerMatches) == 4 {
+		h.serveBlob(w, r, layerMatches[2], layerMatches[3])
+		return
+	}
+
+	log.WithField("uri", r.RequestURI).Info("unsupported registry route")
+
+	w.WriteHeader(404)
+}
+
+func main() {
+	logs.Init(version)
+	cfg, err := config.FromEnv()
+	if err != nil {
+		log.WithError(err).Fatal("failed to load configuration")
+	}
+
+	var s storage.Backend
+
+	switch cfg.Backend {
+	case config.GCS:
+		s, err = storage.NewGCSBackend()
+	case config.FileSystem:
+		s, err = storage.NewFSBackend()
+	}
+	if err != nil {
+		log.WithError(err).Fatal("failed to initialise storage backend")
+	}
+
+	log.WithField("backend", s.Name()).Info("initialised storage backend")
+
+	cache, err := builder.NewCache()
+	if err != nil {
+		log.WithError(err).Fatal("failed to instantiate build cache")
+	}
+
+	var pop layers.Popularity
+	if cfg.PopUrl != "" {
+		pop, err = downloadPopularity(cfg.PopUrl)
+		if err != nil {
+			log.WithError(err).WithField("popURL", cfg.PopUrl).
+				Fatal("failed to fetch popularity information")
+		}
+	}
+
+	state := builder.State{
+		Cache:   &cache,
+		Cfg:     cfg,
+		Pop:     pop,
+		Storage: s,
+	}
+
+	log.WithFields(log.Fields{
+		"version": version,
+		"port":    cfg.Port,
+	}).Info("starting Nixery")
+
+	// All /v2/ requests belong to the registry handler.
+	http.Handle("/v2/", &registryHandler{
+		state: &state,
+	})
+
+	// All other roots are served by the static file server.
+	webDir := http.Dir(cfg.WebDir)
+	http.Handle("/", http.FileServer(webDir))
+
+	log.Fatal(http.ListenAndServe(":"+cfg.Port, nil))
+}
diff --git a/tools/nixery/config/config.go b/tools/nixery/config/config.go
new file mode 100644
index 0000000000..73ff5c8356
--- /dev/null
+++ b/tools/nixery/config/config.go
@@ -0,0 +1,73 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package config implements structures to store Nixery's configuration at
+// runtime as well as the logic for instantiating this configuration from the
+// environment.
+package config
+
+import (
+	"os"
+
+	log "github.com/sirupsen/logrus"
+)
+
+func getConfig(key, desc, def string) string {
+	value := os.Getenv(key)
+	if value == "" && def == "" {
+		log.WithFields(log.Fields{
+			"option":      key,
+			"description": desc,
+		}).Fatal("missing required configuration envvar")
+	} else if value == "" {
+		return def
+	}
+
+	return value
+}
+
+// Backend represents the possible storage backend types
+type Backend int
+
+const (
+	GCS = iota
+	FileSystem
+)
+
+// Config holds the Nixery configuration options.
+type Config struct {
+	Port    string    // Port on which to launch HTTP server
+	Pkgs    PkgSource // Source for Nix package set
+	Timeout string    // Timeout for a single Nix builder (seconds)
+	WebDir  string    // Directory with static web assets
+	PopUrl  string    // URL to the Nix package popularity count
+	Backend Backend   // Storage backend to use for Nixery
+}
+
+func FromEnv() (Config, error) {
+	pkgs, err := pkgSourceFromEnv()
+	if err != nil {
+		return Config{}, err
+	}
+
+	var b Backend
+	switch os.Getenv("NIXERY_STORAGE_BACKEND") {
+	case "gcs":
+		b = GCS
+	case "filesystem":
+		b = FileSystem
+	default:
+		log.WithField("values", []string{
+			"gcs",
+		}).Fatal("NIXERY_STORAGE_BACKEND must be set to a supported value (gcs or filesystem)")
+	}
+
+	return Config{
+		Port:    getConfig("PORT", "HTTP port", ""),
+		Pkgs:    pkgs,
+		Timeout: getConfig("NIX_TIMEOUT", "Nix builder timeout", "60"),
+		WebDir:  getConfig("WEB_DIR", "Static web file dir", ""),
+		PopUrl:  os.Getenv("NIX_POPULARITY_URL"),
+		Backend: b,
+	}, nil
+}
diff --git a/tools/nixery/config/pkgsource.go b/tools/nixery/config/pkgsource.go
new file mode 100644
index 0000000000..c7508a4d3a
--- /dev/null
+++ b/tools/nixery/config/pkgsource.go
@@ -0,0 +1,148 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+package config
+
+import (
+	"crypto/sha1"
+	"encoding/json"
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// PkgSource represents the source from which the Nix package set used
+// by Nixery is imported. Users configure the source by setting one of
+// the supported environment variables.
+type PkgSource interface {
+	// Convert the package source into the representation required
+	// for calling Nix.
+	Render(tag string) (string, string)
+
+	// Create a key by which builds for this source and image
+	// combination can be cached.
+	//
+	// The empty string means that this value is not cacheable due
+	// to the package source being a moving target (such as a
+	// channel).
+	CacheKey(pkgs []string, tag string) string
+}
+
+type GitSource struct {
+	repository string
+}
+
+// Regex to determine whether a git reference is a commit hash or
+// something else (branch/tag).
+//
+// Used to check whether a git reference is cacheable, and to pass the
+// correct git structure to Nix.
+//
+// Note: If a user creates a branch or tag with the name of a commit
+// and references it intentionally, this heuristic will fail.
+var commitRegex = regexp.MustCompile(`^[0-9a-f]{40}$`)
+
+func (g *GitSource) Render(tag string) (string, string) {
+	args := map[string]string{
+		"url": g.repository,
+	}
+
+	// The 'git' source requires a tag to be present. If the user
+	// has not specified one, it is assumed that the default
+	// 'master' branch should be used.
+	if tag == "latest" || tag == "" {
+		tag = "master"
+	}
+
+	if commitRegex.MatchString(tag) {
+		args["rev"] = tag
+	} else {
+		args["ref"] = tag
+	}
+
+	j, _ := json.Marshal(args)
+
+	return "git", string(j)
+}
+
+func (g *GitSource) CacheKey(pkgs []string, tag string) string {
+	// Only full commit hashes can be used for caching, as
+	// everything else is potentially a moving target.
+	if !commitRegex.MatchString(tag) {
+		return ""
+	}
+
+	unhashed := strings.Join(pkgs, "") + tag
+	hashed := fmt.Sprintf("%x", sha1.Sum([]byte(unhashed)))
+
+	return hashed
+}
+
+type NixChannel struct {
+	channel string
+}
+
+func (n *NixChannel) Render(tag string) (string, string) {
+	return "nixpkgs", n.channel
+}
+
+func (n *NixChannel) CacheKey(pkgs []string, tag string) string {
+	// Since Nix channels are downloaded from the nixpkgs-channels
+	// Github, users can specify full commit hashes as the
+	// "channel", in which case builds are cacheable.
+	if !commitRegex.MatchString(n.channel) {
+		return ""
+	}
+
+	unhashed := strings.Join(pkgs, "") + n.channel
+	hashed := fmt.Sprintf("%x", sha1.Sum([]byte(unhashed)))
+
+	return hashed
+}
+
+type PkgsPath struct {
+	path string
+}
+
+func (p *PkgsPath) Render(tag string) (string, string) {
+	return "path", p.path
+}
+
+func (p *PkgsPath) CacheKey(pkgs []string, tag string) string {
+	// Path-based builds are not currently cacheable because we
+	// have no local hash of the package folder's state easily
+	// available.
+	return ""
+}
+
+// Retrieve a package source from the environment. If no source is
+// specified, the Nix code will default to a recent NixOS channel.
+func pkgSourceFromEnv() (PkgSource, error) {
+	if channel := os.Getenv("NIXERY_CHANNEL"); channel != "" {
+		log.WithField("channel", channel).Info("using Nix package set from Nix channel or commit")
+
+		return &NixChannel{
+			channel: channel,
+		}, nil
+	}
+
+	if git := os.Getenv("NIXERY_PKGS_REPO"); git != "" {
+		log.WithField("repo", git).Info("using Nix package set from git repository")
+
+		return &GitSource{
+			repository: git,
+		}, nil
+	}
+
+	if path := os.Getenv("NIXERY_PKGS_PATH"); path != "" {
+		log.WithField("path", path).Info("using Nix package set at local path")
+
+		return &PkgsPath{
+			path: path,
+		}, nil
+	}
+
+	return nil, fmt.Errorf("no valid package source has been specified")
+}
diff --git a/tools/nixery/default.nix b/tools/nixery/default.nix
new file mode 100644
index 0000000000..3c9e50cac0
--- /dev/null
+++ b/tools/nixery/default.nix
@@ -0,0 +1,129 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+# This function header aims to provide compatibility between builds of
+# Nixery taking place inside/outside of the TVL depot.
+#
+# In the future, Nixery will transition to using //nix/buildGo for its
+# build system and this will need some major adaptations to support
+# that.
+{ depot ? { nix.readTree.drvTargets = x: x; }
+, pkgs ? import <nixpkgs> { }
+, preLaunch ? ""
+, extraPackages ? [ ]
+, maxLayers ? 20
+, commitHash ? null
+, ...
+}@args:
+
+with pkgs;
+
+let
+  inherit (pkgs) buildGoModule;
+
+  # Avoid extracting this from git until we have a way to plumb
+  # through revision numbers.
+  nixery-commit-hash = "depot";
+
+in
+depot.nix.readTree.drvTargets rec {
+  # Implementation of the Nix image building logic
+  nixery-prepare-image = import ./prepare-image { inherit pkgs; };
+
+  # Use mdBook to build a static asset page which Nixery can then
+  # serve. This is primarily used for the public instance at
+  # nixery.dev.
+  nixery-book = callPackage ./docs { };
+
+  nixery-popcount = callPackage ./popcount { };
+
+  # Build Nixery's Go code, resulting in the binaries used for various
+  # bits of functionality.
+  #
+  # The server binary is wrapped to ensure that required environment
+  # variables are set at runtime.
+  nixery = buildGoModule rec {
+    name = "nixery";
+    src = ./.;
+    doCheck = true;
+
+    # Needs to be updated after every modification of go.mod/go.sum
+    vendorSha256 = "115dfdhpklgmp6dsy59bp0i9inqim208lf1sqbnl9jy91bnnbl32";
+
+    buildFlagsArray = [
+      "-ldflags=-s -w -X main.version=${nixery-commit-hash}"
+    ];
+
+    nativeBuildInputs = [ makeWrapper ];
+    postInstall = ''
+      wrapProgram $out/bin/server \
+        --set WEB_DIR "${nixery-book}" \
+        --prefix PATH : ${nixery-prepare-image}/bin
+    '';
+
+    # Nixery is mirrored to Github at tazjin/nixery; this is
+    # automatically updated from CI for canon builds.
+    passthru.meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+      filter = ":/tools/nixery";
+      remote = "git@github.com:tazjin/nixery.git";
+      ref = "refs/heads/master";
+    };
+  };
+
+  # Container image containing Nixery and Nix itself. This image can
+  # be run on Kubernetes, published on AppEngine or whatever else is
+  # desired.
+  nixery-image =
+    let
+      # Wrapper script for the wrapper script (meta!) which configures
+      # the container environment appropriately.
+      #
+      # Most importantly, sandboxing is disabled to avoid privilege
+      # issues in containers.
+      nixery-launch-script = writeShellScriptBin "nixery" ''
+        set -e
+        export PATH=${coreutils}/bin:$PATH
+        export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt
+        mkdir -p /tmp
+
+        # Create the build user/group required by Nix
+        echo 'nixbld:x:30000:nixbld' >> /etc/group
+        echo 'nixbld:x:30000:30000:nixbld:/tmp:/bin/bash' >> /etc/passwd
+        echo 'root:x:0:0:root:/root:/bin/bash' >> /etc/passwd
+        echo 'root:x:0:' >> /etc/group
+
+        # Disable sandboxing to avoid running into privilege issues
+        mkdir -p /etc/nix
+        echo 'sandbox = false' >> /etc/nix/nix.conf
+
+        # In some cases users building their own image might want to
+        # customise something on the inside (e.g. set up an environment
+        # for keys or whatever).
+        #
+        # This can be achieved by setting a 'preLaunch' script.
+        ${preLaunch}
+
+        exec ${nixery}/bin/server
+      '';
+    in
+    dockerTools.buildLayeredImage {
+      name = "nixery";
+      config.Cmd = [ "${nixery-launch-script}/bin/nixery" ];
+
+      inherit maxLayers;
+      contents = [
+        bashInteractive
+        cacert
+        coreutils
+        git
+        gnutar
+        gzip
+        iana-etc
+        nix
+        nixery-prepare-image
+        nixery-launch-script
+        openssh
+        zlib
+      ] ++ extraPackages;
+    };
+}
diff --git a/tools/nixery/docs/.gitignore b/tools/nixery/docs/.gitignore
new file mode 100644
index 0000000000..7585238efe
--- /dev/null
+++ b/tools/nixery/docs/.gitignore
@@ -0,0 +1 @@
+book
diff --git a/tools/nixery/docs/book.toml b/tools/nixery/docs/book.toml
new file mode 100644
index 0000000000..bf6ccbb27f
--- /dev/null
+++ b/tools/nixery/docs/book.toml
@@ -0,0 +1,8 @@
+[book]
+authors = ["Vincent Ambo <tazjin@google.com>"]
+language = "en"
+multilingual = false
+src = "src"
+
+[output.html]
+additional-css = ["theme/nixery.css"]
diff --git a/tools/nixery/docs/default.nix b/tools/nixery/docs/default.nix
new file mode 100644
index 0000000000..876a34dcf1
--- /dev/null
+++ b/tools/nixery/docs/default.nix
@@ -0,0 +1,26 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+# Builds the documentation page using the Rust project's 'mdBook'
+# tool.
+#
+# Some of the documentation is pulled in and included from other
+# sources.
+
+{ fetchFromGitHub, mdbook, runCommand, rustPlatform }:
+
+let
+  nix-1p = fetchFromGitHub {
+    owner = "tazjin";
+    repo = "nix-1p";
+    rev = "9f0baf5e270128d9101ba4446cf6844889e399a2";
+    sha256 = "1pf9i90gn98vz67h296w5lnwhssk62dc6pij983dff42dbci7lhj";
+  };
+in
+runCommand "nixery-book" { } ''
+  mkdir -p $out
+  cp -r ${./.}/* .
+  chmod -R a+w src
+  cp ${nix-1p}/README.md src/nix-1p.md
+  ${mdbook}/bin/mdbook build -d $out
+''
diff --git a/tools/nixery/docs/src/SUMMARY.md b/tools/nixery/docs/src/SUMMARY.md
new file mode 100644
index 0000000000..f1d68a3ac4
--- /dev/null
+++ b/tools/nixery/docs/src/SUMMARY.md
@@ -0,0 +1,8 @@
+# Summary
+
+- [Nixery](./nixery.md)
+  - [Under the hood](./under-the-hood.md)
+  - [Caching](./caching.md)
+  - [Run your own Nixery](./run-your-own.md)
+- [Nix](./nix.md)
+  - [Nix, the language](./nix-1p.md)
diff --git a/tools/nixery/docs/src/caching.md b/tools/nixery/docs/src/caching.md
new file mode 100644
index 0000000000..05ea68ef60
--- /dev/null
+++ b/tools/nixery/docs/src/caching.md
@@ -0,0 +1,69 @@
+# Caching in Nixery
+
+This page gives a quick overview over the caching done by Nixery. All cache data
+is written to Nixery's storage bucket and is based on deterministic identifiers
+or content-addressing, meaning that cache entries under the same key *never
+change*.
+
+## Manifests
+
+Manifests of builds are cached at `$BUCKET/manifests/$KEY`. The effect of this
+cache is that multiple instances of Nixery do not need to rebuild the same
+manifest from scratch.
+
+Since the manifest cache is populated only *after* layers are uploaded, Nixery
+can immediately return the manifest to its clients without needing to check
+whether layers have been uploaded already.
+
+`$KEY` is generated by creating a SHA1 hash of the requested content of a
+manifest plus the package source specification.
+
+Manifests are *only* cached if the package source specification is *not* a
+moving target.
+
+Manifest caching *only* applies in the following cases:
+
+* package source specification is a specific git commit
+* package source specification is a specific NixOS/nixpkgs commit
+
+Manifest caching *never* applies in the following cases:
+
+* package source specification is a local file path (i.e. `NIXERY_PKGS_PATH`)
+* package source specification is a NixOS channel (e.g. `NIXERY_CHANNEL=nixos-20.09`)
+* package source specification is a git branch or tag (e.g. `staging`, `master` or `latest`)
+
+It is thus always preferable to request images from a fully-pinned package
+source.
+
+Manifests can be removed from the manifest cache without negative consequences.
+
+## Layer tarballs
+
+Layer tarballs are the files that Nixery clients retrieve from the storage
+bucket to download an image.
+
+They are stored content-addressably at `$BUCKET/layers/$SHA256HASH` and layer
+requests sent to Nixery will redirect directly to this storage location.
+
+The effect of this cache is that Nixery does not need to upload identical layers
+repeatedly. When Nixery notices that a layer already exists in GCS it will skip
+uploading this layer.
+
+Removing layers from the cache is *potentially problematic* if there are cached
+manifests or layer builds referencing those layers.
+
+To clean up layers, a user must ensure that no other cached resources still
+reference these layers.
+
+## Layer builds
+
+Layer builds are cached at `$BUCKET/builds/$HASH`, where `$HASH` is a SHA1 of
+the Nix store paths included in the layer.
+
+The content of the cached entries is a JSON-object that contains the SHA256
+hashes and sizes of the built layer.
+
+The effect of this cache is that different instances of Nixery will not build,
+hash and upload layers that have identical contents across different instances.
+
+Layer builds can be removed from the cache without negative consequences.
diff --git a/tools/nixery/docs/src/nix-1p.md b/tools/nixery/docs/src/nix-1p.md
new file mode 100644
index 0000000000..a21234150f
--- /dev/null
+++ b/tools/nixery/docs/src/nix-1p.md
@@ -0,0 +1,2 @@
+This page is a placeholder. During the build process, it is replaced by the
+actual `nix-1p` guide from https://github.com/tazjin/nix-1p
diff --git a/tools/nixery/docs/src/nix.md b/tools/nixery/docs/src/nix.md
new file mode 100644
index 0000000000..2bfd75a692
--- /dev/null
+++ b/tools/nixery/docs/src/nix.md
@@ -0,0 +1,31 @@
+# Nix
+
+These sections are designed to give some background information on what Nix is.
+If you've never heard of Nix before looking at Nixery, this might just be the
+page for you!
+
+[Nix][] is a functional package-manager that comes with a number of advantages
+over traditional package managers, such as side-by-side installs of different
+package versions, atomic updates, easy customisability, simple binary caching
+and much more. Feel free to explore the [Nix website][Nix] for an overview of
+Nix itself.
+
+Nix uses a custom programming language also called Nix, which is explained here
+[on its own page][nix-1p].
+
+In addition to the package manager and language, the Nix project also maintains
+[NixOS][] - a Linux distribution built entirely on Nix. On NixOS, users can
+declaratively describe the *entire* configuration of their system and perform
+updates/rollbacks to other system configurations with ease.
+
+Most Nix packages are tracked in the [Nix package set][nixpkgs], usually simply
+referred to as `nixpkgs`. It contains tens of thousands of packages already!
+
+Nixery (which you are looking at!) provides an easy & simple way to get started
+with Nix, in fact you don't even need to know that you're using Nix to make use
+of Nixery.
+
+[Nix]: https://nixos.org/nix/
+[nix-1p]: nix-1p.html
+[NixOS]: https://nixos.org/
+[nixpkgs]: https://github.com/nixos/nixpkgs
diff --git a/tools/nixery/docs/src/nixery-logo.png b/tools/nixery/docs/src/nixery-logo.png
new file mode 100644
index 0000000000..fcf77df3d6
--- /dev/null
+++ b/tools/nixery/docs/src/nixery-logo.png
Binary files differdiff --git a/tools/nixery/docs/src/nixery.md b/tools/nixery/docs/src/nixery.md
new file mode 100644
index 0000000000..d9ba179010
--- /dev/null
+++ b/tools/nixery/docs/src/nixery.md
@@ -0,0 +1,80 @@
+![Nixery](./nixery-logo.png)
+
+------------
+
+Welcome to this instance of [Nixery][]. It provides ad-hoc container images that
+contain packages from the [Nix][] package manager. Images with arbitrary
+packages can be requested via the image name.
+
+Nix not only provides the packages to include in the images, but also builds the
+images themselves by using a special [layering strategy][] that optimises for
+cache efficiency.
+
+For general information on why using Nix makes sense for container images, check
+out [this blog post][layers].
+
+## Demo
+
+<script src="https://asciinema.org/a/262583.js" id="asciicast-262583" async data-autoplay="true" data-loop="true"></script>
+
+## Quick start
+
+Simply pull an image from this registry, separating each package you want
+included by a slash:
+
+    docker pull nixery.dev/shell/git/htop
+
+This gives you an image with `git`, `htop` and an interactively configured
+shell. You could run it like this:
+
+    docker run -ti nixery.dev/shell/git/htop bash
+
+Each path segment corresponds either to a key in the Nix package set, or a
+meta-package that automatically expands to several other packages.
+
+Meta-packages **must** be the first path component if they are used. Currently
+there are only two meta-packages:
+- `shell`, which provides a `bash`-shell with interactive configuration and 
+  standard tools like `coreutils`.
+- `arm64`, which provides ARM64 binaries.
+
+**Tip:** When pulling from a private Nixery instance, replace `nixery.dev` in
+the above examples with your registry address.
+
+## FAQ
+
+If you have a question that is not answered here, feel free to file an issue on
+Github so that we can get it included in this section. The volume of questions
+is quite low, thus by definition your question is already frequently asked.
+
+### Where is the source code for this?
+
+Over [on Github][Nixery]. It is licensed under the Apache 2.0 license. Consult
+the documentation entries in the sidebar for information on how to set up your
+own instance of Nixery.
+
+### Which revision of `nixpkgs` is used for the builds?
+
+The instance at `nixery.dev` tracks a recent NixOS channel, currently NixOS
+20.09. The channel is updated several times a day.
+
+Private registries might be configured to track a different channel (such as
+`nixos-unstable`) or even track a git repository with custom packages.
+
+### Should I depend on `nixery.dev` in production?
+
+While we appreciate the enthusiasm, if you would like to use Nixery in your
+production project we recommend setting up a private instance. The public Nixery
+at `nixery.dev` is run on a best-effort basis and we make no guarantees about
+availability.
+
+### Who made this?
+
+Nixery was written by [tazjin][], but many people have contributed to Nix over
+time, maybe you could become one of them?
+
+[Nixery]: https://github.com/tazjin/nixery
+[Nix]: https://nixos.org/nix
+[layering strategy]: https://storage.googleapis.com/nixdoc/nixery-layers.html
+[layers]: https://grahamc.com/blog/nix-and-layered-docker-images
+[tazjin]: https://tazj.in
diff --git a/tools/nixery/docs/src/run-your-own.md b/tools/nixery/docs/src/run-your-own.md
new file mode 100644
index 0000000000..7ed8bdd0bc
--- /dev/null
+++ b/tools/nixery/docs/src/run-your-own.md
@@ -0,0 +1,194 @@
+## Run your own Nixery
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+
+- [0. Prerequisites](#0-prerequisites)
+- [1. Choose a package set](#1-choose-a-package-set)
+- [2. Build Nixery itself](#2-build-nixery-itself)
+- [3. Prepare configuration](#3-prepare-configuration)
+- [4. Deploy Nixery](#4-deploy-nixery)
+- [5. Productionise](#5-productionise)
+
+<!-- markdown-toc end -->
+
+
+---------
+
+⚠ This page is still under construction! ⚠
+
+--------
+
+Running your own Nixery is not difficult, but requires some setup. Follow the
+steps below to get up & running.
+
+*Note:* Nixery can be run inside of a [GKE][] cluster, providing a local service
+from which images can be requested. Documentation for how to set this up is
+forthcoming, please see [nixery#4][].
+
+## 0. Prerequisites
+
+To run Nixery, you must have:
+
+* [Nix][] (to build Nixery itself)
+* Somewhere to run it (your own server, Google AppEngine, a Kubernetes cluster,
+  whatever!)
+* *Either* a [Google Cloud Storage][gcs] bucket in which to store & serve layers,
+  *or* a comfortable amount of disk space
+
+Note that while the main Nixery process is a server written in Go,
+it invokes a script that itself relies on Nix to be available.
+You can compile the main Nixery daemon without Nix, but it won't
+work without Nix.
+
+(If you are completely new to Nix and don't know how to get
+started, check the [Nix installation documentation][nixinstall].)
+
+## 1. Choose a package set
+
+When running your own Nixery you need to decide which package set you want to
+serve. By default, Nixery builds packages from a recent NixOS channel which
+ensures that most packages are cached upstream and no expensive builds need to
+be performed for trivial things.
+
+However if you are running a private Nixery, chances are high that you intend to
+use it with your own packages. There are three options available:
+
+1. Specify an upstream Nix/NixOS channel[^1], such as `nixos-20.09` or
+   `nixos-unstable`.
+2. Specify your own git-repository with a custom package set[^2]. This makes it
+   possible to pull different tags, branches or commits by modifying the image
+   tag.
+3. Specify a local file path containing a Nix package set. Where this comes from
+   or what it contains is up to you.
+
+## 2. Build Nixery itself
+
+### 2.1. With a container image
+
+The easiest way to run Nixery is to build a container image. This
+section assumes that the container runtime used is Docker, please
+modify instructions accordingly if you are using something else.
+
+With a working Nix installation, you can clone and build the Nixery
+image like this:
+
+```
+git clone https://code.tvl.fyi/depot.git:/tools/nixery.git
+nix-build -A nixery-image
+```
+
+This will create a `result`-symlink which points to a tarball containing the
+image. In Docker, this tarball can be loaded by using `docker load -i result`.
+
+### 2.2. Without a container image
+
+*This method might be more convenient if you intend to work on
+the code of the Nixery server itself, because you won't have to
+rebuild (and reload) an image each time to test your changes.*
+
+You will need to run the two following commands at the root of the repo:
+
+* `go build` to build the `nixery` binary;
+* `nix-env --install --file prepare-image/default.nix` to build
+  the required helpers.
+
+## 3. Prepare configuration
+
+Nixery is configured via environment variables.
+
+You must set *all* of these:
+
+* `NIXERY_STORAGE_BACKEND` (must be set to `gcs` or `filesystem`)
+* `PORT`: HTTP port on which Nixery should listen
+* `WEB_DIR`: directory containing static files (see below)
+
+You must set *one* of these:
+
+* `NIXERY_CHANNEL`: The name of a [Nix/NixOS channel][nixchannel] to use for building,
+  for instance `nixos-21.05`
+* `NIXERY_PKGS_REPO`: URL of a git repository containing a package set (uses
+  locally configured SSH/git credentials)
+* `NIXERY_PKGS_PATH`: A local filesystem path containing a Nix package set to use
+  for building
+
+If `NIXERY_STORAGE_BACKEND` is set to `filesystem`, then `STORAGE_PATH`
+must be set to the directory that will hold the registry blobs.
+That directory must be located on a filesystem that supports extended
+attributes (which means that on most systems, `/tmp` won't work).
+
+If `NIXERY_STORAGE_BACKEND` is set to `gcs`, then `GCS_BUCKET`
+must be set to the [Google Cloud Storage][gcs] bucket that will be
+used to store & serve image layers.
+
+You may set *all* of these:
+
+* `NIX_TIMEOUT`: Number of seconds that any Nix builder is allowed to run
+  (defaults to 60)
+
+To authenticate to the configured GCS bucket, Nixery uses Google's [Application
+Default Credentials][ADC]. Depending on your environment this may require
+additional configuration.
+
+If the `GOOGLE_APPLICATION_CREDENTIALS` environment is configured, the service
+account's private key will be used to create [signed URLs for
+layers][signed-urls].
+
+## 4. Start Nixery
+
+Run the image that was built in step 2.1 with all the environment variables
+mentioned above. Alternatively, set all the environment variables and run
+the Nixery server that was built in step 2.2.
+
+Once Nixery is running you can immediately start requesting images from it.
+
+## 5. Productionise
+
+(⚠ Here be dragons! ⚠)
+
+Nixery is still an early project and has not yet been deployed in any production
+environments and some caveats apply.
+
+Notably, Nixery currently does not support any authentication methods, so anyone
+with network access to the registry can retrieve images.
+
+Running a Nixery inside of a fenced-off environment (such as internal to a
+Kubernetes cluster) should be fine, but you should consider to do all of the
+following:
+
+* Issue a TLS certificate for the hostname you are assigning to Nixery. In fact,
+  Docker will refuse to pull images from registries that do not use TLS (with
+  the exception of `.local` domains).
+* Configure signed GCS URLs to avoid having to make your bucket world-readable.
+* Configure request timeouts for Nixery if you have your own web server in front
+  of it. This will be natively supported by Nixery in the future.
+
+## 6. `WEB_DIR`
+
+All the URLs accessed by Docker registry clients start with `/v2/`.
+This means that it is possible to serve a static website from Nixery
+itself (as long as you don't want to serve anything starting with `/v2`).
+This is how, for instance, https://nixery.dev shows the website for Nixery,
+while it is also possible to e.g. `docker pull nixery.dev/shell`.
+
+When running Nixery, you must set the `WEB_DIR` environment variable.
+When Nixery receives requests that don't look like registry requests,
+it tries to serve them using files in the directory indicated by `WEB_DIR`.
+If the directory doesn't exist, Nixery will run fine but serve 404.
+
+-------
+
+[^1]: Nixery will not work with Nix channels older than `nixos-19.03`.
+
+[^2]: This documentation will be updated with instructions on how to best set up
+    a custom Nix repository. Nixery expects custom package sets to be a superset
+    of `nixpkgs`, as it uses `lib` and other features from `nixpkgs`
+    extensively.
+
+[GKE]: https://cloud.google.com/kubernetes-engine/
+[nixery#4]: https://github.com/tazjin/nixery/issues/4
+[Nix]: https://nixos.org/nix
+[gcs]: https://cloud.google.com/storage/
+[signed-urls]: under-the-hood.html#5-image-layers-are-requested
+[ADC]: https://cloud.google.com/docs/authentication/production#finding_credentials_automatically
+[nixinstall]: https://nixos.org/manual/nix/stable/installation/installing-binary.html
+[nixchannel]: https://nixos.wiki/wiki/Nix_channels
diff --git a/tools/nixery/docs/src/under-the-hood.md b/tools/nixery/docs/src/under-the-hood.md
new file mode 100644
index 0000000000..4b79830010
--- /dev/null
+++ b/tools/nixery/docs/src/under-the-hood.md
@@ -0,0 +1,129 @@
+# Under the hood
+
+This page serves as a quick explanation of what happens under-the-hood when an
+image is requested from Nixery.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+
+- [1. The image manifest is requested](#1-the-image-manifest-is-requested)
+- [2. Nix fetches and prepares image content](#2-nix-fetches-and-prepares-image-content)
+- [3. Layers are grouped, created, hashed, and persisted](#3-layers-are-grouped-created-hashed-and-persisted)
+- [4. The manifest is assembled and returned to the client](#4-the-manifest-is-assembled-and-returned-to-the-client)
+- [5. Image layers are requested](#5-image-layers-are-requested)
+
+<!-- markdown-toc end -->
+
+--------
+
+## 1. The image manifest is requested
+
+When container registry clients such as Docker pull an image, the first thing
+they do is ask for the image manifest. This is a JSON document describing which
+layers are contained in an image, as well as some additional auxiliary
+information.
+
+This request is of the form `GET /v2/$imageName/manifests/$imageTag`.
+
+Nixery receives this request and begins by splitting the image name into its
+path components and substituting meta-packages (such as `shell`) for their
+contents.
+
+For example, requesting `shell/htop/git` results in Nixery expanding the image
+name to `["bashInteractive", "coreutils", "htop", "git"]`.
+
+If Nixery is configured with a private Nix repository, it also looks at the
+image tag and substitutes `latest` with `master`.
+
+It then invokes Nix with three parameters:
+
+1. image contents (as above)
+2. image tag
+3. configured package set source
+
+## 2. Nix fetches and prepares image content
+
+Using the parameters above, Nix imports the package set and begins by mapping
+the image names to attributes in the package set.
+
+A special case during this process is packages with uppercase characters in
+their name, for example anything under `haskellPackages`. The registry protocol
+does not allow uppercase characters, so the Nix code will translate something
+like `haskellpackages` (lowercased) to the correct attribute name.
+
+After identifying all contents, Nix uses the `symlinkJoin` function to
+create a special layer with the "symlink farm" required to let the
+image function like a normal disk image.
+
+Nix then returns information about the image contents as well as the
+location of the special layer to Nixery.
+
+## 3. Layers are grouped, created, hashed, and persisted
+
+With the information received from Nix, Nixery determines the contents
+of each layer while optimising for the best possible cache efficiency
+(see the [layering design doc][] for details).
+
+With the grouped layers, Nixery then begins to create compressed
+tarballs with all required contents for each layer. As these tarballs
+are being created, they are simultaneously being hashed (as the image
+manifest must contain the content-hashes of all layers) and persisted
+to storage.
+
+Storage can be either a remote [Google Cloud Storage][gcs] bucket, or
+a local filesystem path.
+
+During this step, Nixery checks its build cache (see [Caching][]) to
+determine whether a layer needs to be built or is already cached from
+a previous build.
+
+*Note:* While this step is running (which can take some time in the case of
+large first-time image builds), the registry client is left hanging waiting for
+an HTTP response. Unfortunately the registry protocol does not allow for any
+feedback back to the user at this point, so from the user's perspective things
+just ... hang, for a moment.
+
+## 4. The manifest is assembled and returned to the client
+
+Once armed with the hashes of all required layers, Nixery assembles
+the OCI Container Image manifest which describes the structure of the
+built image and names all of its layers by their content hash.
+
+This manifest is returned to the client.
+
+## 5. Image layers are requested
+
+The client now inspects the manifest and determines which of the
+layers it is currently missing based on their content hashes. Note
+that different container runtimes will handle this differently, and in
+the case of certain engine and storage driver combinations (e.g.
+Docker with OverlayFS) layers might be downloaded again even if they
+are already present.
+
+For each of the missing layers, the client now issues a request to
+Nixery that looks like this:
+
+`GET /v2/${imageName}/blob/sha256:${layerHash}`
+
+Nixery receives these requests and handles them based on the
+configured storage backend.
+
+If the storage backend is GCS, it *redirects* them to Google Cloud
+Storage URLs, responding with an `HTTP 303 See Other` status code and
+the actual download URL of the layer.
+
+Nixery supports using private buckets which are not generally world-readable, in
+which case [signed URLs][] are constructed using a private key. These allow the
+registry client to download each layer without needing to care about how the
+underlying authentication works.
+
+If the storage backend is the local filesystem, Nixery will attempt to
+serve the layer back to the client from disk.
+
+---------
+
+That's it. After these five steps the registry client has retrieved all it needs
+to run the image produced by Nixery.
+
+[gcs]: https://cloud.google.com/storage/
+[signed URLs]: https://cloud.google.com/storage/docs/access-control/signed-urls
+[layering design doc]: https://storage.googleapis.com/nixdoc/nixery-layers.html
diff --git a/tools/nixery/docs/theme/favicon.png b/tools/nixery/docs/theme/favicon.png
new file mode 100644
index 0000000000..f510bde197
--- /dev/null
+++ b/tools/nixery/docs/theme/favicon.png
Binary files differdiff --git a/tools/nixery/docs/theme/nixery.css b/tools/nixery/docs/theme/nixery.css
new file mode 100644
index 0000000000..c240e693d5
--- /dev/null
+++ b/tools/nixery/docs/theme/nixery.css
@@ -0,0 +1,3 @@
+h2, h3 {
+  margin-top: 1em;
+}
diff --git a/tools/nixery/go.mod b/tools/nixery/go.mod
new file mode 100644
index 0000000000..005daa337d
--- /dev/null
+++ b/tools/nixery/go.mod
@@ -0,0 +1,13 @@
+module github.com/google/nixery
+
+go 1.15
+
+require (
+	cloud.google.com/go/storage v1.22.1
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/google/go-cmp v0.5.8
+	github.com/pkg/xattr v0.4.7
+	github.com/sirupsen/logrus v1.8.1
+	golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
+	gonum.org/v1/gonum v0.11.0
+)
diff --git a/tools/nixery/go.sum b/tools/nixery/go.sum
new file mode 100644
index 0000000000..d6afad227e
--- /dev/null
+++ b/tools/nixery/go.sum
@@ -0,0 +1,706 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
+cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
+cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
+cloud.google.com/go/compute v1.6.0 h1:XdQIN5mdPTSBVwSIVDuY5e8ZzVAccsHvD3qTEz4zIps=
+cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
+cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg=
+cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
+github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
+github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
+github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
+github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
+github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
+github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
+github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
+github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
+github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/xattr v0.4.7 h1:XoA3KzmFvyPlH4RwX5eMcgtzcaGBaSvgt3IoFQfbrmQ=
+github.com/pkg/xattr v0.4.7/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
+gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
+gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
+gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
+google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
+google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE=
+google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
+google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335 h1:2D0OT6tPVdrQTOnVe1VQjfJPTED6EZ7fdJ/f6Db6OsY=
+google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/tools/nixery/layers/layers.go b/tools/nixery/layers/layers.go
new file mode 100644
index 0000000000..131a0cdbed
--- /dev/null
+++ b/tools/nixery/layers/layers.go
@@ -0,0 +1,353 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// This package reads an export reference graph (i.e. a graph representing the
+// runtime dependencies of a set of derivations) created by Nix and groups it in
+// a way that is likely to match the grouping for other derivation sets with
+// overlapping dependencies.
+//
+// This is used to determine which derivations to include in which layers of a
+// container image.
+//
+// # Inputs
+//
+// * a graph of Nix runtime dependencies, generated via exportReferenceGraph
+// * popularity values of each package in the Nix package set (in the form of a
+//   direct reference count)
+// * a maximum number of layers to allocate for the image (the "layer budget")
+//
+// # Algorithm
+//
+// It works by first creating a (directed) dependency tree:
+//
+// img (root node)
+// │
+// ├───> A ─────┐
+// │            v
+// ├───> B ───> E
+// │            ^
+// ├───> C ─────┘
+// │     │
+// │     v
+// └───> D ───> F
+//       │
+//       └────> G
+//
+// Each node (i.e. package) is then visited to determine how important
+// it is to separate this node into its own layer, specifically:
+//
+// 1. Is the node within a certain threshold percentile of absolute
+//    popularity within all of nixpkgs? (e.g. `glibc`, `openssl`)
+//
+// 2. Is the node's runtime closure above a threshold size? (e.g. 100MB)
+//
+// In either case, a bit is flipped for this node representing each
+// condition and an edge to it is inserted directly from the image
+// root, if it does not already exist.
+//
+// For the rest of the example we assume 'G' is above the threshold
+// size and 'E' is popular.
+//
+// This tree is then transformed into a dominator tree:
+//
+// img
+// │
+// ├───> A
+// ├───> B
+// ├───> C
+// ├───> E
+// ├───> D ───> F
+// └───> G
+//
+// Specifically this means that the paths to A, B, C, E, G, and D
+// always pass through the root (i.e. are dominated by it), whilst F
+// is dominated by D (all paths go through it).
+//
+// The top-level subtrees are considered as the initially selected
+// layers.
+//
+// If the list of layers fits within the layer budget, it is returned.
+//
+// Otherwise, a merge rating is calculated for each layer. This is the
+// product of the layer's total size and its root node's popularity.
+//
+// Layers are then merged in ascending order of merge ratings until
+// they fit into the layer budget.
+//
+// # Threshold values
+//
+// Threshold values for the partitioning conditions mentioned above
+// have not yet been determined, but we will make a good first guess
+// based on gut feeling and proceed to measure their impact on cache
+// hits/misses.
+//
+// # Example
+//
+// Using the logic described above as well as the example presented in
+// the introduction, this program would create the following layer
+// groupings (assuming no additional partitioning):
+//
+// Layer budget: 1
+// Layers: { A, B, C, D, E, F, G }
+//
+// Layer budget: 2
+// Layers: { G }, { A, B, C, D, E, F }
+//
+// Layer budget: 3
+// Layers: { G }, { E }, { A, B, C, D, F }
+//
+// Layer budget: 4
+// Layers: { G }, { E }, { D, F }, { A, B, C }
+//
+// ...
+//
+// Layer budget: 10
+// Layers: { E }, { D, F }, { A }, { B }, { C }
+package layers
+
+import (
+	"crypto/sha1"
+	"fmt"
+	"regexp"
+	"sort"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+	"gonum.org/v1/gonum/graph/flow"
+	"gonum.org/v1/gonum/graph/simple"
+)
+
+// runtimeGraph represents structured information from Nix about the runtime
+// dependencies of a derivation.
+//
+// This is generated in Nix by using the exportReferencesGraph feature.
+type RuntimeGraph struct {
+	References struct {
+		Graph []string `json:"graph"`
+	} `json:"exportReferencesGraph"`
+
+	Graph []struct {
+		Size uint64   `json:"closureSize"`
+		Path string   `json:"path"`
+		Refs []string `json:"references"`
+	} `json:"graph"`
+}
+
+// Popularity data for each Nix package that was calculated in advance.
+//
+// Popularity is a number from 1-100 that represents the
+// popularity percentile in which this package resides inside
+// of the nixpkgs tree.
+type Popularity = map[string]int
+
+// Layer represents the data returned for each layer that Nix should
+// build for the container image.
+type Layer struct {
+	Contents    []string `json:"contents"`
+	MergeRating uint64
+}
+
+// Hash the contents of a layer to create a deterministic identifier that can be
+// used for caching.
+func (l *Layer) Hash() string {
+	sum := sha1.Sum([]byte(strings.Join(l.Contents, ":")))
+	return fmt.Sprintf("%x", sum)
+}
+
+func (a Layer) merge(b Layer) Layer {
+	a.Contents = append(a.Contents, b.Contents...)
+	a.MergeRating += b.MergeRating
+	return a
+}
+
+// closure as pointed to by the graph nodes.
+type closure struct {
+	GraphID    int64
+	Path       string
+	Size       uint64
+	Refs       []string
+	Popularity int
+}
+
+func (c *closure) ID() int64 {
+	return c.GraphID
+}
+
+var nixRegexp = regexp.MustCompile(`^/nix/store/[a-z0-9]+-`)
+
+// PackageFromPath returns the name of a Nix package based on its
+// output store path.
+func PackageFromPath(path string) string {
+	return nixRegexp.ReplaceAllString(path, "")
+}
+
+// DOTID provides a human-readable package name. The name stems from
+// the dot format used by GraphViz, into which the dependency graph
+// can be rendered.
+func (c *closure) DOTID() string {
+	return PackageFromPath(c.Path)
+}
+
+// bigOrPopular checks whether this closure should be considered for
+// separation into its own layer, even if it would otherwise only
+// appear in a subtree of the dominator tree.
+func (c *closure) bigOrPopular() bool {
+	const sizeThreshold = 100 * 1000000 // 100MB
+
+	if c.Size > sizeThreshold {
+		return true
+	}
+
+	// Threshold value is picked arbitrarily right now. The reason
+	// for this is that some packages (such as `cacert`) have very
+	// few direct dependencies, but are required by pretty much
+	// everything.
+	if c.Popularity >= 100 {
+		return true
+	}
+
+	return false
+}
+
+func insertEdges(graph *simple.DirectedGraph, cmap *map[string]*closure, node *closure) {
+	// Big or popular nodes get a separate edge from the top to
+	// flag them for their own layer.
+	if node.bigOrPopular() && !graph.HasEdgeFromTo(0, node.ID()) {
+		edge := graph.NewEdge(graph.Node(0), node)
+		graph.SetEdge(edge)
+	}
+
+	for _, c := range node.Refs {
+		// Nix adds a self reference to each node, which
+		// should not be inserted.
+		if c != node.Path {
+			edge := graph.NewEdge(node, (*cmap)[c])
+			graph.SetEdge(edge)
+		}
+	}
+}
+
+// Create a graph structure from the references supplied by Nix.
+func buildGraph(refs *RuntimeGraph, pop *Popularity) *simple.DirectedGraph {
+	cmap := make(map[string]*closure)
+	graph := simple.NewDirectedGraph()
+
+	// Insert all closures into the graph, as well as a fake root
+	// closure which serves as the top of the tree.
+	//
+	// A map from store paths to IDs is kept to actually insert
+	// edges below.
+	root := &closure{
+		GraphID: 0,
+		Path:    "image_root",
+	}
+	graph.AddNode(root)
+
+	for idx, c := range refs.Graph {
+		node := &closure{
+			GraphID: int64(idx + 1), // inc because of root node
+			Path:    c.Path,
+			Size:    c.Size,
+			Refs:    c.Refs,
+		}
+
+		// The packages `nss-cacert` and `iana-etc` are added
+		// by Nixery to *every single image* and should have a
+		// very high popularity.
+		//
+		// Other popularity values are populated from the data
+		// set assembled by Nixery's popcount.
+		id := node.DOTID()
+		if strings.HasPrefix(id, "nss-cacert") || strings.HasPrefix(id, "iana-etc") {
+			// glibc has ~300k references, these packages need *more*
+			node.Popularity = 500000
+		} else if p, ok := (*pop)[id]; ok {
+			node.Popularity = p
+		} else {
+			node.Popularity = 1
+		}
+
+		graph.AddNode(node)
+		cmap[c.Path] = node
+	}
+
+	// Insert the top-level closures with edges from the root
+	// node, then insert all edges for each closure.
+	for _, p := range refs.References.Graph {
+		edge := graph.NewEdge(root, cmap[p])
+		graph.SetEdge(edge)
+	}
+
+	for _, c := range cmap {
+		insertEdges(graph, &cmap, c)
+	}
+
+	return graph
+}
+
+// Extracts a subgraph starting at the specified root from the
+// dominator tree. The subgraph is converted into a flat list of
+// layers, each containing the store paths and merge rating.
+func groupLayer(dt *flow.DominatorTree, root *closure) Layer {
+	size := root.Size
+	contents := []string{root.Path}
+	children := dt.DominatedBy(root.ID())
+
+	// This iteration does not use 'range' because the list being
+	// iterated is modified during the iteration (yes, I'm sorry).
+	for i := 0; i < len(children); i++ {
+		child := children[i].(*closure)
+		size += child.Size
+		contents = append(contents, child.Path)
+		children = append(children, dt.DominatedBy(child.ID())...)
+	}
+
+	// Contents are sorted to ensure that hashing is consistent
+	sort.Strings(contents)
+
+	return Layer{
+		Contents:    contents,
+		MergeRating: uint64(root.Popularity) * size,
+	}
+}
+
+// Calculate the dominator tree of the entire package set and group
+// each top-level subtree into a layer.
+//
+// Layers are merged together until they fit into the layer budget,
+// based on their merge rating.
+func dominate(budget int, graph *simple.DirectedGraph) []Layer {
+	dt := flow.Dominators(graph.Node(0), graph)
+
+	var layers []Layer
+	for _, n := range dt.DominatedBy(dt.Root().ID()) {
+		layers = append(layers, groupLayer(&dt, n.(*closure)))
+	}
+
+	sort.Slice(layers, func(i, j int) bool {
+		return layers[i].MergeRating < layers[j].MergeRating
+	})
+
+	if len(layers) > budget {
+		log.WithFields(log.Fields{
+			"layers": len(layers),
+			"budget": budget,
+		}).Info("ideal image exceeds layer budget")
+	}
+
+	for len(layers) > budget {
+		merged := layers[0].merge(layers[1])
+		layers[1] = merged
+		layers = layers[1:]
+	}
+
+	return layers
+}
+
+// groupLayers applies the algorithm described above the its input and returns a
+// list of layers, each consisting of a list of Nix store paths that it should
+// contain.
+func GroupLayers(refs *RuntimeGraph, pop *Popularity, budget int) []Layer {
+	graph := buildGraph(refs, pop)
+	return dominate(budget, graph)
+}
diff --git a/tools/nixery/logs/logs.go b/tools/nixery/logs/logs.go
new file mode 100644
index 0000000000..06adc701ef
--- /dev/null
+++ b/tools/nixery/logs/logs.go
@@ -0,0 +1,108 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+package logs
+
+// This file configures different log formatters via logrus. The
+// standard formatter uses a structured JSON format that is compatible
+// with Stackdriver Error Reporting.
+//
+// https://cloud.google.com/error-reporting/docs/formatting-error-messages
+
+import (
+	"bytes"
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+)
+
+type stackdriverFormatter struct{}
+
+type serviceContext struct {
+	Service string `json:"service"`
+	Version string `json:"version"`
+}
+
+type reportLocation struct {
+	FilePath     string `json:"filePath"`
+	LineNumber   int    `json:"lineNumber"`
+	FunctionName string `json:"functionName"`
+}
+
+var nixeryContext = serviceContext{
+	Service: "nixery",
+}
+
+// isError determines whether an entry should be logged as an error
+// (i.e. with attached `context`).
+//
+// This requires the caller information to be present on the log
+// entry, as stacktraces are not available currently.
+func isError(e *log.Entry) bool {
+	l := e.Level
+	return (l == log.ErrorLevel || l == log.FatalLevel || l == log.PanicLevel) &&
+		e.HasCaller()
+}
+
+// logSeverity formats the entry's severity into a format compatible
+// with Stackdriver Logging.
+//
+// The two formats that are being mapped do not have an equivalent set
+// of severities/levels, so the mapping is somewhat arbitrary for a
+// handful of them.
+//
+// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
+func logSeverity(l log.Level) string {
+	switch l {
+	case log.TraceLevel:
+		return "DEBUG"
+	case log.DebugLevel:
+		return "DEBUG"
+	case log.InfoLevel:
+		return "INFO"
+	case log.WarnLevel:
+		return "WARNING"
+	case log.ErrorLevel:
+		return "ERROR"
+	case log.FatalLevel:
+		return "CRITICAL"
+	case log.PanicLevel:
+		return "EMERGENCY"
+	default:
+		return "DEFAULT"
+	}
+}
+
+func (f stackdriverFormatter) Format(e *log.Entry) ([]byte, error) {
+	msg := e.Data
+	msg["serviceContext"] = &nixeryContext
+	msg["message"] = &e.Message
+	msg["eventTime"] = &e.Time
+	msg["severity"] = logSeverity(e.Level)
+
+	if e, ok := msg[log.ErrorKey]; ok {
+		if err, isError := e.(error); isError {
+			msg[log.ErrorKey] = err.Error()
+		} else {
+			delete(msg, log.ErrorKey)
+		}
+	}
+
+	if isError(e) {
+		loc := reportLocation{
+			FilePath:     e.Caller.File,
+			LineNumber:   e.Caller.Line,
+			FunctionName: e.Caller.Function,
+		}
+		msg["context"] = &loc
+	}
+
+	b := new(bytes.Buffer)
+	err := json.NewEncoder(b).Encode(&msg)
+
+	return b.Bytes(), err
+}
+
+func Init(version string) {
+	nixeryContext.Version = version
+	log.SetReportCaller(true)
+	log.SetFormatter(stackdriverFormatter{})
+}
diff --git a/tools/nixery/manifest/manifest.go b/tools/nixery/manifest/manifest.go
new file mode 100644
index 0000000000..d61514d2f6
--- /dev/null
+++ b/tools/nixery/manifest/manifest.go
@@ -0,0 +1,135 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package image implements logic for creating the image metadata
+// (such as the image manifest and configuration).
+package manifest
+
+import (
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+	"sort"
+)
+
+const (
+	// manifest constants
+	schemaVersion = 2
+
+	// media types
+	ManifestType = "application/vnd.docker.distribution.manifest.v2+json"
+	LayerType    = "application/vnd.docker.image.rootfs.diff.tar.gzip"
+	configType   = "application/vnd.docker.container.image.v1+json"
+
+	// image config constants
+	os     = "linux"
+	fsType = "layers"
+)
+
+type Entry struct {
+	MediaType string `json:"mediaType,omitempty"`
+	Size      int64  `json:"size"`
+	Digest    string `json:"digest"`
+
+	// These fields are internal to Nixery and not part of the
+	// serialised entry.
+	MergeRating uint64 `json:"-"`
+	TarHash     string `json:",omitempty"`
+}
+
+type manifest struct {
+	SchemaVersion int     `json:"schemaVersion"`
+	MediaType     string  `json:"mediaType"`
+	Config        Entry   `json:"config"`
+	Layers        []Entry `json:"layers"`
+}
+
+type imageConfig struct {
+	Architecture string `json:"architecture"`
+	OS           string `json:"os"`
+
+	RootFS struct {
+		FSType  string   `json:"type"`
+		DiffIDs []string `json:"diff_ids"`
+	} `json:"rootfs"`
+
+	Config struct {
+		Cmd []string `json:"cmd,omitempty"`
+		Env []string `json:"env,omitempty"`
+	} `json:"config"`
+}
+
+// ConfigLayer represents the configuration layer to be included in
+// the manifest, containing its JSON-serialised content and SHA256
+// hash.
+type ConfigLayer struct {
+	Config []byte
+	SHA256 string
+}
+
+// imageConfig creates an image configuration with the values set to
+// the constant defaults.
+//
+// Outside of this module the image configuration is treated as an
+// opaque blob and it is thus returned as an already serialised byte
+// array and its SHA256-hash.
+func configLayer(arch string, hashes []string, cmd string) ConfigLayer {
+	c := imageConfig{}
+	c.Architecture = arch
+	c.OS = os
+	c.RootFS.FSType = fsType
+	c.RootFS.DiffIDs = hashes
+	if cmd != "" {
+		c.Config.Cmd = []string{cmd}
+	}
+	c.Config.Env = []string{"SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"}
+
+	j, _ := json.Marshal(c)
+
+	return ConfigLayer{
+		Config: j,
+		SHA256: fmt.Sprintf("%x", sha256.Sum256(j)),
+	}
+}
+
+// Manifest creates an image manifest from the specified layer entries
+// and returns its JSON-serialised form as well as the configuration
+// layer.
+//
+// Callers do not need to set the media type for the layer entries.
+func Manifest(arch string, layers []Entry, cmd string) (json.RawMessage, ConfigLayer) {
+	// Sort layers by their merge rating, from highest to lowest.
+	// This makes it likely for a contiguous chain of shared image
+	// layers to appear at the beginning of a layer.
+	//
+	// Due to moby/moby#38446 Docker considers the order of layers
+	// when deciding which layers to download again.
+	sort.Slice(layers, func(i, j int) bool {
+		return layers[i].MergeRating > layers[j].MergeRating
+	})
+
+	hashes := make([]string, len(layers))
+	for i, l := range layers {
+		hashes[i] = l.TarHash
+		l.MediaType = LayerType
+		l.TarHash = ""
+		layers[i] = l
+	}
+
+	c := configLayer(arch, hashes, cmd)
+
+	m := manifest{
+		SchemaVersion: schemaVersion,
+		MediaType:     ManifestType,
+		Config: Entry{
+			MediaType: configType,
+			Size:      int64(len(c.Config)),
+			Digest:    "sha256:" + c.SHA256,
+		},
+		Layers: layers,
+	}
+
+	j, _ := json.Marshal(m)
+
+	return json.RawMessage(j), c
+}
diff --git a/tools/nixery/popcount/README.md b/tools/nixery/popcount/README.md
new file mode 100644
index 0000000000..8485a4d30e
--- /dev/null
+++ b/tools/nixery/popcount/README.md
@@ -0,0 +1,39 @@
+popcount
+========
+
+This script is used to count the popularity for each package in `nixpkgs`, by
+determining how many other packages depend on it.
+
+It skips over all packages that fail to build, are not cached or are unfree -
+but these omissions do not meaningfully affect the statistics.
+
+It currently does not evaluate nested attribute sets (such as
+`haskellPackages`).
+
+## Usage
+
+1. Generate a list of all top-level attributes in `nixpkgs`:
+
+   ```shell
+   nix eval '(with builtins; toJSON (attrNames (import <nixpkgs> {})))' | jq -r | jq > all-top-level.json
+   ```
+
+2. Run `./popcount > all-runtime-deps.txt`
+
+3. Collect and count the results with the following magic incantation:
+
+   ```shell
+   cat all-runtime-deps.txt \
+     | sed -r 's|/nix/store/[a-z0-9]+-||g' \
+     | sort \
+     | uniq -c \
+     | sort -n -r \
+     | awk '{ print "{\"" $2 "\":" $1 "}"}' \
+     | jq -c -s '. | add | with_entries(select(.value > 1))' \
+     > your-output-file
+   ```
+
+   In essence, this will trim Nix's store paths and hashes from the output,
+   count the occurences of each package and return the output as JSON. All
+   packages that have no references other than themselves are removed from the
+   output.
diff --git a/tools/nixery/popcount/default.nix b/tools/nixery/popcount/default.nix
new file mode 100644
index 0000000000..4b16768e4e
--- /dev/null
+++ b/tools/nixery/popcount/default.nix
@@ -0,0 +1,13 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+{ buildGoPackage }:
+
+buildGoPackage {
+  name = "nixery-popcount";
+
+  src = ./.;
+
+  goPackagePath = "github.com/google/nixery/popcount";
+  doCheck = true;
+}
diff --git a/tools/nixery/popcount/popcount.go b/tools/nixery/popcount/popcount.go
new file mode 100644
index 0000000000..b83ac3ed1a
--- /dev/null
+++ b/tools/nixery/popcount/popcount.go
@@ -0,0 +1,280 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Popcount fetches popularity information for each store path in a
+// given Nix channel from the upstream binary cache.
+//
+// It does this simply by inspecting the narinfo files, rather than
+// attempting to deal with instantiation of the binary cache.
+//
+// This is *significantly* faster than attempting to realise the whole
+// channel and then calling `nix path-info` on it.
+//
+// TODO(tazjin): Persist intermediate results (references for each
+// store path) to speed up subsequent runs.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"regexp"
+	"strings"
+)
+
+var client http.Client
+var pathexp = regexp.MustCompile("/nix/store/([a-z0-9]{32})-(.*)$")
+var refsexp = regexp.MustCompile("(?m:^References: (.*)$)")
+var refexp = regexp.MustCompile("^([a-z0-9]{32})-(.*)$")
+
+type meta struct {
+	name   string
+	url    string
+	commit string
+}
+
+type item struct {
+	name string
+	hash string
+}
+
+func failOn(err error, msg string) {
+	if err != nil {
+		log.Fatalf("%s: %s", msg, err)
+	}
+}
+
+func channelMetadata(channel string) meta {
+	// This needs an HTTP client that does not follow redirects
+	// because the channel URL is used explicitly for other
+	// downloads.
+	c := http.Client{
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+
+	resp, err := c.Get(fmt.Sprintf("https://channels.nixos.org/%s", channel))
+	failOn(err, "failed to retrieve channel metadata")
+
+	loc, err := resp.Location()
+	failOn(err, "no redirect location given for channel")
+
+	// TODO(tazjin): These redirects are currently served as 301s, but
+	// should (and used to) be 302s. Check if/when this is fixed and
+	// update accordingly.
+	if !(resp.StatusCode == 301 || resp.StatusCode == 302) {
+		log.Fatalf("Expected redirect for channel, but received '%s'\n", resp.Status)
+	}
+
+	commitResp, err := c.Get(fmt.Sprintf("%s/git-revision", loc.String()))
+	failOn(err, "failed to retrieve commit for channel")
+
+	defer commitResp.Body.Close()
+	commit, err := ioutil.ReadAll(commitResp.Body)
+	failOn(err, "failed to read commit from response")
+	if commitResp.StatusCode != 200 {
+		log.Fatalf("non-success status code when fetching commit: %s (%v)", string(commit), commitResp.StatusCode)
+	}
+
+	return meta{
+		name:   channel,
+		url:    loc.String(),
+		commit: string(commit),
+	}
+}
+
+func downloadStorePaths(c *meta) []string {
+	resp, err := client.Get(fmt.Sprintf("%s/store-paths.xz", c.url))
+	failOn(err, "failed to download store-paths.xz")
+	defer resp.Body.Close()
+
+	cmd := exec.Command("xzcat")
+	stdin, err := cmd.StdinPipe()
+	failOn(err, "failed to open xzcat stdin")
+	stdout, err := cmd.StdoutPipe()
+	failOn(err, "failed to open xzcat stdout")
+	defer stdout.Close()
+
+	go func() {
+		defer stdin.Close()
+		io.Copy(stdin, resp.Body)
+	}()
+
+	err = cmd.Start()
+	failOn(err, "failed to start xzcat")
+
+	paths, err := ioutil.ReadAll(stdout)
+	failOn(err, "failed to read uncompressed store paths")
+
+	err = cmd.Wait()
+	failOn(err, "xzcat failed to decompress")
+
+	return strings.Split(string(paths), "\n")
+}
+
+func storePathToItem(path string) *item {
+	res := pathexp.FindStringSubmatch(path)
+	if len(res) != 3 {
+		return nil
+	}
+
+	return &item{
+		hash: res[1],
+		name: res[2],
+	}
+}
+
+func narInfoToRefs(narinfo string) []string {
+	all := refsexp.FindAllStringSubmatch(narinfo, 1)
+
+	if len(all) != 1 {
+		log.Fatalf("failed to parse narinfo:\n%s\nfound: %v\n", narinfo, all[0])
+	}
+
+	if len(all[0]) != 2 {
+		// no references found
+		return []string{}
+	}
+
+	refs := strings.Split(all[0][1], " ")
+	for i, s := range refs {
+		if s == "" {
+			continue
+		}
+
+		res := refexp.FindStringSubmatch(s)
+		refs[i] = res[2]
+	}
+
+	return refs
+}
+
+func fetchNarInfo(i *item) (string, error) {
+	file, err := ioutil.ReadFile("popcache/" + i.hash)
+	if err == nil {
+		return string(file), nil
+	}
+
+	resp, err := client.Get(fmt.Sprintf("https://cache.nixos.org/%s.narinfo", i.hash))
+	if err != nil {
+		return "", err
+	}
+
+	defer resp.Body.Close()
+
+	narinfo, err := ioutil.ReadAll(resp.Body)
+
+	// best-effort write the file to the cache
+	ioutil.WriteFile("popcache/"+i.hash, narinfo, 0644)
+
+	return string(narinfo), err
+}
+
+// downloader starts a worker that takes care of downloading narinfos
+// for all paths received from the queue.
+//
+// If there is no data remaining in the queue, the downloader exits
+// and informs the finaliser queue about having exited.
+func downloader(queue chan *item, narinfos chan string, downloaders chan struct{}) {
+	for i := range queue {
+		ni, err := fetchNarInfo(i)
+		if err != nil {
+			log.Printf("couldn't fetch narinfo for %s: %s\n", i.name, err)
+			continue
+
+		}
+		narinfos <- ni
+	}
+	downloaders <- struct{}{}
+}
+
+// finaliser counts the number of downloaders that have exited and
+// closes the narinfos queue to signal to the counters that no more
+// elements will arrive.
+func finaliser(count int, downloaders chan struct{}, narinfos chan string) {
+	for range downloaders {
+		count--
+		if count == 0 {
+			close(downloaders)
+			close(narinfos)
+			break
+		}
+	}
+}
+
+func main() {
+	if len(os.Args) == 1 {
+		log.Fatalf("Nix channel must be specified as first argument")
+	}
+
+	err := os.MkdirAll("popcache", 0755)
+	if err != nil {
+		log.Fatalf("Failed to create 'popcache' directory in current folder: %s\n", err)
+	}
+
+	count := 42 // concurrent downloader count
+	channel := os.Args[1]
+	log.Printf("Fetching metadata for channel '%s'\n", channel)
+
+	meta := channelMetadata(channel)
+	log.Printf("Pinned channel '%s' to commit '%s'\n", meta.name, meta.commit)
+
+	paths := downloadStorePaths(&meta)
+	log.Printf("Fetching references for %d store paths\n", len(paths))
+
+	// Download paths concurrently and receive their narinfos into
+	// a channel. Data is collated centrally into a map and
+	// serialised at the /very/ end.
+	downloadQueue := make(chan *item, len(paths))
+	for _, p := range paths {
+		if i := storePathToItem(p); i != nil {
+			downloadQueue <- i
+		}
+	}
+	close(downloadQueue)
+
+	// Set up a task tracking channel for parsing & counting
+	// narinfos, as well as a coordination channel for signaling
+	// that all downloads have finished
+	narinfos := make(chan string, 50)
+	downloaders := make(chan struct{}, count)
+	for i := 0; i < count; i++ {
+		go downloader(downloadQueue, narinfos, downloaders)
+	}
+
+	go finaliser(count, downloaders, narinfos)
+
+	counts := make(map[string]int)
+	for ni := range narinfos {
+		refs := narInfoToRefs(ni)
+		for _, ref := range refs {
+			if ref == "" {
+				continue
+			}
+
+			counts[ref] += 1
+		}
+	}
+
+	// Remove all self-references (i.e. packages not referenced by anyone else)
+	for k, v := range counts {
+		if v == 1 {
+			delete(counts, k)
+		}
+	}
+
+	bytes, _ := json.Marshal(counts)
+	outfile := fmt.Sprintf("popularity-%s-%s.json", meta.name, meta.commit)
+	err = ioutil.WriteFile(outfile, bytes, 0644)
+	if err != nil {
+		log.Fatalf("Failed to write output to '%s': %s\n", outfile, err)
+	}
+
+	log.Printf("Wrote output to '%s'\n", outfile)
+}
diff --git a/tools/nixery/prepare-image/default.nix b/tools/nixery/prepare-image/default.nix
new file mode 100644
index 0000000000..efd9ed3404
--- /dev/null
+++ b/tools/nixery/prepare-image/default.nix
@@ -0,0 +1,18 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+# This file builds a wrapper script called by Nixery to ask for the
+# content information for a given image.
+#
+# The purpose of using a wrapper script is to ensure that the paths to
+# all required Nix files are set correctly at runtime.
+
+{ pkgs ? import <nixpkgs> { } }:
+
+pkgs.writeShellScriptBin "nixery-prepare-image" ''
+  exec ${pkgs.nix}/bin/nix-build \
+    --show-trace \
+    --no-out-link "$@" \
+    --argstr loadPkgs ${./load-pkgs.nix} \
+    ${./prepare-image.nix}
+''
diff --git a/tools/nixery/prepare-image/load-pkgs.nix b/tools/nixery/prepare-image/load-pkgs.nix
new file mode 100644
index 0000000000..7f8ab5479d
--- /dev/null
+++ b/tools/nixery/prepare-image/load-pkgs.nix
@@ -0,0 +1,36 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+# Load a Nix package set from one of the supported source types
+# (nixpkgs, git, path).
+{ srcType, srcArgs, importArgs ? { } }:
+
+with builtins;
+let
+  # If a nixpkgs channel is requested, it is retrieved from Github (as
+  # a tarball) and imported.
+  fetchImportChannel = channel:
+    let
+      url =
+        "https://github.com/NixOS/nixpkgs/archive/${channel}.tar.gz";
+    in
+    import (fetchTarball url) importArgs;
+
+  # If a git repository is requested, it is retrieved via
+  # builtins.fetchGit which defaults to the git configuration of the
+  # outside environment. This means that user-configured SSH
+  # credentials etc. are going to work as expected.
+  fetchImportGit = spec: import (fetchGit spec) importArgs;
+
+  # No special handling is used for paths, so users are expected to pass one
+  # that will work natively with Nix.
+  importPath = path: import (toPath path) importArgs;
+in
+if srcType == "nixpkgs" then
+  fetchImportChannel srcArgs
+else if srcType == "git" then
+  fetchImportGit (fromJSON srcArgs)
+else if srcType == "path" then
+  importPath srcArgs
+else
+  throw ("Invalid package set source specification: ${srcType} (${srcArgs})")
diff --git a/tools/nixery/prepare-image/prepare-image.nix b/tools/nixery/prepare-image/prepare-image.nix
new file mode 100644
index 0000000000..bb88983cf6
--- /dev/null
+++ b/tools/nixery/prepare-image/prepare-image.nix
@@ -0,0 +1,185 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+# This file contains a derivation that outputs structured information
+# about the runtime dependencies of an image with a given set of
+# packages. This is used by Nixery to determine the layer grouping and
+# assemble each layer.
+#
+# In addition it creates and outputs a meta-layer with the symlink
+# structure required for using the image together with the individual
+# package layers.
+
+{
+  # Description of the package set to be used (will be loaded by load-pkgs.nix)
+  srcType ? "nixpkgs"
+, srcArgs ? "nixos-20.09"
+, system ? "x86_64-linux"
+, importArgs ? { }
+, # Path to load-pkgs.nix
+  loadPkgs ? ./load-pkgs.nix
+, # Packages to install by name (which must refer to top-level attributes of
+  # nixpkgs). This is passed in as a JSON-array in string form.
+  packages ? "[]"
+}:
+
+let
+  inherit (builtins)
+    foldl'
+    fromJSON
+    hasAttr
+    length
+    match
+    readFile
+    toFile
+    toJSON;
+
+  # Package set to use for sourcing utilities
+  nativePkgs = import loadPkgs { inherit srcType srcArgs importArgs; };
+  inherit (nativePkgs) coreutils jq openssl lib runCommand writeText symlinkJoin;
+
+  # Package set to use for packages to be included in the image. This
+  # package set is imported with the system set to the target
+  # architecture.
+  pkgs = import loadPkgs {
+    inherit srcType srcArgs;
+    importArgs = importArgs // {
+      inherit system;
+    };
+  };
+
+  # deepFetch traverses the top-level Nix package set to retrieve an item via a
+  # path specified in string form.
+  #
+  # For top-level items, the name of the key yields the result directly. Nested
+  # items are fetched by using dot-syntax, as in Nix itself.
+  #
+  # Due to a restriction of the registry API specification it is not possible to
+  # pass uppercase characters in an image name, however the Nix package set
+  # makes use of camelCasing repeatedly (for example for `haskellPackages`).
+  #
+  # To work around this, if no value is found on the top-level a second lookup
+  # is done on the package set using lowercase-names. This is not done for
+  # nested sets, as they often have keys that only differ in case.
+  #
+  # For example, `deepFetch pkgs "xorg.xev"` retrieves `pkgs.xorg.xev` and
+  # `deepFetch haskellpackages.stylish-haskell` retrieves
+  # `haskellPackages.stylish-haskell`.
+  deepFetch = with lib; s: n:
+    let
+      path = splitString "." n;
+      err = { error = "not_found"; pkg = n; };
+      # The most efficient way I've found to do a lookup against
+      # case-differing versions of an attribute is to first construct a
+      # mapping of all lowercased attribute names to their differently cased
+      # equivalents.
+      #
+      # This map is then used for a second lookup if the top-level
+      # (case-sensitive) one does not yield a result.
+      hasUpper = str: (match ".*[A-Z].*" str) != null;
+      allUpperKeys = filter hasUpper (attrNames s);
+      lowercased = listToAttrs (map
+        (k: {
+          name = toLower k;
+          value = k;
+        })
+        allUpperKeys);
+      caseAmendedPath = map (v: if hasAttr v lowercased then lowercased."${v}" else v) path;
+      fetchLower = attrByPath caseAmendedPath err s;
+    in
+    attrByPath path fetchLower s;
+
+  # allContents contains all packages successfully retrieved by name
+  # from the package set, as well as any errors encountered while
+  # attempting to fetch a package.
+  #
+  # Accumulated error information is returned back to the server.
+  allContents =
+    # Folds over the results of 'deepFetch' on all requested packages to
+    # separate them into errors and content. This allows the program to
+    # terminate early and return only the errors if any are encountered.
+    let
+      splitter = attrs: res:
+        if hasAttr "error" res
+        then attrs // { errors = attrs.errors ++ [ res ]; }
+        else attrs // { contents = attrs.contents ++ [ res ]; };
+      init = { contents = [ ]; errors = [ ]; };
+      fetched = (map (deepFetch pkgs) (fromJSON packages));
+    in
+    foldl' splitter init fetched;
+
+  # Contains the export references graph of all retrieved packages,
+  # which has information about all runtime dependencies of the image.
+  #
+  # This is used by Nixery to group closures into image layers.
+  runtimeGraph = runCommand "runtime-graph.json"
+    {
+      __structuredAttrs = true;
+      exportReferencesGraph.graph = allContents.contents;
+      PATH = "${coreutils}/bin";
+      builder = toFile "builder" ''
+        . .attrs.sh
+        cp .attrs.json ''${outputs[out]}
+      '';
+    } "";
+
+  # Create a symlink forest into all top-level store paths of the
+  # image contents.
+  contentsEnv = symlinkJoin {
+    name = "bulk-layers";
+    paths = allContents.contents;
+
+    # Provide a few essentials that many programs expect:
+    # - a /tmp directory,
+    # - a /usr/bin/env for shell scripts that require it.
+    #
+    # Note that in images that do not actually contain `coreutils`,
+    # /usr/bin/env will be a dangling symlink.
+    #
+    # TODO(tazjin): Don't link /usr/bin/env if coreutils is not included.
+    postBuild = ''
+      mkdir -p $out/tmp
+      mkdir -p $out/usr/bin
+      ln -s ${coreutils}/bin/env $out/usr/bin/env
+    '';
+  };
+
+  # Image layer that contains the symlink forest created above. This
+  # must be included in the image to ensure that the filesystem has a
+  # useful layout at runtime.
+  symlinkLayer = runCommand "symlink-layer.tar" { } ''
+    cp -r ${contentsEnv}/ ./layer
+    tar --transform='s|^\./||' -C layer --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 -cf $out .
+  '';
+
+  # Metadata about the symlink layer which is required for serving it.
+  # Two different hashes are computed for different usages (inclusion
+  # in manifest vs. content-checking in the layer cache).
+  symlinkLayerMeta = fromJSON (readFile (runCommand "symlink-layer-meta.json"
+    {
+      buildInputs = [ coreutils jq openssl ];
+    } ''
+    tarHash=$(sha256sum ${symlinkLayer} | cut -d ' ' -f1)
+    layerSize=$(stat --printf '%s' ${symlinkLayer})
+
+    jq -n -c --arg tarHash $tarHash --arg size $layerSize --arg path ${symlinkLayer} \
+      '{ size: ($size | tonumber), tarHash: $tarHash, path: $path }' >> $out
+  ''));
+
+  # Final output structure returned to Nixery if the build succeeded
+  buildOutput = {
+    runtimeGraph = fromJSON (readFile runtimeGraph);
+    symlinkLayer = symlinkLayerMeta;
+  };
+
+  # Output structure returned if errors occured during the build. Currently the
+  # only error type that is returned in a structured way is 'not_found'.
+  errorOutput = {
+    error = "not_found";
+    pkgs = map (err: err.pkg) allContents.errors;
+  };
+in
+writeText "build-output.json" (if (length allContents.errors) == 0
+then toJSON buildOutput
+else toJSON errorOutput
+)
diff --git a/tools/nixery/scripts/integration-test.sh b/tools/nixery/scripts/integration-test.sh
new file mode 100755
index 0000000000..9d06e96ba2
--- /dev/null
+++ b/tools/nixery/scripts/integration-test.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+set -eou pipefail
+
+# This integration test makes sure that the container image built
+# for Nixery itself runs fine in Docker, and that images pulled
+# from it work in Docker.
+
+IMG=$(docker load -q -i "$(nix-build -A nixery-image)" | awk '{ print $3 }')
+echo "Loaded Nixery image as ${IMG}"
+
+# Run the built nixery docker image in the background, but keep printing its
+# output as it occurs.
+# We can't just mount a tmpfs to /var/cache/nixery, as tmpfs doesn't support
+# user xattrs.
+# So create a temporary directory in the current working directory, and hope
+# it's backed by something supporting user xattrs.
+# We'll notice it isn't if nixery starts complaining about not able to set
+# xattrs anyway.
+if [ -d var-cache-nixery ]; then rm -Rf var-cache-nixery; fi
+mkdir var-cache-nixery
+docker run --privileged --rm -p 8080:8080 --name nixery \
+  -e PORT=8080 \
+  --mount "type=bind,source=${PWD}/var-cache-nixery,target=/var/cache/nixery" \
+  -e NIXERY_CHANNEL=nixos-unstable \
+  -e NIXERY_STORAGE_BACKEND=filesystem \
+  -e STORAGE_PATH=/var/cache/nixery \
+  "${IMG}" &
+
+# Give the container ~20 seconds to come up
+set +e
+attempts=0
+echo -n "Waiting for Nixery to start ..."
+until curl --fail --silent "http://localhost:8080/v2/"; do
+  [[ attempts -eq 30 ]] && echo "Nixery container failed to start!" && exit 1
+  ((attempts++))
+  echo -n "."
+  sleep 1
+done
+set -e
+
+# Pull and run an image of the current CPU architecture
+case $(uname -m) in
+  x86_64)
+    docker run --rm localhost:8080/hello hello
+    ;;
+  aarch64)
+    docker run --rm localhost:8080/arm64/hello hello
+    ;;
+esac
+
+# Pull an image of the opposite CPU architecture (but without running it)
+case $(uname -m) in
+x86_64)
+  docker pull localhost:8080/arm64/hello
+  ;;
+aarch64)
+  docker pull localhost:8080/hello
+  ;;
+esac
diff --git a/tools/nixery/shell.nix b/tools/nixery/shell.nix
new file mode 100644
index 0000000000..b91094722c
--- /dev/null
+++ b/tools/nixery/shell.nix
@@ -0,0 +1,13 @@
+# Copyright 2022 The TVL Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+# Configures a shell environment that builds required local packages to
+# run Nixery.
+{ pkgs ? import <nixpkgs> { } }:
+
+let nixery = import ./default.nix { inherit pkgs; };
+in pkgs.stdenv.mkDerivation {
+  name = "nixery-dev-shell";
+
+  buildInputs = with pkgs; [ jq nixery.nixery-prepare-image ];
+}
diff --git a/tools/nixery/storage/filesystem.go b/tools/nixery/storage/filesystem.go
new file mode 100644
index 0000000000..3df4420f0f
--- /dev/null
+++ b/tools/nixery/storage/filesystem.go
@@ -0,0 +1,99 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Filesystem storage backend for Nixery.
+package storage
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path"
+
+	"github.com/pkg/xattr"
+	log "github.com/sirupsen/logrus"
+)
+
+type FSBackend struct {
+	path string
+}
+
+func NewFSBackend() (*FSBackend, error) {
+	p := os.Getenv("STORAGE_PATH")
+	if p == "" {
+		return nil, fmt.Errorf("STORAGE_PATH must be set for filesystem storage")
+	}
+
+	p = path.Clean(p)
+	err := os.MkdirAll(p, 0755)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create storage dir: %s", err)
+	}
+
+	return &FSBackend{p}, nil
+}
+
+func (b *FSBackend) Name() string {
+	return fmt.Sprintf("Filesystem (%s)", b.path)
+}
+
+func (b *FSBackend) Persist(ctx context.Context, key, contentType string, f Persister) (string, int64, error) {
+	full := path.Join(b.path, key)
+	dir := path.Dir(full)
+	err := os.MkdirAll(dir, 0755)
+	if err != nil {
+		log.WithError(err).WithField("path", dir).Error("failed to create storage directory")
+		return "", 0, err
+	}
+
+	file, err := os.OpenFile(full, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		log.WithError(err).WithField("file", full).Error("failed to write file")
+		return "", 0, err
+	}
+	defer file.Close()
+
+	err = xattr.Set(full, "user.mime_type", []byte(contentType))
+	if err != nil {
+		log.WithError(err).WithField("file", full).Error("failed to store file type in xattrs")
+		return "", 0, err
+	}
+
+	return f(file)
+}
+
+func (b *FSBackend) Fetch(ctx context.Context, key string) (io.ReadCloser, error) {
+	full := path.Join(b.path, key)
+	return os.Open(full)
+}
+
+func (b *FSBackend) Move(ctx context.Context, old, new string) error {
+	newpath := path.Join(b.path, new)
+	err := os.MkdirAll(path.Dir(newpath), 0755)
+	if err != nil {
+		return err
+	}
+
+	return os.Rename(path.Join(b.path, old), newpath)
+}
+
+func (b *FSBackend) Serve(digest string, r *http.Request, w http.ResponseWriter) error {
+	p := path.Join(b.path, "layers", digest)
+
+	log.WithFields(log.Fields{
+		"digest": digest,
+		"path":   p,
+	}).Info("serving blob from filesystem")
+
+	contentType, err := xattr.Get(p, "user.mime_type")
+	if err != nil {
+		log.WithError(err).WithField("file", p).Error("failed to read file type from xattrs")
+		return err
+	}
+	w.Header().Add("Content-Type", string(contentType))
+
+	http.ServeFile(w, r, p)
+	return nil
+}
diff --git a/tools/nixery/storage/gcs.go b/tools/nixery/storage/gcs.go
new file mode 100644
index 0000000000..752c6bbd82
--- /dev/null
+++ b/tools/nixery/storage/gcs.go
@@ -0,0 +1,231 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Google Cloud Storage backend for Nixery.
+package storage
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"time"
+
+	"cloud.google.com/go/storage"
+	log "github.com/sirupsen/logrus"
+	"golang.org/x/oauth2/google"
+)
+
+// HTTP client to use for direct calls to APIs that are not part of the SDK
+var client = &http.Client{}
+
+// API scope needed for renaming objects in GCS
+const gcsScope = "https://www.googleapis.com/auth/devstorage.read_write"
+
+type GCSBackend struct {
+	bucket  string
+	handle  *storage.BucketHandle
+	signing *storage.SignedURLOptions
+}
+
+// Constructs a new GCS bucket backend based on the configured
+// environment variables.
+func NewGCSBackend() (*GCSBackend, error) {
+	bucket := os.Getenv("GCS_BUCKET")
+	if bucket == "" {
+		return nil, fmt.Errorf("GCS_BUCKET must be configured for GCS usage")
+	}
+
+	ctx := context.Background()
+	client, err := storage.NewClient(ctx)
+	if err != nil {
+		log.WithError(err).Fatal("failed to set up Cloud Storage client")
+	}
+
+	handle := client.Bucket(bucket)
+
+	if _, err := handle.Attrs(ctx); err != nil {
+		log.WithError(err).WithField("bucket", bucket).Error("could not access configured bucket")
+		return nil, err
+	}
+
+	signing, err := signingOptsFromEnv()
+	if err != nil {
+		log.WithError(err).Error("failed to configure GCS bucket signing")
+		return nil, err
+	}
+
+	return &GCSBackend{
+		bucket:  bucket,
+		handle:  handle,
+		signing: signing,
+	}, nil
+}
+
+func (b *GCSBackend) Name() string {
+	return "Google Cloud Storage (" + b.bucket + ")"
+}
+
+func (b *GCSBackend) Persist(ctx context.Context, path, contentType string, f Persister) (string, int64, error) {
+	obj := b.handle.Object(path)
+	w := obj.NewWriter(ctx)
+
+	hash, size, err := f(w)
+	if err != nil {
+		log.WithError(err).WithField("path", path).Error("failed to write to GCS")
+		return hash, size, err
+	}
+
+	err = w.Close()
+	if err != nil {
+		log.WithError(err).WithField("path", path).Error("failed to complete GCS upload")
+		return hash, size, err
+	}
+
+	// GCS natively supports content types for objects, which will be
+	// used when serving them back.
+	if contentType != "" {
+		_, err = obj.Update(ctx, storage.ObjectAttrsToUpdate{
+			ContentType: contentType,
+		})
+
+		if err != nil {
+			log.WithError(err).WithField("path", path).Error("failed to update object attrs")
+			return hash, size, err
+		}
+	}
+
+	return hash, size, nil
+}
+
+func (b *GCSBackend) Fetch(ctx context.Context, path string) (io.ReadCloser, error) {
+	obj := b.handle.Object(path)
+
+	// Probe whether the file exists before trying to fetch it
+	_, err := obj.Attrs(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return obj.NewReader(ctx)
+}
+
+// renameObject renames an object in the specified Cloud Storage
+// bucket.
+//
+// The Go API for Cloud Storage does not support renaming objects, but
+// the HTTP API does. The code below makes the relevant call manually.
+func (b *GCSBackend) Move(ctx context.Context, old, new string) error {
+	creds, err := google.FindDefaultCredentials(ctx, gcsScope)
+	if err != nil {
+		return err
+	}
+
+	token, err := creds.TokenSource.Token()
+	if err != nil {
+		return err
+	}
+
+	// as per https://cloud.google.com/storage/docs/renaming-copying-moving-objects#rename
+	url := fmt.Sprintf(
+		"https://www.googleapis.com/storage/v1/b/%s/o/%s/rewriteTo/b/%s/o/%s",
+		url.PathEscape(b.bucket), url.PathEscape(old),
+		url.PathEscape(b.bucket), url.PathEscape(new),
+	)
+
+	req, err := http.NewRequest("POST", url, nil)
+	req.Header.Add("Authorization", "Bearer "+token.AccessToken)
+	_, err = client.Do(req)
+	if err != nil {
+		return err
+	}
+
+	// It seems that 'rewriteTo' copies objects instead of
+	// renaming/moving them, hence a deletion call afterwards is
+	// required.
+	if err = b.handle.Object(old).Delete(ctx); err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"new": new,
+			"old": old,
+		}).Warn("failed to delete renamed object")
+
+		// this error should not break renaming and is not returned
+	}
+
+	return nil
+}
+
+func (b *GCSBackend) Serve(digest string, r *http.Request, w http.ResponseWriter) error {
+	url, err := b.constructLayerUrl(digest)
+	if err != nil {
+		log.WithError(err).WithFields(log.Fields{
+			"digest": digest,
+			"bucket": b.bucket,
+		}).Error("failed to sign GCS URL")
+
+		return err
+	}
+
+	log.WithField("digest", digest).Info("redirecting blob request to GCS bucket")
+
+	w.Header().Set("Location", url)
+	w.WriteHeader(303)
+	return nil
+}
+
+// Configure GCS URL signing in the presence of a service account key
+// (toggled if the user has set GOOGLE_APPLICATION_CREDENTIALS).
+func signingOptsFromEnv() (*storage.SignedURLOptions, error) {
+	path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
+	if path == "" {
+		// No credentials configured -> no URL signing
+		return nil, nil
+	}
+
+	key, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read service account key: %s", err)
+	}
+
+	conf, err := google.JWTConfigFromJSON(key)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse service account key: %s", err)
+	}
+
+	log.WithField("account", conf.Email).Info("GCS URL signing enabled")
+
+	return &storage.SignedURLOptions{
+		Scheme:         storage.SigningSchemeV4,
+		GoogleAccessID: conf.Email,
+		PrivateKey:     conf.PrivateKey,
+		Method:         "GET",
+	}, nil
+}
+
+// layerRedirect constructs the public URL of the layer object in the Cloud
+// Storage bucket, signs it and redirects the user there.
+//
+// Signing the URL allows unauthenticated clients to retrieve objects from the
+// bucket.
+//
+// In case signing is not configured, a redirect to storage.googleapis.com is
+// issued, which means the underlying bucket objects need to be publicly
+// accessible.
+//
+// The Docker client is known to follow redirects, but this might not be true
+// for all other registry clients.
+func (b *GCSBackend) constructLayerUrl(digest string) (string, error) {
+	log.WithField("layer", digest).Info("redirecting layer request to bucket")
+	object := "layers/" + digest
+
+	if b.signing != nil {
+		opts := *b.signing
+		opts.Expires = time.Now().Add(5 * time.Minute)
+		return storage.SignedURL(b.bucket, object, &opts)
+	} else {
+		return ("https://storage.googleapis.com/" + b.bucket + "/" + object), nil
+	}
+}
diff --git a/tools/nixery/storage/storage.go b/tools/nixery/storage/storage.go
new file mode 100644
index 0000000000..5500d61640
--- /dev/null
+++ b/tools/nixery/storage/storage.go
@@ -0,0 +1,40 @@
+// Copyright 2022 The TVL Contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package storage implements an interface that can be implemented by
+// storage backends, such as Google Cloud Storage or the local
+// filesystem.
+package storage
+
+import (
+	"context"
+	"io"
+	"net/http"
+)
+
+type Persister = func(io.Writer) (string, int64, error)
+
+type Backend interface {
+	// Name returns the name of the storage backend, for use in
+	// log messages and such.
+	Name() string
+
+	// Persist provides a user-supplied function with a writer
+	// that stores data in the storage backend.
+	//
+	// It needs to return the SHA256 hash of the data written as
+	// well as the total number of bytes, as those are required
+	// for the image manifest.
+	Persist(ctx context.Context, path, contentType string, f Persister) (string, int64, error)
+
+	// Fetch retrieves data from the storage backend.
+	Fetch(ctx context.Context, path string) (io.ReadCloser, error)
+
+	// Move renames a path inside the storage backend. This is
+	// used for staging uploads while calculating their hashes.
+	Move(ctx context.Context, old, new string) error
+
+	// Serve provides a handler function to serve HTTP requests
+	// for objects in the storage backend.
+	Serve(digest string, r *http.Request, w http.ResponseWriter) error
+}
diff --git a/tools/nsfv-setup/default.nix b/tools/nsfv-setup/default.nix
new file mode 100644
index 0000000000..1e353e3269
--- /dev/null
+++ b/tools/nsfv-setup/default.nix
@@ -0,0 +1,29 @@
+# Configures a running Pulseaudio instance with an LADSP filter that
+# creates a noise-cancelling sink.
+#
+# This can be used to, for example, cancel noise from an incoming
+# video conferencing audio stream.
+#
+# There are some caveats, for example this will not distinguish
+# between noise from different participants and I have no idea what
+# happens if the default sink goes away.
+#
+# If this script is run while an NSFV sink exists, the existing sink
+# will first be removed.
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs) ripgrep pulseaudio;
+  inherit (depot.third_party) nsfv;
+in
+pkgs.writeShellScriptBin "nsfv-setup" ''
+  export PATH="${ripgrep}/bin:${pulseaudio}/bin:$PATH"
+
+  if pacmd list-sinks | rg librnnoise_ladspa.so >/dev/null; then
+    pactl unload-module module-ladspa-sink
+  fi
+
+  SINK=$(${pulseaudio}/bin/pacmd info | ${ripgrep}/bin/rg -r '$1' '^Default sink name: (.*)$')
+  echo "Setting up NSFV filtering to sink ''${SINK}"
+  ${pulseaudio}/bin/pacmd load-module module-ladspa-sink sink_name=NSFV sink_master=''${SINK} label=noise_suppressor_mono plugin=${nsfv}/lib/ladspa/librnnoise_ladspa.so control=42 rate=48000
+''
diff --git a/tools/perf-flamegraph.nix b/tools/perf-flamegraph.nix
new file mode 100644
index 0000000000..b472b746ff
--- /dev/null
+++ b/tools/perf-flamegraph.nix
@@ -0,0 +1,12 @@
+# Script that collects perf timing for the execution of a command and writes a
+# flamegraph to stdout
+{ pkgs, ... }:
+
+pkgs.writeShellScriptBin "perf-flamegraph" ''
+  set -euo pipefail
+
+  ${pkgs.linuxPackages.perf}/bin/perf record -g --call-graph dwarf -F max "$@"
+  ${pkgs.linuxPackages.perf}/bin/perf script \
+    | ${pkgs.flamegraph}/bin/stackcollapse-perf.pl \
+    | ${pkgs.flamegraph}/bin/flamegraph.pl
+''
diff --git a/tools/releases/default.nix b/tools/releases/default.nix
new file mode 100644
index 0000000000..2d00a2b01f
--- /dev/null
+++ b/tools/releases/default.nix
@@ -0,0 +1,37 @@
+# Definitions for simple release mechanisms from depot.
+{ depot, lib, pkgs, ... }:
+
+let
+  inherit (lib.strings) makeBinPath sanitizeDerivationName;
+in
+{
+  # Use a josh filter to push a certain subset of canon to another git
+  # repository.
+  #
+  # This expects, of course, that the remote repository has granted
+  # push access to the CI SSH key.
+  filteredGitPush = { filter, remote, ref ? "refs/heads/canon" }: {
+    label = ":git: push '${filter}' to external git repository";
+    branches = [ "refs/heads/canon" ];
+    postBuild = true;
+
+    command = pkgs.writeShellScript "${sanitizeDerivationName filter}-push" ''
+      set -e
+      export PATH="${makeBinPath [ pkgs.git depot.third_party.josh ]}:$PATH"
+
+      echo 'Filtering depot through ${filter}'
+      josh-filter '${filter}'
+
+      echo 'Fetching remote to check if a push is needed'
+      git fetch '${remote}' '${ref}'
+
+      if git merge-base --is-ancestor FILTERED_HEAD FETCH_HEAD; then
+        echo 'Commit already present, nothing to push.'
+        exit 0
+      fi
+
+      echo 'Pushing filtered repository to ${remote}:${ref}'
+      git push '${remote}' 'FILTERED_HEAD:${ref}'
+    '';
+  };
+}
diff --git a/tools/rust-crates-advisory/OWNERS b/tools/rust-crates-advisory/OWNERS
new file mode 100644
index 0000000000..1895955b20
--- /dev/null
+++ b/tools/rust-crates-advisory/OWNERS
@@ -0,0 +1,4 @@
+inherited: true
+owners:
+  - Profpatsch
+  - sterni
diff --git a/tools/rust-crates-advisory/check-security-advisory.rs b/tools/rust-crates-advisory/check-security-advisory.rs
new file mode 100644
index 0000000000..e76b090abc
--- /dev/null
+++ b/tools/rust-crates-advisory/check-security-advisory.rs
@@ -0,0 +1,119 @@
+extern crate semver;
+extern crate toml;
+
+use std::io::Write;
+
+/// reads a security advisory of the form
+/// https://github.com/RustSec/advisory-db/blob/a24932e220dfa9be8b0b501210fef8a0bc7ef43e/EXAMPLE_ADVISORY.md
+/// and a crate version number,
+/// and returns 0 if the crate version is patched
+/// and returns 1 if the crate version is *not* patched
+///
+/// If PRINT_ADVISORY is set, the advisory is printed if it matches.
+
+fn main() {
+    let mut args = std::env::args_os();
+    let file = args.nth(1).expect("security advisory md file is $1");
+    let crate_version = args
+        .nth(0)
+        .expect("crate version is $2")
+        .into_string()
+        .expect("crate version string not utf8");
+    let crate_version = semver::Version::parse(&crate_version)
+        .expect(&format!("this is not a semver version: {}", &crate_version));
+    let filename = file.to_string_lossy();
+
+    let content = std::fs::read(&file).expect(&format!("could not read {}", filename));
+    let content = std::str::from_utf8(&content)
+        .expect(&format!("file {} was not encoded as utf-8", filename));
+    let content = content.trim_start();
+
+    let toml_start = content
+        .strip_prefix("```toml")
+        .expect(&format!("file did not start with ```toml: {}", filename));
+    let toml_end_index = toml_start.find("```").expect(&format!(
+        "the toml section did not end, no `` found: {}",
+        filename
+    ));
+    let toml = &toml_start[..toml_end_index];
+    let toml: toml::Value = toml::de::from_slice(toml.as_bytes())
+        .expect(&format!("could not parse toml: {}", filename));
+
+    let versions = toml
+        .as_table()
+        .expect(&format!("the toml is not a table: {}", filename))
+        .get("versions")
+        .expect(&format!(
+            "the toml does not contain the versions field: {}",
+            filename
+        ))
+        .as_table()
+        .expect(&format!(
+            "the toml versions field must be a table: {}",
+            filename
+        ));
+
+    let unaffected = match versions.get("unaffected") {
+        Some(u) => u
+            .as_array()
+            .expect(&format!(
+                "the toml versions.unaffected field must be a list of semvers: {}",
+                filename
+            ))
+            .iter()
+            .map(|v| {
+                semver::VersionReq::parse(
+                    v.as_str()
+                        .expect(&format!("the version field {} is not a string", v)),
+                )
+                .expect(&format!(
+                    "the version field {} is not a valid semver VersionReq",
+                    v
+                ))
+            })
+            .collect(),
+        None => vec![],
+    };
+
+    let mut patched: Vec<semver::VersionReq> = versions
+        .get("patched")
+        .expect(&format!(
+            "the toml versions.patched field must exist: {}",
+            filename
+        ))
+        .as_array()
+        .expect(&format!(
+            "the toml versions.patched field must be a list of semvers: {}",
+            filename
+        ))
+        .iter()
+        .map(|v| {
+            semver::VersionReq::parse(
+                v.as_str()
+                    .expect(&format!("the version field {} is not a string", v)),
+            )
+            .expect(&format!(
+                "the version field {} is not a valid semver VersionReq",
+                v
+            ))
+        })
+        .collect();
+
+    patched.extend_from_slice(&unaffected[..]);
+    let is_patched_or_unaffected = patched.iter().any(|req| req.matches(&crate_version));
+
+    if is_patched_or_unaffected {
+        std::process::exit(0);
+    } else {
+        if std::env::var_os("PRINT_ADVISORY").is_some() {
+            write!(
+                std::io::stderr(),
+                "Advisory {} matched!\n{}\n",
+                filename,
+                content
+            )
+            .unwrap();
+        }
+        std::process::exit(1);
+    }
+}
diff --git a/tools/rust-crates-advisory/default.nix b/tools/rust-crates-advisory/default.nix
new file mode 100644
index 0000000000..b3e8c850eb
--- /dev/null
+++ b/tools/rust-crates-advisory/default.nix
@@ -0,0 +1,200 @@
+{ depot, pkgs, lib, ... }:
+
+let
+
+  bins =
+    depot.nix.getBins pkgs.s6-portable-utils [ "s6-ln" "s6-cat" "s6-echo" "s6-mkdir" "s6-test" "s6-touch" "s6-dirname" ]
+    // depot.nix.getBins pkgs.coreutils [ "printf" ]
+    // depot.nix.getBins pkgs.lr [ "lr" ]
+    // depot.nix.getBins pkgs.cargo-audit [ "cargo-audit" ]
+    // depot.nix.getBins pkgs.jq [ "jq" ]
+    // depot.nix.getBins pkgs.findutils [ "find" ]
+    // depot.nix.getBins pkgs.gnused [ "sed" ]
+  ;
+
+  crate-advisories = "${depot.third_party.rustsec-advisory-db}/crates";
+
+  our-crates = lib.filter (v: v ? outPath)
+    (builtins.attrValues depot.third_party.rust-crates);
+
+  our-crates-lock-file = pkgs.writeText "our-crates-Cargo.lock"
+    (lib.concatMapStrings
+      (crate: ''
+        [[package]]
+        name = "${crate.crateName}"
+        version = "${crate.version}"
+        source = "registry+https://github.com/rust-lang/crates.io-index"
+
+      '')
+      our-crates);
+
+  check-security-advisory = depot.nix.writers.rustSimple
+    {
+      name = "parse-security-advisory";
+      dependencies = [
+        depot.third_party.rust-crates.toml
+        depot.third_party.rust-crates.semver
+      ];
+    }
+    (builtins.readFile ./check-security-advisory.rs);
+
+  # $1 is the directory with advisories for crate $2 with version $3
+  check-crate-advisory = depot.nix.writeExecline "check-crate-advisory" { readNArgs = 3; } [
+    "pipeline"
+    [ bins.lr "-0" "-t" "depth == 1" "$1" ]
+    "forstdin"
+    "-0"
+    "-Eo"
+    "0"
+    "advisory"
+    "if"
+    [ depot.tools.eprintf "advisory %s\n" "$advisory" ]
+    check-security-advisory
+    "$advisory"
+    "$3"
+  ];
+
+  # Run through everything in the `crate-advisories` repository
+  # and check whether we can parse all the advisories without crashing.
+  test-parsing-all-security-advisories = depot.nix.runExecline "check-all-our-crates" { } [
+    "pipeline"
+    [ bins.lr "-0" "-t" "depth == 1" crate-advisories ]
+    "if"
+    [
+      # this will succeed as long as check-crate-advisory doesn’t `panic!()` (status 101)
+      "forstdin"
+      "-0"
+      "-E"
+      "-x"
+      "101"
+      "crate_advisories"
+      check-crate-advisory
+      "$crate_advisories"
+      "foo"
+      "0.0.0"
+    ]
+    "importas"
+    "out"
+    "out"
+    bins.s6-touch
+    "$out"
+  ];
+
+
+  lock-file-report = pkgs.writers.writeBash "lock-file-report" ''
+    set -u
+
+    if test "$#" -lt 2; then
+      echo "Usage: $0 IDENTIFIER LOCKFILE [CHECKLIST [MAINTAINERS]]" >&2
+      echo 2>&1
+      echo "  IDENTIFIER  Unique string describing the lock file" >&2
+      echo "  LOCKFILE    Path to Cargo.lock file" >&2
+      echo "  CHECKLIST   Whether to use GHFM checklists in the output (true or false)" >&2
+      echo "  MAINTAINERS List of @names to cc in case of advisories" >&2
+      exit 100
+    fi
+
+    "${bins.cargo-audit}" audit --json --no-fetch \
+      --db "${depot.third_party.rustsec-advisory-db}" \
+      --file "$2" \
+    | "${bins.jq}" --raw-output --join-output \
+      --from-file "${./format-audit-result.jq}" \
+      --arg maintainers "''${4:-}" \
+      --argjson checklist "''${3:-false}" \
+      --arg attr "$1"
+
+    exit "''${PIPESTATUS[0]}" # inherit exit code from cargo-audit
+  '';
+
+  tree-lock-file-report = depot.nix.writeExecline "tree-lock-file-report"
+    {
+      readNArgs = 1;
+    } [
+    "backtick"
+    "-E"
+    "report"
+    [
+      "pipeline"
+      [ bins.find "$1" "-name" "Cargo.lock" "-and" "-type" "f" "-print0" ]
+      "forstdin"
+      "-E"
+      "-0"
+      "lockFile"
+      "backtick"
+      "-E"
+      "depotPath"
+      [
+        "pipeline"
+        [ bins.s6-dirname "$lockFile" ]
+        bins.sed
+        "s|^\\.|/|"
+      ]
+      lock-file-report
+      "$depotPath"
+      "$lockFile"
+      "false"
+    ]
+    "if"
+    [ bins.printf "%s\n" "$report" ]
+    # empty report implies success (no advisories)
+    bins.s6-test
+    "-z"
+    "$report"
+  ];
+
+  check-all-our-lock-files = depot.nix.writeExecline "check-all-our-lock-files" { } [
+    "backtick"
+    "-EI"
+    "report"
+    [
+      "foreground"
+      [
+        lock-file-report
+        "//third_party/rust-crates"
+        our-crates-lock-file
+        "false"
+      ]
+      tree-lock-file-report
+      "."
+    ]
+    "ifelse"
+    [
+      bins.s6-test
+      "-z"
+      "$report"
+    ]
+    [
+      "exit"
+      "0"
+    ]
+    "pipeline"
+    [
+      "printf"
+      "%s"
+      "$report"
+    ]
+    "buildkite-agent"
+    "annotate"
+    "--style"
+    "warning"
+    "--context"
+    "check-all-our-lock-files"
+  ];
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    test-parsing-all-security-advisories
+    check-crate-advisory
+    lock-file-report
+    ;
+
+
+  tree-lock-file-report = tree-lock-file-report // {
+    meta.ci.extraSteps.run = {
+      label = "Check all crates used in depot for advisories";
+      alwaysRun = true;
+      command = check-all-our-lock-files;
+    };
+  };
+}
diff --git a/tools/rust-crates-advisory/format-audit-result.jq b/tools/rust-crates-advisory/format-audit-result.jq
new file mode 100644
index 0000000000..d42ff6e55c
--- /dev/null
+++ b/tools/rust-crates-advisory/format-audit-result.jq
@@ -0,0 +1,75 @@
+# This is a jq script to format the JSON output of cargo-audit into a short
+# markdown report for humans. It is used by //users/sterni/nixpkgs-crate-holes
+# and //tools/rust-crates-advisory:check-all-our-lock-files which will provide
+# you with example invocations.
+#
+# It needs the following arguments passed to it:
+#
+# - maintainers: Either the empty string or a list of maintainers to @mention
+#   for the current lock file.
+# - attr: An attribute name (or otherwise unique identifier) to associate the
+#   report for the current lock file with.
+# - checklist: If true, the markdown report will use GHFM checklists for the
+#   report, allowing to tick of attributes as taken care of.
+
+# Link to human-readable advisory info for a given vulnerability
+def link:
+  [ "https://rustsec.org/advisories/", .advisory.id, ".html" ] | add;
+
+# Format a list of version constraints
+def version_list:
+  [ .[] | "`" + . + "`" ] | join("; ");
+
+# show paths to fixing this vulnerability:
+#
+# - if there are patched releases, show them (the version we are using presumably
+#   predates the vulnerability discovery, so we likely want to upgrade to a
+#   patched release).
+# - if there are no patched releases, show the unaffected versions (in case we
+#   want to downgrade).
+# - otherwise we state that no unaffected versions are available at this time.
+#
+# This logic should be useful, but is slightly dumber than cargo-audit's
+# suggestion when using the non-JSON output.
+def patched:
+  if .versions.patched == [] then
+    if .versions.unaffected != [] then
+       "unaffected: " + (.versions.unaffected | version_list)
+    else
+      "no unaffected version available"
+    end
+  else
+    "patched: " + (.versions.patched | version_list)
+  end;
+
+# if the vulnerability has aliases (like CVE-*) emit them in parens
+def aliases:
+  if .advisory.aliases == [] then
+    ""
+  else
+    [ " (", (.advisory.aliases | join(", ")), ")" ] | add
+  end;
+
+# each vulnerability is rendered as a (normal) sublist item
+def format_vulnerability:
+  [ "  - "
+  , .package.name, " ", .package.version, ": "
+  , "[", .advisory.id, "](", link, ")"
+  , aliases
+  , ", ", patched
+  , "\n"
+  ] | add;
+
+# be quiet if no found vulnerabilities, otherwise render a GHFM checklist item
+if .vulnerabilities.found | not then
+  ""
+else
+  ([ "-", if $checklist then " [ ] " else " " end
+   , "`", $attr, "`: "
+   , (.vulnerabilities.count | tostring)
+   , " advisories for Cargo.lock"
+   , if $maintainers != "" then " (cc " + $maintainers + ")" else "" end
+   , "\n"
+   ] + (.vulnerabilities.list | map(format_vulnerability))
+  ) | add
+end
diff --git a/tools/tvlc/OWNERS b/tools/tvlc/OWNERS
new file mode 100644
index 0000000000..9e7830ab21
--- /dev/null
+++ b/tools/tvlc/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+ - riking
diff --git a/tools/tvlc/common.sh b/tools/tvlc/common.sh
new file mode 100644
index 0000000000..fe7605857f
--- /dev/null
+++ b/tools/tvlc/common.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+set -eu
+set -o pipefail
+
+source path-scripts
+
+XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
+tvlc_root="$XDG_DATA_HOME/tvlc"
+
+nice_checkout_root=
+if [ -f "$tvlc_root"/nice_checkout_root ]; then
+  nice_checkout_root="$(cat "$tvlc_root"/nice_checkout_root)"
+fi
+nice_checkout_root="${nice_checkout_root:-$HOME/tvlc}"
+
+depot_root=
+if [ -f "$tvlc_root/depot_root" ]; then
+  depot_root="$(cat "$tvlc_root/depot_root")"
+fi
+if [ -d /depot ]; then
+  # don't require config on tvl nixos servers
+  depot_root="${depot_root:-/depot}"
+fi
+if [ -n "$depot_root" ]; then
+  export DEPOT_ROOT="$depot_root"
+fi
+
+if [ ! -d "$tvlc_root" ]; then
+  echo "tvlc: setup required"
+  echo "please run 'tvlc setup' from the depot root"
+  exit 1
+fi
diff --git a/tools/tvlc/default.nix b/tools/tvlc/default.nix
new file mode 100644
index 0000000000..a6f201485f
--- /dev/null
+++ b/tools/tvlc/default.nix
@@ -0,0 +1,51 @@
+{ pkgs, depot, ... }:
+
+let
+  pathScripts = pkgs.writeShellScript "imports" ''
+    export tvix_instantiate="${depot.third_party.nix}/bin/nix-instantiate"
+    export depot_scanner="${depot.tools.depot-scanner}/bin/depot-scanner"
+  '';
+
+  # setup: git rev-parse --show-toplevel > $tvlc_root/depot_root
+  # setup: mkdir $tvlc_root/clients
+  # setup: echo 1 > $tvlc_root/next_clientid
+
+  commonsh = pkgs.stdenv.mkDerivation {
+    name = "common.sh";
+    src = ./common.sh;
+    doCheck = true;
+    unpackPhase = "true";
+    buildPhase = ''
+      substitute ${./common.sh} $out --replace path-scripts ${pathScripts}
+    '';
+    checkPhase = ''
+      ${pkgs.shellcheck}/bin/shellcheck $out ${pathScripts} && echo "SHELLCHECK OK"
+    '';
+    installPhase = ''
+      chmod +x $out
+    '';
+  };
+
+  tvlcNew = pkgs.stdenv.mkDerivation {
+    name = "tvlc-new";
+    src = ./tvlc-new;
+    doCheck = true;
+
+    unpackPhase = "true";
+    buildPhase = ''
+      substitute ${./tvlc-new} $out --replace common.sh ${commonsh}
+    '';
+    checkPhase = ''
+      ${pkgs.shellcheck}/bin/shellcheck $out ${commonsh} ${pathScripts} && echo "SHELLCHECK OK"
+    '';
+    installPhase = ''
+      chmod +x $out
+    '';
+  };
+
+in
+{
+  inherit pathScripts;
+  inherit commonsh;
+  inherit tvlcNew;
+}
diff --git a/tools/tvlc/tvlc-new b/tools/tvlc/tvlc-new
new file mode 100755
index 0000000000..4ef0df5d33
--- /dev/null
+++ b/tools/tvlc/tvlc-new
@@ -0,0 +1,103 @@
+#!/bin/bash
+
+source common.sh
+
+set -eu
+set -o pipefail
+
+function usage() {
+  echo "tvlc new [-n|--name CLIENTNAME] [derivation...]"
+  echo ""
+  cat <<EOF
+  The 'new' command creates a new git sparse checkout with the given name, and
+  contents needed to build the Nix derivation(s) specified on the command line.
+
+  Options:
+    -n/--name client-name: Sets the git branch and nice checkout name for the
+	workspace. If the option is not provided, the name will be based on the
+	first non-option command-line argument.
+    --branch branch-name: Sets the git branch name only.
+EOF
+}
+
+checkout_name=
+branch_name=
+
+options=$(getopt -o 'n:' --long debug --long name: -- "$@")
+eval set -- "$options"
+while true; do
+  case "$1" in
+  -h)
+    usage
+    exit 0
+    ;;
+  -v)
+    version
+    exit 0
+    ;;
+  -n|--name)
+    shift
+    checkout_name="$1"
+    if [ -z "$branch_name" ]; then
+      branch_name=tvlc-"$1"
+    fi
+    ;;
+  --branch)
+    shift
+    branch_name="$1"
+    ;;
+  --)
+    shift
+    break
+    ;;
+  esac
+  shift
+done
+
+if [ $# -eq 0 ]; then
+  echo "error: workspace name, target derivations required"
+  exit 1
+fi
+
+if [ -z "$checkout_name" ]; then
+  # TODO(riking): deduce
+  echo "error: workspace name (-n) required"
+  exit 1
+fi
+
+if [ -d "$nice_checkout_root/$checkout_name" ]; then
+  echo "error: checkout $checkout_name already exists"
+  # nb: shellescape checkout_name because we expect the user to copy-paste it
+  # shellcheck disable=SC1003
+  echo "consider deleting it with tvlc remove '${checkout_name/'/\'}'"
+  exit 1
+fi
+if [ -f "$DEPOT_ROOT/.git/refs/heads/$branch_name" ]; then
+  echo "error: branch $branch_name already exists in git"
+  # shellcheck disable=SC1003
+  echo "consider deleting it with cd $DEPOT_ROOT; git branch -d '${checkout_name/'/\'}'"
+  exit 1
+fi
+
+# The big one: call into Nix to figure out what paths the desired derivations depend on.
+readarray -t includedPaths < <("$depot_scanner" --mode 'print' --only 'DEPOT' --relpath --depot "$DEPOT_ROOT" --nix-bin "$tvix_instantiate" "$@")
+
+# bash math
+checkout_id=$(("$(cat "$tvlc_root/next_clientid")"))
+next_checkout_id=$(("$checkout_id"+1))
+echo "$next_checkout_id" > "$tvlc_root/next_clientid"
+
+checkout_dir="$tvlc_root/clients/$checkout_id"
+mkdir "$checkout_dir"
+cd "$DEPOT_ROOT"
+git worktree add --no-checkout -b "$branch_name" "$checkout_dir"
+# BUG: git not creating the /info/ subdir
+mkdir "$DEPOT_ROOT/.git/worktrees/$checkout_id/info"
+
+cd "$checkout_dir"
+git sparse-checkout init --cone
+git sparse-checkout set "${includedPaths[@]}"
+
+ln -s "$checkout_dir" "$nice_checkout_root"/"$checkout_name"
+
+echo "$nice_checkout_root/$checkout_name"
diff --git a/tvix/.envrc b/tvix/.envrc
new file mode 100644
index 0000000000..ea1ec94e43
--- /dev/null
+++ b/tvix/.envrc
@@ -0,0 +1,10 @@
+source_env ../.envrc
+
+if type lorri &>/dev/null; then
+    echo "direnv: using lorri from PATH ($(type -p lorri))"
+    eval "$(lorri direnv)"
+else
+    # fall back to using direnv's builtin nix support
+    # to prevent bootstrapping problems.
+    use nix
+fi
diff --git a/tvix/.gitignore b/tvix/.gitignore
new file mode 100644
index 0000000000..f807dfa42c
--- /dev/null
+++ b/tvix/.gitignore
@@ -0,0 +1,3 @@
+/target
+/result-*
+/result
diff --git a/tvix/.vscode/extensions.json b/tvix/.vscode/extensions.json
new file mode 100644
index 0000000000..dd7012c107
--- /dev/null
+++ b/tvix/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+    "recommendations": [
+        "matklad.rust-analyzer"
+    ],
+    "unwantedRecommendations": [
+        "rust-lang.rust"
+    ]
+}
diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock
new file mode 100644
index 0000000000..15d97837e6
--- /dev/null
+++ b/tvix/Cargo.lock
@@ -0,0 +1,248 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f34b09b9ee8c7c7b400fe2f8df39cafc9538b03d6ba7f4ae13e4cb90bfbb7d"
+dependencies = [
+ "atty",
+ "bitflags",
+ "indexmap",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.112"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+
+[[package]]
+name = "tvix"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "tempfile",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/tvix/Cargo.toml b/tvix/Cargo.toml
new file mode 100644
index 0000000000..8b0fbd846a
--- /dev/null
+++ b/tvix/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "tvix"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies.clap]
+version = "3.0.5"
+[dependencies.tempfile]
+version = "3.2.0"
+
+[[bin]]
+name = "nix-store"
diff --git a/tvix/LICENSE b/tvix/LICENSE
new file mode 100644
index 0000000000..f288702d2f
--- /dev/null
+++ b/tvix/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/tvix/OWNERS b/tvix/OWNERS
new file mode 100644
index 0000000000..3005e4f6d3
--- /dev/null
+++ b/tvix/OWNERS
@@ -0,0 +1,5 @@
+inherited: false
+owners:
+  - adisbladis
+  - flokli
+  - tazjin
diff --git a/tvix/README.md b/tvix/README.md
new file mode 100644
index 0000000000..9569cedf33
--- /dev/null
+++ b/tvix/README.md
@@ -0,0 +1,20 @@
+Tvix
+====
+
+For more information about Tvix, contact one of the project owners. We
+are interested in people who would like to help us review designs,
+brainstorm and describe requirements that we may not yet have
+considered.
+
+## License structure
+
+All code implemented for Tvix is licensed under the GPL-3.0, with the
+exception of the protocol buffer definitions used for communication
+between services which are available under a more permissive license
+(MIT).
+
+The idea behind this structure is that any direct usage of our code
+(e.g. linking to it, embedding the evaluator, etc.) will fall under
+the terms of the GPL3, but users are free to implement their own
+components speaking these protocols under the terms of the MIT
+license.
diff --git a/tvix/default.nix b/tvix/default.nix
new file mode 100644
index 0000000000..fb4b367ecb
--- /dev/null
+++ b/tvix/default.nix
@@ -0,0 +1,2 @@
+{ ... }:
+{ }
diff --git a/tvix/docs/.gitignore b/tvix/docs/.gitignore
new file mode 100644
index 0000000000..77699ee8a3
--- /dev/null
+++ b/tvix/docs/.gitignore
@@ -0,0 +1,2 @@
+*.svg
+*.html
diff --git a/tvix/docs/Makefile b/tvix/docs/Makefile
new file mode 100644
index 0000000000..ba9e2bdef6
--- /dev/null
+++ b/tvix/docs/Makefile
@@ -0,0 +1,12 @@
+all: build
+
+puml:
+	plantuml *.puml -tsvg
+
+html:
+	pandoc *.md -f markdown --self-contained -t html -s -o tvix.html --csl=${CSL}
+
+build: puml html
+
+clean:
+	rm -f *.tex *.pdf *.png *.svg
diff --git a/tvix/docs/component-flow.puml b/tvix/docs/component-flow.puml
new file mode 100644
index 0000000000..3bcddbe746
--- /dev/null
+++ b/tvix/docs/component-flow.puml
@@ -0,0 +1,74 @@
+@startuml
+
+title Tvix build flow
+
+actor User
+participant CLI
+participant "Coordinator" as Coord
+participant "Evaluator" as Eval
+database Store
+participant "Builder" as Build
+
+note over CLI,Eval
+    Typically runs locally on the invoking machine
+end note
+/ note over Store, Build
+    Can be either local or remote
+end note
+
+User-->CLI: User initiates build of `hello` (analogous to `nix-build -f '<nixpkgs>' -A hello`)
+
+CLI-->Coord: CLI invokes coordinator
+
+Coord-->Eval: Sends message to start evaluation of `<nixpkgs>` (path lookup) with attribute `hello`
+note right: The paths to the evaluator are local file system paths
+
+Coord<--Eval: Yields derivations to be built
+note right
+    Immediately starts streaming derivations as they are instantiated across
+    the dependency graph so they can be built while the evaluation is still running.
+
+    There are two types of build requests: One for regular "fire and forget" builds
+    and another for IFD (import from derivation).
+
+    These are distinct because IFD needs to be fed back into the evaluator for
+    further processing while a regular build does not.
+end note
+
+loop while has more derivations
+
+    Coord-->Store: Check if desired paths are in store
+    alt Store has path
+        Coord<--Store: Success response
+    else Store does not have path
+        Coord-->Build: Request derivation to be built
+        note left
+            The build request optionally includes a desired store.
+            If a builder is aware of how to push to the store it will do so
+            directly when the build is finished.
+
+            If the store is not known by the builder results will be streamed
+            back to the coordinator for store addition.
+        end note
+
+        alt Build failure
+            Coord<--Build: Fail response
+            note left: It's up to the coordinator whether to exit on build failure
+        else Build success
+            alt Known store
+                Build-->Store: Push outputs to store
+                Build<--Coord: Send success & pushed response
+            else Unknown store
+                Build<--Coord: Send success & not pushed response
+                Coord<--Build: Stream build outputs
+                Coord-->Store: Push outputs to store
+            end
+        end
+
+    end
+end
+
+CLI<--Coord: Respond success/fail
+User<--CLI: Exit success/fail
+
+@enduml
diff --git a/tvix/docs/components.md b/tvix/docs/components.md
new file mode 100644
index 0000000000..19e7baa3ec
--- /dev/null
+++ b/tvix/docs/components.md
@@ -0,0 +1,114 @@
+---
+title: "Tvix - Architecture & data flow"
+numbersections: true
+author:
+- adisbladis
+- flokli
+- tazjin
+email:
+- adis@blad.is
+- mail@tazj.in
+lang: en-GB
+classoption:
+- twocolumn
+header-includes:
+- \usepackage{caption, graphicx, tikz, aeguill, pdflscape}
+---
+
+# Background
+
+We intend for Tvix tooling to be more decoupled than the existing,
+monolithic Nix implementation. In practice, we expect to gain several
+benefits from this, such as:
+
+- Ability to use different builders
+- Ability to use different store implementations
+- No monopolisation of the implementation, allowing users to replace
+  components that they are unhappy with (up to and including the
+  language evaluator)
+- Less hidden intra-dependencies between tools due to explicit RPC/IPC
+  boundaries
+
+Communication between different components of the system will use
+gRPC. The rest of this document outlines the components.
+
+# Components
+
+## Coordinator
+
+*Purpose:* The coordinator (in the simplest case, the Tvix CLI tool)
+oversees the flow of a build process and delegates tasks to the right
+subcomponents. For example, if a user runs the equivalent of
+`nix-build` in a folder containing a `default.nix` file, the
+coordinator will invoke the evaluator, pass the resulting derivations
+to the builder and coordinate any necessary store interactions (for
+substitution and other purposes).
+
+While many users are likely to use the CLI tool as their primary
+method of interacting with Tvix, it is not unlikely that alternative
+coordinators (e.g. for a distributed, "Nix-native" CI system) would be
+implemented. To facilitate this, we are considering implementing the
+coordinator on top of a state-machine model that would make it
+possible to reuse the FSM logic without tying it to any particular
+kind of application.
+
+## Evaluator
+
+*Purpose:* Eval takes care of evaluating Nix code. In a typical build
+flow it would be responsible for producing derivations. It can also be
+used as a standalone tool, for example, in use-cases where Nix is used
+to generate configuration without any build or store involvement.
+
+*Requirements:* For now, it will run on the machine invoking the build
+command itself. We give it filesystem access to handle things like
+imports or `builtins.readFile`.
+
+In the future, we might abstract away raw filesystem access by
+allowing the evaluator to request files from the coordinator (which
+will query the store for it). This might get messy, and the benefits
+are questionable. We might be okay with running the evaluator with
+filesystem access for now and can extend the interface if the need
+arises.
+
+## Builder
+
+*Purpose:* A builder receives derivations from the coordinator and
+builds them.
+
+By making builder a standardised interface it's possible to make the
+sandboxing mechanism used by the build process pluggable.
+
+Nix is currently using a hard-coded
+[libseccomp](https://github.com/seccomp/libseccomp) based sandboxing
+mechanism and another one based on
+[sandboxd](https://www.unix.com/man-page/mojave/8/sandboxd/) on macOS.
+These are only separated by [compiler preprocessor
+macros](https://gcc.gnu.org/onlinedocs/cpp/Ifdef.html) within the same
+source files despite having very little in common with each other.
+
+This makes experimentation with alternative backends difficult and
+porting Nix to other platforms harder than it has to be. We want to
+write a new Linux builder which uses
+[OCI](https://github.com/opencontainers/runtime-spec), the current
+dominant Linux containerisation technology, by default.
+
+With a well-defined builder abstraction, it's also easy to imagine
+other backends such as a Kubernetes-based one in the future.
+
+## Store
+
+*Purpose:* Store takes care of storing build results. It provides a
+unified interface to get file paths and upload new ones.
+
+Most likely, we will end up with multiple implementations of store, a
+few possible ones that come to mind are:
+
+- Local
+- SSH
+- GCP
+- S3
+- Ceph
+
+# Figures
+
+![component flow](./component-flow.svg)
diff --git a/tvix/docs/default.nix b/tvix/docs/default.nix
new file mode 100644
index 0000000000..016d641df5
--- /dev/null
+++ b/tvix/docs/default.nix
@@ -0,0 +1,47 @@
+{ pkgs, lib, ... }:
+
+let
+
+  tl = pkgs.texlive.combine {
+    inherit (pkgs.texlive) scheme-medium wrapfig ulem capt-of
+      titlesec preprint enumitem paralist ctex environ svg
+      beamer trimspaces zhnumber changepage framed pdfpages
+      fvextra minted upquote ifplatform xstring;
+  };
+
+  csl = pkgs.fetchurl {
+    name = "numeric.csl";
+    url = "https://gist.githubusercontent.com/bwiernik/8c6f39cf51ceb3a03107/raw/1d75c2d62113ffbba6ed03a47ad99bde86934f2b/APA%2520Numeric";
+    sha256 = "1yfhhnhbzvhrv93baz98frmgsx5y442nzhb0l956l4j35fb0cc3h";
+  };
+
+in
+pkgs.stdenv.mkDerivation {
+  pname = "tvix-doc";
+  version = "0.1";
+
+  outputs = [ "out" "svg" ];
+
+  src = lib.cleanSource ./.;
+
+  CSL = csl;
+
+  nativeBuildInputs = [
+    pkgs.pandoc
+    pkgs.plantuml
+    tl
+  ];
+
+  installPhase = ''
+    runHook preInstall
+
+    mkdir -p $out
+    cp -v *.html $out/
+
+    mkdir -p $svg
+    cp -v *.svg $svg/
+
+    runHook postSubmit
+  '';
+
+}
diff --git a/tvix/docs/language-spec.md b/tvix/docs/language-spec.md
new file mode 100644
index 0000000000..a714374933
--- /dev/null
+++ b/tvix/docs/language-spec.md
@@ -0,0 +1,78 @@
+---
+title: "Specification of the Nix language"
+numbersections: true
+author:
+- tazjin
+email:
+- tazjin@tvl.su
+lang: en-GB
+---
+
+The Nix Language
+================
+
+WARNING: This document is a work in progress. Please keep an eye on
+[`topic:nix-spec`](https://cl.tvl.fyi/q/topic:nix-spec) for ongoing
+CLs.
+
+Nix is a general-purpose, functional programming language which this
+document aims to describe.
+
+## Background
+
+Nix was designed and implemented as part of the [Nix package
+manager](https://nixos.org/nix). It is primarily used for generating
+so-called [*derivations*](#derivations), which are data structures
+describing how to build a package.
+
+The language has been described in the
+[thesis](https://edolstra.github.io/pubs/phd-thesis.pdf) introducing
+the package manager, but only on a high-level. At the time of writing,
+Nix is informally specified (via its only complete implementation in
+the package manager) and there is no complete overview over its -
+sometimes surprising - semantics.
+
+The primary project written in Nix is
+[nixpkgs](https://github.com/NixOS/nixpkgs/). Uncertainties in the
+process of writing this specification are resolved by investigating
+patterns in nixpkgs, which we consider canonical. The code in nixpkgs
+uses a reasonable subset of the features exposed by the current
+implementation, some of which are *accidental*, and is thus more
+useful for specifying how the language should work.
+
+## Introduction to Nix
+
+Nix is a general-purpose, partially lazy, functional programming
+language which provides higher-order functions, type reflection,
+primitive data types such as integers, strings and floats, and
+compound data structures such as lists and attribute sets.
+
+Nix has syntactic sugar for common operations, such as those for
+attribute sets, and also provides a wide range of built-in functions
+which have organically accumulated over time.
+
+Nix has a variety of legacy features that are not in practical use,
+but are documented in sections of this specification for the sake of
+completeness.
+
+This document describes the syntax and abstract semantics of the Nix
+language, but leaves out implementation details about how Nix can be
+interpreted/compiled/analysed etc.
+
+### Program structure
+
+This section describes the semantic structure of Nix, and how it
+relates to the rest of the specification.
+
+Each Nix program is a single [*expression*](#expressions) denoting a
+[*value*](#values) (commonly a [*function*](#functions)). Each value
+has a [*type*](#types), however this type is not statically known.
+
+Nix code is modularised through the use of the
+[*import*](#builtins-import) built-in function. No separate module
+system exists.
+
+In addition to chapters describing the building blocks mentioned
+above, this specificiation also describes the [*syntax*](#syntax), the
+available [built-in functions](#builtins), [*error handling*](#errors)
+and known [*deficiencies*](#deficiencies) in the language.
diff --git a/tvix/proto/LICENSE b/tvix/proto/LICENSE
new file mode 100644
index 0000000000..36878fe4cb
--- /dev/null
+++ b/tvix/proto/LICENSE
@@ -0,0 +1,21 @@
+Copyright © 2021 The Tvix Authors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+“Software”), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/tvix/proto/default.nix b/tvix/proto/default.nix
new file mode 100644
index 0000000000..ac0ee66e87
--- /dev/null
+++ b/tvix/proto/default.nix
@@ -0,0 +1,9 @@
+# Build protocol buffer definitions to ensure that protos are valid in
+# CI. Note that the output of this build target is not actually used
+# anywhere, it just functions as a CI check for now.
+{ pkgs, ... }:
+
+pkgs.runCommandNoCC "tvix-cc-proto" { } ''
+  mkdir $out
+  ${pkgs.protobuf}/bin/protoc -I ${./.} evaluator.proto --cpp_out=$out
+''
diff --git a/tvix/proto/evaluator.proto b/tvix/proto/evaluator.proto
new file mode 100644
index 0000000000..710a28fb9d
--- /dev/null
+++ b/tvix/proto/evaluator.proto
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2021 The Tvix Authors
+syntax = "proto3";
+
+package tvix.proto.evaluator.v1;
+
+service EvaluatorService {
+  rpc Evaluate(stream EvaluateRequest) returns (stream EvaluateResponse) {}
+}
+
+//
+// Message types for EvaluateRequest
+//
+
+message EvaluateFile {
+  // Absolute path at which the evaluator can find the file to be
+  // evaluated.
+  string file_path = 1;
+
+  // Optional attribute that should be evaluated within the file,
+  // assuming that the value it evaluates to is an attribute set.
+  optional string attribute = 2;
+
+  // Additional arguments to pass into the evaluation, with which the
+  // file's top-level function will be auto-called.
+  map<string, NixValue> arguments = 3;
+}
+
+message EvaluateExpression {
+  // Literal Nix expression to evaluate.
+  string expression = 1;
+
+  // Working directory in which the expression should be evaluated.
+  string working_directory = 2;
+}
+
+message BuildResultChunk {
+  string drv_hash = 1;
+  string output = 2;
+  bytes data = 3;
+
+  // This field may be set on the first build result chunk returned
+  // to the evaluator, indicating the total size of the output that
+  // is going to be streamed in bytes.
+  //
+  // If set, the evaluator can use this to appropriately allocate a
+  // buffer for the output.
+  optional int64 output_size = 4;
+}
+
+// Indicates that a single build has completed successfully. In case
+// that the build outputs were required by the evaluator this also
+// indicates that the output has been returned completely.
+message BuildSuccess {
+  string drv_hash = 1;
+  string output = 2;
+}
+
+// Describes an error that occured during a single build.
+//
+// TODO: We might want a more sophisticated error type.
+message BuildError {
+  string drv_hash = 1;
+  string output = 2;
+  string error = 3;
+}
+
+message BuildResult {
+  oneof build_result {
+    BuildSuccess build_success = 1;
+    BuildError build_error = 2;
+  }
+}
+
+
+/// Messages sent to the evaluator by the build coordinator.
+message EvaluateRequest {
+  oneof message {
+    // Ask the evaluator to evaluate the specified file, and
+    // optionally attribute within that file. Must be the first
+    // message.
+    EvaluateFile evaluate_file = 1;
+
+    // Ask the evaluator to evaluate the specified Nix expression.
+    // Must be the first message.
+    EvaluateExpression evaluate_expression = 2;
+
+    // Send the chunks of a build result, in response to a
+    // BuildRequest.
+    //
+    // Note: This message might change as the store protocol is
+    // designed, as it is possible that mechanisms for transferring
+    // files might be reused between the protocols.
+    BuildResultChunk build_result_chunk = 3;
+
+    // Indicate the result of a single build. See the documentation
+    // for the message types defined above for semantic details.
+    BuildResult build_result = 4;
+  }
+}
+
+//
+// Message types for EvaluateResponse
+//
+
+// TODO: Placeholder type.
+message Derivation {
+  string drv = 1;
+}
+
+// TODO: Placeholder type.
+message NixValue {
+  string value = 1;
+}
+
+// TODO: Placeholder type.
+message NixError {
+  string value = 1;
+}
+
+message BuildRequest {
+  Derivation drv = 1;
+  string output = 2;
+}
+
+// Messages returned to the coordinator by the evaluator.
+message EvaluateResponse {
+  oneof message {
+    // A derivation that was instantiated while reducing the graph,
+    // and whose output is not required by the evaluator.
+    Derivation derivation = 1;
+
+    // A derivation that was instantiated while reducing the graph,
+    // and whose output is required by the evaluator (IFD).
+    BuildRequest build_request = 2;
+
+    // The final value yielded by the evaluation. Stream is closed
+    // after this.
+    NixValue done = 3;
+
+    // Evaluation error. Stream is closed after this.
+    NixError error = 4;
+  }
+}
diff --git a/tvix/shell.nix b/tvix/shell.nix
new file mode 100644
index 0000000000..f57019cd94
--- /dev/null
+++ b/tvix/shell.nix
@@ -0,0 +1,11 @@
+let
+  depot = (import ./.. { });
+  pkgs = depot.third_party.nixpkgs;
+
+in
+pkgs.mkShell {
+  buildInputs = [
+    pkgs.rustup
+    pkgs.rust-analyzer
+  ];
+}
diff --git a/tvix/src/bin/nix-store.rs b/tvix/src/bin/nix-store.rs
new file mode 100644
index 0000000000..e1568fff73
--- /dev/null
+++ b/tvix/src/bin/nix-store.rs
@@ -0,0 +1,104 @@
+fn main() {
+    main_args(std::env::args().collect()).unwrap_or_else(|e| e.exit());
+}
+
+pub fn main_args(args: Vec<String>) -> clap::Result<NixResult> {
+    let matches = clap::App::new("nix-store")
+        .subcommand(clap::App::new("--add").arg(clap::Arg::new("FILE").required(true).index(1)))
+        .try_get_matches_from(args.iter())?;
+    if let Some(add) = matches.subcommand_matches("--add") {
+        let file = add.value_of("FILE").expect("--add needs a file");
+        let file_contents =
+            std::fs::read_to_string(file).expect(&format!("file {} does not exist", file));
+        Ok(NixResult::FileAddedToStore {
+            content: file_contents,
+        })
+    } else {
+        panic!("read some arguments that we do not know: {:?}", args)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum NixResult {
+    FileAddedToStore { content: String },
+}
+
+#[cfg(test)]
+mod integration_tests {
+    use std::collections::VecDeque;
+    use std::io::Write;
+
+    use super::*;
+
+    #[derive(Debug)]
+    enum NixOutput {
+        Err {
+            status: i32,
+            stdout: String,
+            stderr: String,
+        },
+        Ok {
+            stdout: String,
+            stderr: String,
+        },
+    }
+
+    fn run_nix_command(cmd: &str, args: Vec<String>) -> NixOutput {
+        let out = std::process::Command::new(cmd)
+            .args(args)
+            .output()
+            .expect(&format!("could not run {}", cmd));
+        match out.status.code().expect("no status code!") {
+            0 => NixOutput::Ok {
+                stdout: String::from_utf8_lossy(&out.stdout).trim_end().to_string(),
+                stderr: String::from_utf8_lossy(&out.stderr).trim_end().to_string(),
+            },
+            status => NixOutput::Err {
+                status,
+                stdout: String::from_utf8_lossy(&out.stdout).trim_end().to_string(),
+                stderr: String::from_utf8_lossy(&out.stderr).trim_end().to_string(),
+            },
+        }
+    }
+
+    fn nix_nix_store<'a>(args: Vec<String>) -> NixResult {
+        match run_nix_command("nix-store", args) {
+            err @ NixOutput::Err { .. } => panic!("nix-store --add failed: {:#?}", err),
+            NixOutput::Ok { stdout, .. } => NixResult::FileAddedToStore {
+                content: std::fs::read_to_string(&stdout)
+                    .expect(&format!("cannot open {} as store file", stdout)),
+            },
+        }
+    }
+
+    fn tvix_nix_store<'a>(args: Vec<String>) -> NixResult {
+        eprintln!("running tvix with arguments {:?}", args);
+        let mut args = VecDeque::from(args);
+        args.push_front("tvix-store".to_string());
+        super::main_args(Vec::from(args))
+            .unwrap_or_else(|e| panic!("clap command line parsing failed:\n{}", e))
+    }
+
+    #[test]
+    fn test_nix_store_add() {
+        let file_content = "I am a copied file";
+        let mut tempfile = tempfile::NamedTempFile::new().expect("cannot create temp file");
+        tempfile
+            .write_all(file_content.as_bytes())
+            .expect("could not write to tempfile");
+        assert_eq!(
+            tvix_nix_store(vec![
+                "--add".to_string(),
+                tempfile.path().as_os_str().to_string_lossy().into_owned()
+            ]),
+            nix_nix_store(vec![
+                "--add".to_string(),
+                tempfile.path().as_os_str().to_string_lossy().into_owned()
+            ]),
+            "added file contents were not the same"
+        );
+
+        // make sure the tempfile lives till here
+        drop(tempfile)
+    }
+}
diff --git a/tvix/src/main.rs b/tvix/src/main.rs
new file mode 100644
index 0000000000..40086e6f27
--- /dev/null
+++ b/tvix/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello, tvix!");
+}
diff --git a/users/Profpatsch/OWNERS b/users/Profpatsch/OWNERS
new file mode 100644
index 0000000000..5a73d4c3a1
--- /dev/null
+++ b/users/Profpatsch/OWNERS
@@ -0,0 +1,4 @@
+inherited: false
+owners:
+  - Profpatsch
+  - sterni
diff --git a/users/Profpatsch/advent-of-code/2020/01/main.py b/users/Profpatsch/advent-of-code/2020/01/main.py
new file mode 100644
index 0000000000..e636017a54
--- /dev/null
+++ b/users/Profpatsch/advent-of-code/2020/01/main.py
@@ -0,0 +1,22 @@
+import sys
+
+l = []
+with open('./input', 'r') as f:
+    for line in f:
+        l.append(int(line))
+
+s = set(l)
+
+res=None
+for el in s:
+    for el2 in s:
+        if (2020-(el+el2)) in s:
+            res=(el, el2, 2020-(el+el2))
+            break
+
+if res == None:
+    sys.exit("could not find a number that adds to 2020")
+
+print(res)
+
+print(res[0] * res[1] * res[2])
diff --git a/users/Profpatsch/advent-of-code/2020/02/main.py b/users/Profpatsch/advent-of-code/2020/02/main.py
new file mode 100644
index 0000000000..e3b27c382a
--- /dev/null
+++ b/users/Profpatsch/advent-of-code/2020/02/main.py
@@ -0,0 +1,77 @@
+import sys
+
+def parse(line):
+    a = line.split(sep=" ", maxsplit=1)
+    assert len(a) == 2
+    fromto = a[0].split(sep="-")
+    assert len(fromto) == 2
+    (from_, to) = (int(fromto[0]), int(fromto[1]))
+    charpass = a[1].split(sep=": ")
+    assert len(charpass) == 2
+    char = charpass[0]
+    assert len(char) == 1
+    pass_ = charpass[1]
+    assert pass_.endswith("\n")
+    pass_ = pass_[:-1]
+    return {
+        "from": from_,
+        "to": to,
+        "char": char,
+        "pass": pass_
+    }
+
+def char_in_pass(char, pass_):
+    return pass_.count(char)
+
+def validate_01(entry):
+    no = char_in_pass(entry["char"], entry["pass"])
+    if no < entry["from"]:
+        return { "too-small": entry }
+    elif no > entry["to"]:
+        return { "too-big": entry }
+    else:
+        return { "ok": entry }
+
+def char_at_pos(char, pos, pass_):
+    assert pos <= len(pass_)
+    return pass_[pos-1] == char
+
+def validate_02(entry):
+    one = char_at_pos(entry["char"], entry["from"], entry["pass"])
+    two = char_at_pos(entry["char"], entry["to"], entry["pass"])
+    if one and two:
+        return { "both": entry }
+    elif one:
+        return { "one": entry }
+    elif two:
+        return { "two": entry }
+    else:
+        return { "none": entry }
+
+
+res01 = []
+res02 = []
+with open("./input", 'r') as f:
+    for line in f:
+        p = parse(line)
+        res01.append(validate_01(p))
+        res02.append(validate_02(p))
+
+count01=0
+for r in res01:
+    print(r)
+    if r.get("ok", False):
+        count01=count01+1
+
+count02=0
+for r in res02:
+    print(r)
+    if r.get("one", False):
+        count02=count02+1
+    elif r.get("two", False):
+        count02=count02+1
+    else:
+        pass
+
+print("count 1: {}".format(count01))
+print("count 2: {}".format(count02))
diff --git a/users/Profpatsch/advent-of-code/2020/03/main.py b/users/Profpatsch/advent-of-code/2020/03/main.py
new file mode 100644
index 0000000000..4d6baf946c
--- /dev/null
+++ b/users/Profpatsch/advent-of-code/2020/03/main.py
@@ -0,0 +1,66 @@
+import itertools
+import math
+
+def tree_line(init):
+    return {
+        "init-len": len(init),
+        "known": '',
+        "rest": itertools.repeat(init)
+    }
+
+def tree_line_at(pos, tree_line):
+    needed = (pos + 1) - len(tree_line["known"])
+    # internally advance the tree line to the position requested
+    if needed > 0:
+        tree_line["known"] = tree_line["known"] \
+          + ''.join(
+            itertools.islice(
+                tree_line["rest"],
+                1+math.floor(needed / tree_line["init-len"])))
+    # print(tree_line)
+    return tree_line["known"][pos] == '#'
+
+def tree_at(linepos, pos, trees):
+    return tree_line_at(pos, trees[linepos])
+
+def slope_positions(trees, right, down):
+    line = 0
+    pos = 0
+    while line < len(trees):
+        yield (line, pos)
+        line = line + down
+        pos = pos + right
+
+trees = []
+with open("./input", 'r') as f:
+    for line in f:
+        line = line.rstrip()
+        trees.append(tree_line(line))
+
+# print(list(itertools.islice(trees[0], 5)))
+# print(list(map(
+#     lambda x: tree_at(0, x, trees),
+#     range(100)
+# )))
+# print(list(slope_positions(trees, right=3, down=1)))
+
+def count_slope_positions(trees, slope):
+    count = 0
+    for (line, pos) in slope:
+        if tree_at(line, pos, trees):
+            count = count + 1
+    return count
+
+print(
+        count_slope_positions(trees, slope_positions(trees, right=1, down=1))
+    *
+        count_slope_positions(trees, slope_positions(trees, right=3, down=1))
+    *
+        count_slope_positions(trees, slope_positions(trees, right=5, down=1))
+    *
+        count_slope_positions(trees, slope_positions(trees, right=7, down=1))
+    *
+        count_slope_positions(trees, slope_positions(trees, right=1, down=2))
+)
+
+# I realized I could have just used a modulo instead …
diff --git a/users/Profpatsch/advent-of-code/2020/04/main.py b/users/Profpatsch/advent-of-code/2020/04/main.py
new file mode 100644
index 0000000000..36bbed7146
--- /dev/null
+++ b/users/Profpatsch/advent-of-code/2020/04/main.py
@@ -0,0 +1,104 @@
+import sys
+import itertools
+import re
+import pprint
+
+def get_entry(fd):
+    def to_dict(keyval):
+        res = {}
+        for (k, v) in keyval:
+            assert k not in res
+            res[k] = v
+        return res
+
+    res = []
+    for line in fd:
+        if line == "\n":
+            yield to_dict(res)
+            res = []
+        else:
+            line = line.rstrip()
+            items = line.split(" ")
+            for i in items:
+                res.append(i.split(":", maxsplit=2))
+
+def val_hgt(hgt):
+    m = re.fullmatch(r'([0-9]+)(cm|in)', hgt)
+    if m:
+        (i, what) = m.group(1,2)
+        i = int(i)
+        if what == "cm":
+            return i >= 150 and i <= 193
+        elif what == "in":
+            return i >= 59 and i <= 76
+        else:
+            return False
+
+required_fields = [
+    { "name": "byr",
+      "check": lambda s: int(s) >= 1920 and int(s) <= 2002
+    },
+    { "name": "iyr",
+      "check": lambda s: int(s) >= 2010 and int(s) <= 2020
+    },
+    { "name": "eyr",
+      "check": lambda s: int(s) >= 2020 and int(s) <= 2030,
+    },
+    { "name": "hgt",
+      "check": lambda s: val_hgt(s)
+    },
+    { "name": "hcl",
+      "check": lambda s: re.fullmatch(r'#[0-9a-f]{6}', s)
+    },
+    { "name": "ecl",
+      "check": lambda s: re.fullmatch(r'amb|blu|brn|gry|grn|hzl|oth', s)
+    },
+    { "name": "pid",
+      "check": lambda s: re.fullmatch(r'[0-9]{9}', s)
+    },
+    # we should treat it as not required
+    # "cid"
+]
+
+required_dict = {}
+for f in required_fields:
+    required_dict[f["name"]] = f
+
+def validate(keyval):
+    if keyval[0] not in required_dict:
+        return { "ok": keyval }
+    if required_dict[keyval[0]]["check"](keyval[1]):
+        return { "ok": keyval }
+    else:
+        return { "validation": keyval }
+
+def all_fields(entry):
+    missing = []
+    for r in required_dict:
+        if r not in e:
+            missing.append(r)
+    if missing == []:
+        return { "ok": entry }
+    else:
+        return { "missing": missing }
+
+count=0
+for e in get_entry(sys.stdin):
+    a = all_fields(e)
+    if a.get("ok", False):
+        res = {}
+        bad = False
+        for keyval in e.items():
+            r = validate(keyval)
+            if r.get("validation", False):
+                bad = True
+            res[keyval[0]] = r
+        if bad:
+            pprint.pprint({ "validation": res })
+        else:
+            pprint.pprint({ "ok": e })
+            count = count+1
+    else:
+        pprint.pprint(a)
+
+print(count)
diff --git a/users/Profpatsch/aerc-no-config-perms.patch b/users/Profpatsch/aerc-no-config-perms.patch
new file mode 100644
index 0000000000..86b41cd74b
--- /dev/null
+++ b/users/Profpatsch/aerc-no-config-perms.patch
@@ -0,0 +1,12 @@
+diff --git a/config/config.go b/config/config.go
+index 0472daf..5eed379 100644
+--- a/config/config.go
++++ b/config/config.go
+@@ -779,6 +779,7 @@ func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup
+ // checkConfigPerms checks for too open permissions
+ // printing the fix on stdout and returning an error
+ func checkConfigPerms(filename string) error {
++        return nil;
+ 	info, err := os.Stat(filename)
+ 	if err != nil {
+ 		return nil // disregard absent files
diff --git a/users/Profpatsch/aerc.dhall b/users/Profpatsch/aerc.dhall
new file mode 100644
index 0000000000..fb63f7044b
--- /dev/null
+++ b/users/Profpatsch/aerc.dhall
@@ -0,0 +1,157 @@
+let NameVal = λ(T : Type) → { name : Text, value : T }
+
+in  λ ( imports
+      : { -- Take an aerc filter from the aerc distribution /share directory
+          aercFilter : Text → Text
+        , -- given a dsl of functions to create an Ini, render the ini file
+          toIni :
+            { globalSection : List (NameVal Text)
+            , sections : List (NameVal (List (NameVal Text)))
+            } →
+              Text
+        }
+      ) →
+      let List/map
+          : ∀(a : Type) → ∀(b : Type) → (a → b) → List a → List b
+          = λ(a : Type) →
+            λ(b : Type) →
+            λ(f : a → b) →
+            λ(xs : List a) →
+              List/build
+                b
+                ( λ(list : Type) →
+                  λ(cons : b → list → list) →
+                    List/fold a xs list (λ(x : a) → cons (f x))
+                )
+
+      in  { accounts =
+              imports.toIni
+                { globalSection = [] : List (NameVal Text)
+                , sections =
+                  [ { name = "mail"
+                    , value =
+                      [ { name = "archive", value = "Archive" }
+                      , { name = "copy-to", value = "Sent" }
+                      , { name = "default", value = "INBOX" }
+                      , { name = "from"
+                        , value = "Profpatsch <mail@profpatsch.de>"
+                        }
+                      , { name = "source", value = "maildir://~/.Mail/mail" }
+                      , { name = "postpone", value = "Drafts" }
+                      ]
+                    }
+                  ]
+                }
+          , aerc =
+              imports.toIni
+                { globalSection = [] : List (NameVal Text)
+                , sections =
+                  [ { name = "filters"
+                    , value =
+                      [ { name = "text/html"
+                        , value = imports.aercFilter "html"
+                        }
+                      , let _ = "-- TODO: this awk should be taken from nix!"
+
+                        in  { name = "text/*"
+                            , value = "awk -f ${imports.aercFilter "plaintext"}"
+                            }
+                      ]
+                    }
+                  ]
+                }
+          , binds =
+              let
+                  -- keybinding and command to run
+                  Key =
+                    { ctrl : Bool, key : Text, cmd : Text }
+
+              in  let
+                      -- render a key to config format
+                      renderKey =
+                        λ(k : Key) →
+                          if    k.ctrl
+                          then  { name = "<C-${k.key}>", value = k.cmd }
+                          else  { name = k.key, value = k.cmd }
+
+                  let
+
+                      -- render a list of keys to config format
+                      renderKeys =
+                        λ(keys : List Key) →
+                          List/map Key (NameVal Text) renderKey keys
+
+                  let
+                      -- create a section whith a name and a list of keys
+                      sect =
+                        λ(section : Text) →
+                        λ(keys : List Key) →
+                          { name = section, value = renderKeys keys }
+
+                  let
+
+                      -- set key without modifiers
+                      key =
+                        λ(key : Text) → { key }
+
+                  let
+                      -- set special key without modifiers
+                      special =
+                        λ(key : Text) → { key = "<${key}>" }
+
+                  let
+                      -- no modifier
+                      none =
+                        { ctrl = False }
+
+                  let
+                      -- set control key
+                      ctrl =
+                        { ctrl = True }
+
+                  let
+                      -- set a command to execute
+                      cmd =
+                        λ(cmd : Text) → { cmd = ":${cmd}<Enter>" }
+
+                  let
+                      -- set a command, but stay on the prompt
+                      prompt =
+                        λ(cmd : Text) → { cmd = ":${cmd}<Space>" }
+
+                  let config =
+                        { globalSection =
+                            renderKeys
+                              [ ctrl ∧ key "p" ∧ cmd "prev-tab"
+                              , ctrl ∧ key "n" ∧ cmd "next-tab"
+                              , ctrl ∧ key "t" ∧ cmd "term"
+                              ]
+                        , sections =
+                          [ sect
+                              "messages"
+                              [ ctrl ∧ key "q" ∧ cmd "quit"
+                              , none ∧ special "Up" ∧ cmd "prev"
+                              , none ∧ special "Down" ∧ cmd "next"
+                              , none ∧ special "PgUp" ∧ cmd "prev 100%"
+                              , none ∧ special "PgDn" ∧ cmd "next 100%"
+                              , none ∧ key "g" ∧ cmd "select 0"
+                              , none ∧ key "G" ∧ cmd "select -1"
+                              , ctrl ∧ key "Up" ∧ cmd "prev-folder"
+                              , ctrl ∧ key "Down" ∧ cmd "next-folder"
+                              , none ∧ key "v" ∧ cmd "mark -t"
+                              , none ∧ key "V" ∧ cmd "mark -v"
+                              , none ∧ special "Enter" ∧ cmd "view"
+                              , none ∧ key "c" ∧ cmd "compose"
+                              , none ∧ key "|" ∧ prompt "pipe"
+                              , none ∧ key "t" ∧ prompt "term"
+                              , none ∧ key "/" ∧ prompt "search"
+                              , none ∧ key "n" ∧ cmd "next-result"
+                              , none ∧ key "N" ∧ cmd "prev-result"
+                              , none ∧ special "Esc" ∧ cmd "clear"
+                              ]
+                          , sect "view" [ none ∧ key "q" ∧ cmd "close" ]
+                          ]
+                        }
+
+                  in  imports.toIni config
+          }
diff --git a/users/Profpatsch/aerc.nix b/users/Profpatsch/aerc.nix
new file mode 100644
index 0000000000..569f045a00
--- /dev/null
+++ b/users/Profpatsch/aerc.nix
@@ -0,0 +1,51 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  aerc-patched = pkgs.aerc.overrideAttrs (old: {
+    patches = old.patches or [ ] ++ [
+      ./aerc-no-config-perms.patch
+    ];
+  });
+
+  bins = depot.nix.getBins aerc-patched [ "aerc" ];
+
+  config =
+    depot.users.Profpatsch.importDhall.importDhall
+      {
+        root = ./.;
+        files = [
+          "aerc.dhall"
+        ];
+        main = "aerc.dhall";
+        deps = [ ];
+      }
+      {
+        aercFilter = name: "${aerc-patched}/share/aerc/filters/${name}";
+        toIni = depot.users.Profpatsch.toINI { };
+      };
+
+  aerc-config = pkgs.linkFarm "alacritty-config" [
+    {
+      name = "aerc/accounts.conf";
+      path = pkgs.writeText "accounts.conf" config.accounts;
+    }
+    {
+      name = "aerc/aerc.conf";
+      path = pkgs.writeText "aerc.conf" config.aerc;
+    }
+    {
+      name = "aerc/binds.conf";
+      path = pkgs.writeText "binds.conf" config.binds;
+    }
+  ];
+
+  aerc = depot.nix.writeExecline "aerc" { } [
+    "export"
+    "XDG_CONFIG_HOME"
+    aerc-config
+    bins.aerc
+    "$@"
+  ];
+
+in
+aerc
diff --git a/users/Profpatsch/alacritty.dhall b/users/Profpatsch/alacritty.dhall
new file mode 100644
index 0000000000..b4d99c8294
--- /dev/null
+++ b/users/Profpatsch/alacritty.dhall
@@ -0,0 +1,48 @@
+let sol = (./solarized.dhall).hex
+
+let black = "#000000"
+
+let white = "#ffffff"
+
+let
+    -- todo: this looks not too good
+    solarized-dark =
+      { --Colors (Solarized Dark)
+        colors =
+        { -- Default colors
+          primary =
+          { background = black, foreground = white }
+        , -- Cursor colors
+          cursor =
+          { text = sol.base03, cursor = sol.base0 }
+        , -- Normal colors
+          normal =
+          { black = sol.base02
+          , red = sol.red
+          , green = sol.green
+          , yellow = sol.yellow
+          , blue = sol.blue
+          , magenta = sol.magenta
+          , cyan = sol.cyan
+          , white = sol.base2
+          }
+        , -- Bright colors
+          bright =
+          { black = sol.base03
+          , red = sol.orange
+          , green = sol.base01
+          , yellow = sol.base00
+          , blue = sol.base0
+          , magenta = sol.violet
+          , cyan = sol.base1
+          , white = sol.base3
+          }
+        }
+      }
+
+in  { alacritty-config = { font.size = 18, scolling.history = 100000 }
+    ,   -- This disables the dpi-sensitive scaling (cause otherwise the font will be humongous on my laptop screen)
+        alacritty-env
+      . WINIT_X11_SCALE_FACTOR
+      = 1
+    }
diff --git a/users/Profpatsch/alacritty.nix b/users/Profpatsch/alacritty.nix
new file mode 100644
index 0000000000..d2cb8de2fc
--- /dev/null
+++ b/users/Profpatsch/alacritty.nix
@@ -0,0 +1,34 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.alacritty [ "alacritty" ];
+
+  config =
+    depot.users.Profpatsch.importDhall.importDhall {
+      root = ./.;
+      files = [
+        "alacritty.dhall"
+        "solarized.dhall"
+      ];
+      main = "alacritty.dhall";
+      deps = [ ];
+    };
+
+  config-file = lib.pipe config.alacritty-config [
+    (lib.generators.toYAML { })
+    (pkgs.writeText "alacritty.conf")
+  ];
+
+
+  alacritty = depot.nix.writeExecline "alacritty" { } (
+    (lib.concatLists (lib.mapAttrsToList (k: v: [ "export" k (toString v) ]) config.alacritty-env))
+    ++ [
+      bins.alacritty
+      "--config-file"
+      config-file
+      "$@"
+    ]
+  );
+
+in
+alacritty
diff --git a/users/Profpatsch/aliases.nix b/users/Profpatsch/aliases.nix
new file mode 100644
index 0000000000..6a1c2c1a63
--- /dev/null
+++ b/users/Profpatsch/aliases.nix
@@ -0,0 +1,75 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.findutils [ "find" ];
+
+in
+depot.nix.readTree.drvTargets {
+
+  findia = depot.nix.writeExecline "findia"
+    {
+      readNArgs = 1;
+      # TODO: comment out, thanks to sterni blocking the runExecline change
+      # meta.description = ''
+      #   Find case-insensitive anywhere (globbing)
+
+      #   Usage: findia <pattern> <more find(1) arguments>
+      # '';
+    } [
+    bins.find
+    "-iname"
+    "*\${1}*"
+    "$@"
+  ];
+
+  findial = depot.nix.writeExecline "findial"
+    {
+      readNArgs = 1;
+      # TODO: comment out, thanks to sterni blocking the runExecline change
+      # meta.description = ''
+      #   Find case-insensitive anywhere (globbing), follow symlinks";
+
+      #   Usage: findial <pattern> <more find(1) arguments>
+      # '';
+    } [
+    bins.find
+    "-L"
+    "-iname"
+    "*\${1}*"
+    "$@"
+  ];
+
+  findian = depot.nix.writeExecline "findian"
+    {
+      readNArgs = 2;
+      # TODO: comment out, thanks to sterni blocking the runExecline change
+      # meta.description = ''
+      #   Find case-insensitive anywhere (globbing) in directory
+
+      #   Usage: findian <directory> <pattern> <more find(1) arguments>
+      # '';
+    } [
+    bins.find
+    "$1"
+    "-iname"
+    "*\${2}*"
+    "$@"
+  ];
+
+  findiap = depot.nix.writeExecline "findiap"
+    {
+      readNArgs = 2;
+      # TODO: comment out, thanks to sterni blocking the runExecline change
+      # meta.description = ''
+      #   Find case-insensitive anywhere (globbing) in directory, the pattern allows for paths.
+
+      #   Usage: findiap <directory> <pattern> <more find(1) arguments>
+      # '';
+    } [
+    bins.find
+    "$1"
+    "-ipath"
+    "*\${2}*"
+    "$@"
+  ];
+}
diff --git a/users/Profpatsch/arglib/netencode.nix b/users/Profpatsch/arglib/netencode.nix
new file mode 100644
index 0000000000..3f1d121e51
--- /dev/null
+++ b/users/Profpatsch/arglib/netencode.nix
@@ -0,0 +1,42 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  netencode = {
+    rust = depot.nix.writers.rustSimpleLib
+      {
+        name = "arglib-netencode";
+        dependencies = [
+          depot.users.Profpatsch.execline.exec-helpers
+          depot.users.Profpatsch.netencode.netencode-rs
+        ];
+      } ''
+      extern crate netencode;
+      extern crate exec_helpers;
+
+      use netencode::{T};
+      use std::os::unix::ffi::OsStrExt;
+
+      pub fn arglib_netencode(prog_name: &str, env: Option<&std::ffi::OsStr>) -> T {
+          let env = match env {
+              None => std::ffi::OsStr::from_bytes("ARGLIB_NETENCODE".as_bytes()),
+              Some(a) => a
+          };
+          let t = match std::env::var_os(env) {
+              None => exec_helpers::die_user_error(prog_name, format!("could not read args, envvar {} not set", env.to_string_lossy())),
+              // TODO: good error handling for the different parser errors
+              Some(soup) => match netencode::parse::t_t(soup.as_bytes()) {
+                  Ok((remainder, t)) => match remainder.is_empty() {
+                      true => t,
+                      false => exec_helpers::die_environment_problem(prog_name, format!("arglib: there was some unparsed bytes remaining: {:?}", remainder))
+                  },
+                  Err(err) => exec_helpers::die_environment_problem(prog_name, format!("arglib parsing error: {:?}", err))
+              }
+          };
+          std::env::remove_var(env);
+          t
+      }
+    '';
+  };
+
+in
+depot.nix.readTree.drvTargets netencode
diff --git a/users/Profpatsch/atomically-write.nix b/users/Profpatsch/atomically-write.nix
new file mode 100644
index 0000000000..c4d07cfbb1
--- /dev/null
+++ b/users/Profpatsch/atomically-write.nix
@@ -0,0 +1,29 @@
+{ depot, pkgs, ... }:
+# Atomically write a file (just `>` redirection in bash
+# empties a file even if the command crashes).
+#
+# Maybe there is an existing tool for that?
+# But it’s easy enough to implement.
+#
+# Example:
+#   atomically-write
+#     ./to
+#     echo "foo"
+#
+# will atomically write the string "foo" into ./to
+let
+  atomically-write = pkgs.writers.writeDash "atomically-write" ''
+    set -e
+    to=$1
+    shift
+    # assumes that the tempfile is on the same file system, (or in memory)
+    # for the `mv` at the end to be more-or-less atomic.
+    tmp=$(${pkgs.coreutils}/bin/mktemp -d)
+    trap 'rm -r "$tmp"' EXIT
+    "$@" \
+      > "$tmp/out"
+    mv "$tmp/out" "$to"
+  '';
+
+in
+atomically-write
diff --git a/users/Profpatsch/blog/default.nix b/users/Profpatsch/blog/default.nix
new file mode 100644
index 0000000000..9f7b0fdfa2
--- /dev/null
+++ b/users/Profpatsch/blog/default.nix
@@ -0,0 +1,472 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.lowdown [ "lowdown" ]
+    // depot.nix.getBins pkgs.cdb [ "cdbget" "cdbmake" "cdbdump" ]
+    // depot.nix.getBins pkgs.coreutils [ "mv" "cat" "printf" "test" ]
+    // depot.nix.getBins pkgs.s6-networking [ "s6-tcpserver" ]
+    // depot.nix.getBins pkgs.time [ "time" ]
+  ;
+
+  # /
+  # TODO: use
+  toplevel = [
+    {
+      route = [ "notes" ];
+      name = "Notes";
+      page = { cssFile }: router cssFile;
+    }
+    {
+      route = [ "projects" ];
+      name = "Projects";
+      # page = projects;
+    }
+  ];
+
+  # /notes/*
+  notes = [
+    {
+      route = [ "notes" "an-idealized-conflang" ];
+      name = "An Idealized Configuration Language";
+      page = { cssFile }: markdownToHtml {
+        name = "an-idealized-conflang";
+        markdown = ./notes/an-idealized-conflang.md;
+        inherit cssFile;
+      };
+    }
+    {
+      route = [ "notes" "rust-string-conversions" ];
+      name = "Converting between different String types in Rust";
+      page = { cssFile }: markdownToHtml {
+        name = "rust-string-conversions";
+        markdown = ./notes/rust-string-conversions.md;
+        inherit cssFile;
+      };
+    }
+    {
+      route = [ "notes" "preventing-oom" ];
+      name = "Preventing out-of-memory (OOM) errors on Linux";
+      page = { cssFile }: markdownToHtml {
+        name = "preventing-oom";
+        markdown = ./notes/preventing-oom.md;
+        inherit cssFile;
+      };
+    }
+  ];
+
+  projects = [
+    {
+      name = "lorri";
+      description = "<code>nix-shell</code> replacement for projects";
+      link = "https://github.com/nix-community/lorri";
+    }
+    {
+      name = "netencode";
+      description = ''A human-readble nested data exchange format inspired by <a href="https://en.wikipedia.org/wiki/Netstring">netstrings</a> and <a href="https://en.wikipedia.org/wiki/Bencode">bencode</a>.'';
+      link = depotCgitLink { relativePath = "users/Profpatsch/netencode/README.md"; };
+    }
+    {
+      name = "yarn2nix";
+      description = ''nix dependency generator for the <a href="https://yarnpkg.com/"><code>yarn</code> Javascript package manager</a>'';
+      link = "https://github.com/Profpatsch/yarn2nix";
+    }
+  ];
+
+  posts = [
+    {
+      date = "2017-05-04";
+      title = "Ligature Emulation in Emacs";
+      subtitle = "It’s not pretty, but the results are";
+      description = "How to set up ligatures using <code>prettify-symbols-mode</code> and the Hasklig/FiraCode fonts.";
+      page = { cssFile }: markdownToHtml {
+        name = "2017-05-04-ligature-emluation-in-emacs";
+        markdown = ./posts/2017-05-04-ligature-emulation-in-emacs.md;
+        inherit cssFile;
+      };
+      route = [ "posts" "2017-05-04-ligature-emluation-in-emacs" ];
+      tags = [ "emacs" ];
+    }
+  ];
+
+  # convert a markdown file to html via lowdown
+  markdownToHtml =
+    { name
+    , # the file to convert
+      markdown
+    , # css file to add to the final result, as { route }
+      cssFile
+    }:
+    depot.nix.runExecline "${name}.html" { } ([
+      "importas"
+      "out"
+      "out"
+      (depot.users.Profpatsch.lib.debugExec "")
+      bins.lowdown
+      "-s"
+      "-Thtml"
+    ] ++
+    (lib.optional (cssFile != null) ([ "-M" "css=${mkRoute cssFile.route}" ]))
+    ++ [
+      "-o"
+      "$out"
+      markdown
+    ]);
+
+  # takes a { route … } attrset and converts the route lists to an absolute path
+  fullRoute = attrs: lib.pipe attrs [
+    (map (x@{ route, ... }: x // { route = mkRoute route; }))
+  ];
+
+  # a cdb from route to a netencoded version of data for each route
+  router = cssFile: lib.pipe (notes ++ posts) [
+    (map (r: with depot.users.Profpatsch.lens;
+    lib.pipe r [
+      (over (field "route") mkRoute)
+      (over (field "page") (_ { inherit cssFile; }))
+    ]))
+    (map (x: {
+      name = x.route;
+      value = depot.users.Profpatsch.netencode.gen.dwim x;
+    }))
+    lib.listToAttrs
+    (cdbMake "router")
+  ];
+
+  # Create a link to the given source file/directory, given the relative path in the depot repo.
+  # Checks that the file exists at evaluation time.
+  depotCgitLink =
+    {
+      # relative path from the depot root (without leading /).
+      relativePath
+    }:
+      assert
+      (lib.assertMsg
+        (builtins.pathExists (depot.path.origSrc + ("/" + relativePath)))
+        "depotCgitLink: path /${relativePath} does not exist in depot, and depot.path was ${toString depot.path}");
+      "https://code.tvl.fyi/tree/${relativePath}";
+
+  # look up a route by path ($1)
+  router-lookup = cssFile: depot.nix.writeExecline "router-lookup" { readNArgs = 1; } [
+    cdbLookup
+    (router cssFile)
+    "$1"
+  ];
+
+  runExeclineStdout = name: args: cmd: depot.nix.runExecline name args ([
+    "importas"
+    "-ui"
+    "out"
+    "out"
+    "redirfd"
+    "-w"
+    "1"
+    "$out"
+  ] ++ cmd);
+
+  notes-index-html =
+    let o = fullRoute notes;
+    in ''
+      <ul>
+      ${scope o (o: ''
+        <li><a href="${str o.route}">${esc o.name}</a></li>
+      '')}
+      </ul>
+    '';
+
+  notes-index = pkgs.writeText "notes-index.html" notes-index-html;
+
+  # A simple mustache-inspired string interpolation combinator
+  # that takes an object and a template (a function from o to string)
+  # and returns a string.
+  scope = o: tpl:
+    if builtins.typeOf o == "list" then
+      lib.concatMapStringsSep "\n" tpl o
+    else if builtins.typeOf o == "set" then
+      tpl o
+    else throw "${lib.generators.toPretty {} o} not allowed in template";
+
+  # string-escape html (TODO)
+  str = s: s;
+  # html-escape (TODO)
+  esc = s: s;
+  html = s: s;
+
+  projects-index-html =
+    let o = projects;
+    in ''
+      <dl>
+      ${scope o (o: ''
+        <dt><a href="${str o.link}">${esc o.name}</a></dt>
+        <dd>${html o.description}</dd>
+      '')}
+      </dl>
+    '';
+
+  projects-index = pkgs.writeText "projects-index.html" projects-index-html;
+
+  posts-index-html =
+    let o = fullRoute posts;
+    in ''
+      <dl>
+      ${scope o (o: ''
+        <dt>${str o.date} <a href="${str o.route}">${esc o.title}</a></dt>
+        <dd>${html o.description}</dd>
+      '')}
+      </dl>
+    '';
+
+  posts-index = pkgs.writeText "projects-index.html" posts-index-html;
+
+  arglibNetencode = val: depot.nix.writeExecline "arglib-netencode" { } [
+    "export"
+    "ARGLIB_NETENCODE"
+    (depot.users.Profpatsch.netencode.gen.dwim val)
+    "$@"
+  ];
+
+  # A simple http server that serves the site. Yes, it’s horrible.
+  site-server = { cssFile, port }: depot.nix.writeExecline "blog-server" { } [
+    (depot.users.Profpatsch.lib.runInEmptyEnv [ "PATH" ])
+    bins.s6-tcpserver
+    "127.0.0.1"
+    port
+    bins.time
+    "--format=time: %es"
+    "--"
+    runOr
+    return400
+    "pipeline"
+    [
+      (arglibNetencode {
+        what = "request";
+      })
+      depot.users.Profpatsch.read-http
+    ]
+    depot.users.Profpatsch.netencode.record-splice-env
+    runOr
+    return500
+    "importas"
+    "-i"
+    "path"
+    "path"
+    "if"
+    [ depot.tools.eprintf "GET \${path}\n" ]
+    runOr
+    return404
+    "backtick"
+    "-ni"
+    "TEMPLATE_DATA"
+    [
+      # TODO: factor this out of here, this is routing not serving
+      "ifelse"
+      [ bins.test "$path" "=" "/notes" ]
+      [
+        "export"
+        "content-type"
+        "text/html"
+        "export"
+        "serve-file"
+        notes-index
+        depot.users.Profpatsch.netencode.env-splice-record
+      ]
+      "ifelse"
+      [ bins.test "$path" "=" "/projects" ]
+      [
+        "export"
+        "content-type"
+        "text/html"
+        "export"
+        "serve-file"
+        projects-index
+        depot.users.Profpatsch.netencode.env-splice-record
+      ]
+      "ifelse"
+      [ bins.test "$path" "=" "/posts" ]
+      [
+        "export"
+        "content-type"
+        "text/html"
+        "export"
+        "serve-file"
+        posts-index
+        depot.users.Profpatsch.netencode.env-splice-record
+      ]
+      # TODO: ignore potential query arguments. See 404 message
+      "pipeline"
+      [ (router-lookup cssFile) "$path" ]
+      depot.users.Profpatsch.netencode.record-splice-env
+      "importas"
+      "-ui"
+      "page"
+      "page"
+      "export"
+      "content-type"
+      "text/html"
+      "export"
+      "serve-file"
+      "$page"
+      depot.users.Profpatsch.netencode.env-splice-record
+    ]
+    runOr
+    return500
+    "if"
+    [
+      "pipeline"
+      [
+        bins.printf
+        ''
+          HTTP/1.1 200 OK
+          Content-Type: {{{content-type}}}; charset=UTF-8
+          Connection: close
+
+        ''
+      ]
+      depot.users.Profpatsch.netencode.netencode-mustache
+    ]
+    "pipeline"
+    [ "importas" "t" "TEMPLATE_DATA" bins.printf "%s" "$t" ]
+    depot.users.Profpatsch.netencode.record-splice-env
+    "importas"
+    "-ui"
+    "serve-file"
+    "serve-file"
+    bins.cat
+    "$serve-file"
+  ];
+
+  # run argv or $1 if argv returns a failure status code.
+  runOr = depot.nix.writeExecline "run-or" { readNArgs = 1; } [
+    "foreground"
+    [ "$@" ]
+    "importas"
+    "?"
+    "?"
+    "ifelse"
+    [ bins.test "$?" "-eq" "0" ]
+    [ ]
+    "if"
+    [ depot.tools.eprintf "runOr: exited \${?}, running \${1}\n" ]
+    "$1"
+  ];
+
+  return400 = depot.nix.writeExecline "return400" { } [
+    bins.printf
+    "%s"
+    ''
+      HTTP/1.1 400 Bad Request
+      Content-Type: text/plain; charset=UTF-8
+      Connection: close
+
+    ''
+  ];
+
+  return404 = depot.nix.writeExecline "return404" { } [
+    bins.printf
+    "%s"
+    ''
+      HTTP/1.1 404 Not Found
+      Content-Type: text/plain; charset=UTF-8
+      Connection: close
+
+      This page doesn’t exist! Query arguments are not handled at the moment.
+    ''
+  ];
+
+  return500 = depot.nix.writeExecline "return500" { } [
+    bins.printf
+    "%s"
+    ''
+      HTTP/1.1 500 Internal Server Error
+      Content-Type: text/plain; charset=UTF-8
+      Connection: close
+
+      Encountered an internal server error. Please try again.
+    ''
+  ];
+
+  capture-stdin = depot.nix.writers.rustSimple
+    {
+      name = "capture-stdin";
+      dependencies = [ depot.users.Profpatsch.execline.exec-helpers ];
+    } ''
+    extern crate exec_helpers;
+    use std::io::Read;
+    fn main() {
+      let (args, prog) = exec_helpers::args_for_exec("capture-stdin", 1);
+      let valname = &args[1];
+      let mut v : Vec<u8> = vec![];
+      std::io::stdin().lock().read_to_end(&mut v).unwrap();
+      exec_helpers::exec_into_args("capture-stdin", prog, vec![(valname, v)]);
+    }
+  '';
+
+  # go from a list of path elements to an absolute route string
+  mkRoute = route: "/" + lib.concatMapStringsSep "/" urlencodeAscii route;
+
+  # urlencodes, but only ASCII characters
+  # https://en.wikipedia.org/wiki/Percent-encoding
+  urlencodeAscii = urlPiece:
+    let
+      raw = [ "!" "#" "$" "%" "&" "'" "(" ")" "*" "+" "," "/" ":" ";" "=" "?" "@" "[" "]" ];
+      enc = [ "%21" "%23" "%24" "%25" "%26" "%27" "%28" "%29" "%2A" "%2B" "%2C" "%2F" "%3A" "%3B" "%3D" "%3F" "%40" "%5B" "%5D" ];
+      rest = [ "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "-" "_" "." "~" ];
+    in
+    assert lib.assertMsg (lib.all (c: builtins.elem c (raw ++ rest)) (lib.stringToCharacters urlPiece))
+      "urlencodeAscii: the urlPiece must only contain valid url ASCII characters, was: ${urlPiece}";
+    builtins.replaceStrings raw enc urlPiece;
+
+
+  # create a cdb record entry, as required by the cdbmake tool
+  cdbRecord = key: val:
+    "+${toString (builtins.stringLength key)},${toString (builtins.stringLength val)}:"
+    + "${key}->${val}\n";
+
+  # create a full cdbmake input from an attribute set of keys to values (strings)
+  cdbRecords =
+    with depot.nix.yants;
+    defun [ (attrs (either drv string)) string ]
+      (attrs:
+        (lib.concatStrings (lib.mapAttrsToList cdbRecord attrs)) + "\n");
+
+  # run cdbmake on a list of key/value pairs (strings
+  cdbMake = name: attrs: depot.nix.runExecline "${name}.cdb"
+    {
+      stdin = cdbRecords attrs;
+    } [
+    "importas"
+    "out"
+    "out"
+    depot.users.Profpatsch.lib.eprint-stdin
+    "if"
+    [ bins.cdbmake "db" "tmp" ]
+    bins.mv
+    "db"
+    "$out"
+  ];
+
+  # look up a key ($2) in the given cdb ($1)
+  cdbLookup = depot.nix.writeExecline "cdb-lookup" { readNArgs = 2; } [
+    # cdb ($1) on stdin
+    "redirfd"
+    "-r"
+    "0"
+    "$1"
+    # key ($2) lookup
+    bins.cdbget
+    "$2"
+  ];
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    router
+    depotCgitLink
+    site-server
+    notes-index
+    notes-index-html
+    projects-index
+    projects-index-html
+    posts-index-html
+    ;
+
+}
diff --git a/users/Profpatsch/blog/notes/an-idealized-conflang.md b/users/Profpatsch/blog/notes/an-idealized-conflang.md
new file mode 100644
index 0000000000..5c6b39f6e8
--- /dev/null
+++ b/users/Profpatsch/blog/notes/an-idealized-conflang.md
@@ -0,0 +1,298 @@
+tags: netencode, json
+date: 2022-03-31
+certainty: likely
+status: initial
+title: An idealized Configuration Language
+
+# An Idealized Configuration Language
+
+JSON brought us one step closer to what an idealized configuration language is,
+which I define as “data, stripped of all externalities of the system it is working in”.
+
+Specifically, JSON is very close to what I consider the minimal properties to represent structured data.
+
+## A short history, according to me
+
+In the beginning, Lisp defined s-expressions as a stand-in for an actual syntax.
+Then, people figured out that it’s also a way to represent structured data.
+It has scalars, which can be nested into lists, recursively.
+
+```
+(this is (a (list) (of lists)))
+```
+
+This provides the first three rules of our idealized language:
+
+1. A **scalar** is a primitive value that is domain-specific.
+   We can assume a bunch of bytes here, or a text or an integer.
+   
+2. A **list** gives an ordering to `0..n` (or `1..n`) values
+   
+3. Both a scalar and a list are the *same kind* of “thing” (from here on called **value**),
+   lists can be created from arbitrary values *recursively*
+   (for example scalars, or lists of scalars and other lists)
+
+
+Later, ASN.1 came and had the important insight that the same idealized data structure
+can be represented in different fashions,
+for example as a binary-efficient version and a human-readable format.
+
+Then, XML “graced” the world for a decade or two, and the main lesson from it was
+that you don’t want to mix markup languages and configuration languages,
+and that you don’t want a committee to design these things.
+
+---
+
+In the meantime, Brendan Eich designed Javascript. Its prototype-based object system
+arguably stripped down the rituals of existing OO-systems.
+Douglas Crockford later extracted the object format (minus functions) into a syntax, and we got JSON.
+
+```
+{
+  "foo": [
+    { "nested": "attrs" },
+    "some text"
+  ],
+  "bar": 42
+}
+```
+
+JSON adds another fundamental idea into the mix:
+
+4. **Records** are unordered collections of `name`/`value` pairs.
+   A `name` is defined to be a unicode string, so a semantic descriptor of the nested `value`.
+
+Unfortunately, the JSON syntax does not actually specify any semantics of records (`objects` in JSON lingo),
+in particular it does not mention what the meaning is if a `name` appears twice in one record.
+
+If records can have multiple entries with the same `name`, suddenly ordering becomes important!
+But wait, remember earlier we defined *lists* to impose ordering on two values.
+So in order to rectify that problem, we say that
+
+5. A `name` can only appear in a record *once*, names must be unique.
+
+This is the current state of the programming community at large,
+where most “modern” configuration languages basically use a version of the JSON model
+as their underlying data structure. (However not all of them use the same version.)
+
+## Improving JSON’s data model
+
+We are not yet at the final “idealized” configuration language, though.
+
+Modern languages like Standard ML define their data types as a mixture of 
+
+* *records* (“structs” in the C lingo)
+* and *sums* (which you can think about as enums that can hold more `value`s inside them)
+
+This allows to express the common pattern where some fields in a record are only meaningful
+if another field—the so-called `tag`-field—is set to a specific value.
+
+An easy example: if a request can fail with an error message or succeed with a result.
+
+You could model that as 
+
+```
+{
+  "was_error": true,
+  "error_message": "there was an error"
+}
+```
+
+or
+
+```
+{
+  "was_error": false,
+  "result": 42
+}
+```
+
+in your JSON representation.
+
+But in a ML-like language (like, for example, Rust), you would instead model it as
+
+```
+type RequestResult 
+  = Error { error_message: String }
+  | Success { result: i64 }
+```
+
+where the distinction in `Error` or `Success` makes it clear that `error_message` and `result`
+only exist in one of these cases, not the other.
+
+We *can* encode exactly that idea into JSON in multiple ways, but not a “blessed” way.
+
+For example, another way to encode the above would be
+
+```
+{ 
+  "Error": { 
+    "error_message": "there was an error"
+  }
+}
+```
+
+and
+
+```
+{ 
+  "Success": { 
+    "result": 42
+  }
+}
+```
+
+Particularly notice the difference between the language representation, where the type is “closed”only `Success` or `Error` can happen—
+and the data representation where the type is “open”, more cases could potentially exist.
+
+This is an important differentiation from a type system:
+Our idealized configuration language just gives more structure to a bag of data,
+it does not restrict which value can be where.
+Think of a value in an unityped language, like Python.
+
+
+So far we have the notion of 
+
+1. a scalar (a primitive)
+2. a list (ordering on values)
+3. a record (unordered collection of named values)
+
+and in order to get the “open” `tag`ged enumeration values, we introduce
+
+4. a `tag`, which gives a name to a value
+
+We can then redefine `record` to mean “an unordered collection of `tag`ged values”,
+which further reduces the amount of concepts needed.
+
+And that’s it, this is the full idealized configuration language.
+
+
+## Some examples of data modelling with tags
+
+This is all well and good, but what does it look like in practice?
+
+For these examples I will be using JSON with a new `< "tag": value >` syntax
+to represent `tag`s.
+
+From a compatibility standpoint, `tag`s (or sum types) have dual properties to record types.
+
+With a record, when you have a producer that *adds* a field to it, the consumer will still be able to handle the record (provided the semantics of the existing fields is not changed by the new field).
+
+With a tag, *removing* a tag from the producer will mean that the consumer will still be able to handle the tag. It might do one “dead” check on the removed `tag`, but can still handle the remaining ones just fine.
+
+<!-- TODO: some illustration here -->
+    
+An example of how that is applied in practice is that in `protobuf3`, fields of a record are *always* optional fields.
+
+We can model optional fields by wrapping them in `< "Some": value >` or `< "None": {} >` (where the actual value of the `None` is ignored or always an empty record).
+
+So a protobuf with the fields `foo: int` and `bar: string` has to be parsed by the receiver als containing *four* possibilities:
+
+№|foo|bar|
+|--:|---|---|
+|1|`<"None":{}>`|`<"None":{}>`|
+|2|`<"Some":42>`|`<"None":{}>`|
+|3|`<"None":{}>`|`<"Some":"x">`|
+|4|`<"Some":42>`|`<"Some":"x">`|
+
+Now, iff the receiver actually handles all four possibilities
+(and doesn’t just crash if a field is not set, as customary in million-dollar-mistake languages),
+it’s easy to see how removing a field from the producer is semantically equal to always setting it to `<"None":{}>`.
+Since all receivers should be ready to receive `None` for every field, this provides a simple forward-compatibility scheme.
+
+We can abstract this to any kind of tag value:
+If you start with “more” tags, you give yourself space to remove them later without breaking compatibility, typically called “forward compatibility”.
+
+
+## To empty list/record or not to
+
+Something to think about is whether records and fields should be defined
+to always contain at least one element.
+
+As it stands, JSON has multiple ways of expressing the “empty value”:
+
+* `null`
+* `[]`
+* `{}`
+* `""`
+* *leave out the field*
+
+and two of those come from the possibility of having empty structured values.
+
+## Representations of this language
+
+This line of thought originally fell out of me designing [`netencode`](https://code.tvl.fyi/tree/users/Profpatsch/netencode/README.md)
+as a small human-debuggable format for pipeline serialization.
+
+In addition to the concepts mentioned here (especially tags),
+it provides a better set of scalars than JSON (specifically arbitrary bytestrings),
+but it cannot practically be written or modified by hand,
+which might be a good thing depending on how you look at it.
+
+---
+
+The way that is compatible with the rest of the ecosystem is probably to use a subset of json
+to represent our idealized language.
+
+There is multiple ways of encoding tags in json, which each have their pros and cons.
+
+The most common is probably the “tag field” variant, where the tag is pulled into the nested record:
+
+```
+{
+  "_tag": "Success",
+  "result": 42
+}
+```
+
+Which has the advantage that people know how to deal with it and that it’s easy to “just add another field”,
+plus it is backward-compatible when you had a record in the first place.
+
+It has multiple disadvantages however:
+
+* If your value wasn’t a record (e.g. an int) before, you have to put it in a record and assign an arbitrary name to its field
+* People are not forced to “unwrap” the tag first, so they are going to forget to check it
+* The magic “_tag” name cannot be used by any of the record’s fields
+
+
+An in-between version of this with less downsides is to always push a json record onto the stack:
+
+```
+{
+  "tag": "Success",
+  "value": {
+    "result": 42
+  }
+}
+```
+
+This makes it harder for people to miss checking the `tag`, but still possible of course.
+It also makes it easily possible to inspect the contents of `value` without knowing the
+exhaustive list of `tag`s, which can be useful in practice (though often not sound!).
+It also gets rid of the “_tag” field name clash problem.
+
+Disadvantages:
+
+* Breaks the backwards-compatibility with an existing record-based approach if you want to introduce `tag`s
+* Verbosity of representation
+* hard to distinguish a record with the `tag` and `value` fields from a `tag`ed value (though you know the type layout of your data on a higher level, don’t you? ;) )
+
+
+The final, “most pure” representation is the one I gave in the original introduction:
+
+```
+{
+  "Success": {
+    "result": 42
+  }
+}
+```
+
+Now you *have* to match on the `tag` name first, before you can actually access your data,
+and it’s less verbose than the above representation.
+
+Disavantages:
+
+* You also have to *know* what `tag`s to expect, it’s harder to query cause you need to extract the keys and values from the dict and then take the first one.
+* Doing a “tag backwards compat” check is harder,
+  because you can’t just check whether `_tag` or `tag`/`value` are the keys in the dict.
diff --git a/users/Profpatsch/blog/notes/preventing-oom.md b/users/Profpatsch/blog/notes/preventing-oom.md
new file mode 100644
index 0000000000..59ea4f7477
--- /dev/null
+++ b/users/Profpatsch/blog/notes/preventing-oom.md
@@ -0,0 +1,33 @@
+tags: linux
+date: 2020-01-25
+certainty: likely
+status: initial
+title: Preventing out-of-memory (OOM) errors on Linux
+
+# Preventing out-of-memory (OOM) errors on Linux
+
+I’ve been running out of memory more and more often lately. I don’t use any swap space because I am of the opinion that 16GB of memory should be sufficient for most daily and professional tasks. Which is generally true, however sometimes I have a runaway filling my memory. Emacs is very good at doing this for example, prone to filling your RAM when you open json files with very long lines.
+
+In theory, the kernel OOM killer should come in and save the day, but the Linux OOM killer is notorious for being extremely … conservative. It will try to free every internal structure it can before even thinking about touching any userspace processes. At that point, the desktop usually stopped responding minutes ago.
+
+Luckily the kernel provides memory statistics for the whole system, as well as single process, and the [`earlyoom`](https://github.com/rfjakob/earlyoom) tool uses those to keep memory usage under a certain limit. It will start killing processes, “heaviest” first, until the given upper memory limit is satisfied again.
+
+On NixOS, I set:
+
+```nix
+{
+  services.earlyoom = {
+    enable = true;
+    freeMemThreshold = 5; # <%5 free
+  };
+}
+```
+
+and after activation, this simple test shows whether the daemon is working:
+
+```shell
+$ tail /dev/zero
+fish: “tail /dev/zero” terminated by signal SIGTERM (Polite quit request)
+```
+
+`tail /dev/zero` searches for the last line of the file `/dev/zero`, and since it cannot know that there is no next line and no end to the stream of `\0` this file produces, it will fill the RAM as quickly as physically possible. Before it can fill it completely, `earlyoom` recognizes that the limit was breached, singles out the `tail` command as the process using the most amount of memory, and sends it a `SIGTERM`.
diff --git a/users/Profpatsch/blog/notes/rust-string-conversions.md b/users/Profpatsch/blog/notes/rust-string-conversions.md
new file mode 100644
index 0000000000..99071ef9d3
--- /dev/null
+++ b/users/Profpatsch/blog/notes/rust-string-conversions.md
@@ -0,0 +1,53 @@
+# Converting between different String types in Rust
+
+```
+let s: String = ...
+let st: &str = ...
+let u: &[u8] = ...
+let b: [u8; 3] = b"foo"
+let v: Vec<u8> = ...
+let os: OsString = ...
+let ost: OsStr = ...
+
+From       To         Use                                    Comment
+----       --         ---                                    -------
+&str     -> String    String::from(st)
+&str     -> &[u8]     st.as_bytes()
+&str     -> Vec<u8>   st.as_bytes().to_owned()               via &[u8]
+&str     -> &OsStr    OsStr::new(st)
+
+String   -> &str      &s                                     alt. s.as_str()
+String   -> &[u8]     s.as_bytes()
+String   -> Vec<u8>   s.into_bytes()
+String   -> OsString  OsString::from(s)
+
+&[u8]    -> &str      str::from_utf8(u).unwrap()
+&[u8]    -> String    String::from_utf8(u).unwrap()
+&[u8]    -> Vec<u8>   u.to_owned()
+&[u8]    -> &OsStr    OsStr::from_bytes(u)                   use std::os::unix::ffi::OsStrExt;
+
+[u8; 3]  -> &[u8]     &b[..]                                 byte literal
+[u8; 3]  -> &[u8]     "foo".as_bytes()                       alternative via utf8 literal
+
+Vec<u8>  -> &str      str::from_utf8(&v).unwrap()            via &[u8]
+Vec<u8>  -> String    String::from_utf8(v)
+Vec<u8>  -> &[u8]     &v
+Vec<u8>  -> OsString  OsString::from_vec(v)                  use std::os::unix::ffi::OsStringExt;
+
+&OsStr   -> &str      ost.to_str().unwrap()
+&OsStr   -> String    ost.to_os_string().into_string()       via OsString
+                         .unwrap()
+&OsStr   -> Cow<str>  ost.to_string_lossy()                  Unicode replacement characters
+&OsStr   -> OsString  ost.to_os_string()
+&OsStr   -> &[u8]     ost.as_bytes()                         use std::os::unix::ffi::OsStringExt;
+
+OsString -> String    os.into_string().unwrap()              returns original OsString on failure
+OsString -> &str      os.to_str().unwrap()
+OsString -> &OsStr    os.as_os_str()
+OsString -> Vec<u8>   os.into_vec()                          use std::os::unix::ffi::OsStringExt;
+```
+
+
+## Source
+
+Original source is [this document on Pastebin](https://web.archive.org/web/20190710121935/https://pastebin.com/Mhfc6b9i)
diff --git a/users/Profpatsch/blog/posts/2017-05-04-ligature-emulation-in-emacs.md b/users/Profpatsch/blog/posts/2017-05-04-ligature-emulation-in-emacs.md
new file mode 100644
index 0000000000..ba80888bad
--- /dev/null
+++ b/users/Profpatsch/blog/posts/2017-05-04-ligature-emulation-in-emacs.md
@@ -0,0 +1,123 @@
+title: Ligature Emulation in Emacs
+date: 2017-05-04
+
+Monday was (yet another)
+[NixOS hackathon][hackathon] at [OpenLab Augsburg][ola].
+[Maximilian][mhuber] was there and to my amazement
+he got working ligatures in his Haskell files in Emacs! Ever since Hasklig
+updated its format to use ligatures and private Unicode code points a while ago,
+the hack I had used in my config stopped working.
+
+Encouraged by that I decided to take a look on Tuesday. Long story short, I was
+able to [get it working in a pretty satisfying way][done].
+
+[hackathon]: https://www.meetup.com/Munich-NixOS-Meetup/events/239077247/
+[mhuber]: https://github.com/maximilianhuber
+[ola]: https://openlab-augsburg.de
+[done]: https://github.com/i-tu/Hasklig/issues/84#issuecomment-298803495
+
+What’s left to do is package it into a module and push to melpa.
+
+
+### elisp still sucks, but it’s bearable, sometimes
+
+I’m the kind of person who, when trying to fix something elisp related, normally
+gives up two hours later and three macro calls deep. Yes, homoiconic,
+non-lexically-scoped, self-rewriting code is not exactly my fetish.
+This time the task and the library (`prettify-symbols-mode`) were simple enough
+for that to not happen.
+
+Some interesting technical trivia:
+
+- elisp literal character syntax is `?c`. `?\t` is the tab character
+- You join characters by `(string c1 c2 c3 ...)`
+- [dash.el][dash] is pretty awesome and does what a functional programmer
+  expects. Also, Rainbow Dash.
+- Hasklig and FiraCode multi-column symbols actually [only occupy one column, on
+  the far right of the glyph][glyph]. `my-correct-symbol-bounds` fixes emacs’
+  rendering in that case.
+
+
+[dash]: https://github.com/magnars/dash.el
+[glyph]: https://github.com/tonsky/FiraCode/issues/211#issuecomment-239082368
+
+
+## Appendix A
+
+For reference, here’s the complete code as it stands now. Feel free to paste
+into your config; let’s make it [MIT][mit]. Maybe link to this site, in case there are
+updates.
+
+[mit]: https://opensource.org/licenses/MIT
+
+```elisp
+ (defun my-correct-symbol-bounds (pretty-alist)
+    "Prepend a TAB character to each symbol in this alist,
+this way compose-region called by prettify-symbols-mode
+will use the correct width of the symbols
+instead of the width measured by char-width."
+    (mapcar (lambda (el)
+              (setcdr el (string ?\t (cdr el)))
+              el)
+            pretty-alist))
+
+  (defun my-ligature-list (ligatures codepoint-start)
+    "Create an alist of strings to replace with
+codepoints starting from codepoint-start."
+    (let ((codepoints (-iterate '1+ codepoint-start (length ligatures))))
+      (-zip-pair ligatures codepoints)))
+
+  ; list can be found at https://github.com/i-tu/Hasklig/blob/master/GlyphOrderAndAliasDB#L1588
+  (setq my-hasklig-ligatures
+    (let* ((ligs '("&&" "***" "*>" "\\\\" "||" "|>" "::"
+                   "==" "===" "==>" "=>" "=<<" "!!" ">>"
+                   ">>=" ">>>" ">>-" ">-" "->" "-<" "-<<"
+                   "<*" "<*>" "<|" "<|>" "<$>" "<>" "<-"
+                   "<<" "<<<" "<+>" ".." "..." "++" "+++"
+                   "/=" ":::" ">=>" "->>" "<=>" "<=<" "<->")))
+      (my-correct-symbol-bounds (my-ligature-list ligs #Xe100))))
+
+  ;; nice glyphs for haskell with hasklig
+  (defun my-set-hasklig-ligatures ()
+    "Add hasklig ligatures for use with prettify-symbols-mode."
+    (setq prettify-symbols-alist
+          (append my-hasklig-ligatures prettify-symbols-alist))
+    (prettify-symbols-mode))
+
+  (add-hook 'haskell-mode-hook 'my-set-hasklig-ligatures)
+```
+
+## Appendix B (Update 1): FiraCode integration
+
+I also created a mapping for [FiraCode][fira]. You need to grab the [additional
+symbol font][symbol] that adds (most) ligatures to the unicode private use area.
+Consult your system documentation on how to add it to your font cache.
+Next add `"Fira Code"` and `"Fira Code Symbol"` to your font preferences. Symbol
+only contains the additional characters, so you need both.
+
+If you are on NixOS, the font package should be on the main branch shortly, [I
+added a package][symbol-pkg].
+
+[fira]: https://github.com/tonsky/FiraCode/
+[symbol]: https://github.com/tonsky/FiraCode/issues/211#issuecomment-239058632
+[symbol-pkg]: https://github.com/NixOS/nixpkgs/pull/25517
+
+Here’s the mapping adjusted for FiraCode:
+
+```elisp
+  (setq my-fira-code-ligatures
+    (let* ((ligs '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\"
+                  "{-" "[]" "::" ":::" ":=" "!!" "!=" "!==" "-}"
+                  "--" "---" "-->" "->" "->>" "-<" "-<<" "-~"
+                  "#{" "#[" "##" "###" "####" "#(" "#?" "#_" "#_("
+                  ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*"
+                  "/**" "/=" "/==" "/>" "//" "///" "&&" "||" "||="
+                  "|=" "|>" "^=" "$>" "++" "+++" "+>" "=:=" "=="
+                  "===" "==>" "=>" "=>>" "<=" "=<<" "=/=" ">-" ">="
+                  ">=>" ">>" ">>-" ">>=" ">>>" "<*" "<*>" "<|" "<|>"
+                  "<$" "<$>" "<!--" "<-" "<--" "<->" "<+" "<+>" "<="
+                  "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<" "<~"
+                  "<~~" "</" "</>" "~@" "~-" "~=" "~>" "~~" "~~>" "%%"
+                  "x" ":" "+" "+" "*")))
+      (my-correct-symbol-bounds (my-ligature-list ligs #Xe100))))
+```
diff --git a/users/Profpatsch/cdb.nix b/users/Profpatsch/cdb.nix
new file mode 100644
index 0000000000..86e0a2d58f
--- /dev/null
+++ b/users/Profpatsch/cdb.nix
@@ -0,0 +1,93 @@
+{ depot, pkgs, ... }:
+
+let
+  cdbListToNetencode = depot.nix.writers.rustSimple
+    {
+      name = "cdb-list-to-netencode";
+      dependencies = [
+        depot.third_party.rust-crates.nom
+        depot.users.Profpatsch.execline.exec-helpers
+        depot.users.Profpatsch.netencode.netencode-rs
+      ];
+    } ''
+    extern crate nom;
+    extern crate exec_helpers;
+    extern crate netencode;
+    use std::collections::HashMap;
+    use std::io::BufRead;
+    use nom::{IResult};
+    use nom::sequence::{tuple};
+    use nom::bytes::complete::{tag, take};
+    use nom::character::complete::{digit1, char};
+    use nom::error::{context, ErrorKind, ParseError};
+    use nom::combinator::{map_res};
+    use netencode::{T, Tag};
+
+    fn usize_t(s: &[u8]) -> IResult<&[u8], usize> {
+        context(
+            "usize",
+            map_res(
+                map_res(digit1, |n| std::str::from_utf8(n)),
+                |s| s.parse::<usize>())
+        )(s)
+    }
+
+    fn parse_cdb_record(s: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
+        let (s, (_, klen, _, vlen, _)) = tuple((
+            char('+'),
+            usize_t,
+            char(','),
+            usize_t,
+            char(':')
+        ))(s)?;
+        let (s, (key, _, val)) = tuple((
+            take(klen),
+            tag("->"),
+            take(vlen),
+        ))(s)?;
+        Ok((s, (key, val)))
+    }
+
+    fn main() {
+        let mut res = vec![];
+        let stdin = std::io::stdin();
+        let mut lines = stdin.lock().split(b'\n');
+        loop {
+            match lines.next() {
+                None => exec_helpers::die_user_error("cdb-list-to-netencode", "stdin ended but we didn’t receive the empty line to signify the end of the cdbdump input!"),
+                Some(Err(err)) => exec_helpers::die_temporary("cdb-list-to-netencode", format!("could not read from stdin: {}", err)),
+                Some(Ok(line)) =>
+                    if &line == b"" {
+                        // the cdbdump input ends after an empty line (double \n)
+                        break;
+                    } else {
+                        match parse_cdb_record(&line) {
+                            Ok((b"", (key, val))) => {
+                                let (key, val) = match
+                                    std::str::from_utf8(key)
+                                    .and_then(|k| std::str::from_utf8(val).map(|v| (k, v))) {
+                                    Ok((key, val)) => (key.to_owned(), val.to_owned()),
+                                    Err(err) => exec_helpers::die_user_error("cdb-list-to-netencode", format!("cannot decode line {:?}, we only support utf8-encoded key/values pairs for now: {}", String::from_utf8_lossy(&line), err)),
+                                };
+                                let _ = res.push((key, val));
+                            },
+                            Ok((rest, _)) => exec_helpers::die_user_error("cdb-list-to-netencode", format!("could not decode record line {:?}, had some trailing bytes", String::from_utf8_lossy(&line))),
+                            Err(err) => exec_helpers::die_user_error("cdb-list-to-netencode", format!("could not decode record line {:?}: {:?}", String::from_utf8_lossy(&line), err)),
+                        }
+                    }
+            }
+        }
+        let list = T::List(res.into_iter().map(
+            |(k, v)| T::Record(vec![(String::from("key"), T::Text(k)), (String::from("val"), T::Text(v))].into_iter().collect())
+        ).collect());
+        netencode::encode(&mut std::io::stdout(), &list.to_u());
+    }
+
+  '';
+
+in
+{
+  inherit
+    cdbListToNetencode
+    ;
+}
diff --git a/users/Profpatsch/emacs-tree-sitter-move/default.nix b/users/Profpatsch/emacs-tree-sitter-move/default.nix
new file mode 100644
index 0000000000..a9f259d96d
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/default.nix
@@ -0,0 +1,3 @@
+# nothing yet (TODO: expose shell & tool)
+{ ... }:
+{ }
diff --git a/users/Profpatsch/emacs-tree-sitter-move/shell.nix b/users/Profpatsch/emacs-tree-sitter-move/shell.nix
new file mode 100644
index 0000000000..f400d5c021
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/shell.nix
@@ -0,0 +1,17 @@
+{ pkgs ? import ../../../third_party { }, ... }:
+let
+  inherit (pkgs) lib;
+
+  treeSitterGrammars = pkgs.runCommandLocal "grammars" { } ''
+    mkdir -p $out/bin
+    ${lib.concatStringsSep "\n"
+      (lib.mapAttrsToList (name: src: "ln -s ${src}/parser $out/bin/${name}.so") pkgs.tree-sitter.builtGrammars)};
+  '';
+
+in
+pkgs.mkShell {
+  buildInputs = [
+    pkgs.tree-sitter.builtGrammars.python
+  ];
+  TREE_SITTER_GRAMMAR_DIR = treeSitterGrammars;
+}
diff --git a/users/Profpatsch/emacs-tree-sitter-move/test.json b/users/Profpatsch/emacs-tree-sitter-move/test.json
new file mode 100644
index 0000000000..d9f8075976
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/test.json
@@ -0,0 +1,14 @@
+{
+    "foo": {
+        "x": [ 1, 2, 3, 4 ],
+        "bar": "test"
+    },
+    "foo": {
+        "x": [ 1, 2, 3, 4 ],
+        "bar": "test"
+    },
+    "foo": {
+        "x": [ 1, 2, 3, 4 ],
+        "bar": "test"
+    }
+}
diff --git a/users/Profpatsch/emacs-tree-sitter-move/test.py b/users/Profpatsch/emacs-tree-sitter-move/test.py
new file mode 100644
index 0000000000..0f57bae035
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/test.py
@@ -0,0 +1,13 @@
+(4 + 5 + 5)
+
+def foo(a, b, c)
+
+def bar(a, b):
+    4
+    4
+    4
+
+[1, 4, 5, 10]
+
+def foo():
+    pass
diff --git a/users/Profpatsch/emacs-tree-sitter-move/test.sh b/users/Profpatsch/emacs-tree-sitter-move/test.sh
new file mode 100644
index 0000000000..681081f590
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/test.sh
@@ -0,0 +1,14 @@
+function foo () {
+    local x=123
+}
+
+function bar () {
+    local x=123
+}
+
+echo abc def \
+     gef gef
+
+printf \
+    "%s\n" \
+    haha
diff --git a/users/Profpatsch/emacs-tree-sitter-move/tmp.el b/users/Profpatsch/emacs-tree-sitter-move/tmp.el
new file mode 100644
index 0000000000..88d13fa45b
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/tmp.el
@@ -0,0 +1,28 @@
+(defun tree-sitter-load-from-grammar-dir (grammar-dir sym lang-name)
+  (tree-sitter-load
+   sym
+   (format "%s/bin/%s"
+           (getenv grammar-dir)
+           lang-name)))
+
+(defun tree-sitter-init-tmp-langs (alist)
+  (mapcar
+   (lambda (lang)
+     (pcase-let ((`(,name ,sym ,mode) lang))
+       (tree-sitter-load-from-grammar-dir "TREE_SITTER_GRAMMAR_DIR" sym name)
+       (cons mode sym)))
+   alist))
+
+
+(setq tree-sitter-major-mode-language-alist
+      (tree-sitter-init-tmp-langs
+       '(("python" python python-mode)
+         ("json" json js-mode)
+         ("bash" bash sh-mode)
+         )))
+
+(define-key evil-normal-state-map (kbd "C-.") #'tree-sitter-move-reset)
+(define-key evil-normal-state-map (kbd "C-<right>") #'tree-sitter-move-right)
+(define-key evil-normal-state-map (kbd "C-<left>") #'tree-sitter-move-left)
+(define-key evil-normal-state-map (kbd "C-<up>") #'tree-sitter-move-up)
+(define-key evil-normal-state-map (kbd "C-<down>") #'tree-sitter-move-down)
diff --git a/users/Profpatsch/emacs-tree-sitter-move/tree-sitter-move.el b/users/Profpatsch/emacs-tree-sitter-move/tree-sitter-move.el
new file mode 100644
index 0000000000..907e1e4081
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/tree-sitter-move.el
@@ -0,0 +1,139 @@
+;; this is not an actual cursor, just a node.
+;; It’s not super efficient, but cursors can’t be *set* to an arbitrary
+;; subnode, because they can’t access the parent otherwise.
+;; We’d need a way to reset the cursor and walk down to the node?!
+(defvar-local tree-sitter-move--cursor nil
+  "the buffer-local cursor used for movement")
+
+(defvar-local tree-sitter-move--debug-overlay nil
+  "an overlay used to visually display the region currently marked by the cursor")
+
+;;;;; TODO: should everything use named nodes? Only some things?
+;;;;; maybe there should be a pair of functions for everything?
+;;;;; For now restrict to named nodes.
+
+(defun tree-sitter-move--setup ()
+  ;; TODO
+  (progn
+    ;; TODO: if tree-sitter-mode fails to load, display a better error
+    (tree-sitter-mode t)
+    (setq tree-sitter-move--cursor (tsc-root-node tree-sitter-tree))
+    (add-variable-watcher
+     'tree-sitter-move--cursor
+     #'tree-sitter-move--debug-overlay-update)))
+
+(defun tree-sitter-move--debug-overlay-update (sym newval &rest _args)
+  "variable-watcher to update the debug overlay when the cursor changes"
+  (let ((start (tsc-node-start-position newval))
+        (end (tsc-node-end-position newval)))
+    (symbol-macrolet ((o tree-sitter-move--debug-overlay))
+      (if o
+          (move-overlay o start end)
+        (setq o (make-overlay start end))
+        (overlay-put o 'face 'highlight)
+        ))))
+
+(defun tree-sitter-move--debug-overlay-teardown ()
+  "Turn of the overlay visibility and delete the overlay object"
+  (when tree-sitter-move--debug-overlay
+    (delete-overlay tree-sitter-move--debug-overlay)
+    (setq tree-sitter-move--debug-overlay nil)))
+
+(defun tree-sitter-move--teardown ()
+  (setq tree-sitter-move--cursor nil)
+  (tree-sitter-move--debug-overlay-teardown)
+  (tree-sitter-mode nil))
+
+;; Get the syntax node the cursor is on.
+(defun tsc-get-named-node-at-point ()
+  (let ((p (point)))
+    (tsc-get-named-descendant-for-position-range
+     (tsc-root-node tree-sitter-tree) p p)))
+
+;; TODO: is this function necessary?
+;; Maybe tree-sitter always guarantees that parents are named?
+(defun tsc-get-named-parent (node)
+  (when-let ((parent (tsc-get-parent node)))
+    (while (and parent (not (tsc-node-named-p parent)))
+      (setq parent (tsc-get-parent parent)))
+    parent))
+
+(defun tsc-get-first-named-node-with-siblings-up (node)
+  "Returns the first 'upwards' node that has siblings. That includes the current
+  node, so if the given node has siblings, it is returned. Returns nil if there
+  is no such node until the root"
+  (when-let ((has-siblings-p
+              (lambda (parent-node)
+                (> (tsc-count-named-children parent-node)
+                   1)))
+             (cur node)
+             (parent (tsc-get-named-parent node)))
+    (while (and parent (not (funcall has-siblings-p parent)))
+      (setq cur parent)
+      (setq parent (tsc-get-named-parent cur)))
+    cur))
+
+(defun tree-sitter-move--set-cursor-to-node (node)
+  (setq tree-sitter-move--cursor node))
+
+(defun tree-sitter-move--set-cursor-to-node-at-point ()
+  (tree-sitter-move--set-cursor-to-node (tsc-get-named-node-at-point)))
+
+(defun tree-sitter-move--move-point-to-node (node)
+  (set-window-point
+    (selected-window)
+    (tsc-node-start-position node)))
+
+
+;; interactive commands (“do what I expect” section)
+
+(defun tree-sitter-move-reset ()
+  (interactive)
+  (tree-sitter-move--set-cursor-to-node-at-point))
+
+(defun tree-sitter-move-right ()
+  (interactive)
+  (tree-sitter-move--move-skip-non-sibling-nodes 'tsc-get-next-named-sibling))
+
+(defun tree-sitter-move-left ()
+  (interactive)
+  (tree-sitter-move--move-skip-non-sibling-nodes 'tsc-get-prev-named-sibling))
+
+(defun tree-sitter-move-up ()
+  (interactive)
+  (tree-sitter-move--move-skip-non-sibling-nodes 'tsc-get-parent))
+
+;; TODO: does not skip siblings yet, because the skip function only goes up (not down)
+(defun tree-sitter-move-down ()
+  (interactive)
+  (tree-sitter-move--move-if-possible (lambda (n) (tsc-get-nth-named-child n 0))))
+
+(defun tree-sitter-move--move-skip-non-sibling-nodes (move-fn)
+  "Moves to the sidewards next sibling. If the current node does not have siblings, go
+  upwards until something has siblings and then move to the side (right or left)."
+  (tree-sitter-move--move-if-possible
+   (lambda (cur)
+     (when-let ((with-siblings
+                 (tsc-get-first-named-node-with-siblings-up cur)))
+       (funcall move-fn with-siblings)))))
+
+(defun tree-sitter-move--move-if-possible (dir-fn)
+  (let ((next (funcall dir-fn tree-sitter-move--cursor)))
+    (when next
+      (tree-sitter-move--set-cursor-to-node next)
+      (tree-sitter-move--move-point-to-node next))))
+
+; mostly stolen from tree-sitter-mode
+;;;###autoload
+(define-minor-mode tree-sitter-move-mode
+  "Minor mode to do cursor movements via tree-sitter"
+  :init-value nil
+  :lighter " tree-sitter-move"
+  (if tree-sitter-move-mode
+      (tree-sitter--error-protect
+          (progn
+            (tree-sitter-move--setup))
+        (setq tree-sitter-move-mode nil)
+        (tree-sitter-move--teardown))
+    (lambda ())
+    (tree-sitter-move--teardown)))
diff --git a/users/Profpatsch/exactSource.nix b/users/Profpatsch/exactSource.nix
new file mode 100644
index 0000000000..5c713b5b1c
--- /dev/null
+++ b/users/Profpatsch/exactSource.nix
@@ -0,0 +1,90 @@
+{ ... }:
+# SPDX-License-Identifier: MIT
+# Created by Graham Christensen
+# version from https://github.com/grahamc/mayday/blob/c48f7583e622fe2e695a2a929de34679e5818816/exact-source.nix
+
+let
+  # Require that every path specified does exist.
+  #
+  # By default, Nix won't complain if you refer to a missing file
+  # if you don't actually use it:
+  #
+  #     nix-repl> ./bogus
+  #     /home/grahamc/playground/bogus
+  #
+  #     nix-repl> toString ./bogus
+  #     "/home/grahamc/playground/bogus"
+  #
+  # so in order for this interface to be *exact*, we must
+  # specifically require every provided path exists:
+  #
+  #     nix-repl> "${./bogus}"
+  #     error: getting attributes of path
+  #     '/home/grahamc/playground/bogus': No such file or
+  #     directory
+  requireAllPathsExist = paths:
+    let
+      validation = builtins.map (path: "${path}") paths;
+    in
+    builtins.deepSeq validation paths;
+
+  # Break down a given path in to a list of all of the path and
+  # its parent directories.
+  #
+  # `builtins.path` / `builtins.filterSource` will ask about
+  # a containing directory, and we must say YES otherwise it will
+  # not include anything below it.
+  #
+  # Concretely, convert: "/foo/baz/tux" in to:
+  #     [ "/foo/baz/tux" "/foo/baz" "/foo" ]
+  recursivelyPopDir = path:
+    if path == "/" then [ ]
+    else [ path ] ++ (recursivelyPopDir (builtins.dirOf path));
+
+  # Given a list of of strings, dedup the list and return a
+  # list of all unique strings.
+  #
+  # Note: only works on strings ;):
+  #
+  # First convert [ "foo" "foo" "bar" ] in to:
+  #     [
+  #       { name = "foo"; value = ""; }
+  #       { name = "foo"; value = ""; }
+  #       { name = "bar"; value = ""; }
+  #     ]
+  # then convert that to { "foo" = ""; "bar" = ""; }
+  # then get the attribute names, "foo" and "bar".
+  dedup = strings:
+    let
+      name_value_pairs = builtins.map
+        (string: { name = string; value = ""; })
+        strings;
+      attrset_of_strings = builtins.listToAttrs name_value_pairs;
+    in
+    builtins.attrNames attrset_of_strings;
+
+  exactSource = source_root: paths:
+    let
+      all_possible_paths =
+        let
+          # Convert all the paths in to relative paths on disk.
+          # ie: stringPaths will contain [ "/home/grahamc/playground/..." ];
+          # instead of /nix/store paths.
+          string_paths = builtins.map toString
+            (requireAllPathsExist paths);
+
+          all_paths_with_duplicates = builtins.concatMap
+            recursivelyPopDir
+            string_paths;
+        in
+        dedup all_paths_with_duplicates;
+
+      pathIsSpecified = path:
+        builtins.elem path all_possible_paths;
+    in
+    builtins.path {
+      path = source_root;
+      filter = (path: _type: pathIsSpecified path);
+    };
+in
+exactSource
diff --git a/users/Profpatsch/execline/default.nix b/users/Profpatsch/execline/default.nix
new file mode 100644
index 0000000000..752774e6ad
--- /dev/null
+++ b/users/Profpatsch/execline/default.nix
@@ -0,0 +1,37 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  exec-helpers = depot.nix.writers.rustSimpleLib
+    {
+      name = "exec-helpers";
+    }
+    (builtins.readFile ./exec_helpers.rs);
+
+  print-one-env = depot.nix.writers.rustSimple
+    {
+      name = "print-one-env";
+      dependencies = [
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    } ''
+    extern crate exec_helpers;
+    use std::os::unix::ffi::OsStrExt;
+    use std::io::Write;
+
+    fn main() {
+      let args = exec_helpers::args("print-one-env", 1);
+      let valname = std::ffi::OsStr::from_bytes(&args[0]);
+      match std::env::var_os(&valname) {
+        None => exec_helpers::die_user_error("print-one-env", format!("Env variable `{:?}` is not set", valname)),
+        Some(val) => std::io::stdout().write_all(&val.as_bytes()).unwrap()
+      }
+    }
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    exec-helpers
+    print-one-env
+    ;
+}
diff --git a/users/Profpatsch/execline/exec_helpers.rs b/users/Profpatsch/execline/exec_helpers.rs
new file mode 100644
index 0000000000..a57cbca353
--- /dev/null
+++ b/users/Profpatsch/execline/exec_helpers.rs
@@ -0,0 +1,149 @@
+use std::ffi::OsStr;
+use std::os::unix::ffi::{OsStrExt, OsStringExt};
+use std::os::unix::process::CommandExt;
+
+pub fn no_args(current_prog_name: &str) -> () {
+    let mut args = std::env::args_os();
+    // remove argv[0]
+    let _ = args.nth(0);
+    if args.len() > 0 {
+        die_user_error(
+            current_prog_name,
+            format!("Expected no arguments, got {:?}", args.collect::<Vec<_>>()),
+        )
+    }
+}
+
+pub fn args(current_prog_name: &str, no_of_positional_args: usize) -> Vec<Vec<u8>> {
+    let mut args = std::env::args_os();
+    // remove argv[0]
+    let _ = args.nth(0);
+    if args.len() != no_of_positional_args {
+        die_user_error(
+            current_prog_name,
+            format!(
+                "Expected {} arguments, got {}, namely {:?}",
+                no_of_positional_args,
+                args.len(),
+                args.collect::<Vec<_>>()
+            ),
+        )
+    }
+    args.map(|arg| arg.into_vec()).collect()
+}
+
+pub fn args_for_exec(
+    current_prog_name: &str,
+    no_of_positional_args: usize,
+) -> (Vec<Vec<u8>>, Vec<Vec<u8>>) {
+    let mut args = std::env::args_os();
+    // remove argv[0]
+    let _ = args.nth(0);
+    let mut args = args.map(|arg| arg.into_vec());
+    let mut pos_args = vec![];
+    // get positional args
+    for i in 1..no_of_positional_args + 1 {
+        pos_args.push(args.nth(0).expect(&format!(
+            "{}: expects {} positional args, only got {}",
+            current_prog_name, no_of_positional_args, i
+        )));
+    }
+    // prog... is the rest of the iterator
+    let prog: Vec<Vec<u8>> = args.collect();
+    (pos_args, prog)
+}
+
+pub fn exec_into_args<'a, 'b, Args, Arg, Env, Key, Val>(
+    current_prog_name: &str,
+    args: Args,
+    env_additions: Env,
+) -> !
+where
+    Args: IntoIterator<Item = Arg>,
+    Arg: AsRef<[u8]>,
+    Env: IntoIterator<Item = (Key, Val)>,
+    Key: AsRef<[u8]>,
+    Val: AsRef<[u8]>,
+{
+    // TODO: is this possible without collecting into a Vec first, just leaving it an IntoIterator?
+    let args = args.into_iter().collect::<Vec<Arg>>();
+    let mut args = args.iter().map(|v| OsStr::from_bytes(v.as_ref()));
+    let prog = args.nth(0).expect(&format!(
+        "{}: first argument must be an executable",
+        current_prog_name
+    ));
+    // TODO: same here
+    let env = env_additions.into_iter().collect::<Vec<(Key, Val)>>();
+    let env = env
+        .iter()
+        .map(|(k, v)| (OsStr::from_bytes(k.as_ref()), OsStr::from_bytes(v.as_ref())));
+    let err = std::process::Command::new(prog).args(args).envs(env).exec();
+    die_missing_executable(
+        current_prog_name,
+        format!(
+            "exec failed: {}, while trying to execing into {:?}",
+            err, prog
+        ),
+    );
+}
+
+/// Exit 1 to signify a generic expected error
+/// (e.g. something that sometimes just goes wrong, like a nix build).
+pub fn die_expected_error<S>(current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    die_with(1, current_prog_name, msg)
+}
+
+/// Exit 100 to signify a user error (“the user is holding it wrong”).
+/// This is a permanent error, if the program is executed the same way
+/// it should crash with 100 again.
+pub fn die_user_error<S>(current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    die_with(100, current_prog_name, msg)
+}
+
+/// Exit 101 to signify an unexpected crash (failing assertion or panic).
+/// This is the same exit code that `panic!()` emits.
+pub fn die_panic<S>(current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    die_with(101, current_prog_name, msg)
+}
+
+/// Exit 111 to signify a temporary error (such as resource exhaustion)
+pub fn die_temporary<S>(current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    die_with(111, current_prog_name, msg)
+}
+
+/// Exit 126 to signify an environment problem
+/// (the user has set up stuff incorrectly so the program cannot work)
+pub fn die_environment_problem<S>(current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    die_with(126, current_prog_name, msg)
+}
+
+/// Exit 127 to signify a missing executable.
+pub fn die_missing_executable<S>(current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    die_with(127, current_prog_name, msg)
+}
+
+fn die_with<S>(status: i32, current_prog_name: &str, msg: S) -> !
+where
+    S: AsRef<str>,
+{
+    eprintln!("{}: {}", current_prog_name, msg.as_ref());
+    std::process::exit(status)
+}
diff --git a/users/Profpatsch/git-db/default.nix b/users/Profpatsch/git-db/default.nix
new file mode 100644
index 0000000000..ad5d927677
--- /dev/null
+++ b/users/Profpatsch/git-db/default.nix
@@ -0,0 +1,10 @@
+{ depot, pkgs, lib, ... }:
+
+depot.nix.writers.rustSimple
+{
+  name = "git-db";
+  dependencies = [
+    depot.third_party.rust-crates.git2
+  ];
+}
+  (builtins.readFile ./git-db.rs)
diff --git a/users/Profpatsch/git-db/git-db.rs b/users/Profpatsch/git-db/git-db.rs
new file mode 100644
index 0000000000..c8019bf036
--- /dev/null
+++ b/users/Profpatsch/git-db/git-db.rs
@@ -0,0 +1,90 @@
+extern crate git2;
+use std::os::unix::ffi::OsStrExt;
+use std::path::PathBuf;
+
+const DEFAULT_BRANCH: &str = "refs/heads/main";
+
+fn main() {
+    let git_db_dir = std::env::var_os("GIT_DB_DIR").expect("set GIT_DB_DIR");
+    let git_db = PathBuf::from(git_db_dir).join("git");
+
+    std::fs::create_dir_all(&git_db).unwrap();
+
+    let repo = git2::Repository::init_opts(
+        &git_db,
+        git2::RepositoryInitOptions::new()
+            .bare(true)
+            .mkpath(true)
+            .description("git-db database")
+            .initial_head(DEFAULT_BRANCH),
+    )
+    .expect(&format!(
+        "unable to create or open bare git repo at {}",
+        &git_db.display()
+    ));
+
+    let mut index = repo.index().expect("cannot get the git index file");
+    eprintln!("{:#?}", index.version());
+    index.clear().expect("could not clean the index");
+
+    let now = std::time::SystemTime::now()
+        .duration_since(std::time::SystemTime::UNIX_EPOCH)
+        .expect("unable to get system time");
+
+    let now_git_time = git2::IndexTime::new(
+        now.as_secs() as i32, // lol
+        u32::from(now.subsec_nanos()),
+    );
+
+    let data = "hi, it’s me".as_bytes();
+
+    index
+        .add_frombuffer(
+            &git2::IndexEntry {
+            mtime: now_git_time,
+            ctime: now_git_time,
+            // don’t make sense
+            dev: 0,
+            ino: 0,
+            mode: /*libc::S_ISREG*/ 0b1000 << (3+9) | /* read write for owner */ 0o644,
+            uid: 0,
+            gid: 0,
+            file_size: data.len() as u32, // lol again
+            id: git2::Oid::zero(),
+            flags: 0,
+            flags_extended: 0,
+            path: "hi.txt".as_bytes().to_owned(),
+        },
+            data,
+        )
+        .expect("could not add data to index");
+
+    let oid = index.write_tree().expect("could not write index tree");
+
+    let to_add_tree = repo
+        .find_tree(oid)
+        .expect("we just created this tree, where did it go?");
+
+    let parent_commits = match repo.find_reference(DEFAULT_BRANCH) {
+        Ok(ref_) => vec![ref_.peel_to_commit().expect(&format!(
+            "reference {} does not point to a commit",
+            DEFAULT_BRANCH
+        ))],
+        Err(err) => match err.code() {
+            // no commit exists yet
+            git2::ErrorCode::NotFound => vec![],
+            _ => panic!("could not read latest commit from {}", DEFAULT_BRANCH),
+        },
+    };
+    repo.commit(
+        Some(DEFAULT_BRANCH),
+        &git2::Signature::now("Mr. Authorboy", "author@example.com").unwrap(),
+        &git2::Signature::now("Mr. Commiterboy", "committer@example.com").unwrap(),
+        "This is my first commit!\n\
+         \n\
+         I wonder if it supports extended commit descriptions?\n",
+        &to_add_tree,
+        &parent_commits.iter().collect::<Vec<_>>()[..],
+    )
+    .expect("could not commit the index we just wrote");
+}
diff --git a/users/Profpatsch/imap-idle.nix b/users/Profpatsch/imap-idle.nix
new file mode 100644
index 0000000000..84af5d0e54
--- /dev/null
+++ b/users/Profpatsch/imap-idle.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  imap-idle = depot.nix.writers.rustSimple
+    {
+      name = "imap-idle";
+      dependencies = [
+        depot.users.Profpatsch.arglib.netencode.rust
+        depot.third_party.rust-crates.imap
+        depot.third_party.rust-crates.epoll
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    }
+    (builtins.readFile ./imap-idle.rs);
+
+in
+imap-idle
diff --git a/users/Profpatsch/imap-idle.rs b/users/Profpatsch/imap-idle.rs
new file mode 100644
index 0000000000..937847b879
--- /dev/null
+++ b/users/Profpatsch/imap-idle.rs
@@ -0,0 +1,140 @@
+extern crate exec_helpers;
+// extern crate arglib_netencode;
+// extern crate netencode;
+extern crate epoll;
+extern crate imap;
+
+// use netencode::dec;
+use imap::extensions::idle::SetReadTimeout;
+use std::convert::TryFrom;
+use std::fs::File;
+use std::io::{Read, Write};
+use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
+use std::time::Duration;
+
+/// Implements an UCSPI client that wraps fd 6 & 7
+/// and implements Write and Read with a timeout.
+/// See https://cr.yp.to/proto/ucspi.txt
+#[derive(Debug)]
+struct UcspiClient {
+    read: File,
+    read_epoll_fd: RawFd,
+    read_timeout: Option<Duration>,
+    write: File,
+}
+
+impl UcspiClient {
+    /// Use fd 6 and 7 to connect to the net, as is specified.
+    /// Unsafe because fd 6 and 7 are global resources and we don’t mutex them.
+    pub unsafe fn new_from_6_and_7() -> std::io::Result<Self> {
+        unsafe {
+            let read_epoll_fd = epoll::create(false)?;
+            Ok(UcspiClient {
+                read: File::from_raw_fd(6),
+                read_epoll_fd,
+                read_timeout: None,
+                write: File::from_raw_fd(7),
+            })
+        }
+    }
+}
+
+/// Emulates set_read_timeout() like on a TCP socket with an epoll on read.
+/// The BSD socket API is rather bad, so fd != fd,
+/// and if we cast the `UcspiClient` fds to `TcpStream` instead of `File`,
+/// we’d break any UCSPI client programs that *don’t* connect to TCP.
+/// Instead we use the (linux) `epoll` API in read to wait on the timeout.
+impl SetReadTimeout for UcspiClient {
+    fn set_read_timeout(&mut self, timeout: Option<Duration>) -> imap::Result<()> {
+        self.read_timeout = timeout;
+        Ok(())
+    }
+}
+
+impl Read for UcspiClient {
+    // TODO: test the epoll code with a short timeout
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        const NO_DATA: u64 = 0;
+        // in order to implement the read_timeout,
+        // we use epoll to wait for either data or time out
+        epoll::ctl(
+            self.read_epoll_fd,
+            epoll::ControlOptions::EPOLL_CTL_ADD,
+            self.read.as_raw_fd(),
+            epoll::Event::new(epoll::Events::EPOLLIN, NO_DATA),
+        )?;
+        let UNUSED = epoll::Event::new(epoll::Events::EPOLLIN, NO_DATA);
+        let wait = epoll::wait(
+            self.read_epoll_fd,
+            match self.read_timeout {
+                Some(duration) => {
+                    i32::try_from(duration.as_millis()).expect("duration too big for epoll")
+                }
+                None => -1, // infinite
+            },
+            // event that was generated; but we don’t care
+            &mut vec![UNUSED; 1][..],
+        );
+        // Delete the listen fd from the epoll fd before reacting
+        // (otherwise it fails on the next read with `EPOLL_CTL_ADD`)
+        epoll::ctl(
+            self.read_epoll_fd,
+            epoll::ControlOptions::EPOLL_CTL_DEL,
+            self.read.as_raw_fd(),
+            UNUSED,
+        )?;
+        match wait {
+            // timeout happened (0 events)
+            Ok(0) => Err(std::io::Error::new(
+                std::io::ErrorKind::TimedOut,
+                "ucspi read timeout",
+            )),
+            // its ready for reading, we can read
+            Ok(_) => self.read.read(buf),
+            // error
+            err => err,
+        }
+    }
+}
+
+/// Just proxy through the `Write` of the write fd.
+impl Write for UcspiClient {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.write.write(buf)
+    }
+    fn flush(&mut self) -> std::io::Result<()> {
+        self.write.flush()
+    }
+}
+
+/// Connect to IMAP account and listen for new mails on the INBOX.
+fn main() {
+    exec_helpers::no_args("imap-idle");
+
+    // TODO: use arglib_netencode
+    let username = std::env::var("IMAP_USERNAME").expect("username");
+    let password = std::env::var("IMAP_PASSWORD").expect("password");
+
+    let net = unsafe { UcspiClient::new_from_6_and_7().expect("no ucspi client for you") };
+    let client = imap::Client::new(net);
+    let mut session = client
+        .login(username, password)
+        .map_err(|(err, _)| err)
+        .expect("unable to login");
+    eprintln!("{:#?}", session);
+    let list = session.list(None, Some("*"));
+    eprintln!("{:#?}", list);
+    let mailbox = session.examine("INBOX");
+    eprintln!("{:#?}", mailbox);
+    fn now() -> String {
+        String::from_utf8_lossy(&std::process::Command::new("date").output().unwrap().stdout)
+            .trim_right()
+            .to_string()
+    }
+    loop {
+        eprintln!("{}: idling on INBOX", now());
+        let mut handle = session.idle().expect("cannot idle on INBOX");
+        let () = handle.wait_keepalive().expect("waiting on idle failed");
+        eprintln!("{}: The mailbox has changed!", now());
+    }
+}
diff --git a/users/Profpatsch/importDhall.nix b/users/Profpatsch/importDhall.nix
new file mode 100644
index 0000000000..1947ad1ce1
--- /dev/null
+++ b/users/Profpatsch/importDhall.nix
@@ -0,0 +1,93 @@
+{ pkgs, depot, lib, ... }:
+let
+
+  # import the dhall file as nix expression via dhall-nix.
+  # Converts the normalized dhall expression to a nix file,
+  # puts it in the store and imports it.
+  # Types are erased, functions are converted to nix functions,
+  # unions values are nix functions that take a record of match
+  # functions for their alternatives.
+  # TODO: document better
+  importDhall =
+    {
+      # Root path of the dhall file tree to import (will be filtered by files)
+      root
+    , # A list of files which should be taken from `root` (relative paths).
+      # This is for minimizing the amount of things that have to be copied to the store.
+      # TODO: can you have directory prefixes?
+      files
+    , # The path of the dhall file which should be evaluated, relative to `root`, has to be in `files`
+      main
+    , # List of dependencies (TODO: what is a dependency?)
+      deps
+    , # dhall type of `main`, or `null` if anything should be possible.
+      type ? null
+    }:
+    let
+      absRoot = path: toString root + "/" + path;
+      src =
+        depot.users.Profpatsch.exactSource
+          root
+          # exactSource wants nix paths, but I think relative paths
+          # as strings are more intuitive.
+          ([ (absRoot main) ] ++ (map absRoot files));
+
+      cache = ".cache";
+      cacheDhall = "${cache}/dhall";
+
+      hadTypeAnnot = type != null;
+      typeAnnot = lib.optionalString hadTypeAnnot ": ${type}";
+
+      convert = pkgs.runCommandLocal "dhall-to-nix" { inherit deps; } ''
+        mkdir -p ${cacheDhall}
+        for dep in $deps; do
+          ${pkgs.xorg.lndir}/bin/lndir -silent $dep/${cacheDhall} ${cacheDhall}
+        done
+
+        export XDG_CACHE_HOME=$(pwd)/${cache}
+        # go into the source directory, so that the type can import files.
+        # TODO: This is a bit of a hack hrm.
+        cd "${src}"
+        printf 'Generating dhall nix code. Run
+        %s --file %s
+        to reproduce
+        ' \
+          ${pkgs.dhall}/bin/dhall \
+          ${absRoot main}
+        ${if hadTypeAnnot then ''
+            printf '%s' ${lib.escapeShellArg "${src}/${main} ${typeAnnot}"} \
+              | ${pkgs.dhall-nix}/bin/dhall-to-nix \
+              > $out
+          ''
+          else ''
+            printf 'No type annotation given, the dhall expression type was:\n'
+            ${pkgs.dhall}/bin/dhall type --file "${src}/${main}"
+            printf '%s' ${lib.escapeShellArg "${src}/${main}"} \
+              | ${pkgs.dhall-nix}/bin/dhall-to-nix \
+              > $out
+          ''}
+
+      '';
+    in
+    import convert;
+
+
+  # read dhall file in as JSON, then import as nix expression.
+  # The dhall file must not try to import from non-local URLs!
+  readDhallFileAsJson = dhallType: file:
+    let
+      convert = pkgs.runCommandLocal "dhall-to-json" { } ''
+        printf '%s' ${lib.escapeShellArg "${file} : ${dhallType}"} \
+          | ${pkgs.dhall-json}/bin/dhall-to-json \
+          > $out
+      '';
+    in
+    builtins.fromJSON (builtins.readFile convert);
+
+in
+{
+  inherit
+    importDhall
+    readDhallFileAsJson
+    ;
+}
diff --git a/users/Profpatsch/lens.nix b/users/Profpatsch/lens.nix
new file mode 100644
index 0000000000..28f7506bdd
--- /dev/null
+++ b/users/Profpatsch/lens.nix
@@ -0,0 +1,137 @@
+{ ... }:
+let
+  id = x: x;
+
+  const = x: y: x;
+
+  comp = f: g: x: f (g x);
+
+  _ = v: f: f v;
+
+  # Profunctor (p :: Type -> Type -> Type)
+  Profunctor = rec {
+    # dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
+    dimap = f: g: x: lmap f (rmap g x);
+    # lmap :: (a -> b) -> p b c -> p a c
+    lmap = f: dimap f id;
+    # rmap :: (c -> d) -> p b c -> p b d
+    rmap = g: dimap id g;
+  };
+
+  # Profunctor (->)
+  profunctorFun = Profunctor // {
+    # dimap :: (a -> b) -> (c -> d) -> (b -> c) -> a -> d
+    dimap = ab: cd: bc: a: cd (bc (ab a));
+    # lmap :: (a -> b) -> (b -> c) -> (a -> c)
+    lmap = ab: bc: a: bc (ab a);
+    # rmap :: (c -> d) -> (b -> c) -> (b -> d)
+    rmap = cd: bc: b: cd (bc b);
+  };
+
+  tuple = fst: snd: {
+    inherit fst snd;
+  };
+
+  swap = { fst, snd }: {
+    fst = snd;
+    snd = fst;
+  };
+
+  # Profunctor p => Strong (p :: Type -> Type -> Type)
+  Strong = pro: pro // rec {
+    # firstP :: p a b -> p (a, c) (b, c)
+    firstP = pab: pro.dimap swap swap (pro.secondP pab);
+    # secondP :: p a b -> p (c, a) (c, b)
+    secondP = pab: pro.dimap swap swap (pro.firstP pab);
+  };
+
+  # Strong (->)
+  strongFun = Strong profunctorFun // {
+    # firstP :: (a -> b) -> (a, c) -> (b, c)
+    firstP = f: { fst, snd }: { fst = f fst; inherit snd; };
+    # secondP :: (a -> b) -> (c, a) -> (c, b)
+    secondP = f: { snd, fst }: { snd = f snd; inherit fst; };
+  };
+
+  # Iso s t a b :: forall p. Profunctor p -> p a b -> p s t
+
+  # iso :: (s -> a) -> (b -> t) -> Iso s t a b
+  iso = pro: pro.dimap;
+
+  # Lens s t a b :: forall p. Strong p -> p a b -> p s t
+
+  # lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
+  lens = strong: get: set: pab:
+    lensP
+      strong
+      (s: tuple (get s) (b: set s b))
+      pab;
+
+  # lensP :: (s -> (a, b -> t)) -> Lens s t a b
+  lensP = strong: to: pab:
+    strong.dimap
+      to
+      ({ fst, snd }: snd fst)
+      (strong.firstP pab);
+
+  # first element of a tuple
+  # _1 :: Lens (a, c) (b, c) a b
+  _1 = strong: strong.firstP;
+
+  # second element of a tuple
+  # _2 :: Lens (c, a) (c, b) a b
+  _2 = strong: strong.secondP;
+
+  # a the given field in the record
+  # field :: (f :: String) -> Lens { f :: a; ... } { f :: b; ... } a b
+  field = name: strong:
+    lens
+      strong
+      (attrs: attrs.${name})
+      (attrs: a: attrs // { ${name} = a; });
+
+  # Setter :: (->) a b -> (->) s t
+  # Setter :: (a -> b) -> (s -> t)
+
+
+  # Subclasses of profunctor for (->).
+  # We only have Strong for now, but when we implement Choice we need to add it here.
+  profunctorSubclassesFun = strongFun;
+
+  # over :: Setter s t a b -> (a -> b) -> s -> t
+  over = setter:
+    # A setter needs to be instanced to the profunctor-subclass instances of (->).
+    (setter profunctorSubclassesFun);
+
+  # set :: Setter s t a b -> b -> s -> t
+  set = setter: b: over setter (const b);
+
+  # combine a bunch of optics, for the subclass instance of profunctor you give it.
+  optic = accessors: profunctorSubclass:
+    builtins.foldl' comp id
+      (map (accessor: accessor profunctorSubclass) accessors);
+
+
+in
+{
+  inherit
+    id
+    _
+    const
+    comp
+    Profunctor
+    profunctorFun
+    Strong
+    strongFun
+    iso
+    lens
+    optic
+    _1
+    _2
+    field
+    tuple
+    swap
+    over
+    set
+    ;
+}
diff --git a/users/Profpatsch/lib.nix b/users/Profpatsch/lib.nix
new file mode 100644
index 0000000000..879d87755d
--- /dev/null
+++ b/users/Profpatsch/lib.nix
@@ -0,0 +1,108 @@
+{ depot, pkgs, ... }:
+let
+  bins = depot.nix.getBins pkgs.coreutils [ "printf" "echo" "cat" "printenv" "tee" ]
+    // depot.nix.getBins pkgs.bash [ "bash" ]
+    // depot.nix.getBins pkgs.fdtools [ "multitee" ]
+  ;
+
+  # Print `msg` and and argv to stderr, then execute into argv
+  debugExec = msg: depot.nix.writeExecline "debug-exec" { } [
+    "if"
+    [
+      "fdmove"
+      "-c"
+      "1"
+      "2"
+      "if"
+      [ bins.printf "%s: " msg ]
+      "if"
+      [ bins.echo "$@" ]
+    ]
+    "$@"
+  ];
+
+  # Print stdin to stderr and stdout
+  eprint-stdin = depot.nix.writeExecline "eprint-stdin" { } [
+    "pipeline"
+    [ bins.multitee "0-1,2" ]
+    "$@"
+  ];
+
+  # Assume the input on stdin is netencode, pretty print it to stderr and forward it to stdout
+  eprint-stdin-netencode = depot.nix.writeExecline "eprint-stdin-netencode" { } [
+    "pipeline"
+    [
+      # move stdout to 3
+      "fdmove"
+      "3"
+      "1"
+      # the multitee copies stdin to 1 (the other pipeline end) and 3 (the stdout of the outer pipeline block)
+      "pipeline"
+      [ bins.multitee "0-1,3" ]
+      # make stderr the stdout of pretty, merging with the stderr of pretty
+      "fdmove"
+      "-c"
+      "1"
+      "2"
+      depot.users.Profpatsch.netencode.pretty
+    ]
+    "$@"
+  ];
+
+  # print the given environment variable in $1 to stderr, then execute into the rest of argv
+  eprintenv = depot.nix.writeExecline "eprintenv" { readNArgs = 1; } [
+    "ifelse"
+    [ "fdmove" "-c" "1" "2" bins.printenv "$1" ]
+    [ "$@" ]
+    "if"
+    [ depot.tools.eprintf "eprintenv: could not find \"\${1}\" in the environment\n" ]
+    "$@"
+  ];
+
+  # Split stdin into two commands, given by a block and the rest of argv
+  #
+  # Example (execline):
+  #
+  #   pipeline [ echo foo ]
+  #   split-stdin [ fdmove 1 2 foreground [ cat ] echo "bar" ] cat
+  #
+  #   stdout: foo\n
+  #   stderr: foo\nbar\n
+  split-stdin = depot.nix.writeExecline "split-stdin" { argMode = "env"; } [
+    "pipeline"
+    [
+      # this is horrible yes but the quickest way I knew how to implement it
+      "runblock"
+      "1"
+      bins.bash
+      "-c"
+      ''${bins.tee} >("$@")''
+      "bash-split-stdin"
+    ]
+    "runblock"
+    "-r"
+    "1"
+  ];
+
+  # remove everything but a few selected environment variables
+  runInEmptyEnv = keepVars:
+    let
+      importas = pkgs.lib.concatMap (var: [ "importas" "-i" var var ]) keepVars;
+      # we have to explicitely call export here, because PATH is probably empty
+      export = pkgs.lib.concatMap (var: [ "${pkgs.execline}/bin/export" var ''''${${var}}'' ]) keepVars;
+    in
+    depot.nix.writeExecline "empty-env" { }
+      (importas ++ [ "emptyenv" ] ++ export ++ [ "${pkgs.execline}/bin/exec" "$@" ]);
+
+
+in
+{
+  inherit
+    debugExec
+    eprint-stdin
+    eprint-stdin-netencode
+    eprintenv
+    split-stdin
+    runInEmptyEnv
+    ;
+}
diff --git a/users/Profpatsch/netencode/README.md b/users/Profpatsch/netencode/README.md
new file mode 100644
index 0000000000..8dc39f6337
--- /dev/null
+++ b/users/Profpatsch/netencode/README.md
@@ -0,0 +1,115 @@
+# netencode 0.1-unreleased
+
+[bencode][] and [netstring][]-inspired pipe format that should be trivial to generate correctly in every context (only requires a `byte_length()` and a `printf()`), easy to parse (100 lines of code or less), mostly human-decipherable for easy debugging, and support nested record and sum types.
+
+
+## scalars
+
+Scalars have the format `[type prefix][size]:[value],`.
+
+where size is a natural number without leading zeroes.
+
+### unit
+
+The unit (`u`) has only one value.
+
+* The unit is: `u,`
+
+### numbers
+
+Naturals (`n`) and Integers (`i`), with a maximum size in bits.
+
+Bit sizes are specified in 2^n increments, 1 to 9 (`n1`..`n9`, `i1`..`n9`).
+
+* Natural `1234` that fits in 32 bits (2^5): `n5:1234,`
+* Integer `-42` that fits in 8 bits (2^3): `i3:-42,`
+* Integer `23` that fits in 64 bits (2^6): `i6:23,`
+* Integer `-1` that fits in 512 bits (2^9): `i9:-1,`
+* Natural `0` that fits in 1 bit (2^1): `n1:0,`
+
+An implementation can define the biggest numbers it supports, and has to throw an error for anything bigger. It has to support everything smaller, so for example if you support up to i6/n6, you have to support 1–6 as well. An implementation could support up to the current architecture’s wordsize for example.
+
+Floats are not supported, you can implement fixed-size decimals or ratios using integers.
+
+### booleans
+
+A boolean is represented as `n1`.
+
+* `n1:0,`: false
+* `n1:1,`: true
+
+TODO: should we add `f,` and `t,`?
+
+### text
+
+Text (`t`) that *must* be encoded as UTF-8, starting with its length in bytes:
+
+* The string `hello world` (11 bytes): `t11:hello world,`
+* The string `今日は` (9 bytes): `t9:今日は,`
+* The string `:,` (2 bytes): `t2::,,`
+* The empty sting `` (0 bytes): `t0:,`
+
+### binary
+
+Arbitrary binary strings (`b`) that can contain any data, starting with its length in bytes.
+
+* The ASCII string `hello world` as binary data (11 bytes): `b11:hello world,`
+* The empty binary string (0 bytes): `b0:,`
+* The bytestring with `^D` (1 byte): `b1:,`
+
+Since the binary strings are length-prefixd, they can contain `\0` and no escaping is required. Care has to be taken in languages with `\0`-terminated bytestrings.
+
+Use text (`t`) if you have utf-8 encoded data.
+
+## tagged values
+
+### tags
+
+A tag (`<`) gives a value a name. The tag is UTF-8 encoded, starting with its length in bytes and proceeding with the value.
+
+* The tag `foo` (3 bytes) tagging the text `hello` (5 bytes): `<3:foo|t5:hello,`
+* The tag `` (0 bytes) tagging the 8-bit integer 0: `<0:|i3:0,`
+
+### records (products/records), also maps
+
+A record (`{`) is a concatenation of tags (`<`). It needs to be closed with `}`.
+
+If tag names repeat the *earlier* ones should be ignored.
+Using the last tag corresponds with the way most languages handle converting a list of tuples to Maps, by using a for-loop and Map.insert without checking the contents first. Otherwise you’d have to revert the list first or remember which keys you already inserted.
+
+Ordering of tags in a record does not matter.
+
+Similar to text, records start with the length of their *whole encoded content*, in bytes. This makes it possible to treat their contents as opaque bytestrings.
+
+* There is no empty record. (TODO: make the empty record the unit type, remove `u,`?)
+* A record with one empty field, `foo`: `{9:<3:foo|u,}`
+* A record with two fields, `foo` and `x`: `{21:<3:foo|u,<1:x|t3:baz,}`
+* The same record: `{21:<1:x|t3:baz,<3:foo|u,}`
+* The same record (later occurences of fields are ignored): `{28:<1:x|t3:baz,<3:foo|u,<1:x|u,}`
+
+### sums (tagged unions)
+
+Simply a tagged value. The tag marker `<` indicates it is a sum if it appears outside of a record.
+
+## lists
+
+A list (`[`) imposes an ordering on a sequence of values. It needs to be closed with `]`. Values in it are simply concatenated.
+
+Similar to records, lists start with the length of their whole encoded content.
+
+* The empty list: `[0:]`
+* The list with one element, the string `foo`: `[7:t3:foo,]`
+* The list with text `foo` followed by i3 `-42`: `[14:t3:foo,i3:-42,]`
+* The list with `Some` and `None` tags: `[33:<4:Some|t3:foo,<4None|u,<4None|u,]`
+
+## motivation
+
+TODO
+
+## guarantees
+
+TODO: do I want unique representation (bijection like bencode?) This would put more restrictions on the generator, like sorting records in lexicographic order, but would make it possible to compare without decoding
+
+
+[bencode]: https://en.wikipedia.org/wiki/Bencode
+[netstring]: https://en.wikipedia.org/wiki/Netstring
diff --git a/users/Profpatsch/netencode/default.nix b/users/Profpatsch/netencode/default.nix
new file mode 100644
index 0000000000..d389258148
--- /dev/null
+++ b/users/Profpatsch/netencode/default.nix
@@ -0,0 +1,160 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  netencode-rs = depot.nix.writers.rustSimpleLib
+    {
+      name = "netencode";
+      dependencies = [
+        depot.third_party.rust-crates.nom
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    }
+    (builtins.readFile ./netencode.rs);
+
+  gen = import ./gen.nix { inherit lib; };
+
+  pretty-rs = depot.nix.writers.rustSimpleLib
+    {
+      name = "netencode-pretty";
+      dependencies = [
+        netencode-rs
+      ];
+    }
+    (builtins.readFile ./pretty.rs);
+
+  pretty = depot.nix.writers.rustSimple
+    {
+      name = "netencode-pretty";
+      dependencies = [
+        netencode-rs
+        pretty-rs
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    } ''
+    extern crate netencode;
+    extern crate netencode_pretty;
+    extern crate exec_helpers;
+
+    fn main() {
+      let (_, prog) = exec_helpers::args_for_exec("netencode-pretty", 0);
+      let mut buf = vec![];
+      let u = netencode::u_from_stdin_or_die_user_error("netencode-pretty", &mut buf);
+      match netencode_pretty::Pretty::from_u(u).print_multiline(&mut std::io::stdout()) {
+        Ok(()) => {},
+        Err(err) => exec_helpers::die_temporary("netencode-pretty", format!("could not write to stdout: {}", err))
+      }
+    }
+  '';
+
+  netencode-mustache = depot.nix.writers.rustSimple
+    {
+      name = "netencode_mustache";
+      dependencies = [
+        depot.users.Profpatsch.arglib.netencode.rust
+        netencode-rs
+        depot.third_party.rust-crates.mustache
+      ];
+    }
+    (builtins.readFile ./netencode-mustache.rs);
+
+
+  record-get = depot.nix.writers.rustSimple
+    {
+      name = "record-get";
+      dependencies = [
+        netencode-rs
+        depot.users.Profpatsch.execline.exec-helpers
+        depot.users.Profpatsch.arglib.netencode.rust
+      ];
+    } ''
+    extern crate netencode;
+    extern crate arglib_netencode;
+    extern crate exec_helpers;
+    use netencode::{encode, dec};
+    use netencode::dec::{Decoder, DecodeError};
+
+    fn main() {
+        let mut buf = vec![];
+        let args = exec_helpers::args("record-get", 1);
+        let field = match std::str::from_utf8(&args[0]) {
+            Ok(f) => f,
+            Err(_e) => exec_helpers::die_user_error("record-get", format!("The field name needs to be valid unicode"))
+        };
+        let u = netencode::u_from_stdin_or_die_user_error("record-get", &mut buf);
+        match (dec::RecordDot {field, inner: dec::AnyU }).dec(u) {
+            Ok(u) => encode(&mut std::io::stdout(), &u).expect("encoding to stdout failed"),
+            Err(DecodeError(err)) => exec_helpers::die_user_error("record-get", err)
+        }
+    }
+  '';
+
+  record-splice-env = depot.nix.writers.rustSimple
+    {
+      name = "record-splice-env";
+      dependencies = [
+        netencode-rs
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    } ''
+    extern crate netencode;
+    extern crate exec_helpers;
+    use netencode::dec::{Record, Try, ScalarAsBytes, Decoder, DecodeError};
+
+    fn main() {
+        let mut buf = vec![];
+        let u = netencode::u_from_stdin_or_die_user_error("record-splice-env", &mut buf);
+        let (_, prog) = exec_helpers::args_for_exec("record-splice-env", 0);
+        match Record(Try(ScalarAsBytes)).dec(u) {
+            Ok(map) => {
+                exec_helpers::exec_into_args(
+                    "record-splice-env",
+                    prog,
+                    // some elements can’t be decoded as scalars, so just ignore them
+                    map.into_iter().filter_map(|(k, v)| v.map(|v2| (k, v2)))
+                );
+            },
+            Err(DecodeError(err)) => exec_helpers::die_user_error("record-splice-env", err),
+        }
+    }
+  '';
+
+  env-splice-record = depot.nix.writers.rustSimple
+    {
+      name = "env-splice-record";
+      dependencies = [
+        netencode-rs
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    } ''
+    extern crate netencode;
+    extern crate exec_helpers;
+    use netencode::{T};
+    use std::os::unix::ffi::OsStringExt;
+
+    fn main() {
+        exec_helpers::no_args("env-splice-record");
+        let mut res = std::collections::HashMap::new();
+        for (key, val) in std::env::vars_os() {
+          match (String::from_utf8(key.into_vec()), String::from_utf8(val.into_vec())) {
+            (Ok(k), Ok(v)) => { let _ = res.insert(k, T::Text(v)); },
+            // same as in record-splice-env, we ignore non-utf8 variables
+            (_, _) => {},
+          }
+        }
+        netencode::encode(&mut std::io::stdout(), &T::Record(res).to_u()).unwrap()
+    }
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    netencode-rs
+    pretty-rs
+    pretty
+    netencode-mustache
+    record-get
+    record-splice-env
+    env-splice-record
+    gen
+    ;
+}
diff --git a/users/Profpatsch/netencode/gen.nix b/users/Profpatsch/netencode/gen.nix
new file mode 100644
index 0000000000..efc9629ca0
--- /dev/null
+++ b/users/Profpatsch/netencode/gen.nix
@@ -0,0 +1,73 @@
+{ lib }:
+let
+
+  netstring = tag: suffix: s:
+    "${tag}${toString (builtins.stringLength s)}:${s}${suffix}";
+
+  unit = "u,";
+
+  n1 = b: if b then "n1:1," else "n1:0,";
+
+  n = i: n: "n${toString i}:${toString n},";
+  i = i: n: "i${toString i}:${toString n},";
+
+  n3 = n 3;
+  n6 = n 6;
+  n7 = n 7;
+
+  i3 = i 3;
+  i6 = i 6;
+  i7 = i 7;
+
+  text = netstring "t" ",";
+  binary = netstring "b" ",";
+
+  tag = key: val: netstring "<" "|" key + val;
+
+  concatStrings = builtins.concatStringsSep "";
+
+  record = lokv: netstring "{" "}"
+    (concatStrings (map ({ key, val }: tag key val) lokv));
+
+  list = l: netstring "[" "]" (concatStrings l);
+
+  dwim = val:
+    let
+      match = {
+        "bool" = n1;
+        "int" = i6;
+        "string" = text;
+        "set" = attrs:
+          # it could be a derivation, then just return the path
+          if attrs.type or "" == "derivation" then text "${attrs}"
+          else
+            record (lib.mapAttrsToList
+              (k: v: {
+                key = k;
+                val = dwim v;
+              })
+              attrs);
+        "list" = l: list (map dwim l);
+      };
+    in
+    match.${builtins.typeOf val} val;
+
+in
+{
+  inherit
+    unit
+    n1
+    n3
+    n6
+    n7
+    i3
+    i6
+    i7
+    text
+    binary
+    tag
+    record
+    list
+    dwim
+    ;
+}
diff --git a/users/Profpatsch/netencode/netencode-mustache.rs b/users/Profpatsch/netencode/netencode-mustache.rs
new file mode 100644
index 0000000000..73ed5be1de
--- /dev/null
+++ b/users/Profpatsch/netencode/netencode-mustache.rs
@@ -0,0 +1,52 @@
+extern crate arglib_netencode;
+extern crate mustache;
+extern crate netencode;
+
+use mustache::Data;
+use netencode::T;
+use std::collections::HashMap;
+use std::io::Read;
+use std::os::unix::ffi::OsStrExt;
+
+fn netencode_to_mustache_data_dwim(t: T) -> Data {
+    match t {
+        // TODO: good idea?
+        T::Unit => Data::Null,
+        T::N1(b) => Data::Bool(b),
+        T::N3(u) => Data::String(u.to_string()),
+        T::N6(u) => Data::String(u.to_string()),
+        T::N7(u) => Data::String(u.to_string()),
+        T::I3(i) => Data::String(i.to_string()),
+        T::I6(i) => Data::String(i.to_string()),
+        T::I7(i) => Data::String(i.to_string()),
+        T::Text(s) => Data::String(s),
+        T::Binary(b) => unimplemented!(),
+        T::Sum(tag) => unimplemented!(),
+        T::Record(xs) => Data::Map(
+            xs.into_iter()
+                .map(|(key, val)| (key, netencode_to_mustache_data_dwim(val)))
+                .collect::<HashMap<_, _>>(),
+        ),
+        T::List(xs) => Data::Vec(
+            xs.into_iter()
+                .map(|x| netencode_to_mustache_data_dwim(x))
+                .collect::<Vec<_>>(),
+        ),
+    }
+}
+
+pub fn from_stdin() -> () {
+    let data = netencode_to_mustache_data_dwim(arglib_netencode::arglib_netencode(
+        "netencode-mustache",
+        Some(std::ffi::OsStr::new("TEMPLATE_DATA")),
+    ));
+    let mut stdin = String::new();
+    std::io::stdin().read_to_string(&mut stdin).unwrap();
+    mustache::compile_str(&stdin)
+        .and_then(|templ| templ.render_data(&mut std::io::stdout(), &data))
+        .unwrap()
+}
+
+pub fn main() {
+    from_stdin()
+}
diff --git a/users/Profpatsch/netencode/netencode.rs b/users/Profpatsch/netencode/netencode.rs
new file mode 100644
index 0000000000..bb08dca4aa
--- /dev/null
+++ b/users/Profpatsch/netencode/netencode.rs
@@ -0,0 +1,891 @@
+extern crate exec_helpers;
+extern crate nom;
+
+use std::collections::HashMap;
+use std::fmt::{Debug, Display};
+use std::io::{Read, Write};
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum T {
+    // Unit
+    Unit,
+    // Boolean
+    N1(bool),
+    // Naturals
+    N3(u8),
+    N6(u64),
+    N7(u128),
+    // Integers
+    I3(i8),
+    I6(i64),
+    I7(i128),
+    // Text
+    // TODO: make into &str
+    Text(String),
+    // TODO: rename to Bytes
+    Binary(Vec<u8>),
+    // Tags
+    // TODO: make into &str
+    // TODO: rename to Tag
+    Sum(Tag<String, T>),
+    // TODO: make into &str
+    Record(HashMap<String, T>),
+    List(Vec<T>),
+}
+
+impl T {
+    pub fn to_u<'a>(&'a self) -> U<'a> {
+        match self {
+            T::Unit => U::Unit,
+            T::N1(b) => U::N1(*b),
+            T::N3(u) => U::N3(*u),
+            T::N6(u) => U::N6(*u),
+            T::N7(u) => U::N7(*u),
+            T::I3(i) => U::I3(*i),
+            T::I6(i) => U::I6(*i),
+            T::I7(i) => U::I7(*i),
+            T::Text(t) => U::Text(t.as_str()),
+            T::Binary(v) => U::Binary(v),
+            T::Sum(Tag { tag, val }) => U::Sum(Tag {
+                tag: tag.as_str(),
+                val: Box::new(val.to_u()),
+            }),
+            T::Record(map) => U::Record(map.iter().map(|(k, v)| (k.as_str(), v.to_u())).collect()),
+            T::List(l) => U::List(l.iter().map(|v| v.to_u()).collect::<Vec<U<'a>>>()),
+        }
+    }
+
+    pub fn encode<'a>(&'a self) -> Vec<u8> {
+        match self {
+            // TODO: don’t go via U, inefficient
+            o => o.to_u().encode(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum U<'a> {
+    Unit,
+    // Boolean
+    N1(bool),
+    // Naturals
+    N3(u8),
+    N6(u64),
+    N7(u128),
+    // Integers
+    I3(i8),
+    I6(i64),
+    I7(i128),
+    // Text
+    Text(&'a str),
+    Binary(&'a [u8]),
+    // TODO: the U-recursion we do here means we can’t be breadth-lazy anymore
+    // like we originally planned; maybe we want to go `U<'a>` → `&'a [u8]` again?
+    // Tags
+    // TODO: rename to Tag
+    Sum(Tag<&'a str, U<'a>>),
+    Record(HashMap<&'a str, U<'a>>),
+    List(Vec<U<'a>>),
+}
+
+impl<'a> U<'a> {
+    pub fn encode(&self) -> Vec<u8> {
+        let mut c = std::io::Cursor::new(vec![]);
+        encode(&mut c, self);
+        c.into_inner()
+    }
+
+    pub fn to_t(&self) -> T {
+        match self {
+            U::Unit => T::Unit,
+            U::N1(b) => T::N1(*b),
+            U::N3(u) => T::N3(*u),
+            U::N6(u) => T::N6(*u),
+            U::N7(u) => T::N7(*u),
+            U::I3(i) => T::I3(*i),
+            U::I6(i) => T::I6(*i),
+            U::I7(i) => T::I7(*i),
+            U::Text(t) => T::Text((*t).to_owned()),
+            U::Binary(v) => T::Binary((*v).to_owned()),
+            U::Sum(Tag { tag, val }) => T::Sum(Tag {
+                tag: (*tag).to_owned(),
+                val: Box::new(val.to_t()),
+            }),
+            U::Record(map) => T::Record(
+                map.iter()
+                    .map(|(k, v)| ((*k).to_owned(), v.to_t()))
+                    .collect::<HashMap<String, T>>(),
+            ),
+            U::List(l) => T::List(l.iter().map(|v| v.to_t()).collect::<Vec<T>>()),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Tag<S, A> {
+    // TODO: make into &str
+    pub tag: S,
+    pub val: Box<A>,
+}
+
+impl<S, A> Tag<S, A> {
+    fn map<F, B>(self, f: F) -> Tag<S, B>
+    where
+        F: Fn(A) -> B,
+    {
+        Tag {
+            tag: self.tag,
+            val: Box::new(f(*self.val)),
+        }
+    }
+}
+
+fn encode_tag<W: Write>(w: &mut W, tag: &str, val: &U) -> std::io::Result<()> {
+    write!(w, "<{}:{}|", tag.len(), tag)?;
+    encode(w, val)?;
+    Ok(())
+}
+
+pub fn encode<W: Write>(w: &mut W, u: &U) -> std::io::Result<()> {
+    match u {
+        U::Unit => write!(w, "u,"),
+        U::N1(b) => {
+            if *b {
+                write!(w, "n1:1,")
+            } else {
+                write!(w, "n1:0,")
+            }
+        }
+        U::N3(n) => write!(w, "n3:{},", n),
+        U::N6(n) => write!(w, "n6:{},", n),
+        U::N7(n) => write!(w, "n7:{},", n),
+        U::I3(i) => write!(w, "i3:{},", i),
+        U::I6(i) => write!(w, "i6:{},", i),
+        U::I7(i) => write!(w, "i7:{},", i),
+        U::Text(s) => {
+            write!(w, "t{}:", s.len());
+            w.write_all(s.as_bytes());
+            write!(w, ",")
+        }
+        U::Binary(s) => {
+            write!(w, "b{}:", s.len());
+            w.write_all(&s);
+            write!(w, ",")
+        }
+        U::Sum(Tag { tag, val }) => encode_tag(w, tag, val),
+        U::Record(m) => {
+            let mut c = std::io::Cursor::new(vec![]);
+            for (k, v) in m {
+                encode_tag(&mut c, k, v)?;
+            }
+            write!(w, "{{{}:", c.get_ref().len())?;
+            w.write_all(c.get_ref())?;
+            write!(w, "}}")
+        }
+        U::List(l) => {
+            let mut c = std::io::Cursor::new(vec![]);
+            for u in l {
+                encode(&mut c, u)?;
+            }
+            write!(w, "[{}:", c.get_ref().len())?;
+            w.write_all(c.get_ref())?;
+            write!(w, "]")
+        }
+    }
+}
+
+pub fn text(s: String) -> T {
+    T::Text(s)
+}
+
+pub fn u_from_stdin_or_die_user_error<'a>(prog_name: &'_ str, stdin_buf: &'a mut Vec<u8>) -> U<'a> {
+    std::io::stdin().lock().read_to_end(stdin_buf);
+    let u = match parse::u_u(stdin_buf) {
+        Ok((rest, u)) => match rest {
+            b"" => u,
+            _ => exec_helpers::die_user_error(
+                prog_name,
+                format!(
+                    "stdin contained some soup after netencode value: {:?}",
+                    String::from_utf8_lossy(rest)
+                ),
+            ),
+        },
+        Err(err) => exec_helpers::die_user_error(
+            prog_name,
+            format!("unable to parse netencode from stdin: {:?}", err),
+        ),
+    };
+    u
+}
+
+pub mod parse {
+    use super::{Tag, T, U};
+
+    use std::collections::HashMap;
+    use std::ops::Neg;
+    use std::str::FromStr;
+
+    use nom::branch::alt;
+    use nom::bytes::streaming::{tag, take};
+    use nom::character::streaming::{char, digit1};
+    use nom::combinator::{flat_map, map, map_parser, map_res, opt};
+    use nom::error::{context, ErrorKind, ParseError};
+    use nom::sequence::tuple;
+    use nom::IResult;
+
+    fn unit_t(s: &[u8]) -> IResult<&[u8], ()> {
+        let (s, _) = context("unit", tag("u,"))(s)?;
+        Ok((s, ()))
+    }
+
+    fn usize_t(s: &[u8]) -> IResult<&[u8], usize> {
+        context(
+            "usize",
+            map_res(map_res(digit1, |n| std::str::from_utf8(n)), |s| {
+                s.parse::<usize>()
+            }),
+        )(s)
+    }
+
+    fn sized(begin: char, end: char) -> impl Fn(&[u8]) -> IResult<&[u8], &[u8]> {
+        move |s: &[u8]| {
+            // This is the point where we check the descriminator;
+            // if the beginning char does not match, we can immediately return.
+            let (s, _) = char(begin)(s)?;
+            let (s, (len, _)) = tuple((usize_t, char(':')))(s)?;
+            let (s, (res, _)) = tuple((take(len), char(end)))(s)?;
+            Ok((s, res))
+        }
+    }
+
+    fn uint_t<'a, I: FromStr + 'a>(t: &'static str) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], I> {
+        move |s: &'a [u8]| {
+            let (s, (_, _, int, _)) = tuple((
+                tag(t.as_bytes()),
+                char(':'),
+                map_res(map_res(digit1, |n: &[u8]| std::str::from_utf8(n)), |s| {
+                    s.parse::<I>()
+                }),
+                char(','),
+            ))(s)?;
+            Ok((s, int))
+        }
+    }
+
+    fn bool_t<'a>() -> impl Fn(&'a [u8]) -> IResult<&'a [u8], bool> {
+        context(
+            "bool",
+            alt((map(tag("n1:0,"), |_| false), map(tag("n1:1,"), |_| true))),
+        )
+    }
+
+    fn int_t<'a, I: FromStr + Neg<Output = I>>(
+        t: &'static str,
+    ) -> impl Fn(&'a [u8]) -> IResult<&[u8], I> {
+        context(t, move |s: &'a [u8]| {
+            let (s, (_, _, neg, int, _)) = tuple((
+                tag(t.as_bytes()),
+                char(':'),
+                opt(char('-')),
+                map_res(map_res(digit1, |n: &[u8]| std::str::from_utf8(n)), |s| {
+                    s.parse::<I>()
+                }),
+                char(','),
+            ))(s)?;
+            let res = match neg {
+                Some(_) => -int,
+                None => int,
+            };
+            Ok((s, res))
+        })
+    }
+
+    fn tag_t(s: &[u8]) -> IResult<&[u8], Tag<String, T>> {
+        // recurses into the main parser
+        map(tag_g(t_t), |Tag { tag, val }| Tag {
+            tag: tag.to_string(),
+            val,
+        })(s)
+    }
+
+    fn tag_g<'a, P, O>(inner: P) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Tag<&'a str, O>>
+    where
+        P: Fn(&'a [u8]) -> IResult<&'a [u8], O>,
+    {
+        move |s: &[u8]| {
+            let (s, tag) = sized('<', '|')(s)?;
+            let (s, val) = inner(s)?;
+            Ok((
+                s,
+                Tag {
+                    tag: std::str::from_utf8(tag)
+                        .map_err(|_| nom::Err::Failure((s, ErrorKind::Char)))?,
+                    val: Box::new(val),
+                },
+            ))
+        }
+    }
+
+    /// parse text scalar (`t5:hello,`)
+    fn text(s: &[u8]) -> IResult<&[u8], T> {
+        let (s, res) = text_g(s)?;
+        Ok((s, T::Text(res.to_string())))
+    }
+
+    fn text_g(s: &[u8]) -> IResult<&[u8], &str> {
+        let (s, res) = sized('t', ',')(s)?;
+        Ok((
+            s,
+            std::str::from_utf8(res).map_err(|_| nom::Err::Failure((s, ErrorKind::Char)))?,
+        ))
+    }
+
+    fn binary<'a>() -> impl Fn(&'a [u8]) -> IResult<&'a [u8], T> {
+        map(binary_g(), |b| T::Binary(b.to_owned()))
+    }
+
+    fn binary_g() -> impl Fn(&[u8]) -> IResult<&[u8], &[u8]> {
+        sized('b', ',')
+    }
+
+    fn list_t(s: &[u8]) -> IResult<&[u8], Vec<T>> {
+        list_g(t_t)(s)
+    }
+
+    /// Wrap the inner parser of an `many0`/`fold_many0`, so that the parser
+    /// is not called when the `s` is empty already, preventing it from
+    /// returning `Incomplete` on streaming parsing.
+    fn inner_no_empty_string<'a, P, O>(inner: P) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], O>
+    where
+        O: Clone,
+        P: Fn(&'a [u8]) -> IResult<&'a [u8], O>,
+    {
+        move |s: &'a [u8]| {
+            if s.is_empty() {
+                // This is a bit hacky, `many0` considers the inside done
+                // when a parser returns `Err::Error`, ignoring the actual error content
+                Err(nom::Err::Error((s, nom::error::ErrorKind::Many0)))
+            } else {
+                inner(s)
+            }
+        }
+    }
+
+    fn list_g<'a, P, O>(inner: P) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Vec<O>>
+    where
+        O: Clone,
+        P: Fn(&'a [u8]) -> IResult<&'a [u8], O>,
+    {
+        map_parser(
+            sized('[', ']'),
+            nom::multi::many0(inner_no_empty_string(inner)),
+        )
+    }
+
+    fn record_t<'a>(s: &'a [u8]) -> IResult<&'a [u8], HashMap<String, T>> {
+        let (s, r) = record_g(t_t)(s)?;
+        Ok((
+            s,
+            r.into_iter()
+                .map(|(k, v)| (k.to_string(), v))
+                .collect::<HashMap<_, _>>(),
+        ))
+    }
+
+    fn record_g<'a, P, O>(inner: P) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], HashMap<&'a str, O>>
+    where
+        O: Clone,
+        P: Fn(&'a [u8]) -> IResult<&'a [u8], O>,
+    {
+        move |s: &'a [u8]| {
+            let (s, map) = map_parser(
+                sized('{', '}'),
+                nom::multi::fold_many0(
+                    inner_no_empty_string(tag_g(&inner)),
+                    HashMap::new(),
+                    |mut acc: HashMap<_, _>, Tag { tag, mut val }| {
+                        // ignore earlier tags with the same name
+                        // according to netencode spec
+                        let _ = acc.insert(tag, *val);
+                        acc
+                    },
+                ),
+            )(s)?;
+            if map.is_empty() {
+                // records must not be empty, according to the spec
+                Err(nom::Err::Failure((s, nom::error::ErrorKind::Many1)))
+            } else {
+                Ok((s, map))
+            }
+        }
+    }
+
+    pub fn u_u(s: &[u8]) -> IResult<&[u8], U> {
+        alt((
+            map(text_g, U::Text),
+            map(binary_g(), U::Binary),
+            map(unit_t, |()| U::Unit),
+            map(tag_g(u_u), |t| U::Sum(t)),
+            map(list_g(u_u), U::List),
+            map(record_g(u_u), U::Record),
+            map(bool_t(), |u| U::N1(u)),
+            map(uint_t("n3"), |u| U::N3(u)),
+            map(uint_t("n6"), |u| U::N6(u)),
+            map(uint_t("n7"), |u| U::N7(u)),
+            map(int_t("i3"), |u| U::I3(u)),
+            map(int_t("i6"), |u| U::I6(u)),
+            map(int_t("i7"), |u| U::I7(u)),
+            // less common
+            map(uint_t("n2"), |u| U::N3(u)),
+            map(uint_t("n4"), |u| U::N6(u)),
+            map(uint_t("n5"), |u| U::N6(u)),
+            map(int_t("i1"), |u| U::I3(u)),
+            map(int_t("i2"), |u| U::I3(u)),
+            map(int_t("i4"), |u| U::I6(u)),
+            map(int_t("i5"), |u| U::I6(u)),
+            // TODO: 8, 9 not supported
+        ))(s)
+    }
+
+    pub fn t_t(s: &[u8]) -> IResult<&[u8], T> {
+        alt((
+            text,
+            binary(),
+            map(unit_t, |_| T::Unit),
+            map(tag_t, |t| T::Sum(t)),
+            map(list_t, |l| T::List(l)),
+            map(record_t, |p| T::Record(p)),
+            map(bool_t(), |u| T::N1(u)),
+            // 8, 64 and 128 bit
+            map(uint_t("n3"), |u| T::N3(u)),
+            map(uint_t("n6"), |u| T::N6(u)),
+            map(uint_t("n7"), |u| T::N7(u)),
+            map(int_t("i3"), |u| T::I3(u)),
+            map(int_t("i6"), |u| T::I6(u)),
+            map(int_t("i7"), |u| T::I7(u)),
+            // less common
+            map(uint_t("n2"), |u| T::N3(u)),
+            map(uint_t("n4"), |u| T::N6(u)),
+            map(uint_t("n5"), |u| T::N6(u)),
+            map(int_t("i1"), |u| T::I3(u)),
+            map(int_t("i2"), |u| T::I3(u)),
+            map(int_t("i4"), |u| T::I6(u)),
+            map(int_t("i5"), |u| T::I6(u)),
+            // TODO: 8, 9 not supported
+        ))(s)
+    }
+
+    #[cfg(test)]
+    mod tests {
+        use super::*;
+
+        #[test]
+        fn test_parse_unit_t() {
+            assert_eq!(unit_t("u,".as_bytes()), Ok(("".as_bytes(), ())));
+        }
+
+        #[test]
+        fn test_parse_bool_t() {
+            assert_eq!(bool_t()("n1:0,".as_bytes()), Ok(("".as_bytes(), false)));
+            assert_eq!(bool_t()("n1:1,".as_bytes()), Ok(("".as_bytes(), true)));
+        }
+
+        #[test]
+        fn test_parse_usize_t() {
+            assert_eq!(usize_t("32foo".as_bytes()), Ok(("foo".as_bytes(), 32)));
+        }
+
+        #[test]
+        fn test_parse_int_t() {
+            assert_eq!(
+                uint_t::<u8>("n3")("n3:42,abc".as_bytes()),
+                Ok(("abc".as_bytes(), 42))
+            );
+            assert_eq!(
+                uint_t::<u8>("n3")("n3:1024,abc".as_bytes()),
+                Err(nom::Err::Error((
+                    "1024,abc".as_bytes(),
+                    nom::error::ErrorKind::MapRes
+                )))
+            );
+            assert_eq!(
+                int_t::<i64>("i6")("i6:-23,abc".as_bytes()),
+                Ok(("abc".as_bytes(), -23))
+            );
+            assert_eq!(
+                int_t::<i128>("i3")("i3:0,:abc".as_bytes()),
+                Ok((":abc".as_bytes(), 0))
+            );
+            assert_eq!(
+                uint_t::<u8>("n7")("n7:09,".as_bytes()),
+                Ok(("".as_bytes(), 9))
+            );
+            // assert_eq!(
+            //     length("c"),
+            //     Err(nom::Err::Error(("c", nom::error::ErrorKind::Digit)))
+            // );
+            // assert_eq!(
+            //     length(":"),
+            //     Err(nom::Err::Error((":", nom::error::ErrorKind::Digit)))
+            // );
+        }
+
+        #[test]
+        fn test_parse_text() {
+            assert_eq!(
+                text("t5:hello,".as_bytes()),
+                Ok(("".as_bytes(), T::Text("hello".to_owned()))),
+                "{}",
+                r"t5:hello,"
+            );
+            assert_eq!(
+                text("t4:fo".as_bytes()),
+                // The content of the text should be 4 long
+                Err(nom::Err::Incomplete(nom::Needed::Size(4))),
+                "{}",
+                r"t4:fo,"
+            );
+            assert_eq!(
+                text("t9:今日は,".as_bytes()),
+                Ok(("".as_bytes(), T::Text("今日は".to_owned()))),
+                "{}",
+                r"t9:今日は,"
+            );
+        }
+
+        #[test]
+        fn test_parse_binary() {
+            assert_eq!(
+                binary()("b5:hello,".as_bytes()),
+                Ok(("".as_bytes(), T::Binary(Vec::from("hello".to_owned())))),
+                "{}",
+                r"b5:hello,"
+            );
+            assert_eq!(
+                binary()("b4:fo".as_bytes()),
+                // The content of the byte should be 4 long
+                Err(nom::Err::Incomplete(nom::Needed::Size(4))),
+                "{}",
+                r"b4:fo,"
+            );
+            assert_eq!(
+                binary()("b4:foob".as_bytes()),
+                // The content is 4 bytes now, but the finishing , is missing
+                Err(nom::Err::Incomplete(nom::Needed::Size(1))),
+                "{}",
+                r"b4:fo,"
+            );
+            assert_eq!(
+                binary()("b9:今日は,".as_bytes()),
+                Ok(("".as_bytes(), T::Binary(Vec::from("今日は".as_bytes())))),
+                "{}",
+                r"b9:今日は,"
+            );
+        }
+
+        #[test]
+        fn test_list() {
+            assert_eq!(
+                list_t("[0:]".as_bytes()),
+                Ok(("".as_bytes(), vec![])),
+                "{}",
+                r"[0:]"
+            );
+            assert_eq!(
+                list_t("[6:u,u,u,]".as_bytes()),
+                Ok(("".as_bytes(), vec![T::Unit, T::Unit, T::Unit,])),
+                "{}",
+                r"[6:u,u,u,]"
+            );
+            assert_eq!(
+                list_t("[15:u,[7:t3:foo,]u,]".as_bytes()),
+                Ok((
+                    "".as_bytes(),
+                    vec![T::Unit, T::List(vec![T::Text("foo".to_owned())]), T::Unit,]
+                )),
+                "{}",
+                r"[15:u,[7:t3:foo,]u,]"
+            );
+        }
+
+        #[test]
+        fn test_record() {
+            assert_eq!(
+                record_t("{21:<1:a|u,<1:b|u,<1:c|u,}".as_bytes()),
+                Ok((
+                    "".as_bytes(),
+                    vec![
+                        ("a".to_owned(), T::Unit),
+                        ("b".to_owned(), T::Unit),
+                        ("c".to_owned(), T::Unit),
+                    ]
+                    .into_iter()
+                    .collect::<HashMap<String, T>>()
+                )),
+                "{}",
+                r"{21:<1:a|u,<1:b|u,<1:c|u,}"
+            );
+            // duplicated keys are ignored (first is taken)
+            assert_eq!(
+                record_t("{25:<1:a|u,<1:b|u,<1:a|i1:-1,}".as_bytes()),
+                Ok((
+                    "".as_bytes(),
+                    vec![("a".to_owned(), T::I3(-1)), ("b".to_owned(), T::Unit),]
+                        .into_iter()
+                        .collect::<HashMap<_, _>>()
+                )),
+                "{}",
+                r"{25:<1:a|u,<1:b|u,<1:a|i1:-1,}"
+            );
+            // empty records are not allowed
+            assert_eq!(
+                record_t("{0:}".as_bytes()),
+                Err(nom::Err::Failure((
+                    "".as_bytes(),
+                    nom::error::ErrorKind::Many1
+                ))),
+                "{}",
+                r"{0:}"
+            );
+        }
+
+        #[test]
+        fn test_parse() {
+            assert_eq!(
+                t_t("n3:255,".as_bytes()),
+                Ok(("".as_bytes(), T::N3(255))),
+                "{}",
+                r"n3:255,"
+            );
+            assert_eq!(
+                t_t("t6:halloo,".as_bytes()),
+                Ok(("".as_bytes(), T::Text("halloo".to_owned()))),
+                "{}",
+                r"t6:halloo,"
+            );
+            assert_eq!(
+                t_t("<3:foo|t6:halloo,".as_bytes()),
+                Ok((
+                    "".as_bytes(),
+                    T::Sum(Tag {
+                        tag: "foo".to_owned(),
+                        val: Box::new(T::Text("halloo".to_owned()))
+                    })
+                )),
+                "{}",
+                r"<3:foo|t6:halloo,"
+            );
+            // { a: Unit
+            // , foo: List <A: Unit | B: List i3> }
+            assert_eq!(
+                t_t("{52:<1:a|u,<3:foo|[33:<1:A|u,<1:A|n1:1,<1:B|[7:i3:127,]]}".as_bytes()),
+                Ok((
+                    "".as_bytes(),
+                    T::Record(
+                        vec![
+                            ("a".to_owned(), T::Unit),
+                            (
+                                "foo".to_owned(),
+                                T::List(vec![
+                                    T::Sum(Tag {
+                                        tag: "A".to_owned(),
+                                        val: Box::new(T::Unit)
+                                    }),
+                                    T::Sum(Tag {
+                                        tag: "A".to_owned(),
+                                        val: Box::new(T::N1(true))
+                                    }),
+                                    T::Sum(Tag {
+                                        tag: "B".to_owned(),
+                                        val: Box::new(T::List(vec![T::I3(127)]))
+                                    }),
+                                ])
+                            )
+                        ]
+                        .into_iter()
+                        .collect::<HashMap<String, T>>()
+                    )
+                )),
+                "{}",
+                r"{52:<1:a|u,<3:foo|[33:<1:A|u,<1:A|n1:1,<1:B|[7:i3:127,]]}"
+            );
+        }
+    }
+}
+
+pub mod dec {
+    use super::*;
+    use std::collections::HashMap;
+
+    pub struct DecodeError(pub String);
+
+    pub trait Decoder<'a> {
+        type A;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError>;
+    }
+
+    /// Any netencode, as `T`.
+    #[derive(Clone, Copy)]
+    pub struct AnyT;
+    /// Any netencode, as `U`.
+    #[derive(Clone, Copy)]
+    pub struct AnyU;
+
+    impl<'a> Decoder<'a> for AnyT {
+        type A = T;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            Ok(u.to_t())
+        }
+    }
+
+    impl<'a> Decoder<'a> for AnyU {
+        type A = U<'a>;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            Ok(u)
+        }
+    }
+
+    /// A text
+    #[derive(Clone, Copy)]
+    pub struct Text;
+
+    /// A bytestring
+    // TODO: rename to Bytes
+    #[derive(Clone, Copy)]
+    pub struct Binary;
+
+    impl<'a> Decoder<'a> for Text {
+        type A = &'a str;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match u {
+                U::Text(t) => Ok(t),
+                other => Err(DecodeError(format!("Cannot decode {:?} into Text", other))),
+            }
+        }
+    }
+
+    impl<'a> Decoder<'a> for Binary {
+        type A = &'a [u8];
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match u {
+                U::Binary(b) => Ok(b),
+                other => Err(DecodeError(format!(
+                    "Cannot decode {:?} into Binary",
+                    other
+                ))),
+            }
+        }
+    }
+
+    /// Any scalar, converted to bytes.
+    #[derive(Clone, Copy)]
+    pub struct ScalarAsBytes;
+
+    impl<'a> Decoder<'a> for ScalarAsBytes {
+        type A = Vec<u8>;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match u {
+                U::N3(u) => Ok(format!("{}", u).into_bytes()),
+                U::N6(u) => Ok(format!("{}", u).into_bytes()),
+                U::N7(u) => Ok(format!("{}", u).into_bytes()),
+                U::I3(i) => Ok(format!("{}", i).into_bytes()),
+                U::I6(i) => Ok(format!("{}", i).into_bytes()),
+                U::I7(i) => Ok(format!("{}", i).into_bytes()),
+                U::Text(t) => Ok(t.as_bytes().to_owned()),
+                U::Binary(b) => Ok(b.to_owned()),
+                o => Err(DecodeError(format!("Cannot decode {:?} into scalar", o))),
+            }
+        }
+    }
+
+    /// A map of Ts (TODO: rename to map)
+    #[derive(Clone, Copy)]
+    pub struct Record<T>(pub T);
+
+    impl<'a, Inner> Decoder<'a> for Record<Inner>
+    where
+        Inner: Decoder<'a>,
+    {
+        type A = HashMap<&'a str, Inner::A>;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match u {
+                U::Record(map) => map
+                    .into_iter()
+                    .map(|(k, v)| self.0.dec(v).map(|v2| (k, v2)))
+                    .collect::<Result<Self::A, _>>(),
+                o => Err(DecodeError(format!("Cannot decode {:?} into record", o))),
+            }
+        }
+    }
+
+    /// Assume a record and project out the field with the given name and type.
+    #[derive(Clone, Copy)]
+    pub struct RecordDot<'a, T> {
+        pub field: &'a str,
+        pub inner: T,
+    }
+
+    impl<'a, Inner> Decoder<'a> for RecordDot<'_, Inner>
+    where
+        Inner: Decoder<'a> + Clone,
+    {
+        type A = Inner::A;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match Record(self.inner.clone()).dec(u) {
+                Ok(mut map) => match map.remove(self.field) {
+                    Some(inner) => Ok(inner),
+                    None => Err(DecodeError(format!(
+                        "Cannot find `{}` in record map",
+                        self.field
+                    ))),
+                },
+                Err(err) => Err(err),
+            }
+        }
+    }
+
+    /// Equals one of the listed `A`s exactly, after decoding.
+    #[derive(Clone)]
+    pub struct OneOf<T, A> {
+        pub inner: T,
+        pub list: Vec<A>,
+    }
+
+    impl<'a, Inner> Decoder<'a> for OneOf<Inner, Inner::A>
+    where
+        Inner: Decoder<'a>,
+        Inner::A: Display + Debug + PartialEq,
+    {
+        type A = Inner::A;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match self.inner.dec(u) {
+                Ok(inner) => match self.list.iter().any(|x| x.eq(&inner)) {
+                    true => Ok(inner),
+                    false => Err(DecodeError(format!(
+                        "{} is not one of {:?}",
+                        inner, self.list
+                    ))),
+                },
+                Err(err) => Err(err),
+            }
+        }
+    }
+
+    /// Try decoding as `T`.
+    #[derive(Clone)]
+    pub struct Try<T>(pub T);
+
+    impl<'a, Inner> Decoder<'a> for Try<Inner>
+    where
+        Inner: Decoder<'a>,
+    {
+        type A = Option<Inner::A>;
+        fn dec(&self, u: U<'a>) -> Result<Self::A, DecodeError> {
+            match self.0.dec(u) {
+                Ok(inner) => Ok(Some(inner)),
+                Err(err) => Ok(None),
+            }
+        }
+    }
+}
diff --git a/users/Profpatsch/netencode/pretty.rs b/users/Profpatsch/netencode/pretty.rs
new file mode 100644
index 0000000000..935c3d4a8a
--- /dev/null
+++ b/users/Profpatsch/netencode/pretty.rs
@@ -0,0 +1,163 @@
+extern crate netencode;
+
+use netencode::{Tag, T, U};
+
+pub enum Pretty {
+    Single {
+        r#type: char,
+        length: String,
+        val: String,
+        trailer: char,
+    },
+    Tag {
+        r#type: char,
+        length: String,
+        key: String,
+        inner: char,
+        val: Box<Pretty>,
+    },
+    Multi {
+        r#type: char,
+        length: String,
+        vals: Vec<Pretty>,
+        trailer: char,
+    },
+}
+
+impl Pretty {
+    pub fn from_u<'a>(u: U<'a>) -> Pretty {
+        match u {
+            U::Unit => Self::scalar('u', "", ""),
+            U::N1(b) => Self::scalar('n', "1:", if b { "1" } else { "0" }),
+            U::N3(n) => Self::scalar('n', "3:", n),
+            U::N6(n) => Self::scalar('n', "6:", n),
+            U::N7(n) => Self::scalar('n', "7:", n),
+            U::I3(i) => Self::scalar('i', "3:", i),
+            U::I6(i) => Self::scalar('i', "6:", i),
+            U::I7(i) => Self::scalar('i', "7:", i),
+            U::Text(s) => Pretty::Single {
+                r#type: 't',
+                length: format!("{}:", s.len()),
+                val: s.to_string(),
+                trailer: ',',
+            },
+            U::Binary(s) => Pretty::Single {
+                r#type: 'b',
+                length: format!("{}:", s.len()),
+                // For pretty printing we want the string to be visible obviously.
+                // Instead of not supporting binary, let’s use lossy conversion.
+                val: String::from_utf8_lossy(s).into_owned(),
+                trailer: ',',
+            },
+            U::Sum(Tag { tag, val }) => Self::pretty_tag(tag, Self::from_u(*val)),
+            U::Record(m) => Pretty::Multi {
+                r#type: '{',
+                // TODO: we are losing the size here, should we recompute it? Keep it?
+                length: String::from(""),
+                vals: m
+                    .into_iter()
+                    .map(|(k, v)| Self::pretty_tag(k, Self::from_u(v)))
+                    .collect(),
+                trailer: '}',
+            },
+            U::List(l) => Pretty::Multi {
+                r#type: '[',
+                // TODO: we are losing the size here, should we recompute it? Keep it?
+                length: String::from(""),
+                vals: l.into_iter().map(|v| Self::from_u(v)).collect(),
+                trailer: ']',
+            },
+        }
+    }
+
+    fn scalar<D>(r#type: char, length: &str, d: D) -> Pretty
+    where
+        D: std::fmt::Display,
+    {
+        Pretty::Single {
+            r#type,
+            length: length.to_string(),
+            val: format!("{}", d),
+            trailer: ',',
+        }
+    }
+
+    fn pretty_tag(tag: &str, val: Pretty) -> Pretty {
+        Pretty::Tag {
+            r#type: '<',
+            length: format!("{}:", tag.len()),
+            key: tag.to_string(),
+            inner: '|',
+            val: Box::new(val),
+        }
+    }
+
+    pub fn print_multiline<W>(&self, mut w: &mut W) -> std::io::Result<()>
+    where
+        W: std::io::Write,
+    {
+        Self::go(&mut w, self, 0, true);
+        write!(w, "\n")
+    }
+
+    fn go<W>(mut w: &mut W, p: &Pretty, depth: usize, is_newline: bool) -> std::io::Result<()>
+    where
+        W: std::io::Write,
+    {
+        const full: usize = 4;
+        const half: usize = 2;
+        let i = &vec![b' '; depth * full];
+        let iandhalf = &vec![b' '; depth * full + half];
+        let (i, iandhalf) = unsafe {
+            (
+                std::str::from_utf8_unchecked(i),
+                std::str::from_utf8_unchecked(iandhalf),
+            )
+        };
+        if is_newline {
+            write!(&mut w, "{}", i);
+        }
+        match p {
+            Pretty::Single {
+                r#type,
+                length,
+                val,
+                trailer,
+            } => write!(&mut w, "{} {}{}", r#type, val, trailer),
+            Pretty::Tag {
+                r#type,
+                length,
+                key,
+                inner,
+                val,
+            } => {
+                write!(&mut w, "{} {} {}", r#type, key, inner)?;
+                Self::go::<W>(&mut w, val, depth, false)
+            }
+            // if the length is 0 or 1, we print on one line,
+            // only if there’s more than one element we split the resulting value.
+            // we never break lines on arbitrary column sizes, since that is just silly.
+            Pretty::Multi {
+                r#type,
+                length,
+                vals,
+                trailer,
+            } => match vals.len() {
+                0 => write!(&mut w, "{} {}", r#type, trailer),
+                1 => {
+                    write!(&mut w, "{} ", r#type);
+                    Self::go::<W>(&mut w, &vals[0], depth, false)?;
+                    write!(&mut w, "{}", trailer)
+                }
+                more => {
+                    write!(&mut w, "\n{}{} \n", iandhalf, r#type)?;
+                    for v in vals {
+                        Self::go::<W>(&mut w, v, depth + 1, true)?;
+                        write!(&mut w, "\n")?;
+                    }
+                    write!(&mut w, "{}{}", iandhalf, trailer)
+                }
+            },
+        }
+    }
+}
diff --git a/users/Profpatsch/netstring/README.md b/users/Profpatsch/netstring/README.md
new file mode 100644
index 0000000000..b8daea11d1
--- /dev/null
+++ b/users/Profpatsch/netstring/README.md
@@ -0,0 +1,18 @@
+# Netstring
+
+Netstrings are a djb invention. They are intended as a serialization format. Instead of inline control characters like `\n` or `\0` to signal the end of a string, they use a run-length encoding given as the number of bytes, encoded in ASCII, at the beginning of the string.
+
+```
+hello -> 5:hello,
+foo! -> 4:foo!,
+こんにちは -> 15:こんにちは,
+```
+
+They can be used to encode e.g. lists by simply concatenating and reading them in one-by-one.
+
+If you need a more complex encoding, you could start encoding e.g. tuples as netstrings-in-netstrings, or you could use [`netencode`](../netencode/README.md) instead, which is what-if-json-but-netstrings, and takes the idea of netstrings to their logical conclusion.
+
+Resources:
+
+Spec: http://cr.yp.to/proto/netstrings.txt
+Wiki: https://en.wikipedia.org/wiki/Netstring
diff --git a/users/Profpatsch/netstring/default.nix b/users/Profpatsch/netstring/default.nix
new file mode 100644
index 0000000000..e85cf24dd8
--- /dev/null
+++ b/users/Profpatsch/netstring/default.nix
@@ -0,0 +1,69 @@
+{ lib, pkgs, depot, ... }:
+let
+  toNetstring = s:
+    "${toString (builtins.stringLength s)}:${s},";
+
+  toNetstringList = xs:
+    lib.concatStrings (map toNetstring xs);
+
+  toNetstringKeyVal = attrs:
+    lib.concatStrings
+      (lib.mapAttrsToList
+        (k: v: toNetstring (toNetstring k + toNetstring v))
+        attrs);
+
+  python-netstring = depot.users.Profpatsch.writers.python3Lib
+    {
+      name = "netstring";
+    } ''
+    def read_netstring(bytes):
+        (int_length, rest) = bytes.split(sep=b':', maxsplit=1)
+        val = rest[:int(int_length)]
+        # has to end on a ,
+        assert(rest[len(val)] == ord(','))
+        return (val, rest[len(val) + 1:])
+
+    def read_netstring_key_val(bytes):
+        (keyvalnet, rest) = read_netstring(bytes)
+        (key, valnet) = read_netstring(keyvalnet)
+        (val, nothing) = read_netstring(valnet)
+        assert(nothing == b"")
+        return (key, val, rest)
+
+    def read_netstring_key_val_list(bytes):
+        rest = bytes
+        res = {}
+        while rest != b"":
+            (key, val, r) = read_netstring_key_val(rest)
+            rest = r
+            res[key] = val
+        return res
+  '';
+
+  rust-netstring = depot.nix.writers.rustSimpleLib
+    {
+      name = "netstring";
+    } ''
+    pub fn to_netstring(s: &[u8]) -> Vec<u8> {
+        let len = s.len();
+        // length of the integer as ascii
+        let i_len = ((len as f64).log10() as usize) + 1;
+        let ns_len = i_len + 1 + len + 1;
+        let mut res = Vec::with_capacity(ns_len);
+        res.extend_from_slice(format!("{}:", len).as_bytes());
+        res.extend_from_slice(s);
+        res.push(b',');
+        res
+    }
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    toNetstring
+    toNetstringList
+    toNetstringKeyVal
+    python-netstring
+    rust-netstring
+    ;
+}
diff --git a/users/Profpatsch/netstring/tests/default.nix b/users/Profpatsch/netstring/tests/default.nix
new file mode 100644
index 0000000000..6a1062988f
--- /dev/null
+++ b/users/Profpatsch/netstring/tests/default.nix
@@ -0,0 +1,64 @@
+{ depot, lib, pkgs, ... }:
+
+let
+
+  python-netstring-test = depot.users.Profpatsch.writers.python3
+    {
+      name = "python-netstring-test";
+      libraries = p: [
+        depot.users.Profpatsch.netstring.python-netstring
+      ];
+    } ''
+    import netstring
+
+    def assEq(left, right):
+      assert left == right, "{} /= {}".format(str(left), str(right))
+
+    assEq(
+      netstring.read_netstring(b"""${depot.nix.netstring.fromString "hi!"}"""),
+      (b"hi!", b"")
+    )
+
+    assEq(
+      netstring.read_netstring_key_val(
+        b"""${depot.nix.netstring.attrsToKeyValList { foo = "42"; }}"""
+      ),
+      (b'foo', b'42', b"")
+    )
+
+    assEq(
+      netstring.read_netstring_key_val_list(
+        b"""${depot.nix.netstring.attrsToKeyValList { foo = "42"; bar = "hi"; }}"""
+      ),
+      { b'foo': b'42', b'bar': b'hi' }
+    )
+  '';
+
+  rust-netstring-test = depot.nix.writers.rustSimple
+    {
+      name = "rust-netstring-test";
+      dependencies = [
+        depot.users.Profpatsch.netstring.rust-netstring
+      ];
+    } ''
+    extern crate netstring;
+
+    fn main() {
+      assert_eq!(
+        std::str::from_utf8(&netstring::to_netstring(b"hello")).unwrap(),
+        r##"${depot.nix.netstring.fromString "hello"}"##
+      );
+      assert_eq!(
+        std::str::from_utf8(&netstring::to_netstring("こんにちは".as_bytes())).unwrap(),
+        r##"${depot.nix.netstring.fromString "こんにちは"}"##
+      );
+    }
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    python-netstring-test
+    rust-netstring-test
+    ;
+}
diff --git a/users/Profpatsch/nix-home/default.nix b/users/Profpatsch/nix-home/default.nix
new file mode 100644
index 0000000000..3f0b7c9c39
--- /dev/null
+++ b/users/Profpatsch/nix-home/default.nix
@@ -0,0 +1,203 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.stow [ "stow" ]
+    // depot.nix.getBins pkgs.coreutils [ "mkdir" "ln" "printenv" "rm" ]
+    // depot.nix.getBins pkgs.xe [ "xe" ]
+    // depot.nix.getBins pkgs.lr [ "lr" ]
+    // depot.nix.getBins pkgs.nix [ "nix-store" ]
+  ;
+
+  # run stow to populate the target directory with the given stow package, read from stowDir.
+  # Bear in mind that `stowDirOriginPath` should always be semantically bound to the given `stowDir`, otherwise stow might become rather confused.
+  runStow =
+    {
+      # “stow package” to stow (see manpage)
+      # TODO: allow this function to un-stow multiple packages!
+      stowPackage
+    , # “target directory” to stow in (see manpage)
+      targetDir
+    , # The “stow directory” (see manpage), containing “stow packages” (see manpage)
+      stowDir
+    , # representative directory for the stowDir in the file system, against which stow will create relative links.
+      # ATTN: this is always overwritten with the contents of `stowDir`! You shouldn’t re-use the same `stowDirOriginPath` for different `stowDir`s, otherwise there might be surprises.
+      stowDirOriginPath
+    ,
+    }: depot.nix.writeExecline "stow-${stowPackage}" { } [
+      # first, create a temporary stow directory to use as source
+      # (stow will use it to determine the origin of files)
+      "if"
+      [ bins.mkdir "-p" stowDirOriginPath ]
+      # remove old symlinks
+      "if"
+      [
+        "pipeline"
+        [
+          bins.lr
+          "-0"
+          "-t"
+          "depth == 1 && type == l"
+          stowDirOriginPath
+        ]
+        bins.xe
+        "-0"
+        bins.rm
+      ]
+      # create an indirect gc root so our config is not cleaned under our asses by a garbage collect
+      "if"
+      [
+        bins.nix-store
+        "--realise"
+        "--indirect"
+        "--add-root"
+        "${stowDirOriginPath}/.nix-stowdir-gc-root"
+        stowDir
+      ]
+      # populate with new stow targets
+      "if"
+      [
+        "elglob"
+        "-w0"
+        "stowPackages"
+        "${stowDir}/*"
+        bins.ln
+        "--force"
+        "-st"
+        stowDirOriginPath
+        "$stowPackages"
+      ]
+      # stow always looks for $HOME/.stowrc to read more arguments
+      "export"
+      "HOME"
+      "/homeless-shelter"
+      bins.stow
+      # always run restow for now; this does more stat but will remove stale links
+      "--restow"
+      "--dir"
+      stowDirOriginPath
+      "--target"
+      targetDir
+      stowPackage
+    ];
+
+  # create a stow dir from a list of drv paths and a stow package name.
+  makeStowDir =
+    (with depot.nix.yants;
+    defun
+      [
+        (list (struct {
+          originalDir = drv;
+          stowPackage = string;
+        }))
+        drv
+      ])
+      (dirs:
+        depot.nix.runExecline "make-stow-dir"
+          {
+            stdin = lib.pipe dirs [
+              (map depot.users.Profpatsch.netencode.gen.dwim)
+              depot.users.Profpatsch.netstring.toNetstringList
+            ];
+          } [
+          "importas"
+          "out"
+          "out"
+          "if"
+          [ bins.mkdir "-p" "$out" ]
+          "forstdin"
+          "-d"
+          ""
+          "-o"
+          "0"
+          "line"
+          "pipeline"
+          [
+            depot.users.Profpatsch.execline.print-one-env
+            "line"
+          ]
+          depot.users.Profpatsch.netencode.record-splice-env
+          "importas"
+          "-ui"
+          "originalDir"
+          "originalDir"
+          "importas"
+          "-ui"
+          "stowPackage"
+          "stowPackage"
+          bins.ln
+          "-sT"
+          "$originalDir"
+          "\${out}/\${stowPackage}"
+        ]);
+
+  # this is a dumb way of generating a pure list of packages from a depot namespace.
+  readTreeNamespaceDrvs = namespace:
+    lib.pipe namespace [
+      (lib.filterAttrs (_: v: lib.isDerivation v))
+      (lib.mapAttrsToList (k: v: {
+        name = k;
+        drv = v;
+      }))
+    ];
+
+  scriptsStow =
+    lib.pipe { } [
+      (_: makeStowDir [{
+        stowPackage = "scripts";
+        originalDir = pkgs.linkFarm "scripts-farm"
+          ([
+            {
+              name = "scripts/ytextr";
+              path = depot.users.Profpatsch.ytextr;
+            }
+          ]
+          ++
+          (lib.pipe depot.users.Profpatsch.aliases [
+            readTreeNamespaceDrvs
+            (map ({ name, drv }: {
+              name = "scripts/${name}";
+              path = drv;
+            }))
+          ]));
+      }])
+      (d: runStow {
+        stowDir = d;
+        stowPackage = "scripts";
+        targetDir = "/home/philip";
+        stowDirOriginPath = "/home/philip/.local/share/nix-home/stow-origin";
+      })
+    ];
+
+
+
+  terminalEmulatorStow =
+    lib.pipe { } [
+      (_: makeStowDir [{
+        stowPackage = "terminal-emulator";
+        originalDir = pkgs.linkFarm "terminal-emulator-farm"
+          ([
+            {
+              name = "bin/terminal-emulator";
+              path = depot.users.Profpatsch.alacritty;
+            }
+          ]);
+
+      }])
+      (d: runStow {
+        stowDir = d;
+        stowPackage = "terminal-emulator";
+        targetDir = "/home/philip";
+        # TODO: this should only be done once, in a single runStow instead of multiple
+        stowDirOriginPath = "/home/philip/.local/share/nix-home/stow-origin-terminal-emulator";
+      })
+    ];
+
+in
+
+# TODO: run multiple stows with runStow?
+  # TODO: temp setup
+depot.nix.writeExecline "nix-home" { } [
+  "if"
+  [ scriptsStow ]
+  terminalEmulatorStow
+]
diff --git a/users/Profpatsch/nixpkgs-rewriter/MetaStdenvLib.hs b/users/Profpatsch/nixpkgs-rewriter/MetaStdenvLib.hs
new file mode 100644
index 0000000000..3ed96a7b6e
--- /dev/null
+++ b/users/Profpatsch/nixpkgs-rewriter/MetaStdenvLib.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE PartialTypeSignatures #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NamedFieldPuns #-}
+import Nix.Parser
+import Nix.Expr.Types
+import Nix.Expr.Types.Annotated
+import System.Environment (getArgs)
+import System.Exit (die)
+import Data.Fix (Fix(..))
+import qualified Data.Text as Text
+import qualified Data.ByteString.Lazy.Char8 as BL
+import qualified Data.Aeson as A
+import qualified Data.Aeson.Encoding as A
+import Data.Function ((&))
+import qualified System.IO as IO
+import qualified Text.Megaparsec.Pos as MP
+
+main = do
+  (nixFile:_) <- getArgs
+  (parseNixFileLoc nixFile :: IO _) >>= \case
+    Failure err -> do
+      ePutStrLn $ show err
+      die "oh no"
+    Success expr -> do
+      case snd $ match expr of
+        NoArguments -> do
+          ePutStrLn $ "NoArguments in " <> nixFile
+          printPairs mempty
+        YesLib vars -> do
+          ePutStrLn $ "lib in " <> show vars <> " in " <> nixFile
+          printPairs mempty
+        NoLib vars srcSpan -> do
+          ePutStrLn $ nixFile <> " needs lib added"
+          printPairs
+            $ "fileName" A..= nixFile
+            <> "fromLine" A..= (srcSpan & spanBegin & sourceLine)
+            <> "fromColumn" A..= (srcSpan & spanBegin & sourceColumn)
+            <> "toLine" A..= (srcSpan & spanEnd & sourceLine)
+            <> "toColumn" A..= (srcSpan & spanEnd & sourceColumn)
+
+printPairs pairs = BL.putStrLn $ A.encodingToLazyByteString $ A.pairs pairs
+
+ePutStrLn = IO.hPutStrLn IO.stderr
+
+data Descend = YesDesc | NoDesc
+  deriving Show
+data Matched =  NoArguments | NoLib [VarName] SrcSpan | YesLib [VarName]
+  deriving Show
+
+match :: Fix (Compose (Ann SrcSpan) NExprF) -> (Descend, Matched)
+match = \case
+  (AnnE outerSpan (NAbs (ParamSet params _ _) (AnnE innerSpan _))) -> (NoDesc,
+    let vars = map fst params in
+    case (any (== "lib") vars) of
+      True -> YesLib vars
+      False ->
+          -- The span of the arglist is from the beginning of the match
+          -- to the beginning of the inner expression
+          let varSpan = SrcSpan
+                { spanBegin = outerSpan & spanBegin
+                -- -1 to prevent the spans from overlapping
+                , spanEnd = sourcePosMinus1 (innerSpan & spanBegin) }
+          in NoLib vars varSpan)
+  _ -> (NoDesc, NoArguments)
+
+-- | Remove one from a source positon.
+--
+-- That means if the current position is at the very beginning of a line,
+-- jump to the previous line.
+sourcePosMinus1 :: SourcePos -> SourcePos
+sourcePosMinus1 src@(SourcePos { sourceLine, sourceColumn }) =
+  let
+    col = MP.mkPos $ max (MP.unPos sourceColumn - 1) 1
+    line = MP.mkPos $ case MP.unPos sourceColumn of
+      1 -> max (MP.unPos sourceLine - 1) 1
+      _ -> MP.unPos sourceLine
+  in src
+    { sourceLine = line
+    , sourceColumn = col }
diff --git a/users/Profpatsch/nixpkgs-rewriter/default.nix b/users/Profpatsch/nixpkgs-rewriter/default.nix
new file mode 100644
index 0000000000..0740a870aa
--- /dev/null
+++ b/users/Profpatsch/nixpkgs-rewriter/default.nix
@@ -0,0 +1,148 @@
+{ depot, pkgs, ... }:
+let
+  inherit (depot.nix)
+    writeExecline
+    ;
+  inherit (depot.users.Profpatsch.lib)
+    debugExec
+    ;
+
+  bins = depot.nix.getBins pkgs.coreutils [ "head" "shuf" ]
+    // depot.nix.getBins pkgs.jq [ "jq" ]
+    // depot.nix.getBins pkgs.findutils [ "xargs" ]
+    // depot.nix.getBins pkgs.gnused [ "sed" ]
+  ;
+
+  export-json-object = pkgs.writers.writePython3 "export-json-object" { } ''
+    import json
+    import sys
+    import os
+
+    d = json.load(sys.stdin)
+
+    if d == {}:
+        sys.exit(0)
+
+    for k, v in d.items():
+        os.environ[k] = str(v)
+
+    os.execvp(sys.argv[1], sys.argv[1:])
+  '';
+
+  meta-stdenv-lib = pkgs.writers.writeHaskell "meta-stdenv-lib"
+    {
+      libraries = [
+        pkgs.haskellPackages.hnix
+        pkgs.haskellPackages.aeson
+      ];
+    } ./MetaStdenvLib.hs;
+
+  replace-between-lines = writeExecline "replace-between-lines" { readNArgs = 1; } [
+    "importas"
+    "-ui"
+    "file"
+    "fileName"
+    "importas"
+    "-ui"
+    "from"
+    "fromLine"
+    "importas"
+    "-ui"
+    "to"
+    "toLine"
+    "if"
+    [ depot.tools.eprintf "%s-%s\n" "$from" "$to" ]
+    (debugExec "adding lib")
+    bins.sed
+    "-e"
+    "\${from},\${to} \${1}"
+    "-i"
+    "$file"
+  ];
+
+  add-lib-if-necessary = writeExecline "add-lib-if-necessary" { readNArgs = 1; } [
+    "pipeline"
+    [ meta-stdenv-lib "$1" ]
+    export-json-object
+    # first replace any stdenv.lib mentions in the arg header
+    # if this is not done, the replace below kills these.
+    # Since we want it anyway ultimately, let’s do it here.
+    "if"
+    [ replace-between-lines "s/stdenv\.lib/lib/" ]
+    # then add the lib argument
+    # (has to be before stdenv, otherwise default arguments might be in the way)
+    replace-between-lines
+    "s/stdenv/lib, stdenv/"
+  ];
+
+  metaString = ''meta = with stdenv.lib; {'';
+
+  replace-stdenv-lib = pkgs.writers.writeBash "replace-stdenv-lib" ''
+    set -euo pipefail
+    sourceDir="$1"
+    for file in $(
+      ${pkgs.ripgrep}/bin/rg \
+        --files-with-matches \
+        --fixed-strings \
+        -e '${metaString}' \
+        "$sourceDir"
+    )
+    do
+      echo "replacing stdenv.lib meta in $file" >&2
+      ${bins.sed} -e '/${metaString}/ s/stdenv.lib/lib/' \
+          -i "$file"
+      ${add-lib-if-necessary} "$file"
+    done
+  '';
+
+  instantiate-nixpkgs-randomly = writeExecline "instantiate-nixpkgs-randomly" { readNArgs = 1; } [
+    "export"
+    "NIXPKGS_ALLOW_BROKEN"
+    "1"
+    "export"
+    "NIXPKGS_ALLOW_UNFREE"
+    "1"
+    "export"
+    "NIXPKGS_ALLOW_INSECURE"
+    "1"
+    "export"
+    "NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM"
+    "1"
+    "pipeline"
+    [
+      "nix"
+      "eval"
+      "--raw"
+      ''(
+          let pkgs = import ''${1} {};
+          in builtins.toJSON (builtins.attrNames pkgs)
+        )''
+    ]
+    "pipeline"
+    [ bins.jq "-r" ".[]" ]
+    "pipeline"
+    [ bins.shuf ]
+    "pipeline"
+    [ bins.head "-n" "1000" ]
+    bins.xargs
+    "-I"
+    "{}"
+    "-n1"
+    "if"
+    [ depot.tools.eprintf "instantiating %s\n" "{}" ]
+    "nix-instantiate"
+    "$1"
+    "-A"
+    "{}"
+  ];
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    instantiate-nixpkgs-randomly
+    # requires hnix, which we don’t want in tvl for now
+    # uncomment manually if you want to use it.
+    #   meta-stdenv-lib
+    #   replace-stdenv-lib
+    ;
+}
diff --git a/users/Profpatsch/read-http.nix b/users/Profpatsch/read-http.nix
new file mode 100644
index 0000000000..d9ad6fc30d
--- /dev/null
+++ b/users/Profpatsch/read-http.nix
@@ -0,0 +1,19 @@
+{ depot, pkgs, ... }:
+
+let
+
+  read-http = depot.nix.writers.rustSimple
+    {
+      name = "read-http";
+      dependencies = [
+        depot.third_party.rust-crates.ascii
+        depot.third_party.rust-crates.httparse
+        depot.users.Profpatsch.netencode.netencode-rs
+        depot.users.Profpatsch.arglib.netencode.rust
+        depot.users.Profpatsch.execline.exec-helpers
+      ];
+    }
+    (builtins.readFile ./read-http.rs);
+
+in
+read-http
diff --git a/users/Profpatsch/read-http.rs b/users/Profpatsch/read-http.rs
new file mode 100644
index 0000000000..efaded87e6
--- /dev/null
+++ b/users/Profpatsch/read-http.rs
@@ -0,0 +1,249 @@
+extern crate arglib_netencode;
+extern crate ascii;
+extern crate exec_helpers;
+extern crate httparse;
+extern crate netencode;
+
+use exec_helpers::{die_expected_error, die_temporary, die_user_error};
+use std::collections::HashMap;
+use std::io::{Read, Write};
+use std::os::unix::io::FromRawFd;
+
+use netencode::dec::Decoder;
+use netencode::{dec, T, U};
+
+enum What {
+    Request,
+    Response,
+}
+
+// reads a http request (stdin), and writes all headers to stdout, as netencoded record.
+// The keys are text, but can be lists of text iff headers appear multiple times, so beware.
+fn main() -> std::io::Result<()> {
+    exec_helpers::no_args("read-http");
+
+    let args = dec::RecordDot {
+        field: "what",
+        inner: dec::OneOf {
+            list: vec!["request", "response"],
+            inner: dec::Text,
+        },
+    };
+    let what: What = match args.dec(arglib_netencode::arglib_netencode("read-http", None).to_u()) {
+        Ok("request") => What::Request,
+        Ok("response") => What::Response,
+        Ok(v) => panic!("shouldn’t happen!, value was: {}", v),
+        Err(dec::DecodeError(err)) => die_user_error("read-http", err),
+    };
+
+    fn read_stdin_to_complete<F>(mut parse: F) -> ()
+    where
+        F: FnMut(&[u8]) -> httparse::Result<usize>,
+    {
+        let mut res = httparse::Status::Partial;
+        loop {
+            if let httparse::Status::Complete(_) = res {
+                return;
+            }
+            let mut buf = [0; 2048];
+            match std::io::stdin().read(&mut buf[..]) {
+                Ok(size) => {
+                    if size == 0 {
+                        break;
+                    }
+                }
+                Err(err) => {
+                    die_temporary("read-http", format!("could not read from stdin, {:?}", err))
+                }
+            }
+            match parse(&buf) {
+                Ok(status) => {
+                    res = status;
+                }
+                Err(err) => {
+                    die_temporary("read-http", format!("httparse parsing failed: {:#?}", err))
+                }
+            }
+        }
+    }
+
+    fn normalize_headers<'a>(headers: &'a [httparse::Header]) -> HashMap<String, U<'a>> {
+        let mut res = HashMap::new();
+        for httparse::Header { name, value } in headers {
+            let val = ascii::AsciiStr::from_ascii(*value)
+                .expect(&format!(
+                    "read-http: we require header values to be ASCII, but the header {} was {:?}",
+                    name, value
+                ))
+                .as_str();
+            // lowercase the header names, since the standard doesn’t care
+            // and we want unique strings to match against
+            let name_lower = name.to_lowercase();
+            match res.insert(name_lower, U::Text(val)) {
+                None => (),
+                Some(U::Text(t)) => {
+                    let name_lower = name.to_lowercase();
+                    let _ = res.insert(name_lower, U::List(vec![U::Text(t), U::Text(val)]));
+                    ()
+                }
+                Some(U::List(mut l)) => {
+                    let name_lower = name.to_lowercase();
+                    l.push(U::Text(val));
+                    let _ = res.insert(name_lower, U::List(l));
+                    ()
+                }
+                Some(o) => panic!("read-http: header not text nor list: {:?}", o),
+            }
+        }
+        res
+    }
+
+    // tries to read until the end of the http header (deliniated by two newlines "\r\n\r\n")
+    fn read_till_end_of_header<R: Read>(buf: &mut Vec<u8>, reader: R) -> Option<()> {
+        let mut chonker = Chunkyboi::new(reader, 4096);
+        loop {
+            // TODO: attacker can send looooong input, set upper maximum
+            match chonker.next() {
+                Some(Ok(chunk)) => {
+                    buf.extend_from_slice(&chunk);
+                    if chunk.windows(4).any(|c| c == b"\r\n\r\n") {
+                        return Some(());
+                    }
+                }
+                Some(Err(err)) => {
+                    die_temporary("read-http", format!("error reading from stdin: {:?}", err))
+                }
+                None => return None,
+            }
+        }
+    }
+
+    // max header size chosen arbitrarily
+    let mut headers = [httparse::EMPTY_HEADER; 128];
+    let stdin = std::io::stdin();
+
+    match what {
+        Request => {
+            let mut req = httparse::Request::new(&mut headers);
+            let mut buf: Vec<u8> = vec![];
+            match read_till_end_of_header(&mut buf, stdin.lock()) {
+                Some(()) => match req.parse(&buf) {
+                    Ok(httparse::Status::Complete(_body_start)) => {}
+                    Ok(httparse::Status::Partial) => {
+                        die_expected_error("read-http", "httparse should have gotten a full header")
+                    }
+                    Err(err) => die_expected_error(
+                        "read-http",
+                        format!("httparse response parsing failed: {:#?}", err),
+                    ),
+                },
+                None => die_expected_error(
+                    "read-http",
+                    format!("httparse end of stdin reached before able to parse request headers"),
+                ),
+            }
+            let method = req.method.expect("method must be filled on complete parse");
+            let path = req.path.expect("path must be filled on complete parse");
+            write_dict_req(method, path, &normalize_headers(req.headers))
+        }
+        Response => {
+            let mut resp = httparse::Response::new(&mut headers);
+            let mut buf: Vec<u8> = vec![];
+            match read_till_end_of_header(&mut buf, stdin.lock()) {
+                Some(()) => match resp.parse(&buf) {
+                    Ok(httparse::Status::Complete(_body_start)) => {}
+                    Ok(httparse::Status::Partial) => {
+                        die_expected_error("read-http", "httparse should have gotten a full header")
+                    }
+                    Err(err) => die_expected_error(
+                        "read-http",
+                        format!("httparse response parsing failed: {:#?}", err),
+                    ),
+                },
+                None => die_expected_error(
+                    "read-http",
+                    format!("httparse end of stdin reached before able to parse response headers"),
+                ),
+            }
+            let code = resp.code.expect("code must be filled on complete parse");
+            let reason = resp
+                .reason
+                .expect("reason must be filled on complete parse");
+            write_dict_resp(code, reason, &normalize_headers(resp.headers))
+        }
+    }
+}
+
+fn write_dict_req<'a, 'buf>(
+    method: &'buf str,
+    path: &'buf str,
+    headers: &'a HashMap<String, U<'a>>,
+) -> std::io::Result<()> {
+    let mut http = vec![("method", U::Text(method)), ("path", U::Text(path))]
+        .into_iter()
+        .collect();
+    write_dict(http, headers)
+}
+
+fn write_dict_resp<'a, 'buf>(
+    code: u16,
+    reason: &'buf str,
+    headers: &'a HashMap<String, U<'a>>,
+) -> std::io::Result<()> {
+    let mut http = vec![
+        ("status", U::N6(code as u64)),
+        ("status-text", U::Text(reason)),
+    ]
+    .into_iter()
+    .collect();
+    write_dict(http, headers)
+}
+
+fn write_dict<'buf, 'a>(
+    mut http: HashMap<&str, U<'a>>,
+    headers: &'a HashMap<String, U<'a>>,
+) -> std::io::Result<()> {
+    match http.insert(
+        "headers",
+        U::Record(
+            headers
+                .iter()
+                .map(|(k, v)| (k.as_str(), v.clone()))
+                .collect(),
+        ),
+    ) {
+        None => (),
+        Some(_) => panic!("read-http: headers already in dict"),
+    };
+    netencode::encode(&mut std::io::stdout(), &U::Record(http))?;
+    Ok(())
+}
+
+// iter helper
+
+struct Chunkyboi<T> {
+    inner: T,
+    buf: Vec<u8>,
+}
+
+impl<R: Read> Chunkyboi<R> {
+    fn new(inner: R, chunksize: usize) -> Self {
+        let buf = vec![0; chunksize];
+        Chunkyboi { inner, buf }
+    }
+}
+
+impl<R: Read> Iterator for Chunkyboi<R> {
+    type Item = std::io::Result<Vec<u8>>;
+
+    fn next(&mut self) -> Option<std::io::Result<Vec<u8>>> {
+        match self.inner.read(&mut self.buf) {
+            Ok(0) => None,
+            Ok(read) => {
+                // clone a new buffer so we can reuse the internal one
+                Some(Ok(self.buf[..read].to_owned()))
+            }
+            Err(err) => Some(Err(err)),
+        }
+    }
+}
diff --git a/users/Profpatsch/reverse-haskell-deps.hs b/users/Profpatsch/reverse-haskell-deps.hs
new file mode 100644
index 0000000000..6b644df9ec
--- /dev/null
+++ b/users/Profpatsch/reverse-haskell-deps.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+import qualified Text.HTML.TagSoup as Tag
+import qualified Data.Text as Text
+import Data.Text (Text)
+import qualified Data.List as List
+import Data.Maybe
+import Text.Nicify
+import qualified Text.Read as Read
+import Numeric.Natural
+import Data.Either
+import qualified Data.ByteString as ByteString
+import qualified Data.Text.Encoding
+
+parseNat :: Text.Text -> Maybe Natural
+parseNat = Read.readMaybe . Text.unpack
+
+printNice :: Show a => a -> IO ()
+printNice = putStrLn . nicify . show
+
+type Tag = Tag.Tag Text.Text
+
+main = do
+  reverseHtml <- readStdinUtf8
+  printNice $ List.sortOn snd $ packagesAndReverseDeps reverseHtml
+
+  where
+    readStdinUtf8 = Data.Text.Encoding.decodeUtf8 <$> ByteString.getContents
+
+-- | reads the table provided by https://packdeps.haskellers.com/reverse
+-- figuring out all sections (starting with the link to the package name),
+-- then figuring out the name of the package and the first column,
+-- which is the number of reverse dependencies of the package
+packagesAndReverseDeps reverseHtml = do
+  let tags = Tag.parseTags reverseHtml
+  let sections =  Tag.partitions (isJust . reverseLink) tags
+  let sectionNames = map (fromJust . reverseLink . head) sections
+  mapMaybe
+    (\(name :: Text.Text, sect) -> do
+        reverseDeps <- firstNaturalNumber sect
+        pure (sectionPackageName name sect, reverseDeps) :: Maybe (Text.Text, Natural))
+    $ zip sectionNames sections
+
+
+  where
+    reverseLink = \case
+      Tag.TagOpen "a" attrs -> mapFind attrReverseLink attrs
+      _ -> Nothing
+
+    attrReverseLink = \case
+      ("href", lnk) -> if
+          | "packdeps.haskellers.com/reverse/" `Text.isInfixOf` lnk -> Just lnk
+          | otherwise -> Nothing
+      _ -> Nothing
+
+    sectionPackageName :: Text -> [Tag] -> Text
+    sectionPackageName sectionName = \case
+      (_: Tag.TagText name : _) -> name
+      (_: el : _) -> sectionName
+      xs -> sectionName
+
+
+    firstNaturalNumber :: [Tag] -> Maybe Natural
+    firstNaturalNumber =
+      mapFind (\case
+        Tag.TagText t -> parseNat t
+        _ -> Nothing)
+
+    mapFind :: (a -> Maybe b) -> [a] -> Maybe b
+    mapFind f xs = fromJust . f <$> List.find (isJust . f) xs
diff --git a/users/Profpatsch/reverse-haskell-deps.nix b/users/Profpatsch/reverse-haskell-deps.nix
new file mode 100644
index 0000000000..6df7bc6329
--- /dev/null
+++ b/users/Profpatsch/reverse-haskell-deps.nix
@@ -0,0 +1,31 @@
+{ depot, pkgs, ... }:
+
+# Parses https://packdeps.haskellers.com/reverse
+# and outputs the amount of reverse dependencies of each hackage package.
+
+let
+
+  rev = depot.nix.writeExecline "reverse-haskell-deps" { } [
+    "pipeline"
+    [
+      "${pkgs.curl}/bin/curl"
+      "-L"
+      "https://packdeps.haskellers.com/reverse"
+    ]
+    rev-hs
+
+  ];
+
+  rev-hs = pkgs.writers.writeHaskell "revers-haskell-deps-hs"
+    {
+      libraries = [
+        pkgs.haskellPackages.nicify-lib
+        pkgs.haskellPackages.tagsoup
+      ];
+
+    }
+    ./reverse-haskell-deps.hs;
+
+
+in
+rev
diff --git a/users/Profpatsch/solarized.dhall b/users/Profpatsch/solarized.dhall
new file mode 100644
index 0000000000..01e14d64f4
--- /dev/null
+++ b/users/Profpatsch/solarized.dhall
@@ -0,0 +1,39 @@
+-- SOLARIZED HEX     16/8 TERMCOL  XTERM/HEX   L*A*B      RGB         HSB
+-- --------- ------- ---- -------  ----------- ---------- ----------- -----------
+-- base03    #002b36  8/4 brblack  234 #1c1c1c 15 -12 -12   0  43  54 193 100  21
+-- base02    #073642  0/4 black    235 #262626 20 -12 -12   7  54  66 192  90  26
+-- base01    #586e75 10/7 brgreen  240 #585858 45 -07 -07  88 110 117 194  25  46
+-- base00    #657b83 11/7 bryellow 241 #626262 50 -07 -07 101 123 131 195  23  51
+-- base0     #839496 12/6 brblue   244 #808080 60 -06 -03 131 148 150 186  13  59
+-- base1     #93a1a1 14/4 brcyan   245 #8a8a8a 65 -05 -02 147 161 161 180   9  63
+-- base2     #eee8d5  7/7 white    254 #e4e4e4 92 -00  10 238 232 213  44  11  93
+-- base3     #fdf6e3 15/7 brwhite  230 #ffffd7 97  00  10 253 246 227  44  10  99
+-- yellow    #b58900  3/3 yellow   136 #af8700 60  10  65 181 137   0  45 100  71
+-- orange    #cb4b16  9/3 brred    166 #d75f00 50  50  55 203  75  22  18  89  80
+-- red       #dc322f  1/1 red      160 #d70000 50  65  45 220  50  47   1  79  86
+-- magenta   #d33682  5/5 magenta  125 #af005f 50  65 -05 211  54 130 331  74  83
+-- violet    #6c71c4 13/5 brmagenta 61 #5f5faf 50  15 -45 108 113 196 237  45  77
+-- blue      #268bd2  4/4 blue      33 #0087ff 55 -10 -45  38 139 210 205  82  82
+-- cyan      #2aa198  6/6 cyan      37 #00afaf 60 -35 -05  42 161 152 175  74  63
+-- green     #859900  2/2 green     64 #5f8700 60 -20  65 133 153   0  68 100  60
+{
+  hex  =
+    {
+      base03 = "#002b36",
+      base02 = "#073642",
+      base01 = "#586e75",
+      base00 = "#657b83",
+      base0 = "#839496",
+      base1 = "#93a1a1",
+      base2 = "#eee8d5",
+      base3 = "#fdf6e3",
+      yellow = "#b58900",
+      orange    = "#cb4b16",
+      red       = "#dc322f",
+      magenta   = "#d33682",
+      violet    = "#6c71c4",
+      blue      = "#268bd2",
+      cyan      = "#2aa198",
+      green     = "#859900",
+    }
+}
diff --git a/users/Profpatsch/struct-edit/default.nix b/users/Profpatsch/struct-edit/default.nix
new file mode 100644
index 0000000000..11a7200ce4
--- /dev/null
+++ b/users/Profpatsch/struct-edit/default.nix
@@ -0,0 +1,13 @@
+{ depot, ... }:
+depot.nix.buildGo.program {
+  name = "struct-edit";
+  srcs = [
+    ./main.go
+  ];
+  deps = [
+    depot.third_party.gopkgs."github.com".charmbracelet.bubbletea
+    depot.third_party.gopkgs."github.com".charmbracelet.lipgloss
+    depot.third_party.gopkgs."github.com".muesli.termenv
+    depot.third_party.gopkgs."github.com".mattn.go-isatty
+  ];
+}
diff --git a/users/Profpatsch/struct-edit/main.go b/users/Profpatsch/struct-edit/main.go
new file mode 100644
index 0000000000..c1a7013385
--- /dev/null
+++ b/users/Profpatsch/struct-edit/main.go
@@ -0,0 +1,431 @@
+package main
+
+import (
+	json "encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"sort"
+	"strings"
+
+	tea "github.com/charmbracelet/bubbletea"
+	lipgloss "github.com/charmbracelet/lipgloss"
+	// termenv "github.com/muesli/termenv"
+	// isatty "github.com/mattn/go-isatty"
+)
+
+// Keeps the full data structure and a path that indexes our current position into it.
+type model struct {
+	path []index
+	data val
+}
+
+// an index into a value, uint for lists and string for maps.
+// nil for any scalar value.
+// TODO: use an actual interface for these
+type index interface{}
+
+/// recursive value that we can represent.
+type val struct {
+	// the “type” of value; see tag const belove
+	tag tag
+	// last known position of our cursor
+	last_index index
+	// documentation (TODO)
+	doc string
+	// the actual value;
+	// the actual structure is behind a pointer so we can replace the struct.
+	// determined by the tag
+	// tagString -> *string
+	// tagFloat -> *float64
+	// tagList -> *[]val
+	// tagMap -> *map[string]val
+	val interface{}
+}
+
+type tag string
+
+const (
+	tagString tag = "string"
+	tagFloat  tag = "float"
+	tagList   tag = "list"
+	tagMap    tag = "map"
+)
+
+// print a value, flat
+func (v val) Render() string {
+	s := ""
+	switch v.tag {
+	case tagString:
+		s += *v.val.(*string)
+	case tagFloat:
+		s += fmt.Sprint(*v.val.(*float64))
+	case tagList:
+		s += "[ "
+		vs := []string{}
+		for _, enum := range v.enumerate() {
+			vs = append(vs, enum.v.Render())
+		}
+		s += strings.Join(vs, ", ")
+		s += " ]"
+	case tagMap:
+		s += "{ "
+		vs := []string{}
+		for _, enum := range v.enumerate() {
+			vs = append(vs, fmt.Sprintf("%s: %s", enum.i.(string), enum.v.Render()))
+		}
+		s += strings.Join(vs, ", ")
+		s += " }"
+	default:
+		s += fmt.Sprintf("<unknown: %v>", v)
+	}
+	return s
+}
+
+// render an index, depending on the type
+func renderIndex(i index) (s string) {
+	switch i := i.(type) {
+	case nil:
+		s = ""
+	// list index
+	case uint:
+		s = "*"
+	// map index
+	case string:
+		s = i + ":"
+	}
+	return
+}
+
+// take an arbitrary (within restrictions) go value and construct a val from it
+func makeVal(i interface{}) val {
+	var v val
+	switch i := i.(type) {
+	case string:
+		v = val{
+			tag:        tagString,
+			last_index: index(nil),
+			doc:        "",
+			val:        &i,
+		}
+	case float64:
+		v = val{
+			tag:        tagFloat,
+			last_index: index(nil),
+			doc:        "",
+			val:        &i,
+		}
+	case []interface{}:
+		ls := []val{}
+		for _, i := range i {
+			ls = append(ls, makeVal(i))
+		}
+		v = val{
+			tag:        tagList,
+			last_index: pos1Inner(tagList, &ls),
+			doc:        "",
+			val:        &ls,
+		}
+	case map[string]interface{}:
+		ls := map[string]val{}
+		for k, i := range i {
+			ls[k] = makeVal(i)
+		}
+		v = val{
+			tag:        tagMap,
+			last_index: pos1Inner(tagMap, &ls),
+			doc:        "",
+			val:        &ls,
+		}
+	default:
+		log.Fatalf("makeVal: cannot read json of type %T", i)
+	}
+	return v
+}
+
+// return an index that points at the first entry in val
+func (v val) pos1() index {
+	return v.enumerate()[0].i
+}
+
+func pos1Inner(tag tag, v interface{}) index {
+	return enumerateInner(tag, v)[0].i
+}
+
+type enumerate struct {
+	i index
+	v val
+}
+
+// enumerate gives us a stable ordering of elements in this val.
+// for scalars it’s just a nil index & the val itself.
+// Guaranteed to always return at least one element.
+func (v val) enumerate() (e []enumerate) {
+	e = enumerateInner(v.tag, v.val)
+	if e == nil {
+		e = append(e, enumerate{
+			i: nil,
+			v: v,
+		})
+	}
+	return
+}
+
+// like enumerate, but returns an empty slice for scalars without inner vals.
+func enumerateInner(tag tag, v interface{}) (e []enumerate) {
+	switch tag {
+	case tagString:
+		fallthrough
+	case tagFloat:
+		e = nil
+	case tagList:
+		for i, v := range *v.(*[]val) {
+			e = append(e, enumerate{i: index(uint(i)), v: v})
+		}
+	case tagMap:
+		// map sorting order is not stable (actually randomized thank jabber)
+		// so let’s sort them
+		keys := []string{}
+		m := *v.(*map[string]val)
+		for k, _ := range m {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, k := range keys {
+			e = append(e, enumerate{i: index(k), v: m[k]})
+		}
+	default:
+		log.Fatalf("unknown val tag %s, %v", tag, v)
+	}
+	return
+}
+
+func (m model) PathString() string {
+	s := "/ "
+	var is []string
+	for _, v := range m.path {
+		is = append(is, fmt.Sprintf("%v", v))
+	}
+	s += strings.Join(is, " / ")
+	return s
+}
+
+// walk the given path down in data, to get the value at that point.
+// Assumes that all path indexes are valid indexes into data.
+// Returns a pointer to the value at point, in order to be able to change it.
+func walk(data *val, path []index) (*val, bool, error) {
+	res := data
+	atPath := func(index int) string {
+		return fmt.Sprintf("at path %v", path[:index+1])
+	}
+	errf := func(ty string, val interface{}, index int) error {
+		return fmt.Errorf("walk: can’t walk into %s %v %s", ty, val, atPath(index))
+	}
+	for i, p := range path {
+		switch res.tag {
+		case tagString:
+			return nil, true, nil
+		case tagFloat:
+			return nil, true, nil
+		case tagList:
+			switch p := p.(type) {
+			case uint:
+				list := *res.val.(*[]val)
+				if int(p) >= len(list) || p < 0 {
+					return nil, false, fmt.Errorf("index out of bounds %s", atPath(i))
+				}
+				res = &list[p]
+			default:
+				return nil, false, fmt.Errorf("not a list index %s", atPath(i))
+			}
+		case tagMap:
+			switch p := p.(type) {
+			case string:
+				m := *res.val.(*map[string]val)
+				if a, ok := m[p]; ok {
+					res = &a
+				} else {
+					return nil, false, fmt.Errorf("index %s not in map %s", p, atPath(i))
+				}
+			default:
+				return nil, false, fmt.Errorf("not a map index %v %s", p, atPath(i))
+			}
+
+		default:
+			return nil, false, errf(string(res.tag), res.val, i)
+		}
+	}
+	return res, false, nil
+}
+
+// descend into the selected index. Assumes that the index is valid.
+// Will not descend into scalars.
+func (m model) descend() (model, error) {
+	// TODO: two walks?!
+	this, _, err := walk(&m.data, m.path)
+	if err != nil {
+		return m, err
+	}
+	newPath := append(m.path, this.last_index)
+	_, bounce, err := walk(&m.data, newPath)
+	if err != nil {
+		return m, err
+	}
+	// only descend if we *can*
+	if !bounce {
+		m.path = newPath
+	}
+	return m, nil
+}
+
+// ascend to one level up. stops at the root.
+func (m model) ascend() (model, error) {
+	if len(m.path) > 0 {
+		m.path = m.path[:len(m.path)-1]
+		_, _, err := walk(&m.data, m.path)
+		return m, err
+	}
+	return m, nil
+}
+
+/// go to the next item, or wraparound
+func (min model) next() (m model, err error) {
+	m = min
+	this, _, err := walk(&m.data, m.path)
+	if err != nil {
+		return
+	}
+	enumL := this.enumerate()
+	setNext := false
+	for _, enum := range enumL {
+		if setNext {
+			this.last_index = enum.i
+			setNext = false
+			break
+		}
+		if enum.i == this.last_index {
+			setNext = true
+		}
+	}
+	// wraparound
+	if setNext {
+		this.last_index = enumL[0].i
+	}
+	return
+}
+
+/// go to the previous item, or wraparound
+func (min model) prev() (m model, err error) {
+	m = min
+	this, _, err := walk(&m.data, m.path)
+	if err != nil {
+		return
+	}
+	enumL := this.enumerate()
+	// last element, wraparound
+	prevIndex := enumL[len(enumL)-1].i
+	for _, enum := range enumL {
+		if enum.i == this.last_index {
+			this.last_index = prevIndex
+			break
+		}
+		prevIndex = enum.i
+	}
+	return
+}
+
+/// bubbletea implementations
+
+func (m model) Init() tea.Cmd {
+	return nil
+}
+
+func initialModel(v interface{}) model {
+	val := makeVal(v)
+	return model{
+		path: []index{},
+		data: val,
+	}
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var err error
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		switch msg.String() {
+		case "ctrl+c", "q":
+			return m, tea.Quit
+
+		case "up":
+			m, err = m.prev()
+
+		case "down":
+			m, err = m.next()
+
+		case "right":
+			m, err = m.descend()
+
+		case "left":
+			m, err = m.ascend()
+
+			// 	case "enter":
+			// 		_, ok := m.selected[m.cursor]
+			// 		if ok {
+			// 			delete(m.selected, m.cursor)
+			// 		} else {
+			// 			m.selected[m.cursor] = struct{}{}
+			// 		}
+		}
+
+	}
+	if err != nil {
+		log.Fatal(err)
+	}
+	return m, nil
+}
+
+var pathColor = lipgloss.NewStyle().
+	// light blue
+	Foreground(lipgloss.Color("12"))
+
+var selectedColor = lipgloss.NewStyle().
+	Bold(true)
+
+func (m model) View() string {
+	s := pathColor.Render(m.PathString())
+	cur, _, err := walk(&m.data, m.path)
+	if err != nil {
+		log.Fatal(err)
+	}
+	s += cur.doc + "\n"
+	s += "\n"
+	for _, enum := range cur.enumerate() {
+		is := renderIndex(enum.i)
+		if is != "" {
+			s += is + " "
+		}
+		if enum.i == cur.last_index {
+			s += selectedColor.Render(enum.v.Render())
+		} else {
+			s += enum.v.Render()
+		}
+		s += "\n"
+	}
+
+	// s += fmt.Sprintf("%v\n", m)
+	// s += fmt.Sprintf("%v\n", cur)
+
+	return s
+}
+
+func main() {
+	var input interface{}
+	err := json.NewDecoder(os.Stdin).Decode(&input)
+	if err != nil {
+		log.Fatal("json from stdin: ", err)
+	}
+	p := tea.NewProgram(initialModel(input))
+	if err := p.Start(); err != nil {
+		log.Fatal("bubbletea TUI error: ", err)
+	}
+}
diff --git a/users/Profpatsch/toINI.nix b/users/Profpatsch/toINI.nix
new file mode 100644
index 0000000000..537505d30b
--- /dev/null
+++ b/users/Profpatsch/toINI.nix
@@ -0,0 +1,79 @@
+{ lib, ... }:
+let
+  /* Generate an INI-style config file from an attrset
+   * specifying the global section (no header), and a
+   * list of sections which contain name/value pairs.
+   *
+   * generators.toINI {} {
+   *   globalSection = [
+   *     { name = "someGlobalKey"; value = "hi"; }
+   *   ];
+   *   sections = [
+   *     { name = "foo"; value = [
+   *         { name = "hi"; value = "${pkgs.hello}"; }
+   *         { name = "ciao"; value = "bar"; }
+   *       ];
+   *     }
+   *     { name = "baz";
+   *       value = [ { name = "also, integers"; value = 42; } ];
+   *     }
+   *   ];
+   * }
+   *
+   *> someGlobalKey=hi
+   *>
+   *> [foo]
+   *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
+   *> ciao=bar
+   *>
+   *> [baz]
+   *> also, integers=42
+   *>
+   *
+   * The mk* configuration attributes can generically change
+   * the way sections and key-value strings are generated.
+   *
+   * Order of the sections and of keys is preserved,
+   * duplicate keys are allowed.
+   */
+  toINI =
+    {
+      # apply transformations (e.g. escapes) to section names
+      mkSectionName ? (name: lib.strings.escape [ "[" "]" ] name)
+    , # format a setting line from key and value
+      mkKeyValue ? lib.generators.mkKeyValueDefault { } "="
+    ,
+    }: { globalSection, sections }:
+    let
+      mkSection = sectName: sectValues: ''
+        [${mkSectionName sectName}]
+      '' + toKeyValue { inherit mkKeyValue; } sectValues;
+      # map input to ini sections
+      mkSections = lib.strings.concatMapStringsSep "\n"
+        ({ name, value }: mkSection name value)
+        sections;
+      mkGlobalSection =
+        if globalSection == [ ]
+        then ""
+        else toKeyValue { inherit mkKeyValue; } globalSection
+          + "\n";
+    in
+    mkGlobalSection
+    + mkSections;
+
+  /* Generate a name-value-style config file from a list.
+   *
+   * mkKeyValue is the same as in toINI.
+   */
+  toKeyValue =
+    { mkKeyValue ? lib.generators.mkKeyValueDefault { } "="
+    ,
+    }:
+    let
+      mkLine = k: v: mkKeyValue k v + "\n";
+      mkLines = k: v: [ (mkLine k v) ];
+    in
+    nameValues: lib.strings.concatStrings (lib.concatLists (map ({ name, value }: mkLines name value) nameValues));
+
+in
+toINI
diff --git a/users/Profpatsch/tree-sitter.nix b/users/Profpatsch/tree-sitter.nix
new file mode 100644
index 0000000000..2224da2a3b
--- /dev/null
+++ b/users/Profpatsch/tree-sitter.nix
@@ -0,0 +1,209 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.coreutils [ "head" "printf" "cat" ]
+    // depot.nix.getBins pkgs.ncurses [ "tput" ]
+    // depot.nix.getBins pkgs.bc [ "bc" ]
+    // depot.nix.getBins pkgs.ocamlPackages.sexp [ "sexp" ];
+
+  print-ast = depot.nix.writers.rustSimple
+    {
+      name = "print-ast";
+      dependencies = with depot.third_party.rust-crates; [
+        libloading
+        tree-sitter
+      ];
+    } ''
+    extern crate libloading;
+    extern crate tree_sitter;
+    use std::mem;
+    use std::io::{Read};
+    use libloading::{Library, Symbol};
+    use tree_sitter::{Language, Parser};
+
+    /// Load the shared lib FILE and return the language under SYMBOL-NAME.
+    /// Inspired by the rust source of emacs-tree-sitter.
+    fn _load_language(file: String, symbol_name: String) -> Result<Language, libloading::Error> {
+        let lib = Library::new(file)?;
+        let tree_sitter_lang: Symbol<'_, unsafe extern "C" fn() -> _> =
+            unsafe { lib.get(symbol_name.as_bytes())? };
+        let language: Language = unsafe { tree_sitter_lang() };
+        // Avoid segmentation fault by not unloading the lib, as language is a static piece of data.
+        // TODO: Attach an Rc<Library> to Language instead.
+        mem::forget(lib);
+        Ok(language)
+    }
+
+    fn main() {
+      let mut args = std::env::args();
+      let so = args.nth(1).unwrap();
+      let symbol_name = args.nth(0).unwrap();
+      let file = args.nth(0).unwrap();
+      let mut parser = Parser::new();
+      let lang = _load_language(so, symbol_name).unwrap();
+      parser.set_language(lang).unwrap();
+      let bytes = std::fs::read(&file).unwrap();
+      print!("{}", parser.parse(&bytes, None).unwrap().root_node().to_sexp());
+    }
+
+
+  '';
+
+  tree-sitter-nix = buildTreeSitterGrammar {
+    language = "tree-sitter-nix";
+    source = pkgs.fetchFromGitHub {
+      owner = "cstrahan";
+      repo = "tree-sitter-nix";
+      rev = "791b5ff0e4f0da358cbb941788b78d436a2ca621";
+      sha256 = "1y5b3wh3fcmbgq8r2i97likzfp1zp02m58zacw5a1cjqs5raqz66";
+    };
+  };
+
+  watch-file-modified = depot.nix.writers.rustSimple
+    {
+      name = "watch-file-modified";
+      dependencies = [
+        depot.third_party.rust-crates.inotify
+        depot.users.Profpatsch.netstring.rust-netstring
+      ];
+    } ''
+    extern crate inotify;
+    extern crate netstring;
+    use inotify::{EventMask, WatchMask, Inotify};
+    use std::io::Write;
+
+    fn main() {
+        let mut inotify = Inotify::init()
+            .expect("Failed to initialize inotify");
+
+        let file = std::env::args().nth(1).unwrap();
+
+        let file_watch = inotify
+            .add_watch(
+                &file,
+                WatchMask::MODIFY
+            )
+            .expect("Failed to add inotify watch");
+
+        let mut buffer = [0u8; 4096];
+        loop {
+            let events = inotify
+                .read_events_blocking(&mut buffer)
+                .expect("Failed to read inotify events");
+
+            for event in events {
+                if event.wd == file_watch {
+                  std::io::stdout().write(&netstring::to_netstring(file.as_bytes()));
+                  std::io::stdout().flush();
+                }
+            }
+        }
+    }
+
+  '';
+
+  # clear screen and set LINES and COLUMNS to terminal height & width
+  clear-screen = depot.nix.writeExecline "clear-screen" { } [
+    "if"
+    [ bins.tput "clear" ]
+    "backtick"
+    "-in"
+    "LINES"
+    [ bins.tput "lines" ]
+    "backtick"
+    "-in"
+    "COLUMNS"
+    [ bins.tput "cols" ]
+    "$@"
+  ];
+
+  print-nix-file = depot.nix.writeExecline "print-nix-file" { readNArgs = 1; } [
+    "pipeline"
+    [ print-ast "${tree-sitter-nix}/parser" "tree_sitter_nix" "$1" ]
+    "pipeline"
+    [ bins.sexp "print" ]
+    clear-screen
+    "importas"
+    "-ui"
+    "lines"
+    "LINES"
+    "backtick"
+    "-in"
+    "ls"
+    [
+      "pipeline"
+      # when you pull out bc to decrement an integer it’s time to switch to python lol
+      [ bins.printf "x=%s; --x\n" "$lines" ]
+      bins.bc
+    ]
+    "importas"
+    "-ui"
+    "l"
+    "ls"
+    bins.head
+    "-n\${l}"
+  ];
+
+  print-nix-file-on-update = depot.nix.writeExecline "print-nix-file-on-update" { readNArgs = 1; } [
+    "if"
+    [ print-nix-file "$1" ]
+    "pipeline"
+    [ watch-file-modified "$1" ]
+    "forstdin"
+    "-d"
+    ""
+    "file"
+    "importas"
+    "file"
+    "file"
+    print-nix-file
+    "$file"
+  ];
+
+  # copied from nixpkgs
+  buildTreeSitterGrammar =
+    {
+      # language name
+      language
+      # source for the language grammar
+    , source
+    }:
+
+    pkgs.stdenv.mkDerivation {
+
+      pname = "${language}-grammar";
+      inherit (pkgs.tree-sitter) version;
+
+      src = source;
+
+      buildInputs = [ pkgs.tree-sitter ];
+
+      dontUnpack = true;
+      configurePhase = ":";
+      buildPhase = ''
+        runHook preBuild
+        scanner_cc="$src/src/scanner.cc"
+        if [ ! -f "$scanner_cc" ]; then
+          scanner_cc=""
+        fi
+        $CXX -I$src/src/ -c $scanner_cc
+        $CC -I$src/src/ -shared -o parser -Os  scanner.o $src/src/parser.c -lstdc++
+        runHook postBuild
+      '';
+      installPhase = ''
+        runHook preInstall
+        mkdir $out
+        mv parser $out/
+        runHook postInstall
+      '';
+    };
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    print-ast
+    tree-sitter-nix
+    print-nix-file-on-update
+    watch-file-modified
+    ;
+}
diff --git a/users/Profpatsch/writers/default.nix b/users/Profpatsch/writers/default.nix
new file mode 100644
index 0000000000..02f39da02d
--- /dev/null
+++ b/users/Profpatsch/writers/default.nix
@@ -0,0 +1,97 @@
+{ depot, pkgs, lib, ... }:
+let
+  bins = depot.nix.getBins pkgs.s6-portable-utils [ "s6-mkdir" "s6-cat" "s6-ln" "s6-ls" "s6-touch" ]
+    // depot.nix.getBins pkgs.coreutils [ "printf" ];
+
+  inherit (depot.nix.yants) defun struct restrict attrs list string drv any;
+
+  inherit (depot.nix) drvSeqL;
+
+  FlakeError =
+    restrict
+      "flake error"
+      (s: lib.any (prefix: (builtins.substring 0 1 s) == prefix)
+        [ "E" "W" ])
+      string;
+  Libraries = defun [ (attrs any) (list drv) ];
+
+  python3 =
+    { name
+    , libraries ? (_: [ ])
+    , flakeIgnore ? [ ]
+    }: pkgs.writers.writePython3 name {
+      libraries = Libraries libraries pkgs.python3Packages;
+      flakeIgnore =
+        let
+          ignoreTheseErrors = [
+            # whitespace after {
+            "E201"
+            # whitespace before }
+            "E202"
+            # fuck 4-space indentation
+            "E121"
+            "E111"
+            # who cares about blank lines …
+            # … at end of files
+            "W391"
+            # … between functions
+            "E302"
+            "E305"
+          ];
+        in
+        list FlakeError (ignoreTheseErrors ++ flakeIgnore);
+    };
+
+  # TODO: add the same flake check as the pyhon3 writer
+  python3Lib = { name, libraries ? (_: [ ]) }: moduleString:
+    let
+      srcTree = depot.nix.runExecline.local name { stdin = moduleString; } [
+        "importas"
+        "out"
+        "out"
+        "if"
+        [ bins.s6-mkdir "-p" "\${out}/${name}" ]
+        "if"
+        [
+          "redirfd"
+          "-w"
+          "1"
+          "\${out}/setup.py"
+          bins.printf
+          ''
+            from distutils.core import setup
+
+            setup(
+              name='%s',
+              packages=['%s']
+            )
+          ''
+          name
+          name
+        ]
+        "if"
+        [
+          # redirect stdin to the init py
+          "redirfd"
+          "-w"
+          "1"
+          "\${out}/${name}/__init__.py"
+          bins.s6-cat
+        ]
+      ];
+    in
+    pkgs.python3Packages.buildPythonPackage {
+      inherit name;
+      src = srcTree;
+      propagatedBuildInputs = libraries pkgs.python3Packages;
+      doCheck = false;
+    };
+
+
+in
+{
+  inherit
+    python3
+    python3Lib
+    ;
+}
diff --git a/users/Profpatsch/writers/tests/default.nix b/users/Profpatsch/writers/tests/default.nix
new file mode 100644
index 0000000000..d0d62d3b0e
--- /dev/null
+++ b/users/Profpatsch/writers/tests/default.nix
@@ -0,0 +1,56 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.users.Profpatsch.writers)
+    python3Lib
+    python3
+    ;
+
+  inherit (pkgs)
+    coreutils
+    ;
+
+  run = drv: depot.nix.runExecline.local "run-${drv.name}" { } [
+    "if"
+    [ drv ]
+    "importas"
+    "out"
+    "out"
+    "${coreutils}/bin/touch"
+    "$out"
+  ];
+
+  pythonTransitiveLib = python3Lib
+    {
+      name = "transitive";
+    } ''
+    def transitive(s):
+      return s + " 1 2 3"
+  '';
+
+  pythonTestLib = python3Lib
+    {
+      name = "test_lib";
+      libraries = _: [ pythonTransitiveLib ];
+    } ''
+    import transitive
+    def test():
+      return transitive.transitive("test")
+  '';
+
+  pythonWithLib = run (python3
+    {
+      name = "python-with-lib";
+      libraries = _: [ pythonTestLib ];
+    } ''
+    import test_lib
+
+    assert(test_lib.test() == "test 1 2 3")
+  '');
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    pythonWithLib
+    ;
+}
diff --git a/users/Profpatsch/ytextr/create-symlink-farm.nix b/users/Profpatsch/ytextr/create-symlink-farm.nix
new file mode 100644
index 0000000000..7b3a45b916
--- /dev/null
+++ b/users/Profpatsch/ytextr/create-symlink-farm.nix
@@ -0,0 +1,19 @@
+{
+  # list of package attribute names to get at run time
+  packageNamesAtRuntimeJsonPath
+,
+}:
+let
+  pkgs = import <nixpkgs> { };
+
+  getPkg = pkgName: pkgs.${pkgName};
+
+  packageNamesAtRuntime = builtins.fromJSON (builtins.readFile packageNamesAtRuntimeJsonPath);
+
+  runtime = map getPkg packageNamesAtRuntime;
+
+in
+pkgs.symlinkJoin {
+  name = "symlink-farm";
+  paths = runtime;
+}
diff --git a/users/Profpatsch/ytextr/default.nix b/users/Profpatsch/ytextr/default.nix
new file mode 100644
index 0000000000..ac630603b9
--- /dev/null
+++ b/users/Profpatsch/ytextr/default.nix
@@ -0,0 +1,82 @@
+{ depot, pkgs, lib, ... }:
+
+# ytextr is a wrapper arount yt-dlp (previously youtube-dl)
+# that extracts a single video according to my preferred settings.
+#
+# It will be sandboxed to the current directory, since I don’t particularly
+# trust the massive codebase of that tool (with hundreds of contributors).
+#
+# Since the rules for downloading videos is usually against the wishes
+# of proprietary vendors, and a video is many megabytes anyway,
+# it will be fetched from the most recent nixpkgs unstable channel before running.
+
+let
+  bins = depot.nix.getBins pkgs.nix [ "nix-build" ]
+    // depot.nix.getBins pkgs.bubblewrap [ "bwrap" ];
+
+  # Run a command, with the given packages in scope, and `packageNamesAtRuntime` being fetched at the start in the given nix `channel`.
+  nix-run-with-channel =
+    {
+      # The channel to get `packageNamesAtRuntime` from
+      channel
+    , # executable to run with `packageNamesAtRuntime` in PATH
+      # and the argv
+      executable
+    , # A list of nixpkgs package attribute names that should be put into PATH when running `command`.
+      packageNamesAtRuntime
+    ,
+    }: depot.nix.writeExecline "nix-run-with-channel-${channel}" { } [
+      # TODO: prevent race condition by writing a temporary gc root
+      "backtick"
+      "-iE"
+      "storepath"
+      [
+        bins.nix-build
+        "-I"
+        "nixpkgs=channel:${channel}"
+        "--arg"
+        "packageNamesAtRuntimeJsonPath"
+        (pkgs.writeText "packageNamesAtRuntime.json" (builtins.toJSON packageNamesAtRuntime))
+        ./create-symlink-farm.nix
+      ]
+      "importas"
+      "-ui"
+      "PATH"
+      "PATH"
+      "export"
+      "PATH"
+      "\${storepath}/bin:\${PATH}"
+      executable
+      "$@"
+    ];
+
+in
+nix-run-with-channel {
+  channel = "nixos-unstable";
+  packageNamesAtRuntime = [ "yt-dlp" ];
+  executable = depot.nix.writeExecline "ytextr" { readNArgs = 1; } [
+    "getcwd"
+    "-E"
+    "cwd"
+    bins.bwrap
+    "--ro-bind"
+    "/nix/store"
+    "/nix/store"
+    "--ro-bind"
+    "/etc"
+    "/etc"
+    "--bind"
+    "$cwd"
+    "$cwd"
+    "yt-dlp"
+    "--no-playlist"
+    "--write-sub"
+    "--all-subs"
+    "--embed-subs"
+    "--merge-output-format"
+    "mkv"
+    "-f"
+    "bestvideo[height<=?1080]+bestaudio/best"
+    "$1"
+  ];
+}
diff --git a/users/cynthia/OWNERS b/users/cynthia/OWNERS
new file mode 100644
index 0000000000..da62f3777a
--- /dev/null
+++ b/users/cynthia/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - cynthia
diff --git a/users/cynthia/keys.nix b/users/cynthia/keys.nix
new file mode 100644
index 0000000000..e2f4ce488c
--- /dev/null
+++ b/users/cynthia/keys.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  all = [
+    "cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICsj3W6QczgxE3s5GGT8qg0aLrCM+QeRnSq9RkiZtKvz meow"
+  ];
+}
diff --git a/users/edef/OWNERS b/users/edef/OWNERS
new file mode 100644
index 0000000000..05f7639c89
--- /dev/null
+++ b/users/edef/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - edef
diff --git a/users/edef/depot-scan/default.nix b/users/edef/depot-scan/default.nix
new file mode 100644
index 0000000000..a9c0f382ff
--- /dev/null
+++ b/users/edef/depot-scan/default.nix
@@ -0,0 +1,12 @@
+{ pkgs, ... }:
+
+pkgs.writeShellScriptBin "depot-scan" ''
+  set -euo pipefail
+
+  path="''${1:-$(git rev-parse --show-prefix)}"
+  path="''${path%%/}"
+  attr="''${path//\//.}"
+  root="$(git rev-parse --show-toplevel)"
+  echo "scanning //$path" >&2
+  nix-instantiate -E "import ${./wrap.nix} $root" -A "$attr" -vv 2> >(${pkgs.perl}/bin/perl ${./depot-scan.pl}) >&2
+''
diff --git a/users/edef/depot-scan/depot-scan.pl b/users/edef/depot-scan/depot-scan.pl
new file mode 100755
index 0000000000..8808e2eb00
--- /dev/null
+++ b/users/edef/depot-scan/depot-scan.pl
@@ -0,0 +1,11 @@
+#! /usr/bin/env -S perl -ln
+use strict;
+
+if (/^evaluating file '(.*)'$/ or
+    /^copied source '(.*)' -> '.*'$/ or
+    /^trace: depot-scan '(.*)'$/) {
+    print $1;
+    next;
+}
+
+print STDERR unless /^instantiated '.*' -> '.*'$/;
diff --git a/users/edef/depot-scan/wrap.nix b/users/edef/depot-scan/wrap.nix
new file mode 100644
index 0000000000..77362b3f61
--- /dev/null
+++ b/users/edef/depot-scan/wrap.nix
@@ -0,0 +1,16 @@
+# this wraps import to override readFile and readDir to trace the files it touches
+# technique inspired by lorri
+let
+
+  global = {
+    import = global.scopedImport { };
+    scopedImport = x: builtins.scopedImport (global // x);
+    builtins = builtins // {
+      inherit (global) import scopedImport;
+      readFile = path: builtins.trace "depot-scan '${toString path}'" (builtins.readFile path);
+      readDir = path: builtins.trace "depot-scan '${toString path}'" (builtins.readDir path);
+    };
+  };
+
+in
+global.import
diff --git a/users/edef/keys.nix b/users/edef/keys.nix
new file mode 100644
index 0000000000..53e88c9e73
--- /dev/null
+++ b/users/edef/keys.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  all = [
+    "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+  ];
+}
diff --git a/users/ericvolp12/OWNERS b/users/ericvolp12/OWNERS
new file mode 100644
index 0000000000..5a012a695b
--- /dev/null
+++ b/users/ericvolp12/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - ericvolp12
diff --git a/users/eta/OWNERS b/users/eta/OWNERS
new file mode 100644
index 0000000000..f212e89e2a
--- /dev/null
+++ b/users/eta/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - eta
diff --git a/users/eta/keys.nix b/users/eta/keys.nix
new file mode 100644
index 0000000000..ebdc8479a5
--- /dev/null
+++ b/users/eta/keys.nix
@@ -0,0 +1,12 @@
+{ ... }:
+
+let
+  keys = {
+    yubikey4 = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD6E1wuWaXQARNoLnmlOJndwI7/ms3Ga7MJxsUvFtaSiy3g8h/hz4WgyR7YT+hUYjFihh/YkGS9Zy9aEqAa5zBGLcZtgj1O0qOl2joynm679zdlcwAart74fXSJYYupT9tFeXXeWLO1g054lVJ5xZ9KLpBBk+6yzlmmm5KuoitKBqBbadzsqAeKhNn1Nq9ITPU4vxTFk+sXp/nxk/JoUOM8S2N4YuoX9OVenDHKh9DtOcvDZhlosGmunO33/YaU2XB95ZE6cNhEtVlkbyR3a2SsAYz1qGgfH0HSyoK3LJoAM4Aiz99ktuKiI/zMy4k4TV00OCi1sCPEjzUoijZRZt5FMH/TVr9dJROVjHcL9g9//fW3jwqojf7uuJFlTJb47RxjTk4Jb4F6K7HhOs7bgh3WuOjvhyRYbCYcg+RfnwjJk+hfM5GcjZ8J4UZdNc5LyIcfH8W1v9DADBCgz7QcmfrfMloYtEgjK/5XVrtBtiMtUOgpfKujawF55d1Vj26+CxeID8NHMXzZYEMeyRpi/WXlC+lq1Wx4Fj8gvideOw/3gAdj2G3SJWdSPk8XpIFQ1fm3tXB0ltyV5TszIJhfMnmsKJeEm3YlTCR1sMW7nr3wEdMqa6mpcWZTWU+dppmAGr2c+OGSnXkCi7Z2h/YJE6X+izrOrqRspG2fCM8GlfRFWw== cardno:000607469311";
+    yubikey5 = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKCJx23px0Vknw1NlD+arcqeVXxcogPUMJgF/PGp6wA/tg7hHUKs2udC+gDMYlxQ9IpnWOwZ/9yvqzTDwUU3R/4= YubiKey #15026444 PIV Slot 9a";
+  };
+  configs = {
+    whitby = [ keys.yubikey4 keys.yubikey5 ];
+  };
+in
+configs
diff --git a/users/firefly/OWNERS b/users/firefly/OWNERS
new file mode 100644
index 0000000000..55d62a5723
--- /dev/null
+++ b/users/firefly/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - firefly
diff --git a/users/firefly/keys.nix b/users/firefly/keys.nix
new file mode 100644
index 0000000000..1d7467a074
--- /dev/null
+++ b/users/firefly/keys.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+rec {
+  as = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN9i8fs10/BjNEqFXD+3fQeQ0SuHnQx4WpuqUg4caeed firefly@as";
+
+  whitby = [ as ];
+}
diff --git a/users/flokli/OWNERS b/users/flokli/OWNERS
new file mode 100644
index 0000000000..63e0fbda3c
--- /dev/null
+++ b/users/flokli/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - flokli
diff --git a/users/flokli/keys.nix b/users/flokli/keys.nix
new file mode 100644
index 0000000000..790c9862f8
--- /dev/null
+++ b/users/flokli/keys.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  all = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPTVTXOutUZZjXLB0lUSgeKcSY/8mxKkC0ingGK1whD2 flokli"
+  ];
+}
diff --git a/users/grfn/OWNERS b/users/grfn/OWNERS
new file mode 100644
index 0000000000..da7ac5cb9e
--- /dev/null
+++ b/users/grfn/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - grfn
diff --git a/users/grfn/achilles/.envrc b/users/grfn/achilles/.envrc
new file mode 100644
index 0000000000..051d09d292
--- /dev/null
+++ b/users/grfn/achilles/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
diff --git a/users/grfn/achilles/.gitignore b/users/grfn/achilles/.gitignore
new file mode 100644
index 0000000000..ea8c4bf7f3
--- /dev/null
+++ b/users/grfn/achilles/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/users/grfn/achilles/Cargo.lock b/users/grfn/achilles/Cargo.lock
new file mode 100644
index 0000000000..3c767db1e4
--- /dev/null
+++ b/users/grfn/achilles/Cargo.lock
@@ -0,0 +1,885 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "achilles"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bimap",
+ "clap",
+ "crate-root",
+ "derive_more",
+ "inkwell",
+ "itertools",
+ "lazy_static",
+ "llvm-sys",
+ "nom",
+ "nom-trace",
+ "pratt",
+ "pretty_assertions",
+ "proptest",
+ "test-strategy",
+ "thiserror",
+ "void",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bimap"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b"
+
+[[package]]
+name = "bit-set"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitvec"
+version = "0.19.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "3.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_lex",
+ "indexmap",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "crate-root"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59c6fe4622b269032d2c5140a592d67a9c409031d286174fcde172fbed86f0d3"
+
+[[package]]
+name = "ctor"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "inkwell"
+version = "0.1.0"
+source = "git+https://github.com/TheDan64/inkwell?branch=master#6ab2b19e1b90be55fa4f9f056f29bd1ed557b990"
+dependencies = [
+ "either",
+ "inkwell_internals",
+ "libc",
+ "llvm-sys",
+ "once_cell",
+ "parking_lot",
+]
+
+[[package]]
+name = "inkwell_internals"
+version = "0.5.0"
+source = "git+https://github.com/TheDan64/inkwell?branch=master#6ab2b19e1b90be55fa4f9f056f29bd1ed557b990"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "cfg-if",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
+
+[[package]]
+name = "llvm-sys"
+version = "110.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b918288a585ac36703abefcbc5d4c43137b604ec0c2d39abefb55e25c7501dc"
+dependencies = [
+ "cc",
+ "lazy_static",
+ "libc",
+ "regex",
+ "semver 0.11.0",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "nom"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
+dependencies = [
+ "bitvec",
+ "funty",
+ "lexical-core",
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "nom-trace"
+version = "0.2.1"
+source = "git+https://github.com/glittershark/nom-trace?branch=nom-6#6168d2e15cc51efd12d80260159b76a764dba138"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+
+[[package]]
+name = "output_vt100"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "pratt"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bbc12f7936a7b195790dd6d9b982b66c54f45ff6766decf25c44cac302dce"
+
+[[package]]
+name = "pretty_assertions"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b"
+dependencies = [
+ "ansi_term",
+ "ctor",
+ "diff",
+ "output_vt100",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proptest"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
+dependencies = [
+ "bit-set",
+ "bitflags",
+ "byteorder",
+ "lazy_static",
+ "num-traits",
+ "quick-error 2.0.1",
+ "rand",
+ "rand_chacha",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver 1.0.9",
+]
+
+[[package]]
+name = "rusty-fork"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
+dependencies = [
+ "fnv",
+ "quick-error 1.2.3",
+ "tempfile",
+ "wait-timeout",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd"
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "structmeta"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd9c2155aa89fb2c2cb87d99a610c689e7c47099b3e9f1c8a8f53faf4e3d2e3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "structmeta-derive",
+ "syn",
+]
+
+[[package]]
+name = "structmeta-derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bafede0d0a2f21910f36d47b1558caae3076ed80f6f3ad0fc85a91e6ba7e5938"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "test-strategy"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22c726321a7c108ca1de4ed2e6a362ead7193ecfbe0b326c5dff602b65a09e6a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "structmeta",
+ "syn",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
diff --git a/users/grfn/achilles/Cargo.toml b/users/grfn/achilles/Cargo.toml
new file mode 100644
index 0000000000..f091399a0d
--- /dev/null
+++ b/users/grfn/achilles/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "achilles"
+version = "0.1.0"
+authors = ["Griffin Smith <root@gws.fyi>"]
+edition = "2018"
+
+[dependencies]
+anyhow = "1.0.38"
+bimap = "0.6.0"
+clap = "3.0.0-beta.2"
+derive_more = "0.99.11"
+inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", features = ["llvm11-0"] }
+itertools = "0.10.0"
+lazy_static = "1.4.0"
+llvm-sys = "110.0.1"
+nom = "6.1.2"
+nom-trace = { git = "https://github.com/glittershark/nom-trace", branch = "nom-6" }
+pratt = "0.3.0"
+proptest = "1.0.0"
+test-strategy = "0.1.1"
+thiserror = "1.0.24"
+void = "1.0.2"
+
+[dev-dependencies]
+crate-root = "0.1.3"
+pretty_assertions = "0.7.1"
diff --git a/users/grfn/achilles/ach/.gitignore b/users/grfn/achilles/ach/.gitignore
new file mode 100644
index 0000000000..ac5296ebbd
--- /dev/null
+++ b/users/grfn/achilles/ach/.gitignore
@@ -0,0 +1,7 @@
+*.ll
+*.o
+
+functions
+simple
+externs
+units
diff --git a/users/grfn/achilles/ach/Makefile b/users/grfn/achilles/ach/Makefile
new file mode 100644
index 0000000000..3a8cd2865e
--- /dev/null
+++ b/users/grfn/achilles/ach/Makefile
@@ -0,0 +1,15 @@
+default: simple
+
+%.ll: %.ach
+	cargo run -- compile $< -o $@ -f llvm
+
+%.o: %.ll
+	llc $< -o $@ -filetype=obj
+
+%: %.o
+	clang $< -o $@
+
+.PHONY: clean
+
+clean:
+	@rm -f *.ll *.o simple functions
diff --git a/users/grfn/achilles/ach/externs.ach b/users/grfn/achilles/ach/externs.ach
new file mode 100644
index 0000000000..faf8ce90e3
--- /dev/null
+++ b/users/grfn/achilles/ach/externs.ach
@@ -0,0 +1,5 @@
+extern puts : fn cstring -> int
+
+fn main =
+    let _ = puts "foobar"
+    in 0
diff --git a/users/grfn/achilles/ach/functions.ach b/users/grfn/achilles/ach/functions.ach
new file mode 100644
index 0000000000..dc6e7a1f3e
--- /dev/null
+++ b/users/grfn/achilles/ach/functions.ach
@@ -0,0 +1,8 @@
+ty id : fn a -> a
+fn id x = x
+
+ty plus : fn int -> int
+fn plus (x: int) (y: int) = x + y
+
+ty main : fn -> int
+fn main = plus (id 2) 7
diff --git a/users/grfn/achilles/ach/simple.ach b/users/grfn/achilles/ach/simple.ach
new file mode 100644
index 0000000000..20f1677235
--- /dev/null
+++ b/users/grfn/achilles/ach/simple.ach
@@ -0,0 +1 @@
+fn main = let x = 2; y = 3 in x + y
diff --git a/users/grfn/achilles/ach/units.ach b/users/grfn/achilles/ach/units.ach
new file mode 100644
index 0000000000..70635d978c
--- /dev/null
+++ b/users/grfn/achilles/ach/units.ach
@@ -0,0 +1,7 @@
+extern puts : fn cstring -> int
+
+ty print : fn cstring -> ()
+fn print x = let _ = puts x in ()
+
+ty main : fn -> int
+fn main = let _ = print "hi" in 0
diff --git a/users/grfn/achilles/default.nix b/users/grfn/achilles/default.nix
new file mode 100644
index 0000000000..6dab0a5a62
--- /dev/null
+++ b/users/grfn/achilles/default.nix
@@ -0,0 +1,27 @@
+{ depot, pkgs, ... }:
+
+let
+  llvmPackages = pkgs.llvmPackages_11;
+in
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+
+  buildInputs = [
+    llvmPackages.clang
+    llvmPackages.llvm
+    llvmPackages.bintools
+    llvmPackages.libclang.lib
+  ] ++ (with pkgs; [
+    zlib
+    ncurses
+    libxml2
+    libffi
+    pkgconfig
+  ]);
+
+  doCheck = true;
+
+  # Trouble linking against LLVM, maybe since rustc's llvmPackages got bumped?
+  meta.ci.skip = true;
+}
diff --git a/users/grfn/achilles/shell.nix b/users/grfn/achilles/shell.nix
new file mode 100644
index 0000000000..1434cf8a32
--- /dev/null
+++ b/users/grfn/achilles/shell.nix
@@ -0,0 +1,18 @@
+with (import ../../.. { }).third_party.nixpkgs;
+
+mkShell {
+  buildInputs = [
+    clang_11
+    llvm_11.lib
+    llvmPackages_11.bintools
+    llvmPackages_11.clang
+    llvmPackages_11.libclang.lib
+    zlib
+    ncurses
+    libxml2
+    libffi
+    pkg-config
+  ];
+
+  LLVM_SYS_110_PREFIX = llvmPackages_11.bintools;
+}
diff --git a/users/grfn/achilles/src/ast/hir.rs b/users/grfn/achilles/src/ast/hir.rs
new file mode 100644
index 0000000000..cdfaef567d
--- /dev/null
+++ b/users/grfn/achilles/src/ast/hir.rs
@@ -0,0 +1,364 @@
+use std::collections::HashMap;
+
+use itertools::Itertools;
+
+use super::{BinaryOperator, Ident, Literal, UnaryOperator};
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Pattern<'a, T> {
+    Id(Ident<'a>, T),
+    Tuple(Vec<Pattern<'a, T>>),
+}
+
+impl<'a, T> Pattern<'a, T> {
+    pub fn to_owned(&self) -> Pattern<'static, T>
+    where
+        T: Clone,
+    {
+        match self {
+            Pattern::Id(id, t) => Pattern::Id(id.to_owned(), t.clone()),
+            Pattern::Tuple(pats) => {
+                Pattern::Tuple(pats.into_iter().map(Pattern::to_owned).collect())
+            }
+        }
+    }
+
+    pub fn traverse_type<F, U, E>(self, f: F) -> Result<Pattern<'a, U>, E>
+    where
+        F: Fn(T) -> Result<U, E> + Clone,
+    {
+        match self {
+            Pattern::Id(id, t) => Ok(Pattern::Id(id, f(t)?)),
+            Pattern::Tuple(pats) => Ok(Pattern::Tuple(
+                pats.into_iter()
+                    .map(|pat| pat.traverse_type(f.clone()))
+                    .collect::<Result<Vec<_>, _>>()?,
+            )),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Binding<'a, T> {
+    pub pat: Pattern<'a, T>,
+    pub body: Expr<'a, T>,
+}
+
+impl<'a, T> Binding<'a, T> {
+    fn to_owned(&self) -> Binding<'static, T>
+    where
+        T: Clone,
+    {
+        Binding {
+            pat: self.pat.to_owned(),
+            body: self.body.to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Expr<'a, T> {
+    Ident(Ident<'a>, T),
+
+    Literal(Literal<'a>, T),
+
+    Tuple(Vec<Expr<'a, T>>, T),
+
+    UnaryOp {
+        op: UnaryOperator,
+        rhs: Box<Expr<'a, T>>,
+        type_: T,
+    },
+
+    BinaryOp {
+        lhs: Box<Expr<'a, T>>,
+        op: BinaryOperator,
+        rhs: Box<Expr<'a, T>>,
+        type_: T,
+    },
+
+    Let {
+        bindings: Vec<Binding<'a, T>>,
+        body: Box<Expr<'a, T>>,
+        type_: T,
+    },
+
+    If {
+        condition: Box<Expr<'a, T>>,
+        then: Box<Expr<'a, T>>,
+        else_: Box<Expr<'a, T>>,
+        type_: T,
+    },
+
+    Fun {
+        type_args: Vec<Ident<'a>>,
+        args: Vec<(Ident<'a>, T)>,
+        body: Box<Expr<'a, T>>,
+        type_: T,
+    },
+
+    Call {
+        fun: Box<Expr<'a, T>>,
+        type_args: HashMap<Ident<'a>, T>,
+        args: Vec<Expr<'a, T>>,
+        type_: T,
+    },
+}
+
+impl<'a, T> Expr<'a, T> {
+    pub fn type_(&self) -> &T {
+        match self {
+            Expr::Ident(_, t) => t,
+            Expr::Literal(_, t) => t,
+            Expr::Tuple(_, t) => t,
+            Expr::UnaryOp { type_, .. } => type_,
+            Expr::BinaryOp { type_, .. } => type_,
+            Expr::Let { type_, .. } => type_,
+            Expr::If { type_, .. } => type_,
+            Expr::Fun { type_, .. } => type_,
+            Expr::Call { type_, .. } => type_,
+        }
+    }
+
+    pub fn traverse_type<F, U, E>(self, f: F) -> Result<Expr<'a, U>, E>
+    where
+        F: Fn(T) -> Result<U, E> + Clone,
+    {
+        match self {
+            Expr::Ident(id, t) => Ok(Expr::Ident(id, f(t)?)),
+            Expr::Literal(lit, t) => Ok(Expr::Literal(lit, f(t)?)),
+            Expr::UnaryOp { op, rhs, type_ } => Ok(Expr::UnaryOp {
+                op,
+                rhs: Box::new(rhs.traverse_type(f.clone())?),
+                type_: f(type_)?,
+            }),
+            Expr::BinaryOp {
+                lhs,
+                op,
+                rhs,
+                type_,
+            } => Ok(Expr::BinaryOp {
+                lhs: Box::new(lhs.traverse_type(f.clone())?),
+                op,
+                rhs: Box::new(rhs.traverse_type(f.clone())?),
+                type_: f(type_)?,
+            }),
+            Expr::Let {
+                bindings,
+                body,
+                type_,
+            } => Ok(Expr::Let {
+                bindings: bindings
+                    .into_iter()
+                    .map(|Binding { pat, body }| {
+                        Ok(Binding {
+                            pat: pat.traverse_type(f.clone())?,
+                            body: body.traverse_type(f.clone())?,
+                        })
+                    })
+                    .collect::<Result<Vec<_>, E>>()?,
+                body: Box::new(body.traverse_type(f.clone())?),
+                type_: f(type_)?,
+            }),
+            Expr::If {
+                condition,
+                then,
+                else_,
+                type_,
+            } => Ok(Expr::If {
+                condition: Box::new(condition.traverse_type(f.clone())?),
+                then: Box::new(then.traverse_type(f.clone())?),
+                else_: Box::new(else_.traverse_type(f.clone())?),
+                type_: f(type_)?,
+            }),
+            Expr::Fun {
+                args,
+                type_args,
+                body,
+                type_,
+            } => Ok(Expr::Fun {
+                args: args
+                    .into_iter()
+                    .map(|(id, t)| Ok((id, f.clone()(t)?)))
+                    .collect::<Result<Vec<_>, E>>()?,
+                type_args,
+                body: Box::new(body.traverse_type(f.clone())?),
+                type_: f(type_)?,
+            }),
+            Expr::Call {
+                fun,
+                type_args,
+                args,
+                type_,
+            } => Ok(Expr::Call {
+                fun: Box::new(fun.traverse_type(f.clone())?),
+                type_args: type_args
+                    .into_iter()
+                    .map(|(id, ty)| Ok((id, f.clone()(ty)?)))
+                    .collect::<Result<HashMap<_, _>, E>>()?,
+                args: args
+                    .into_iter()
+                    .map(|e| e.traverse_type(f.clone()))
+                    .collect::<Result<Vec<_>, E>>()?,
+                type_: f(type_)?,
+            }),
+            Expr::Tuple(members, t) => Ok(Expr::Tuple(
+                members
+                    .into_iter()
+                    .map(|t| t.traverse_type(f.clone()))
+                    .try_collect()?,
+                f(t)?,
+            )),
+        }
+    }
+
+    pub fn to_owned(&self) -> Expr<'static, T>
+    where
+        T: Clone,
+    {
+        match self {
+            Expr::Ident(id, t) => Expr::Ident(id.to_owned(), t.clone()),
+            Expr::Literal(lit, t) => Expr::Literal(lit.to_owned(), t.clone()),
+            Expr::UnaryOp { op, rhs, type_ } => Expr::UnaryOp {
+                op: *op,
+                rhs: Box::new((**rhs).to_owned()),
+                type_: type_.clone(),
+            },
+            Expr::BinaryOp {
+                lhs,
+                op,
+                rhs,
+                type_,
+            } => Expr::BinaryOp {
+                lhs: Box::new((**lhs).to_owned()),
+                op: *op,
+                rhs: Box::new((**rhs).to_owned()),
+                type_: type_.clone(),
+            },
+            Expr::Let {
+                bindings,
+                body,
+                type_,
+            } => Expr::Let {
+                bindings: bindings.iter().map(|b| b.to_owned()).collect(),
+                body: Box::new((**body).to_owned()),
+                type_: type_.clone(),
+            },
+            Expr::If {
+                condition,
+                then,
+                else_,
+                type_,
+            } => Expr::If {
+                condition: Box::new((**condition).to_owned()),
+                then: Box::new((**then).to_owned()),
+                else_: Box::new((**else_).to_owned()),
+                type_: type_.clone(),
+            },
+            Expr::Fun {
+                args,
+                type_args,
+                body,
+                type_,
+            } => Expr::Fun {
+                args: args
+                    .iter()
+                    .map(|(id, t)| (id.to_owned(), t.clone()))
+                    .collect(),
+                type_args: type_args.iter().map(|arg| arg.to_owned()).collect(),
+                body: Box::new((**body).to_owned()),
+                type_: type_.clone(),
+            },
+            Expr::Call {
+                fun,
+                type_args,
+                args,
+                type_,
+            } => Expr::Call {
+                fun: Box::new((**fun).to_owned()),
+                type_args: type_args
+                    .iter()
+                    .map(|(id, t)| (id.to_owned(), t.clone()))
+                    .collect(),
+                args: args.iter().map(|e| e.to_owned()).collect(),
+                type_: type_.clone(),
+            },
+            Expr::Tuple(members, t) => {
+                Expr::Tuple(members.into_iter().map(Expr::to_owned).collect(), t.clone())
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Decl<'a, T> {
+    Fun {
+        name: Ident<'a>,
+        type_args: Vec<Ident<'a>>,
+        args: Vec<(Ident<'a>, T)>,
+        body: Box<Expr<'a, T>>,
+        type_: T,
+    },
+
+    Extern {
+        name: Ident<'a>,
+        arg_types: Vec<T>,
+        ret_type: T,
+    },
+}
+
+impl<'a, T> Decl<'a, T> {
+    pub fn name(&self) -> &Ident<'a> {
+        match self {
+            Decl::Fun { name, .. } => name,
+            Decl::Extern { name, .. } => name,
+        }
+    }
+
+    pub fn set_name(&mut self, new_name: Ident<'a>) {
+        match self {
+            Decl::Fun { name, .. } => *name = new_name,
+            Decl::Extern { name, .. } => *name = new_name,
+        }
+    }
+
+    pub fn type_(&self) -> Option<&T> {
+        match self {
+            Decl::Fun { type_, .. } => Some(type_),
+            Decl::Extern { .. } => None,
+        }
+    }
+
+    pub fn traverse_type<F, U, E>(self, f: F) -> Result<Decl<'a, U>, E>
+    where
+        F: Fn(T) -> Result<U, E> + Clone,
+    {
+        match self {
+            Decl::Fun {
+                name,
+                type_args,
+                args,
+                body,
+                type_,
+            } => Ok(Decl::Fun {
+                name,
+                type_args,
+                args: args
+                    .into_iter()
+                    .map(|(id, t)| Ok((id, f(t)?)))
+                    .try_collect()?,
+                body: Box::new(body.traverse_type(f.clone())?),
+                type_: f(type_)?,
+            }),
+            Decl::Extern {
+                name,
+                arg_types,
+                ret_type,
+            } => Ok(Decl::Extern {
+                name,
+                arg_types: arg_types.into_iter().map(f.clone()).try_collect()?,
+                ret_type: f(ret_type)?,
+            }),
+        }
+    }
+}
diff --git a/users/grfn/achilles/src/ast/mod.rs b/users/grfn/achilles/src/ast/mod.rs
new file mode 100644
index 0000000000..5438d29d2c
--- /dev/null
+++ b/users/grfn/achilles/src/ast/mod.rs
@@ -0,0 +1,484 @@
+pub(crate) mod hir;
+
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::convert::TryFrom;
+use std::fmt::{self, Display, Formatter};
+
+use itertools::Itertools;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct InvalidIdentifier<'a>(Cow<'a, str>);
+
+#[derive(Debug, PartialEq, Eq, Hash, Clone)]
+pub struct Ident<'a>(pub Cow<'a, str>);
+
+impl<'a> From<&'a Ident<'a>> for &'a str {
+    fn from(id: &'a Ident<'a>) -> Self {
+        id.0.as_ref()
+    }
+}
+
+impl<'a> Display for Ident<'a> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl<'a> Ident<'a> {
+    pub fn to_owned(&self) -> Ident<'static> {
+        Ident(Cow::Owned(self.0.clone().into_owned()))
+    }
+
+    /// Construct an identifier from a &str without checking that it's a valid identifier
+    pub fn from_str_unchecked(s: &'a str) -> Self {
+        debug_assert!(is_valid_identifier(s));
+        Self(Cow::Borrowed(s))
+    }
+
+    pub fn from_string_unchecked(s: String) -> Self {
+        debug_assert!(is_valid_identifier(&s));
+        Self(Cow::Owned(s))
+    }
+}
+
+pub fn is_valid_identifier<S>(s: &S) -> bool
+where
+    S: AsRef<str> + ?Sized,
+{
+    s.as_ref()
+        .chars()
+        .any(|c| !c.is_alphanumeric() || !"_".contains(c))
+}
+
+impl<'a> TryFrom<&'a str> for Ident<'a> {
+    type Error = InvalidIdentifier<'a>;
+
+    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
+        if is_valid_identifier(s) {
+            Ok(Ident(Cow::Borrowed(s)))
+        } else {
+            Err(InvalidIdentifier(Cow::Borrowed(s)))
+        }
+    }
+}
+
+impl<'a> TryFrom<String> for Ident<'a> {
+    type Error = InvalidIdentifier<'static>;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        if is_valid_identifier(&s) {
+            Ok(Ident(Cow::Owned(s)))
+        } else {
+            Err(InvalidIdentifier(Cow::Owned(s)))
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub enum BinaryOperator {
+    /// `+`
+    Add,
+
+    /// `-`
+    Sub,
+
+    /// `*`
+    Mul,
+
+    /// `/`
+    Div,
+
+    /// `^`
+    Pow,
+
+    /// `==`
+    Equ,
+
+    /// `!=`
+    Neq,
+}
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub enum UnaryOperator {
+    /// !
+    Not,
+
+    /// -
+    Neg,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Literal<'a> {
+    Unit,
+    Int(u64),
+    Bool(bool),
+    String(Cow<'a, str>),
+}
+
+impl<'a> Literal<'a> {
+    pub fn to_owned(&self) -> Literal<'static> {
+        match self {
+            Literal::Int(i) => Literal::Int(*i),
+            Literal::Bool(b) => Literal::Bool(*b),
+            Literal::String(s) => Literal::String(Cow::Owned(s.clone().into_owned())),
+            Literal::Unit => Literal::Unit,
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Pattern<'a> {
+    Id(Ident<'a>),
+    Tuple(Vec<Pattern<'a>>),
+}
+
+impl<'a> Pattern<'a> {
+    pub fn to_owned(&self) -> Pattern<'static> {
+        match self {
+            Pattern::Id(id) => Pattern::Id(id.to_owned()),
+            Pattern::Tuple(pats) => Pattern::Tuple(pats.iter().map(Pattern::to_owned).collect()),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Binding<'a> {
+    pub pat: Pattern<'a>,
+    pub type_: Option<Type<'a>>,
+    pub body: Expr<'a>,
+}
+
+impl<'a> Binding<'a> {
+    fn to_owned(&self) -> Binding<'static> {
+        Binding {
+            pat: self.pat.to_owned(),
+            type_: self.type_.as_ref().map(|t| t.to_owned()),
+            body: self.body.to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Expr<'a> {
+    Ident(Ident<'a>),
+
+    Literal(Literal<'a>),
+
+    UnaryOp {
+        op: UnaryOperator,
+        rhs: Box<Expr<'a>>,
+    },
+
+    BinaryOp {
+        lhs: Box<Expr<'a>>,
+        op: BinaryOperator,
+        rhs: Box<Expr<'a>>,
+    },
+
+    Let {
+        bindings: Vec<Binding<'a>>,
+        body: Box<Expr<'a>>,
+    },
+
+    If {
+        condition: Box<Expr<'a>>,
+        then: Box<Expr<'a>>,
+        else_: Box<Expr<'a>>,
+    },
+
+    Fun(Box<Fun<'a>>),
+
+    Call {
+        fun: Box<Expr<'a>>,
+        args: Vec<Expr<'a>>,
+    },
+
+    Tuple(Vec<Expr<'a>>),
+
+    Ascription {
+        expr: Box<Expr<'a>>,
+        type_: Type<'a>,
+    },
+}
+
+impl<'a> Expr<'a> {
+    pub fn to_owned(&self) -> Expr<'static> {
+        match self {
+            Expr::Ident(ref id) => Expr::Ident(id.to_owned()),
+            Expr::Literal(ref lit) => Expr::Literal(lit.to_owned()),
+            Expr::Tuple(ref members) => {
+                Expr::Tuple(members.into_iter().map(Expr::to_owned).collect())
+            }
+            Expr::UnaryOp { op, rhs } => Expr::UnaryOp {
+                op: *op,
+                rhs: Box::new((**rhs).to_owned()),
+            },
+            Expr::BinaryOp { lhs, op, rhs } => Expr::BinaryOp {
+                lhs: Box::new((**lhs).to_owned()),
+                op: *op,
+                rhs: Box::new((**rhs).to_owned()),
+            },
+            Expr::Let { bindings, body } => Expr::Let {
+                bindings: bindings.iter().map(|binding| binding.to_owned()).collect(),
+                body: Box::new((**body).to_owned()),
+            },
+            Expr::If {
+                condition,
+                then,
+                else_,
+            } => Expr::If {
+                condition: Box::new((**condition).to_owned()),
+                then: Box::new((**then).to_owned()),
+                else_: Box::new((**else_).to_owned()),
+            },
+            Expr::Fun(fun) => Expr::Fun(Box::new((**fun).to_owned())),
+            Expr::Call { fun, args } => Expr::Call {
+                fun: Box::new((**fun).to_owned()),
+                args: args.iter().map(|arg| arg.to_owned()).collect(),
+            },
+            Expr::Ascription { expr, type_ } => Expr::Ascription {
+                expr: Box::new((**expr).to_owned()),
+                type_: type_.to_owned(),
+            },
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Arg<'a> {
+    pub ident: Ident<'a>,
+    pub type_: Option<Type<'a>>,
+}
+
+impl<'a> Arg<'a> {
+    pub fn to_owned(&self) -> Arg<'static> {
+        Arg {
+            ident: self.ident.to_owned(),
+            type_: self.type_.as_ref().map(Type::to_owned),
+        }
+    }
+}
+
+impl<'a> TryFrom<&'a str> for Arg<'a> {
+    type Error = <Ident<'a> as TryFrom<&'a str>>::Error;
+
+    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+        Ok(Arg {
+            ident: Ident::try_from(value)?,
+            type_: None,
+        })
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Fun<'a> {
+    pub args: Vec<Arg<'a>>,
+    pub body: Expr<'a>,
+}
+
+impl<'a> Fun<'a> {
+    pub fn to_owned(&self) -> Fun<'static> {
+        Fun {
+            args: self.args.iter().map(|arg| arg.to_owned()).collect(),
+            body: self.body.to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Decl<'a> {
+    Fun {
+        name: Ident<'a>,
+        body: Fun<'a>,
+    },
+    Ascription {
+        name: Ident<'a>,
+        type_: Type<'a>,
+    },
+    Extern {
+        name: Ident<'a>,
+        type_: FunctionType<'a>,
+    },
+}
+
+////
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct FunctionType<'a> {
+    pub args: Vec<Type<'a>>,
+    pub ret: Box<Type<'a>>,
+}
+
+impl<'a> FunctionType<'a> {
+    pub fn to_owned(&self) -> FunctionType<'static> {
+        FunctionType {
+            args: self.args.iter().map(|a| a.to_owned()).collect(),
+            ret: Box::new((*self.ret).to_owned()),
+        }
+    }
+}
+
+impl<'a> Display for FunctionType<'a> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "fn {} -> {}", self.args.iter().join(", "), self.ret)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Type<'a> {
+    Int,
+    Float,
+    Bool,
+    CString,
+    Unit,
+    Tuple(Vec<Type<'a>>),
+    Var(Ident<'a>),
+    Function(FunctionType<'a>),
+}
+
+impl<'a> Type<'a> {
+    pub fn to_owned(&self) -> Type<'static> {
+        match self {
+            Type::Int => Type::Int,
+            Type::Float => Type::Float,
+            Type::Bool => Type::Bool,
+            Type::CString => Type::CString,
+            Type::Unit => Type::Unit,
+            Type::Var(v) => Type::Var(v.to_owned()),
+            Type::Function(f) => Type::Function(f.to_owned()),
+            Type::Tuple(members) => Type::Tuple(members.iter().map(Type::to_owned).collect()),
+        }
+    }
+
+    pub fn alpha_equiv(&self, other: &Self) -> bool {
+        fn do_alpha_equiv<'a>(
+            substs: &mut HashMap<&'a Ident<'a>, &'a Ident<'a>>,
+            lhs: &'a Type,
+            rhs: &'a Type,
+        ) -> bool {
+            match (lhs, rhs) {
+                (Type::Var(v1), Type::Var(v2)) => substs.entry(v1).or_insert(v2) == &v2,
+                (
+                    Type::Function(FunctionType {
+                        args: args1,
+                        ret: ret1,
+                    }),
+                    Type::Function(FunctionType {
+                        args: args2,
+                        ret: ret2,
+                    }),
+                ) => {
+                    args1.len() == args2.len()
+                        && args1
+                            .iter()
+                            .zip(args2)
+                            .all(|(a1, a2)| do_alpha_equiv(substs, a1, a2))
+                        && do_alpha_equiv(substs, ret1, ret2)
+                }
+                _ => lhs == rhs,
+            }
+        }
+
+        let mut substs = HashMap::new();
+        do_alpha_equiv(&mut substs, self, other)
+    }
+
+    pub fn traverse_type_vars<'b, F>(self, mut f: F) -> Type<'b>
+    where
+        F: FnMut(Ident<'a>) -> Type<'b> + Clone,
+    {
+        match self {
+            Type::Var(tv) => f(tv),
+            Type::Function(FunctionType { args, ret }) => Type::Function(FunctionType {
+                args: args
+                    .into_iter()
+                    .map(|t| t.traverse_type_vars(f.clone()))
+                    .collect(),
+                ret: Box::new(ret.traverse_type_vars(f)),
+            }),
+            Type::Int => Type::Int,
+            Type::Float => Type::Float,
+            Type::Bool => Type::Bool,
+            Type::CString => Type::CString,
+            Type::Tuple(members) => Type::Tuple(
+                members
+                    .into_iter()
+                    .map(|t| t.traverse_type_vars(f.clone()))
+                    .collect(),
+            ),
+            Type::Unit => Type::Unit,
+        }
+    }
+
+    pub fn as_tuple(&self) -> Option<&Vec<Type<'a>>> {
+        if let Self::Tuple(v) = self {
+            Some(v)
+        } else {
+            None
+        }
+    }
+}
+
+impl<'a> Display for Type<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Type::Int => f.write_str("int"),
+            Type::Float => f.write_str("float"),
+            Type::Bool => f.write_str("bool"),
+            Type::CString => f.write_str("cstring"),
+            Type::Unit => f.write_str("()"),
+            Type::Var(v) => v.fmt(f),
+            Type::Function(ft) => ft.fmt(f),
+            Type::Tuple(ms) => write!(f, "({})", ms.iter().join(", ")),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn type_var(n: &str) -> Type<'static> {
+        Type::Var(Ident::try_from(n.to_owned()).unwrap())
+    }
+
+    mod alpha_equiv {
+        use super::*;
+
+        #[test]
+        fn trivial() {
+            assert!(Type::Int.alpha_equiv(&Type::Int));
+            assert!(!Type::Int.alpha_equiv(&Type::Bool));
+        }
+
+        #[test]
+        fn simple_type_var() {
+            assert!(type_var("a").alpha_equiv(&type_var("b")));
+        }
+
+        #[test]
+        fn function_with_type_vars_equiv() {
+            assert!(Type::Function(FunctionType {
+                args: vec![type_var("a")],
+                ret: Box::new(type_var("b")),
+            })
+            .alpha_equiv(&Type::Function(FunctionType {
+                args: vec![type_var("b")],
+                ret: Box::new(type_var("a")),
+            })))
+        }
+
+        #[test]
+        fn function_with_type_vars_non_equiv() {
+            assert!(!Type::Function(FunctionType {
+                args: vec![type_var("a")],
+                ret: Box::new(type_var("a")),
+            })
+            .alpha_equiv(&Type::Function(FunctionType {
+                args: vec![type_var("b")],
+                ret: Box::new(type_var("a")),
+            })))
+        }
+    }
+}
diff --git a/users/grfn/achilles/src/codegen/llvm.rs b/users/grfn/achilles/src/codegen/llvm.rs
new file mode 100644
index 0000000000..9a71ac954e
--- /dev/null
+++ b/users/grfn/achilles/src/codegen/llvm.rs
@@ -0,0 +1,486 @@
+use std::convert::{TryFrom, TryInto};
+use std::path::Path;
+use std::result;
+
+use inkwell::basic_block::BasicBlock;
+use inkwell::builder::Builder;
+pub use inkwell::context::Context;
+use inkwell::module::Module;
+use inkwell::support::LLVMString;
+use inkwell::types::{BasicType, BasicTypeEnum, FunctionType, IntType, StructType};
+use inkwell::values::{AnyValueEnum, BasicValueEnum, FunctionValue, StructValue};
+use inkwell::{AddressSpace, IntPredicate};
+use itertools::Itertools;
+use thiserror::Error;
+
+use crate::ast::hir::{Binding, Decl, Expr, Pattern};
+use crate::ast::{BinaryOperator, Ident, Literal, Type, UnaryOperator};
+use crate::common::env::Env;
+
+#[derive(Debug, PartialEq, Eq, Error)]
+pub enum Error {
+    #[error("Undefined variable {0}")]
+    UndefinedVariable(Ident<'static>),
+
+    #[error("LLVM Error: {0}")]
+    LLVMError(String),
+}
+
+impl From<LLVMString> for Error {
+    fn from(s: LLVMString) -> Self {
+        Self::LLVMError(s.to_string())
+    }
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+pub struct Codegen<'ctx, 'ast> {
+    context: &'ctx Context,
+    pub module: Module<'ctx>,
+    builder: Builder<'ctx>,
+    env: Env<&'ast Ident<'ast>, AnyValueEnum<'ctx>>,
+    function_stack: Vec<FunctionValue<'ctx>>,
+    identifier_counter: u32,
+}
+
+impl<'ctx, 'ast> Codegen<'ctx, 'ast> {
+    pub fn new(context: &'ctx Context, module_name: &str) -> Self {
+        let module = context.create_module(module_name);
+        let builder = context.create_builder();
+        Self {
+            context,
+            module,
+            builder,
+            env: Default::default(),
+            function_stack: Default::default(),
+            identifier_counter: 0,
+        }
+    }
+
+    pub fn new_function<'a>(
+        &'a mut self,
+        name: &str,
+        ty: FunctionType<'ctx>,
+    ) -> &'a FunctionValue<'ctx> {
+        self.function_stack
+            .push(self.module.add_function(name, ty, None));
+        let basic_block = self.append_basic_block("entry");
+        self.builder.position_at_end(basic_block);
+        self.function_stack.last().unwrap()
+    }
+
+    pub fn finish_function(&mut self, res: Option<&BasicValueEnum<'ctx>>) -> FunctionValue<'ctx> {
+        self.builder.build_return(match res {
+            // lol
+            Some(val) => Some(val),
+            None => None,
+        });
+        self.function_stack.pop().unwrap()
+    }
+
+    pub fn append_basic_block(&self, name: &str) -> BasicBlock<'ctx> {
+        self.context
+            .append_basic_block(*self.function_stack.last().unwrap(), name)
+    }
+
+    fn bind_pattern(&mut self, pat: &'ast Pattern<'ast, Type>, val: AnyValueEnum<'ctx>) {
+        match pat {
+            Pattern::Id(id, _) => self.env.set(id, val),
+            Pattern::Tuple(pats) => {
+                for (i, pat) in pats.iter().enumerate() {
+                    let member = self
+                        .builder
+                        .build_extract_value(
+                            StructValue::try_from(val).unwrap(),
+                            i as _,
+                            "pat_bind",
+                        )
+                        .unwrap();
+                    self.bind_pattern(pat, member.into());
+                }
+            }
+        }
+    }
+
+    pub fn codegen_expr(
+        &mut self,
+        expr: &'ast Expr<'ast, Type>,
+    ) -> Result<Option<AnyValueEnum<'ctx>>> {
+        match expr {
+            Expr::Ident(id, _) => self
+                .env
+                .resolve(id)
+                .cloned()
+                .ok_or_else(|| Error::UndefinedVariable(id.to_owned()))
+                .map(Some),
+            Expr::Literal(lit, ty) => {
+                let ty = self.codegen_int_type(ty);
+                match lit {
+                    Literal::Int(i) => Ok(Some(AnyValueEnum::IntValue(ty.const_int(*i, false)))),
+                    Literal::Bool(b) => Ok(Some(AnyValueEnum::IntValue(
+                        ty.const_int(if *b { 1 } else { 0 }, false),
+                    ))),
+                    Literal::String(s) => Ok(Some(
+                        self.builder
+                            .build_global_string_ptr(s, "s")
+                            .as_pointer_value()
+                            .into(),
+                    )),
+                    Literal::Unit => Ok(None),
+                }
+            }
+            Expr::UnaryOp { op, rhs, .. } => {
+                let rhs = self.codegen_expr(rhs)?.unwrap();
+                match op {
+                    UnaryOperator::Not => unimplemented!(),
+                    UnaryOperator::Neg => Ok(Some(AnyValueEnum::IntValue(
+                        self.builder.build_int_neg(rhs.into_int_value(), "neg"),
+                    ))),
+                }
+            }
+            Expr::BinaryOp { lhs, op, rhs, .. } => {
+                let lhs = self.codegen_expr(lhs)?.unwrap();
+                let rhs = self.codegen_expr(rhs)?.unwrap();
+                match op {
+                    BinaryOperator::Add => {
+                        Ok(Some(AnyValueEnum::IntValue(self.builder.build_int_add(
+                            lhs.into_int_value(),
+                            rhs.into_int_value(),
+                            "add",
+                        ))))
+                    }
+                    BinaryOperator::Sub => {
+                        Ok(Some(AnyValueEnum::IntValue(self.builder.build_int_sub(
+                            lhs.into_int_value(),
+                            rhs.into_int_value(),
+                            "add",
+                        ))))
+                    }
+                    BinaryOperator::Mul => {
+                        Ok(Some(AnyValueEnum::IntValue(self.builder.build_int_sub(
+                            lhs.into_int_value(),
+                            rhs.into_int_value(),
+                            "add",
+                        ))))
+                    }
+                    BinaryOperator::Div => Ok(Some(AnyValueEnum::IntValue(
+                        self.builder.build_int_signed_div(
+                            lhs.into_int_value(),
+                            rhs.into_int_value(),
+                            "add",
+                        ),
+                    ))),
+                    BinaryOperator::Pow => unimplemented!(),
+                    BinaryOperator::Equ => Ok(Some(AnyValueEnum::IntValue(
+                        self.builder.build_int_compare(
+                            IntPredicate::EQ,
+                            lhs.into_int_value(),
+                            rhs.into_int_value(),
+                            "eq",
+                        ),
+                    ))),
+                    BinaryOperator::Neq => todo!(),
+                }
+            }
+            Expr::Let { bindings, body, .. } => {
+                self.env.push();
+                for Binding { pat, body, .. } in bindings {
+                    if let Some(val) = self.codegen_expr(body)? {
+                        self.bind_pattern(pat, val);
+                    }
+                }
+                let res = self.codegen_expr(body);
+                self.env.pop();
+                res
+            }
+            Expr::If {
+                condition,
+                then,
+                else_,
+                type_,
+            } => {
+                let then_block = self.append_basic_block("then");
+                let else_block = self.append_basic_block("else");
+                let join_block = self.append_basic_block("join");
+                let condition = self.codegen_expr(condition)?.unwrap();
+                self.builder.build_conditional_branch(
+                    condition.into_int_value(),
+                    then_block,
+                    else_block,
+                );
+                self.builder.position_at_end(then_block);
+                let then_res = self.codegen_expr(then)?;
+                self.builder.build_unconditional_branch(join_block);
+
+                self.builder.position_at_end(else_block);
+                let else_res = self.codegen_expr(else_)?;
+                self.builder.build_unconditional_branch(join_block);
+
+                self.builder.position_at_end(join_block);
+                if let Some(phi_type) = self.codegen_type(type_) {
+                    let phi = self.builder.build_phi(phi_type, "join");
+                    phi.add_incoming(&[
+                        (
+                            &BasicValueEnum::try_from(then_res.unwrap()).unwrap(),
+                            then_block,
+                        ),
+                        (
+                            &BasicValueEnum::try_from(else_res.unwrap()).unwrap(),
+                            else_block,
+                        ),
+                    ]);
+                    Ok(Some(phi.as_basic_value().into()))
+                } else {
+                    Ok(None)
+                }
+            }
+            Expr::Call { fun, args, .. } => {
+                if let Expr::Ident(id, _) = &**fun {
+                    let function = self
+                        .module
+                        .get_function(id.into())
+                        .or_else(|| self.env.resolve(id)?.clone().try_into().ok())
+                        .ok_or_else(|| Error::UndefinedVariable(id.to_owned()))?;
+                    let args = args
+                        .iter()
+                        .map(|arg| Ok(self.codegen_expr(arg)?.unwrap().try_into().unwrap()))
+                        .collect::<Result<Vec<_>>>()?;
+                    Ok(self
+                        .builder
+                        .build_call(function, &args, "call")
+                        .try_as_basic_value()
+                        .left()
+                        .map(|val| val.into()))
+                } else {
+                    todo!()
+                }
+            }
+            Expr::Fun { args, body, .. } => {
+                let fname = self.fresh_ident("f");
+                let cur_block = self.builder.get_insert_block().unwrap();
+                let env = self.env.save(); // TODO: closures
+                let function = self.codegen_function(&fname, args, body)?;
+                self.builder.position_at_end(cur_block);
+                self.env.restore(env);
+                Ok(Some(function.into()))
+            }
+            Expr::Tuple(members, ty) => {
+                let values = members
+                    .into_iter()
+                    .map(|expr| self.codegen_expr(expr))
+                    .collect::<Result<Vec<_>>>()?
+                    .into_iter()
+                    .filter_map(|x| x)
+                    .map(|x| x.try_into().unwrap())
+                    .collect_vec();
+                let field_types = ty.as_tuple().unwrap();
+                let tuple_type = self.codegen_tuple_type(field_types);
+                Ok(Some(tuple_type.const_named_struct(&values).into()))
+            }
+        }
+    }
+
+    pub fn codegen_function(
+        &mut self,
+        name: &str,
+        args: &'ast [(Ident<'ast>, Type)],
+        body: &'ast Expr<'ast, Type>,
+    ) -> Result<FunctionValue<'ctx>> {
+        let arg_types = args
+            .iter()
+            .filter_map(|(_, at)| self.codegen_type(at))
+            .collect::<Vec<_>>();
+
+        self.new_function(
+            name,
+            match self.codegen_type(body.type_()) {
+                Some(ret_ty) => ret_ty.fn_type(&arg_types, false),
+                None => self.context.void_type().fn_type(&arg_types, false),
+            },
+        );
+        self.env.push();
+        for (i, (arg, _)) in args.iter().enumerate() {
+            self.env.set(
+                arg,
+                self.cur_function().get_nth_param(i as u32).unwrap().into(),
+            );
+        }
+        let res = self.codegen_expr(body)?;
+        self.env.pop();
+        Ok(self.finish_function(res.map(|av| av.try_into().unwrap()).as_ref()))
+    }
+
+    pub fn codegen_extern(
+        &mut self,
+        name: &str,
+        args: &'ast [Type],
+        ret: &'ast Type,
+    ) -> Result<()> {
+        let arg_types = args
+            .iter()
+            .map(|t| self.codegen_type(t).unwrap())
+            .collect::<Vec<_>>();
+        self.module.add_function(
+            name,
+            match self.codegen_type(ret) {
+                Some(ret_ty) => ret_ty.fn_type(&arg_types, false),
+                None => self.context.void_type().fn_type(&arg_types, false),
+            },
+            None,
+        );
+        Ok(())
+    }
+
+    pub fn codegen_decl(&mut self, decl: &'ast Decl<'ast, Type>) -> Result<()> {
+        match decl {
+            Decl::Fun {
+                name, args, body, ..
+            } => {
+                self.codegen_function(name.into(), args, body)?;
+                Ok(())
+            }
+            Decl::Extern {
+                name,
+                arg_types,
+                ret_type,
+            } => self.codegen_extern(name.into(), arg_types, ret_type),
+        }
+    }
+
+    pub fn codegen_main(&mut self, expr: &'ast Expr<'ast, Type>) -> Result<()> {
+        self.new_function("main", self.context.i64_type().fn_type(&[], false));
+        let res = self.codegen_expr(expr)?;
+        if *expr.type_() != Type::Int {
+            self.builder
+                .build_return(Some(&self.context.i64_type().const_int(0, false)));
+        } else {
+            self.finish_function(res.map(|r| r.try_into().unwrap()).as_ref());
+        }
+        Ok(())
+    }
+
+    fn codegen_type(&self, type_: &'ast Type) -> Option<BasicTypeEnum<'ctx>> {
+        // TODO
+        match type_ {
+            Type::Int => Some(self.context.i64_type().into()),
+            Type::Float => Some(self.context.f64_type().into()),
+            Type::Bool => Some(self.context.bool_type().into()),
+            Type::CString => Some(
+                self.context
+                    .i8_type()
+                    .ptr_type(AddressSpace::Generic)
+                    .into(),
+            ),
+            Type::Function(_) => todo!(),
+            Type::Var(_) => unreachable!(),
+            Type::Unit => None,
+            Type::Tuple(ts) => Some(self.codegen_tuple_type(ts).into()),
+        }
+    }
+
+    fn codegen_tuple_type(&self, ts: &'ast [Type]) -> StructType<'ctx> {
+        self.context.struct_type(
+            ts.iter()
+                .filter_map(|t| self.codegen_type(t))
+                .collect_vec()
+                .as_slice(),
+            false,
+        )
+    }
+
+    fn codegen_int_type(&self, type_: &'ast Type) -> IntType<'ctx> {
+        // TODO
+        self.context.i64_type()
+    }
+
+    pub fn print_to_file<P>(&self, path: P) -> Result<()>
+    where
+        P: AsRef<Path>,
+    {
+        Ok(self.module.print_to_file(path)?)
+    }
+
+    pub fn binary_to_file<P>(&self, path: P) -> Result<()>
+    where
+        P: AsRef<Path>,
+    {
+        if self.module.write_bitcode_to_path(path.as_ref()) {
+            Ok(())
+        } else {
+            Err(Error::LLVMError(
+                "Error writing bitcode to output path".to_owned(),
+            ))
+        }
+    }
+
+    fn fresh_ident(&mut self, prefix: &str) -> String {
+        self.identifier_counter += 1;
+        format!("{}{}", prefix, self.identifier_counter)
+    }
+
+    fn cur_function(&self) -> &FunctionValue<'ctx> {
+        self.function_stack.last().unwrap()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use inkwell::execution_engine::JitFunction;
+    use inkwell::OptimizationLevel;
+
+    use super::*;
+
+    fn jit_eval<T>(expr: &str) -> anyhow::Result<T> {
+        let expr = crate::parser::expr(expr).unwrap().1;
+
+        let expr = crate::tc::typecheck_expr(expr).unwrap();
+
+        let context = Context::create();
+        let mut codegen = Codegen::new(&context, "test");
+        let execution_engine = codegen
+            .module
+            .create_jit_execution_engine(OptimizationLevel::None)
+            .unwrap();
+
+        codegen.codegen_function("test", &[], &expr)?;
+
+        unsafe {
+            let fun: JitFunction<unsafe extern "C" fn() -> T> =
+                execution_engine.get_function("test")?;
+            Ok(fun.call())
+        }
+    }
+
+    #[test]
+    fn add_literals() {
+        assert_eq!(jit_eval::<i64>("1 + 2").unwrap(), 3);
+    }
+
+    #[test]
+    fn variable_shadowing() {
+        assert_eq!(
+            jit_eval::<i64>("let x = 1 in (let x = 2 in x) + x").unwrap(),
+            3
+        );
+    }
+
+    #[test]
+    fn eq() {
+        assert_eq!(
+            jit_eval::<i64>("let x = 1 in if x == 1 then 2 else 4").unwrap(),
+            2
+        );
+    }
+
+    #[test]
+    fn function_call() {
+        let res = jit_eval::<i64>("let id = fn x = x in id 1").unwrap();
+        assert_eq!(res, 1);
+    }
+
+    #[test]
+    fn bind_tuple_pattern() {
+        let res = jit_eval::<i64>("let (x, y) = (1, 2) in x + y").unwrap();
+        assert_eq!(res, 3);
+    }
+}
diff --git a/users/grfn/achilles/src/codegen/mod.rs b/users/grfn/achilles/src/codegen/mod.rs
new file mode 100644
index 0000000000..8ef057dba0
--- /dev/null
+++ b/users/grfn/achilles/src/codegen/mod.rs
@@ -0,0 +1,25 @@
+pub mod llvm;
+
+use inkwell::execution_engine::JitFunction;
+use inkwell::OptimizationLevel;
+pub use llvm::*;
+
+use crate::ast::hir::Expr;
+use crate::ast::Type;
+use crate::common::Result;
+
+pub fn jit_eval<T>(expr: &Expr<Type>) -> Result<T> {
+    let context = Context::create();
+    let mut codegen = Codegen::new(&context, "eval");
+    let execution_engine = codegen
+        .module
+        .create_jit_execution_engine(OptimizationLevel::None)
+        .map_err(Error::from)?;
+    codegen.codegen_function("test", &[], &expr)?;
+
+    unsafe {
+        let fun: JitFunction<unsafe extern "C" fn() -> T> =
+            execution_engine.get_function("eval").unwrap();
+        Ok(fun.call())
+    }
+}
diff --git a/users/grfn/achilles/src/commands/check.rs b/users/grfn/achilles/src/commands/check.rs
new file mode 100644
index 0000000000..0bea482c14
--- /dev/null
+++ b/users/grfn/achilles/src/commands/check.rs
@@ -0,0 +1,39 @@
+use clap::Clap;
+use std::path::PathBuf;
+
+use crate::ast::Type;
+use crate::{parser, tc, Result};
+
+/// Typecheck a file or expression
+#[derive(Clap)]
+pub struct Check {
+    /// File to check
+    path: Option<PathBuf>,
+
+    /// Expression to check
+    #[clap(long, short = 'e')]
+    expr: Option<String>,
+}
+
+fn run_expr(expr: String) -> Result<Type<'static>> {
+    let (_, parsed) = parser::expr(&expr)?;
+    let hir_expr = tc::typecheck_expr(parsed)?;
+    Ok(hir_expr.type_().to_owned())
+}
+
+fn run_path(path: PathBuf) -> Result<Type<'static>> {
+    todo!()
+}
+
+impl Check {
+    pub fn run(self) -> Result<()> {
+        let type_ = match (self.path, self.expr) {
+            (None, None) => Err("Must specify either a file or expression to check".into()),
+            (Some(_), Some(_)) => Err("Cannot specify both a file and expression to check".into()),
+            (None, Some(expr)) => run_expr(expr),
+            (Some(path), None) => run_path(path),
+        }?;
+        println!("type: {}", type_);
+        Ok(())
+    }
+}
diff --git a/users/grfn/achilles/src/commands/compile.rs b/users/grfn/achilles/src/commands/compile.rs
new file mode 100644
index 0000000000..be8767575a
--- /dev/null
+++ b/users/grfn/achilles/src/commands/compile.rs
@@ -0,0 +1,31 @@
+use std::path::PathBuf;
+
+use clap::Clap;
+
+use crate::common::Result;
+use crate::compiler::{self, CompilerOptions};
+
+/// Compile a source file
+#[derive(Clap)]
+pub struct Compile {
+    /// File to compile
+    file: PathBuf,
+
+    /// Output file
+    #[clap(short = 'o')]
+    out_file: PathBuf,
+
+    #[clap(flatten)]
+    options: CompilerOptions,
+}
+
+impl Compile {
+    pub fn run(self) -> Result<()> {
+        eprintln!(
+            ">>> {} -> {}",
+            &self.file.to_string_lossy(),
+            self.out_file.to_string_lossy()
+        );
+        compiler::compile_file(&self.file, &self.out_file, &self.options)
+    }
+}
diff --git a/users/grfn/achilles/src/commands/eval.rs b/users/grfn/achilles/src/commands/eval.rs
new file mode 100644
index 0000000000..efd7399ed1
--- /dev/null
+++ b/users/grfn/achilles/src/commands/eval.rs
@@ -0,0 +1,28 @@
+use clap::Clap;
+
+use crate::{codegen, interpreter, parser, tc, Result};
+
+/// Evaluate an expression and print its result
+#[derive(Clap)]
+pub struct Eval {
+    /// JIT-compile with LLVM instead of interpreting
+    #[clap(long)]
+    jit: bool,
+
+    /// Expression to evaluate
+    expr: String,
+}
+
+impl Eval {
+    pub fn run(self) -> Result<()> {
+        let (_, parsed) = parser::expr(&self.expr)?;
+        let hir = tc::typecheck_expr(parsed)?;
+        let result = if self.jit {
+            codegen::jit_eval::<i64>(&hir)?.into()
+        } else {
+            interpreter::eval(&hir)?
+        };
+        println!("{}", result);
+        Ok(())
+    }
+}
diff --git a/users/grfn/achilles/src/commands/mod.rs b/users/grfn/achilles/src/commands/mod.rs
new file mode 100644
index 0000000000..fd0a822708
--- /dev/null
+++ b/users/grfn/achilles/src/commands/mod.rs
@@ -0,0 +1,7 @@
+pub mod check;
+pub mod compile;
+pub mod eval;
+
+pub use check::Check;
+pub use compile::Compile;
+pub use eval::Eval;
diff --git a/users/grfn/achilles/src/common/env.rs b/users/grfn/achilles/src/common/env.rs
new file mode 100644
index 0000000000..59a5e46c46
--- /dev/null
+++ b/users/grfn/achilles/src/common/env.rs
@@ -0,0 +1,59 @@
+use std::borrow::Borrow;
+use std::collections::HashMap;
+use std::hash::Hash;
+use std::mem;
+
+/// A lexical environment
+#[derive(Debug, PartialEq, Eq)]
+pub struct Env<K: Eq + Hash, V>(Vec<HashMap<K, V>>);
+
+impl<K, V> Default for Env<K, V>
+where
+    K: Eq + Hash,
+{
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<K, V> Env<K, V>
+where
+    K: Eq + Hash,
+{
+    pub fn new() -> Self {
+        Self(vec![Default::default()])
+    }
+
+    pub fn push(&mut self) {
+        self.0.push(Default::default());
+    }
+
+    pub fn pop(&mut self) {
+        self.0.pop();
+    }
+
+    pub fn save(&mut self) -> Self {
+        mem::take(self)
+    }
+
+    pub fn restore(&mut self, saved: Self) {
+        *self = saved;
+    }
+
+    pub fn set(&mut self, k: K, v: V) {
+        self.0.last_mut().unwrap().insert(k, v);
+    }
+
+    pub fn resolve<'a, Q>(&'a self, k: &Q) -> Option<&'a V>
+    where
+        K: Borrow<Q>,
+        Q: Hash + Eq + ?Sized,
+    {
+        for ctx in self.0.iter().rev() {
+            if let Some(res) = ctx.get(k) {
+                return Some(res);
+            }
+        }
+        None
+    }
+}
diff --git a/users/grfn/achilles/src/common/error.rs b/users/grfn/achilles/src/common/error.rs
new file mode 100644
index 0000000000..51575a895e
--- /dev/null
+++ b/users/grfn/achilles/src/common/error.rs
@@ -0,0 +1,59 @@
+use std::{io, result};
+
+use thiserror::Error;
+
+use crate::{codegen, interpreter, parser, tc};
+
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error(transparent)]
+    IOError(#[from] io::Error),
+
+    #[error("Error parsing input: {0}")]
+    ParseError(#[from] parser::Error),
+
+    #[error("Error evaluating expression: {0}")]
+    EvalError(#[from] interpreter::Error),
+
+    #[error("Compile error: {0}")]
+    CodegenError(#[from] codegen::Error),
+
+    #[error("Type error: {0}")]
+    TypeError(#[from] tc::Error),
+
+    #[error("{0}")]
+    Message(String),
+}
+
+impl From<String> for Error {
+    fn from(s: String) -> Self {
+        Self::Message(s)
+    }
+}
+
+impl<'a> From<&'a str> for Error {
+    fn from(s: &'a str) -> Self {
+        Self::Message(s.to_owned())
+    }
+}
+
+impl<'a> From<nom::Err<nom::error::Error<&'a str>>> for Error {
+    fn from(e: nom::Err<nom::error::Error<&'a str>>) -> Self {
+        use nom::error::Error as NomError;
+        use nom::Err::*;
+
+        Self::ParseError(match e {
+            Incomplete(i) => Incomplete(i),
+            Error(NomError { input, code }) => Error(NomError {
+                input: input.to_owned(),
+                code,
+            }),
+            Failure(NomError { input, code }) => Failure(NomError {
+                input: input.to_owned(),
+                code,
+            }),
+        })
+    }
+}
+
+pub type Result<T> = result::Result<T, Error>;
diff --git a/users/grfn/achilles/src/common/mod.rs b/users/grfn/achilles/src/common/mod.rs
new file mode 100644
index 0000000000..8368a6dd18
--- /dev/null
+++ b/users/grfn/achilles/src/common/mod.rs
@@ -0,0 +1,6 @@
+pub(crate) mod env;
+pub(crate) mod error;
+pub(crate) mod namer;
+
+pub use error::{Error, Result};
+pub use namer::{Namer, NamerOf};
diff --git a/users/grfn/achilles/src/common/namer.rs b/users/grfn/achilles/src/common/namer.rs
new file mode 100644
index 0000000000..016e9f6ed9
--- /dev/null
+++ b/users/grfn/achilles/src/common/namer.rs
@@ -0,0 +1,122 @@
+use std::fmt::Display;
+use std::marker::PhantomData;
+
+pub struct Namer<T, F> {
+    make_name: F,
+    counter: u64,
+    _phantom: PhantomData<T>,
+}
+
+impl<T, F> Namer<T, F> {
+    pub fn new(make_name: F) -> Self {
+        Namer {
+            make_name,
+            counter: 0,
+            _phantom: PhantomData,
+        }
+    }
+}
+
+impl Namer<String, Box<dyn Fn(u64) -> String>> {
+    pub fn with_prefix<T>(prefix: T) -> Self
+    where
+        T: Display + 'static,
+    {
+        Namer::new(move |i| format!("{}{}", prefix, i)).boxed()
+    }
+
+    pub fn with_suffix<T>(suffix: T) -> Self
+    where
+        T: Display + 'static,
+    {
+        Namer::new(move |i| format!("{}{}", i, suffix)).boxed()
+    }
+
+    pub fn alphabetic() -> Self {
+        Namer::new(|i| {
+            if i <= 26 {
+                std::char::from_u32((i + 96) as u32).unwrap().to_string()
+            } else {
+                format!(
+                    "{}{}",
+                    std::char::from_u32(((i % 26) + 96) as u32).unwrap(),
+                    i - 26
+                )
+            }
+        })
+        .boxed()
+    }
+}
+
+impl<T, F> Namer<T, F>
+where
+    F: Fn(u64) -> T,
+{
+    pub fn make_name(&mut self) -> T {
+        self.counter += 1;
+        (self.make_name)(self.counter)
+    }
+
+    pub fn boxed(self) -> NamerOf<T>
+    where
+        F: 'static,
+    {
+        Namer {
+            make_name: Box::new(self.make_name),
+            counter: self.counter,
+            _phantom: self._phantom,
+        }
+    }
+
+    pub fn map<G, U>(self, f: G) -> NamerOf<U>
+    where
+        G: Fn(T) -> U + 'static,
+        T: 'static,
+        F: 'static,
+    {
+        Namer {
+            counter: self.counter,
+            make_name: Box::new(move |x| f((self.make_name)(x))),
+            _phantom: PhantomData,
+        }
+    }
+}
+
+pub type NamerOf<T> = Namer<T, Box<dyn Fn(u64) -> T>>;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn prefix() {
+        let mut namer = Namer::with_prefix("t");
+        assert_eq!(namer.make_name(), "t1");
+        assert_eq!(namer.make_name(), "t2");
+    }
+
+    #[test]
+    fn suffix() {
+        let mut namer = Namer::with_suffix("t");
+        assert_eq!(namer.make_name(), "1t");
+        assert_eq!(namer.make_name(), "2t");
+    }
+
+    #[test]
+    fn alphabetic() {
+        let mut namer = Namer::alphabetic();
+        assert_eq!(namer.make_name(), "a");
+        assert_eq!(namer.make_name(), "b");
+        (0..25).for_each(|_| {
+            namer.make_name();
+        });
+        assert_eq!(namer.make_name(), "b2");
+    }
+
+    #[test]
+    fn custom_callback() {
+        let mut namer = Namer::new(|n| n + 1);
+        assert_eq!(namer.make_name(), 2);
+        assert_eq!(namer.make_name(), 3);
+    }
+}
diff --git a/users/grfn/achilles/src/compiler.rs b/users/grfn/achilles/src/compiler.rs
new file mode 100644
index 0000000000..45b215473d
--- /dev/null
+++ b/users/grfn/achilles/src/compiler.rs
@@ -0,0 +1,89 @@
+use std::fmt::{self, Display};
+use std::path::Path;
+use std::str::FromStr;
+use std::{fs, result};
+
+use clap::Clap;
+use test_strategy::Arbitrary;
+
+use crate::codegen::{self, Codegen};
+use crate::common::Result;
+use crate::passes::hir::{monomorphize, strip_positive_units};
+use crate::{parser, tc};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Arbitrary)]
+pub enum OutputFormat {
+    LLVM,
+    Bitcode,
+}
+
+impl Default for OutputFormat {
+    fn default() -> Self {
+        Self::Bitcode
+    }
+}
+
+impl FromStr for OutputFormat {
+    type Err = String;
+
+    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
+        match s {
+            "llvm" => Ok(Self::LLVM),
+            "binary" => Ok(Self::Bitcode),
+            _ => Err(format!(
+                "Invalid output format {}, expected one of {{llvm, binary}}",
+                s
+            )),
+        }
+    }
+}
+
+impl Display for OutputFormat {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            OutputFormat::LLVM => f.write_str("llvm"),
+            OutputFormat::Bitcode => f.write_str("binary"),
+        }
+    }
+}
+
+#[derive(Clap, Debug, PartialEq, Eq, Default)]
+pub struct CompilerOptions {
+    #[clap(long, short = 'f', default_value)]
+    format: OutputFormat,
+}
+
+pub fn compile_file(input: &Path, output: &Path, options: &CompilerOptions) -> Result<()> {
+    let src = fs::read_to_string(input)?;
+    let (_, decls) = parser::toplevel(&src)?;
+    let mut decls = tc::typecheck_toplevel(decls)?;
+    monomorphize::run_toplevel(&mut decls);
+    strip_positive_units::run_toplevel(&mut decls);
+
+    let context = codegen::Context::create();
+    let mut codegen = Codegen::new(
+        &context,
+        &input
+            .file_stem()
+            .map_or("UNKNOWN".to_owned(), |s| s.to_string_lossy().into_owned()),
+    );
+    for decl in &decls {
+        codegen.codegen_decl(decl)?;
+    }
+    match options.format {
+        OutputFormat::LLVM => codegen.print_to_file(output)?,
+        OutputFormat::Bitcode => codegen.binary_to_file(output)?,
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use test_strategy::proptest;
+
+    #[proptest]
+    fn output_format_display_from_str_round_trip(of: OutputFormat) {
+        assert_eq!(OutputFormat::from_str(&of.to_string()), Ok(of));
+    }
+}
diff --git a/users/grfn/achilles/src/interpreter/error.rs b/users/grfn/achilles/src/interpreter/error.rs
new file mode 100644
index 0000000000..268d6f479a
--- /dev/null
+++ b/users/grfn/achilles/src/interpreter/error.rs
@@ -0,0 +1,19 @@
+use std::result;
+
+use thiserror::Error;
+
+use crate::ast::{Ident, Type};
+
+#[derive(Debug, PartialEq, Eq, Error)]
+pub enum Error {
+    #[error("Undefined variable {0}")]
+    UndefinedVariable(Ident<'static>),
+
+    #[error("Unexpected type {actual}, expected type {expected}")]
+    InvalidType {
+        actual: Type<'static>,
+        expected: Type<'static>,
+    },
+}
+
+pub type Result<T> = result::Result<T, Error>;
diff --git a/users/grfn/achilles/src/interpreter/mod.rs b/users/grfn/achilles/src/interpreter/mod.rs
new file mode 100644
index 0000000000..70df7a0724
--- /dev/null
+++ b/users/grfn/achilles/src/interpreter/mod.rs
@@ -0,0 +1,203 @@
+mod error;
+mod value;
+
+use itertools::Itertools;
+use value::Val;
+
+pub use self::error::{Error, Result};
+pub use self::value::{Function, Value};
+use crate::ast::hir::{Binding, Expr, Pattern};
+use crate::ast::{BinaryOperator, FunctionType, Ident, Literal, Type, UnaryOperator};
+use crate::common::env::Env;
+
+#[derive(Debug, Default)]
+pub struct Interpreter<'a> {
+    env: Env<&'a Ident<'a>, Value<'a>>,
+}
+
+impl<'a> Interpreter<'a> {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    fn resolve(&self, var: &'a Ident<'a>) -> Result<Value<'a>> {
+        self.env
+            .resolve(var)
+            .cloned()
+            .ok_or_else(|| Error::UndefinedVariable(var.to_owned()))
+    }
+
+    fn bind_pattern(&mut self, pattern: &'a Pattern<'a, Type>, value: Value<'a>) {
+        match pattern {
+            Pattern::Id(id, _) => self.env.set(id, value),
+            Pattern::Tuple(pats) => {
+                for (pat, val) in pats.iter().zip(value.as_tuple().unwrap().clone()) {
+                    self.bind_pattern(pat, val);
+                }
+            }
+        }
+    }
+
+    pub fn eval(&mut self, expr: &'a Expr<'a, Type>) -> Result<Value<'a>> {
+        let res = match expr {
+            Expr::Ident(id, _) => self.resolve(id),
+            Expr::Literal(Literal::Int(i), _) => Ok((*i).into()),
+            Expr::Literal(Literal::Bool(b), _) => Ok((*b).into()),
+            Expr::Literal(Literal::String(s), _) => Ok(s.clone().into()),
+            Expr::Literal(Literal::Unit, _) => unreachable!(),
+            Expr::UnaryOp { op, rhs, .. } => {
+                let rhs = self.eval(rhs)?;
+                match op {
+                    UnaryOperator::Neg => -rhs,
+                    _ => unimplemented!(),
+                }
+            }
+            Expr::BinaryOp { lhs, op, rhs, .. } => {
+                let lhs = self.eval(lhs)?;
+                let rhs = self.eval(rhs)?;
+                match op {
+                    BinaryOperator::Add => lhs + rhs,
+                    BinaryOperator::Sub => lhs - rhs,
+                    BinaryOperator::Mul => lhs * rhs,
+                    BinaryOperator::Div => lhs / rhs,
+                    BinaryOperator::Pow => todo!(),
+                    BinaryOperator::Equ => Ok(lhs.eq(&rhs).into()),
+                    BinaryOperator::Neq => todo!(),
+                }
+            }
+            Expr::Let { bindings, body, .. } => {
+                self.env.push();
+                for Binding { pat, body, .. } in bindings {
+                    let val = self.eval(body)?;
+                    self.bind_pattern(pat, val);
+                }
+                let res = self.eval(body)?;
+                self.env.pop();
+                Ok(res)
+            }
+            Expr::If {
+                condition,
+                then,
+                else_,
+                ..
+            } => {
+                let condition = self.eval(condition)?;
+                if *(condition.as_type::<bool>()?) {
+                    self.eval(then)
+                } else {
+                    self.eval(else_)
+                }
+            }
+            Expr::Call { ref fun, args, .. } => {
+                let fun = self.eval(fun)?;
+                let expected_type = FunctionType {
+                    args: args.iter().map(|_| Type::Int).collect(),
+                    ret: Box::new(Type::Int),
+                };
+
+                let Function {
+                    args: function_args,
+                    body,
+                    ..
+                } = fun.as_function(expected_type)?;
+                let arg_values = function_args.iter().zip(
+                    args.iter()
+                        .map(|v| self.eval(v))
+                        .collect::<Result<Vec<_>>>()?,
+                );
+                let mut interpreter = Interpreter::new();
+                for (arg_name, arg_value) in arg_values {
+                    interpreter.env.set(arg_name, arg_value);
+                }
+                Ok(Value::from(*interpreter.eval(body)?.as_type::<i64>()?))
+            }
+            Expr::Fun {
+                type_args: _,
+                args,
+                body,
+                type_,
+            } => {
+                let type_ = match type_ {
+                    Type::Function(ft) => ft.clone(),
+                    _ => unreachable!("Function expression without function type"),
+                };
+
+                Ok(Value::from(value::Function {
+                    // TODO
+                    type_,
+                    args: args.iter().map(|(arg, _)| arg.to_owned()).collect(),
+                    body: (**body).to_owned(),
+                }))
+            }
+            Expr::Tuple(members, _) => Ok(Val::Tuple(
+                members
+                    .into_iter()
+                    .map(|expr| self.eval(expr))
+                    .try_collect()?,
+            )
+            .into()),
+        }?;
+        debug_assert_eq!(&res.type_(), expr.type_());
+        Ok(res)
+    }
+}
+
+pub fn eval<'a>(expr: &'a Expr<'a, Type>) -> Result<Value<'a>> {
+    let mut interpreter = Interpreter::new();
+    interpreter.eval(expr)
+}
+
+#[cfg(test)]
+mod tests {
+    use std::convert::TryFrom;
+
+    use super::value::{TypeOf, Val};
+    use super::*;
+    use BinaryOperator::*;
+
+    fn int_lit(i: u64) -> Box<Expr<'static, Type<'static>>> {
+        Box::new(Expr::Literal(Literal::Int(i), Type::Int))
+    }
+
+    fn do_eval<T>(src: &str) -> T
+    where
+        for<'a> &'a T: TryFrom<&'a Val<'a>>,
+        T: Clone + TypeOf,
+    {
+        let expr = crate::parser::expr(src).unwrap().1;
+        let hir = crate::tc::typecheck_expr(expr).unwrap();
+        let res = eval(&hir).unwrap();
+        res.as_type::<T>().unwrap().clone()
+    }
+
+    #[test]
+    fn simple_addition() {
+        let expr = Expr::BinaryOp {
+            lhs: int_lit(1),
+            op: Mul,
+            rhs: int_lit(2),
+            type_: Type::Int,
+        };
+        let res = eval(&expr).unwrap();
+        assert_eq!(*res.as_type::<i64>().unwrap(), 2);
+    }
+
+    #[test]
+    fn variable_shadowing() {
+        let res = do_eval::<i64>("let x = 1 in (let x = 2 in x) + x");
+        assert_eq!(res, 3);
+    }
+
+    #[test]
+    fn conditional_with_equals() {
+        let res = do_eval::<i64>("let x = 1 in if x == 1 then 2 else 4");
+        assert_eq!(res, 2);
+    }
+
+    #[test]
+    #[ignore]
+    fn function_call() {
+        let res = do_eval::<i64>("let id = fn x = x in id 1");
+        assert_eq!(res, 1);
+    }
+}
diff --git a/users/grfn/achilles/src/interpreter/value.rs b/users/grfn/achilles/src/interpreter/value.rs
new file mode 100644
index 0000000000..272d1167a3
--- /dev/null
+++ b/users/grfn/achilles/src/interpreter/value.rs
@@ -0,0 +1,224 @@
+use std::borrow::Cow;
+use std::convert::TryFrom;
+use std::fmt::{self, Display};
+use std::ops::{Add, Div, Mul, Neg, Sub};
+use std::rc::Rc;
+use std::result;
+
+use derive_more::{Deref, From, TryInto};
+use itertools::Itertools;
+
+use super::{Error, Result};
+use crate::ast::hir::Expr;
+use crate::ast::{FunctionType, Ident, Type};
+
+#[derive(Debug, Clone)]
+pub struct Function<'a> {
+    pub type_: FunctionType<'a>,
+    pub args: Vec<Ident<'a>>,
+    pub body: Expr<'a, Type<'a>>,
+}
+
+#[derive(From, TryInto)]
+#[try_into(owned, ref)]
+pub enum Val<'a> {
+    Int(i64),
+    Float(f64),
+    Bool(bool),
+    String(Cow<'a, str>),
+    Tuple(Vec<Value<'a>>),
+    Function(Function<'a>),
+}
+
+impl<'a> TryFrom<Val<'a>> for String {
+    type Error = ();
+
+    fn try_from(value: Val<'a>) -> result::Result<Self, Self::Error> {
+        match value {
+            Val::String(s) => Ok(s.into_owned()),
+            _ => Err(()),
+        }
+    }
+}
+
+impl<'a> fmt::Debug for Val<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Val::Int(x) => f.debug_tuple("Int").field(x).finish(),
+            Val::Float(x) => f.debug_tuple("Float").field(x).finish(),
+            Val::Bool(x) => f.debug_tuple("Bool").field(x).finish(),
+            Val::String(s) => f.debug_tuple("String").field(s).finish(),
+            Val::Function(Function { type_, .. }) => {
+                f.debug_struct("Function").field("type_", type_).finish()
+            }
+            Val::Tuple(members) => f.debug_tuple("Tuple").field(members).finish(),
+        }
+    }
+}
+
+impl<'a> PartialEq for Val<'a> {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Val::Int(x), Val::Int(y)) => x == y,
+            (Val::Float(x), Val::Float(y)) => x == y,
+            (Val::Bool(x), Val::Bool(y)) => x == y,
+            (Val::Function(_), Val::Function(_)) => false,
+            (_, _) => false,
+        }
+    }
+}
+
+impl<'a> From<u64> for Val<'a> {
+    fn from(i: u64) -> Self {
+        Self::from(i as i64)
+    }
+}
+
+impl<'a> Display for Val<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Val::Int(x) => x.fmt(f),
+            Val::Float(x) => x.fmt(f),
+            Val::Bool(x) => x.fmt(f),
+            Val::String(s) => write!(f, "{:?}", s),
+            Val::Function(Function { type_, .. }) => write!(f, "<{}>", type_),
+            Val::Tuple(members) => write!(f, "({})", members.iter().join(", ")),
+        }
+    }
+}
+
+impl<'a> Val<'a> {
+    pub fn type_(&self) -> Type {
+        match self {
+            Val::Int(_) => Type::Int,
+            Val::Float(_) => Type::Float,
+            Val::Bool(_) => Type::Bool,
+            Val::String(_) => Type::CString,
+            Val::Function(Function { type_, .. }) => Type::Function(type_.clone()),
+            Val::Tuple(members) => Type::Tuple(members.iter().map(|expr| expr.type_()).collect()),
+        }
+    }
+
+    pub fn as_type<'b, T>(&'b self) -> Result<&'b T>
+    where
+        T: TypeOf + 'b + Clone,
+        &'b T: TryFrom<&'b Self>,
+    {
+        <&T>::try_from(self).map_err(|_| Error::InvalidType {
+            actual: self.type_().to_owned(),
+            expected: <T as TypeOf>::type_of(),
+        })
+    }
+
+    pub fn as_function<'b>(&'b self, function_type: FunctionType) -> Result<&'b Function<'a>> {
+        match self {
+            Val::Function(f) if f.type_ == function_type => Ok(&f),
+            _ => Err(Error::InvalidType {
+                actual: self.type_().to_owned(),
+                expected: Type::Function(function_type.to_owned()),
+            }),
+        }
+    }
+
+    pub fn as_tuple(&self) -> Option<&Vec<Value<'a>>> {
+        if let Self::Tuple(v) = self {
+            Some(v)
+        } else {
+            None
+        }
+    }
+
+    pub fn try_into_tuple(self) -> result::Result<Vec<Value<'a>>, Self> {
+        if let Self::Tuple(v) = self {
+            Ok(v)
+        } else {
+            Err(self)
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Deref)]
+pub struct Value<'a>(Rc<Val<'a>>);
+
+impl<'a> Display for Value<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'a, T> From<T> for Value<'a>
+where
+    Val<'a>: From<T>,
+{
+    fn from(x: T) -> Self {
+        Self(Rc::new(x.into()))
+    }
+}
+
+impl<'a> Neg for Value<'a> {
+    type Output = Result<Value<'a>>;
+
+    fn neg(self) -> Self::Output {
+        Ok((-self.as_type::<i64>()?).into())
+    }
+}
+
+impl<'a> Add for Value<'a> {
+    type Output = Result<Value<'a>>;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Ok((self.as_type::<i64>()? + rhs.as_type::<i64>()?).into())
+    }
+}
+
+impl<'a> Sub for Value<'a> {
+    type Output = Result<Value<'a>>;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Ok((self.as_type::<i64>()? - rhs.as_type::<i64>()?).into())
+    }
+}
+
+impl<'a> Mul for Value<'a> {
+    type Output = Result<Value<'a>>;
+
+    fn mul(self, rhs: Self) -> Self::Output {
+        Ok((self.as_type::<i64>()? * rhs.as_type::<i64>()?).into())
+    }
+}
+
+impl<'a> Div for Value<'a> {
+    type Output = Result<Value<'a>>;
+
+    fn div(self, rhs: Self) -> Self::Output {
+        Ok((self.as_type::<f64>()? / rhs.as_type::<f64>()?).into())
+    }
+}
+
+pub trait TypeOf {
+    fn type_of() -> Type<'static>;
+}
+
+impl TypeOf for i64 {
+    fn type_of() -> Type<'static> {
+        Type::Int
+    }
+}
+
+impl TypeOf for bool {
+    fn type_of() -> Type<'static> {
+        Type::Bool
+    }
+}
+
+impl TypeOf for f64 {
+    fn type_of() -> Type<'static> {
+        Type::Float
+    }
+}
+
+impl TypeOf for String {
+    fn type_of() -> Type<'static> {
+        Type::CString
+    }
+}
diff --git a/users/grfn/achilles/src/main.rs b/users/grfn/achilles/src/main.rs
new file mode 100644
index 0000000000..5ae1b59b3a
--- /dev/null
+++ b/users/grfn/achilles/src/main.rs
@@ -0,0 +1,36 @@
+use clap::Clap;
+
+pub mod ast;
+pub mod codegen;
+pub(crate) mod commands;
+pub(crate) mod common;
+pub mod compiler;
+pub mod interpreter;
+pub(crate) mod passes;
+#[macro_use]
+pub mod parser;
+pub mod tc;
+
+pub use common::{Error, Result};
+
+#[derive(Clap)]
+struct Opts {
+    #[clap(subcommand)]
+    subcommand: Command,
+}
+
+#[derive(Clap)]
+enum Command {
+    Eval(commands::Eval),
+    Compile(commands::Compile),
+    Check(commands::Check),
+}
+
+fn main() -> anyhow::Result<()> {
+    let opts = Opts::parse();
+    match opts.subcommand {
+        Command::Eval(eval) => Ok(eval.run()?),
+        Command::Compile(compile) => Ok(compile.run()?),
+        Command::Check(check) => Ok(check.run()?),
+    }
+}
diff --git a/users/grfn/achilles/src/parser/expr.rs b/users/grfn/achilles/src/parser/expr.rs
new file mode 100644
index 0000000000..b18ce4a0dc
--- /dev/null
+++ b/users/grfn/achilles/src/parser/expr.rs
@@ -0,0 +1,717 @@
+use std::borrow::Cow;
+
+use nom::character::complete::{digit1, multispace0, multispace1};
+use nom::{
+    alt, call, char, complete, delimited, do_parse, flat_map, many0, map, named, opt, parse_to,
+    preceded, separated_list0, separated_list1, tag, tuple,
+};
+use pratt::{Affix, Associativity, PrattParser, Precedence};
+
+use super::util::comma;
+use crate::ast::{BinaryOperator, Binding, Expr, Fun, Literal, Pattern, UnaryOperator};
+use crate::parser::{arg, ident, type_};
+
+#[derive(Debug)]
+enum TokenTree<'a> {
+    Prefix(UnaryOperator),
+    // Postfix(char),
+    Infix(BinaryOperator),
+    Primary(Expr<'a>),
+    Group(Vec<TokenTree<'a>>),
+}
+
+named!(prefix(&str) -> TokenTree, map!(alt!(
+    complete!(char!('-')) => { |_| UnaryOperator::Neg } |
+    complete!(char!('!')) => { |_| UnaryOperator::Not }
+), TokenTree::Prefix));
+
+named!(infix(&str) -> TokenTree, map!(alt!(
+    complete!(tag!("==")) => { |_| BinaryOperator::Equ } |
+    complete!(tag!("!=")) => { |_| BinaryOperator::Neq } |
+    complete!(char!('+')) => { |_| BinaryOperator::Add } |
+    complete!(char!('-')) => { |_| BinaryOperator::Sub } |
+    complete!(char!('*')) => { |_| BinaryOperator::Mul } |
+    complete!(char!('/')) => { |_| BinaryOperator::Div } |
+    complete!(char!('^')) => { |_| BinaryOperator::Pow }
+), TokenTree::Infix));
+
+named!(primary(&str) -> TokenTree, alt!(
+    do_parse!(
+        multispace0 >>
+        char!('(') >>
+        multispace0 >>
+        group: group >>
+        multispace0 >>
+        char!(')') >>
+        multispace0 >>
+            (TokenTree::Group(group))
+    ) |
+    delimited!(multispace0, simple_expr, multispace0) => { |s| TokenTree::Primary(s) }
+));
+
+named!(
+    rest(&str) -> Vec<(TokenTree, Vec<TokenTree>, TokenTree)>,
+    many0!(tuple!(
+        infix,
+        delimited!(multispace0, many0!(prefix), multispace0),
+        primary
+        // many0!(postfix)
+    ))
+);
+
+named!(group(&str) -> Vec<TokenTree>, do_parse!(
+    prefix: many0!(prefix)
+        >> primary: primary
+        // >> postfix: many0!(postfix)
+        >> rest: rest
+        >> ({
+            let mut res = prefix;
+            res.push(primary);
+            // res.append(&mut postfix);
+            for (infix, mut prefix, primary/*, mut postfix*/) in rest {
+                res.push(infix);
+                res.append(&mut prefix);
+                res.push(primary);
+                // res.append(&mut postfix);
+            }
+            res
+        })
+));
+
+fn token_tree(i: &str) -> nom::IResult<&str, Vec<TokenTree>> {
+    group(i)
+}
+
+struct ExprParser;
+
+impl<'a, I> PrattParser<I> for ExprParser
+where
+    I: Iterator<Item = TokenTree<'a>>,
+{
+    type Error = pratt::NoError;
+    type Input = TokenTree<'a>;
+    type Output = Expr<'a>;
+
+    fn query(&mut self, input: &Self::Input) -> Result<Affix, Self::Error> {
+        use BinaryOperator::*;
+        use UnaryOperator::*;
+
+        Ok(match input {
+            TokenTree::Infix(Add) => Affix::Infix(Precedence(6), Associativity::Left),
+            TokenTree::Infix(Sub) => Affix::Infix(Precedence(6), Associativity::Left),
+            TokenTree::Infix(Mul) => Affix::Infix(Precedence(7), Associativity::Left),
+            TokenTree::Infix(Div) => Affix::Infix(Precedence(7), Associativity::Left),
+            TokenTree::Infix(Pow) => Affix::Infix(Precedence(8), Associativity::Right),
+            TokenTree::Infix(Equ) => Affix::Infix(Precedence(4), Associativity::Right),
+            TokenTree::Infix(Neq) => Affix::Infix(Precedence(4), Associativity::Right),
+            TokenTree::Prefix(Neg) => Affix::Prefix(Precedence(6)),
+            TokenTree::Prefix(Not) => Affix::Prefix(Precedence(6)),
+            TokenTree::Primary(_) => Affix::Nilfix,
+            TokenTree::Group(_) => Affix::Nilfix,
+        })
+    }
+
+    fn primary(&mut self, input: Self::Input) -> Result<Self::Output, Self::Error> {
+        Ok(match input {
+            TokenTree::Primary(expr) => expr,
+            TokenTree::Group(group) => self.parse(&mut group.into_iter()).unwrap(),
+            _ => unreachable!(),
+        })
+    }
+
+    fn infix(
+        &mut self,
+        lhs: Self::Output,
+        op: Self::Input,
+        rhs: Self::Output,
+    ) -> Result<Self::Output, Self::Error> {
+        let op = match op {
+            TokenTree::Infix(op) => op,
+            _ => unreachable!(),
+        };
+        Ok(Expr::BinaryOp {
+            lhs: Box::new(lhs),
+            op,
+            rhs: Box::new(rhs),
+        })
+    }
+
+    fn prefix(&mut self, op: Self::Input, rhs: Self::Output) -> Result<Self::Output, Self::Error> {
+        let op = match op {
+            TokenTree::Prefix(op) => op,
+            _ => unreachable!(),
+        };
+
+        Ok(Expr::UnaryOp {
+            op,
+            rhs: Box::new(rhs),
+        })
+    }
+
+    fn postfix(
+        &mut self,
+        _lhs: Self::Output,
+        _op: Self::Input,
+    ) -> Result<Self::Output, Self::Error> {
+        unreachable!()
+    }
+}
+
+named!(int(&str) -> Literal, map!(flat_map!(digit1, parse_to!(u64)), Literal::Int));
+
+named!(bool_(&str) -> Literal, alt!(
+    complete!(tag!("true")) => { |_| Literal::Bool(true) } |
+    complete!(tag!("false")) => { |_| Literal::Bool(false) }
+));
+
+fn string_internal(i: &str) -> nom::IResult<&str, Cow<'_, str>, nom::error::Error<&str>> {
+    // TODO(grfn): use String::split_once when that's stable
+    let (s, rem) = if let Some(pos) = i.find('"') {
+        (&i[..pos], &i[(pos + 1)..])
+    } else {
+        return Err(nom::Err::Error(nom::error::Error::new(
+            i,
+            nom::error::ErrorKind::Tag,
+        )));
+    };
+
+    Ok((rem, Cow::Borrowed(s)))
+}
+
+named!(string(&str) -> Literal, preceded!(
+    complete!(char!('"')),
+    map!(
+        string_internal,
+        |s| Literal::String(s)
+    )
+));
+
+named!(unit(&str) -> Literal, map!(complete!(tag!("()")), |_| Literal::Unit));
+
+named!(literal(&str) -> Literal, alt!(int | bool_ | string | unit));
+
+named!(literal_expr(&str) -> Expr, map!(literal, Expr::Literal));
+
+named!(tuple(&str) -> Expr, do_parse!(
+    complete!(tag!("("))
+        >> multispace0
+        >> fst: expr
+        >> comma
+        >> rest: separated_list0!(
+            comma,
+            expr
+        )
+        >> multispace0
+        >> tag!(")")
+        >> ({
+            let mut members = Vec::with_capacity(rest.len() + 1);
+            members.push(fst);
+            members.append(&mut rest.clone());
+            Expr::Tuple(members)
+        })
+));
+
+named!(tuple_pattern(&str) -> Pattern, do_parse!(
+    complete!(tag!("("))
+        >> multispace0
+        >> pats: separated_list0!(
+            comma,
+            pattern
+        )
+        >> multispace0
+        >> tag!(")")
+        >> (Pattern::Tuple(pats))
+));
+
+named!(pattern(&str) -> Pattern, alt!(
+    ident => { |id| Pattern::Id(id) } |
+    tuple_pattern
+));
+
+named!(binding(&str) -> Binding, do_parse!(
+    multispace0
+        >> pat: pattern
+        >> multispace0
+        >> type_: opt!(preceded!(tuple!(tag!(":"), multispace0), type_))
+        >> multispace0
+        >> char!('=')
+        >> multispace0
+        >> body: expr
+        >> (Binding {
+            pat,
+            type_,
+            body
+        })
+));
+
+named!(let_(&str) -> Expr, do_parse!(
+    tag!("let")
+        >> multispace0
+        >> bindings: separated_list1!(alt!(char!(';') | char!('\n')), binding)
+        >> multispace0
+        >> tag!("in")
+        >> multispace0
+        >> body: expr
+        >> (Expr::Let {
+            bindings,
+            body: Box::new(body)
+        })
+));
+
+named!(if_(&str) -> Expr, do_parse! (
+    tag!("if")
+        >> multispace0
+        >> condition: expr
+        >> multispace0
+        >> tag!("then")
+        >> multispace0
+        >> then: expr
+        >> multispace0
+        >> tag!("else")
+        >> multispace0
+        >> else_: expr
+        >> (Expr::If {
+            condition: Box::new(condition),
+            then: Box::new(then),
+            else_: Box::new(else_)
+        })
+));
+
+named!(ident_expr(&str) -> Expr, map!(ident, Expr::Ident));
+
+fn ascripted<'a>(
+    p: impl Fn(&'a str) -> nom::IResult<&'a str, Expr, nom::error::Error<&'a str>> + 'a,
+) -> impl Fn(&'a str) -> nom::IResult<&str, Expr, nom::error::Error<&'a str>> {
+    move |i| {
+        do_parse!(
+            i,
+            expr: p
+                >> multispace0
+                >> complete!(tag!(":"))
+                >> multispace0
+                >> type_: type_
+                >> (Expr::Ascription {
+                    expr: Box::new(expr),
+                    type_
+                })
+        )
+    }
+}
+
+named!(paren_expr(&str) -> Expr,
+       delimited!(complete!(tag!("(")), expr, complete!(tag!(")"))));
+
+named!(funcref(&str) -> Expr, alt!(
+    ident_expr |
+    tuple |
+    paren_expr
+));
+
+named!(no_arg_call(&str) -> Expr, do_parse!(
+    fun: funcref
+        >> complete!(tag!("()"))
+        >> (Expr::Call {
+            fun: Box::new(fun),
+            args: vec![],
+        })
+));
+
+named!(fun_expr(&str) -> Expr, do_parse!(
+    tag!("fn")
+        >> multispace1
+        >> args: separated_list0!(multispace1, arg)
+        >> multispace0
+        >> char!('=')
+        >> multispace0
+        >> body: expr
+        >> (Expr::Fun(Box::new(Fun {
+            args,
+            body
+        })))
+));
+
+named!(fn_arg(&str) -> Expr, alt!(
+    ident_expr |
+    literal_expr |
+    tuple |
+    paren_expr
+));
+
+named!(call_with_args(&str) -> Expr, do_parse!(
+    fun: funcref
+        >> multispace1
+        >> args: separated_list1!(multispace1, fn_arg)
+        >> (Expr::Call {
+            fun: Box::new(fun),
+            args
+        })
+));
+
+named!(simple_expr_unascripted(&str) -> Expr, alt!(
+    let_ |
+    if_ |
+    fun_expr |
+    literal_expr |
+    ident_expr |
+    tuple
+));
+
+named!(simple_expr(&str) -> Expr, alt!(
+    call!(ascripted(simple_expr_unascripted)) |
+    simple_expr_unascripted
+));
+
+named!(pub expr(&str) -> Expr, alt!(
+    no_arg_call |
+    call_with_args |
+    map!(token_tree, |tt| {
+        ExprParser.parse(&mut tt.into_iter()).unwrap()
+    }) |
+    simple_expr
+));
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+    use crate::ast::{Arg, Ident, Pattern, Type};
+    use std::convert::TryFrom;
+    use BinaryOperator::*;
+    use Expr::{BinaryOp, If, Let, UnaryOp};
+    use UnaryOperator::*;
+
+    pub(crate) fn ident_expr(s: &str) -> Box<Expr> {
+        Box::new(Expr::Ident(Ident::try_from(s).unwrap()))
+    }
+
+    mod operators {
+        use super::*;
+
+        #[test]
+        fn mul_plus() {
+            let (rem, res) = expr("x*y+z").unwrap();
+            assert!(rem.is_empty());
+            assert_eq!(
+                res,
+                BinaryOp {
+                    lhs: Box::new(BinaryOp {
+                        lhs: ident_expr("x"),
+                        op: Mul,
+                        rhs: ident_expr("y")
+                    }),
+                    op: Add,
+                    rhs: ident_expr("z")
+                }
+            )
+        }
+
+        #[test]
+        fn mul_plus_ws() {
+            let (rem, res) = expr("x * y    +    z").unwrap();
+            assert!(rem.is_empty(), "non-empty remainder: \"{}\"", rem);
+            assert_eq!(
+                res,
+                BinaryOp {
+                    lhs: Box::new(BinaryOp {
+                        lhs: ident_expr("x"),
+                        op: Mul,
+                        rhs: ident_expr("y")
+                    }),
+                    op: Add,
+                    rhs: ident_expr("z")
+                }
+            )
+        }
+
+        #[test]
+        fn unary() {
+            let (rem, res) = expr("x * -z").unwrap();
+            assert!(rem.is_empty(), "non-empty remainder: \"{}\"", rem);
+            assert_eq!(
+                res,
+                BinaryOp {
+                    lhs: ident_expr("x"),
+                    op: Mul,
+                    rhs: Box::new(UnaryOp {
+                        op: Neg,
+                        rhs: ident_expr("z"),
+                    })
+                }
+            )
+        }
+
+        #[test]
+        fn mul_literal() {
+            let (rem, res) = expr("x * 3").unwrap();
+            assert!(rem.is_empty());
+            assert_eq!(
+                res,
+                BinaryOp {
+                    lhs: ident_expr("x"),
+                    op: Mul,
+                    rhs: Box::new(Expr::Literal(Literal::Int(3))),
+                }
+            )
+        }
+
+        #[test]
+        fn equ() {
+            let res = test_parse!(expr, "x * 7 == 7");
+            assert_eq!(
+                res,
+                BinaryOp {
+                    lhs: Box::new(BinaryOp {
+                        lhs: ident_expr("x"),
+                        op: Mul,
+                        rhs: Box::new(Expr::Literal(Literal::Int(7)))
+                    }),
+                    op: Equ,
+                    rhs: Box::new(Expr::Literal(Literal::Int(7)))
+                }
+            )
+        }
+    }
+
+    #[test]
+    fn unit() {
+        assert_eq!(test_parse!(expr, "()"), Expr::Literal(Literal::Unit));
+    }
+
+    #[test]
+    fn bools() {
+        assert_eq!(
+            test_parse!(expr, "true"),
+            Expr::Literal(Literal::Bool(true))
+        );
+        assert_eq!(
+            test_parse!(expr, "false"),
+            Expr::Literal(Literal::Bool(false))
+        );
+    }
+
+    #[test]
+    fn tuple() {
+        assert_eq!(
+            test_parse!(expr, "(1, \"seven\")"),
+            Expr::Tuple(vec![
+                Expr::Literal(Literal::Int(1)),
+                Expr::Literal(Literal::String(Cow::Borrowed("seven")))
+            ])
+        )
+    }
+
+    #[test]
+    fn simple_string_lit() {
+        assert_eq!(
+            test_parse!(expr, "\"foobar\""),
+            Expr::Literal(Literal::String(Cow::Borrowed("foobar")))
+        )
+    }
+
+    #[test]
+    fn let_complex() {
+        let res = test_parse!(expr, "let x = 1; y = x * 7 in (x + y) * 4");
+        assert_eq!(
+            res,
+            Let {
+                bindings: vec![
+                    Binding {
+                        pat: Pattern::Id(Ident::try_from("x").unwrap()),
+                        type_: None,
+                        body: Expr::Literal(Literal::Int(1))
+                    },
+                    Binding {
+                        pat: Pattern::Id(Ident::try_from("y").unwrap()),
+                        type_: None,
+                        body: Expr::BinaryOp {
+                            lhs: ident_expr("x"),
+                            op: Mul,
+                            rhs: Box::new(Expr::Literal(Literal::Int(7)))
+                        }
+                    }
+                ],
+                body: Box::new(Expr::BinaryOp {
+                    lhs: Box::new(Expr::BinaryOp {
+                        lhs: ident_expr("x"),
+                        op: Add,
+                        rhs: ident_expr("y"),
+                    }),
+                    op: Mul,
+                    rhs: Box::new(Expr::Literal(Literal::Int(4))),
+                })
+            }
+        )
+    }
+
+    #[test]
+    fn if_simple() {
+        let res = test_parse!(expr, "if x == 8 then 9 else 20");
+        assert_eq!(
+            res,
+            If {
+                condition: Box::new(BinaryOp {
+                    lhs: ident_expr("x"),
+                    op: Equ,
+                    rhs: Box::new(Expr::Literal(Literal::Int(8))),
+                }),
+                then: Box::new(Expr::Literal(Literal::Int(9))),
+                else_: Box::new(Expr::Literal(Literal::Int(20)))
+            }
+        )
+    }
+
+    #[test]
+    fn no_arg_call() {
+        let res = test_parse!(expr, "f()");
+        assert_eq!(
+            res,
+            Expr::Call {
+                fun: ident_expr("f"),
+                args: vec![]
+            }
+        );
+    }
+
+    #[test]
+    fn unit_call() {
+        let res = test_parse!(expr, "f ()");
+        assert_eq!(
+            res,
+            Expr::Call {
+                fun: ident_expr("f"),
+                args: vec![Expr::Literal(Literal::Unit)]
+            }
+        )
+    }
+
+    #[test]
+    fn call_with_args() {
+        let res = test_parse!(expr, "f x 1");
+        assert_eq!(
+            res,
+            Expr::Call {
+                fun: ident_expr("f"),
+                args: vec![*ident_expr("x"), Expr::Literal(Literal::Int(1))]
+            }
+        )
+    }
+
+    #[test]
+    fn call_funcref() {
+        let res = test_parse!(expr, "(let x = 1 in x) 2");
+        assert_eq!(
+            res,
+            Expr::Call {
+                fun: Box::new(Expr::Let {
+                    bindings: vec![Binding {
+                        pat: Pattern::Id(Ident::try_from("x").unwrap()),
+                        type_: None,
+                        body: Expr::Literal(Literal::Int(1))
+                    }],
+                    body: ident_expr("x")
+                }),
+                args: vec![Expr::Literal(Literal::Int(2))]
+            }
+        )
+    }
+
+    #[test]
+    fn anon_function() {
+        let res = test_parse!(expr, "let id = fn x = x in id 1");
+        assert_eq!(
+            res,
+            Expr::Let {
+                bindings: vec![Binding {
+                    pat: Pattern::Id(Ident::try_from("id").unwrap()),
+                    type_: None,
+                    body: Expr::Fun(Box::new(Fun {
+                        args: vec![Arg::try_from("x").unwrap()],
+                        body: *ident_expr("x")
+                    }))
+                }],
+                body: Box::new(Expr::Call {
+                    fun: ident_expr("id"),
+                    args: vec![Expr::Literal(Literal::Int(1))],
+                })
+            }
+        );
+    }
+
+    #[test]
+    fn tuple_binding() {
+        let res = test_parse!(expr, "let (x, y) = (1, 2) in x");
+        assert_eq!(
+            res,
+            Expr::Let {
+                bindings: vec![Binding {
+                    pat: Pattern::Tuple(vec![
+                        Pattern::Id(Ident::from_str_unchecked("x")),
+                        Pattern::Id(Ident::from_str_unchecked("y"))
+                    ]),
+                    body: Expr::Tuple(vec![
+                        Expr::Literal(Literal::Int(1)),
+                        Expr::Literal(Literal::Int(2))
+                    ]),
+                    type_: None
+                }],
+                body: Box::new(Expr::Ident(Ident::from_str_unchecked("x")))
+            }
+        )
+    }
+
+    mod ascriptions {
+        use super::*;
+
+        #[test]
+        fn bare_ascription() {
+            let res = test_parse!(expr, "1: float");
+            assert_eq!(
+                res,
+                Expr::Ascription {
+                    expr: Box::new(Expr::Literal(Literal::Int(1))),
+                    type_: Type::Float
+                }
+            )
+        }
+
+        #[test]
+        fn fn_body_ascription() {
+            let res = test_parse!(expr, "let const_1 = fn x = 1: int in const_1 2");
+            assert_eq!(
+                res,
+                Expr::Let {
+                    bindings: vec![Binding {
+                        pat: Pattern::Id(Ident::try_from("const_1").unwrap()),
+                        type_: None,
+                        body: Expr::Fun(Box::new(Fun {
+                            args: vec![Arg::try_from("x").unwrap()],
+                            body: Expr::Ascription {
+                                expr: Box::new(Expr::Literal(Literal::Int(1))),
+                                type_: Type::Int,
+                            }
+                        }))
+                    }],
+                    body: Box::new(Expr::Call {
+                        fun: ident_expr("const_1"),
+                        args: vec![Expr::Literal(Literal::Int(2))]
+                    })
+                }
+            )
+        }
+
+        #[test]
+        fn let_binding_ascripted() {
+            let res = test_parse!(expr, "let x: int = 1 in x");
+            assert_eq!(
+                res,
+                Expr::Let {
+                    bindings: vec![Binding {
+                        pat: Pattern::Id(Ident::try_from("x").unwrap()),
+                        type_: Some(Type::Int),
+                        body: Expr::Literal(Literal::Int(1))
+                    }],
+                    body: ident_expr("x")
+                }
+            )
+        }
+    }
+}
diff --git a/users/grfn/achilles/src/parser/macros.rs b/users/grfn/achilles/src/parser/macros.rs
new file mode 100644
index 0000000000..406e5c0e69
--- /dev/null
+++ b/users/grfn/achilles/src/parser/macros.rs
@@ -0,0 +1,16 @@
+#[cfg(test)]
+#[macro_use]
+macro_rules! test_parse {
+    ($parser: ident, $src: expr) => {{
+        let res = $parser($src);
+        nom_trace::print_trace!();
+        let (rem, res) = res.unwrap();
+        assert!(
+            rem.is_empty(),
+            "non-empty remainder: \"{}\", parsed: {:?}",
+            rem,
+            res
+        );
+        res
+    }};
+}
diff --git a/users/grfn/achilles/src/parser/mod.rs b/users/grfn/achilles/src/parser/mod.rs
new file mode 100644
index 0000000000..e088cbca10
--- /dev/null
+++ b/users/grfn/achilles/src/parser/mod.rs
@@ -0,0 +1,240 @@
+use nom::character::complete::{multispace0, multispace1};
+use nom::error::{ErrorKind, ParseError};
+use nom::{alt, char, complete, do_parse, eof, many0, named, separated_list0, tag, terminated};
+
+#[macro_use]
+pub(crate) mod macros;
+mod expr;
+mod type_;
+mod util;
+
+use crate::ast::{Arg, Decl, Fun, Ident};
+pub use expr::expr;
+use type_::function_type;
+pub use type_::type_;
+
+pub type Error = nom::Err<nom::error::Error<String>>;
+
+pub(crate) fn is_reserved(s: &str) -> bool {
+    matches!(
+        s,
+        "if" | "then"
+            | "else"
+            | "let"
+            | "in"
+            | "fn"
+            | "ty"
+            | "int"
+            | "float"
+            | "bool"
+            | "true"
+            | "false"
+            | "cstring"
+    )
+}
+
+pub(crate) fn ident<'a, E>(i: &'a str) -> nom::IResult<&'a str, Ident, E>
+where
+    E: ParseError<&'a str>,
+{
+    let mut chars = i.chars();
+    if let Some(f) = chars.next() {
+        if f.is_alphabetic() || f == '_' {
+            let mut idx = 1;
+            for c in chars {
+                if !(c.is_alphanumeric() || c == '_') {
+                    break;
+                }
+                idx += 1;
+            }
+            let id = &i[..idx];
+            if is_reserved(id) {
+                Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::Satisfy)))
+            } else {
+                Ok((&i[idx..], Ident::from_str_unchecked(id)))
+            }
+        } else {
+            Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::Satisfy)))
+        }
+    } else {
+        Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::Eof)))
+    }
+}
+
+named!(ascripted_arg(&str) -> Arg, do_parse!(
+    complete!(char!('(')) >>
+        multispace0 >>
+        ident: ident >>
+        multispace0 >>
+        complete!(char!(':')) >>
+        multispace0 >>
+        type_: type_ >>
+        multispace0 >>
+        complete!(char!(')')) >>
+        (Arg {
+            ident,
+            type_: Some(type_)
+        })
+));
+
+named!(arg(&str) -> Arg, alt!(
+    ident => { |ident| Arg {ident, type_: None}} |
+    ascripted_arg
+));
+
+named!(extern_decl(&str) -> Decl, do_parse!(
+    complete!(tag!("extern"))
+        >> multispace1
+        >> name: ident
+        >> multispace0
+        >> char!(':')
+        >> multispace0
+        >> type_: function_type
+        >> multispace0
+        >> (Decl::Extern {
+            name,
+            type_
+        })
+));
+
+named!(fun_decl(&str) -> Decl, do_parse!(
+    complete!(tag!("fn"))
+        >> multispace1
+        >> name: ident
+        >> multispace1
+        >> args: separated_list0!(multispace1, arg)
+        >> multispace0
+        >> char!('=')
+        >> multispace0
+        >> body: expr
+        >> (Decl::Fun {
+            name,
+            body: Fun {
+                args,
+                body
+            }
+        })
+));
+
+named!(ascription_decl(&str) -> Decl, do_parse!(
+    complete!(tag!("ty"))
+        >> multispace1
+        >> name: ident
+        >> multispace0
+        >> complete!(char!(':'))
+        >> multispace0
+        >> type_: type_
+        >> multispace0
+        >> (Decl::Ascription {
+            name,
+            type_
+        })
+));
+
+named!(pub decl(&str) -> Decl, alt!(
+    ascription_decl |
+    fun_decl |
+    extern_decl
+));
+
+named!(pub toplevel(&str) -> Vec<Decl>, do_parse!(
+    decls: many0!(decl)
+        >> multispace0
+        >> eof!()
+        >> (decls)));
+
+#[cfg(test)]
+mod tests {
+    use std::convert::TryInto;
+
+    use crate::ast::{BinaryOperator, Expr, FunctionType, Literal, Type};
+
+    use super::*;
+    use expr::tests::ident_expr;
+
+    #[test]
+    fn fn_decl() {
+        let res = test_parse!(decl, "fn id x = x");
+        assert_eq!(
+            res,
+            Decl::Fun {
+                name: "id".try_into().unwrap(),
+                body: Fun {
+                    args: vec!["x".try_into().unwrap()],
+                    body: *ident_expr("x"),
+                }
+            }
+        )
+    }
+
+    #[test]
+    fn ascripted_fn_args() {
+        test_parse!(ascripted_arg, "(x : int)");
+        let res = test_parse!(decl, "fn plus1 (x : int) = x + 1");
+        assert_eq!(
+            res,
+            Decl::Fun {
+                name: "plus1".try_into().unwrap(),
+                body: Fun {
+                    args: vec![Arg {
+                        ident: "x".try_into().unwrap(),
+                        type_: Some(Type::Int),
+                    }],
+                    body: Expr::BinaryOp {
+                        lhs: ident_expr("x"),
+                        op: BinaryOperator::Add,
+                        rhs: Box::new(Expr::Literal(Literal::Int(1))),
+                    }
+                }
+            }
+        );
+    }
+
+    #[test]
+    fn multiple_decls() {
+        let res = test_parse!(
+            toplevel,
+            "fn id x = x
+             fn plus x y = x + y
+             fn main = plus (id 2) 7"
+        );
+        assert_eq!(res.len(), 3);
+        let res = test_parse!(
+            toplevel,
+            "fn id x = x\nfn plus x y = x + y\nfn main = plus (id 2) 7\n"
+        );
+        assert_eq!(res.len(), 3);
+    }
+
+    #[test]
+    fn top_level_ascription() {
+        let res = test_parse!(toplevel, "ty id : fn a -> a");
+        assert_eq!(
+            res,
+            vec![Decl::Ascription {
+                name: "id".try_into().unwrap(),
+                type_: Type::Function(FunctionType {
+                    args: vec![Type::Var("a".try_into().unwrap())],
+                    ret: Box::new(Type::Var("a".try_into().unwrap()))
+                })
+            }]
+        )
+    }
+
+    #[test]
+    fn return_unit() {
+        assert_eq!(
+            test_parse!(decl, "fn g _ = ()"),
+            Decl::Fun {
+                name: "g".try_into().unwrap(),
+                body: Fun {
+                    args: vec![Arg {
+                        ident: "_".try_into().unwrap(),
+                        type_: None,
+                    }],
+                    body: Expr::Literal(Literal::Unit),
+                },
+            }
+        )
+    }
+}
diff --git a/users/grfn/achilles/src/parser/type_.rs b/users/grfn/achilles/src/parser/type_.rs
new file mode 100644
index 0000000000..b80f0e0860
--- /dev/null
+++ b/users/grfn/achilles/src/parser/type_.rs
@@ -0,0 +1,152 @@
+use nom::character::complete::{multispace0, multispace1};
+use nom::{alt, delimited, do_parse, map, named, opt, separated_list0, tag, terminated, tuple};
+
+use super::ident;
+use super::util::comma;
+use crate::ast::{FunctionType, Type};
+
+named!(pub function_type(&str) -> FunctionType, do_parse!(
+    tag!("fn")
+        >> multispace1
+        >> args: map!(opt!(terminated!(separated_list0!(
+            comma,
+            type_
+        ), multispace1)), |args| args.unwrap_or_default())
+        >> tag!("->")
+        >> multispace1
+        >> ret: type_
+        >> (FunctionType {
+            args,
+            ret: Box::new(ret)
+        })
+));
+
+named!(tuple_type(&str) -> Type, do_parse!(
+    tag!("(")
+        >> multispace0
+        >> fst: type_
+        >> comma
+        >> rest: separated_list0!(
+            comma,
+            type_
+        )
+        >> multispace0
+        >> tag!(")")
+        >> ({
+            let mut members = Vec::with_capacity(rest.len() + 1);
+            members.push(fst);
+            members.append(&mut rest.clone());
+            Type::Tuple(members)
+        })
+));
+
+named!(pub type_(&str) -> Type, alt!(
+    tag!("int") => { |_| Type::Int } |
+    tag!("float") => { |_| Type::Float } |
+    tag!("bool") => { |_| Type::Bool } |
+    tag!("cstring") => { |_| Type::CString } |
+    tag!("()") => { |_| Type::Unit } |
+    tuple_type |
+    function_type => { |ft| Type::Function(ft) }|
+    ident => { |id| Type::Var(id) } |
+    delimited!(
+        tuple!(tag!("("), multispace0),
+        type_,
+        tuple!(tag!(")"), multispace0)
+    )
+));
+
+#[cfg(test)]
+mod tests {
+    use std::convert::TryFrom;
+
+    use super::*;
+    use crate::ast::Ident;
+
+    #[test]
+    fn simple_types() {
+        assert_eq!(test_parse!(type_, "int"), Type::Int);
+        assert_eq!(test_parse!(type_, "float"), Type::Float);
+        assert_eq!(test_parse!(type_, "bool"), Type::Bool);
+        assert_eq!(test_parse!(type_, "cstring"), Type::CString);
+        assert_eq!(test_parse!(type_, "()"), Type::Unit);
+    }
+
+    #[test]
+    fn no_arg_fn_type() {
+        assert_eq!(
+            test_parse!(type_, "fn -> int"),
+            Type::Function(FunctionType {
+                args: vec![],
+                ret: Box::new(Type::Int)
+            })
+        );
+    }
+
+    #[test]
+    fn fn_type_with_args() {
+        assert_eq!(
+            test_parse!(type_, "fn int, bool -> int"),
+            Type::Function(FunctionType {
+                args: vec![Type::Int, Type::Bool],
+                ret: Box::new(Type::Int)
+            })
+        );
+    }
+
+    #[test]
+    fn fn_taking_fn() {
+        assert_eq!(
+            test_parse!(type_, "fn fn int, bool -> bool, float -> float"),
+            Type::Function(FunctionType {
+                args: vec![
+                    Type::Function(FunctionType {
+                        args: vec![Type::Int, Type::Bool],
+                        ret: Box::new(Type::Bool)
+                    }),
+                    Type::Float
+                ],
+                ret: Box::new(Type::Float)
+            })
+        )
+    }
+
+    #[test]
+    fn parenthesized() {
+        assert_eq!(
+            test_parse!(type_, "fn (fn int, bool -> bool), float -> float"),
+            Type::Function(FunctionType {
+                args: vec![
+                    Type::Function(FunctionType {
+                        args: vec![Type::Int, Type::Bool],
+                        ret: Box::new(Type::Bool)
+                    }),
+                    Type::Float
+                ],
+                ret: Box::new(Type::Float)
+            })
+        )
+    }
+
+    #[test]
+    fn tuple() {
+        assert_eq!(
+            test_parse!(type_, "(int, int)"),
+            Type::Tuple(vec![Type::Int, Type::Int])
+        )
+    }
+
+    #[test]
+    fn type_vars() {
+        assert_eq!(
+            test_parse!(type_, "fn x, y -> x"),
+            Type::Function(FunctionType {
+                args: vec![
+                    Type::Var(Ident::try_from("x").unwrap()),
+                    Type::Var(Ident::try_from("y").unwrap()),
+                ],
+                ret: Box::new(Type::Var(Ident::try_from("x").unwrap())),
+            })
+        )
+    }
+}
diff --git a/users/grfn/achilles/src/parser/util.rs b/users/grfn/achilles/src/parser/util.rs
new file mode 100644
index 0000000000..bb53fb7fff
--- /dev/null
+++ b/users/grfn/achilles/src/parser/util.rs
@@ -0,0 +1,8 @@
+use nom::character::complete::multispace0;
+use nom::{complete, map, named, tag, tuple};
+
+named!(pub(crate) comma(&str) -> (), map!(tuple!(
+    multispace0,
+    complete!(tag!(",")),
+    multispace0
+) ,|_| ()));
diff --git a/users/grfn/achilles/src/passes/hir/mod.rs b/users/grfn/achilles/src/passes/hir/mod.rs
new file mode 100644
index 0000000000..872c449eb0
--- /dev/null
+++ b/users/grfn/achilles/src/passes/hir/mod.rs
@@ -0,0 +1,211 @@
+use std::collections::HashMap;
+
+use crate::ast::hir::{Binding, Decl, Expr, Pattern};
+use crate::ast::{BinaryOperator, Ident, Literal, UnaryOperator};
+
+pub(crate) mod monomorphize;
+pub(crate) mod strip_positive_units;
+
+pub(crate) trait Visitor<'a, 'ast, T: 'ast>: Sized + 'a {
+    type Error;
+
+    fn visit_type(&mut self, _type: &mut T) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_ident(&mut self, _ident: &mut Ident<'ast>) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_literal(&mut self, _literal: &mut Literal<'ast>) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_unary_operator(&mut self, _op: &mut UnaryOperator) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_binary_operator(&mut self, _op: &mut BinaryOperator) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_pattern(&mut self, _pat: &mut Pattern<'ast, T>) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_binding(&mut self, binding: &mut Binding<'ast, T>) -> Result<(), Self::Error> {
+        self.visit_pattern(&mut binding.pat)?;
+        self.visit_expr(&mut binding.body)?;
+        Ok(())
+    }
+
+    fn post_visit_call(
+        &mut self,
+        _fun: &mut Expr<'ast, T>,
+        _type_args: &mut HashMap<Ident<'ast>, T>,
+        _args: &mut Vec<Expr<'ast, T>>,
+    ) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn pre_visit_call(
+        &mut self,
+        _fun: &mut Expr<'ast, T>,
+        _type_args: &mut HashMap<Ident<'ast>, T>,
+        _args: &mut Vec<Expr<'ast, T>>,
+    ) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_tuple(&mut self, members: &mut Vec<Expr<'ast, T>>) -> Result<(), Self::Error> {
+        for expr in members {
+            self.visit_expr(expr)?;
+        }
+        Ok(())
+    }
+
+    fn pre_visit_expr(&mut self, _expr: &mut Expr<'ast, T>) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_expr(&mut self, expr: &mut Expr<'ast, T>) -> Result<(), Self::Error> {
+        self.pre_visit_expr(expr)?;
+        match expr {
+            Expr::Ident(id, t) => {
+                self.visit_ident(id)?;
+                self.visit_type(t)?;
+            }
+            Expr::Literal(lit, t) => {
+                self.visit_literal(lit)?;
+                self.visit_type(t)?;
+            }
+            Expr::UnaryOp { op, rhs, type_ } => {
+                self.visit_unary_operator(op)?;
+                self.visit_expr(rhs)?;
+                self.visit_type(type_)?;
+            }
+            Expr::BinaryOp {
+                lhs,
+                op,
+                rhs,
+                type_,
+            } => {
+                self.visit_expr(lhs)?;
+                self.visit_binary_operator(op)?;
+                self.visit_expr(rhs)?;
+                self.visit_type(type_)?;
+            }
+            Expr::Let {
+                bindings,
+                body,
+                type_,
+            } => {
+                for binding in bindings.iter_mut() {
+                    self.visit_binding(binding)?;
+                }
+                self.visit_expr(body)?;
+                self.visit_type(type_)?;
+            }
+            Expr::If {
+                condition,
+                then,
+                else_,
+                type_,
+            } => {
+                self.visit_expr(condition)?;
+                self.visit_expr(then)?;
+                self.visit_expr(else_)?;
+                self.visit_type(type_)?;
+            }
+            Expr::Fun {
+                args,
+                body,
+                type_args,
+                type_,
+            } => {
+                for (ident, t) in args {
+                    self.visit_ident(ident)?;
+                    self.visit_type(t)?;
+                }
+                for ta in type_args {
+                    self.visit_ident(ta)?;
+                }
+                self.visit_expr(body)?;
+                self.visit_type(type_)?;
+            }
+            Expr::Call {
+                fun,
+                args,
+                type_args,
+                type_,
+            } => {
+                self.pre_visit_call(fun, type_args, args)?;
+                self.visit_expr(fun)?;
+                for arg in args.iter_mut() {
+                    self.visit_expr(arg)?;
+                }
+                self.visit_type(type_)?;
+                self.post_visit_call(fun, type_args, args)?;
+            }
+            Expr::Tuple(tup, type_) => {
+                self.visit_tuple(tup)?;
+                self.visit_type(type_)?;
+            }
+        }
+
+        Ok(())
+    }
+
+    fn post_visit_decl(&mut self, _decl: &'a Decl<'ast, T>) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn post_visit_fun_decl(
+        &mut self,
+        _name: &mut Ident<'ast>,
+        _type_args: &mut Vec<Ident>,
+        _args: &mut Vec<(Ident, T)>,
+        _body: &mut Box<Expr<T>>,
+        _type_: &mut T,
+    ) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    fn visit_decl(&mut self, decl: &'a mut Decl<'ast, T>) -> Result<(), Self::Error> {
+        match decl {
+            Decl::Fun {
+                name,
+                type_args,
+                args,
+                body,
+                type_,
+            } => {
+                self.visit_ident(name)?;
+                for type_arg in type_args.iter_mut() {
+                    self.visit_ident(type_arg)?;
+                }
+                for (arg, t) in args.iter_mut() {
+                    self.visit_ident(arg)?;
+                    self.visit_type(t)?;
+                }
+                self.visit_expr(body)?;
+                self.visit_type(type_)?;
+                self.post_visit_fun_decl(name, type_args, args, body, type_)?;
+            }
+            Decl::Extern {
+                name,
+                arg_types,
+                ret_type,
+            } => {
+                self.visit_ident(name)?;
+                for arg_t in arg_types {
+                    self.visit_type(arg_t)?;
+                }
+                self.visit_type(ret_type)?;
+            }
+        }
+
+        self.post_visit_decl(decl)?;
+        Ok(())
+    }
+}
diff --git a/users/grfn/achilles/src/passes/hir/monomorphize.rs b/users/grfn/achilles/src/passes/hir/monomorphize.rs
new file mode 100644
index 0000000000..251a988f4f
--- /dev/null
+++ b/users/grfn/achilles/src/passes/hir/monomorphize.rs
@@ -0,0 +1,139 @@
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::convert::TryInto;
+use std::mem;
+
+use void::{ResultVoidExt, Void};
+
+use crate::ast::hir::{Decl, Expr};
+use crate::ast::{self, Ident};
+
+use super::Visitor;
+
+#[derive(Default)]
+pub(crate) struct Monomorphize<'a, 'ast> {
+    decls: HashMap<&'a Ident<'ast>, &'a Decl<'ast, ast::Type<'ast>>>,
+    extra_decls: Vec<Decl<'ast, ast::Type<'ast>>>,
+    remove_decls: HashSet<Ident<'ast>>,
+}
+
+impl<'a, 'ast> Monomorphize<'a, 'ast> {
+    pub(crate) fn new() -> Self {
+        Default::default()
+    }
+}
+
+impl<'a, 'ast> Visitor<'a, 'ast, ast::Type<'ast>> for Monomorphize<'a, 'ast> {
+    type Error = Void;
+
+    fn post_visit_call(
+        &mut self,
+        fun: &mut Expr<'ast, ast::Type<'ast>>,
+        type_args: &mut HashMap<Ident<'ast>, ast::Type<'ast>>,
+        args: &mut Vec<Expr<'ast, ast::Type<'ast>>>,
+    ) -> Result<(), Self::Error> {
+        let new_fun = match fun {
+            Expr::Ident(id, _) => {
+                let decl: Decl<_> = (**self.decls.get(id).unwrap()).clone();
+                let name = RefCell::new(id.to_string());
+                let type_args = mem::take(type_args);
+                let mut monomorphized = decl
+                    .traverse_type(|ty| -> Result<_, Void> {
+                        Ok(ty.clone().traverse_type_vars(|v| {
+                            let concrete = type_args.get(&v).unwrap();
+                            name.borrow_mut().push_str(&concrete.to_string());
+                            concrete.clone()
+                        }))
+                    })
+                    .void_unwrap();
+                let name: Ident = name.into_inner().try_into().unwrap();
+                if name != *id {
+                    self.remove_decls.insert(id.clone());
+                    monomorphized.set_name(name.clone());
+                    let type_ = monomorphized.type_().unwrap().clone();
+                    self.extra_decls.push(monomorphized);
+                    Some(Expr::Ident(name, type_))
+                } else {
+                    None
+                }
+            }
+            _ => todo!(),
+        };
+        if let Some(new_fun) = new_fun {
+            *fun = new_fun;
+        }
+        Ok(())
+    }
+
+    fn post_visit_decl(
+        &mut self,
+        decl: &'a Decl<'ast, ast::Type<'ast>>,
+    ) -> Result<(), Self::Error> {
+        self.decls.insert(decl.name(), decl);
+        Ok(())
+    }
+}
+
+pub(crate) fn run_toplevel<'a>(toplevel: &mut Vec<Decl<'a, ast::Type<'a>>>) {
+    let mut pass = Monomorphize::new();
+    for decl in toplevel.iter_mut() {
+        pass.visit_decl(decl).void_unwrap();
+    }
+    let remove_decls = mem::take(&mut pass.remove_decls);
+    let mut extra_decls = mem::take(&mut pass.extra_decls);
+    toplevel.retain(|decl| !remove_decls.contains(decl.name()));
+    extra_decls.append(toplevel);
+    *toplevel = extra_decls;
+}
+
+#[cfg(test)]
+mod tests {
+    use std::convert::TryFrom;
+
+    use super::*;
+    use crate::parser::toplevel;
+    use crate::tc::typecheck_toplevel;
+
+    #[test]
+    fn call_id_decl() {
+        let (_, program) = toplevel(
+            "ty id : fn a -> a
+             fn id x = x
+
+             ty main : fn -> int
+             fn main = id 0",
+        )
+        .unwrap();
+        let mut program = typecheck_toplevel(program).unwrap();
+        run_toplevel(&mut program);
+
+        let find_decl = |ident: &str| {
+            program.iter().find(|decl| {
+                matches!(decl, Decl::Fun {name, ..} if name == &Ident::try_from(ident).unwrap())
+            }).unwrap()
+        };
+
+        let main = find_decl("main");
+        let body = match main {
+            Decl::Fun { body, .. } => body,
+            _ => unreachable!(),
+        };
+
+        let expected_type = ast::Type::Function(ast::FunctionType {
+            args: vec![ast::Type::Int],
+            ret: Box::new(ast::Type::Int),
+        });
+
+        match &**body {
+            Expr::Call { fun, .. } => {
+                let fun = match &**fun {
+                    Expr::Ident(fun, _) => fun,
+                    _ => unreachable!(),
+                };
+                let called_decl = find_decl(fun.into());
+                assert_eq!(called_decl.type_().unwrap(), &expected_type);
+            }
+            _ => unreachable!(),
+        }
+    }
+}
diff --git a/users/grfn/achilles/src/passes/hir/strip_positive_units.rs b/users/grfn/achilles/src/passes/hir/strip_positive_units.rs
new file mode 100644
index 0000000000..85ee1cce48
--- /dev/null
+++ b/users/grfn/achilles/src/passes/hir/strip_positive_units.rs
@@ -0,0 +1,191 @@
+use std::collections::HashMap;
+use std::mem;
+
+use ast::hir::{Binding, Pattern};
+use ast::Literal;
+use void::{ResultVoidExt, Void};
+
+use crate::ast::hir::{Decl, Expr};
+use crate::ast::{self, Ident};
+
+use super::Visitor;
+
+/// Strip all values with a unit type in positive (non-return) position
+pub(crate) struct StripPositiveUnits {}
+
+impl<'a, 'ast> Visitor<'a, 'ast, ast::Type<'ast>> for StripPositiveUnits {
+    type Error = Void;
+
+    fn pre_visit_expr(
+        &mut self,
+        expr: &mut Expr<'ast, ast::Type<'ast>>,
+    ) -> Result<(), Self::Error> {
+        let mut extracted = vec![];
+        if let Expr::Call { args, .. } = expr {
+            // TODO(grfn): replace with drain_filter once it's stabilized
+            let mut i = 0;
+            while i != args.len() {
+                if args[i].type_() == &ast::Type::Unit {
+                    let expr = args.remove(i);
+                    if !matches!(expr, Expr::Literal(Literal::Unit, _)) {
+                        extracted.push(expr)
+                    };
+                } else {
+                    i += 1
+                }
+            }
+        }
+
+        if !extracted.is_empty() {
+            let body = mem::replace(expr, Expr::Literal(Literal::Unit, ast::Type::Unit));
+            *expr = Expr::Let {
+                bindings: extracted
+                    .into_iter()
+                    .map(|expr| Binding {
+                        pat: Pattern::Id(
+                            Ident::from_str_unchecked("___discarded"),
+                            expr.type_().clone(),
+                        ),
+                        body: expr,
+                    })
+                    .collect(),
+                type_: body.type_().clone(),
+                body: Box::new(body),
+            };
+        }
+
+        Ok(())
+    }
+
+    fn post_visit_call(
+        &mut self,
+        _fun: &mut Expr<'ast, ast::Type<'ast>>,
+        _type_args: &mut HashMap<Ident<'ast>, ast::Type<'ast>>,
+        args: &mut Vec<Expr<'ast, ast::Type<'ast>>>,
+    ) -> Result<(), Self::Error> {
+        args.retain(|arg| arg.type_() != &ast::Type::Unit);
+        Ok(())
+    }
+
+    fn visit_type(&mut self, type_: &mut ast::Type<'ast>) -> Result<(), Self::Error> {
+        if let ast::Type::Function(ft) = type_ {
+            ft.args.retain(|a| a != &ast::Type::Unit);
+        }
+        Ok(())
+    }
+
+    fn post_visit_fun_decl(
+        &mut self,
+        _name: &mut Ident<'ast>,
+        _type_args: &mut Vec<Ident>,
+        args: &mut Vec<(Ident, ast::Type<'ast>)>,
+        _body: &mut Box<Expr<ast::Type<'ast>>>,
+        _type_: &mut ast::Type<'ast>,
+    ) -> Result<(), Self::Error> {
+        args.retain(|(_, ty)| ty != &ast::Type::Unit);
+        Ok(())
+    }
+}
+
+pub(crate) fn run_toplevel<'a>(toplevel: &mut Vec<Decl<'a, ast::Type<'a>>>) {
+    let mut pass = StripPositiveUnits {};
+    for decl in toplevel.iter_mut() {
+        pass.visit_decl(decl).void_unwrap();
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::parser::toplevel;
+    use crate::tc::typecheck_toplevel;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn unit_only_arg() {
+        let (_, program) = toplevel(
+            "ty f : fn () -> int
+             fn f _ = 1
+
+             ty main : fn -> int
+             fn main = f ()",
+        )
+        .unwrap();
+
+        let (_, expected) = toplevel(
+            "ty f : fn -> int
+             fn f = 1
+
+             ty main : fn -> int
+             fn main = f()",
+        )
+        .unwrap();
+        let expected = typecheck_toplevel(expected).unwrap();
+
+        let mut program = typecheck_toplevel(program).unwrap();
+        run_toplevel(&mut program);
+
+        assert_eq!(program, expected);
+    }
+
+    #[test]
+    fn unit_and_other_arg() {
+        let (_, program) = toplevel(
+            "ty f : fn (), int -> int
+             fn f _ x = x
+
+             ty main : fn -> int
+             fn main = f () 1",
+        )
+        .unwrap();
+
+        let (_, expected) = toplevel(
+            "ty f : fn int -> int
+             fn f x = x
+
+             ty main : fn -> int
+             fn main = f 1",
+        )
+        .unwrap();
+        let expected = typecheck_toplevel(expected).unwrap();
+
+        let mut program = typecheck_toplevel(program).unwrap();
+        run_toplevel(&mut program);
+
+        assert_eq!(program, expected);
+    }
+
+    #[test]
+    fn unit_expr_and_other_arg() {
+        let (_, program) = toplevel(
+            "ty f : fn (), int -> int
+             fn f _ x = x
+
+             ty g : fn int -> ()
+             fn g _ = ()
+
+             ty main : fn -> int
+             fn main = f (g 2) 1",
+        )
+        .unwrap();
+
+        let (_, expected) = toplevel(
+            "ty f : fn int -> int
+             fn f x = x
+
+             ty g : fn int -> ()
+             fn g _ = ()
+
+             ty main : fn -> int
+             fn main = let ___discarded = g 2 in f 1",
+        )
+        .unwrap();
+        assert_eq!(expected.len(), 6);
+        let expected = typecheck_toplevel(expected).unwrap();
+
+        let mut program = typecheck_toplevel(program).unwrap();
+        run_toplevel(&mut program);
+
+        assert_eq!(program, expected);
+    }
+}
diff --git a/users/grfn/achilles/src/passes/mod.rs b/users/grfn/achilles/src/passes/mod.rs
new file mode 100644
index 0000000000..306869bef1
--- /dev/null
+++ b/users/grfn/achilles/src/passes/mod.rs
@@ -0,0 +1 @@
+pub(crate) mod hir;
diff --git a/users/grfn/achilles/src/tc/mod.rs b/users/grfn/achilles/src/tc/mod.rs
new file mode 100644
index 0000000000..5825bab1fb
--- /dev/null
+++ b/users/grfn/achilles/src/tc/mod.rs
@@ -0,0 +1,808 @@
+use bimap::BiMap;
+use derive_more::From;
+use itertools::Itertools;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::convert::{TryFrom, TryInto};
+use std::fmt::{self, Display};
+use std::{mem, result};
+use thiserror::Error;
+
+use crate::ast::{self, hir, Arg, BinaryOperator, Ident, Literal, Pattern};
+use crate::common::env::Env;
+use crate::common::{Namer, NamerOf};
+
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("Undefined variable {0}")]
+    UndefinedVariable(Ident<'static>),
+
+    #[error("Mismatched types: expected {expected}, but got {actual}")]
+    TypeMismatch { expected: Type, actual: Type },
+
+    #[error("Mismatched types, expected numeric type, but got {0}")]
+    NonNumeric(Type),
+
+    #[error("Ambiguous type {0}")]
+    AmbiguousType(TyVar),
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+pub struct TyVar(u64);
+
+impl Display for TyVar {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "t{}", self.0)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub struct NullaryType(String);
+
+impl Display for NullaryType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(&self.0)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum PrimType {
+    Int,
+    Float,
+    Bool,
+    CString,
+}
+
+impl<'a> From<PrimType> for ast::Type<'a> {
+    fn from(pr: PrimType) -> Self {
+        match pr {
+            PrimType::Int => ast::Type::Int,
+            PrimType::Float => ast::Type::Float,
+            PrimType::Bool => ast::Type::Bool,
+            PrimType::CString => ast::Type::CString,
+        }
+    }
+}
+
+impl Display for PrimType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            PrimType::Int => f.write_str("int"),
+            PrimType::Float => f.write_str("float"),
+            PrimType::Bool => f.write_str("bool"),
+            PrimType::CString => f.write_str("cstring"),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, From)]
+pub enum Type {
+    #[from(ignore)]
+    Univ(TyVar),
+    #[from(ignore)]
+    Exist(TyVar),
+    Nullary(NullaryType),
+    Prim(PrimType),
+    Tuple(Vec<Type>),
+    Unit,
+    Fun {
+        args: Vec<Type>,
+        ret: Box<Type>,
+    },
+}
+
+impl<'a> TryFrom<Type> for ast::Type<'a> {
+    type Error = Type;
+
+    fn try_from(value: Type) -> result::Result<Self, Self::Error> {
+        match value {
+            Type::Unit => Ok(ast::Type::Unit),
+            Type::Univ(_) => todo!(),
+            Type::Exist(_) => Err(value),
+            Type::Nullary(_) => todo!(),
+            Type::Prim(p) => Ok(p.into()),
+            Type::Tuple(members) => Ok(ast::Type::Tuple(
+                members.into_iter().map(|ty| ty.try_into()).try_collect()?,
+            )),
+            Type::Fun { ref args, ref ret } => Ok(ast::Type::Function(ast::FunctionType {
+                args: args
+                    .clone()
+                    .into_iter()
+                    .map(Self::try_from)
+                    .try_collect()
+                    .map_err(|_| value.clone())?,
+                ret: Box::new((*ret.clone()).try_into().map_err(|_| value.clone())?),
+            })),
+        }
+    }
+}
+
+const INT: Type = Type::Prim(PrimType::Int);
+const FLOAT: Type = Type::Prim(PrimType::Float);
+const BOOL: Type = Type::Prim(PrimType::Bool);
+const CSTRING: Type = Type::Prim(PrimType::CString);
+
+impl Display for Type {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Type::Nullary(nt) => nt.fmt(f),
+            Type::Prim(p) => p.fmt(f),
+            Type::Univ(TyVar(n)) => write!(f, "∀{}", n),
+            Type::Exist(TyVar(n)) => write!(f, "∃{}", n),
+            Type::Fun { args, ret } => write!(f, "fn {} -> {}", args.iter().join(", "), ret),
+            Type::Tuple(members) => write!(f, "({})", members.iter().join(", ")),
+            Type::Unit => write!(f, "()"),
+        }
+    }
+}
+
+struct Typechecker<'ast> {
+    ty_var_namer: NamerOf<TyVar>,
+    ctx: HashMap<TyVar, Type>,
+    env: Env<Ident<'ast>, Type>,
+
+    /// AST type var -> type
+    instantiations: Env<Ident<'ast>, Type>,
+
+    /// AST type-var -> universal TyVar
+    type_vars: RefCell<(BiMap<Ident<'ast>, TyVar>, NamerOf<Ident<'static>>)>,
+}
+
+impl<'ast> Typechecker<'ast> {
+    fn new() -> Self {
+        Self {
+            ty_var_namer: Namer::new(TyVar).boxed(),
+            type_vars: RefCell::new((
+                Default::default(),
+                Namer::alphabetic().map(|n| Ident::try_from(n).unwrap()),
+            )),
+            ctx: Default::default(),
+            env: Default::default(),
+            instantiations: Default::default(),
+        }
+    }
+
+    fn bind_pattern(
+        &mut self,
+        pat: Pattern<'ast>,
+        type_: Type,
+    ) -> Result<hir::Pattern<'ast, Type>> {
+        match pat {
+            Pattern::Id(ident) => {
+                self.env.set(ident.clone(), type_.clone());
+                Ok(hir::Pattern::Id(ident, type_))
+            }
+            Pattern::Tuple(members) => {
+                let mut tys = Vec::with_capacity(members.len());
+                let mut hir_members = Vec::with_capacity(members.len());
+                for pat in members {
+                    let ty = self.fresh_ex();
+                    hir_members.push(self.bind_pattern(pat, ty.clone())?);
+                    tys.push(ty);
+                }
+                let tuple_type = Type::Tuple(tys);
+                self.unify(&tuple_type, &type_)?;
+                Ok(hir::Pattern::Tuple(hir_members))
+            }
+        }
+    }
+
+    pub(crate) fn tc_expr(&mut self, expr: ast::Expr<'ast>) -> Result<hir::Expr<'ast, Type>> {
+        match expr {
+            ast::Expr::Ident(ident) => {
+                let type_ = self
+                    .env
+                    .resolve(&ident)
+                    .ok_or_else(|| Error::UndefinedVariable(ident.to_owned()))?
+                    .clone();
+                Ok(hir::Expr::Ident(ident, type_))
+            }
+            ast::Expr::Literal(lit) => {
+                let type_ = match lit {
+                    Literal::Int(_) => Type::Prim(PrimType::Int),
+                    Literal::Bool(_) => Type::Prim(PrimType::Bool),
+                    Literal::String(_) => Type::Prim(PrimType::CString),
+                    Literal::Unit => Type::Unit,
+                };
+                Ok(hir::Expr::Literal(lit.to_owned(), type_))
+            }
+            ast::Expr::Tuple(members) => {
+                let members = members
+                    .into_iter()
+                    .map(|expr| self.tc_expr(expr))
+                    .collect::<Result<Vec<_>>>()?;
+                let type_ = Type::Tuple(members.iter().map(|expr| expr.type_().clone()).collect());
+                Ok(hir::Expr::Tuple(members, type_))
+            }
+            ast::Expr::UnaryOp { op, rhs } => todo!(),
+            ast::Expr::BinaryOp { lhs, op, rhs } => {
+                let lhs = self.tc_expr(*lhs)?;
+                let rhs = self.tc_expr(*rhs)?;
+                let type_ = match op {
+                    BinaryOperator::Equ | BinaryOperator::Neq => {
+                        self.unify(lhs.type_(), rhs.type_())?;
+                        Type::Prim(PrimType::Bool)
+                    }
+                    BinaryOperator::Add | BinaryOperator::Sub | BinaryOperator::Mul => {
+                        let ty = self.unify(lhs.type_(), rhs.type_())?;
+                        // if !matches!(ty, Type::Int | Type::Float) {
+                        //     return Err(Error::NonNumeric(ty));
+                        // }
+                        ty
+                    }
+                    BinaryOperator::Div => todo!(),
+                    BinaryOperator::Pow => todo!(),
+                };
+                Ok(hir::Expr::BinaryOp {
+                    lhs: Box::new(lhs),
+                    op,
+                    rhs: Box::new(rhs),
+                    type_,
+                })
+            }
+            ast::Expr::Let { bindings, body } => {
+                self.env.push();
+                let bindings = bindings
+                    .into_iter()
+                    .map(
+                        |ast::Binding { pat, type_, body }| -> Result<hir::Binding<Type>> {
+                            let body = self.tc_expr(body)?;
+                            if let Some(type_) = type_ {
+                                let type_ = self.type_from_ast_type(type_);
+                                self.unify(body.type_(), &type_)?;
+                            }
+                            let pat = self.bind_pattern(pat, body.type_().clone())?;
+                            Ok(hir::Binding { pat, body })
+                        },
+                    )
+                    .collect::<Result<Vec<hir::Binding<Type>>>>()?;
+                let body = self.tc_expr(*body)?;
+                self.env.pop();
+                Ok(hir::Expr::Let {
+                    bindings,
+                    type_: body.type_().clone(),
+                    body: Box::new(body),
+                })
+            }
+            ast::Expr::If {
+                condition,
+                then,
+                else_,
+            } => {
+                let condition = self.tc_expr(*condition)?;
+                self.unify(&Type::Prim(PrimType::Bool), condition.type_())?;
+                let then = self.tc_expr(*then)?;
+                let else_ = self.tc_expr(*else_)?;
+                let type_ = self.unify(then.type_(), else_.type_())?;
+                Ok(hir::Expr::If {
+                    condition: Box::new(condition),
+                    then: Box::new(then),
+                    else_: Box::new(else_),
+                    type_,
+                })
+            }
+            ast::Expr::Fun(f) => {
+                let ast::Fun { args, body } = *f;
+                self.env.push();
+                let args: Vec<_> = args
+                    .into_iter()
+                    .map(|Arg { ident, type_ }| {
+                        let ty = match type_ {
+                            Some(t) => self.type_from_ast_type(t),
+                            None => self.fresh_ex(),
+                        };
+                        self.env.set(ident.clone(), ty.clone());
+                        (ident, ty)
+                    })
+                    .collect();
+                let body = self.tc_expr(body)?;
+                self.env.pop();
+                Ok(hir::Expr::Fun {
+                    type_: Type::Fun {
+                        args: args.iter().map(|(_, ty)| ty.clone()).collect(),
+                        ret: Box::new(body.type_().clone()),
+                    },
+                    type_args: vec![], // TODO fill in once we do let generalization
+                    args,
+                    body: Box::new(body),
+                })
+            }
+            ast::Expr::Call { fun, args } => {
+                let ret_ty = self.fresh_ex();
+                let arg_tys = args.iter().map(|_| self.fresh_ex()).collect::<Vec<_>>();
+                let ft = Type::Fun {
+                    args: arg_tys.clone(),
+                    ret: Box::new(ret_ty.clone()),
+                };
+                let fun = self.tc_expr(*fun)?;
+                self.instantiations.push();
+                self.unify(&ft, fun.type_())?;
+                let args = args
+                    .into_iter()
+                    .zip(arg_tys)
+                    .map(|(arg, ty)| {
+                        let arg = self.tc_expr(arg)?;
+                        self.unify(&ty, arg.type_())?;
+                        Ok(arg)
+                    })
+                    .try_collect()?;
+                let type_args = self.commit_instantiations();
+                Ok(hir::Expr::Call {
+                    fun: Box::new(fun),
+                    type_args,
+                    args,
+                    type_: ret_ty,
+                })
+            }
+            ast::Expr::Ascription { expr, type_ } => {
+                let expr = self.tc_expr(*expr)?;
+                let type_ = self.type_from_ast_type(type_);
+                self.unify(expr.type_(), &type_)?;
+                Ok(expr)
+            }
+        }
+    }
+
+    pub(crate) fn tc_decl(
+        &mut self,
+        decl: ast::Decl<'ast>,
+    ) -> Result<Option<hir::Decl<'ast, Type>>> {
+        match decl {
+            ast::Decl::Fun { name, body } => {
+                let mut expr = ast::Expr::Fun(Box::new(body));
+                if let Some(type_) = self.env.resolve(&name) {
+                    expr = ast::Expr::Ascription {
+                        expr: Box::new(expr),
+                        type_: self.finalize_type(type_.clone())?,
+                    };
+                }
+
+                self.env.push();
+                let body = self.tc_expr(expr)?;
+                let type_ = body.type_().clone();
+                self.env.set(name.clone(), type_);
+                self.env.pop();
+                match body {
+                    hir::Expr::Fun {
+                        type_args,
+                        args,
+                        body,
+                        type_,
+                    } => Ok(Some(hir::Decl::Fun {
+                        name,
+                        type_args,
+                        args,
+                        body,
+                        type_,
+                    })),
+                    _ => unreachable!(),
+                }
+            }
+            ast::Decl::Ascription { name, type_ } => {
+                let type_ = self.type_from_ast_type(type_);
+                self.env.set(name.clone(), type_);
+                Ok(None)
+            }
+            ast::Decl::Extern { name, type_ } => {
+                let type_ = self.type_from_ast_type(ast::Type::Function(type_));
+                self.env.set(name.clone(), type_.clone());
+                let (arg_types, ret_type) = match type_ {
+                    Type::Fun { args, ret } => (args, *ret),
+                    _ => unreachable!(),
+                };
+                Ok(Some(hir::Decl::Extern {
+                    name,
+                    arg_types,
+                    ret_type,
+                }))
+            }
+        }
+    }
+
+    fn fresh_tv(&mut self) -> TyVar {
+        self.ty_var_namer.make_name()
+    }
+
+    fn fresh_ex(&mut self) -> Type {
+        Type::Exist(self.fresh_tv())
+    }
+
+    fn fresh_univ(&mut self) -> Type {
+        Type::Univ(self.fresh_tv())
+    }
+
+    fn unify(&mut self, ty1: &Type, ty2: &Type) -> Result<Type> {
+        match (ty1, ty2) {
+            (Type::Unit, Type::Unit) => Ok(Type::Unit),
+            (Type::Exist(tv), ty) | (ty, Type::Exist(tv)) => match self.resolve_tv(*tv)? {
+                Some(existing_ty) if self.types_match(ty, &existing_ty) => Ok(ty.clone()),
+                Some(var @ ast::Type::Var(_)) => {
+                    let var = self.type_from_ast_type(var);
+                    self.unify(&var, ty)
+                }
+                Some(existing_ty) => match ty {
+                    Type::Exist(_) => {
+                        let rhs = self.type_from_ast_type(existing_ty);
+                        self.unify(ty, &rhs)
+                    }
+                    _ => Err(Error::TypeMismatch {
+                        expected: ty.clone(),
+                        actual: self.type_from_ast_type(existing_ty),
+                    }),
+                },
+                None => match self.ctx.insert(*tv, ty.clone()) {
+                    Some(existing) => self.unify(&existing, ty),
+                    None => Ok(ty.clone()),
+                },
+            },
+            (Type::Univ(u1), Type::Univ(u2)) if u1 == u2 => Ok(ty2.clone()),
+            (Type::Univ(u), ty) | (ty, Type::Univ(u)) => {
+                let ident = self.name_univ(*u);
+                match self.instantiations.resolve(&ident) {
+                    Some(existing_ty) if ty == existing_ty => Ok(ty.clone()),
+                    Some(existing_ty) => Err(Error::TypeMismatch {
+                        expected: ty.clone(),
+                        actual: existing_ty.clone(),
+                    }),
+                    None => {
+                        self.instantiations.set(ident, ty.clone());
+                        Ok(ty.clone())
+                    }
+                }
+            }
+            (Type::Prim(p1), Type::Prim(p2)) if p1 == p2 => Ok(ty2.clone()),
+            (Type::Tuple(t1), Type::Tuple(t2)) if t1.len() == t2.len() => {
+                let ts = t1
+                    .iter()
+                    .zip(t2.iter())
+                    .map(|(t1, t2)| self.unify(t1, t2))
+                    .try_collect()?;
+                Ok(Type::Tuple(ts))
+            }
+            (
+                Type::Fun {
+                    args: args1,
+                    ret: ret1,
+                },
+                Type::Fun {
+                    args: args2,
+                    ret: ret2,
+                },
+            ) => {
+                let args = args1
+                    .iter()
+                    .zip(args2)
+                    .map(|(t1, t2)| self.unify(t1, t2))
+                    .try_collect()?;
+                let ret = self.unify(ret1, ret2)?;
+                Ok(Type::Fun {
+                    args,
+                    ret: Box::new(ret),
+                })
+            }
+            (Type::Nullary(_), _) | (_, Type::Nullary(_)) => todo!(),
+            _ => Err(Error::TypeMismatch {
+                expected: ty1.clone(),
+                actual: ty2.clone(),
+            }),
+        }
+    }
+
+    fn finalize_expr(
+        &self,
+        expr: hir::Expr<'ast, Type>,
+    ) -> Result<hir::Expr<'ast, ast::Type<'ast>>> {
+        expr.traverse_type(|ty| self.finalize_type(ty))
+    }
+
+    fn finalize_decl(
+        &mut self,
+        decl: hir::Decl<'ast, Type>,
+    ) -> Result<hir::Decl<'ast, ast::Type<'ast>>> {
+        let res = decl.traverse_type(|ty| self.finalize_type(ty))?;
+        if let Some(type_) = res.type_() {
+            let ty = self.type_from_ast_type(type_.clone());
+            self.env.set(res.name().clone(), ty);
+        }
+        Ok(res)
+    }
+
+    fn finalize_type(&self, ty: Type) -> Result<ast::Type<'static>> {
+        let ret = match ty {
+            Type::Exist(tv) => self.resolve_tv(tv)?.ok_or(Error::AmbiguousType(tv)),
+            Type::Univ(tv) => Ok(ast::Type::Var(self.name_univ(tv))),
+            Type::Unit => Ok(ast::Type::Unit),
+            Type::Nullary(_) => todo!(),
+            Type::Prim(pr) => Ok(pr.into()),
+            Type::Tuple(members) => Ok(ast::Type::Tuple(
+                members
+                    .into_iter()
+                    .map(|ty| self.finalize_type(ty))
+                    .try_collect()?,
+            )),
+            Type::Fun { args, ret } => Ok(ast::Type::Function(ast::FunctionType {
+                args: args
+                    .into_iter()
+                    .map(|ty| self.finalize_type(ty))
+                    .try_collect()?,
+                ret: Box::new(self.finalize_type(*ret)?),
+            })),
+        };
+        ret
+    }
+
+    fn resolve_tv(&self, tv: TyVar) -> Result<Option<ast::Type<'static>>> {
+        let mut res = &Type::Exist(tv);
+        Ok(loop {
+            match res {
+                Type::Exist(tv) => {
+                    res = match self.ctx.get(tv) {
+                        Some(r) => r,
+                        None => return Ok(None),
+                    };
+                }
+                Type::Univ(tv) => {
+                    let ident = self.name_univ(*tv);
+                    if let Some(r) = self.instantiations.resolve(&ident) {
+                        res = r;
+                    } else {
+                        break Some(ast::Type::Var(ident));
+                    }
+                }
+                Type::Nullary(_) => todo!(),
+                Type::Prim(pr) => break Some((*pr).into()),
+                Type::Unit => break Some(ast::Type::Unit),
+                Type::Fun { args, ret } => todo!(),
+                Type::Tuple(_) => break Some(self.finalize_type(res.clone())?),
+            }
+        })
+    }
+
+    fn type_from_ast_type(&mut self, ast_type: ast::Type<'ast>) -> Type {
+        match ast_type {
+            ast::Type::Unit => Type::Unit,
+            ast::Type::Int => INT,
+            ast::Type::Float => FLOAT,
+            ast::Type::Bool => BOOL,
+            ast::Type::CString => CSTRING,
+            ast::Type::Tuple(members) => Type::Tuple(
+                members
+                    .into_iter()
+                    .map(|ty| self.type_from_ast_type(ty))
+                    .collect(),
+            ),
+            ast::Type::Function(ast::FunctionType { args, ret }) => Type::Fun {
+                args: args
+                    .into_iter()
+                    .map(|t| self.type_from_ast_type(t))
+                    .collect(),
+                ret: Box::new(self.type_from_ast_type(*ret)),
+            },
+            ast::Type::Var(id) => Type::Univ({
+                let opt_tv = { self.type_vars.borrow_mut().0.get_by_left(&id).copied() };
+                opt_tv.unwrap_or_else(|| {
+                    let tv = self.fresh_tv();
+                    self.type_vars
+                        .borrow_mut()
+                        .0
+                        .insert_no_overwrite(id, tv)
+                        .unwrap();
+                    tv
+                })
+            }),
+        }
+    }
+
+    fn name_univ(&self, tv: TyVar) -> Ident<'static> {
+        let mut vars = self.type_vars.borrow_mut();
+        vars.0
+            .get_by_right(&tv)
+            .map(Ident::to_owned)
+            .unwrap_or_else(|| {
+                let name = loop {
+                    let name = vars.1.make_name();
+                    if !vars.0.contains_left(&name) {
+                        break name;
+                    }
+                };
+                vars.0.insert_no_overwrite(name.clone(), tv).unwrap();
+                name
+            })
+    }
+
+    fn commit_instantiations(&mut self) -> HashMap<Ident<'ast>, Type> {
+        let mut res = HashMap::new();
+        let mut ctx = mem::take(&mut self.ctx);
+        for (_, v) in ctx.iter_mut() {
+            if let Type::Univ(tv) = v {
+                let tv_name = self.name_univ(*tv);
+                if let Some(concrete) = self.instantiations.resolve(&tv_name) {
+                    res.insert(tv_name, concrete.clone());
+                    *v = concrete.clone();
+                }
+            }
+        }
+        self.ctx = ctx;
+        self.instantiations.pop();
+        res
+    }
+
+    fn types_match(&self, type_: &Type, ast_type: &ast::Type<'ast>) -> bool {
+        match (type_, ast_type) {
+            (Type::Univ(u), ast::Type::Var(v)) => {
+                Some(u) == self.type_vars.borrow().0.get_by_left(v)
+            }
+            (Type::Univ(_), _) => false,
+            (Type::Exist(_), _) => false,
+            (Type::Unit, ast::Type::Unit) => true,
+            (Type::Unit, _) => false,
+            (Type::Nullary(_), _) => todo!(),
+            (Type::Prim(pr), ty) => ast::Type::from(*pr) == *ty,
+            (Type::Tuple(members), ast::Type::Tuple(members2)) => members
+                .iter()
+                .zip(members2.iter())
+                .all(|(t1, t2)| self.types_match(t1, t2)),
+            (Type::Tuple(members), _) => false,
+            (Type::Fun { args, ret }, ast::Type::Function(ft)) => {
+                args.len() == ft.args.len()
+                    && args
+                        .iter()
+                        .zip(&ft.args)
+                        .all(|(a1, a2)| self.types_match(a1, &a2))
+                    && self.types_match(&*ret, &*ft.ret)
+            }
+            (Type::Fun { .. }, _) => false,
+        }
+    }
+}
+
+pub fn typecheck_expr(expr: ast::Expr) -> Result<hir::Expr<ast::Type>> {
+    let mut typechecker = Typechecker::new();
+    let typechecked = typechecker.tc_expr(expr)?;
+    typechecker.finalize_expr(typechecked)
+}
+
+pub fn typecheck_toplevel(decls: Vec<ast::Decl>) -> Result<Vec<hir::Decl<ast::Type>>> {
+    let mut typechecker = Typechecker::new();
+    let mut res = Vec::with_capacity(decls.len());
+    for decl in decls {
+        if let Some(hir_decl) = typechecker.tc_decl(decl)? {
+            let hir_decl = typechecker.finalize_decl(hir_decl)?;
+            res.push(hir_decl);
+        }
+        typechecker.ctx.clear();
+    }
+    Ok(res)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    macro_rules! assert_type {
+        ($expr: expr, $type: expr) => {
+            use crate::parser::{expr, type_};
+            let parsed_expr = test_parse!(expr, $expr);
+            let parsed_type = test_parse!(type_, $type);
+            let res = typecheck_expr(parsed_expr).unwrap_or_else(|e| panic!("{}", e));
+            assert!(
+                res.type_().alpha_equiv(&parsed_type),
+                "{} inferred type {}, but expected {}",
+                $expr,
+                res.type_(),
+                $type
+            );
+        };
+
+        (toplevel($program: expr), $($decl: ident => $type: expr),+ $(,)?) => {{
+            use crate::parser::{toplevel, type_};
+            let program = test_parse!(toplevel, $program);
+            let res = typecheck_toplevel(program).unwrap_or_else(|e| panic!("{}", e));
+            $(
+            let parsed_type = test_parse!(type_, $type);
+            let ident = Ident::try_from(::std::stringify!($decl)).unwrap();
+            let decl = res.iter().find(|decl| {
+                matches!(decl, crate::ast::hir::Decl::Fun { name, .. } if name == &ident)
+            }).unwrap_or_else(|| panic!("Could not find declaration for {}", ident));
+            assert!(
+                decl.type_().unwrap().alpha_equiv(&parsed_type),
+                "inferred type {} for {}, but expected {}",
+                decl.type_().unwrap(),
+                ident,
+                $type
+            );
+            )+
+        }};
+    }
+
+    macro_rules! assert_type_error {
+        ($expr: expr) => {
+            use crate::parser::expr;
+            let parsed_expr = test_parse!(expr, $expr);
+            let res = typecheck_expr(parsed_expr);
+            assert!(
+                res.is_err(),
+                "Expected type error, but got type: {}",
+                res.unwrap().type_()
+            );
+        };
+    }
+
+    #[test]
+    fn literal_int() {
+        assert_type!("1", "int");
+    }
+
+    #[test]
+    fn conditional() {
+        assert_type!("if 1 == 2 then 3 else 4", "int");
+    }
+
+    #[test]
+    #[ignore]
+    fn add_bools() {
+        assert_type_error!("true + false");
+    }
+
+    #[test]
+    fn call_generic_function() {
+        assert_type!("(fn x = x) 1", "int");
+    }
+
+    #[test]
+    fn call_let_bound_generic() {
+        assert_type!("let id = fn x = x in id 1", "int");
+    }
+
+    #[test]
+    fn universal_ascripted_let() {
+        assert_type!("let id: fn a -> a = fn x = x in id 1", "int");
+    }
+
+    #[test]
+    fn call_generic_function_toplevel() {
+        assert_type!(
+            toplevel(
+                "ty id : fn a -> a
+                 fn id x = x
+
+                 fn main = id 0"
+            ),
+            main => "fn -> int",
+            id => "fn a -> a",
+        );
+    }
+
+    #[test]
+    #[ignore]
+    fn let_generalization() {
+        assert_type!("let id = fn x = x in if id true then id 1 else 2", "int");
+    }
+
+    #[test]
+    fn concrete_function() {
+        assert_type!("fn x = x + 1", "fn int -> int");
+    }
+
+    #[test]
+    fn arg_ascriptions() {
+        assert_type!("fn (x: int) = x", "fn int -> int");
+    }
+
+    #[test]
+    fn call_concrete_function() {
+        assert_type!("(fn x = x + 1) 2", "int");
+    }
+
+    #[test]
+    fn conditional_non_bool() {
+        assert_type_error!("if 3 then true else false");
+    }
+
+    #[test]
+    fn let_int() {
+        assert_type!("let x = 1 in x", "int");
+    }
+}
diff --git a/users/grfn/achilles/tests/compile.rs b/users/grfn/achilles/tests/compile.rs
new file mode 100644
index 0000000000..0f1086bfd8
--- /dev/null
+++ b/users/grfn/achilles/tests/compile.rs
@@ -0,0 +1,79 @@
+use std::process::Command;
+
+use crate_root::root;
+
+struct Fixture {
+    name: &'static str,
+    exit_code: i32,
+    expected_output: &'static str,
+}
+
+const FIXTURES: &[Fixture] = &[
+    Fixture {
+        name: "simple",
+        exit_code: 5,
+        expected_output: "",
+    },
+    Fixture {
+        name: "functions",
+        exit_code: 9,
+        expected_output: "",
+    },
+    Fixture {
+        name: "externs",
+        exit_code: 0,
+        expected_output: "foobar\n",
+    },
+    Fixture {
+        name: "units",
+        exit_code: 0,
+        expected_output: "hi\n",
+    },
+];
+
+#[test]
+fn compile_and_run_files() {
+    let ach = root().unwrap().join("ach");
+
+    println!("Running: `make clean`");
+    assert!(
+        Command::new("make")
+            .arg("clean")
+            .current_dir(&ach)
+            .spawn()
+            .unwrap()
+            .wait()
+            .unwrap()
+            .success(),
+        "make clean failed"
+    );
+
+    for Fixture {
+        name,
+        exit_code,
+        expected_output,
+    } in FIXTURES
+    {
+        println!(">>> Testing: {}", name);
+
+        println!("    Running: `make {}`", name);
+        assert!(
+            Command::new("make")
+                .arg(name)
+                .current_dir(&ach)
+                .spawn()
+                .unwrap()
+                .wait()
+                .unwrap()
+                .success(),
+            "make failed"
+        );
+
+        let out_path = ach.join(name);
+        println!("    Running: `{}`", out_path.to_str().unwrap());
+        let output = Command::new(out_path).output().unwrap();
+        assert_eq!(output.status.code().unwrap(), *exit_code,);
+        assert_eq!(output.stdout, expected_output.as_bytes());
+        println!("    OK");
+    }
+}
diff --git a/users/grfn/bbbg/.clj-kondo/config.edn b/users/grfn/bbbg/.clj-kondo/config.edn
new file mode 100644
index 0000000000..8faddb77ec
--- /dev/null
+++ b/users/grfn/bbbg/.clj-kondo/config.edn
@@ -0,0 +1 @@
+{:lint-as {garden.def/defstyles clojure.core/def}}
diff --git a/users/grfn/bbbg/.envrc b/users/grfn/bbbg/.envrc
new file mode 100644
index 0000000000..051d09d292
--- /dev/null
+++ b/users/grfn/bbbg/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
diff --git a/users/grfn/bbbg/.gitignore b/users/grfn/bbbg/.gitignore
new file mode 100644
index 0000000000..99dbfc4436
--- /dev/null
+++ b/users/grfn/bbbg/.gitignore
@@ -0,0 +1,9 @@
+/target
+/classes
+*.jar
+*.class
+/.nrepl-port
+/.cpcache
+/.clojure
+/result
+/.clj-kondo/.cache
diff --git a/users/grfn/bbbg/Makefile b/users/grfn/bbbg/Makefile
new file mode 100644
index 0000000000..fc45477984
--- /dev/null
+++ b/users/grfn/bbbg/Makefile
@@ -0,0 +1,2 @@
+deps.nix: deps.edn
+	clj2nix ./deps.edn ./deps.nix '-A:uberjar' '-A:clj-test'
diff --git a/users/grfn/bbbg/README.md b/users/grfn/bbbg/README.md
new file mode 100644
index 0000000000..a7181333b9
--- /dev/null
+++ b/users/grfn/bbbg/README.md
@@ -0,0 +1,129 @@
+# Brooklyn-Based Board Gaming signup sheet
+
+This directory contains a small web application that acts as a signup
+sheet and attendee tracking system for [my local board gaming
+meetup](https://www.meetup.com/brooklyn-based-board-gaming/).
+
+## Development
+
+### Installing dependencies
+
+#### With Nix + Docker ("blessed way")
+
+Prerequisites:
+
+-   [Nix](https://nixos.org/)
+-   [lorri](https://github.com/nix-community/lorri)
+-   [Docker](https://www.docker.com/)
+
+From this directory in a full checkout of depot, run the following
+commands to install all development dependencies:
+
+``` shell-session
+$ pwd
+/path/to/depot/users/grfn/bbbg
+$ direnv allow
+$ lorri watch --once # Wait for a single nix shell build
+```
+
+Then, to run a docker container with the development database:
+
+``` shell-session
+$ pwd
+/path/to/depot/users/grfn/bbbg
+$ arion up -d
+```
+
+#### Choose-your-own-adventure
+
+Note that the **authoritative** source for dev dependencies is the `shell.nix`
+file in this directory - those may diverge from what's written here; if so
+follow those versions rather than these.
+
+-   Install the [clojure command-line
+    tools](https://clojure.org/guides/getting_started), with openjdk 11
+-   Install and run a postgresql 12 database, with:
+    -   A user with superuser priveleges, the username `bbbg` and the
+        password `password`
+    -   A database called `bbbg` owned by that user.
+-   Export the following environment variables in a context visible by
+    whatever method you use to run the application:
+    -   `PGHOST=localhost`
+    -   `PGUSER=bbbg`
+    -   `PGDATABASE=bbbg`
+    -   `PGPASSWORD=bbbg`
+
+### Running the application
+
+Before running the app, you'll need an oauth2 client-id and client secret for a
+Discord app. The application can either load those from a
+[pass](https://www.passwordstore.org/) password store, or read them from
+plaintext files in a directory. In either case, they should be accessible at the
+paths `bbbg/discord-client-id` and `bbbg/discord-client-secret` respectively.
+
+#### From the command line
+
+``` shell-session
+$ clj -A:dev
+Clojure 1.11.0-alpha3
+user=> (require 'bbbg.core)
+nil
+user=> ;; Optionally, if you're using a directory with plaintext files for the discord client ID and client secret:
+user=> (bbbg.util.dev-secrets/set-backend! [:dir "/path/to/that/directory"])
+user=> (bbbg.core/run-dev)
+##<SystemMap>
+user=> (bbbg.db/migrate! (:db bbbg.core/system))
+11:57:26.536 [main] INFO  migratus.core - Starting migrations {  }
+11:57:26.538 [main] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... {  }
+11:57:26.883 [main] INFO  com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.impossibl.postgres.jdbc.PGDirectConnection@3cae770e {  }
+11:57:26.884 [main] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. {  }
+11:57:26.923 [main] INFO  migratus.core - Ending migrations {  }
+nil
+```
+
+This will run a web server for the application listening at
+<http://localhost:8888>
+
+#### In Emacs, with [CIDER](https://docs.cider.mx/cider/index.html) + [direnv](https://github.com/wbolster/emacs-direnv)
+
+Open `//users/grfn/bbbg/src/bbbg/core.clj` in a buffer, then follow the
+instructions at the end of the file
+
+## Deployment
+
+### With nix+terraform
+
+Deployment configuration is located in the `tf.nix` file, which is
+currently tightly coupled to my own infrastructure and AWS account but
+could hypothetically be adjusted to be general-purpose.
+
+To deploy a new version of the application, after following "installing
+dependencies" above, run the following command in a context with ec2
+credentials available:
+
+``` shell-session
+$ terraform apply
+```
+
+The current deploy configuration includes:
+
+-   An ec2 instance running nixos, with a postgresql database and the
+    bbbg application running as a service, behind nginx with an
+    auto-renewing letsencrypt cert
+-   The DNS A record for `bbbg.gws.fyi` pointing at that ec2 instance,
+    in the cloudflare zone for `gws.fyi`
+
+### Otherwise
+
+¯\\\_(ツ)_/¯
+
+You'll need:
+
+-   An uberjar for bbbg; the canonical way of building that is `nix-build
+    /path/to/depot -A users.grfn.bbbg.server-jar` but I\'m not sure how that
+    works outside of nix
+-   A postgresql database
+-   Environment variables telling the app how to connect to that
+    database. See `config.systemd.services.bbbg-server.environment` in
+    `module.nix` for which env vars are currently being exported by the
+    NixOS module that runs the production version of the app
diff --git a/users/grfn/bbbg/arion-compose.nix b/users/grfn/bbbg/arion-compose.nix
new file mode 100644
index 0000000000..c8a6dd156d
--- /dev/null
+++ b/users/grfn/bbbg/arion-compose.nix
@@ -0,0 +1,15 @@
+{ ... }:
+
+{
+  services = {
+    postgres.service = {
+      image = "postgres:12";
+      environment = {
+        POSTGRES_DB = "bbbg";
+        POSTGRES_USER = "bbbg";
+        POSTGRES_PASSWORD = "password";
+      };
+      ports = [ "5432:5432" ];
+    };
+  };
+}
diff --git a/users/grfn/bbbg/arion-pkgs.nix b/users/grfn/bbbg/arion-pkgs.nix
new file mode 100644
index 0000000000..c6d603be2a
--- /dev/null
+++ b/users/grfn/bbbg/arion-pkgs.nix
@@ -0,0 +1,2 @@
+let depot = import ../../.. { };
+in depot.third_party.nixpkgs
diff --git a/users/grfn/bbbg/default.nix b/users/grfn/bbbg/default.nix
new file mode 100644
index 0000000000..6afb68353c
--- /dev/null
+++ b/users/grfn/bbbg/default.nix
@@ -0,0 +1,82 @@
+args@{ depot, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+  inherit (depot.third_party) gitignoreSource;
+
+  deps = import ./deps.nix {
+    inherit (pkgs) fetchMavenArtifact fetchgit lib;
+  };
+in
+rec {
+  meta.ci.targets = [
+    "db-util"
+    "server"
+    "tf"
+  ];
+
+  depsPaths = deps.makePaths { };
+
+  resources = builtins.filterSource (_: type: type != "symlink") ./resources;
+
+  classpath.dev = concatStringsSep ":" (
+    (map gitignoreSource [ ./src ./test ./env/dev ]) ++ [ resources ] ++ depsPaths
+  );
+
+  classpath.test = concatStringsSep ":" (
+    (map gitignoreSource [ ./src ./test ./env/test ]) ++ [ resources ] ++ depsPaths
+  );
+
+  classpath.prod = concatStringsSep ":" (
+    (map gitignoreSource [ ./src ./env/prod ]) ++ [ resources ] ++ depsPaths
+  );
+
+  testClojure = pkgs.writeShellScript "test-clojure" ''
+    export HOME=$(pwd)
+    ${pkgs.clojure}/bin/clojure -Scp ${depsPaths}
+  '';
+
+  mkJar = name: opts:
+    with pkgs;
+    assert (hasSuffix ".jar" name);
+    stdenv.mkDerivation rec {
+      inherit name;
+      dontUnpack = true;
+      buildPhase = ''
+        export HOME=$(pwd)
+        cp ${./pom.xml} pom.xml
+        cp ${./deps.edn} deps.edn
+        ${clojure}/bin/clojure \
+          -Scp ${classpath.prod} \
+          -A:uberjar \
+          ${name} \
+          -C ${opts}
+      '';
+
+      doCheck = true;
+
+      checkPhase = ''
+        echo "checking for existence of ${name}"
+        [ -f ${name} ]
+      '';
+
+      installPhase = ''
+        cp ${name} $out
+      '';
+    };
+
+  db-util-jar = mkJar "bbbg-db-util.jar" "-m bbbg.db";
+
+  db-util = pkgs.writeShellScriptBin "bbbg-db-util" ''
+    exec ${pkgs.openjdk17_headless}/bin/java -jar ${db-util-jar} "$@"
+  '';
+
+  server-jar = mkJar "bbbg-server.jar" "-m bbbg.core";
+
+  server = pkgs.writeShellScriptBin "bbbg-server" ''
+    exec ${pkgs.openjdk17_headless}/bin/java -jar ${server-jar} "$@"
+  '';
+
+  tf = import ./tf.nix args;
+}
diff --git a/users/grfn/bbbg/deps.edn b/users/grfn/bbbg/deps.edn
new file mode 100644
index 0000000000..39ce843c22
--- /dev/null
+++ b/users/grfn/bbbg/deps.edn
@@ -0,0 +1,70 @@
+{:deps
+ {org.clojure/clojure {:mvn/version "1.11.0-alpha3"}
+
+  ;; DB
+  com.github.seancorfield/next.jdbc {:mvn/version "1.2.761"}
+  com.impossibl.pgjdbc-ng/pgjdbc-ng {:mvn/version "0.8.9"}
+  com.zaxxer/HikariCP {:mvn/version "5.0.0"}
+  migratus/migratus {:mvn/version "1.3.5"}
+  com.github.seancorfield/honeysql {:mvn/version "2.2.840"}
+  nilenso/honeysql-postgres {:mvn/version "0.4.112"}
+
+  ;; HTTP
+  http-kit/http-kit {:mvn/version "2.5.3"}
+  ring/ring {:mvn/version "1.9.4"}
+  compojure/compojure {:mvn/version "1.6.2"}
+  javax.servlet/servlet-api {:mvn/version "2.5"}
+  ring-oauth2/ring-oauth2 {:mvn/version "0.2.0"}
+  clj-http/clj-http {:mvn/version "3.12.3"}
+  ring-logger/ring-logger {:mvn/version "1.0.1"}
+
+  ;; Web
+  hiccup/hiccup {:mvn/version "1.0.5"}
+  garden/garden {:mvn/version "1.3.10"}
+
+
+  ;; Logging + Observability
+  ch.qos.logback/logback-classic {:mvn/version "1.3.0-alpha12"}
+  org.slf4j/jul-to-slf4j {:mvn/version "2.0.0-alpha4"}
+  org.slf4j/jcl-over-slf4j {:mvn/version "2.0.0-alpha4"}
+  org.slf4j/log4j-over-slf4j {:mvn/version "2.0.0-alpha4"}
+  cambium/cambium.core {:mvn/version "1.1.1"}
+  cambium/cambium.codec-cheshire {:mvn/version "1.0.0"}
+  cambium/cambium.logback.core {:mvn/version "0.4.5"}
+  cambium/cambium.logback.json {:mvn/version "0.4.5"}
+  clj-commons/iapetos {:mvn/version "0.1.12"}
+
+  ;; Utilities
+  com.stuartsierra/component {:mvn/version "1.0.0"}
+  yogthos/config {:mvn/version "1.1.9"}
+  clojure.java-time/clojure.java-time {:mvn/version "0.3.3"}
+  cheshire/cheshire {:mvn/version "5.10.1"}
+  org.apache.commons/commons-lang3 {:mvn/version "3.12.0"}
+  org.clojure/data.csv {:mvn/version "1.0.0"}
+
+  ;; Spec
+  org.clojure/spec.alpha {:mvn/version "0.3.218"}
+  org.clojure/core.specs.alpha {:mvn/version "0.2.62"}
+  expound/expound {:mvn/version "0.8.10"}
+  org.clojure/test.check {:mvn/version "1.1.1"}}
+
+ :paths
+ ["src"
+  "test"
+  "resources"
+  "target/classes"]
+ :aliases
+ {:dev {:extra-paths ["env/dev"]
+        :jvm-opts ["-XX:-OmitStackTraceInFastThrow"]}
+  :clj-test {:extra-paths ["test" "env/test"]
+             :extra-deps {io.github.cognitect-labs/test-runner
+                          {:git/url "https://github.com/cognitect-labs/test-runner"
+                           :sha "cc75980b43011773162b485f46f939dc5fba91e4"}}
+             :main-opts ["-m" "cognitect.test-runner"
+                         "-d" "test"]}
+  :uberjar {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}}
+            :extra-paths ["env/prod"]
+            :main-opts ["-m" "hf.depstar.uberjar"]}
+
+  :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "1.3.1"}}
+             :main-opts ["-m" "antq.core"]}}}
diff --git a/users/grfn/bbbg/deps.nix b/users/grfn/bbbg/deps.nix
new file mode 100644
index 0000000000..02f5ecb468
--- /dev/null
+++ b/users/grfn/bbbg/deps.nix
@@ -0,0 +1,1494 @@
+# generated by clj2nix-1.1.0-rc
+{ fetchMavenArtifact, fetchgit, lib }:
+
+let
+  repos = [
+    "https://repo1.maven.org/maven2/"
+    "https://repo.clojars.org/"
+  ];
+
+in
+rec {
+  makePaths = { extraClasspaths ? [ ] }:
+    if (builtins.typeOf extraClasspaths != "list")
+    then builtins.throw "extraClasspaths must be of type 'list'!"
+    else (lib.concatMap
+      (dep:
+        builtins.map
+          (path:
+            if builtins.isString path then
+              path
+            else if builtins.hasAttr "jar" path then
+              path.jar
+            else if builtins.hasAttr "outPath" path then
+              path.outPath
+            else
+              path
+          )
+          dep.paths)
+      packages) ++ extraClasspaths;
+  makeClasspaths = { extraClasspaths ? [ ] }:
+    if (builtins.typeOf extraClasspaths != "list")
+    then builtins.throw "extraClasspaths must be of type 'list'!"
+    else builtins.concatStringsSep ":" (makePaths { inherit extraClasspaths; });
+  packageSources = builtins.map (dep: dep.src) packages;
+  packages = [
+    rec {
+      name = "cambium.logback.json/cambium";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "cambium.logback.json";
+        groupId = "cambium";
+        sha512 = "8e3f32bc1e11071ddc8700204333ba653585de7985c03d14c351950a7896975092e9deffd658bfec7b0b8b9cc72dc025d8e5179a185bd25da26e500218ec37a5";
+        version = "0.4.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clojure/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clojure";
+        groupId = "org.clojure";
+        sha512 = "a242514f623a17601b360886563c4a4fe09335e4e16522ac42bbcacda073ae77651cfed446daae7fe74061bb7dff5adc454769c0edc0ded350136c3c707e75b9";
+        version = "1.11.0-alpha3";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "joda-time/joda-time";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "joda-time";
+        groupId = "joda-time";
+        sha512 = "012fb9aa9b00b456f72a92374855a7f062f8617c026c436eee2cda67dffa2f8622201909c0f4f454bb346ff5a3ed6f60c236fafb19fa66f612d9861f27b38d3a";
+        version = "2.10";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "commons-codec/commons-codec";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "commons-codec";
+        groupId = "commons-codec";
+        sha512 = "da30a716770795fce390e4dd340a8b728f220c6572383ffef55bd5839655d5611fcc06128b2144f6cdcb36f53072a12ec80b04afee787665e7ad0b6e888a6787";
+        version = "1.15";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "HikariCP/com.zaxxer";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "HikariCP";
+        groupId = "com.zaxxer";
+        sha512 = "a41b6d8b1c4656e633459824f10320965976eeead01bd5cb24911040073181730e61feb797aef89d9e01c922e89cb58654f364df0a6b1bf62ab3e6f9cc367d77";
+        version = "5.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-devel/ring";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-devel";
+        groupId = "ring";
+        sha512 = "79a1ec9f9d03aa4fa0426353970b13468ee65ce314b51ab7a2682212a196a9b5c985eacdee5dbc6ff2f1b536a4e06d0e85e9dd7cc9a49958735c9c4e6d427fd5";
+        version = "1.9.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "simpleclient/io.prometheus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "simpleclient";
+        groupId = "io.prometheus";
+        sha512 = "60af1cefff04e7036467eae54f5930d5677e4ab066f8ed38a391b54df17733acfefac45e19ee53cef289347bddce5fc69a2766f4e580d21a22cfd9e2348e2723";
+        version = "0.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "commons-lang3/org.apache.commons";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "commons-lang3";
+        groupId = "org.apache.commons";
+        sha512 = "fbdbc0943cb3498b0148e86a39b773f97c8e6013740f72dbc727faeabea402073e2cc8c4d68198e5fc6b08a13b7700236292e99d4785f2c9989f2e5fac11fd81";
+        version = "3.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "tools.logging/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "tools.logging";
+        groupId = "org.clojure";
+        sha512 = "b7a9680f1156fc7c1574a4364ca550d47668ba727fc80110fdd00c159bedb45c5be82f09cdfb8e8e988e3381e2cf8881ea70651e38001e3eaa4ece31ad0bf0c5";
+        version = "1.2.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "core.specs.alpha/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "core.specs.alpha";
+        groupId = "org.clojure";
+        sha512 = "f521f95b362a47bb35f7c85528c34537f905fb3dd24f2284201e445635a0df701b35d8419d53c6507cc78d3717c1f83cda35ea4c82abd8943cd2ab3de3fcad70";
+        version = "0.2.62";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-common/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-common";
+        groupId = "io.netty";
+        sha512 = "7efc2f6774a3dbe8408fe182e19830b5b7a994a0d1b0eb50699df691c2450befa05ac205bbf341ad57bef3a04281ce435031e97e725c5c4edfc705a418828ce8";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jackson-databind/com.fasterxml.jackson.core";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jackson-databind";
+        groupId = "com.fasterxml.jackson.core";
+        sha512 = "9f771e78af669b1e1683d6c5903bbf4790aaa88b6b420c2018437da318c3fa4220cd7fa726f3e42a1b8075def1fdbd3744937c15f3bcedfca3050199247363e8";
+        version = "2.12.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "expound/expound";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "expound";
+        groupId = "expound";
+        sha512 = "ca0a57cfd215cff6be36d1f83461ec2d0559c0eae172c8a8bd6e1676d49933d3c30a71192889bd75d813581707d5eda0ec05de03326396bc0cedebf2d71811e5";
+        version = "0.8.10";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "spec.alpha/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "spec.alpha";
+        groupId = "org.clojure";
+        sha512 = "ddfe4fa84622abd8ac56e2aa565a56e6bdc0bf330f377ff3e269ddc241bb9dbcac332c13502dfd4c09c2c08fe24d8d2e8cf3d04a1bc819ca5657b4e41feaa7c2";
+        version = "0.3.218";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "tools.cli/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "tools.cli";
+        groupId = "org.clojure";
+        sha512 = "1d88aa03eb6a664bf2c0ce22c45e7296d54d716e29b11904115be80ea1661623cf3e81fc222d164047058239010eb678af92ffedc7c3006475cceb59f3b21265";
+        version = "1.0.206";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "compojure/compojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "compojure";
+        groupId = "compojure";
+        sha512 = "1f4ba1354bd95772963a4ef0e129dde59d16f4f9fac0f89f2505a1d5de3b4527e45073219c0478e0b3285da46793e7c145ec5a55a9dae2fca6b77dc8d67b4db6";
+        version = "1.6.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "commons-fileupload/commons-fileupload";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "commons-fileupload";
+        groupId = "commons-fileupload";
+        sha512 = "a8780b7dd7ab68f9e1df38e77a5207c45ff50ec53d8b1476570d069edc8f59e52fb1d0fc534d7e513ac5a01b385ba73c320794c82369a72bd6d817a3b3b21f39";
+        version = "1.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jetty-http/org.eclipse.jetty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jetty-http";
+        groupId = "org.eclipse.jetty";
+        sha512 = "60422ff3ef311f1d9d7340c2accdf611d40e738a39e9128967175ede4990439f4725995988849957742d488f749dd2e0740f74dc5bd9b3364e32fbaa66689308";
+        version = "9.4.42.v20210604";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jetty-util/org.eclipse.jetty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jetty-util";
+        groupId = "org.eclipse.jetty";
+        sha512 = "d69084e2cfe0c3af1dc7ee2745d563549a4068b6e8aed5cd2b9f31167168fb64d418c4134a6dfb811b627ec0051d7ff71e0a02e4e775d18a53543d0871c44730";
+        version = "9.4.42.v20210604";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "janino/org.codehaus.janino";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "janino";
+        groupId = "org.codehaus.janino";
+        sha512 = "6853d7d53d3629df43a3a17ff5c989f59ec14e9030be5f67426deb9d0797fa3996b0609d582c65f22a4f7680c941b39ab6d466c480b2fea4bf92218a9b89651d";
+        version = "3.1.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jcl-over-slf4j/org.slf4j";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jcl-over-slf4j";
+        groupId = "org.slf4j";
+        sha512 = "23662fe407fcdbcba8865a8cd3f8bb09d4eb178a2a6511a32e35b995722b345e73f5dc1dd85d2d0a5c707db05aa57e0b3d0b96b59e55403fc486343d5ca4c0d6";
+        version = "2.0.0-alpha4";
+
+      };
+      paths = [ src ];
+    }
+
+    (rec {
+      name = "io.github.cognitect-labs/test-runner";
+      src = fetchgit {
+        name = "test-runner";
+        url = "https://github.com/cognitect-labs/test-runner";
+        rev = "cc75980b43011773162b485f46f939dc5fba91e4";
+        sha256 = "1661ddmmqva1yiz9p09i5l32lfpi0a99h56022zgvz03nca2ksbg";
+      };
+      paths = map (path: src + path) [
+        "/src"
+      ];
+    })
+
+    rec {
+      name = "cambium.logback.core/cambium";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "cambium.logback.core";
+        groupId = "cambium";
+        sha512 = "83ee9a583dd8a7b2e82e0981b4e51b005095a27257eb1b07165d9701645609060c466ae67fb9431f524a544d52b71fa00009b8acf05aadbeb549043515f9b382";
+        version = "0.4.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "httpasyncclient/org.apache.httpcomponents";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "httpasyncclient";
+        groupId = "org.apache.httpcomponents";
+        sha512 = "0a80db5dbf772f02d02ba6c7c163e8da9517dd7195714b495acb845c429580c1fc926d3e71c115e75be8c145651dce2fdfa0dc380132f7809c14b3ad95492aee";
+        version = "4.1.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "logback-jackson/ch.qos.logback.contrib";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "logback-jackson";
+        groupId = "ch.qos.logback.contrib";
+        sha512 = "d9a3d4cb6cf4eda6fc18e2d374007d27c6ddba98e989a8d8a01b49859b280450113f685df6e16c5fbe0472bc9e26308bc7e8b7e0aedab9404cf0b492d7511685";
+        version = "0.1.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "simpleclient_tracer_otel/io.prometheus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "simpleclient_tracer_otel";
+        groupId = "io.prometheus";
+        sha512 = "bce192e6162cb3ada7dd6c2d10456e78bce71c170faa09bad2896272fa1bd4a036288d707f3d47747991d8946c74fe21c565713fb15c7052305eb753c94dd939";
+        version = "0.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-codec/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-codec";
+        groupId = "io.netty";
+        sha512 = "f6d9c4a5b508ca0d5f0e213473088f5d7b2e184e447dc092e69227109e28da9b8e68b2238ca6ab4e9915bacacf59cc0dce6ebcbbb05dad34a03b7976d9670c51";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-oauth2/ring-oauth2";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-oauth2";
+        groupId = "ring-oauth2";
+        sha512 = "3ed765b4bbb5749fcdcdb501b93ab656a413ade5af24c7aa34639718ed1fd0a5f325b05bd135540d56e55cbb456a2cb7852ba0e45bc5233e28229986eef75bb9";
+        version = "0.2.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "tools.macro/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "tools.macro";
+        groupId = "org.clojure";
+        sha512 = "65ce5e29379620ac458274c53cd9926e4b764fcaebb1a2b3bc8aef86bbe10c79e654b028bc4328905d2495a680fa90f5002cf5c47885f6449fad43a04a594b26";
+        version = "0.1.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jackson-dataformat-cbor/com.fasterxml.jackson.dataformat";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jackson-dataformat-cbor";
+        groupId = "com.fasterxml.jackson.dataformat";
+        sha512 = "ea5d049eac1b94666479c5e36de14d8fa4b7f24cb92f0f310d2ec2b4de66ef9023161060e67228ef2d7420a002ef861db12a29cad0864638c21612da49686f4f";
+        version = "2.12.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "depstar/seancorfield";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "depstar";
+        groupId = "seancorfield";
+        sha512 = "0f4458b39b8b1949755bc2fe64b239673a9efa3a0140998464bbbcab216ec847344c1b8920611f7c9ca07261850f3a08144ae221cc2c41813a080189e32f9c10";
+        version = "1.0.94";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "logback-core/ch.qos.logback";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "logback-core";
+        groupId = "ch.qos.logback";
+        sha512 = "fc554548f499e284007eeecf76bf4e1995effb6ac8a6262aa96118f623bf9085a9d5bec3741833dd3cae6a76b2ff78c6d0a1fe68bc01213207c93d8e2da345ca";
+        version = "1.3.0-alpha12";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "honeysql/honeysql";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "honeysql";
+        groupId = "honeysql";
+        sha512 = "74d1d93c968b33686848e3bf8934f3b5f002c2b69b1b55a3a3b172c952e9991324e6e95e3a0ce2fecf1de0d3a036f4dff7286df689f0733f253909464e0269f6";
+        version = "1.0.461";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-buffer/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-buffer";
+        groupId = "io.netty";
+        sha512 = "181b55d99d8d46bbf5f67f05bdccb0381af23a9fca3e6d935e6cde727b132c67133de1c3d81ed19b04c1a5b232be0de16ec1de7e81b532878bc69564237c15dc";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "slingshot/slingshot";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "slingshot";
+        groupId = "slingshot";
+        sha512 = "ff2b2a27b441d230261c7f3ec8c38aa551865e05ab6438a74bd12bfcbc5f6bdc88199d42aaf5932b47df84f3d2700c8f514b9f4e9b5da28d29da7ff6b09a7fb5";
+        version = "0.12.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "httpcore-nio/org.apache.httpcomponents";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "httpcore-nio";
+        groupId = "org.apache.httpcomponents";
+        sha512 = "002af5f72b68a4ff1b1ff46b788013283d195e1d62ee1d7b102aa930b30f77f7e215a6d18edbea0fccd18fb1fa3a66cc4aef6070d72d6d1886f0044dfe0e16c7";
+        version = "4.4.10";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-jetty-adapter/ring";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-jetty-adapter";
+        groupId = "ring";
+        sha512 = "93075903ad73a8b73cb77ee9f53ed33594f40a5dafe8129089adb4cfa333e37468764203c00244568f02abf0c0eee9f5d9a9f96c420919027cf2746a41ec38e3";
+        version = "1.9.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "simpleclient_tracer_common/io.prometheus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "simpleclient_tracer_common";
+        groupId = "io.prometheus";
+        sha512 = "6f717af63340efd84c5467ae752be7e66f586f0e8b57adb5b7a8ef99b223203ed829aad6797f6ef1811d6d861b00a621a1288c9271ec2ba77018d6d9eb9e7987";
+        version = "0.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "component/com.stuartsierra";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "component";
+        groupId = "com.stuartsierra";
+        sha512 = "108b02f51165ad07c2cf5232fbd954d052880c2456e6fb6db3342bda6851c76b73bf9145f03fb0df2b5782fe39f368b2868780c1e8e2dfa2ab2c68dd97f34ab7";
+        version = "1.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-handler/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-handler";
+        groupId = "io.netty";
+        sha512 = "48874727553dd7084f5c48d90de123704ae334837c3a103f598887bb21405dd62c57603b59300ac2fcdd936f0af99ed0730487fb9fb8917d236b8fe3f78f3c02";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "yuicompressor/com.yahoo.platform.yui";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "yuicompressor";
+        groupId = "com.yahoo.platform.yui";
+        sha512 = "ba2588bd50eaa3005b1919daad9f9c86a33351ceb9b7b5f0a9a498a548cc523e99f9345917a64303f8e23925feea226386d3eac01f640f788d1be4c7cf0315e0";
+        version = "2.4.8";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "commons-io/commons-io";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "commons-io";
+        groupId = "commons-io";
+        sha512 = "6af22dffaaecd1553147e788b5cf50368582318f396e456fe9ff33f5175836713a5d700e51720465c932c2b1987daa83027358005812d6a95d5755432de3a79d";
+        version = "2.10.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "tools.namespace/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "tools.namespace";
+        groupId = "org.clojure";
+        sha512 = "2cdb9c5d9bc4fd01dae182e9ad4b91eeaa2487003a977e7d8d5e66f562a9544b59f558710eccf421ea63cbbfa953ac8944fe9b9a76049fb82a47eb2bdcb3a4d7";
+        version = "1.1.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "honeysql/com.github.seancorfield";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "honeysql";
+        groupId = "com.github.seancorfield";
+        sha512 = "a0e5ebbf922aaf170c2d74ec0efc0df7e3bda92d0b8cc5f40ee4c8ddcb8c7e0e46556fac381513e0ac76b10f681c14c2d2569010c2f8eab4ff04f6373c2bf229";
+        version = "2.2.840";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jackson-core/com.fasterxml.jackson.core";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jackson-core";
+        groupId = "com.fasterxml.jackson.core";
+        sha512 = "428e0ebb16dd4c74ab0adf712058fd0dc0cd788f6e6f90c60c627da6577b345fac60a30694e111f1cd4e3e8bf79a1f1b820d30ada114984b26c28e299e326eaa";
+        version = "2.12.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clj-time/clj-time";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clj-time";
+        groupId = "clj-time";
+        sha512 = "cfeb46af59fd4112aa5a5d0087a39355f0fc19514b4c02bc6c3d9f81c9bda40491686207836e9a7943aebeb82a3b36f4e8b7407a8908c5ef151122644b278d75";
+        version = "0.15.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clj-http/clj-http";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clj-http";
+        groupId = "clj-http";
+        sha512 = "9884557d4f38068cb3234aec80acc0de8f9716645529693ffd9bd6db8221f5d1cf9e2d1b8bf7c7df4215d71372b02d83043ebf8fc27dc422552b32c9bdba1602";
+        version = "3.12.3";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jul-to-slf4j/org.slf4j";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jul-to-slf4j";
+        groupId = "org.slf4j";
+        sha512 = "350cfb889248d724b27dce697f635f12d9db463f107830b9518ce184dc4cc1ab3933eb5bdab08515e69766c3d5be24547dac289d6406c44eca90717230714b91";
+        version = "2.0.0-alpha4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "migratus/migratus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "migratus";
+        groupId = "migratus";
+        sha512 = "ee5ce8601930d063e0d9d90fc8e165b78fc1587bfd7e0fc9922735bc2f9fc27f8cf8bf10d49d6fd57b899ac4b250145bd653915ed770424416e026ba37d1b604";
+        version = "1.3.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "httpcore/org.apache.httpcomponents";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "httpcore";
+        groupId = "org.apache.httpcomponents";
+        sha512 = "f16a652f4a7b87dbf7cb16f8590d54a3f719c4c7b2f8883ce59db2d73be4701b64f2ca8a2c45aca6a5dbeaddeedff0c280a03722f70c076e239b645faa54eff9";
+        version = "4.4.14";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "httpclient-cache/org.apache.httpcomponents";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "httpclient-cache";
+        groupId = "org.apache.httpcomponents";
+        sha512 = "e150e8dc49c8c9972d8b324b56bb292b15e2f0e686f1292c4edac975615dfb16e5edb8ab325e614732a7d43a03061ca4fe93fe1e1f7487851a4d4d3af50a61f9";
+        version = "4.5.13";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "instaparse/instaparse";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "instaparse";
+        groupId = "instaparse";
+        sha512 = "ec2fcf4a09319a8fa9489b08fd9c9a5fe6e63155dde74d096f947fabc4f68d3d1bf68faf21e175e80eaee785f563a1903d30c550f93fb13a16a240609e3dfa2e";
+        version = "1.4.8";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "honeysql-postgres/nilenso";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "honeysql-postgres";
+        groupId = "nilenso";
+        sha512 = "d4accd3b8819cf715ecdb29496cf5a6a5ad3871fd579e55c7148d4e05774cb896c681b0c6f84df88aa9cd8e6ef9bfd65788ede9a49ba365ad0e32ee350091879";
+        version = "0.4.112";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clj-tuple/clj-tuple";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clj-tuple";
+        groupId = "clj-tuple";
+        sha512 = "dd626944d0aba679a21b164ed0c77ea84449359361496cba810f83b9fdeab751e5889963888098ce4bf8afa112dbda0a46ed60348a9c01ad36a2e255deb7ab6d";
+        version = "0.2.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jackson-annotations/com.fasterxml.jackson.core";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jackson-annotations";
+        groupId = "com.fasterxml.jackson.core";
+        sha512 = "6fdad6c5bb71a97331a662fe26265aacab6869f3307a710697d5c2f256fd48935764bfb0b3505a2cbb1605daf0b7350abdf84a1b1cf2bb1e91d9184565243c8e";
+        version = "2.12.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "hiccup/hiccup";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "hiccup";
+        groupId = "hiccup";
+        sha512 = "034f15be46c35029f41869c912f82cb2929fbbb0524ea64bd98dcdb9cf09875b28c75e926fa5fff53942b0f9e543e85a73a2d03c3f2112eecae30fcef8b148f4";
+        version = "1.0.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "riddley/riddley";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "riddley";
+        groupId = "riddley";
+        sha512 = "b478ecba9d1ab9d38c84a42354586fcece763000907b40c97bc43c0f16dc560b0860144efe410193cb3b7cb0149fbc1724fdd737cc3ba53de23618f5b30e6f9f";
+        version = "0.1.12";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "java.classpath/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "java.classpath";
+        groupId = "org.clojure";
+        sha512 = "90cd8edeaea02bd908d8cfb0cf5b1cf901aeb38ea3f4971c4b813d33210438aae6fff8e724a8272d2ea9441d373e7d936fa5870e309c1e9721299f662dbbdb9a";
+        version = "1.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "simpleclient_pushgateway/io.prometheus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "simpleclient_pushgateway";
+        groupId = "io.prometheus";
+        sha512 = "31c8878929f516ba7030cc9ec4ac4cbcb09955a9fdae23c6904bc481e40e70e1b3e05619c49b646119077ef6f57c430cc7944f6bafdbca24c9efa8145474fcf7";
+        version = "0.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ns-tracker/ns-tracker";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ns-tracker";
+        groupId = "ns-tracker";
+        sha512 = "cfb6c2c9f899b43d1284acdc572b34b977936c4df734b38137dfea045421b74d529509cde23695f1dc5ee06d046c2f6b61a2cd98058da1c7220c21dd0361964f";
+        version = "0.4.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clout/clout";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clout";
+        groupId = "clout";
+        sha512 = "99d6e1a8c5726ca4e5d12b280a39e6d1182d734922600f27d588d3d65fbc830c5e03f9e0421ff25c819deee4d1f389fd3906222716ace1eb17ce70ef9c5e8f4b";
+        version = "2.2.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "commons-logging/commons-logging";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "commons-logging";
+        groupId = "commons-logging";
+        sha512 = "ed00dbfabd9ae00efa26dd400983601d076fe36408b7d6520084b447e5d1fa527ce65bd6afdcb58506c3a808323d28e88f26cb99c6f5db9ff64f6525ecdfa557";
+        version = "1.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clojure.java-time/clojure.java-time";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clojure.java-time";
+        groupId = "clojure.java-time";
+        sha512 = "62d8a286ec3393594e7f84eba22dbb02c1305a80a18b2574058ae963d3f3e829ff960c8b66e89069e6c071a11f869203134c6c4cdec6f8e516c9b314796c8108";
+        version = "0.3.3";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "data.csv/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "data.csv";
+        groupId = "org.clojure";
+        sha512 = "b039775a859ed27eca8f8ae74ccb6afde3ad1fe2b3cbe542240c324d60fe1237e495eb1300ee9eb4ff4ef59f01faf7aec6ef1dd6a025ee4fe556c1d91acfcf1b";
+        version = "1.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "simpleclient_tracer_otel_agent/io.prometheus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "simpleclient_tracer_otel_agent";
+        groupId = "io.prometheus";
+        sha512 = "97694210d9a5b48a7cb9dda2a187432c4813edb3051edfa5832a0a471e0b2d5988dab92b70c292e78f59b169345deb5c1c706361fd726f3dc2480766dedfdcec";
+        version = "0.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "next.jdbc/com.github.seancorfield";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "next.jdbc";
+        groupId = "com.github.seancorfield";
+        sha512 = "0b4b01ba126bb8b1e2c14262db9fca75456b274d09535d9a7bb386699bf20dc9ac11590d210769e7429ca59ebfdfbb06916b3ff275cc817d74eac5bbabdab8f2";
+        version = "1.2.761";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "java.jdbc/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "java.jdbc";
+        groupId = "org.clojure";
+        sha512 = "6162b7774dca58b62a94bc5a04ba845e4c7065c9c589cc3bb802becfec0baf0989a338c1bf9a5db7c3128873702840d5f2451628f3aac977245975d65a683b7d";
+        version = "0.7.11";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-transport/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-transport";
+        groupId = "io.netty";
+        sha512 = "c11d690ffeaf3267b2166f73a43108fb89d588fcef3f6d3053bf4b6f6669483baa618fd97438010692a6fa28334372d5a31b7c0996961d4eabb60cbdc358a536";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "crypto-random/crypto-random";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "crypto-random";
+        groupId = "crypto-random";
+        sha512 = "3520df744f250dbe061d1a5d7a05b7143f3a67a4c3f9ad87b8044ee68a36a702a0bcb3a203e35d380899dd01c28e01988b0a7af914b942ccbe0c35c9bdb22e11";
+        version = "1.2.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-transport-native-unix-common/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-transport-native-unix-common";
+        groupId = "io.netty";
+        sha512 = "b63e5f8a44b7f37f3dba378bd06af64dd1d7be3f0b1a7d47ad139ff06e0212b4c7081275b1b5b12183aeb72eb5f9bf9ef03ed8c78bc302aeb4817dca7bd89f3a";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-codec/ring";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-codec";
+        groupId = "ring";
+        sha512 = "38b9775a794831b8afd8d66991a75aa5910cd50952c9035866bf9cc01353810aedafbc3f35d8f9e56981ebf9e5c37c00b968759ed087d2855348b3f46d8d0487";
+        version = "1.1.3";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "spy/com.impossibl.pgjdbc-ng";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "spy";
+        groupId = "com.impossibl.pgjdbc-ng";
+        sha512 = "173615c39aa6015a732e329217b40e3ea1c304c9c168d2764d6ef23ab8775e2f4432339bc22d049662561f09d3fd890b5415738620d64dcedb762d5da26b4ebb";
+        version = "0.8.9";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "logback-json-core/ch.qos.logback.contrib";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "logback-json-core";
+        groupId = "ch.qos.logback.contrib";
+        sha512 = "2a826036f21997e2979fda83ae3e33cf62f3b2b2df15a7b11d1fd8a52163b09f0f2f8d72f5fdcea0ec1289b3d27727ed5e6b0bcdf4c5d741f4bac07b7b6139e8";
+        version = "0.1.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "httpclient/org.apache.httpcomponents";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "httpclient";
+        groupId = "org.apache.httpcomponents";
+        sha512 = "3567739186e551f84cad3e4b6b270c5b8b19aba297675a96bcdff3663ff7d20d188611d21f675fe5ff1bfd7d8ca31362070910d7b92ab1b699872a120aa6f089";
+        version = "4.5.13";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "crypto-equality/crypto-equality";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "crypto-equality";
+        groupId = "crypto-equality";
+        sha512 = "54cf3bd28f633665962bf6b41f5ccbf2634d0db210a739e10a7b12f635e13c7ef532efe1d5d8c0120bb46478bbd08000b179f4c2dd52123242dab79fea97d6a6";
+        version = "1.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "cheshire/cheshire";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "cheshire";
+        groupId = "cheshire";
+        sha512 = "855e9c42a8d1c64f4db5cda45e31e914eb5ed99a715e8d7a5759a9c4ab6c69a82353635ca7b0837880c6cf9b41b11184ae11e09cbf2c07aa13db32c539e5dfd4";
+        version = "5.10.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "tigris/tigris";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "tigris";
+        groupId = "tigris";
+        sha512 = "fdff4ef5e7175a973aaef98de4f37dee8e125fc711c495382e280aaf3e11341fe8925d52567ca60f3f1795511ade11bc23461c88959632dfae3cf50374d02bf6";
+        version = "0.1.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "config/yogthos";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "config";
+        groupId = "yogthos";
+        sha512 = "3437992d192465edc74aec5259d5e0c0ad7e631dff860b2ee14cef27f13cee7c60487202cf00fc160a95fb0b85ce1ddf56cbdd0c008b47ac598061bf115f6a23";
+        version = "1.1.9";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jetty-io/org.eclipse.jetty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jetty-io";
+        groupId = "org.eclipse.jetty";
+        sha512 = "a8c5f73089daa0c8b27f836acddf40bcbf07bbb2571a4d73653be8aac3fb339022f546326722f216bad78a68886934d24db9bec54235124592dd29dbeab69051";
+        version = "9.4.42.v20210604";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "logback-json-classic/ch.qos.logback.contrib";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "logback-json-classic";
+        groupId = "ch.qos.logback.contrib";
+        sha512 = "d30bf70217d316914d83d46cc15783f656354084087d59cbc0620a746f10b4a42e56d33b3e50a8b3596a64ec8314730bf5ff9a3f7dc3417bdd0582665be009ec";
+        version = "0.1.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "tools.reader/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "tools.reader";
+        groupId = "org.clojure";
+        sha512 = "3481259c7a1eac719db2921e60173686726a0c2b65879d51a64d516a37f6120db8ffbb74b8bd273404285d7b25143ab5c7ced37e7c0eaf4ab1e44586ccd3c651";
+        version = "1.3.6";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "simpleclient_common/io.prometheus";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "simpleclient_common";
+        groupId = "io.prometheus";
+        sha512 = "dedd003638eb3651c112e2d697ac94eb4e3b3e32c94fa41bb1efe2c889a347cdc7bd13256e05423f3370592d4fd65faf8db57f0387ab75814d7fa77b14cbbadf";
+        version = "0.12.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "commons-compiler/org.codehaus.janino";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "commons-compiler";
+        groupId = "org.codehaus.janino";
+        sha512 = "f0778b891ef14d8ee6776747eab0b25da716cdc530752a81aedec2a77570e2f66402179b9408a6efde8125c808eb060a720d2f4977c1f1d022bdaae7eac8d011";
+        version = "3.1.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "servlet-api/javax.servlet";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "servlet-api";
+        groupId = "javax.servlet";
+        sha512 = "363ba5590436ab82067b7a2e14b481aeb2b12ca4048d7a1519a2e549b2d3c09ddf718ac64dc2be6c2fc24c51fdc9c8160261329403113369588ce27d87771db6";
+        version = "2.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "iapetos/clj-commons";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "iapetos";
+        groupId = "clj-commons";
+        sha512 = "d17f36c0cf0ec78db5e893e5c033f8562b31650bda6f5ee582e68f84a07a3631d04d6f69e4e18b1ca64e732c180fa669dfb69a78849e13f601cd563a7a8aab94";
+        version = "0.1.12";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "javax.servlet-api/javax.servlet";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "javax.servlet-api";
+        groupId = "javax.servlet";
+        sha512 = "32f7e3565c6cdf3d9a562f8fd597fe5059af0cf6b05b772a144a74bbc95927ac275eb38374538ec1c72adcce4c8e1e2c9f774a7b545db56b8085af0065e4a1e5";
+        version = "3.1.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "potemkin/potemkin";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "potemkin";
+        groupId = "potemkin";
+        sha512 = "5abc050bf7ff0b27d8c45aaa5e378201980815b711b2db99735db73304576c17e285026ea48a714bf0b0df7ad7a008de38b7d182cdc0e8989f4be1e6b3afa8aa";
+        version = "0.4.5";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-resolver/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-resolver";
+        groupId = "io.netty";
+        sha512 = "fabf893de74264caa1799c15d184ed8f20b7bf9b1c41abb29f29adf728a934951f97892a4924634f9efbda17c8cf74ea3ff97bafca616711e3c5f79b8ed9ef3e";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-transport-native-epoll/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-transport-native-epoll";
+        groupId = "io.netty";
+        sha512 = "6fbc2dd2622699f3fc1f329acbd94baf7f1d8923c5cfcae262e6f2d64b4fd71b606561bce5e2b511dff8e052cdade930091fab683fd98713f6b62a622a2c6254";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "clj-stacktrace/clj-stacktrace";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "clj-stacktrace";
+        groupId = "clj-stacktrace";
+        sha512 = "993f8a544203801fc074eefacee8e553e426422b3492d47b857d87ac73cde72c91e29f629382b9eae8cf9600bc2c4c29d2e7169e509c46302ab973c86e73af0c";
+        version = "0.2.8";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "cambium.codec-cheshire/cambium";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "cambium.codec-cheshire";
+        groupId = "cambium";
+        sha512 = "614491cf752a597f29ae29885db6c1ed191341303d89183bee52e4e2c76eb8eb14693562ad09484f379a074b36d97085e848ec3845e069440e6422506c1636f1";
+        version = "1.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "slf4j-api/org.slf4j";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "slf4j-api";
+        groupId = "org.slf4j";
+        sha512 = "ad705ab6fd5cd904ef6861c0adf08af19593cf6a486b18de548fe3d68e57b1baa7e02947584fd4dcc350ddcddcf906c01e8d9ba7943a202690d0d788627696b5";
+        version = "2.0.0-alpha4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "test.check/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "test.check";
+        groupId = "org.clojure";
+        sha512 = "b8d7a330b0b5514cd6a00c4382052fab51c3c9d3bc53133f8506791fa670e7c5ecd65094977ea5ced91f59623b0abd1ab8feeec96d63c5c6e459b265a655c577";
+        version = "1.1.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-logger/ring-logger";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-logger";
+        groupId = "ring-logger";
+        sha512 = "b675a61c173289fc610d84920ba40178bf62b3bc680923cb66866d78ee2a508296b27a1ab14b66bfbe0304a64166a7e3c3ddee36564dd4a2f988861bce455a3a";
+        version = "1.0.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-servlet/ring";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-servlet";
+        groupId = "ring";
+        sha512 = "3d8e6ec224e13d54810a945c0b6c0d2d863736a48d8c4bfc8fadb96b6b0fa9baa638644d0d92d8a53650b188e6e75d391731b08b26eb0f551e90a7504e7f4267";
+        version = "1.9.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "logback-classic/ch.qos.logback";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "logback-classic";
+        groupId = "ch.qos.logback";
+        sha512 = "f9fe0f126061f4abe3973b631b8d8244ba9e9d77783479a6500d629d772050dee508a001fc14d2131407fbdd0d33dd6b8aeb9b1ea9125b471bb8412e8de659e6";
+        version = "1.3.0-alpha12";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "dependency/com.stuartsierra";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "dependency";
+        groupId = "com.stuartsierra";
+        sha512 = "d32fbc4813bd16f2ed8c82e2915e1fb564e88422159bd3580a85c8cd969d1bbbe315bdc13d29c2f0eaceeeafcf649ee712c8df4532464d560aaeae4ae5953866";
+        version = "1.0.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "camel-snake-kebab/camel-snake-kebab";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "camel-snake-kebab";
+        groupId = "camel-snake-kebab";
+        sha512 = "589d34b500560b7113760a16bfb6f0ccd8f162a1ce8c9bc829495432159ba9c95aebf6bc43aa126237a0525806a205a05f9910122074902b659e7fd151d176b1";
+        version = "0.4.2";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring/ring";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring";
+        groupId = "ring";
+        sha512 = "93c48fb670736b91fb41d8076e1e9c4f53c67693d15e75290da319e7d7881b829a24180029b3a0fa051473c6c77ac3c97b519254ebf2b2c9538b185e79b69162";
+        version = "1.9.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "netty-transport-native-kqueue/io.netty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "netty-transport-native-kqueue";
+        groupId = "io.netty";
+        sha512 = "87e10c06e394a1698d65381d3be8336f753c55e3e899e297510161d0c72540023f30f9032322957e035ead793204a084b988bc21a2bc312fcf7567a22d02a3c4";
+        version = "4.1.63.Final";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "java.data/org.clojure";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "java.data";
+        groupId = "org.clojure";
+        sha512 = "225e1eafd1a659278212d831f7cd8609359f8c880ef3d69b4ade6301ce3c511307ce31d94cb82d5407314b990bd04714ec26273bb3036b248116a7a75fa75e1f";
+        version = "1.0.95";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jetty-server/org.eclipse.jetty";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jetty-server";
+        groupId = "org.eclipse.jetty";
+        sha512 = "b347f8a6e5b84e0f460037027e238a61edec710ade768c95e7be13dcea498abe43d5e622ee69ac7494138d1a8fcf92e07b7deab569c554831c57baad71c53b9b";
+        version = "9.4.42.v20210604";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "httpmime/org.apache.httpcomponents";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "httpmime";
+        groupId = "org.apache.httpcomponents";
+        sha512 = "e1b0ee84bce78576074dc1b6836a69d8f5518eade38562e6890e3ddaa72b7f54bf735c8e2286142c58cddf45f745da31261e5d73b7d8092eb6ecfb20946eb36c";
+        version = "4.5.13";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "log4j-over-slf4j/org.slf4j";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "log4j-over-slf4j";
+        groupId = "org.slf4j";
+        sha512 = "48fa023c57294b73b9bd2f53e3dd3169e03426e5b3aa9d80e1bb1a9abf927fc26ef9f64d02b9769d5577d83094d0f41f044d35bb3b4f6037d66d6b2f19b484a1";
+        version = "2.0.0-alpha4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "ring-core/ring";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "ring-core";
+        groupId = "ring";
+        sha512 = "38d7214a3fc1b80ab55999036638dd1971272e01bec4cb8e0ee0a4aa83f51b8c41ba8a5850b0660227f067d2f9c6d75c0c0737725ea02762bbf8d192dc72febe";
+        version = "1.9.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "cambium.core/cambium";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "cambium.core";
+        groupId = "cambium";
+        sha512 = "0e1fe626c6d0b31aad84ea2e4466273065925548ee5915f442b7997ebfe795faea36dbeac50a0f8c16bbd20d877511e3f8c4ff4f2b916a4538513aaa5cc20112";
+        version = "1.1.1";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "medley/medley";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "medley";
+        groupId = "medley";
+        sha512 = "749ef43b5ea2cae7dc96db871cdd15c7b3c9cfbd96828c20ab08e67d39a5e938357d15994d8d413bc68678285d6c666f2a7296fbf305706d03b3007254e3c55c";
+        version = "1.3.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "garden/garden";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "garden";
+        groupId = "garden";
+        sha512 = "2cc29f071b68bf451835f76de351ac2efb930b5df9ca7237fdca439d3c4d797d7fa207a147886efe1738ab1c50b76c1e366bf9ffcd6f286b0b211260aedd0b25";
+        version = "1.3.10";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jackson-dataformat-smile/com.fasterxml.jackson.dataformat";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jackson-dataformat-smile";
+        groupId = "com.fasterxml.jackson.dataformat";
+        sha512 = "69676964a2b09516b8ffd0d847b6f9a9b843424185453731b548c25e7e9ce30e808c56d66923f9183e2b5c1ba007421b146a6806e768b8e6b07470d60227f1dd";
+        version = "2.12.4";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "jaxb-api/javax.xml.bind";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "jaxb-api";
+        groupId = "javax.xml.bind";
+        sha512 = "0c5bfc2c9f655bf5e6d596e0c196dcb9344d6dc78bf774207c8f8b6be59f69addf2b3121e81491983eff648dfbd55002b9878132de190825dad3ef3a1265b367";
+        version = "2.3.0";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "pgjdbc-ng/com.impossibl.pgjdbc-ng";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "pgjdbc-ng";
+        groupId = "com.impossibl.pgjdbc-ng";
+        sha512 = "a34ac9146257329f6e9b354f13f564c65dbea6463addae383e3918d3a64c90c67f5f7fda6b5c3866de991a568d6690edb3fb09f2507593390a6e30ec0c79e02c";
+        version = "0.8.9";
+
+      };
+      paths = [ src ];
+    }
+
+    rec {
+      name = "http-kit/http-kit";
+      src = fetchMavenArtifact {
+        inherit repos;
+        artifactId = "http-kit";
+        groupId = "http-kit";
+        sha512 = "4186a2429984745e18730aa8fd545f1fc1812083819ebf77aecfc04e0d31585358a5e25a308c7f21d81359418bbc72390c281f5ed91ae116cf1af79860ba22c3";
+        version = "2.5.3";
+
+      };
+      paths = [ src ];
+    }
+
+  ];
+}
+  
\ No newline at end of file
diff --git a/users/grfn/bbbg/env/dev/bbbg-signup/env.clj b/users/grfn/bbbg/env/dev/bbbg-signup/env.clj
new file mode 100644
index 0000000000..c30e328ffa
--- /dev/null
+++ b/users/grfn/bbbg/env/dev/bbbg-signup/env.clj
@@ -0,0 +1,3 @@
+(ns bbbg.env)
+
+(def environment :env/dev)
diff --git a/users/grfn/bbbg/env/dev/logback.xml b/users/grfn/bbbg/env/dev/logback.xml
new file mode 100644
index 0000000000..7aa21978bb
--- /dev/null
+++ b/users/grfn/bbbg/env/dev/logback.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg { %mdc }%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="INFO">
+    <appender-ref ref="STDOUT" />
+  </root>
+
+  <logger name="user" level="ALL" />
+  <logger name="ci.windtunnel" level="ALL" />
+</configuration>
diff --git a/users/grfn/bbbg/env/prod/bbbg-signup/env.clj b/users/grfn/bbbg/env/prod/bbbg-signup/env.clj
new file mode 100644
index 0000000000..46e8cd67e3
--- /dev/null
+++ b/users/grfn/bbbg/env/prod/bbbg-signup/env.clj
@@ -0,0 +1,3 @@
+(ns bbbg.env)
+
+(def environment :env/prod)
diff --git a/users/grfn/bbbg/env/prod/logback.xml b/users/grfn/bbbg/env/prod/logback.xml
new file mode 100644
index 0000000000..b81118ed6b
--- /dev/null
+++ b/users/grfn/bbbg/env/prod/logback.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <!-- Silence Logback's own status messages about config parsing -->
+  <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
+
+  <!-- Console output -->
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <!-- Only log level INFO and above -->
+    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+      <level>INFO</level>
+    </filter>
+    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+      <layout class="cambium.logback.json.FlatJsonLayout">
+        <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
+          <prettyPrint>false</prettyPrint>
+        </jsonFormatter>
+        <!-- <context>api</context> -->
+        <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampFormat>
+        <timestampFormatTimezoneId>UTC</timestampFormatTimezoneId>
+        <appendLineSeparator>true</appendLineSeparator>
+      </layout>
+    </encoder>
+  </appender>
+
+
+  <root level="INFO">
+    <appender-ref ref="STDOUT" />
+  </root>
+
+  <logger name="user" level="ALL" />
+</configuration>
diff --git a/users/grfn/bbbg/env/test/bbbg-signup/env.clj b/users/grfn/bbbg/env/test/bbbg-signup/env.clj
new file mode 100644
index 0000000000..352147a6d0
--- /dev/null
+++ b/users/grfn/bbbg/env/test/bbbg-signup/env.clj
@@ -0,0 +1,3 @@
+(ns bbbg.env)
+
+(def environment :env/test)
diff --git a/users/grfn/bbbg/env/test/logback.xml b/users/grfn/bbbg/env/test/logback.xml
new file mode 100644
index 0000000000..8554f3d0ed
--- /dev/null
+++ b/users/grfn/bbbg/env/test/logback.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+      <pattern>%msg%n</pattern>
+    </encoder>
+  </appender>
+  <root level="OFF">
+    <appender-ref ref="CONSOLE"/>
+  </root>
+</configuration>
diff --git a/users/grfn/bbbg/module.nix b/users/grfn/bbbg/module.nix
new file mode 100644
index 0000000000..70bb2c77e4
--- /dev/null
+++ b/users/grfn/bbbg/module.nix
@@ -0,0 +1,137 @@
+{ config, lib, pkgs, depot, ... }:
+
+let
+  bbbg = depot.users.grfn.bbbg;
+  cfg = config.services.bbbg;
+in
+{
+  options = with lib; {
+    services.bbbg = {
+      enable = mkEnableOption "BBBG Server";
+
+      port = mkOption {
+        type = types.int;
+        default = 7222;
+        description = "Port to listen to for the HTTP server";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "bbbg.gws.fyi";
+        description = "Domain to host under";
+      };
+
+      proxy = {
+        enable = mkEnableOption "NGINX reverse proxy";
+      };
+
+      database = {
+        enable = mkEnableOption "BBBG Database Server";
+
+        user = mkOption {
+          type = types.str;
+          default = "bbbg";
+          description = "Database username";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Database host";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "bbbg";
+          description = "Database name";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 5432;
+          description = "Database host";
+        };
+      };
+    };
+  };
+
+  config = lib.mkMerge [
+    (lib.mkIf cfg.enable {
+      systemd.services.bbbg-server = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        serviceConfig = {
+          DynamicUser = true;
+          Restart = "always";
+          EnvironmentFile = config.age.secretsDir + "/bbbg";
+        };
+
+        environment = {
+          PGHOST = cfg.database.host;
+          PGUSER = cfg.database.user;
+          PGDATABASE = cfg.database.name;
+          PORT = toString cfg.port;
+          BASE_URL = "https://${cfg.domain}";
+        };
+
+        script = "${bbbg.server}/bin/bbbg-server";
+      };
+
+      systemd.services.migrate-bbbg = {
+        description = "Run database migrations for BBBG";
+        wantedBy = [ "bbbg-server.service" ];
+        after = ([ "network.target" ]
+          ++ (if cfg.database.enable
+        then [ "postgresql.service" ]
+        else [ ]));
+
+        serviceConfig = {
+          Type = "oneshot";
+          EnvironmentFile = config.age.secretsDir + "/bbbg";
+        };
+
+        environment = {
+          PGHOST = cfg.database.host;
+          PGUSER = cfg.database.user;
+          PGDATABASE = cfg.database.name;
+        };
+
+        script = "${bbbg.db-util}/bin/bbbg-db-util migrate";
+      };
+    })
+    (lib.mkIf cfg.database.enable {
+      services.postgresql = {
+        enable = true;
+        authentication = lib.mkForce ''
+          local all all trust
+          host all all 127.0.0.1/32 password
+          host all all ::1/128 password
+          hostnossl all all 127.0.0.1/32 password
+          hostnossl all all ::1/128  password
+        '';
+
+        ensureDatabases = [
+          cfg.database.name
+        ];
+
+        ensureUsers = [{
+          name = cfg.database.user;
+          ensurePermissions = {
+            "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
+          };
+        }];
+      };
+    })
+    (lib.mkIf cfg.proxy.enable {
+      services.nginx = {
+        enable = true;
+        virtualHosts."${cfg.domain}" = {
+          enableACME = true;
+          forceSSL = true;
+          locations."/".proxyPass = "http://localhost:${toString cfg.port}";
+        };
+      };
+    })
+  ];
+}
diff --git a/users/grfn/bbbg/pom.xml b/users/grfn/bbbg/pom.xml
new file mode 100644
index 0000000000..012c0985f1
--- /dev/null
+++ b/users/grfn/bbbg/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>fyi.gws</groupId>
+  <artifactId>bbbg</artifactId>
+  <version>0.1.0-SNAPSHOT</version>
+  <name>fyi.gws/bbbg</name>
+  <description>webhook listener for per-branch deploys</description>
+  <url>https://bbbg.gws.fyi</url>
+  <developers>
+    <developer>
+      <name>Griffin Smith</name>
+    </developer>
+  </developers>
+  <dependencies>
+    <dependency>
+      <groupId>org.clojure</groupId>
+      <artifactId>clojure</artifactId>
+      <version>1.11.0-alpha3</version>
+    </dependency>
+  </dependencies>
+  <build>
+    <sourceDirectory>src</sourceDirectory>
+  </build>
+  <repositories>
+    <repository>
+      <id>clojars</id>
+      <url>https://repo.clojars.org/</url>
+    </repository>
+    <repository>
+      <id>sonatype</id>
+      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
+    </repository>
+  </repositories>
+  <distributionManagement>
+    <repository>
+      <id>clojars</id>
+      <name>Clojars repository</name>
+      <url>https://clojars.org/repo</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/users/grfn/bbbg/resources/base.css b/users/grfn/bbbg/resources/base.css
new file mode 100644
index 0000000000..c86c3f24f0
--- /dev/null
+++ b/users/grfn/bbbg/resources/base.css
@@ -0,0 +1,152 @@
+/* montserrat-italic - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: italic;
+  font-weight: 400;
+  src: local("Montserrat Italic"), local("Montserrat-Italic"),
+    url("/fonts/montserrat-v15-latin-italic.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* montserrat-regular - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: normal;
+  font-weight: 400;
+  src: local("Montserrat Regular"), local("Montserrat-Regular"),
+    url("/fonts/montserrat-v15-latin-regular.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* montserrat-500 - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: normal;
+  font-weight: 500;
+  src: local("Montserrat Medium"), local("Montserrat-Medium"),
+    url("/fonts/montserrat-v15-latin-500.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* montserrat-500italic - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: italic;
+  font-weight: 500;
+  src: local("Montserrat Medium Italic"), local("Montserrat-MediumItalic"),
+    url("/fonts/montserrat-v15-latin-500italic.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-500italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* montserrat-600 - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: normal;
+  font-weight: 600;
+  src: local("Montserrat SemiBold"), local("Montserrat-SemiBold"),
+    url("/fonts/montserrat-v15-latin-600.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* montserrat-800 - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: normal;
+  font-weight: 800;
+  src: local("Montserrat ExtraBold"), local("Montserrat-ExtraBold"),
+    url("/fonts/montserrat-v15-latin-800.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* montserrat-800italic - latin */
+@font-face {
+  font-family: "Montserrat";
+  font-style: italic;
+  font-weight: 800;
+  src: local("Montserrat ExtraBold Italic"), local("Montserrat-ExtraBoldItalic"),
+    url("/fonts/montserrat-v15-latin-800italic.woff2") format("woff2"),
+    /* Chrome 26+, Opera 23+, Firefox 39+ */
+      url("/fonts/montserrat-v15-latin-800italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+body {
+  width: 100%;
+  font-family: "Montserrat", Helvetica, sans-serif;
+  margin: 0;
+  box-sizing: border-box;
+}
+
+*,
+::before,
+::after {
+  box-sizing: border-box;
+}
+
+ul,
+ol {
+  padding: 0;
+}
+
+body,
+h1,
+h2,
+h3,
+h4,
+p,
+ul,
+ol,
+li,
+figure,
+figcaption,
+blockquote,
+dl,
+dd {
+  margin: 0;
+}
+
+body {
+  min-height: 100vh;
+  scroll-behavior: smooth;
+  text-rendering: optimizeSpeed;
+  line-height: 1.5;
+}
+
+ul[class],
+ol[class] {
+  list-style: none;
+}
+
+a:not([class]) {
+  text-decoration-skip-ink: auto;
+}
+
+img {
+  max-width: 100%;
+  display: block;
+}
+
+article > * + * {
+  margin-top: 1em;
+}
+
+input,
+button,
+textarea,
+select {
+  font: inherit;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  * {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+    scroll-behavior: auto !important;
+  }
+}
diff --git a/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.down.sql b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.down.sql
new file mode 100644
index 0000000000..69b818a4f4
--- /dev/null
+++ b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.down.sql
@@ -0,0 +1,14 @@
+drop table "public"."user";
+
+-- ;;
+
+drop table "public"."event_attendee";
+
+
+-- ;;
+
+drop table "public"."event";
+
+-- ;;
+
+drop table "public"."attendee";
diff --git a/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql
new file mode 100644
index 0000000000..9718d84748
--- /dev/null
+++ b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql
@@ -0,0 +1,32 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+-- ;;
+CREATE TABLE "attendee" (
+    "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
+    "meetup_name" TEXT NOT NULL,
+    "discord_name" TEXT,
+    "meetup_user_id" TEXT,
+    "organizer_notes" TEXT NOT NULL DEFAULT '',
+    "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
+);
+-- ;;
+CREATE TABLE "event" (
+    "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
+    "date" DATE NOT NULL,
+    "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
+);
+-- ;;
+CREATE TABLE "event_attendee" (
+    "event_id" UUID NOT NULL REFERENCES "event" ("id"),
+    "attendee_id" UUID NOT NULL REFERENCES "attendee" ("id"),
+    "rsvpd_attending" BOOL,
+    "attended" BOOL,
+    "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
+    PRIMARY KEY ("event_id", "attendee_id")
+);
+-- ;;
+CREATE TABLE "user" (
+    "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
+    "username" TEXT NOT NULL,
+    "discord_user_id" TEXT NOT NULL,
+    "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
+);
diff --git a/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql
new file mode 100644
index 0000000000..936abf6c7d
--- /dev/null
+++ b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql
@@ -0,0 +1 @@
+DROP TABLE "attendee_check";
diff --git a/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql
new file mode 100644
index 0000000000..5e82dcb171
--- /dev/null
+++ b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql
@@ -0,0 +1,7 @@
+CREATE TABLE attendee_check (
+    "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
+    "attendee_id" UUID NOT NULL REFERENCES attendee ("id"),
+    "user_id" UUID NOT NULL REFERENCES "public"."user" ("id"),
+    "last_dose_at" DATE,
+    "checked_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
+);
diff --git a/users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql b/users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql
new file mode 100644
index 0000000000..cbee0c00ac
--- /dev/null
+++ b/users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql
@@ -0,0 +1 @@
+drop index attendee_uniq_meetup_user_id;
diff --git a/users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql b/users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql
new file mode 100644
index 0000000000..5895cad56b
--- /dev/null
+++ b/users/grfn/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql
@@ -0,0 +1,2 @@
+create unique index "attendee_uniq_meetup_user_id" on attendee (meetup_user_id);
+-- ;;
diff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff
new file mode 100644
index 0000000000..1c83d8518d
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2
new file mode 100644
index 0000000000..9dc5c7f158
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff
new file mode 100644
index 0000000000..71476d858f
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2
new file mode 100644
index 0000000000..0fb9838c9d
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff
new file mode 100644
index 0000000000..e7f8a31ba3
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2
new file mode 100644
index 0000000000..29cc1a9734
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff
new file mode 100644
index 0000000000..79203dd780
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2
new file mode 100644
index 0000000000..0abb707aed
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff
new file mode 100644
index 0000000000..65415571a7
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2
new file mode 100644
index 0000000000..674e6eabe7
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff
new file mode 100644
index 0000000000..67f1e85379
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2
new file mode 100644
index 0000000000..469aede09c
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff
new file mode 100644
index 0000000000..676a065e24
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff
Binary files differdiff --git a/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2 b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2
new file mode 100644
index 0000000000..70788c2732
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2
Binary files differdiff --git a/users/grfn/bbbg/resources/public/main.js b/users/grfn/bbbg/resources/public/main.js
new file mode 100644
index 0000000000..87c0b64d0a
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/main.js
@@ -0,0 +1,73 @@
+window.onload = () => {
+  const input = document.getElementById("name-autocomplete");
+  if (input != null) {
+    const attendeeList = document.getElementById("attendees-list");
+    const filterAttendees = (filter) => {
+      if (filter == "") {
+        for (let elt of attendeeList.querySelectorAll("li")) {
+          elt.classList.remove("hidden");
+        }
+
+        return;
+      }
+
+      let re = "";
+      for (let c of filter) {
+        re += `${c}.*`;
+      }
+      let filterRe = new RegExp(re, "i");
+
+      for (let elt of attendeeList.querySelectorAll("li")) {
+        const attendee = JSON.parse(elt.dataset.attendee);
+        if (attendee["bbbg.attendee/meetup-name"].match(filterRe) == null) {
+          elt.classList.add("hidden");
+        } else {
+          elt.classList.remove("hidden");
+        }
+      }
+    };
+
+    const attendeeIDInput = document.getElementById("attendee-id");
+    const submit = document.querySelector("#submit-button");
+    const signupForm = document.getElementById("signup-form");
+
+    input.oninput = (e) => {
+      filterAttendees(e.target.value);
+      attendeeIDInput.value = null;
+      submit.classList.add("hidden");
+      submit.setAttribute("disabled", "disabled");
+      signupForm.setAttribute("disabled", "disabled");
+    };
+
+    attendeeList.addEventListener("click", (e) => {
+      if (!(e.target instanceof HTMLLIElement)) {
+        return;
+      }
+      if (e.target.dataset.attendee == null) {
+        return;
+      }
+
+      const attendee = JSON.parse(e.target.dataset.attendee);
+      input.value = attendee["bbbg.attendee/meetup-name"];
+      attendeeIDInput.value = attendee["bbbg.attendee/id"];
+
+      submit.classList.remove("hidden");
+      submit.removeAttribute("disabled");
+      signupForm.removeAttribute("disabled");
+    });
+  }
+
+  document.querySelectorAll("form").forEach((form) => {
+    form.addEventListener("submit", (e) => {
+      if (e.target.attributes.disabled) {
+        e.preventDefault();
+      }
+
+      const confirmMessage = e.target.dataset.confirm;
+      if (confirmMessage != null && !confirm(confirmMessage)) {
+        e.stopImmediatePropagation();
+        e.preventDefault();
+      }
+    });
+  });
+};
diff --git a/users/grfn/bbbg/resources/public/robots.txt b/users/grfn/bbbg/resources/public/robots.txt
new file mode 100644
index 0000000000..1f53798bb4
--- /dev/null
+++ b/users/grfn/bbbg/resources/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/users/grfn/bbbg/shell.nix b/users/grfn/bbbg/shell.nix
new file mode 100644
index 0000000000..e26569657f
--- /dev/null
+++ b/users/grfn/bbbg/shell.nix
@@ -0,0 +1,29 @@
+let
+  depot = import ../../.. { };
+in
+with depot.third_party.nixpkgs;
+
+mkShell {
+  buildInputs = [
+    arion
+    depot.third_party.clj2nix
+    clojure
+    openjdk11_headless
+    postgresql_12
+    nix-prefetch-git
+    (writeShellScriptBin "terraform" ''
+      set -e
+      module=$(nix-build ~/code/depot -A users.grfn.bbbg.tf.module)
+      rm -f ~/tfstate/bbbg/*.json
+      cp ''${module}/*.json ~/tfstate/bbbg
+      exec ${depot.users.grfn.bbbg.tf.terraform}/bin/terraform \
+        -chdir=/home/grfn/tfstate/bbbg \
+        "$@"
+    '')
+  ];
+
+  PGHOST = "localhost";
+  PGUSER = "bbbg";
+  PGDATABASE = "bbbg";
+  PGPASSWORD = "password";
+}
diff --git a/users/grfn/bbbg/src/bbbg/attendee.clj b/users/grfn/bbbg/src/bbbg/attendee.clj
new file mode 100644
index 0000000000..49a6d621de
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/attendee.clj
@@ -0,0 +1,10 @@
+(ns bbbg.attendee
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
+
+(s/def ::meetup-name (s/and string? seq))
+
+(s/def ::discord-name (s/nilable string?))
+
+(s/def ::organizer-notes string?)
diff --git a/users/grfn/bbbg/src/bbbg/attendee_check.clj b/users/grfn/bbbg/src/bbbg/attendee_check.clj
new file mode 100644
index 0000000000..f34c41198e
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/attendee_check.clj
@@ -0,0 +1,4 @@
+(ns bbbg.attendee-check
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
diff --git a/users/grfn/bbbg/src/bbbg/core.clj b/users/grfn/bbbg/src/bbbg/core.clj
new file mode 100644
index 0000000000..632774d5cd
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/core.clj
@@ -0,0 +1,69 @@
+(ns bbbg.core
+  (:gen-class)
+  (:require
+   [bbbg.db :as db]
+   [bbbg.web :as web]
+   [clojure.spec.alpha :as s]
+   [clojure.spec.test.alpha :as stest]
+   [com.stuartsierra.component :as component]
+   [expound.alpha :as exp]))
+
+(s/def ::config
+  (s/merge
+   ::db/config
+   ::web/config))
+
+(defn make-system [config]
+  (component/system-map
+   :db (db/make-database config)
+   :web (web/make-server config)))
+
+(defn env->config []
+  (s/assert
+   ::config
+   (merge
+    (db/env->config)
+    (web/env->config))))
+
+(defn dev-config []
+  (s/assert
+   ::config
+   (merge
+    (db/dev-config)
+    (web/dev-config))))
+
+(defonce system nil)
+
+(defn init-dev []
+  (s/check-asserts true)
+  (set! s/*explain-out* exp/printer)
+  (stest/instrument))
+
+(defn run-dev []
+  (init-dev)
+  (alter-var-root
+   #'system
+   (fn [sys]
+     (when sys
+       (component/start sys))
+     (component/start (make-system (dev-config))))))
+
+(defn -main [& _args]
+  (alter-var-root
+   #'system
+   (constantly (component/start (make-system (env->config))))))
+
+(comment
+  ;; To run the application:
+  ;; 1. `M-x cider-jack-in`
+  ;; 2. `M-x cider-load-buffer` in this buffer
+  ;; 3. (optionally) configure the secrets backend in `bbbg.util.dev-secrets`
+  ;; 4. Put your cursor after the following form and run `M-x cider-eval-last-sexp`
+  ;;
+  ;; A web server will be listening on http://localhost:8888
+
+  (do
+    (run-dev)
+    (bbbg.db/migrate! (:db system)))
+
+  )
diff --git a/users/grfn/bbbg/src/bbbg/db.clj b/users/grfn/bbbg/src/bbbg/db.clj
new file mode 100644
index 0000000000..5bbf88925a
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db.clj
@@ -0,0 +1,366 @@
+(ns bbbg.db
+  (:gen-class)
+  (:refer-clojure :exclude [get list count])
+  (:require [camel-snake-kebab.core :as csk :refer [->kebab-case ->snake_case]]
+            [bbbg.util.core :as u]
+            [clojure.set :as set]
+            [clojure.spec.alpha :as s]
+            [clojure.string :as str]
+            [com.stuartsierra.component :as component]
+            [config.core :refer [env]]
+            [honeysql.format :as hformat]
+            [migratus.core :as migratus]
+            [next.jdbc :as jdbc]
+            [next.jdbc.connection :as jdbc.conn]
+            next.jdbc.date-time
+            [next.jdbc.optional :as jdbc.opt]
+            [next.jdbc.result-set :as rs]
+            [next.jdbc.sql :as sql])
+  (:import [com.impossibl.postgres.jdbc PGSQLSimpleException]
+           com.zaxxer.hikari.HikariDataSource
+           [java.sql Connection ResultSet Types]
+           javax.sql.DataSource))
+
+(s/def ::host string?)
+(s/def ::database string?)
+(s/def ::user string?)
+(s/def ::password string?)
+
+(s/def ::config
+  (s/keys :opt [::host
+                ::database
+                ::user
+                ::password]))
+
+(s/fdef make-database
+  :args
+  (s/cat :config (s/keys :opt [::config])))
+
+(s/fdef env->config :ret ::config)
+
+(s/def ::db any?)
+
+;;;
+
+(def default-config
+  (s/assert
+   ::config
+   {::host "localhost"
+    ::database "bbbg"
+    ::user "bbbg"
+    ::password "password"}))
+
+(defn dev-config [] default-config)
+
+(defn env->config []
+  (->>
+   {::host (:pghost env)
+    ::database (:pgdatabase env)
+    ::user (:pguser env)
+    ::password (:pgpassword env)}
+   u/remove-nils
+   (s/assert ::config)))
+
+(defn ->db-spec [config]
+  (-> default-config
+      (merge config)
+      (set/rename-keys
+       {::host :host
+        ::database :dbname
+        ::user :username
+        ::password :password})
+      (assoc :dbtype "pgsql")))
+
+(defn connection
+  "Make a one-off connection from the given `::config` map, or the environment
+  if not provided"
+  ([] (connection (env->config)))
+  ([config]
+   (-> config
+       ->db-spec
+       (set/rename-keys {:username :user})
+       jdbc/get-datasource
+       jdbc/get-connection)))
+
+(defrecord Database [config]
+  component/Lifecycle
+  (start [this]
+    (assoc this :pool (jdbc.conn/->pool HikariDataSource (->db-spec config))))
+  (stop [this]
+    (some-> this :pool .close)
+    (dissoc this :pool))
+
+  clojure.lang.IFn
+  (invoke [this] (:pool this)))
+
+(defn make-database [config]
+  (map->Database {:config config}))
+
+(defn database? [x]
+  (or
+   (instance? Database x)
+   (and (map? x) (contains? x :pool))))
+
+;;;
+;;; Migrations
+;;;
+
+(defn migratus-config
+  [db]
+  {:store :database
+   :migration-dir "migrations/"
+   :migration-table-name "__migrations__"
+   :db
+   (let [db (if (ifn? db) (db) db)]
+     (cond
+       (.isInstance Connection db)
+       {:connection db}
+       (.isInstance DataSource db)
+       {:datasource db}
+       :else (throw
+              (ex-info "migratus-config called with value of unrecognized type"
+                       {:value db}))))})
+
+(defn generate-migration
+  ([db name] (generate-migration db name :sql))
+  ([db name type] (migratus/create (migratus-config db) name type)))
+
+(defn migrate!
+  [db] (migratus/migrate (migratus-config db)))
+
+(defn rollback!
+  [db] (migratus/rollback (migratus-config db)))
+
+;;;
+;;; Database interaction
+;;;
+
+(defn ->key-ns [tn]
+  (let [tn (name tn)
+        tn (if (str/starts-with? tn "public.")
+             (second (str/split tn #"\." 2))
+             tn)]
+    (str "bbbg." (->kebab-case tn))))
+
+(defn ->table-name [kns]
+  (let [kns (name kns)]
+    (->snake_case
+     (if (str/starts-with? kns "public.")
+       kns
+       (str "public." (last (str/split kns #"\.")))))))
+
+(defn ->column
+  ([col] (->column nil col))
+  ([table col]
+   (let [col-table (some-> col namespace ->table-name)
+         snake-col (-> col name ->snake_case (str/replace #"\?$" ""))]
+     (if (or (not (namespace col))
+             (not table)
+             (= (->table-name table) col-table))
+       snake-col
+       ;; different table, assume fk
+       (str
+        (str/replace-first col-table "public." "")
+        "_"
+        snake-col)))))
+
+(defn ->value [v]
+  (if (keyword? v)
+    (-> v name csk/->snake_case_string)
+    v))
+
+(defn process-key-map [table key-map]
+  (into {}
+        (map (fn [[k v]] [(->column table k)
+                          (->value v)]))
+        key-map))
+
+(defn fkize [col]
+  (if (str/ends-with? col "-id")
+    (let [table (str/join "-" (butlast (str/split (name col) #"-")))]
+      (keyword (->key-ns table) "id"))
+    col))
+
+(def ^:private enum-members-cache (atom {}))
+(defn- enum-members
+  "Returns a set of enum members as strings for the enum with the given name"
+  [db name]
+  (if-let [e (find @enum-members-cache name)]
+    (val e)
+    (let [r (try
+              (-> (jdbc/execute-one!
+                   (db)
+                   [(format "select enum_range(null::%s) as members" name)])
+                  :members
+                  .getArray
+                  set)
+              (catch PGSQLSimpleException _
+                nil))]
+      (swap! enum-members-cache assoc name r)
+      r)))
+
+(def ^{:private true
+       :dynamic true}
+  *meta-db*
+  "Database connection to use to query metadata"
+  nil)
+
+(extend-protocol rs/ReadableColumn
+  String
+  (read-column-by-label [x _] x)
+  (read-column-by-index [x rsmeta idx]
+    (if-not *meta-db*
+      x
+      (let [typ (.getColumnTypeName rsmeta idx)]
+        ;; TODO: Is there a better way to figure out if a type is an enum?
+        (if (enum-members *meta-db* typ)
+          (keyword (csk/->kebab-case-string typ)
+                   (csk/->kebab-case-string x))
+          x)))))
+
+(comment
+  (->key-ns :public.user)
+  (->key-ns :public.api-token)
+  (->key-ns :api-token)
+  (->table-name :api-token)
+  (->table-name :public.user)
+  (->table-name :bbbg.user)
+  )
+
+(defn as-fq-maps [^ResultSet rs _opts]
+  (let [qualify #(when (seq %) (str "bbbg." (->kebab-case %)))
+        rsmeta (.getMetaData rs)
+        cols (mapv
+              (fn [^Integer i]
+                (let [ty (.getColumnType rsmeta i)
+                      lab (.getColumnLabel rsmeta i)
+                      n (str (->kebab-case lab)
+                             (when (= ty Types/BOOLEAN) "?"))]
+                  (fkize
+                   (if-let [q (some-> rsmeta (.getTableName i) qualify not-empty)]
+                     (keyword q n)
+                     (keyword n)))))
+              (range 1 (inc (.getColumnCount rsmeta))))]
+    (jdbc.opt/->MapResultSetOptionalBuilder rs rsmeta cols)))
+
+(def jdbc-opts
+  {:builder-fn as-fq-maps
+   :column-fn ->snake_case
+   :table-fn ->snake_case})
+
+(defmethod hformat/fn-handler "count-distinct" [_ field]
+  (str "count(distinct " (hformat/to-sql field) ")"))
+
+(defn fetch
+  "Fetch a single row from the db matching the given `sql-map` or query"
+  [db sql-map & [opts]]
+  (s/assert
+   (s/nilable (s/keys))
+   (binding [*meta-db* db]
+     (jdbc/execute-one!
+      (db)
+      (if (map? sql-map)
+        (hformat/format sql-map)
+        sql-map)
+      (merge jdbc-opts opts)))))
+
+(defn get
+  "Retrieve a single record from the given table by ID"
+  [db table id & [opts]]
+  (when id
+    (fetch
+     db
+     {:select [:*]
+      :from [table]
+      :where [:= :id id]}
+     opts)))
+
+(defn list
+  "Returns a list of rows from the db matching the given sql-map, table or
+  query"
+  [db sql-map-or-table & [opts]]
+  (s/assert
+   (s/coll-of (s/keys))
+   (binding [*meta-db* db]
+     (jdbc/execute!
+      (db)
+      (cond
+        (map? sql-map-or-table)
+        (hformat/format sql-map-or-table)
+        (keyword? sql-map-or-table)
+        (hformat/format {:select [:*] :from [sql-map-or-table]})
+        :else
+        sql-map-or-table)
+      (merge jdbc-opts opts)))))
+
+(defn count
+  [db sql-map]
+  (binding [*meta-db* db]
+    (:count
+     (fetch db {:select [[:%count.* :count]], :from [[sql-map :sq]]}))))
+
+(defn exists?
+  "Returns true if the given sql query-map would return any results"
+  [db sql-map]
+  (binding [*meta-db* db]
+    (pos?
+     (count db sql-map))))
+
+(defn execute!
+  "Given a database and a honeysql query map, perform an operation on the
+  database and discard the results"
+  [db sql-map & [opts]]
+  (jdbc/execute!
+   (db)
+   (hformat/format sql-map)
+   (merge jdbc-opts opts)))
+
+(defn insert!
+  "Given a database, a table name, and a data hash map, inserts the
+  data as a single row in the database and attempts to return a map of generated
+  keys."
+  [db table key-map & [opts]]
+  (binding [*meta-db* db]
+    (sql/insert!
+     (db)
+     table
+     (process-key-map table key-map)
+     (merge jdbc-opts opts))))
+
+(defn update!
+  "Given a database, a table name, a hash map of columns and values
+  to set, and a honeysql predicate, perform an update on the table.
+  Will "
+  [db table key-map where-params & [opts]]
+  (binding [*meta-db* db]
+    (execute! db
+              {:update table
+               :set (u/map-keys keyword (process-key-map table key-map))
+               :where where-params
+               :returning [:id]}
+              opts)))
+
+(defn delete!
+  "Delete all rows from the given table matching the given where clause"
+  [db table where-clause]
+  (binding [*meta-db* db]
+    (sql/delete! (db) table (hformat/format-predicate where-clause))))
+
+(defmacro with-transaction [[sym db opts] & body]
+  `(jdbc/with-transaction
+     [tx# (~db) ~opts]
+     (let [~sym (constantly tx#)]
+       ~@body)))
+
+(defn -main [& args]
+  (let [db (component/start (make-database (env->config)))]
+    (case (first args)
+      "migrate" (migrate! db)
+      "rollback" (rollback! db))))
+
+(comment
+  (def db (:db bbbg.core/system))
+  (generate-migration db "add-attendee-unique-meetup-id")
+  (migrate! db)
+
+  )
diff --git a/users/grfn/bbbg/src/bbbg/db/attendee.clj b/users/grfn/bbbg/src/bbbg/db/attendee.clj
new file mode 100644
index 0000000000..da5ee29321
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db/attendee.clj
@@ -0,0 +1,85 @@
+(ns bbbg.db.attendee
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.db :as db]
+   [bbbg.util.sql :refer [count-where]]
+   honeysql-postgres.helpers
+   [honeysql.helpers
+    :refer
+    [merge-group-by merge-join merge-left-join merge-select merge-where]]
+   [bbbg.util.core :as u]))
+
+(defn search
+  ([q] (search {:select [:attendee.*] :from [:attendee]} q))
+  ([db-or-query q]
+   (if (db/database? db-or-query)
+     (db/list db-or-query (search q))
+     (cond-> db-or-query
+       q (merge-where
+          [:or
+           [:ilike :meetup_name (str "%" q "%")]
+           [:ilike :discord_name (str "%" q "%")]]))))
+  ([db query q]
+   (db/list db (search query q))))
+
+(defn for-event
+  ([event-id]
+   (for-event {:select [:attendee.*]
+               :from [:attendee]}
+              event-id))
+  ([db-or-query event-id]
+   (if (db/database? db-or-query)
+     (db/list db-or-query (for-event event-id))
+     (-> db-or-query
+         (merge-select :event-attendee.*)
+         (merge-join :event_attendee [:= :attendee.id :event_attendee.attendee_id])
+         (merge-where [:= :event_attendee.event_id event-id]))))
+  ([db query event-id]
+   (db/list db (for-event query event-id))))
+
+(defn with-stats
+  ([] (with-stats {:select [:attendee.*]
+                   :from [:attendee]}))
+  ([query]
+   (-> query
+       (merge-left-join :event_attendee [:= :attendee.id :event_attendee.attendee_id])
+       (merge-group-by :attendee.id)
+       (merge-select
+        [(count-where :event_attendee.rsvpd_attending) :events-rsvpd]
+        [(count-where :event_attendee.attended) :events-attended]
+        [(count-where [:and
+                       :event_attendee.rsvpd_attending
+                       [:not :event_attendee.attended]])
+         :no-shows]))))
+
+(defn upsert-all!
+  [db attendees]
+  (when (seq attendees)
+    (db/list
+     db
+     {:insert-into :attendee
+      :values (map #(->> %
+                         (db/process-key-map :attendee)
+                         (u/map-keys keyword))
+                   attendees)
+      :upsert {:on-conflict [:meetup-user-id]
+               :do-update-set [:meetup-name]}
+      :returning [:id :meetup-user-id]})))
+
+(comment
+  (def db (:db bbbg.core/system))
+  (db/database? db)
+  (search db "gri")
+  (db/insert! db :attendee {::attendee/meetup-name "Griffin Smith"
+                            ::attendee/discord-name "grfn"
+                            })
+
+  (search db (with-stats) "gri")
+
+  (search (with-stats) "gri")
+
+  (db/list db (with-stats))
+
+  (db/insert! db :attendee {::attendee/meetup-name "Rando Guy"
+                            ::attendee/discord-name "rando"})
+  )
diff --git a/users/grfn/bbbg/src/bbbg/db/attendee_check.clj b/users/grfn/bbbg/src/bbbg/db/attendee_check.clj
new file mode 100644
index 0000000000..492f786bd6
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db/attendee_check.clj
@@ -0,0 +1,55 @@
+(ns bbbg.db.attendee-check
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.attendee-check :as attendee-check]
+   [bbbg.db :as db]
+   [bbbg.user :as user]
+   [bbbg.util.core :as u]))
+
+(defn create! [db params]
+  (db/insert! db :attendee-check
+              (select-keys params [::attendee/id
+                                   ::user/id
+                                   ::attendee-check/last-dose-at])))
+
+(defn attendees-with-last-checks
+  [db attendees]
+  (when (seq attendees)
+    (let [ids (map ::attendee/id attendees)
+          checks
+          (db/list db {:select [:attendee-check.*]
+                       :from [:attendee-check]
+                       :join [[{:select [:%max.attendee-check.checked-at
+                                         :attendee-check.attendee-id]
+                                :from [:attendee-check]
+                                :group-by [:attendee-check.attendee-id]
+                                :where [:in :attendee-check.attendee-id ids]}
+                               :last-check]
+                              [:=
+                               :attendee-check.attendee-id
+                               :last-check.attendee-id]]})
+          users (if (seq checks)
+                  (u/key-by
+                   ::user/id
+                   (db/list db {:select [:public.user.*]
+                                :from [:public.user]
+                                :where [:in :id (map ::user/id checks)]}))
+                  {})
+          checks (map #(assoc % :user (users (::user/id %))) checks)
+          attendee-id->check (u/key-by ::attendee/id checks)]
+      (map #(assoc % :last-check (attendee-id->check (::attendee/id %)))
+           attendees))))
+
+(comment
+  (def db (:db bbbg.core/system))
+
+  (attendees-with-last-checks
+   db
+   (db/list db :attendee)
+   )
+
+  (db/insert! db :attendee-check
+              {::attendee/id #uuid "58bcd372-ff6e-49df-b280-23d24c5ba0f0"
+               ::user/id #uuid "303fb606-5ef0-4682-ad7d-6429c670cd78"
+               ::attendee-check/last-dose-at "2021-12-19"})
+  )
diff --git a/users/grfn/bbbg/src/bbbg/db/event.clj b/users/grfn/bbbg/src/bbbg/db/event.clj
new file mode 100644
index 0000000000..1b5a4e11ec
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db/event.clj
@@ -0,0 +1,94 @@
+(ns bbbg.db.event
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.db :as db]
+   [bbbg.event :as event]
+   [bbbg.util.sql :refer [count-where]]
+   [honeysql.helpers
+    :refer [merge-group-by merge-left-join merge-select merge-where]]
+   [java-time :refer [local-date local-date-time local-time]]))
+
+(defn create! [db event]
+  (db/insert! db :event (select-keys event [::event/date])))
+
+(defn attended!
+  [db params]
+  (db/execute!
+   db
+   {:insert-into :event-attendee
+    :values [{:event_id (::event/id params)
+              :attendee_id (::attendee/id params)
+              :attended true}]
+    :upsert {:on-conflict [:event-id :attendee-id]
+             :do-update-set! {:attended true}}}))
+
+(defn on-day
+  ([day] {:select [:event.*]
+          :from [:event]
+          :where [:= :date (str day)]})
+  ([db day]
+   (db/list db (on-day day))))
+
+
+(def end-of-day-hour
+  ;; 7am utc = 3am nyc
+  7)
+
+(defn current-day
+  ([] (current-day (local-date-time)))
+  ([dt]
+   (if (<= 0
+           (.getHour (local-time dt))
+           end-of-day-hour)
+     (java-time/minus
+      (local-date dt)
+      (java-time/days 1))
+     (local-date dt))))
+
+(comment
+  (current-day
+   (local-date-time
+    2022 5 1
+    1 13 0))
+  )
+
+(defn today
+  ([] (on-day (current-day)))
+  ([db] (db/list db (today))))
+
+(defn upcoming
+  ([] (upcoming {:select [:event.*] :from [:event]}))
+  ([query]
+   (merge-where query [:>= :date (local-date)])))
+
+(defn past
+  ([] (past {:select [:event.*] :from [:event]}))
+  ([query]
+   (merge-where query [:< :date (local-date)])))
+
+(defn with-attendee-counts
+  [query]
+  (-> query
+      (merge-left-join :event_attendee [:= :event.id :event_attendee.event-id])
+      (merge-select :%count.event_attendee.attendee_id)
+      (merge-group-by :event.id :event_attendee.event-id)))
+
+(defn with-stats
+  [query]
+  (-> query
+      (merge-left-join :event_attendee [:= :event.id :event_attendee.event-id])
+      (merge-select
+       [(count-where :event-attendee.rsvpd_attending) :num-rsvps]
+       [(count-where :event-attendee.attended) :num-attendees])
+      (merge-group-by :event.id)))
+
+(comment
+  (def db (:db bbbg.core/system))
+  (db/list db (-> (today) (with-attendee-counts)))
+
+  (honeysql.format/format
+   (honeysql-postgres.helpers/upsert {:insert-into :foo
+                                      :values {:bar 1}}
+                                     (-> (honeysql-postgres.helpers/on-conflict :did)
+                                         (honeysql-postgres.helpers/do-update-set! [:did true]))))
+  )
diff --git a/users/grfn/bbbg/src/bbbg/db/event_attendee.clj b/users/grfn/bbbg/src/bbbg/db/event_attendee.clj
new file mode 100644
index 0000000000..31411e5d45
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db/event_attendee.clj
@@ -0,0 +1,17 @@
+(ns bbbg.db.event-attendee
+  (:require honeysql-postgres.format
+            [bbbg.db :as db]
+            [bbbg.util.core :as u]))
+
+(defn upsert-all!
+  [db attendees]
+  (when (seq attendees)
+    (db/execute!
+     db
+     {:insert-into :event-attendee
+      :values (map #(->> %
+                         (db/process-key-map :event-attendee)
+                         (u/map-keys keyword))
+                   attendees)
+      :upsert {:on-conflict [:event-id :attendee-id]
+               :do-update-set [:rsvpd-attending]}})))
diff --git a/users/grfn/bbbg/src/bbbg/db/user.clj b/users/grfn/bbbg/src/bbbg/db/user.clj
new file mode 100644
index 0000000000..700105ef63
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db/user.clj
@@ -0,0 +1,19 @@
+(ns bbbg.db.user
+  (:require [bbbg.db :as db]
+            [bbbg.user :as user]))
+
+(defn create! [db attrs]
+  (db/insert! db
+              :public.user
+              (select-keys attrs [::user/id
+                                  ::user/username
+                                  ::user/discord-user-id])))
+
+(defn find-or-create! [db attrs]
+  (or
+   (db/fetch db {:select [:*]
+                 :from [:public.user]
+                 :where [:=
+                         :discord-user-id
+                         (::user/discord-user-id attrs)]})
+   (create! db attrs)))
diff --git a/users/grfn/bbbg/src/bbbg/discord.clj b/users/grfn/bbbg/src/bbbg/discord.clj
new file mode 100644
index 0000000000..e854ec1d14
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/discord.clj
@@ -0,0 +1,44 @@
+(ns bbbg.discord
+  (:refer-clojure :exclude [get])
+  (:require
+   [bbbg.util.dev-secrets :refer [secret]]
+   [clj-http.client :as http]
+   [clojure.string :as str]))
+
+(def base-uri "https://discord.com/api")
+
+(defn api-uri [path]
+  (str base-uri
+       (when-not (str/starts-with? path "/") "/")
+       path))
+
+(defn get
+  ([token path]
+   (get token path {}))
+  ([token path params]
+   (:body
+    (http/get (api-uri path)
+              (-> params
+                  (assoc :accept :json
+                         :as :json)
+                  (assoc-in [:headers "authorization"]
+                            (str "Bearer " (:token token))))))))
+
+(defn me [token]
+  (get token "/users/@me"))
+
+(defn guilds [token]
+  (get token "/users/@me/guilds"))
+
+(defn guild-member [token guild-id]
+  (get token (str "/users/@me/guilds/" guild-id "/member")))
+
+(comment
+  (def token {:token (secret "bbbg/test-token")})
+  (me token)
+  (guilds token)
+  (guild-member token "841295283564052510")
+
+  (get token "/guilds/841295283564052510/roles")
+
+  )
diff --git a/users/grfn/bbbg/src/bbbg/discord/auth.clj b/users/grfn/bbbg/src/bbbg/discord/auth.clj
new file mode 100644
index 0000000000..35bc580e39
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/discord/auth.clj
@@ -0,0 +1,90 @@
+(ns bbbg.discord.auth
+  (:require
+   [bbbg.discord :as discord]
+   [bbbg.util.core :as u]
+   [bbbg.util.dev-secrets :refer [secret]]
+   clj-time.coerce
+   [clojure.spec.alpha :as s]
+   [config.core :refer [env]]
+   [ring.middleware.oauth2 :refer [wrap-oauth2]]))
+
+(s/def ::client-id string?)
+(s/def ::client-secret string?)
+(s/def ::bbbg-guild-id string?)
+(s/def ::bbbg-organizer-role string?)
+
+(s/def ::config (s/keys :req [::client-id
+                              ::client-secret
+                              ::bbbg-guild-id
+                              ::bbbg-organizer-role]))
+
+;;;
+
+(defn env->config []
+  (s/assert
+   ::config
+   {::client-id (:discord-client-id env)
+    ::client-secret (:discord-client-secret env)
+    ::bbbg-guild-id (:bbbg-guild-id env "841295283564052510")
+    ::bbbg-organizer-role (:bbbg-organizer-role
+                           env
+                           ;; TODO this might not be the right id
+                           "908428000817725470")}))
+
+(defn dev-config []
+  (s/assert
+   ::config
+   {::client-id (secret "bbbg/discord-client-id")
+    ::client-secret (secret "bbbg/discord-client-secret")
+    ::bbbg-guild-id "841295283564052510"
+    ::bbbg-organizer-role "908428000817725470"}))
+
+;;;
+
+(def access-token-url
+  "https://discord.com/api/oauth2/token")
+
+(def authorization-url
+  "https://discord.com/api/oauth2/authorize")
+
+(def revoke-url
+  "https://discord.com/api/oauth2/token/revoke")
+
+(def scopes ["guilds"
+             "guilds.members.read"
+             "identify"])
+
+(defn discord-oauth-profile [{:keys [base-url] :as env}]
+  {:authorize-uri authorization-url
+   :access-token-uri access-token-url
+   :client-id (::client-id env)
+   :client-secret (::client-secret env)
+   :scopes scopes
+   :launch-uri "/auth/discord"
+   :redirect-uri (str base-url "/auth/discord/redirect")
+   :landing-uri (str base-url "/auth/success")})
+
+(comment
+  (-> "https://bbbg-staging.gws.fyi/auth/login"
+      (java.net.URI/create)
+      (.resolve "https://bbbg.gws.fyi/auth/discord/redirect")
+      str)
+  )
+
+(defn wrap-discord-auth [handler env]
+  (wrap-oauth2 handler {:discord (discord-oauth-profile env)}))
+
+(defn check-discord-auth
+  "Check that the user with the given token has the correct level of discord
+  auth"
+  [{::keys [bbbg-guild-id bbbg-organizer-role]} token]
+  (and (some (comp #{bbbg-guild-id} :id)
+             (discord/guilds token))
+       (some #{bbbg-organizer-role}
+             (:roles (discord/guild-member token bbbg-guild-id)))))
+
+(comment
+  (#'ring.middleware.oauth2/valid-profile?
+   (discord-oauth-profile
+    (dev-config)))
+  )
diff --git a/users/grfn/bbbg/src/bbbg/event.clj b/users/grfn/bbbg/src/bbbg/event.clj
new file mode 100644
index 0000000000..aa0578f354
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/event.clj
@@ -0,0 +1,4 @@
+(ns bbbg.event
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
diff --git a/users/grfn/bbbg/src/bbbg/event_attendee.clj b/users/grfn/bbbg/src/bbbg/event_attendee.clj
new file mode 100644
index 0000000000..7b6b4c2764
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/event_attendee.clj
@@ -0,0 +1,6 @@
+(ns bbbg.event-attendee
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::attended? boolean?)
+
+(s/def ::rsvpd-attending? boolean?)
diff --git a/users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj b/users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj
new file mode 100644
index 0000000000..d7307c4067
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj
@@ -0,0 +1,68 @@
+(ns bbbg.handlers.attendee-checks
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.attendee-check :as attendee-check]
+   [bbbg.db :as db]
+   [bbbg.db.attendee-check :as db.attendee-check]
+   [bbbg.handlers.core :refer [page-response wrap-auth-required]]
+   [bbbg.user :as user]
+   [bbbg.util.display :refer [format-date]]
+   [compojure.coercions :refer [as-uuid]]
+   [compojure.core :refer [context GET POST]]
+   [ring.util.response :refer [not-found redirect]]
+   [bbbg.views.flash :as flash]))
+
+(defn- edit-attendee-checks-page [{:keys [existing-check]
+                                   attendee-id ::attendee/id}]
+  [:div.page
+   (when existing-check
+     [:p
+      "Already checked on "
+      (-> existing-check ::attendee-check/checked-at format-date)
+      " by "
+      (::user/username existing-check)])
+   [:form.attendee-checks-form
+    {:method :post
+     :action (str "/attendees/" attendee-id "/checks")}
+    [:div.form-group
+     [:label
+      "Last Dose"
+      [:input {:type :date
+               :name :last-dose-at}]]]
+    [:div.form-group
+     [:input {:type :submit
+              :value "Mark Checked"}]]]])
+
+(defn attendee-checks-routes [{:keys [db]}]
+  (wrap-auth-required
+   (context "/attendees/:attendee-id/checks" [attendee-id :<< as-uuid]
+     (GET "/edit" []
+       (if (db/exists? db {:select [1]
+                           :from [:attendee]
+                           :where [:= :id attendee-id]})
+         (let [existing-check (db/fetch
+                               db
+                               {:select [:attendee-check.*
+                                         :public.user.*]
+                                :from [:attendee-check]
+                                :join [:public.user
+                                       [:=
+                                        :attendee-check.user-id
+                                        :public.user.id]]
+                                :where [:= :attendee-id attendee-id]})]
+           (page-response
+            (edit-attendee-checks-page
+             {:existing-check existing-check
+              ::attendee/id attendee-id})))
+         (not-found "Attendee not found")))
+     (POST "/" {{:keys [last-dose-at]} :params
+                {user-id ::user/id} :session}
+       (db.attendee-check/create!
+        db
+        {::attendee/id attendee-id
+         ::user/id user-id
+         ::attendee-check/last-dose-at last-dose-at})
+       (-> (redirect "/attendees")
+           (flash/add-flash
+            #:flash{:type :success
+                    :message "Successfully updated vaccination status"}))))))
diff --git a/users/grfn/bbbg/src/bbbg/handlers/attendees.clj b/users/grfn/bbbg/src/bbbg/handlers/attendees.clj
new file mode 100644
index 0000000000..ce84b88e97
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/handlers/attendees.clj
@@ -0,0 +1,162 @@
+(ns bbbg.handlers.attendees
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.attendee-check :as attendee-check]
+   [bbbg.db :as db]
+   [bbbg.db.attendee :as db.attendee]
+   [bbbg.db.attendee-check :as db.attendee-check]
+   [bbbg.db.event :as db.event]
+   [bbbg.event :as event]
+   [bbbg.handlers.core :refer [page-response wrap-auth-required]]
+   [bbbg.user :as user]
+   [bbbg.util.display :refer [format-date]]
+   [bbbg.views.flash :as flash]
+   [cheshire.core :as json]
+   [compojure.coercions :refer [as-uuid]]
+   [compojure.core :refer [GET POST routes]]
+   [honeysql.helpers :refer [merge-where]]
+   [ring.util.response :refer [content-type not-found redirect response]])
+  (:import
+   java.util.UUID))
+
+(defn- attendees-page [{:keys [attendees q edit-notes]}]
+  [:div.page
+   [:form.search-form {:method :get :action "/attendees"}
+    [:input.search-input
+     {:type "search"
+      :name "q"
+      :value q
+      :title "Search Attendees"}]
+    [:input {:type "submit"
+             :value "Search Attendees"}]]
+   [:table.attendees
+    [:thead
+     [:tr
+      [:th "Meetup Name"]
+      [:th "Discord Name"]
+      [:th "Events RSVPd"]
+      [:th "Events Attended"]
+      [:th "No-Shows"]
+      [:th "Last Vaccination Check"]
+      [:th "Notes"]]]
+    [:tbody
+     (for [attendee (sort-by
+                     (comp #{edit-notes} ::attendee/id)
+                     (comp - compare)
+                     attendees)
+           :let [id (::attendee/id attendee)]]
+       [:tr
+        [:td.attendee-name (::attendee/meetup-name attendee)]
+        [:td
+         [:label.mobile-label "Discord Name: "]
+         (or (not-empty (::attendee/discord-name attendee))
+             "—")]
+        [:td
+         [:label.mobile-label "Events RSVPd: "]
+         (:events-rsvpd attendee)]
+        [:td
+         [:label.mobile-label "Events Attended: "]
+         (:events-attended attendee)]
+        [:td
+         [:label.mobile-label "No-shows: "]
+         (:no-shows attendee)]
+        [:td
+         [:label.mobile-label "Last Vaccination Check: "]
+         (if-let [last-check (:last-check attendee)]
+           (str "✔️ "(-> last-check
+                        ::attendee-check/checked-at
+                        format-date)
+                ", by "
+                (get-in last-check [:user ::user/username]))
+           (list
+            [:span {:title "Not Checked"}
+             "❌"]
+            " "
+            [:a {:href (str "/attendees/" id "/checks/edit")}
+             "Edit"] ))]
+        (if (= edit-notes id)
+          [:td
+           [:form.organizer-notes {:method :post
+                                   :action (str "/attendees/" id "/notes")}
+            [:div.form-group
+             [:input {:type :text :name "notes"
+                      :value (::attendee/organizer-notes attendee)
+                      :autofocus true}]]
+            [:div.form-group
+             [:input {:type "Submit" :value "Save Notes"}]]]]
+          [:td
+           [:p
+            (::attendee/organizer-notes attendee)]
+           [:p
+            [:a {:href (str "/attendees?edit-notes=" id)}
+             "Edit Notes"]]])])]]])
+
+(defn attendees-routes [{:keys [db]}]
+  (routes
+   (wrap-auth-required
+    (routes
+     (GET "/attendees" [q edit-notes]
+       (let [attendees (db/list db (cond-> (db.attendee/with-stats)
+                                     q (db.attendee/search q)))
+             attendees (db.attendee-check/attendees-with-last-checks
+                        db
+                        attendees)
+             edit-notes (some-> edit-notes UUID/fromString)]
+         (page-response (attendees-page {:attendees attendees
+                                         :q q
+                                         :edit-notes edit-notes}))))
+
+     (POST "/attendees/:id/notes" [id :<< as-uuid notes]
+       (if (seq (db/update! db
+                            :attendee
+                            {::attendee/organizer-notes notes}
+                            [:= :id id]))
+         (-> (redirect "/attendees")
+             (flash/add-flash
+              #:flash{:type :success
+                      :message "Notes updated successfully"}))
+         (not-found "Attendee not found")))))
+
+   (GET "/attendees.json" [q event_id attended]
+     (let [results
+           (db/list
+            db
+            (cond->
+                (if q
+                  (db.attendee/search q)
+                  {:select [:attendee.*] :from [:attendee]})
+                event_id (db.attendee/for-event event_id)
+                (some? attended)
+                (merge-where
+                 (case attended
+                   "true" :attended
+                   "false" [:or [:= :attended nil] [:not :attended]]))))]
+       (-> {:results results}
+           json/generate-string
+           response
+           (content-type "application/json"))))
+
+   (POST "/event_attendees" [event_id attendee_id]
+     (if (and (db/exists? db {:select [:id] :from [:event] :where [:= :id event_id]})
+              (db/exists? db {:select [:id] :from [:attendee] :where [:= :id attendee_id]}))
+       (do
+         (db.event/attended! db {::event/id event_id
+                                 ::attendee/id attendee_id})
+         (-> (redirect (str "/signup-forms/" event_id))
+             (flash/add-flash
+              #:flash{:type :success
+                      :message "Thank you for signing in! Enjoy the event."})))
+       (response "Something went wrong")))))
+
+(comment
+  (def db (:db bbbg.core/system))
+  (db/list db :attendee)
+  (db/list db
+           (->
+            (db.attendee/search "gr")
+            (db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf")))
+  (honeysql.format/format
+   (->
+    (db.attendee/search "gr")
+    (db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf")))
+  )
diff --git a/users/grfn/bbbg/src/bbbg/handlers/core.clj b/users/grfn/bbbg/src/bbbg/handlers/core.clj
new file mode 100644
index 0000000000..caa679ee87
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/handlers/core.clj
@@ -0,0 +1,91 @@
+(ns bbbg.handlers.core
+  (:require
+   [bbbg.user :as user]
+   [bbbg.views.flash :as flash]
+   [hiccup.core :refer [html]]
+   [ring.util.response :refer [content-type response]]
+   [clojure.string :as str]))
+
+(def ^:dynamic *authenticated?* false)
+
+(defn authenticated? [request]
+  (some? (get-in request [:session ::user/id])))
+
+(defn wrap-auth-required [handler]
+  (fn [req]
+    (when (authenticated? req)
+      (handler req))))
+
+(defn wrap-dynamic-auth [handler]
+  (fn [req]
+    (binding [*authenticated?* (authenticated? req)]
+      (handler req))))
+
+(def ^:dynamic *current-uri*)
+
+(defn wrap-current-uri [handler]
+  (fn [req]
+    (binding [*current-uri* (:uri req)]
+      (handler req))))
+
+(defn nav-item [href label]
+  (let [active?
+        (when *current-uri*
+          (str/starts-with?
+           *current-uri*
+           href))]
+    [:li {:class (when active? "active")}
+     [:a {:href href}
+      label]]))
+
+(defn global-nav []
+  [:nav.global-nav
+   [:ul
+    (nav-item "/events" "Events")
+    (when *authenticated?*
+      (nav-item "/attendees" "Attendees"))
+    [:li.spacer]
+    [:li
+     (if *authenticated?*
+       [:form.link-form
+        {:method :post
+         :action "/auth/sign-out"}
+        [:input {:type "submit"
+                 :value "Sign Out"}]]
+       [:a {:href "/auth/discord"}
+        "Sign In"])]]])
+
+(defn render-page [opts & body]
+  (let [[{:keys [title]} body]
+        (if (map? opts)
+          [opts body]
+          [{} (concat [opts] body)])]
+    (html
+     [:html {:lang "en"}
+      [:head
+       [:meta {:charset "UTF-8"}]
+       [:meta {:name "viewport"
+               :content "width=device-width,initial-scale=1"}]
+       [:title (if title
+                 (str title " - BBBG")
+                 "BBBG")]
+       [:link {:rel "stylesheet"
+               :type "text/css"
+               :href "/main.css"}]]
+      [:body
+       [:div.content
+        (global-nav)
+        #_(flash/render-flash flash/test-flash)
+        (flash/render-flash)
+        body]
+       [:script {:src "/main.js"}]]])))
+
+(defn page-response [& render-page-args]
+  (-> (apply render-page render-page-args)
+      response
+      (content-type "text/html")))
+
+(comment
+  (render-page
+   [:h1 "hi"])
+  )
diff --git a/users/grfn/bbbg/src/bbbg/handlers/events.clj b/users/grfn/bbbg/src/bbbg/handlers/events.clj
new file mode 100644
index 0000000000..6f6d6f3585
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/handlers/events.clj
@@ -0,0 +1,259 @@
+(ns bbbg.handlers.events
+  (:require
+   [bbbg.db :as db]
+   [bbbg.db.attendee :as db.attendee]
+   [bbbg.db.event :as db.event]
+   [bbbg.event :as event]
+   [bbbg.handlers.core :refer [*authenticated?* page-response]]
+   [bbbg.meetup.import :refer [import-attendees!]]
+   [bbbg.util.display :refer [format-date pluralize]]
+   [bbbg.util.time :as t]
+   [bbbg.views.flash :as flash]
+   [compojure.coercions :refer [as-uuid]]
+   [compojure.core :refer [context GET POST]]
+   [java-time :refer [local-date]]
+   [ring.util.response :refer [not-found redirect]]
+   [bbbg.attendee :as attendee]
+   [bbbg.event-attendee :as event-attendee]
+   [bbbg.db.attendee-check :as db.attendee-check]
+   [bbbg.attendee-check :as attendee-check]
+   [bbbg.user :as user])
+  (:import
+   java.time.format.FormatStyle))
+
+(defn- num-attendees [event]
+  (str
+   (:num-attendees event)
+   (if (= (t/->LocalDate (::event/date event))
+          (local-date))
+     " Signed In"
+     (str " Attendee" (when-not (= 1 (:num-attendees event)) "s")))))
+
+(def index-type->label
+  {:upcoming "Upcoming"
+   :past "Past"})
+(def other-index-type
+  {:upcoming :past
+   :past :upcoming})
+
+(defn events-index
+  [{:keys [events num-events type]}]
+  [:div.page
+   [:div.page-header
+    [:h1
+     (pluralize
+      num-events
+      (str (index-type->label type) " Event"))]
+    [:a {:href (str "/events"
+                    (when (= :upcoming type)
+                      "/past"))}
+     "View "
+     (index-type->label (other-index-type type))
+     " Events"]]
+   (when *authenticated?*
+     [:a.button {:href "/events/new"}
+      "Create New Event"])
+   [:ul.events-list
+    (for [event (sort-by
+                 ::event/date
+                 (comp - compare)
+                 events)]
+      [:li
+       [:p
+        [:a {:href (str "/events/" (::event/id event))}
+         (format-date (::event/date event)
+                      FormatStyle/FULL)]]
+       [:p
+        (pluralize (:num-rsvps event) "RSVP")
+        ", "
+        (num-attendees event)]])]])
+
+(defn- import-attendee-list-form-group []
+  [:div.form-group
+   [:label "Import Attendee List"
+    [:br]
+    [:input {:type :file
+             :name :attendees}]]])
+
+(defn import-attendees-form [event]
+  [:form {:method :post
+          :action (str "/events/" (::event/id event) "/attendees")
+          :enctype "multipart/form-data"}
+   (import-attendee-list-form-group)
+   [:div.form-group
+    [:input {:type :submit
+             :value "Import"}]]])
+
+(defn event-page [{:keys [event attendees]}]
+  [:div.page
+   [:div.page-header
+    [:h1 (format-date (::event/date event)
+                      FormatStyle/FULL)]
+    [:div.spacer]
+    [:a.button {:href (str "/signup-forms/" (::event/id event) )}
+     "Go to Signup Form"]
+    [:form#delete-event
+     {:method :post
+      :action (str "/events/" (::event/id event) "/delete")
+      :data-confirm "Are you sure you want to delete this event?"}
+     [:input.error {:type "submit"
+                    :value "Delete Event"}]]]
+   [:div.stats
+    [:p (pluralize (:num-rsvps event) "RSVP")]
+    [:p (num-attendees event)]]
+   [:div
+    (import-attendees-form event)]
+   [:div
+    [:table.attendees
+     [:thead
+      [:th "Meetup Name"]
+      [:th "Discord Name"]
+      [:th "RSVP"]
+      [:th "Signed In"]
+      [:th "Last Vaccination Check"]]
+     [:tbody
+      (for [attendee (sort-by (juxt (comp not ::event-attendee/rsvpd-attending?)
+                                    (comp not ::event-attendee/attended?)
+                                    (comp some? :last-check)
+                                    ::attendee/meetup-name)
+                              attendees)]
+        [:tr
+         [:td.attendee-name (::attendee/meetup-name attendee)]
+         [:td
+          [:label.mobile-label "Discord Name: "]
+          (or (not-empty (::attendee/discord-name attendee))
+              "—")]
+         [:td
+          [:label.mobile-label "RSVP: "]
+          (if (::event-attendee/rsvpd-attending? attendee)
+            [:span {:title "Yes"} "✔️"]
+            [:span {:title "No"} "❌"])]
+         [:td
+          [:label.mobile-label "Signed In: "]
+          (if (::event-attendee/attended? attendee)
+            [:span {:title "Yes"} "✔️"]
+            [:span {:title "No"} "❌"])]
+         [:td
+          [:label.mobile-label "Last Vaccination Check: "]
+          (if-let [last-check (:last-check attendee)]
+            (str "✔️ "(-> last-check
+                         ::attendee-check/checked-at
+                         format-date)
+                 ", by "
+                 (get-in last-check [:user ::user/username]))
+            (list
+             [:span {:title "Not Checked"}
+              "❌"]
+             " "
+             [:a {:href (str "/attendees/"
+                             (::attendee/id attendee)
+                             "/checks/edit")}
+              "Edit"]))]])]]]])
+
+(defn import-attendees-page [{:keys [event]}]
+  [:div.page
+   [:h1 "Import Attendees for " (format-date (::event/date event))]
+   (import-attendees-form event)])
+
+(defn event-form
+  ([] (event-form {}))
+  ([event]
+   [:div.page
+    [:div.page-header
+     [:h1 "Create New Event"]]
+    [:form {:method "POST"
+            :action "/events"
+            :enctype "multipart/form-data"}
+     [:div.form-group
+      [:label "Date"
+       [:input {:type "date"
+                :id "date"
+                :name "date"
+                :value (str (::event/date event))}]]]
+     (import-attendee-list-form-group)
+     [:div.form-group
+      [:input {:type "submit"
+               :value "Create Event"}]]]]))
+
+(defn- events-list-handler [db query type]
+  (let [events (db/list db (db.event/with-stats query))
+        num-events (db/count db query)]
+    (page-response
+     (events-index {:events events
+                    :num-events num-events
+                    :type type}))))
+
+(defn events-routes [{:keys [db]}]
+  (context "/events" []
+    (GET "/" []
+      (events-list-handler db (db.event/upcoming) :upcoming))
+
+    (GET "/past" []
+      (events-list-handler db (db.event/past) :past))
+
+    (GET "/new" [date]
+      (page-response
+       {:title "New Event"}
+       (event-form {::event/date date})))
+
+    (POST "/" [date attendees]
+      (let [event (db.event/create! db {::event/date date})
+            message
+            (if attendees
+              (let [num-attendees
+                    (import-attendees! db
+                                       (::event/id event)
+                                       (:tempfile attendees))]
+                (format "Event created with %d attendees"
+                        num-attendees))
+              "Event created")]
+        (-> (str "/signup-forms/" (::event/id event))
+            redirect
+            (flash/add-flash {:flash/type :success
+                              :flash/message message}))))
+
+    (context "/:id" [id :<< as-uuid]
+      (GET "/" []
+        (if-let [event (db/fetch db
+                                 (-> {:select [:event.*]
+                                      :from [:event]
+                                      :where [:= :event.id id]}
+                                     (db.event/with-stats)))]
+          (let [attendees (db.attendee-check/attendees-with-last-checks
+                           db
+                           (db/list db (db.attendee/for-event id)))]
+            (page-response
+             (event-page {:event event
+                          :attendees attendees})))
+          (not-found "Event Not Found")))
+
+      (POST "/delete" []
+        (db/delete! db :event_attendee [:= :event-id id])
+        (db/delete! db :event [:= :id id])
+        (-> (redirect "/events")
+            (flash/add-flash
+             #:flash {:type :success
+                      :message "Successfully deleted event"})))
+
+      (GET "/attendees/import" []
+        (if-let [event (db/get db :event id)]
+          (page-response
+           (import-attendees-page {:event event}))
+          (not-found "Event Not Found")))
+
+      (POST "/attendees" [attendees]
+        (let [num-imported (import-attendees! db id (:tempfile attendees))]
+          (-> (redirect (str "/events/" id))
+              (flash/add-flash
+               #:flash{:type :success
+                       :message (format "Successfully imported %d attendees"
+                                        num-imported)})))))))
+
+(comment
+  (def db (:db bbbg.core/system))
+
+  (-> (db/list db :event)
+      first
+      ::event/date
+      format-date)
+  )
diff --git a/users/grfn/bbbg/src/bbbg/handlers/home.clj b/users/grfn/bbbg/src/bbbg/handlers/home.clj
new file mode 100644
index 0000000000..17d4875536
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/handlers/home.clj
@@ -0,0 +1,52 @@
+(ns bbbg.handlers.home
+  (:require
+   [bbbg.db.user :as db.user]
+   [bbbg.discord.auth :as discord.auth]
+   [bbbg.handlers.core :refer [page-response authenticated?]]
+   [bbbg.user :as user]
+   [bbbg.views.flash :as flash]
+   [compojure.core :refer [GET POST routes]]
+   [ring.util.response :refer [redirect]]
+   [bbbg.discord :as discord]))
+
+(defn- home-page []
+  [:div.home-page
+   [:a.signup-form-link {:href "/signup-forms"}
+    "Event Signup Form"]])
+
+(defn auth-failure []
+  [:div.auth-failure
+   [:p
+    "Sorry, only users with the Organizers role in discord can sign in"]
+   [:p
+    [:a {:href "/"} "Go Back"]]])
+
+(defn home-routes [{:keys [db] :as env}]
+  (routes
+   (GET "/" [] (page-response (home-page)))
+
+   (POST "/auth/sign-out" request
+     (if (authenticated? request)
+       (-> (redirect "/")
+           (update :session dissoc ::user/id)
+           (flash/add-flash
+            {:flash/message "Successfully Signed Out"
+             :flash/type :success}))
+       (redirect "/")))
+
+   (GET "/auth/success" request
+     (let [token (get-in request [:oauth2/access-tokens :discord])]
+       (if (discord.auth/check-discord-auth env token)
+         (let [discord-user (discord/me token)
+               user (db.user/find-or-create!
+                     db
+                     #::user{:username (:username discord-user)
+                             :discord-user-id (:id discord-user)})]
+           (-> (redirect "/")
+               (assoc-in [:session ::user/id] (::user/id user))
+               (flash/add-flash
+                {:flash/message "Successfully Signed In"
+                 :flash/type :success})))
+         (->
+          (page-response (auth-failure))
+          (assoc :status 401)))))))
diff --git a/users/grfn/bbbg/src/bbbg/handlers/signup_form.clj b/users/grfn/bbbg/src/bbbg/handlers/signup_form.clj
new file mode 100644
index 0000000000..ed1d7644f5
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/handlers/signup_form.clj
@@ -0,0 +1,93 @@
+(ns bbbg.handlers.signup-form
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.db :as db]
+   [bbbg.db.attendee :as db.attendee]
+   [bbbg.db.event :as db.event]
+   [bbbg.event :as event]
+   [bbbg.handlers.core
+    :refer [*authenticated?* authenticated? page-response]]
+   [cheshire.core :as json]
+   [compojure.core :refer [context GET]]
+   [honeysql.helpers :refer [merge-where]]
+   [java-time :refer [local-date]]
+   [ring.util.response :refer [redirect]]))
+
+(defn no-events-page [{:keys [authenticated?]}]
+  [:div.page
+   [:p
+    "There are no events for today"]
+   (when authenticated?
+     [:p
+      [:a.button {:href (str "/events/new?date=" (str (local-date)))}
+       "Create New Event"]])])
+
+(defn signup-page [{:keys [event attendees]}]
+  [:div.signup-page
+   [:form#signup-form
+    {:method "POST"
+     :action "/event_attendees"
+     :disabled "disabled"}
+    [:input#name-autocomplete
+     {:type "search"
+      :title "Name"
+      :name "name"
+      :spellcheck "false"
+      :autocorrect "off"
+      :autocomplete "off"
+      :autocapitalize "off"
+      :maxlength "2048"}]
+    [:input#attendee-id {:type "hidden" :name "attendee_id"}]
+    [:input#event-id {:type "hidden" :name "event_id" :value (::event/id event)}]
+    [:input#submit-button.hidden
+     {:type "submit"
+      :value "Sign In"
+      :disabled "disabled"}]]
+   [:ul#attendees-list
+    (if (seq attendees)
+      (for [attendee attendees]
+        [:li {:data-attendee (json/generate-string attendee)
+              :role "button"}
+         (::attendee/meetup-name attendee)])
+      [:li.no-attendees
+       [:p
+        "Nobody has RSVPed to this event yet, or no attendee list has been
+         imported"]
+       (when *authenticated?*
+         [:p
+          [:a.button
+           {:href (str "/events/"
+                       (::event/id event)
+                       "/attendees/import")}
+           "Import Attendee List"]])])]])
+
+(defn event-not-found []
+  [:div.event-not-found
+   [:p "Event not found"]
+   [:p [:a {:href (str "/events/new")} "Create a new event"]]])
+
+;;;
+
+(defn signup-form-routes [{:keys [db]}]
+  (context "/signup-forms" []
+    (GET "/" request
+      (if-let [event (db/fetch db (db.event/today))]
+        (redirect (str "/signup-forms/" (::event/id event)))
+        (page-response (no-events-page
+                        {:authenticated? (authenticated? request)}))))
+
+    (GET "/:event-id" [event-id]
+      (if-let [event (db/get db :event event-id)]
+        (let [attendees (db/list db
+                                 (->
+                                  (db.attendee/for-event event-id)
+                                  (merge-where
+                                   [:and
+                                    [:or
+                                     [:= :attended nil]
+                                     [:not :attended]]
+                                    :rsvpd_attending])))]
+          (page-response
+           (signup-page {:event event
+                         :attendees attendees})))
+        (event-not-found)))))
diff --git a/users/grfn/bbbg/src/bbbg/meetup/import.clj b/users/grfn/bbbg/src/bbbg/meetup/import.clj
new file mode 100644
index 0000000000..d13d63e16c
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/meetup/import.clj
@@ -0,0 +1,125 @@
+(ns bbbg.meetup.import
+  (:require
+   [bbbg.attendee :as attendee]
+   [bbbg.db.attendee :as db.attendee]
+   [bbbg.db.event-attendee :as db.event-attendee]
+   [bbbg.event :as event]
+   [bbbg.event-attendee :as event-attendee]
+   [bbbg.meetup-user :as meetup-user]
+   [bbbg.util.core :as u]
+   [bbbg.util.spec :as u.s]
+   [clojure.data.csv :as csv]
+   [clojure.java.io :as io]
+   [clojure.spec.alpha :as s]
+   [clojure.string :as str]
+   [expound.alpha :as exp]))
+
+(def spreadsheet-column->key
+  {"Name" :name
+   "User ID" :user-id
+   "Title" :title
+   "Event Host" :event-host
+   "RSVP" :rsvp
+   "Guests" :guests
+   "RSVPed on" :rsvped-on
+   "Joined Group on" :joined-group-on
+   "URL of Member Profile" :member-profile-url})
+
+(defn read-attendees [f]
+  (with-open [reader (io/reader f)]
+    (let [[headers & rows] (-> reader (csv/read-csv :separator \tab))
+          keys (map spreadsheet-column->key headers)]
+      (doall
+       (->> rows
+            (map (partial zipmap keys))
+            (map (partial u/filter-kv (fn [k _] (some? k))))
+            (filter (partial some (comp seq val))))))))
+
+;;;
+
+(s/def ::imported-attendee
+  (s/keys :req [::attendee/meetup-name
+                ::meetup-user/id]))
+
+(def key->attendee-col
+  {:name ::attendee/meetup-name
+   :user-id ::meetup-user/id})
+
+(defn row-user-id->user-id [row-id]
+  (str/replace-first row-id "user " ""))
+
+(defn check-attendee [attendee]
+  ()
+  (if (s/valid? ::imported-attendee attendee)
+    attendee
+    (throw (ex-info
+            (str "Invalid imported attendee\n"
+                 (exp/expound-str ::imported-attendee attendee))
+            (assoc (s/explain-data ::imported-attendee attendee)
+                   ::s/failure
+                   ::s/assertion-failed)))))
+
+(defn row->attendee [r]
+  (u.s/assert!
+   ::imported-attendee
+   (update (u/keep-keys key->attendee-col r)
+           ::meetup-user/id row-user-id->user-id)))
+
+;;;
+
+(s/def ::imported-event-attendee
+  (s/keys :req [::event-attendee/rsvpd-attending?
+                ::attendee/id
+                ::event/id]))
+
+(def key->event-attendee-col
+  {:rsvp ::event-attendee/rsvpd-attending?})
+
+(defn row->event-attendee
+  [{event-id ::event/id :keys [meetup-id->attendee-id]} r]
+  (let [attendee-id (-> r :user-id row-user-id->user-id meetup-id->attendee-id)]
+    (u.s/assert!
+     ::imported-event-attendee
+     (-> (u/keep-keys key->event-attendee-col r)
+         (update ::event-attendee/rsvpd-attending?
+                 (partial = "Yes"))
+         (assoc ::event/id event-id
+                ::attendee/id attendee-id)))))
+
+;;;
+
+(defn import-attendees! [db event-id f]
+  (let [rows (read-attendees f)
+        attendees (db.attendee/upsert-all! db (map row->attendee rows))
+        meetup-id->attendee-id (into {}
+                                     (map (juxt ::meetup-user/id ::attendee/id))
+                                     attendees)]
+    (db.event-attendee/upsert-all!
+     db
+     (map (partial row->event-attendee
+                   {::event/id event-id
+                    :meetup-id->attendee-id meetup-id->attendee-id})
+          rows))
+    (count rows)))
+
+;;; Spreadsheet columns:
+;;;
+;;; Name
+;;; User ID
+;;; Title
+;;; Event Host
+;;; RSVP
+;;; Guests
+;;; RSVPed on
+;;; Joined Group on
+;;; URL of Member Profile
+;;; Have you been to one of our events before? Note, attendance at all events will require proof of vaccination until further notice.
+
+(comment
+  (def -filename- "/home/grfn/code/depot/users/grfn/bbbg/sample-data.tsv")
+  (def event-id #uuid "09f8fed6-7480-451b-89a2-bb4edaeae657")
+
+  (read-attendees -filename-)
+  (import-attendees! (:db bbbg.core/system) event-id -filename-)
+
+  )
diff --git a/users/grfn/bbbg/src/bbbg/meetup_user.clj b/users/grfn/bbbg/src/bbbg/meetup_user.clj
new file mode 100644
index 0000000000..945d681c6f
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/meetup_user.clj
@@ -0,0 +1,6 @@
+(ns bbbg.meetup-user
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::id
+  (s/nilable
+   (s/and string? seq)))
diff --git a/users/grfn/bbbg/src/bbbg/styles.clj b/users/grfn/bbbg/src/bbbg/styles.clj
new file mode 100644
index 0000000000..a860ae6076
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/styles.clj
@@ -0,0 +1,407 @@
+;; -*- eval: (rainbow-mode) -*-
+(ns bbbg.styles
+  (:require
+   [garden.color :as color]
+   [garden.compiler :refer [compile-css]]
+   [garden.def :refer [defstyles]]
+   [garden.selectors
+    :refer [& active attr= descendant focus hover nth-child]]
+   [garden.stylesheet :refer [at-media]]
+   [garden.units :refer [px]]))
+
+(def black "#342e37")
+
+(def silver "#f9fafb")
+
+(def gray "#aaa")
+
+(def gray-light "#ddd")
+
+(def purple "#837aff")
+
+(def red "#c42348")
+
+(def orange "#fa824c")
+
+(def yellow "#FACB0F")
+
+(def blue "#026fb1")
+
+(def green "#87E24B")
+
+(def contextual-colors
+  {:success green
+   :info blue
+   :warning yellow
+   :error red})
+
+;;;
+
+(def content-width (px 1200))
+(def mobile-width (px 480))
+
+(defn desktop [& rules]
+  (at-media
+   {:screen true
+    :min-width content-width}
+   [:& rules]))
+
+(defn mobile [& rules]
+  (at-media
+   {:screen true
+    :max-width mobile-width}
+   [:& rules]))
+
+(defn not-mobile [& rules]
+  (at-media
+   {:screen true
+    :min-width mobile-width}
+   [:& rules]))
+
+
+;;;
+
+(defstyles global-nav
+  [:.global-nav
+   {:background-color silver}
+
+   [:>ul
+    {:display :flex
+     :flex-direction :row
+     :list-style :none}
+
+    (desktop
+     {:width content-width
+      :margin "0 auto"})]
+
+   [:a (descendant :.link-form (attr= "type" "submit"))
+    {:padding "1rem 1.5rem"
+     :display :block
+     :color black
+     :text-decoration :none}
+
+    [(& hover)
+     {:color blue}]]
+
+   [:li.active
+    {:font-weight "bold"
+     :border-bottom [["1px" "solid" black]]}]]
+
+  [:.spacer
+   {:flex 1}])
+
+(def link-conditional-styles
+  (list
+   [(& hover) (& active)
+    {:text-decoration :underline}]
+   [(& active)
+    {:color purple}]))
+
+(defstyles link-form
+  [:form.link-form
+   {:margin 0}
+   [(attr= "type" "submit")
+    {:background "none"
+     :border "none"
+     :padding 0
+     :color blue
+     :text-decoration :none
+     :cursor :pointer}
+    link-conditional-styles]])
+
+(defstyles search-form
+  [:.search-form
+   {:display :flex
+    :flex-direction :row
+    :width "100%"}
+
+   [:>*+*
+    {:margin-left "0.75rem"}]
+
+   [:input
+    {:flex 1}]
+
+   [(attr= "type" "submit")
+    {:flex 0}]])
+
+(defstyles forms
+  (let [text-input-types
+        #{"date"
+          "datetime-local"
+          "email"
+          "month"
+          "number"
+          "password"
+          "search"
+          "tel"
+          "text"
+          "time"
+          "url"
+          "week"}
+        each-text-type (fn [& rules]
+                         (into
+                          []
+                          (concat
+                           (map (comp & (partial attr= "type"))
+                                text-input-types)
+                           rules)))]
+    (each-text-type
+     {:width "100%"
+      :display "block"
+      :padding "0.6rem 0.75rem"
+      :border [["1px" "solid" gray-light]]
+      :border-radius "3px"
+      :box-shadow [["inset" 0 "1px" "5px" "rgba(0,0,0,0.075)"]]
+      :transition "border-color 150ms"
+      :background "none"}
+     [(& focus)
+      {:outline "none"
+       :border-color purple}]))
+
+  [(attr= "type" "submit") :button :.button
+   {:background-color (color/lighten blue 30)
+    :padding "0.6rem 0.75rem"
+    :border-radius "3px"
+    :border [[(px 1) "solid" (color/lighten blue 30)]]
+    :cursor :pointer
+    :display :inline-block}
+
+   [(& hover)
+    {:border-color blue
+     :text-decoration :none
+     :box-shadow [[0 "1px" "5px" "rgba(0,0,0,0.075)"]]}
+    [(:a &)
+     {:text-decoration :none}]]
+
+   [(& active)
+    {:background-color blue
+     :color :white
+     :box-shadow :none}
+    [(& :a)
+     {:text-decoration :none}]]
+
+   (for [[context color] contextual-colors]
+     [(& (keyword (str "." (name context))))
+      {:background-color (color/lighten color 30)
+       :border-color (color/lighten color 30)
+       :color black}
+
+      [(& hover)
+       {:border-color color}]])]
+
+  [:label
+   {:font-weight 600
+    :width "100%"}
+
+   [:input
+    {:font-weight "initial"
+     :margin-top "0.3rem"}]]
+
+  [:.form-group
+   {:display :flex
+    :margin-bottom "0.8rem"
+    :flex-direction :column}
+
+   [(attr= "type" "submit")
+    {:text-align :right
+     :align-self :flex-end}]])
+
+(defstyles tables
+  [:table
+   {:width "100%"
+    :border-collapse "collapse"}]
+
+  [:th
+   {:text-align "left"}]
+
+  [:td :th
+   {:padding "0.75rem 1rem"
+    :border-spacing 0
+    :border "none"}]
+
+  [:tr
+   {:border-spacing 0
+    :border "none"}
+   [(& (nth-child :even))
+    {:background-color silver}]])
+
+(defstyles flash
+  [:.flash-messages
+   {:max-width "800px"
+    :margin "1rem auto"}
+
+   (at-media
+    {:screen true
+     :max-width "800px"}
+    [:&
+     {:margin-left "1rem"
+      :margin-right "1rem"}])]
+
+  [:.flash-message
+   {:padding "1rem 1.5rem"
+    :border "1px solid"
+    :margin-bottom "1rem"}]
+
+  (for [[context color] contextual-colors]
+    [(& (keyword (str ".flash-" (name context))))
+     {:border-color color
+      :background-color (color/lighten color 30)
+      :border-radius "3px"}]))
+
+(defstyles home-page
+  [:.home-page
+   {:display :flex
+    :flex 1
+    :justify-content :center
+    :align-items :center}
+   [:.signup-form-link
+    {:display :block
+     :border [["1px" :solid blue]]
+     :border-radius "3px"
+     :color black
+     :font-size "2rem"
+     :background-color (color/lighten blue 50)
+     :margin-left "auto"
+     :margin-right "auto"
+     :padding "2rem"}
+    (desktop
+     {:padding "5rem"
+      :margin-left 0
+      :margin-right 0})
+    [(& hover) (& active)
+     {:text-decoration :none}]
+    [(& active)
+     {:background-color (color/lighten blue 30)}]]])
+
+(defstyles signup-page
+  [:.signup-page
+   {:margin "1rem"}
+   (desktop
+    {:width content-width
+     :margin "1rem auto"})]
+
+  [:#signup-form
+   {:display :flex
+    :flex-direction :row
+    :width "100%"}
+
+   [:*
+    {:flex 1}]
+
+   [:*+*
+    {:margin-left "1rem"}]
+
+   [(attr= "type" "submit")
+    {:flex 0}]]
+
+  [:#attendees-list
+   {:list-style "none"
+    :overflow-y "auto"
+    :height "calc(100vh - 8.32425rem)"}
+
+   [:li
+    {:padding "0.75rem 1rem"
+     :margin "0.35rem 0"
+     :border-radius "3px"
+     :background-color silver}]]
+
+  [:.no-attendees
+   {:text-align "center"
+    :margin-top "6rem"}
+
+   [:.button
+    {:margin-top "0.5rem"}]]
+
+  [:.hidden
+   {:display :none}])
+
+(defstyles attendees
+  [:.attendee-checks-form
+   {:max-width "340px"
+    :margin-left "auto"
+    :margin-right "auto"}]
+
+  [:.attendees
+   (mobile
+    {:display :block}
+
+    [:thead {:display :none}]
+    [:tbody :tr :td
+     {:display :block}]
+
+    [:tr
+     {:background-color silver
+      :padding "0.5rem 0.8rem"
+      :margin-bottom "1rem"
+      :border-radius "3px"}]
+    [:td {:padding "0.2rem 0"}]
+
+    [:.attendee-name
+     {:font-weight "bold"
+      :margin-bottom "0.9rem"}])
+
+   (not-mobile
+    [:.mobile-label
+     {:display :none}])])
+
+(defstyles events
+  [:.events-list
+   {:margin-top "1rem"}
+
+   [:li
+    {:margin-bottom "1rem"}]])
+
+(defstyles styles
+  forms
+  tables
+  global-nav
+  link-form
+  search-form
+  flash
+  home-page
+  signup-page
+  attendees
+  events
+
+  [:body
+   {:color black}]
+
+  [:.content
+   {:display :flex
+    :flex-direction :column
+    :height "100%"
+    :width "100%"}]
+
+  [:.page
+   {:margin-top "1rem"
+    :margin-left "1rem"
+    :margin-right "1rem"}
+
+   (desktop
+    {:width content-width
+     :margin-left "auto"
+     :margin-right "auto"})]
+
+  [:.page-header
+   {:display :flex
+    :flex-wrap :wrap
+    :padding-bottom "0.7rem"
+    :margin-bottom "1rem"
+    :border-bottom [["1px" "solid" silver]]
+    :align-items :center}
+
+   [:*+*
+    {:margin-left "0.5rem"}]
+
+   [:form
+    {:margin-block-end 0}]]
+
+  [(attr= "role" "button")
+   {:cursor :pointer}]
+
+  [:a {:color blue
+       :text-decoration :none}
+   link-conditional-styles])
+
+(def stylesheet
+  (compile-css styles))
diff --git a/users/grfn/bbbg/src/bbbg/user.clj b/users/grfn/bbbg/src/bbbg/user.clj
new file mode 100644
index 0000000000..f48c8d7338
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/user.clj
@@ -0,0 +1,8 @@
+(ns bbbg.user
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
+
+(s/def ::discord-id string?)
+
+(s/def ::username string?)
diff --git a/users/grfn/bbbg/src/bbbg/util/core.clj b/users/grfn/bbbg/src/bbbg/util/core.clj
new file mode 100644
index 0000000000..d458aa5592
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/util/core.clj
@@ -0,0 +1,138 @@
+(ns bbbg.util.core
+  (:require
+   [clojure.java.shell :refer [sh]]
+   [clojure.string :as str])
+  (:import
+   java.util.UUID))
+
+(defn remove-nils
+  "Remove all keys with nil values from m"
+  [m]
+  (let [!m (transient m)]
+    (doseq [[k v] m]
+      (when (nil? v)
+        (dissoc! !m k)))
+    (persistent! !m)))
+
+
+(defn alongside
+  "Apply a pair of functions to the first and second element of a two element
+  vector, respectively. The two argument form partially applies, such that:
+
+  ((alongside f g) xy) ≡ (alongside f g xy)
+
+  This is equivalent to (***) in haskell's Control.Arrow"
+  ([f g] (partial alongside f g))
+  ([f g [x y]] [(f x) (g y)]))
+
+(defn map-kv
+  "Map a pair of functions over the keys and values of a map, respectively.
+  Preserves metadata on the incoming map.
+  The two argument form returns a transducer that yields map-entries.
+
+  (partial map-kv identity identity) ≡ identity"
+  ([kf vf]
+   (map (fn [[k v]]
+          ;; important to return a map-entry here so that callers down the road
+          ;; can use `key` or `val`
+          (first {(kf k) (vf v)}))))
+  ([kf vf m]
+   (into (empty m) (map-kv kf vf) m)))
+
+(defn filter-kv
+  "Returns a map containing the elements of m for which (f k v) returns logical
+  true. The one-argument form returns a transducer that yields map entries"
+  ([f] (filter (partial apply f)))
+  ([f m]
+   (into (empty m) (filter-kv f) m)))
+
+(defn map-keys
+  "Map f over the keys of m. Preserves metadata on the incoming map. The
+  one-argument form returns a transducer that yields map-entries."
+  ([f] (map-kv f identity))
+  ([f m] (map-kv f identity m)))
+
+(defn keep-keys
+  "Map f over the keys of m, keeping only those entries for which f does not
+  return nil. Preserves metadata on the incoming map. The one-argument form
+  returns a transducer that yields map-entries."
+  ([f] (keep (fn [[k v]] (when-let [k' (f k)]
+                          (first {k' v})))))
+  ([f m] (into (empty m) (keep-keys f) m)))
+
+(defn map-vals
+  "Map f over the values of m. Preserves metadata on the incoming map. The
+  one-argument form returns a transducer that yields map-entries."
+  ([f] (map-kv identity f))
+  ([f m] (map-kv identity f m)))
+
+(defn map-keys-recursive [f x]
+  (cond
+    (map? x) (map-kv f (partial map-keys-recursive f) x)
+    (sequential? x) (map (partial map-keys-recursive f) x)
+    :else x))
+
+(defn denamespace [x]
+  (if (keyword? x)
+    (keyword (name x))
+    (map-keys-recursive denamespace x)))
+
+(defn reverse-merge
+  "Like `clojure.core/merge`, except duplicate keys from maps earlier in the
+  argument list take precedence
+
+    => (merge {:x 1} {:x 2})
+    {:x 2}
+
+    => (sut/reverse-merge {:x 1} {:x 2})
+    {:x 1}"
+  [& ms]
+  (apply merge (reverse ms)))
+
+(defn invert-map
+  "Invert the keys and vals of m. Behavior with duplicate vals is undefined.
+
+  => (sut/invert-map {:x 1 :y 2})
+  {1 :x 2 :y}"
+  [m]
+  (into {} (map (comp vec reverse)) m))
+
+(defn ->uuid
+  "Converts x to uuid, returning nil if x is nil or empty"
+  [x]
+  (cond
+    (not x) nil
+    (uuid? x) x
+    (and (string? x) (seq x))
+    (UUID/fromString x)))
+
+(defn key-by
+  "Create a map from a seq obtaining keys via f
+
+    => (sut/key-by :x [{:x 1} {:x 2 :y 3}])
+    {1 {:x 1}, 2 {:x 2 :y 3}}"
+  [f l]
+  (into {} (map (juxt f identity)) l))
+
+(defn distinct-by
+  "Like clojure.core/distinct, but can take a function f by which
+  distinctiveness is calculated"
+  [distinction-fn coll]
+  (let [step (fn step [xs seen]
+               (lazy-seq
+                ((fn [[f :as xs] seen]
+                   (when-let [s (seq xs)]
+                     (if (contains? seen (distinction-fn f))
+                       (recur (rest s) seen)
+                       (cons f (step (rest s) (conj seen (distinction-fn f)))))))
+                 xs seen)))]
+    (step coll #{})))
+
+(defn pass [n]
+  (let [{:keys [exit out err]} (sh "pass" n)]
+    (if (= 0 exit)
+      (str/trim out)
+      (throw (Exception.
+              (format "`pass` command failed\nStandard output:%s\nStandard Error:%s"
+                      out
+                      err))))))
diff --git a/users/grfn/bbbg/src/bbbg/util/dev_secrets.clj b/users/grfn/bbbg/src/bbbg/util/dev_secrets.clj
new file mode 100644
index 0000000000..88f1b50caa
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/util/dev_secrets.clj
@@ -0,0 +1,59 @@
+(ns bbbg.util.dev-secrets
+  "Utility library for loading secrets during development from multiple
+  backends.
+
+  # Supported backends
+
+  - [Pass][0] (the default)
+
+        (bbbg.util.dev-secrets/set-backend! :pass)
+
+    Loads all secrets by shelling out to `pass <secret-name>`
+
+    [0]: https://www.passwordstore.org/
+
+  - Directory
+
+        (bbbg.util.dev-secrets/set-backend! [:dir \"/path/to/secret/directory\"])
+
+     Loads all secrets by reading the secret name as a (plaintext!) file rooted
+     at the given directory"
+  (:require [bbbg.util.core :as u]
+            [clojure.string :as str]
+            [clojure.java.io :as io]))
+
+(def ^:dynamic *secret-backend* :pass)
+
+(defn set-backend!
+  "Change the default secret-backend"
+  [backend]
+  (alter-var-root #'*secret-backend* (constantly backend)))
+
+(defmulti ^:private load-secret
+  (fn [backend _secret]
+    (if (coll? backend) (first backend) backend)))
+
+(defmethod load-secret :pass [_ secret]
+  (u/pass secret))
+
+(defmethod load-secret :dir [[_ dir] secret]
+  (str/trim (slurp (io/file dir secret))))
+
+(defn secret
+  "Load the value for the given `secret-name' from the currently selected
+  backend"
+  [secret-name]
+  (load-secret *secret-backend* secret-name))
+
+(comment
+  (secret "bbbg/discord-client-id")
+
+  (binding [*secret-backend* [:dir "/tmp/bbbg-secrets"]]
+    (secret "bbbg/discord-client-id"))
+
+  (set-backend! [:dir "/tmp/bbbg-secrets"])
+  (secret "bbbg/discord-client-id")
+
+  (set-backend! :pass)
+  (secret "bbbg/discord-client-id")
+  )
diff --git a/users/grfn/bbbg/src/bbbg/util/display.clj b/users/grfn/bbbg/src/bbbg/util/display.clj
new file mode 100644
index 0000000000..40716632a3
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/util/display.clj
@@ -0,0 +1,23 @@
+(ns bbbg.util.display
+  (:require
+   [bbbg.util.time :as t])
+  (:import
+   [java.time.format DateTimeFormatter FormatStyle]))
+
+(defn format-date
+  ([d] (format-date d FormatStyle/MEDIUM))
+  ([d ^FormatStyle format-style]
+   (let [formatter (DateTimeFormatter/ofLocalizedDate format-style)]
+     (.format (t/->LocalDate d) formatter))))
+
+(defn pluralize
+  ([n sing plur]
+   (str (or n 0) " " (if (= 1 n) sing plur)))
+  ([n sing]
+   (pluralize n sing (str sing "s"))))
+
+(comment
+  (format-date #inst "2021-12-19T05:00:00.000-00:00")
+  (format-date #inst "2021-12-19T05:00:00.000-00:00"
+               FormatStyle/FULL)
+  )
diff --git a/users/grfn/bbbg/src/bbbg/util/spec.clj b/users/grfn/bbbg/src/bbbg/util/spec.clj
new file mode 100644
index 0000000000..89ac926699
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/util/spec.clj
@@ -0,0 +1,16 @@
+(ns bbbg.util.spec
+  (:require [expound.alpha :as exp]
+            [clojure.spec.alpha :as s]))
+
+(defn assert!
+  ([spec s] (assert! "Spec assertion failed" spec s))
+  ([message spec x]
+   (if (s/valid? spec x)
+     x
+     (throw (ex-info
+             (str message
+                  "\n"
+                  (exp/expound-str spec x))
+             (assoc (s/explain-data spec x)
+                    ::s/failure
+                    ::s/assertion-failed))))))
diff --git a/users/grfn/bbbg/src/bbbg/util/sql.clj b/users/grfn/bbbg/src/bbbg/util/sql.clj
new file mode 100644
index 0000000000..988959fd06
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/util/sql.clj
@@ -0,0 +1,5 @@
+(ns bbbg.util.sql
+  (:require [honeysql.core :as hsql]))
+
+(defn count-where [cond]
+  (hsql/call :count (hsql/call :case cond #sql/raw "1" :else nil)))
diff --git a/users/grfn/bbbg/src/bbbg/util/time.clj b/users/grfn/bbbg/src/bbbg/util/time.clj
new file mode 100644
index 0000000000..0278f89f5e
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/util/time.clj
@@ -0,0 +1,152 @@
+(ns bbbg.util.time
+  "Utilities for dealing with date/time"
+  (:require [clojure.spec.alpha :as s]
+            [clojure.test.check.generators :as gen]
+            [java-time :as jt])
+  (:import [java.time
+            LocalDateTime LocalTime OffsetDateTime ZoneId ZoneOffset
+            LocalDate Year]
+           [java.time.format DateTimeFormatter DateTimeParseException]
+           java.util.Calendar
+           org.apache.commons.lang3.time.DurationFormatUtils))
+
+(set! *warn-on-reflection* true)
+
+(defprotocol ToOffsetDateTime
+  (->OffsetDateTime [this]
+    "Coerces its argument to a `java.time.OffsetDateTime`"))
+
+(extend-protocol ToOffsetDateTime
+  OffsetDateTime
+  (->OffsetDateTime [odt] odt)
+
+  java.util.Date
+  (->OffsetDateTime [d]
+    (-> d
+        .toInstant
+        (OffsetDateTime/ofInstant (ZoneId/of "UTC")))))
+
+(defprotocol ToLocalTime (->LocalTime [this]))
+(extend-protocol ToLocalTime
+  LocalTime
+  (->LocalTime [lt] lt)
+
+  java.sql.Time
+  (->LocalTime [t]
+    (let [^Calendar cal (doto (Calendar/getInstance)
+                          (.setTime t))]
+      (LocalTime/of
+       (.get cal Calendar/HOUR_OF_DAY)
+       (.get cal Calendar/MINUTE)
+       (.get cal Calendar/SECOND))))
+
+  java.util.Date
+  (->LocalTime [d]
+    (-> d .toInstant (LocalTime/ofInstant (ZoneId/of "UTC")))))
+
+(defn local-time? [x] (satisfies? ToLocalTime x))
+(s/def ::local-time
+  (s/with-gen local-time?
+    #(gen/let [hour (gen/choose 0 23)
+               minute (gen/choose 0 59)
+               second (gen/choose 0 59)
+               nanos gen/nat]
+       (LocalTime/of hour minute second nanos))))
+
+(defprotocol ToLocalDate (->LocalDate [this]))
+(extend-protocol ToLocalDate
+  LocalDate
+  (->LocalDate [ld] ld)
+
+  java.sql.Date
+  (->LocalDate [sd] (.toLocalDate sd))
+
+  java.util.Date
+  (->LocalDate [d]
+    (-> d .toInstant (LocalDate/ofInstant (ZoneId/of "UTC")))))
+
+(defn local-date? [x] (satisfies? ToLocalDate x))
+(s/def ::local-date
+  (s/with-gen local-date?
+    #(gen/let [year (gen/choose Year/MIN_VALUE Year/MAX_VALUE)
+               day (gen/choose 1 (if (.isLeap (Year/of year))
+                                   366
+                                   365))]
+       (LocalDate/ofYearDay year day))))
+
+(extend-protocol Inst
+  OffsetDateTime
+  (inst-ms* [zdt]
+    (inst-ms* (.toInstant zdt)))
+
+  LocalDateTime
+  (inst-ms* [^LocalDateTime ldt]
+    (inst-ms* (.toInstant ldt ZoneOffset/UTC))))
+
+(let [formatter DateTimeFormatter/ISO_OFFSET_DATE_TIME]
+  (defn ^OffsetDateTime parse-iso-8601
+    "Parse s as an iso-8601 datetime, returning nil if invalid"
+    [^String s]
+    (try
+      (OffsetDateTime/parse s formatter)
+      (catch DateTimeParseException _ nil)))
+
+  (defn format-iso-8601
+    "Format dt, which can be an OffsetDateTime or java.util.Date, as iso-8601"
+    [dt]
+    (some->> dt ->OffsetDateTime (.format formatter))))
+
+(let [formatter DateTimeFormatter/ISO_TIME]
+  (defn parse-iso-8601-time
+    "Parse s as an iso-8601 timestamp, returning nil if invalid"
+    [^String s]
+    (try
+      (LocalTime/parse s formatter)
+      (catch DateTimeParseException _ nil)))
+
+  (defn format-iso-8601-time
+    "Format lt, which can be a LocalTime or java.sql.Time, as an iso-8601
+    formatted timestamp without a date."
+    [lt]
+    (some->> lt ->LocalTime (.format formatter))))
+
+(defmethod print-dup LocalTime [t w]
+  (binding [*out* w]
+    (print "#local-time ")
+    (print (str "\"" (format-iso-8601-time t) "\""))))
+
+(defmethod print-method LocalTime [t w]
+  (print-dup t w))
+
+(let [formatter DateTimeFormatter/ISO_LOCAL_DATE]
+  (defn parse-iso-8601-date
+    "Parse s as an iso-8601 date, returning nil if invalid"
+    [^String s]
+    (try
+      (LocalDate/parse s formatter)
+      (catch DateTimeParseException _ nil)))
+
+  (defn format-iso-8601-date
+    "Format lt, which can be a LocalDate, as an iso-8601 formatted date without
+    a timestamp."
+    [lt]
+    (some->> lt ->LocalDate (.format formatter))))
+
+(defmethod print-dup LocalDate [t w]
+  (binding [*out* w]
+    (print "#local-date ")
+    (print (str "\"" (format-iso-8601-date t) "\""))))
+
+(defmethod print-method LocalDate [t w]
+  (print-dup t w))
+
+
+(defn ^String human-format-duration
+  "Human-format the given duration"
+  [^java.time.Duration dur]
+  (DurationFormatUtils/formatDurationWords (Math/abs (.toMillis dur)) true true))
+
+(comment
+  (human-format-duration (jt/hours 5))
+  (human-format-duration (jt/plus (jt/hours 5) (jt/minutes 7)))
+  )
diff --git a/users/grfn/bbbg/src/bbbg/views/flash.clj b/users/grfn/bbbg/src/bbbg/views/flash.clj
new file mode 100644
index 0000000000..a44b21d4cb
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/views/flash.clj
@@ -0,0 +1,39 @@
+(ns bbbg.views.flash
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def :flash/type #{:success :error :warning :info})
+(s/def :flash/message string?)
+(s/def ::flash (s/keys :req [:flash/type :flash/message]))
+(s/fdef add-flash :args (s/cat :resp map? :flash ::flash) :ret map?)
+
+;;;
+
+(def ^:dynamic *flash* nil)
+
+(defn wrap-page-flash [handler]
+  (fn
+    ([request]
+     (binding [*flash* (:flash request)]
+       (handler request)))
+    ([request respond raise]
+     (binding [*flash* (:flash request)]
+       (handler request respond raise)))))
+
+(defn add-flash [resp flash]
+  (update-in resp [:flash :flash/messages] conj flash))
+
+(defn render-flash
+  ([] (render-flash *flash*))
+  ([flash]
+   (when-some [messages (not-empty (:flash/messages flash))]
+     [:ul.flash-messages
+      (for [message messages]
+        [:li.flash-message
+         {:class (str "flash-" (-> message :flash/type name))}
+         (:flash/message message)])])))
+
+(def test-flash
+  {:flash/messages
+   (for [type [:success :error :warning :info]]
+     {:flash/type type
+      :flash/message (str "Sample " type " message")})})
diff --git a/users/grfn/bbbg/src/bbbg/web.clj b/users/grfn/bbbg/src/bbbg/web.clj
new file mode 100644
index 0000000000..f9755577a5
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/web.clj
@@ -0,0 +1,140 @@
+(ns bbbg.web
+  (:require
+   [bbbg.discord.auth :as discord.auth :refer [wrap-discord-auth]]
+   [bbbg.handlers.attendee-checks :as attendee-checks]
+   [bbbg.handlers.attendees :as attendees]
+   [bbbg.handlers.core :refer [wrap-current-uri wrap-dynamic-auth]]
+   [bbbg.handlers.events :as events]
+   [bbbg.handlers.home :as home]
+   [bbbg.handlers.signup-form :as signup-form]
+   [bbbg.styles :refer [stylesheet]]
+   [bbbg.util.core :as u]
+   [bbbg.views.flash :refer [wrap-page-flash]]
+   [cambium.core :as log]
+   clj-time.coerce
+   [clojure.java.io :as io]
+   [clojure.spec.alpha :as s]
+   [com.stuartsierra.component :as component]
+   [compojure.core :refer [GET routes]]
+   [config.core :refer [env]]
+   [org.httpkit.server :as http-kit]
+   [ring.logger :refer [wrap-with-logger]]
+   [ring.middleware.flash :refer [wrap-flash]]
+   [ring.middleware.keyword-params :refer [wrap-keyword-params]]
+   [ring.middleware.multipart-params :refer [wrap-multipart-params]]
+   [ring.middleware.params :refer [wrap-params]]
+   [ring.middleware.resource :refer [wrap-resource]]
+   [ring.middleware.session :refer [wrap-session]]
+   [ring.middleware.session.cookie :refer [cookie-store]]
+   [ring.util.response :refer [content-type response]])
+  (:import
+   java.util.Base64))
+
+(s/def ::port pos-int?)
+
+(s/def ::cookie-secret
+  (s/and bytes? #(= 16 (count %))))
+
+(s/def ::config
+  (s/merge
+   (s/keys :req [::port]
+           :opt [::cookie-secret
+                 ::base-url])
+   ::discord.auth/config))
+
+(s/fdef make-server
+  :args (s/cat :config ::config))
+
+
+(defn- string->cookie-secret [raw]
+  (s/assert
+   ::cookie-secret
+   (when raw
+     (.decode (Base64/getDecoder)
+              (.getBytes raw "UTF-8")))))
+
+(defn env->config []
+  (s/assert
+   ::config
+   (u/remove-nils
+    (merge
+     {::port (:port env 8888)
+      ::cookie-secret (some-> env :cookie-secret string->cookie-secret)
+      ::base-url (:base-url env)}
+     (discord.auth/env->config)))))
+
+(defn dev-config []
+  (s/assert
+   ::config
+   (merge
+    {::port 8888
+     ::cookie-secret (into-array Byte/TYPE (repeat 16 0))}
+    (discord.auth/dev-config))))
+
+;;;
+
+(defn app-routes [env]
+  (routes
+   (GET "/main.css" []
+     (-> (response
+          (str
+           "\n/* begin base.css */\n"
+           (slurp (io/resource "base.css"))
+           "\n/* end base.css */\n"
+           stylesheet))
+         (content-type "text/css")))
+
+   (attendees/attendees-routes env)
+   (attendee-checks/attendee-checks-routes env)
+   (signup-form/signup-form-routes env)
+   (events/events-routes env)
+   (home/home-routes env)))
+
+(defn middleware [app env]
+  (-> app
+      (wrap-resource "public")
+      (wrap-with-logger
+       {:log-fn
+        (fn [{:keys [level throwable message]}]
+          (log/log level {} throwable message))})
+      wrap-current-uri
+      wrap-dynamic-auth
+      (wrap-discord-auth env)
+      wrap-keyword-params
+      wrap-multipart-params
+      wrap-params
+      wrap-page-flash
+      wrap-flash
+      (wrap-session {:store (cookie-store
+                             {:key (:cookie-secret env)
+                              :readers {'clj-time/date-time
+                                        clj-time.coerce/from-string}})
+                     :cookie-attrs {:same-site :lax}})))
+
+(defn handler [env]
+  (-> (app-routes env)
+      (middleware env)))
+
+(defrecord WebServer [port cookie-secret db]
+  component/Lifecycle
+  (start [this]
+    (assoc this
+           ::shutdown-fn
+           (http-kit/run-server
+            (fn [r] ((handler this) r))
+            {:port port})))
+  (stop [this]
+    (if-let [shutdown-fn (::shutdown-fn this)]
+      (do (shutdown-fn :timeout 100)
+          (dissoc this ::shutdown-fn))
+      this)))
+
+(defn make-server [{::keys [port cookie-secret]
+                    :as env}]
+  (component/using
+   (map->WebServer
+    (merge
+     {:port port
+      :cookie-secret cookie-secret}
+     env))
+   [:db]))
diff --git a/users/grfn/bbbg/test/bbbg/meetup/import_test.clj b/users/grfn/bbbg/test/bbbg/meetup/import_test.clj
new file mode 100644
index 0000000000..d7d698a58c
--- /dev/null
+++ b/users/grfn/bbbg/test/bbbg/meetup/import_test.clj
@@ -0,0 +1,7 @@
+(ns bbbg.meetup.import-test
+  (:require [bbbg.meetup.import :as sut]
+            [clojure.test :refer :all]))
+
+(deftest test-row-user-id->user-id
+  (is (= "246364067" (sut/row-user-id->user-id "user 246364067")))
+  (is (= "246364067" (sut/row-user-id->user-id "246364067"))))
diff --git a/users/grfn/bbbg/tf.nix b/users/grfn/bbbg/tf.nix
new file mode 100644
index 0000000000..d5b19d9ebc
--- /dev/null
+++ b/users/grfn/bbbg/tf.nix
@@ -0,0 +1,96 @@
+{ depot, ... }:
+
+let
+  inherit (depot.users.grfn)
+    terraform
+    ;
+
+in
+terraform.workspace "bbbg"
+{
+  plugins = (p: with p; [
+    aws
+    cloudflare
+  ]);
+}
+{
+  machine = terraform.nixosMachine {
+    name = "bbbg";
+    instanceType = "t3a.small";
+    rootVolumeSizeGb = 250;
+    extraIngressPorts = [ 80 443 ];
+    configuration = { pkgs, lib, config, depot, ... }: {
+      imports = [
+        ./module.nix
+        "${depot.third_party.agenix.src}/modules/age.nix"
+      ];
+
+      services.openssh.enable = true;
+
+      services.nginx = {
+        enable = true;
+        recommendedTlsSettings = true;
+        recommendedOptimisation = true;
+        recommendedGzipSettings = true;
+        recommendedProxySettings = true;
+      };
+
+      networking.firewall.enable = false;
+
+      programs.zsh.enable = true;
+
+      users.users.grfn = {
+        isNormalUser = true;
+        initialPassword = "password";
+        extraGroups = [
+          "wheel"
+          "networkmanager"
+          "audio"
+          "docker"
+        ];
+        shell = pkgs.zsh;
+        openssh.authorizedKeys.keys = [
+          depot.users.grfn.keys.main
+        ];
+      };
+
+      security.sudo.extraRules = [{
+        groups = [ "wheel" ];
+        commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
+      }];
+
+      nix.gc = {
+        automatic = true;
+        dates = "weekly";
+        options = "--delete-older-than 30d";
+      };
+
+      age.secrets = {
+        bbbg.file =
+          depot.users.grfn.secrets."bbbg.age";
+      };
+
+      services.bbbg.enable = true;
+      services.bbbg.database.enable = true;
+      services.bbbg.proxy.enable = true;
+      services.bbbg.domain = "bbbg.gws.fyi";
+
+      security.acme.defaults.email = "root@gws.fyi";
+      security.acme.acceptTerms = true;
+    };
+  };
+
+  dns = {
+    data.cloudflare_zone.gws-fyi = {
+      name = "gws.fyi";
+    };
+
+    resource.cloudflare_record.bbbg = {
+      zone_id = "\${data.cloudflare_zone.gws-fyi.id}";
+      name = "bbbg";
+      type = "A";
+      value = "\${aws_instance.bbbg_machine.public_ip}";
+      proxied = false;
+    };
+  };
+}
diff --git a/users/grfn/emacs.d/+bindings.el b/users/grfn/emacs.d/+bindings.el
new file mode 100644
index 0000000000..abd4b771e2
--- /dev/null
+++ b/users/grfn/emacs.d/+bindings.el
@@ -0,0 +1,1430 @@
+;; -*- lexical-binding: t; -*-
+
+(load! "utils")
+(require 'f)
+(require 'predd)
+
+(undefine-key! :keymaps 'doom-leader-map "/")
+
+(defmacro find-file-in! (path &optional project-p)
+  "Returns an interactive function for searching files."
+  `(lambda () (interactive)
+     (let ((default-directory ,path))
+       (call-interactively
+        ',(command-remapping
+           (if project-p
+               #'projectile-find-file
+             #'find-file))))))
+
+(defun dired-mode-p () (eq 'dired-mode major-mode))
+
+(defun grfn/dired-minus ()
+  (interactive)
+  (if (dired-mode-p)
+      (dired-up-directory)
+    (when buffer-file-name
+      (-> (buffer-file-name)
+          (f-dirname)
+          (dired)))))
+
+(defmacro define-move-and-insert
+    (name &rest body)
+  `(defun ,name (count &optional vcount skip-empty-lines)
+     ;; Following interactive form taken from the source for `evil-insert'
+     (interactive
+      (list (prefix-numeric-value current-prefix-arg)
+            (and (evil-visual-state-p)
+                 (memq (evil-visual-type) '(line block))
+                 (save-excursion
+                   (let ((m (mark)))
+                     ;; go to upper-left corner temporarily so
+                     ;; `count-lines' yields accurate results
+                     (evil-visual-rotate 'upper-left)
+                     (prog1 (count-lines evil-visual-beginning evil-visual-end)
+                       (set-mark m)))))
+            (evil-visual-state-p)))
+     (atomic-change-group
+       ,@body
+       (evil-insert count vcount skip-empty-lines))))
+
+(define-move-and-insert grfn/insert-at-sexp-end
+  (when (not (equal (get-char) "("))
+    (backward-up-list))
+  (forward-sexp)
+  (backward-char))
+
+(define-move-and-insert grfn/insert-at-sexp-start
+  (backward-up-list)
+  (forward-char))
+
+(define-move-and-insert grfn/insert-at-form-start
+  (backward-sexp)
+  (backward-char)
+  (insert " "))
+
+(define-move-and-insert grfn/insert-at-form-end
+  (forward-sexp)
+  (insert " "))
+
+(load! "splitjoin")
+
+(defun +hlissner/install-snippets ()
+  "Install my snippets from https://github.com/hlissner/emacs-snippets into
+private/hlissner/snippets."
+  (interactive)
+  (doom-fetch :github "hlissner/emacs-snippets"
+              (expand-file-name "snippets" (doom-module-path :private 'hlissner))))
+
+(defun +hlissner/yank-buffer-filename ()
+  "Copy the current buffer's path to the kill ring."
+  (interactive)
+  (if-let* ((filename (or buffer-file-name (bound-and-true-p list-buffers-directory))))
+      (message (kill-new (abbreviate-file-name filename)))
+    (error "Couldn't find filename in current buffer")))
+
+(defmacro +def-finder! (name dir)
+  "Define a pair of find-file and browse functions."
+  `(progn
+     (defun ,(intern (format "+find-in-%s" name)) ()
+       (interactive)
+       (let ((default-directory ,dir)
+             projectile-project-name
+             projectile-require-project-root
+             projectile-cached-buffer-file-name
+             projectile-cached-project-root)
+         (call-interactively #'projectile-find-file)))
+     (defun ,(intern (format "+hlissner/browse-%s" name)) ()
+       (interactive)
+       (let ((default-directory ,dir))
+         (call-interactively (command-remapping #'find-file))))))
+
+(+def-finder! templates +file-templates-dir)
+(+def-finder! snippets +grfn-snippets-dir)
+(+def-finder! dotfiles (expand-file-name ".dotfiles" "~"))
+(+def-finder! doomd (expand-file-name ".doom.d" "~"))
+(+def-finder! notes +org-dir)
+(+def-finder! home-config (expand-file-name "code/system/home" "~"))
+(+def-finder! system-config (expand-file-name "code/system/system" "~"))
+
+(defun +grfn/paxedit-kill (&optional n)
+  (interactive "p")
+  (or (paxedit-comment-kill)
+      (when (paxedit-symbol-cursor-within?)
+        (paxedit-symbol-kill))
+      (paxedit-implicit-sexp-kill n)
+      (paxedit-sexp-kill n)
+      (message paxedit-message-kill)))
+
+;;;
+
+(evil-set-command-property 'flycheck-next-error :repeat nil)
+(evil-set-command-property 'flycheck-prev-error :repeat nil)
+
+;;;
+
+(map!
+ [remap evil-jump-to-tag] #'projectile-find-tag
+ [remap find-tag]         #'projectile-find-tag
+ ;; ensure there are no conflicts
+ :nmvo doom-leader-key nil
+ :nmvo doom-localleader-key nil)
+
+(undefine-key! :keymaps 'doom-leader-map "/")
+
+(map!
+ ;; --- Global keybindings ---------------------------
+ ;; Make M-x available everywhere
+ :gnvime "M-x" #'execute-extended-command
+ :gnvime "A-x" #'execute-extended-command
+ ;; Emacs debug utilities
+ :gnvime "M-;" #'eval-expression
+ :gnvime "M-:" #'doom/open-scratch-buffer
+ ;; Text-scaling
+ "M-+"       (λ! (text-scale-set 0))
+ "M-="       #'text-scale-increase
+ "M--"       #'text-scale-decrease
+ ;; Simple window navigation/manipulation
+ "C-`"       #'doom/popup-toggle
+ "C-~"       #'doom/popup-raise
+ "M-t"       #'+workspace/new
+ "M-T"       #'+workspace/display
+ "M-w"       #'delete-window
+ "M-W"       #'+workspace/close-workspace-or-frame
+ "M-n"       #'evil-buffer-new
+ "M-N"       #'make-frame
+ "M-1"       (λ! (+workspace/switch-to 0))
+ "M-2"       (λ! (+workspace/switch-to 1))
+ "M-3"       (λ! (+workspace/switch-to 2))
+ "M-4"       (λ! (+workspace/switch-to 3))
+ "M-5"       (λ! (+workspace/switch-to 4))
+ "M-6"       (λ! (+workspace/switch-to 5))
+ "M-7"       (λ! (+workspace/switch-to 6))
+ "M-8"       (λ! (+workspace/switch-to 7))
+ "M-9"       (λ! (+workspace/switch-to 8))
+ "M-0"       #'+workspace/switch-to-last
+ ;; Other sensible, textmate-esque global bindings
+ :ne "M-r"   #'+eval/buffer
+ :ne "M-R"   #'+eval/region-and-replace
+ :ne "M-b"   #'+eval/build
+ :ne "M-a"   #'mark-whole-buffer
+ :ne "M-c"   #'evil-yank
+ :ne "M-q"   (if (daemonp) #'delete-frame #'save-buffers-kill-emacs)
+ :ne "M-f"   #'swiper
+ :ne "C-M-f" #'doom/toggle-fullscreen
+ :n  "M-s"   #'save-buffer
+ :m  "A-j"   #'+hlissner:multi-next-line
+ :m  "A-k"   #'+hlissner:multi-previous-line
+ :nv "C-SPC" #'+evil:fold-toggle
+ :gnvimer "M-v" #'clipboard-yank
+ ;; Easier window navigation
+ :en "C-h"   #'evil-window-left
+ :en "C-j"   #'evil-window-down
+ :en "C-k"   #'evil-window-up
+ :en "C-l"   #'evil-window-right
+ :n "U" #'undo-tree-visualize
+
+ "C-x p"     #'doom/other-popup
+
+ :n "K" #'+lookup/documentation
+ :n "g d" #'+lookup/definition
+
+
+ ;; --- <leader> -------------------------------------
+ (:leader
+   :desc "Ex command"              :nv ";"  #'evil-ex
+   :desc "M-x"                     :nv ":"  #'execute-extended-command
+   :desc "Pop up scratch buffer"   :nv "x"  #'doom/open-scratch-buffer
+   :desc "Org Capture"             :nv "X"  #'org-capture
+   :desc "Org Capture"             :nv "a"  #'org-capture
+
+   ;; Most commonly used
+   :desc "Find file in project"    :n "SPC" #'projectile-find-file
+   :desc "Switch workspace buffer" :n ","   #'persp-switch-to-buffer
+   :desc "Switch buffer"           :n "<"   #'switch-to-buffer
+   :desc "Browse files"            :n "."   #'find-file
+   :desc "Toggle last popup"       :n "~"   #'doom/popup-toggle
+   :desc "Eval expression"         :n "`"   #'eval-expression
+   :desc "Blink cursor line"       :n "DEL" #'+doom/blink-cursor
+   :desc "Jump to bookmark"        :n "RET" #'bookmark-jump
+
+   ;; C-u is used by evil
+   :desc "Universal argument"      :n "u"  #'universal-argument
+   :desc "window"                  :n "w"  evil-window-map
+
+   (:desc "previous..." :prefix "["
+     :desc "Text size"             :nv "[" #'text-scale-decrease
+     :desc "Buffer"                :nv "b" #'doom/previous-buffer
+     :desc "Diff Hunk"             :nv "d" #'git-gutter:previous-hunk
+     :desc "Todo"                  :nv "t" #'hl-todo-previous
+     :desc "Error"                 :nv "e" #'flycheck-previous-error
+     :desc "Workspace"             :nv "w" #'+workspace/switch-left
+     :desc "Smart jump"            :nv "h" #'smart-backward
+     :desc "Spelling error"        :nv "s" #'evil-prev-flyspell-error
+     :desc "Spelling correction"   :n  "S" #'flyspell-correct-previous-word-generic
+     :desc "Git conflict"          :n  "n" #'smerge-prev)
+
+   (:desc "next..." :prefix "]"
+     :desc "Text size"             :nv "]" #'text-scale-increase
+     :desc "Buffer"                :nv "b" #'doom/next-buffer
+     :desc "Diff Hunk"             :nv "d" #'git-gutter:next-hunk
+     :desc "Todo"                  :nv "t" #'hl-todo-next
+     :desc "Error"                 :nv "e" #'flycheck-next-error
+     :desc "Workspace"             :nv "w" #'+workspace/switch-right
+     :desc "Smart jump"            :nv "l" #'smart-forward
+     :desc "Spelling error"        :nv "s" #'evil-next-flyspell-error
+     :desc "Spelling correction"   :n  "S" #'flyspell-correct-word-generic
+     :desc "Git conflict"          :n  "n" #'smerge-next)
+
+   (:desc "search" :prefix "/"
+     :desc "Swiper"                :nv "/" #'swiper
+     :desc "Imenu"                 :nv "i" #'imenu
+     :desc "Imenu across buffers"  :nv "I" #'imenu-anywhere
+     :desc "Online providers"      :nv "o" #'+lookup/online-select)
+
+   (:desc "workspace" :prefix "TAB"
+     :desc "Display tab bar"          :n "TAB" #'+workspace/display
+     :desc "New workspace"            :n "n"   #'+workspace/new
+     :desc "Load workspace from file" :n "l"   #'+workspace/load
+     :desc "Load last session"        :n "L"   (λ! (+workspace/load-session))
+     :desc "Save workspace to file"   :n "s"   #'+workspace/save
+     :desc "Autosave current session" :n "S"   #'+workspace/save-session
+     :desc "Switch workspace"         :n "."   #'+workspace/switch-to
+     :desc "Kill all buffers"         :n "x"   #'doom/kill-all-buffers
+     :desc "Delete session"           :n "X"   #'+workspace/kill-session
+     :desc "Delete this workspace"    :n "d"   #'+workspace/delete
+     :desc "Load session"             :n "L"   #'+workspace/load-session
+     :desc "Next workspace"           :n "]"   #'+workspace/switch-right
+     :desc "Previous workspace"       :n "["   #'+workspace/switch-left
+     :desc "Switch to 1st workspace"  :n "1"   (λ! (+workspace/switch-to 0))
+     :desc "Switch to 2nd workspace"  :n "2"   (λ! (+workspace/switch-to 1))
+     :desc "Switch to 3rd workspace"  :n "3"   (λ! (+workspace/switch-to 2))
+     :desc "Switch to 4th workspace"  :n "4"   (λ! (+workspace/switch-to 3))
+     :desc "Switch to 5th workspace"  :n "5"   (λ! (+workspace/switch-to 4))
+     :desc "Switch to 6th workspace"  :n "6"   (λ! (+workspace/switch-to 5))
+     :desc "Switch to 7th workspace"  :n "7"   (λ! (+workspace/switch-to 6))
+     :desc "Switch to 8th workspace"  :n "8"   (λ! (+workspace/switch-to 7))
+     :desc "Switch to 9th workspace"  :n "9"   (λ! (+workspace/switch-to 8))
+     :desc "Switch to last workspace" :n "0"   #'+workspace/switch-to-last)
+
+   (:desc "buffer" :prefix "b"
+     :desc "New empty buffer"        :n "n" #'evil-buffer-new
+     :desc "Switch workspace buffer" :n "b" #'switch-to-buffer
+     :desc "Switch buffer"           :n "B" #'switch-to-buffer
+     :desc "Kill buffer"             :n "k" #'doom/kill-this-buffer
+     :desc "Kill other buffers"      :n "o" #'doom/kill-other-buffers
+     :desc "Save buffer"             :n "s" #'save-buffer
+     :desc "Pop scratch buffer"      :n "x" #'doom/open-scratch-buffer
+     :desc "Bury buffer"             :n "z" #'bury-buffer
+     :desc "Next buffer"             :n "]" #'doom/next-buffer
+     :desc "Previous buffer"         :n "[" #'doom/previous-buffer
+     :desc "Sudo edit this file"     :n "S" #'doom/sudo-this-file)
+
+   (:desc "code" :prefix "c"
+     :desc "List errors"               :n  "x" #'flycheck-list-errors
+     :desc "Evaluate buffer/region"    :n  "e" #'+eval/buffer
+                                       :v  "e" #'+eval/region
+     :desc "Evaluate & replace region" :nv "E" #'+eval:replace-region
+     :desc "Build tasks"               :nv "b" #'+eval/build
+     :desc "Jump to definition"        :n  "d" #'+lookup/definition
+     :desc "Jump to references"        :n  "D" #'+lookup/references
+     :desc "Open REPL"                 :n  "r" #'+eval/open-repl
+                                       :v  "r" #'+eval:repl)
+
+   (:desc "file" :prefix "f"
+     :desc "Find file"                  :n "." #'find-file
+     :desc "Sudo find file"             :n ">" #'doom/sudo-find-file
+     :desc "Find file in project"       :n "/" #'projectile-find-file
+     :desc "Find file from here"        :n "?" #'counsel-file-jump
+     :desc "Find other file"            :n "a" #'projectile-find-other-file
+     :desc "Open project editorconfig"  :n "c" #'editorconfig-find-current-editorconfig
+     :desc "Find file in dotfiles"      :n "d" #'+find-in-dotfiles
+     :desc "Find file in system config" :n "s" #'+find-in-system-config
+     :desc "Find file in home config"   :n "h" #'+find-in-home-config
+     :desc "Browse dotfiles"            :n "D" #'+hlissner/browse-dotfiles
+     :desc "Find file in emacs.d"       :n "e" #'+find-in-doomd
+     :desc "Browse emacs.d"             :n "E" #'+hlissner/browse-doomd
+     :desc "Recent files"               :n "r" #'recentf-open-files
+     :desc "Recent project files"       :n "R" #'projectile-recentf
+     :desc "Yank filename"              :n "y" #'+hlissner/yank-buffer-filename)
+
+   (:desc "git" :prefix "g"
+     :desc "Git status"            :n  "S" #'magit-status
+     :desc "Git blame"             :n  "b" #'magit-blame
+     :desc "Git time machine"      :n  "t" #'git-timemachine-toggle
+     :desc "Git stage hunk"        :n  "s" #'git-gutter:stage-hunk
+     :desc "Git revert hunk"       :n  "r" #'git-gutter:revert-hunk
+     :desc "Git revert buffer"     :n  "R" #'vc-revert
+     ;; :desc "List gists"            :n  "g" #'+gist:list
+     :desc "Git grep"              :n  "g" #'counsel-projectile-rg
+     :desc "Checkout Branch"       :n  "c" #'counsel-git-checkout
+     :desc "Next hunk"             :nv "]" #'git-gutter:next-hunk
+     :desc "Previous hunk"         :nv "[" #'git-gutter:previous-hunk
+
+     (:desc "smerge" :prefix "m"
+       :desc "Keep Current" :n "SPC" #'smerge-keep-current
+       :desc "Keep All"     :n "a" #'smerge-keep-all
+       :desc "Keep Upper"   :n "u" #'smerge-keep-upper
+       :desc "Keep Lower"   :n "l" #'smerge-keep-lower))
+
+   (:desc "help" :prefix "h"
+     :n "h" help-map
+     :desc "Apropos"               :n  "a" #'apropos
+     :desc "Reload theme"          :n  "R" #'doom//reload-theme
+     :desc "Find library"          :n  "l" #'find-library
+     :desc "Toggle Emacs log"      :n  "m" #'doom/popup-toggle-messages
+     :desc "Command log"           :n  "L" #'global-command-log-mode
+     :desc "Describe function"     :n  "f" #'describe-function
+     :desc "Describe key"          :n  "k" #'describe-key
+     :desc "Describe char"         :n  "c" #'describe-char
+     :desc "Describe mode"         :n  "M" #'describe-mode
+     :desc "Describe variable"     :n  "v" #'describe-variable
+     :desc "Describe face"         :n  "F" #'describe-face
+     :desc "Describe DOOM setting" :n  "s" #'doom/describe-setting
+     :desc "Describe DOOM module"  :n  "d" #'doom/describe-module
+     :desc "Find definition"       :n  "." #'+lookup/definition
+     :desc "Find references"       :n  "/" #'+lookup/references
+     :desc "Find documentation"    :n  "h" #'+lookup/documentation
+     :desc "What face"             :n  "'" #'doom/what-face
+     :desc "What minor modes"      :n  ";" #'doom/what-minor-mode
+     :desc "Info"                  :n  "i" #'info
+     :desc "Toggle profiler"       :n  "p" #'doom/toggle-profiler)
+
+   (:desc "insert" :prefix "i"
+     :desc "From kill-ring"        :nv "y" #'counsel-yank-pop
+     :desc "From snippet"          :nv "s" #'yas-insert-snippet)
+
+   (:desc "notes" :prefix "n"
+     :desc "Agenda"                 :n  "a" #'org-agenda
+     :desc "Find file in notes"     :n  "n" #'+find-in-notes
+     :desc "Store link"             :n  "l" #'org-store-link
+     :desc "Browse notes"           :n  "N" #'+hlissner/browse-notes
+     :desc "Org capture"            :n  "x" #'+org-capture/open
+     :desc "Create clubhouse story" :n  "c" #'org-tracker-create-issue
+     :desc "Archive subtree"        :n  "k" #'org-archive-subtree
+     :desc "Goto clocked-in note"   :n  "g" #'org-clock-goto
+     :desc "Clock Out"              :n  "o" #'org-clock-out)
+
+
+   (:desc "open" :prefix "o"
+     :desc "Default browser"       :n  "b" #'browse-url-of-file
+     :desc "Debugger"              :n  "d" #'+debug/open
+     :desc "Terminal in project"   :n  "T" #'+term/open-popup-in-project
+
+     :desc "Slack IM"              :n  "i" #'slack-im-select
+     :desc "Slack Channel"         :n  "c" #'slack-channel-select
+     :desc "Slack Group"           :n  "g" #'slack-group-select
+     :desc "Slack Unreads"         :n  "u" #'slack-select-unread-rooms
+     :desc "Slack Threads"         :n  "r" #'slack-all-threads
+
+     :desc "Email"                 :n "m" #'notmuch-jump-search
+
+     (:desc "ERC" :prefix "e"
+       :desc "Channel" :n "c" #'erc-switch-to-buffer)
+
+     ;; applications
+     :desc "APP: elfeed"           :n "E" #'=rss
+     :desc "APP: twitter"          :n "T" #'=twitter
+
+     (:desc "spotify" :prefix "s"
+       :desc "Search track"  :n "t" #'counsel-spotify-search-track
+       :desc "Search album"  :n "a" #'counsel-spotify-search-album
+       :desc "Search artist" :n "A" #'counsel-spotify-search-artist)
+
+     ;; macos
+     (:when IS-MAC
+       :desc "Reveal in Finder"          :n "o" #'+macos/reveal-in-finder
+       :desc "Reveal project in Finder"  :n "O" #'+macos/reveal-project-in-finder
+       :desc "Send to Transmit"          :n "u" #'+macos/send-to-transmit
+       :desc "Send project to Transmit"  :n "U" #'+macos/send-project-to-transmit
+       :desc "Send to Launchbar"         :n "l" #'+macos/send-to-launchbar
+       :desc "Send project to Launchbar" :n "L" #'+macos/send-project-to-launchbar))
+
+   (:desc "Email" :prefix "M"
+     :desc "Compose" :n "m" #'+notmuch/compose)
+
+   (:desc "project" :prefix "p"
+     :desc "Browse project"          :n  "." (find-file-in! (doom-project-root))
+     :desc "Find file in project"    :n  "/" #'projectile-find-file
+     :desc "Run cmd in project root" :nv "!" #'projectile-run-shell-command-in-root
+     :desc "Switch project"          :n  "p" #'projectile-switch-project
+     :desc "Recent project files"    :n  "r" #'projectile-recentf
+     :desc "List project tasks"      :n  "t" #'+ivy/tasks
+     :desc "Pop term in project"     :n  "o" #'+term/open-popup-in-project
+     :desc "Invalidate cache"        :n  "x" #'projectile-invalidate-cache)
+
+   (:desc "quit" :prefix "q"
+     :desc "Quit"                   :n "q" #'evil-save-and-quit
+     :desc "Quit (forget session)"  :n "Q" #'+workspace/kill-session-and-quit)
+
+   (:desc "remote" :prefix "r"
+     :desc "Upload local"           :n "u" #'+upload/local
+     :desc "Upload local (force)"   :n "U" (λ! (+upload/local t))
+     :desc "Download remote"        :n "d" #'+upload/remote-download
+     :desc "Diff local & remote"    :n "D" #'+upload/diff
+     :desc "Browse remote files"    :n "." #'+upload/browse
+     :desc "Detect remote changes"  :n ">" #'+upload/check-remote)
+
+   (:desc "snippets" :prefix "s"
+     :desc "New snippet"            :n  "n" #'yas-new-snippet
+     :desc "Insert snippet"         :nv "i" #'yas-insert-snippet
+     :desc "Find snippet for mode"  :n  "s" #'yas-visit-snippet-file
+     :desc "Find snippet"           :n  "S" #'+find-in-snippets)
+
+   (:desc "toggle" :prefix "t"
+     :desc "Flyspell"               :n "s" #'flyspell-mode
+     :desc "Flycheck"               :n "f" #'flycheck-mode
+     :desc "Line numbers"           :n "l" #'doom/toggle-line-numbers
+     :desc "Fullscreen"             :n "f" #'doom/toggle-fullscreen
+     :desc "Indent guides"          :n "i" #'highlight-indentation-mode
+     :desc "Indent guides (column)" :n "I" #'highlight-indentation-current-column-mode
+     :desc "Impatient mode"         :n "h" #'+impatient-mode/toggle
+     :desc "Big mode"               :n "b" #'doom-big-font-mode
+     :desc "Evil goggles"           :n "g" #'+evil-goggles/toggle))
+
+
+ ;; --- vim-vinegar
+ :n "-" #'grfn/dired-minus
+ (:after dired-mode
+         (:map dired-mode-map
+        "-" #'grfn/dired-minus))
+
+ (:map smartparens-mode-map
+   :n "g o" #'sp-raise-sexp)
+
+ ;; --- vim-sexp-mappings-for-regular-people
+ (:after paxedit
+   (:map paxedit-mode-map
+     :i ";"                          #'paxedit-insert-semicolon
+     :i "("                          #'paxedit-open-round
+     :i "["                          #'paxedit-open-bracket
+     :i "{"                          #'paxedit-open-curly
+     :n [remap evil-yank-line]       #'paxedit-copy
+     :n [remap evil-delete-line]     #'+grfn/paxedit-kill
+     :n "g o"                        #'paxedit-sexp-raise
+     :n [remap evil-join-whitespace] #'paxedit-compress
+     :n "g S"                        #'paxedit-format-1
+     :n "g k"                        #'paxedit-backward-up
+     :n "g j"                        #'paxedit-backward-end))
+
+ ;; --- vim-splitjoin
+ :n [remap evil-join-whitespace] #'+splitjoin/join
+ :n "gS"                         #'+splitjoin/split
+
+ ;; --- Personal vim-esque bindings ------------------
+ :n  "zx" #'doom/kill-this-buffer
+ :n  "ZX" #'bury-buffer
+ :n  "]b" #'doom/next-buffer
+ :n  "[b" #'doom/previous-buffer
+ :n  "]w" #'+workspace/switch-right
+ :n  "[w" #'+workspace/switch-left
+ :m  "gt" #'+workspace/switch-right
+ :m  "gT" #'+workspace/switch-left
+ :m  "gd" #'+lookup/definition
+ :m  "gD" #'+lookup/references
+ :m  "K" #'+lookup/documentation
+ :n  "gp" #'+evil/reselect-paste
+ :n  "gr" #'+eval:region
+ :n  "gR" #'+eval/buffer
+ :v  "gR" #'+eval:replace-region
+ :v  "@"  #'+evil:macro-on-all-lines
+ :n  "g@" #'+evil:macro-on-all-lines
+ ;; repeat in visual mode (FIXME buggy)
+ :v  "."  #'evil-repeat
+ ;; don't leave visual mode after shifting
+ ;; :v  "<"  #'+evil/visual-dedent  ; vnoremap < <gv
+ ;; :v  ">"  #'+evil/visual-indent  ; vnoremap > >gv
+ ;; paste from recent yank register (which isn't overwritten)
+ :v  "C-p" "\"0p"
+
+ (:map evil-window-map ; prefix "C-w"
+   ;; Navigation
+   "C-h"     #'evil-window-left
+   "C-j"     #'evil-window-down
+   "C-k"     #'evil-window-up
+   "C-l"     #'evil-window-right
+   "C-w"     #'ace-window
+   ;; Swapping windows
+   "H"       #'+evil/window-move-left
+   "J"       #'+evil/window-move-down
+   "K"       #'+evil/window-move-up
+   "L"       #'+evil/window-move-right
+   "C-S-w"   #'ace-swap-window
+   ;; Window undo/redo
+   "u"       #'winner-undo
+   "C-u"     #'winner-undo
+   "C-r"     #'winner-redo
+   "o"       #'doom/window-enlargen
+   ;; Delete window
+   "c"       #'+workspace/close-window-or-workspace
+   "C-C"     #'ace-delete-window
+   ;; Popups
+   "p"       #'doom/popup-toggle
+   "m"       #'doom/popup-toggle-messages
+   "P"       #'doom/popup-close-all)
+
+
+ ;; --- Plugin bindings ------------------------------
+ ;; auto-yasnippet
+ :i  [C-tab] #'aya-expand
+ :nv [C-tab] #'aya-create
+
+ ;; company-mode (vim-like omnicompletion)
+ :i "C-SPC"  #'+company/complete
+ (:prefix "C-x"
+   :i "C-l"   #'+company/whole-lines
+   :i "C-k"   #'+company/dict-or-keywords
+   :i "C-f"   #'company-files
+   :i "C-]"   #'company-etags
+   :i "s"     #'company-ispell
+   :i "C-s"   #'company-yasnippet
+   :i "C-o"   #'company-capf
+   :i "C-n"   #'company-dabbrev-code
+   :i "C-p"   #'+company/dabbrev-code-previous)
+ (:after company
+   (:map company-active-map
+     ;; Don't interfere with `evil-delete-backward-word' in insert mode
+     "C-w"        nil
+     "C-o"        #'company-search-kill-others
+     "C-n"        #'company-select-next
+     "C-p"        #'company-select-previous
+     "C-h"        #'company-quickhelp-manual-begin
+     "C-S-h"      #'company-show-doc-buffer
+     "C-S-s"      #'company-search-candidates
+     "C-s"        #'company-filter-candidates
+     "C-SPC"      #'company-complete-common
+     "C-h"        #'company-quickhelp-manual-begin
+     [tab]        #'company-complete-common-or-cycle
+     [backtab]    #'company-select-previous
+     [escape]     (λ! (company-abort) (evil-normal-state 1)))
+   ;; Automatically applies to `company-filter-map'
+   (:map company-search-map
+     "C-n"        #'company-search-repeat-forward
+     "C-p"        #'company-search-repeat-backward
+     "C-s"        (λ! (company-search-abort) (company-filter-candidates))
+     [escape]     #'company-search-abort))
+
+ ;; counsel
+;  (:after counsel
+;    (:map counsel-ag-map
+;      [backtab]  #'+ivy/wgrep-occur      ; search/replace on results
+;      "C-SPC"    #'ivy-call-and-recenter ; preview))
+
+ ;; evil-commentary
+ ;; :n  "gc"  #'evil-commentary
+
+ ;; evil-exchange
+ :n  "gx"  #'evil-exchange
+
+ ;; evil-magit
+ (:after evil-magit
+   :map (magit-status-mode-map magit-revision-mode-map)
+   :n "C-j" nil
+   :n "C-k" nil)
+
+ ;; Smerge
+ :n "]n" #'smerge-next
+ :n "[n" #'smerge-prev
+
+ ;; evil-mc
+ (:prefix "gz"
+   :nv "m" #'evil-mc-make-all-cursors
+   :nv "u" #'evil-mc-undo-all-cursors
+   :nv "z" #'+evil/mc-make-cursor-here
+   :nv "t" #'+evil/mc-toggle-cursors
+   :nv "n" #'evil-mc-make-and-goto-next-cursor
+   :nv "p" #'evil-mc-make-and-goto-prev-cursor
+   :nv "N" #'evil-mc-make-and-goto-last-cursor
+   :nv "P" #'evil-mc-make-and-goto-first-cursor
+   :nv "d" #'evil-mc-make-and-goto-next-match
+   :nv "D" #'evil-mc-make-and-goto-prev-match)
+ (:after evil-mc
+   :map evil-mc-key-map
+   :nv "C-n" #'evil-mc-make-and-goto-next-cursor
+   :nv "C-N" #'evil-mc-make-and-goto-last-cursor
+   :nv "C-p" #'evil-mc-make-and-goto-prev-cursor
+   :nv "C-P" #'evil-mc-make-and-goto-first-cursor)
+
+ ;; evil-multiedit
+ :v  "R"     #'evil-multiedit-match-all
+ :n  "M-d"   #'evil-multiedit-match-symbol-and-next
+ :n  "M-D"   #'evil-multiedit-match-symbol-and-prev
+ :v  "M-d"   #'evil-multiedit-match-and-next
+ :v  "M-D"   #'evil-multiedit-match-and-prev
+ :nv "C-M-d" #'evil-multiedit-restore
+ (:after evil-multiedit
+   (:map evil-multiedit-state-map
+     "M-d" #'evil-multiedit-match-and-next
+     "M-D" #'evil-multiedit-match-and-prev
+     "RET" #'evil-multiedit-toggle-or-restrict-region)
+   (:map (evil-multiedit-state-map evil-multiedit-insert-state-map)
+     "C-n" #'evil-multiedit-next
+     "C-p" #'evil-multiedit-prev))
+
+ ;; evil-snipe
+ (:after evil-snipe
+   ;; Binding to switch to evil-easymotion/avy after a snipe
+   :map evil-snipe-parent-transient-map
+   "C-;" (λ! (require 'evil-easymotion)
+             (call-interactively
+              (evilem-create #'evil-snipe-repeat
+                             :bind ((evil-snipe-scope 'whole-buffer)
+                                    (evil-snipe-enable-highlight)
+                                    (evil-snipe-enable-incremental-highlight))))))
+
+ ;; evil-surround
+ :v  "S"  #'evil-surround-region
+ :o  "s"  #'evil-surround-edit
+ :o  "S"  #'evil-Surround-edit
+
+ ;; expand-region
+ :v  "v"  #'er/expand-region
+ :v  "V"  #'er/contract-region
+
+ ;; flycheck
+ :m  "]e" #'flycheck-next-error
+ :m  "[e" #'flycheck-previous-error
+ (:after flycheck
+   :map flycheck-error-list-mode-map
+   :n "C-n" #'flycheck-error-list-next-error
+   :n "C-p" #'flycheck-error-list-previous-error
+   :n "j"   #'flycheck-error-list-next-error
+   :n "k"   #'flycheck-error-list-previous-error
+   :n "RET" #'flycheck-error-list-goto-error)
+
+ ;; flyspell
+ :m  "]S" #'flyspell-correct-word-generic
+ :m  "[S" #'flyspell-correct-previous-word-generic
+
+ ;; git-gutter
+ :m  "]d" #'git-gutter:next-hunk
+ :m  "[d" #'git-gutter:previous-hunk
+
+ ;; git-timemachine
+ (:after git-timemachine
+   (:map git-timemachine-mode-map
+     :n "C-p" #'git-timemachine-show-previous-revision
+     :n "C-n" #'git-timemachine-show-next-revision
+     :n "[["  #'git-timemachine-show-previous-revision
+     :n "]]"  #'git-timemachine-show-next-revision
+     :n "q"   #'git-timemachine-quit
+     :n "gb"  #'git-timemachine-blame))
+
+ ;; gist
+ (:after gist
+   :map gist-list-menu-mode-map
+   :n "RET" #'+gist/open-current
+   :n "b"   #'gist-browse-current-url
+   :n "c"   #'gist-add-buffer
+   :n "d"   #'gist-kill-current
+   :n "f"   #'gist-fork
+   :n "q"   #'quit-window
+   :n "r"   #'gist-list-reload
+   :n "s"   #'gist-star
+   :n "S"   #'gist-unstar
+   :n "y"   #'gist-print-current-url)
+
+ ;; helm
+ (:after helm
+   (:map helm-map
+     "ESC"        nil
+     "C-S-n"      #'helm-next-source
+     "C-S-p"      #'helm-previous-source
+     "C-u"        #'helm-delete-minibuffer-contents
+     "C-w"        #'backward-kill-word
+     "C-r"        #'evil-paste-from-register ; Evil registers in helm! Glorious!
+     "C-b"        #'backward-word
+     [left]       #'backward-char
+     [right]      #'forward-char
+     [escape]     #'helm-keyboard-quit
+     [tab]        #'helm-execute-persistent-action)
+
+   (:after helm-files
+     (:map helm-generic-files-map
+       :e "ESC"     #'helm-keyboard-quit)
+     (:map helm-find-files-map
+       "C-w" #'helm-find-files-up-one-level
+       "TAB" #'helm-execute-persistent-action))
+
+   (:after helm-ag
+     (:map helm-ag-map
+       "<backtab>"  #'helm-ag-edit)))
+
+ ;; hl-todo
+ :m  "]t" #'hl-todo-next
+ :m  "[t" #'hl-todo-previous
+
+ ;; ivy
+ (:after ivy
+   :map ivy-minibuffer-map
+   [escape] #'keyboard-escape-quit
+   "C-SPC" #'ivy-call-and-recenter
+   "TAB" #'ivy-partial
+   "M-v" #'yank
+   "M-z" #'undo
+   "C-r" #'evil-paste-from-register
+   "C-k" #'ivy-previous-line
+   "C-j" #'ivy-next-line
+   "C-l" #'ivy-alt-done
+   "C-w" #'ivy-backward-kill-word
+   "C-u" #'ivy-kill-line
+   "C-b" #'backward-word
+   "C-f" #'forward-word)
+
+ ;; neotree
+ (:after neotree
+   :map neotree-mode-map
+   :n "g"         nil
+   :n [tab]       #'neotree-quick-look
+   :n "RET"       #'neotree-enter
+   :n [backspace] #'evil-window-prev
+   :n "c"         #'neotree-create-node
+   :n "r"         #'neotree-rename-node
+   :n "d"         #'neotree-delete-node
+   :n "j"         #'neotree-next-line
+   :n "k"         #'neotree-previous-line
+   :n "n"         #'neotree-next-line
+   :n "p"         #'neotree-previous-line
+   :n "h"         #'+neotree/collapse-or-up
+   :n "l"         #'+neotree/expand-or-open
+   :n "J"         #'neotree-select-next-sibling-node
+   :n "K"         #'neotree-select-previous-sibling-node
+   :n "H"         #'neotree-select-up-node
+   :n "L"         #'neotree-select-down-node
+   :n "G"         #'evil-goto-line
+   :n "gg"        #'evil-goto-first-line
+   :n "v"         #'neotree-enter-vertical-split
+   :n "s"         #'neotree-enter-horizontal-split
+   :n "q"         #'neotree-hide
+   :n "R"         #'neotree-refresh)
+
+ ;; realgud
+ (:after realgud
+   :map realgud:shortkey-mode-map
+   :n "j" #'evil-next-line
+   :n "k" #'evil-previous-line
+   :n "h" #'evil-backward-char
+   :n "l" #'evil-forward-char
+   :m "n" #'realgud:cmd-next
+   :m "b" #'realgud:cmd-break
+   :m "B" #'realgud:cmd-clear
+   :n "c" #'realgud:cmd-continue)
+
+ ;; rotate-text
+ :n  "gs"  #'rotate-text
+
+ ;; smart-forward
+ :m  "g]" #'smart-forward
+ :m  "g[" #'smart-backward
+
+ ;; undo-tree -- undo/redo for visual regions
+ :v "C-u" #'undo-tree-undo
+ :v "C-r" #'undo-tree-redo
+
+ ;; yasnippet
+ (:after yasnippet
+   (:map yas-keymap
+     "C-e"           #'+snippets/goto-end-of-field
+     "C-a"           #'+snippets/goto-start-of-field
+     "<M-right>"     #'+snippets/goto-end-of-field
+     "<M-left>"      #'+snippets/goto-start-of-field
+     "<M-backspace>" #'+snippets/delete-to-start-of-field
+     [escape]        #'evil-normal-state
+     [backspace]     #'+snippets/delete-backward-char
+     [delete]        #'+snippets/delete-forward-char-or-field)
+   (:map yas-minor-mode-map
+     :i "<tab>" yas-maybe-expand
+     :v "<tab>" #'+snippets/expand-on-region))
+
+
+ ;; --- Major mode bindings --------------------------
+
+ ;; Markdown
+ (:after markdown-mode
+   (:map markdown-mode-map
+     ;; fix conflicts with private bindings
+     "<backspace>" nil
+     "<M-left>"    nil
+     "<M-right>"   nil))
+
+ ;; Rust
+ (:after rust
+   (:map rust-mode-map
+     "K"     #'racer-describe
+     "g RET" #'cargo-process-test))
+
+ ;; Elixir
+ (:after alchemist
+   (:map elixir-mode-map
+     :n "K"     #'alchemist-help-search-at-point
+     :n "g RET" #'alchemist-project-run-tests-for-current-file
+     :n "g \\"  #'alchemist-mix-test-at-point
+     :n "g SPC" #'alchemist-mix-compile))
+
+ ;; Haskell
+ (:after haskell-mode
+   (:map haskell-mode-map
+     ;; :n "K"     #'intero-info
+     :n "K"     #'lsp-describe-thing-at-point
+     ;; :n "g d"   #'lsp-ui-peek-find-definitions
+     :n "g d"   #'lsp-ui-peek-find-definitions
+     :n "g R"   #'lsp-find-references
+     ;; :n "g SPC" #'intero-repl-load
+     ;; :n "g y"   #'lsp-ui-
+     ))
+
+ ;; Javascript
+ ;; (:after rjsx-mode
+ ;;   (:map rjsx-mode-map
+ ;;     :n "g d" #'flow-minor-jump-to-definition
+ ;;     :n "K"   #'flow-minor-type-at-pos))
+
+ (:after js2-mode
+   (:map js2-mode-map
+     :n "g d" #'flow-minor-jump-to-definition
+     :n "K"   #'flow-minor-type-at-pos))
+
+ ;; Elisp
+ (:map emacs-lisp-mode-map
+   :n "g SPC" #'eval-buffer
+   :n "g RET" (λ! () (ert t)))
+
+
+ ;; --- Custom evil text-objects ---------------------
+ :textobj "a" #'evil-inner-arg                    #'evil-outer-arg
+ :textobj "B" #'evil-textobj-anyblock-inner-block #'evil-textobj-anyblock-a-block
+ :textobj "i" #'evil-indent-plus-i-indent         #'evil-indent-plus-a-indent
+ :textobj "I" #'evil-indent-plus-i-indent-up      #'evil-indent-plus-a-indent-up
+ :textobj "J" #'evil-indent-plus-i-indent-up-down #'evil-indent-plus-a-indent-up-down
+
+
+ ;; --- Built-in plugins -----------------------------
+ (:after comint
+   ;; TAB auto-completion in term buffers
+   :map comint-mode-map [tab] #'company-complete)
+
+ (:after debug
+   ;; For elisp debugging
+   :map debugger-mode-map
+   :n "RET" #'debug-help-follow
+   :n "e"   #'debugger-eval-expression
+   :n "n"   #'debugger-step-through
+   :n "c"   #'debugger-continue)
+
+ (:map help-mode-map
+   :n "[["  #'help-go-back
+   :n "]]"  #'help-go-forward
+   :n "o"   #'ace-link-help
+   :n "q"   #'quit-window
+   :n "Q"   #'+ivy-quit-and-resume)
+
+ (:after vc-annotate
+   :map vc-annotate-mode-map
+   :n "q"   #'kill-this-buffer
+   :n "d"   #'vc-annotate-show-diff-revision-at-line
+   :n "D"   #'vc-annotate-show-changeset-diff-revision-at-line
+   :n "SPC" #'vc-annotate-show-log-revision-at-line
+   :n "]]"  #'vc-annotate-next-revision
+   :n "[["  #'vc-annotate-prev-revision
+   :n "TAB" #'vc-annotate-toggle-annotation-visibility
+   :n "RET" #'vc-annotate-find-revision-at-line))
+
+;; evil-easymotion
+(after! evil-easymotion
+  (let ((prefix (concat doom-leader-key " /")))
+    ;; NOTE `evilem-default-keybinds' unsets all other keys on the prefix (in
+    ;; motion state)
+    (evilem-default-keybindings prefix)
+    (evilem-define (kbd (concat prefix " n")) #'evil-ex-search-next)
+    (evilem-define (kbd (concat prefix " N")) #'evil-ex-search-previous)
+    (evilem-define (kbd (concat prefix " s")) #'evil-snipe-repeat
+                   :pre-hook (save-excursion (call-interactively #'evil-snipe-s))
+                   :bind ((evil-snipe-scope 'buffer)
+                          (evil-snipe-enable-highlight)
+                          (evil-snipe-enable-incremental-highlight)))
+    (evilem-define (kbd (concat prefix " S")) #'evil-snipe-repeat-reverse
+                   :pre-hook (save-excursion (call-interactively #'evil-snipe-s))
+                   :bind ((evil-snipe-scope 'buffer)
+                          (evil-snipe-enable-highlight)
+                          (evil-snipe-enable-incremental-highlight)))))
+
+
+;;
+;; Keybinding fixes
+;;
+
+;; This section is dedicated to "fixing" certain keys so that they behave
+;; properly, more like vim, or how I like it.
+
+(map! (:map input-decode-map
+        [S-iso-lefttab] [backtab]
+        (:unless window-system "TAB" [tab])) ; Fix TAB in terminal
+
+      ;; I want C-a and C-e to be a little smarter. C-a will jump to
+      ;; indentation. Pressing it again will send you to the true bol. Same goes
+      ;; for C-e, except it will ignore comments and trailing whitespace before
+      ;; jumping to eol.
+      :i "C-a" #'doom/backward-to-bol-or-indent
+      :i "C-e" #'doom/forward-to-last-non-comment-or-eol
+      :i "C-u" #'doom/backward-kill-to-bol-and-indent
+
+      ;; Emacsien motions for insert mode
+      :i "C-b" #'backward-word
+      :i "C-f" #'forward-word
+
+      ;; Highjacks space/backspace to:
+      ;;   a) balance spaces inside brackets/parentheses ( | ) -> (|)
+      ;;   b) delete space-indented blocks intelligently
+      ;;   c) do none of this when inside a string
+      ;; :i "SPC"                          #'doom/inflate-space-maybe
+      ;; :i [remap delete-backward-char]   #'doom/deflate-space-maybe
+      ;; :i [remap newline]                #'doom/newline-and-indent
+
+      (:map org-mode-map
+       :i [remap doom/inflate-space-maybe] #'org-self-insert-command
+       "C-c C-x C-i" #'org-clock-in
+       "C-c C-x <C-i>" #'org-clock-in)
+
+      (:map org-agenda-mode-map
+       "C-c C-x C-i" #'org-agenda-clock-in
+       "C-c C-x <C-i>" #'org-agenda-clock-in)
+
+      ;; Restore common editing keys (and ESC) in minibuffer
+      (:map (minibuffer-local-map
+             minibuffer-local-ns-map
+             minibuffer-local-completion-map
+             minibuffer-local-must-match-map
+             minibuffer-local-isearch-map
+             evil-ex-completion-map
+             evil-ex-search-keymap
+             read-expression-map)
+        ;; [escape] #'abort-recursive-edit
+        "C-r" #'evil-paste-from-register
+        "C-a" #'move-beginning-of-line
+        "C-w" #'doom/minibuffer-kill-word
+        "C-u" #'doom/minibuffer-kill-line
+        "C-b" #'backward-word
+        "C-f" #'forward-word
+        "M-z" #'doom/minibuffer-undo)
+
+      (:map messages-buffer-mode-map
+        "M-;" #'eval-expression
+        "A-;" #'eval-expression)
+
+      (:map tabulated-list-mode-map
+        [remap evil-record-macro] #'doom/popup-close-maybe)
+
+      (:after view
+        (:map view-mode-map "<escape>" #'View-quit-all)))
+
+(defun +sexp-transpose ()
+  (interactive)
+  (case evil-this-operator
+    ('evil-shift-right (paxedit-transpose-forward))
+    ('evil-shift-left  (paxedit-transpose-backward))))
+
+;; (defun nmap (&rest keys-and-ops)
+;;   (->>
+;;    (seq-partition keys-and-ops 2)
+;;    (seq-map
+;;     (lambda (k-op)
+;;       (let* ((k (car k-op))
+;;              (op (cadr k-op))
+;;              (prefix (substring k 0 1))
+;;              (prefix-sym (lookup-key evil-normal-state-map prefix))
+;;              (keyseq (substring k 1)))
+;;         (list keyseq prefix-sym op))))
+;;    (seq-group-by #'car)
+;;    (seq-map
+;;     (lambda (k-ops)
+;;       (let* ((keyseq           (car k-ops))
+;;              (ops              (cdr k-ops))
+;;              (existing-binding (lookup-key evil-operator-state-map keyseq))
+;;              (handler (λ! ()
+;;                           (if-let
+;;                               ((oplist
+;;                                 (seq-find (lambda (op)
+;;                                             (equal (nth 1 op)
+;;                                                    evil-this-operator))
+;;                                           ops)))
+;;                               (message "calling oplist")
+;;                               (->> oplist (nth 2) funcall)
+;;                             (when existing-binding
+;;                               (funcall existing-binding))))))
+;;         (if existing-binding
+;;             (progn
+;;               (define-key evil-operator-state-map
+;;                 (vector 'remap existing-binding)
+;;                 handler)
+;;               (define-key evil-motion-state-map
+;;                 (vector 'remap existing-binding)
+;;                 handler))
+;;           (define-key evil-operator-state-map keyseq handler)))))))
+
+;; (nmap
+;;  ">e" #'paxedit-transpose-forward
+;;  "<e" #'paxedit-transpose-backward)
+
+(require 'paxedit)
+(require 'general)
+(general-evil-setup t)
+
+(nmap
+  ">" (general-key-dispatch 'evil-shift-right
+        "e" 'paxedit-transpose-forward
+        ")" 'sp-forward-slurp-sexp
+        "(" 'sp-backward-barf-sexp
+        "I" 'grfn/insert-at-sexp-end
+        ;; "a" 'grfn/insert-at-form-end
+        ))
+
+(nmap
+  "<" (general-key-dispatch 'evil-shift-left
+        "e" 'paxedit-transpose-backward
+        ")" 'sp-forward-barf-sexp
+        "(" 'sp-backward-slurp-sexp
+        "I" 'grfn/insert-at-sexp-start
+        ;; "a" 'grfn/insert-at-form-start
+        ))
+
+
+(defmacro saving-excursion (&rest body)
+  `(λ! () (save-excursion ,@body)))
+
+(nmap "c" (general-key-dispatch 'evil-change
+            "r c" (saving-excursion (string-inflection-lower-camelcase))
+            "r C" (saving-excursion (string-inflection-camelcase))
+            "r m" (saving-excursion (string-inflection-camelcase))
+            "r s" (saving-excursion (string-inflection-underscore))
+            "r u" (saving-excursion (string-inflection-upcase))
+            "r -" (saving-excursion (string-inflection-kebab-case))
+            "r k" (saving-excursion (string-inflection-kebab-case))
+            ;; "r ." (saving-excursion (string-inflection-dot-case))
+            ;; "r ." (saving-excursion (string-inflection-space-case))
+            ;; "r ." (saving-excursion (string-inflection-title-case))
+            ))
+
+
+(predd-defmulti eval-sexp (lambda (form) major-mode))
+
+(predd-defmethod eval-sexp 'clojure-mode (form)
+  (cider-interactive-eval form))
+
+(predd-defmethod eval-sexp 'emacs-lisp-mode (form)
+  (pp-eval-expression form))
+
+(predd-defmulti eval-sexp-region (lambda (_beg _end) major-mode))
+
+(predd-defmethod eval-sexp-region 'clojure-mode (beg end)
+  (cider-interactive-eval nil nil (list beg end)))
+
+(predd-defmethod eval-sexp-region 'emacs-lisp-mode (beg end)
+  (pp-eval-expression (read (buffer-substring beg end))))
+
+(predd-defmulti eval-sexp-region-context (lambda (_beg _end _context) major-mode))
+
+(predd-defmethod eval-sexp-region-context 'clojure-mode (beg end context)
+  (cider--eval-in-context (buffer-substring beg end)))
+
+(defun pp-eval-context-region (beg end context)
+  (interactive "r\nxContext: ")
+  (let* ((inner-expr (read (buffer-substring beg end)))
+         (full-expr (list 'let* context inner-expr)))
+    (pp-eval-expression full-expr)))
+
+(predd-defmethod eval-sexp-region-context 'emacs-lisp-mode (beg end context)
+  (pp-eval-context-region beg end context))
+
+(predd-defmulti preceding-sexp (lambda () major-mode))
+
+(predd-defmethod preceding-sexp 'clojure-mode ()
+  (cider-last-sexp))
+
+(predd-defmethod preceding-sexp 'emacs-lisp-mode ()
+  (elisp--preceding-sexp))
+
+(defun eval-sexp-at-point ()
+  (interactive)
+  (let ((bounds (bounds-of-thing-at-point 'sexp)))
+    (eval-sexp-region (car bounds)
+                      (cdr bounds))))
+
+(defun eval-last-sexp (_)
+  (interactive)
+  (eval-sexp (preceding-sexp)))
+
+;;;
+
+(defun cider-insert-current-sexp-in-repl (&optional arg)
+  "Insert the expression at point in the REPL buffer.
+If invoked with a prefix ARG eval the expression after inserting it"
+  (interactive "P")
+  (cider-insert-in-repl (cider-sexp-at-point) arg))
+
+(evil-define-operator fireplace-send (beg end)
+  (cider-insert-current-sexp-in-repl nil nil (list beg end)))
+
+(defun +clojure-pprint-expr (form)
+  (format "(with-out-str (clojure.pprint/pprint %s))"
+          form))
+
+(defun cider-eval-read-and-print-handler (&optional buffer)
+  "Make a handler for evaluating and reading then printing result in BUFFER."
+  (nrepl-make-response-handler
+   (or buffer (current-buffer))
+   (lambda (buffer value)
+     (let ((value* (read value)))
+       (with-current-buffer buffer
+         (insert
+          (if (derived-mode-p 'cider-clojure-interaction-mode)
+              (format "\n%s\n" value*)
+            value*)))))
+   (lambda (_buffer out) (cider-emit-interactive-eval-output out))
+   (lambda (_buffer err) (cider-emit-interactive-eval-err-output err))
+   '()))
+
+(defun cider-eval-and-replace (beg end)
+  "Evaluate the expression in region and replace it with its result"
+  (interactive "r")
+  (let ((form (buffer-substring beg end)))
+    (cider-nrepl-sync-request:eval form)
+    (kill-region beg end)
+    (cider-interactive-eval
+     (+clojure-pprint-expr form)
+     (cider-eval-read-and-print-handler))))
+
+(defun cider-eval-current-sexp-and-replace ()
+  "Evaluate the expression at point and replace it with its result"
+  (interactive)
+  (apply #'cider-eval-and-replace (cider-sexp-at-point 'bounds)))
+
+;;;
+
+(evil-define-operator fireplace-eval (beg end)
+  (eval-sexp-region beg end))
+
+(evil-define-operator fireplace-replace (beg end)
+  (cider-eval-and-replace beg end))
+
+(evil-define-operator fireplace-eval-context (beg end)
+  (eval-sexp-region-context beg end))
+
+;;; fireplace-esque eval binding
+(nmap :keymaps 'cider-mode-map
+  "c" (general-key-dispatch 'evil-change
+        "p" (general-key-dispatch 'fireplace-eval
+              "p" 'cider-eval-sexp-at-point
+              "c" 'cider-eval-last-sexp
+              "d" 'cider-eval-defun-at-point
+              "r" 'cider-test-run-test)
+        "q" (general-key-dispatch 'fireplace-send
+              "q" 'cider-insert-current-sexp-in-repl
+              "c" 'cider-insert-last-sexp-in-repl)
+        "x" (general-key-dispatch 'fireplace-eval-context
+              "x" 'cider-eval-sexp-at-point-in-context
+              "c" 'cider-eval-last-sexp-in-context)
+        "!" (general-key-dispatch 'fireplace-replace
+              "!" 'cider-eval-current-sexp-and-replace
+              "c" 'cider-eval-last-sexp-and-replace)
+        "y" 'cider-copy-last-result))
+
+;;;
+
+(nmap :keymaps 'emacs-lisp-mode-map
+  "c" (general-key-dispatch 'evil-change
+        "p" (general-key-dispatch 'fireplace-eval
+              "p" 'eval-sexp-at-point
+              "c" 'eval-last-sexp
+              "d" 'eval-defun
+              "r" 'cider-test-run-test)
+        "x" (general-key-dispatch 'fireplace-eval-context
+              "x" 'cider-eval-sexp-at-point-in-context
+              "c" 'cider-eval-last-sexp-in-context)
+        "!" (general-key-dispatch 'fireplace-replace
+              "!" 'cider-eval-current-sexp-and-replace
+              "c" 'cider-eval-last-sexp-and-replace)
+        "y" 'cider-copy-last-result))
+
+(nmap :keymaps 'sly-mode-map
+  "c" (general-key-dispatch 'evil-change
+        "p" (general-key-dispatch 'sly-eval
+              ;; "p" 'eval-sexp-at-point
+              "c" 'sly-eval-last-expression
+              "d" 'sly-eval-defun
+              ;; "r" 'cider-test-run-test
+              )
+        ;; "x" (general-key-dispatch 'fireplace-eval-context
+        ;;       "x" 'cider-eval-sexp-at-point-in-context
+        ;;       "c" 'cider-eval-last-sexp-in-context
+        ;;       )
+        ;; "!" (general-key-dispatch 'fireplace-replace
+        ;;       "!" 'cider-eval-current-sexp-and-replace
+        ;;       "c" 'cider-eval-last-sexp-and-replace)
+        ;; "y" 'cider-copy-last-result
+        ))
+
+
+;; >) ; slurp forward
+;; <) ; barf forward
+;; <( ; slurp backward
+;; >( ; slurp backward
+
+;; (require 'doom-themes)
+(defun grfn/haskell-test-file-p ()
+  (string-match-p (rx (and "Spec.hs" eol))
+                  (buffer-file-name)))
+
+(require 'haskell)
+
+(defun grfn/intero-run-main ()
+  (interactive)
+  (intero-repl-load)
+  (intero-with-repl-buffer nil
+    (comint-simple-send
+     (get-buffer-process (current-buffer))
+     "main")))
+
+(defun grfn/run-clj-or-cljs-test ()
+  (interactive)
+  (message "Running tests...")
+  (cl-case (cider-repl-type-for-buffer)
+    ('cljs
+     (cider-interactive-eval
+      "(with-out-str (cljs.test/run-tests))"
+      (nrepl-make-response-handler
+       (current-buffer)
+       (lambda (_ value)
+         (with-output-to-temp-buffer "*cljs-test-results*"
+           (print
+            (->> value
+                 (s-replace "\"" "")
+                 (s-replace "\\n" "\n")))))
+       nil nil nil)))
+    (('clj 'multi)
+     (funcall-interactively
+      #'cider-test-run-ns-tests
+      nil))))
+
+(defun cider-copy-last-result ()
+  (interactive)
+  (cider-interactive-eval
+   "*1"
+   (nrepl-make-response-handler
+    (current-buffer)
+    (lambda (_ value)
+      (kill-new value)
+      (message "Copied last result (%s) to clipboard"
+               (if (= (length value) 1) "1 char"
+                 (format "%d chars" (length value)))))
+    nil nil nil)))
+
+
+(defun grfn/insert-new-src-block ()
+  (interactive)
+  (let* ((current-src-block (org-element-at-point))
+         (src-block-head (save-excursion
+                           (goto-char (org-element-property
+                                       :begin current-src-block))
+                           (let ((line (thing-at-point 'line t)))
+                             (if (not (s-starts-with? "#+NAME:" (s-trim line)))
+                                 line
+                               (forward-line)
+                               (thing-at-point 'line t)))))
+         (point-to-insert
+          (if-let (results-loc (org-babel-where-is-src-block-result))
+              (save-excursion
+                (goto-char results-loc)
+                (org-element-property
+                 :end
+                 (org-element-at-point)))
+            (org-element-property :end (org-element-at-point)))))
+    (goto-char point-to-insert)
+    (insert "\n")
+    (insert src-block-head)
+    (let ((contents (point-marker)))
+      (insert "\n#+END_SRC\n")
+      (goto-char contents))))
+
+(defun grfn/+org-insert-item (orig direction)
+  (interactive)
+  (if (and (org-in-src-block-p)
+           (equal direction 'below))
+    (grfn/insert-new-src-block)
+    (funcall orig direction)))
+
+(advice-add #'+org--insert-item :around #'grfn/+org-insert-item)
+;; (advice-add #'+org/insert-item-below :around
+;;             (lambda (orig) (grfn/+org-insert-item orig 'below)))
+
+(defun set-pdb-trace ()
+  (interactive)
+  (end-of-line)
+  (insert (format "\n%simport pdb;pdb.set_trace()"
+                  (make-string (python-indent-calculate-indentation)
+                               ?\s)))
+  (evil-indent (line-beginning-position)
+               (line-end-position)))
+
+(map!
+
+ (:map magit-mode-map
+   :n "#" 'forge-dispatch)
+
+ (:map haskell-mode-map
+   :n "K"     'lsp-info-under-point
+   :n "g d"   'lsp-ui-peek-find-definitions
+   :n "g r"   'lsp-ui-peek-find-references
+   :n "g \\"  '+haskell/repl
+   ;; :n "K"     'intero-info
+   ;; :n "g d"   'intero-goto-definition
+   ;; :n "g SPC" 'intero-repl-load
+   ;; :n "g \\"  'intero-repl
+   ;; :n "g y"   'intero-type-at
+   ;; :n "g RET" 'grfn/run-sputnik-test-for-file
+
+   (:localleader
+     :desc "Apply action"  :n "e" 'intero-repl-eval-region
+     :desc "Rename symbol" :n "r" 'intero-apply-suggestions))
+
+ (:map python-mode-map
+   :n "K" #'anaconda-mode-show-doc
+   :n "g SPC" #'+eval/buffer
+   :n "g RET" #'python-pytest-file
+   :n "g \\" #'+python/open-ipython-repl
+   [remap evil-commentary-yank] #'set-pdb-trace)
+
+ (:after agda2-mode
+   (:map agda2-mode-map
+     :n "g SPC" 'agda2-load
+     :n "g d"   'agda2-goto-definition-keyboard
+     :n "] g"   'agda2-next-goal
+     :n "[ g"   'agda2-previous-goal
+
+     (:localleader
+       :desc "Give"                               :n "SPC" 'agda2-give
+       :desc "Case Split"                         :n "c"   'agda2-make-case
+       :desc "Make Helper"                        :n "h"   'agda2-helper-function-type
+       :desc "Refine"                             :n "r"   'agda2-refine
+       :desc "Auto"                               :n "a"   'agda2-auto-maybe-all
+       :desc "Goal type and context"              :n "t"   'agda2-goal-and-context
+       :desc "Goal type and context and inferred" :n ";"   'agda2-goal-and-context-and-inferred)))
+
+ (:after clojure-mode
+   (:map clojure-mode-map
+     :n "] f" 'forward-sexp
+     :n "[ f" 'backward-sexp))
+
+ (:after cider-mode
+   (:map cider-mode-map
+     :n "g SPC" 'cider-eval-buffer
+     :n "g \\"  'cider-switch-to-repl-buffer
+     :n "K"     'cider-doc
+     :n "g K"   'cider-apropos
+     :n "g d"   'cider-find-dwim
+     :n "C-w ]" 'cider-find-dwim-other-window
+     ;; :n "g RET" 'cider-test-run-ns-tests
+     :n "g RET" 'grfn/run-clj-or-cljs-test
+     :n "g r" #'cljr-rename-symbol
+
+     "C-c C-r r" 'cljr-add-require-to-ns
+     "C-c C-r i" 'cljr-add-import-to-ns
+
+     (:localleader
+       ;; :desc "Inspect last result" :n "i" 'cider-inspect-last-result
+       ;; :desc "Search for documentation" :n "h s" 'cider-apropos-doc
+       :desc "Add require to ns" :n "n r" 'cljr-add-require-to-ns
+       :desc "Add import to ns" :n "n i" 'cljr-add-import-to-ns))
+   (:map cider-repl-mode-map
+     :n "g \\" 'cider-switch-to-last-clojure-buffer))
+
+ (:after w3m
+   (:map w3m-mode-map
+     "/" #'evil-search-forward
+     "?" #'evil-search-backward
+     "r" #'w3m-reload-this-page))
+
+ (:after slack
+   (:map slack-message-buffer-mode-map
+     :i "<up>" #'slack-message-edit))
+
+ (:after org
+   :n "C-c C-x C-o" #'org-clock-out
+   (:map org-mode-map
+     [remap counsel-imenu] #'counsel-org-goto
+     "M-k" #'org-move-subtree-up
+     "M-j" #'org-move-subtree-down
+     (:localleader
+       :n "g" #'counsel-org-goto))
+
+   (:map org-capture-mode-map
+     :n "g RET" #'org-capture-finalize
+     :n "g \\"  #'org-captue-refile))
+
+ (:map lsp-mode-map
+   :n "K"   #'lsp-describe-thing-at-point
+   :n "g r" #'lsp-rename
+   (:localleader
+    :n "a" #'lsp-execute-code-action))
+
+ (:map prolog-mode-map
+  :n "g SPC" #'prolog-compile-buffer
+  :n "g \\" #'run-prolog))
diff --git a/users/grfn/emacs.d/+commands.el b/users/grfn/emacs.d/+commands.el
new file mode 100644
index 0000000000..518f185cb9
--- /dev/null
+++ b/users/grfn/emacs.d/+commands.el
@@ -0,0 +1,149 @@
+;; -*- lexical-binding: t; -*-
+
+(defalias 'ex! 'evil-ex-define-cmd)
+
+(defun delete-file-and-buffer ()
+  "Kill the current buffer and deletes the file it is visiting."
+  (interactive)
+  (let ((filename (buffer-file-name)))
+    (when filename
+      (if (vc-backend filename)
+          (vc-delete-file filename)
+        (progn
+          (delete-file filename)
+          (message "Deleted file %s" filename)
+          (kill-buffer))))))
+
+;;; Commands defined elsewhere
+;;(ex! "al[ign]"      #'+evil:align)
+;;(ex! "g[lobal]"     #'+evil:global)
+
+;;; Custom commands
+;; Editing
+(ex! "@"            #'+evil:macro-on-all-lines)   ; TODO Test me
+(ex! "al[ign]"      #'+evil:align)
+(ex! "enhtml"       #'+web:encode-html-entities)
+(ex! "dehtml"       #'+web:decode-html-entities)
+(ex! "mc"           #'+evil:mc)
+(ex! "iedit"        #'evil-multiedit-ex-match)
+(ex! "na[rrow]"     #'+evil:narrow-buffer)
+(ex! "retab"        #'+evil:retab)
+
+(ex! "glog" #'magit-log-buffer-file)
+
+;; External resources
+;; TODO (ex! "db"          #'doom:db)
+;; TODO (ex! "dbu[se]"     #'doom:db-select)
+;; TODO (ex! "go[ogle]"    #'doom:google-search)
+(ex! "lo[okup]"    #'+jump:online)
+(ex! "dash"        #'+lookup:dash)
+(ex! "dd"          #'+lookup:devdocs)
+(ex! "http"        #'httpd-start)            ; start http server
+(ex! "repl"        #'+eval:repl)             ; invoke or send to repl
+;; TODO (ex! "rx"          'doom:regex)             ; open re-builder
+(ex! "sh[ell]"     #'+eshell:run)
+(ex! "t[mux]"      #'+tmux:run)              ; send to tmux
+(ex! "tcd"         #'+tmux:cd-here)          ; cd to default-directory in tmux
+(ex! "x"           #'doom/open-project-scratch-buffer)
+
+;; GIT
+(ex! "gist"        #'+gist:send)  ; send current buffer/region to gist
+(ex! "gistl"       #'+gist:list)  ; list gists by user
+(ex! "gbrowse"     #'+vcs/git-browse)        ; show file in github/gitlab
+(ex! "gissues"     #'+vcs/git-browse-issues) ; show github issues
+(ex! "git"         #'magit-status)           ; open magit status window
+(ex! "gstage"      #'magit-stage)
+(ex! "gunstage"    #'magit-unstage)
+(ex! "gblame"      #'magit-blame)
+(ex! "grevert"     #'git-gutter:revert-hunk)
+
+;; Dealing with buffers
+(ex! "clean[up]"   #'doom/cleanup-buffers)
+(ex! "k[ill]"      #'doom/kill-this-buffer)
+(ex! "k[ill]all"   #'+hlissner:kill-all-buffers)
+(ex! "k[ill]m"     #'+hlissner:kill-matching-buffers)
+(ex! "k[ill]o"     #'doom/kill-other-buffers)
+(ex! "l[ast]"      #'doom/popup-restore)
+(ex! "m[sg]"       #'view-echo-area-messages)
+(ex! "pop[up]"     #'doom/popup-this-buffer)
+
+;; Project navigation
+(ex! "a"           #'projectile-toggle-between-implementation-and-test)
+(ex! "as"          #'projectile-find-implementation-or-test-other-window)
+(ex! "av"          #'projectile-find-implementation-or-test-other-window)
+(ex! "cd"          #'+hlissner:cd)
+(cond ((featurep! :completion ivy)
+       (ex! "ag"       #'+ivy:ag)
+       (ex! "agc[wd]"  #'+ivy:ag-cwd)
+       (ex! "rg"       #'+ivy:rg)
+       (ex! "rgc[wd]"  #'+ivy:rg-cwd)
+       (ex! "sw[iper]" #'+ivy:swiper)
+       (ex! "todo"     #'+ivy:todo))
+      ((featurep! :completion helm)
+       (ex! "ag"       #'+helm:ag)
+       (ex! "agc[wd]"  #'+helm:ag-cwd)
+       (ex! "rg"       #'+helm:rg)
+       (ex! "rgc[wd]"  #'+helm:rg-cwd)
+       (ex! "sw[oop]"  #'+helm:swoop)
+       (ex! "todo"     #'+helm:todo)))
+
+;; Project tools
+(ex! "build"       #'+eval/build)
+(ex! "debug"       #'+debug/run)
+(ex! "er[rors]"    #'flycheck-list-errors)
+
+;; File operations
+(ex! "cp"          #'+evil:copy-this-file)
+(ex! "mv"          #'+evil:move-this-file)
+(ex! "rm"          #'+evil:delete-this-file)
+
+;; Sessions/tabs
+(ex! "sclear"      #'+workspace/kill-session)
+(ex! "sl[oad]"     #'+workspace:load-session)
+(ex! "ss[ave]"     #'+workspace:save-session)
+(ex! "tabcl[ose]"  #'+workspace:delete)
+(ex! "tabclear"    #'doom/kill-all-buffers)
+(ex! "tabl[ast]"   #'+workspace/switch-to-last)
+(ex! "tabload"     #'+workspace:load)
+(ex! "tabn[ew]"    #'+workspace:new)
+(ex! "tabn[ext]"   #'+workspace:switch-next)
+(ex! "tabp[rev]"   #'+workspace:switch-previous)
+(ex! "tabr[ename]" #'+workspace:rename)
+(ex! "tabs"        #'+workspace/display)
+(ex! "tabsave"     #'+workspace:save)
+
+(ex! "scr[atch]" #'cider-scratch)
+
+;; Org-mode
+(ex! "cap"         #'+org-capture/dwim)
+
+(evil-define-command evil-alembic-revision (args)
+  (interactive "<a>")
+  (apply
+   #'generate-alembic-migration
+   (read-string "Message: ")
+   (s-split "\\s+" (or args ""))))
+(ex! "arev[ision]" #'evil-alembic-revision)
+
+(evil-define-command evil-alembic-upgrade (&optional revision)
+  (interactive "<a>")
+  (alembic-upgrade (or revision "head")))
+
+(ex! "aup[grade]" #'evil-alembic-upgrade)
+
+(evil-define-command evil-alembic-downgrade (&optional revision)
+  (interactive "<a>")
+  (alembic-downgrade revision))
+
+(ex! "adown[grade]" #'evil-alembic-downgrade)
+
+(evil-define-command evil-alembic (args)
+  (interactive "<a>")
+  (run-alembic args))
+
+(ex! "alemb[ic]" #'evil-alembic)
+
+;; Elixir
+(add-hook! elixir-mode
+  (ex! "AV" #'alchemist-project-toggle-file-and-tests-other-window)
+  (ex! "A" #'alchemist-project-toggle-file-and-tests))
diff --git a/users/grfn/emacs.d/+private.el.gpg b/users/grfn/emacs.d/+private.el.gpg
new file mode 100644
index 0000000000..6273c67d6e
--- /dev/null
+++ b/users/grfn/emacs.d/+private.el.gpg
Binary files differdiff --git a/users/grfn/emacs.d/.gitignore b/users/grfn/emacs.d/.gitignore
new file mode 100644
index 0000000000..1fd0e39887
--- /dev/null
+++ b/users/grfn/emacs.d/.gitignore
@@ -0,0 +1,2 @@
+.authinfo.gpg
++private.el
diff --git a/users/grfn/emacs.d/autoload/evil.el b/users/grfn/emacs.d/autoload/evil.el
new file mode 100644
index 0000000000..319c93c05e
--- /dev/null
+++ b/users/grfn/emacs.d/autoload/evil.el
@@ -0,0 +1,37 @@
+;;; /autoload/evil.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
+
+;;;###autoload (autoload '+hlissner:multi-next-line "/autoload/evil" nil t)
+(evil-define-motion +hlissner:multi-next-line (count)
+  "Move down 6 lines."
+  :type line
+  (let ((line-move-visual (or visual-line-mode (derived-mode-p 'text-mode))))
+    (evil-line-move (* 6 (or count 1)))))
+
+;;;###autoload (autoload '+hlissner:multi-previous-line "/autoload/evil" nil t)
+(evil-define-motion +hlissner:multi-previous-line (count)
+  "Move up 6 lines."
+  :type line
+  (let ((line-move-visual (or visual-line-mode (derived-mode-p 'text-mode))))
+    (evil-line-move (- (* 6 (or count 1))))))
+
+;;;###autoload (autoload '+hlissner:cd "/autoload/evil" nil t)
+(evil-define-command +hlissner:cd ()
+  "Change `default-directory' with `cd'."
+  (interactive "<f>")
+  (cd input))
+
+;;;###autoload (autoload '+hlissner:kill-all-buffers "/autoload/evil" nil t)
+(evil-define-command +hlissner:kill-all-buffers (&optional bang)
+  "Kill all buffers. If BANG, kill current session too."
+  (interactive "<!>")
+  (if bang
+      (+workspace/kill-session)
+    (doom/kill-all-buffers)))
+
+;;;###autoload (autoload '+hlissner:kill-matching-buffers "/autoload/evil" nil t)
+(evil-define-command +hlissner:kill-matching-buffers (&optional bang pattern)
+  "Kill all buffers matching PATTERN regexp. If BANG, only match project
+buffers."
+  (interactive "<a>")
+  (doom/kill-matching-buffers pattern bang))
diff --git a/users/grfn/emacs.d/autoload/hlissner.el b/users/grfn/emacs.d/autoload/hlissner.el
new file mode 100644
index 0000000000..87b2236d12
--- /dev/null
+++ b/users/grfn/emacs.d/autoload/hlissner.el
@@ -0,0 +1,53 @@
+;;; autoload/hlissner.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +hlissner/install-snippets ()
+  "Install my snippets from https://github.com/hlissner/emacs-snippets into
+private/hlissner/snippets."
+  (interactive)
+  (doom-fetch :github "hlissner/emacs-snippets"
+              (expand-file-name "snippets" (doom-module-path :private 'hlissner))))
+
+;;;###autoload
+(defun +hlissner/yank-buffer-filename ()
+  "Copy the current buffer's path to the kill ring."
+  (interactive)
+  (if-let* ((filename (or buffer-file-name (bound-and-true-p list-buffers-directory))))
+      (message (kill-new (abbreviate-file-name filename)))
+    (error "Couldn't find filename in current buffer")))
+
+(defmacro +hlissner-def-finder! (name dir)
+  "Define a pair of find-file and browse functions."
+  `(progn
+     (defun ,(intern (format "+hlissner/find-in-%s" name)) ()
+       (interactive)
+       (let ((default-directory ,dir)
+             projectile-project-name
+             projectile-require-project-root
+             projectile-cached-buffer-file-name
+             projectile-cached-project-root)
+         (call-interactively (command-remapping #'projectile-find-file))))
+     (defun ,(intern (format "+hlissner/browse-%s" name)) ()
+       (interactive)
+       (let ((default-directory ,dir))
+         (call-interactively (command-remapping #'find-file))))))
+
+;;;###autoload (autoload '+hlissner/find-in-templates "autoload/hlissner" nil t)
+;;;###autoload (autoload '+hlissner/browse-templates "autoload/hlissner" nil t)
+(+hlissner-def-finder! templates +file-templates-dir)
+
+;;;###autoload (autoload '+hlissner/find-in-snippets "autoload/hlissner" nil t)
+;;;###autoload (autoload '+hlissner/browse-snippets "autoload/hlissner" nil t)
+(+hlissner-def-finder! snippets +hlissner-snippets-dir)
+
+;;;###autoload (autoload '+hlissner/find-in-dotfiles "autoload/hlissner" nil t)
+;;;###autoload (autoload '+hlissner/browse-dotfiles "autoload/hlissner" nil t)
+(+hlissner-def-finder! dotfiles (expand-file-name ".dotfiles" "~"))
+
+;;;###autoload (autoload '+hlissner/find-in-emacsd "autoload/hlissner" nil t)
+;;;###autoload (autoload '+hlissner/browse-emacsd "autoload/hlissner" nil t)
+(+hlissner-def-finder! emacsd doom-emacs-dir)
+
+;;;###autoload (autoload '+hlissner/find-in-notes "autoload/hlissner" nil t)
+;;;###autoload (autoload '+hlissner/browse-notes "autoload/hlissner" nil t)
+(+hlissner-def-finder! notes +org-dir)
diff --git a/users/grfn/emacs.d/clocked-in-elt.el b/users/grfn/emacs.d/clocked-in-elt.el
new file mode 100644
index 0000000000..be4161441d
--- /dev/null
+++ b/users/grfn/emacs.d/clocked-in-elt.el
@@ -0,0 +1,17 @@
+;;; -*- lexical-binding: t; -*-
+(load (expand-file-name "init" (or (getenv "EMACSDIR")
+               (expand-file-name
+                "../.emacs.d"
+                (file-name-directory (file-truename load-file-name))))))
+
+(require 'org-clock)
+(require 'org-element)
+
+(let ((item (or org-clock-marker
+                (car org-clock-history))))
+  (when item
+    (with-current-buffer (marker-buffer item)
+      (goto-char (marker-position item))
+      (let ((element (org-element-at-point)))
+        (when (eq 'headline (car element))
+          (message "%s" (plist-get (cadr element) :raw-value)))))))
diff --git a/users/grfn/emacs.d/clojure.el b/users/grfn/emacs.d/clojure.el
new file mode 100644
index 0000000000..f001a3e12b
--- /dev/null
+++ b/users/grfn/emacs.d/clojure.el
@@ -0,0 +1,53 @@
+;;; -*- lexical-binding: t; -*-
+
+(defun clojure-thing-at-point-setup ()
+  (interactive)
+  ;; Used by cider-find-dwim to parse the symbol at point
+  (setq-local
+   thing-at-point-file-name-chars
+   (concat thing-at-point-file-name-chars
+           "><!?")))
+
+(defun +grfn/clojure-setup ()
+  ;; (flycheck-select-checker 'clj-kondo)
+  (require 'flycheck)
+  (push 'clojure-cider-kibit flycheck-disabled-checkers)
+  (push 'clojure-cider-eastwood flycheck-disabled-checkers)
+  (push 'clojure-cider-typed flycheck-disabled-checkers)
+  )
+
+(after! clojure-mode
+  (define-clojure-indent
+    (PUT 2)
+    (POST 2)
+    (GET 2)
+    (PATCH 2)
+    (DELETE 2)
+    (context 2)
+    (checking 3)
+    (match 1)
+    (domonad 0)
+    (describe 1)
+    (before 1)
+    (it 2))
+
+  (add-hook 'clojure-mode-hook #'clojure-thing-at-point-setup)
+  (add-hook 'clojure-mode-hook #'+grfn/clojure-setup))
+
+(use-package! flycheck-clojure
+  ;; :disabled t
+  :after (flycheck cider)
+  :config
+  (flycheck-clojure-setup))
+
+(after! clj-refactor
+  (setq cljr-magic-requires :prompt
+        cljr-clojure-test-declaration "[clojure.test :refer :all]"
+        cljr-cljc-clojure-test-declaration"#?(:clj [clojure.test :refer :all]
+:cljs [cljs.test :refer-macros [deftest is testing]])"
+        )
+  (add-to-list
+   'cljr-magic-require-namespaces
+   '("s" . "clojure.spec.alpha")))
+
+(set-popup-rule! "^\\*cider-test-report" :size 0.4)
diff --git a/users/grfn/emacs.d/company-sql.el b/users/grfn/emacs.d/company-sql.el
new file mode 100644
index 0000000000..e623aa2de1
--- /dev/null
+++ b/users/grfn/emacs.d/company-sql.el
@@ -0,0 +1,299 @@
+;;; Commentary:
+;;; TODO
+
+;;; Code:
+
+(require 'emacsql)
+(require 'emacsql-psql)
+(require 'dash)
+(require 's)
+(require 'cl-lib)
+
+;;; Config
+
+(defvar-local company-sql-db-host "localhost"
+  "Host of the postgresql database to query for autocomplete information")
+
+(defvar-local company-sql-db-port 5432
+  "Port of the postgresql database to query for autocomplete information")
+
+(defvar-local company-sql-db-user "postgres"
+  "Username of the postgresql database to query for autocomplete information")
+
+(defvar-local company-sql-db-name nil
+  "PostgreSQL database name to query for autocomplete information")
+
+;;; DB Connection
+
+(defvar-local company-sql/connection nil)
+
+(defun company-sql/connect ()
+  (unless company-sql/connection
+    (setq-local company-sql/connection
+                (emacsql-psql company-sql-db-name
+                              :hostname company-sql-db-host
+                              :username company-sql-db-user
+                              :port (number-to-string company-sql-db-port))))
+  company-sql/connection)
+
+;;; Utils
+
+(defmacro comment (&rest _))
+
+(defun ->string (x)
+  (cond
+   ((stringp x) x)
+   ((symbolp x) (symbol-name x))))
+
+(defun alist-get-equal (key alist)
+  "Like `alist-get', but uses `equal' instead of `eq' for comparing keys"
+  (->> alist
+       (-find (lambda (pair) (equal key (car pair))))
+       (cdr)))
+
+;;; Listing relations
+
+(cl-defun company-sql/list-tables (conn)
+  (with-timeout (3)
+    (-map (-compose 'symbol-name 'car)
+          (emacsql conn
+                   [:select [tablename]
+                            :from pg_catalog:pg_tables
+                            :where (and (!= schemaname '"information_schema")
+                                        (!= schemaname '"pg_catalog"))]))))
+
+(cl-defun company-sql/list-columns (conn)
+  (with-timeout (3)
+    (-map
+     (lambda (row)
+       (propertize (symbol-name (nth 0 row))
+                   'table-name (nth 1 row)
+                   'data-type  (nth 2 row)))
+     (emacsql conn
+              [:select [column_name
+                        table_name
+                        data_type]
+                       :from information_schema:columns]))))
+
+;;; Keywords
+
+(defvar company-postgresql/keywords
+  (list
+"a" "abort" "abs" "absent" "absolute" "access" "according" "action" "ada" "add"
+"admin" "after" "aggregate" "all" "allocate" "also" "alter" "always" "analyse"
+"analyze" "and" "any" "are" "array" "array_agg" "array_max_cardinality" "as"
+"asc" "asensitive" "assertion" "assignment" "asymmetric" "at" "atomic" "attach"
+"attribute" "attributes" "authorization" "avg" "backward" "base64" "before"
+"begin" "begin_frame" "begin_partition" "bernoulli" "between" "bigint" "binary"
+"bit" "bit_length" "blob" "blocked" "bom" "boolean" "both" "breadth" "by" "c"
+"cache" "call" "called" "cardinality" "cascade" "cascaded" "case" "cast"
+"catalog" "catalog_name" "ceil" "ceiling" "chain" "char" "character"
+"characteristics" "characters" "character_length" "character_set_catalog"
+"character_set_name" "character_set_schema" "char_length" "check" "checkpoint"
+"class" "class_origin" "clob" "close" "cluster" "coalesce" "cobol" "collate"
+"collation" "collation_catalog" "collation_name" "collation_schema" "collect"
+"column" "columns" "column_name" "command_function" "command_function_code"
+"comment" "comments" "commit" "committed" "concurrently" "condition"
+"condition_number" "configuration" "conflict" "connect" "connection"
+"connection_name" "constraint" "constraints" "constraint_catalog"
+"constraint_name" "constraint_schema" "constructor" "contains" "content"
+"continue" "control" "conversion" "convert" "copy" "corr" "corresponding" "cost"
+"count" "covar_pop" "covar_samp" "create" "cross" "csv" "cube" "cume_dist"
+"current" "current_catalog" "current_date" "current_default_transform_group"
+"current_path" "current_role" "current_row" "current_schema" "current_time"
+"current_timestamp" "current_transform_group_for_type" "current_user" "cursor"
+"cursor_name" "cycle" "data" "database" "datalink" "date"
+"datetime_interval_code" "datetime_interval_precision" "day" "db" "deallocate"
+"dec" "decimal" "declare" "default" "defaults" "deferrable" "deferred" "defined"
+"definer" "degree" "delete" "delimiter" "delimiters" "dense_rank" "depends"
+"depth" "deref" "derived" "desc" "describe" "descriptor" "detach"
+"deterministic" "diagnostics" "dictionary" "disable" "discard" "disconnect"
+"dispatch" "distinct" "dlnewcopy" "dlpreviouscopy" "dlurlcomplete"
+"dlurlcompleteonly" "dlurlcompletewrite" "dlurlpath" "dlurlpathonly"
+"dlurlpathwrite" "dlurlscheme" "dlurlserver" "dlvalue" "do" "document" "domain"
+"double" "drop" "dynamic" "dynamic_function" "dynamic_function_code" "each"
+"element" "else" "empty" "enable" "encoding" "encrypted" "end" "end-exec"
+"end_frame" "end_partition" "enforced" "enum" "equals" "escape" "event" "every"
+"except" "exception" "exclude" "excluding" "exclusive" "exec" "execute" "exists"
+"exp" "explain" "expression" "extension" "external" "extract" "false" "family"
+"fetch" "file" "filter" "final" "first" "first_value" "flag" "float" "floor"
+"following" "for" "force" "foreign" "fortran" "forward" "found" "frame_row"
+"free" "freeze" "from" "fs" "full" "function" "functions" "fusion" "g" "general"
+"generated" "get" "global" "go" "goto" "grant" "granted" "greatest" "group"
+"grouping" "groups" "handler" "having" "header" "hex" "hierarchy" "hold" "hour"
+"id" "identity" "if" "ignore" "ilike" "immediate" "immediately" "immutable"
+"implementation" "implicit" "import" "in" "include" "including" "increment"
+"indent" "index" "indexes" "indicator" "inherit" "inherits" "initially" "inline"
+"inner" "inout" "input" "insensitive" "insert" "instance" "instantiable"
+"instead" "int" "integer" "integrity" "intersect" "intersection" "interval"
+"into" "invoker" "is" "isnull" "isolation" "join" "k" "key" "key_member"
+"key_type" "label" "lag" "language" "large" "last" "last_value" "lateral" "lead"
+"leading" "leakproof" "least" "left" "length" "level" "library" "like"
+"like_regex" "limit" "link" "listen" "ln" "load" "local" "localtime"
+"localtimestamp" "location" "locator" "lock" "locked" "logged" "lower" "m" "map"
+"mapping" "match" "matched" "materialized" "max" "maxvalue" "max_cardinality"
+"member" "merge" "message_length" "message_octet_length" "message_text" "method"
+"min" "minute" "minvalue" "mod" "mode" "modifies" "module" "month" "more" "move"
+"multiset" "mumps" "name" "names" "namespace" "national" "natural" "nchar"
+"nclob" "nesting" "new" "next" "nfc" "nfd" "nfkc" "nfkd" "nil" "no" "none"
+"normalize" "normalized" "not" "nothing" "notify" "notnull" "nowait" "nth_value"
+"ntile" "null" "nullable" "nullif" "nulls" "number" "numeric" "object"
+"occurrences_regex" "octets" "octet_length" "of" "off" "offset" "oids" "old"
+"on" "only" "open" "operator" "option" "options" "or" "order" "ordering"
+"ordinality" "others" "out" "outer" "output" "over" "overlaps" "overlay"
+"overriding" "owned" "owner" "p" "pad" "parallel" "parameter" "parameter_mode"
+"parameter_name" "parameter_ordinal_position" "parameter_specific_catalog"
+"parameter_specific_name" "parameter_specific_schema" "parser" "partial"
+"partition" "pascal" "passing" "passthrough" "password" "path" "percent"
+"percentile_cont" "percentile_disc" "percent_rank" "period" "permission"
+"placing" "plans" "pli" "policy" "portion" "position" "position_regex" "power"
+"precedes" "preceding" "precision" "prepare" "prepared" "preserve" "primary"
+"prior" "privileges" "procedural" "procedure" "procedures" "program" "public"
+"publication" "quote" "range" "rank" "read" "reads" "real" "reassign" "recheck"
+"recovery" "recursive" "ref" "references" "referencing" "refresh" "regr_avgx"
+"regr_avgy" "regr_count" "regr_intercept" "regr_r2" "regr_slope" "regr_sxx"
+"regr_sxy" "regr_syy" "reindex" "relative" "release" "rename" "repeatable"
+"replace" "replica" "requiring" "reset" "respect" "restart" "restore" "restrict"
+"result" "return" "returned_cardinality" "returned_length"
+"returned_octet_length" "returned_sqlstate" "returning" "returns" "revoke"
+"right" "role" "rollback" "rollup" "routine" "routines" "routine_catalog"
+"routine_name" "routine_schema" "row" "rows" "row_count" "row_number" "rule"
+"savepoint" "scale" "schema" "schemas" "schema_name" "scope" "scope_catalog"
+"scope_name" "scope_schema" "scroll" "search" "second" "section" "security"
+"select" "selective" "self" "sensitive" "sequence" "sequences" "serializable"
+"server" "server_name" "session" "session_user" "set" "setof" "sets" "share"
+"show" "similar" "simple" "size" "skip" "smallint" "snapshot" "some" "source"
+"space" "specific" "specifictype" "specific_name" "sql" "sqlcode" "sqlerror"
+"sqlexception" "sqlstate" "sqlwarning" "sqrt" "stable" "standalone" "start"
+"state" "statement" "static" "statistics" "stddev_pop" "stddev_samp" "stdin"
+"stdout" "storage" "strict" "strip" "structure" "style" "subclass_origin"
+"submultiset" "subscription" "substring" "substring_regex" "succeeds" "sum"
+"symmetric" "sysid" "system" "system_time" "system_user" "t" "table" "tables"
+"tablesample" "tablespace" "table_name" "temp" "template" "temporary" "text"
+"then" "ties" "time" "timestamp" "timezone_hour" "timezone_minute" "to" "token"
+"top_level_count" "trailing" "transaction" "transactions_committed"
+"transactions_rolled_back" "transaction_active" "transform" "transforms"
+"translate" "translate_regex" "translation" "treat" "trigger" "trigger_catalog"
+"trigger_name" "trigger_schema" "trim" "trim_array" "true" "truncate" "trusted"
+"type" "types" "uescape" "unbounded" "uncommitted" "under" "unencrypted" "union"
+"unique" "unknown" "unlink" "unlisten" "unlogged" "unnamed" "unnest" "until"
+"untyped" "update" "upper" "uri" "usage" "user" "user_defined_type_catalog"
+"user_defined_type_code" "user_defined_type_name" "user_defined_type_schema"
+"using" "vacuum" "valid" "validate" "validator" "value" "values" "value_of"
+"varbinary" "varchar" "variadic" "varying" "var_pop" "var_samp" "verbose"
+"version" "versioning" "view" "views" "volatile" "when" "whenever" "where"
+"whitespace" "width_bucket" "window" "with" "within" "without" "work" "wrapper"
+"write" "xml" "xmlagg" "xmlattributes" "xmlbinary" "xmlcast" "xmlcomment"
+"xmlconcat" "xmldeclaration" "xmldocument" "xmlelement" "xmlexists" "xmlforest"
+"xmliterate" "xmlnamespaces" "xmlparse" "xmlpi" "xmlquery" "xmlroot" "xmlschema"
+"xmlserialize" "xmltable" "xmltext" "xmlvalidate" "year" "yes" "zone"))
+
+;;; Company backend
+
+(cl-defun company-postgresql/candidates (prefix conn)
+  (-filter
+   (apply-partially #'s-starts-with? prefix)
+   (append (-map (lambda (s)
+                   (propertize s 'company-postgresql-annotation "table"))
+
+           (-map (lambda (s)
+                   (propertize s 'company-postgresql-annotation
+                               (format "%s.%s %s"
+                                       (get-text-property 0 'table-name s)
+                                       s
+                                       (->
+                                        (get-text-property 0 'data-type s)
+                                        (->string)
+                                        (upcase)))))
+                 (company-sql/list-columns conn))
+           (-map (lambda (s)
+                   (propertize s 'company-postgresql-annotation "keyword"))
+                 company-postgresql/keywords)))))
+
+(defun company-postgresql (command &optional arg &rest _)
+  (interactive (list 'interactive))
+  (cl-case command
+    (interactive (company-begin-backend 'company-postgresql))
+    (init (company-sql/connect))
+    (prefix (company-grab-symbol))
+    (annotation
+     (get-text-property 0 'company-postgresql-annotation arg))
+    (candidates (company-postgresql/candidates
+                 arg
+                 (company-sql/connect)))
+    (duplicates t)
+    (ignore-case t)))
+
+;;; org-babel company sql
+
+(defvar-local org-company-sql/connections
+  ())
+
+(defun org-company-sql/connect (conn-params)
+  (or (alist-get-equal conn-params org-company-sql/connections)
+      (let ((conn (apply 'emacsql-psql conn-params)))
+        (add-to-list 'org-company-sql/connections (cons conn-params conn))
+        conn)))
+
+(defun org-company-sql/in-sql-source-block-p ()
+  (let ((org-elt (org-element-at-point)))
+    (and (eq 'src-block (car org-elt))
+         (equal "sql" (plist-get (cadr org-elt)
+                                 :language)))))
+
+(defun org-company-sql/parse-cmdline (cmdline)
+  (let* ((lexed (s-split (rx (one-or-more blank)) cmdline))
+         (go (lambda (state tokens)
+               (if (null tokens) ()
+                 (let ((token (car tokens))
+                       (tokens (cdr tokens)))
+                   (if (null state)
+                       (if (s-starts-with? "-" token)
+                           (funcall go token tokens)
+                         (cons token (funcall go state tokens)))
+                     (cons (cons state token)  ; ("-h" . "localhost")
+                           (funcall go nil tokens)))))))
+         (opts (funcall go nil lexed)))
+    opts))
+
+(defun org-company-sql/source-block-conn-params ()
+  (let* ((block-info (org-babel-get-src-block-info))
+         (params (caddr block-info))
+         (cmdline (alist-get :cmdline params))
+         (parsed (org-company-sql/parse-cmdline cmdline))
+         (opts (-filter #'listp parsed))
+         (positional (-filter #'stringp parsed))
+         (host (alist-get-equal "-h" opts))
+         (port (or (alist-get-equal "-p" opts)
+                   "5432"))
+         (dbname (or (alist-get-equal "-d" opts)
+                     (car positional)))
+         (username (or (alist-get-equal "-U" opts)
+                       (cadr positional))))
+    (list dbname
+          :hostname host
+          :username username
+          :port port)))
+
+(defun org-company-sql/connection-for-source-block ()
+  (org-company-sql/connect
+   (org-company-sql/source-block-conn-params)))
+
+
+(defun company-ob-postgresql (command &optional arg &rest _)
+  (interactive (list 'interactive))
+  (cl-case command
+    (interactive (company-begin-backend 'company-ob-postgresql))
+    (prefix (and (org-company-sql/in-sql-source-block-p)
+                 (company-grab-symbol)))
+    (annotation (get-text-property 0 'company-postgresql-annotation arg))
+    (candidates
+     (company-postgresql/candidates
+      arg
+      (org-company-sql/connection-for-source-block)))
+    (duplicates t)
+    (ignore-case t)))
+
+;;;
+
+(provide 'company-sql)
diff --git a/users/grfn/emacs.d/config.el b/users/grfn/emacs.d/config.el
new file mode 100644
index 0000000000..9ddfe07afb
--- /dev/null
+++ b/users/grfn/emacs.d/config.el
@@ -0,0 +1,1121 @@
+;;; -*- lexical-binding: t; -*-
+
+;; I've swapped these keys on my keyboard
+(setq x-super-keysym 'alt
+      x-alt-keysym   'meta)
+
+(setq user-mail-address "root@gws.fyi"
+      user-full-name    "Griffin Smith")
+
+(let ((font-family (pcase system-type
+                     ('darwin "MesloLGSDZ NF")
+                     ('gnu/linux "Meslo LGSDZ Nerd Font"))))
+  (setq doom-font (font-spec :family font-family :size 14)
+        doom-big-font (font-spec :family font-family :size 24)
+        doom-big-font-increment 5
+        doom-variable-pitch-font (font-spec :family font-family)
+        doom-unicode-font (font-spec :family font-family)))
+
+(require 's)
+
+(undefine-key! :keymaps 'doom-leader-map "/")
+
+(load! "utils")
+(load! "company-sql")
+(load! "show-matching-paren")
+(load! "irc")
+(load! "github-org")
+(load! "org-gcal")
+(load! "grid")
+(load! "nix")
+(load! "email")
+(load! "cpp")
+(load! "lisp")
+(load! "clojure")
+(load! "rust")
+(load! "terraform")
+
+(require 'tvl)
+
+(add-hook! elixir-mode
+  (require 'flycheck-credo)
+  (setq flycheck-elixir-credo-strict t)
+  (flycheck-credo-setup)
+
+  (require 'flycheck-mix) (flycheck-mix-setup)
+
+  (require 'flycheck-dialyxir) (flycheck-dialyxir-setup)
+
+  (flycheck-mode))
+
+(setq +solarized-s-base03    "#002b36"
+      +solarized-s-base02    "#073642"
+      ;; emphasized content
+      +solarized-s-base01    "#586e75"
+      ;; primary content
+      +solarized-s-base00    "#657b83"
+      +solarized-s-base0     "#839496"
+      ;; comments
+      +solarized-s-base1     "#93a1a1"
+      ;; background highlight light
+      +solarized-s-base2     "#eee8d5"
+      ;; background light
+      +solarized-s-base3     "#fdf6e3"
+
+      ;; Solarized accented colors
+      +solarized-yellow    "#b58900"
+      +solarized-orange    "#cb4b16"
+      +solarized-red       "#dc322f"
+      +solarized-magenta   "#d33682"
+      +solarized-violet    "#6c71c4"
+      +solarized-blue      "#268bd2"
+      +solarized-cyan      "#2aa198"
+      +solarized-green     "#859900"
+
+      ;; Darker and lighter accented colors
+      ;; Only use these in exceptional circumstances!
+      +solarized-yellow-d  "#7B6000"
+      +solarized-yellow-l  "#DEB542"
+      +solarized-orange-d  "#8B2C02"
+      +solarized-orange-l  "#F2804F"
+      +solarized-red-d     "#990A1B"
+      +solarized-red-l     "#FF6E64"
+      +solarized-magenta-d "#93115C"
+      +solarized-magenta-l "#F771AC"
+      +solarized-violet-d  "#3F4D91"
+      +solarized-violet-l  "#9EA0E5"
+      +solarized-blue-d    "#00629D"
+      +solarized-blue-l    "#69B7F0"
+      +solarized-cyan-d    "#00736F"
+      +solarized-cyan-l    "#69CABF"
+      +solarized-green-d   "#546E00"
+      +solarized-green-l "#B4C342")
+
+(defcustom theme-overrides nil
+  "Association list of override faces to set for different custom themes.")
+
+(defadvice load-theme (after theme-set-overrides activate)
+  (dolist (theme-settings theme-overrides)
+    (let ((theme (car theme-settings))
+          (faces (cadr theme-settings)))
+      (if (member theme custom-enabled-themes)
+          (progn
+            (dolist (face faces)
+              (custom-theme-set-faces theme face)))))))
+
+(defun alist-set (alist-symbol key value)
+  "Set VALUE of a KEY in ALIST-SYMBOL."
+  (set alist-symbol (cons (list key value) (assq-delete-all key (eval alist-symbol)))))
+
+(comment
+ (custom-theme-set-faces 'grfn-solarized-light
+                         `(font-lock-doc-face
+                           ((t (:foreground ,+solarized-s-base1)))))
+
++solarized-s-base1
+(custom-theme-)
+ (custom-face-get-current-spec 'font-lock-doc-face)
+
+ )
+
+(alist-set 'theme-overrides 'grfn-solarized-light
+           `((font-lock-doc-face ((t (:foreground ,+solarized-s-base1))))
+             (font-lock-preprocessor-face ((t (:foreground ,+solarized-red))))
+             (font-lock-keyword-face ((t (:foreground ,+solarized-green :bold nil))))
+             (font-lock-builtin-face ((t (:foreground ,+solarized-s-base01
+                                                      :bold t))))
+
+             (elixir-attribute-face ((t (:foreground ,+solarized-blue))))
+             (elixir-atom-face ((t (:foreground ,+solarized-cyan))))
+             (linum ((t (:background ,+solarized-s-base2 :foreground ,+solarized-s-base1))))
+             (line-number ((t (:background ,+solarized-s-base2 :foreground ,+solarized-s-base1))))
+             (line-number-current-line ((t (:background ,+solarized-s-base2 :foreground ,+solarized-s-base1))))
+
+             (haskell-operator-face ((t (:foreground ,+solarized-green))))
+             (haskell-keyword-face ((t (:foreground ,+solarized-cyan))))
+
+             (org-drawer ((t (:foreground ,+solarized-s-base1
+                              :bold t))))))
+
+(setq solarized-use-variable-pitch nil
+      solarized-scale-org-headlines nil
+      solarized-use-less-bold t)
+
+(add-to-list 'custom-theme-load-path "~/.doom.d/themes")
+(load-theme 'grfn-solarized-light t)
+
+(defface haskell-import-face `((t (:foreground ,+solarized-magenta))) "")
+
+(setq doom-theme 'grfn-solarized-light)
+; (setq doom-theme 'doom-solarized-light)
+
+(add-hook! doom-post-init
+  (set-face-attribute 'bold nil :weight 'ultra-light)
+  (set-face-bold 'bold nil)
+  (enable-theme 'grfn-solarized-light))
+
+(defun rx-words (&rest words)
+  (rx-to-string
+   `(and symbol-start (group (or ,@words)) symbol-end)))
+
+(font-lock-add-keywords
+ 'elixir-mode
+ `((,(rx-words "def"
+               "defp"
+               "test"
+               "describe"
+               "property"
+               "defrecord"
+               "defmodule"
+               "defstruct"
+               "defdelegate"
+               "defprotocol"
+               "defimpl"
+               "use"
+               "import"
+               "alias"
+               "require"
+               "assert"
+               "refute"
+               "assert_raise")
+    .
+    'font-lock-preprocessor-face)))
+
+(font-lock-add-keywords
+ 'elixir-mode
+ `((,(rx-words "def"
+               "defp"
+               "test"
+               "describe"
+               "property"
+               "defrecord"
+               "defmodule"
+               "defstruct"
+               "defdelegate"
+               "use"
+               "import"
+               "alias"
+               "require"
+               "assert"
+               "refute"
+               "assert_raise")
+    .
+    'font-lock-preprocessor-face)))
+
+(font-lock-add-keywords
+ 'haskell-mode
+ `((,(rx-words "import") . 'haskell-import-face)))
+
+;; (font-lock-add-keywords
+;;  'haskell-mode
+;;  `((,(rx "-- |") . 'haskell-keyword-face)))
+
+
+;; (load-file (let ((coding-system-for-read 'utf-8))
+;;                 (shell-command-to-string "agda-mode locate")))
+
+(defvar +grfn-dir (file-name-directory load-file-name))
+(defvar +grfn-snippets-dir (expand-file-name "snippets/" +grfn-dir))
+
+;;
+(load! "+bindings")
+(load! "+commands")
+(load! "cpp")
+
+
+(add-to-list 'load-path "/home/grfn/code/org-tracker")
+(require 'org-tracker)
+(use-package! org-tracker
+  :hook (org-mode . org-tracker-mode)
+  :config
+  (setq org-tracker-state-alist '(("INBOX" . "Inbox")
+                                  ("BACKLOG" . "Backlog")
+                                  ("TODO" . "Selected for Development")
+                                  ("ACTIVE" . "In Progress")
+                                  ("PR" . "Code Review")
+                                  ("DONE" . "Done"))
+        org-tracker-username "griffin@readyset.io"
+        org-tracker-claim-ticket-on-status-update '("ACTIVE" "PR" "DONE")
+        org-tracker-create-stories-with-labels 'existing)
+
+  (defun org-tracker-headlines-from-assigned-to-me (level)
+    (interactive "*nLevel: ")
+    (funcall-interactively
+     #'org-tracker-headlines-from-search
+     level
+     "assignee = currentUser() and statusCategory = 2")))
+
+(load! "+private")
+
+(require 'dash)
+
+(use-package! predd)
+
+
+;;
+;; Global config
+;;
+
+(setq doom-modeline-buffer-file-name-style 'relative-to-project
+      doom-modeline-modal-icon nil
+      doom-modeline-github t)
+
+;;
+;; Modules
+;;
+
+(after! smartparens
+  ;; Auto-close more conservatively and expand braces on RET
+  (let ((unless-list '(sp-point-before-word-p
+                       sp-point-after-word-p
+                       sp-point-before-same-p)))
+    (sp-pair "'"  nil :unless unless-list)
+    (sp-pair "\"" nil :unless unless-list))
+  (sp-pair "{" nil :post-handlers '(("||\n[i]" "RET") ("| " " "))
+           :unless '(sp-point-before-word-p sp-point-before-same-p))
+  (sp-pair "(" nil :post-handlers '(("||\n[i]" "RET") ("| " " "))
+           :unless '(sp-point-before-word-p sp-point-before-same-p))
+  (sp-pair "[" nil :post-handlers '(("| " " "))
+           :unless '(sp-point-before-word-p sp-point-before-same-p)))
+
+;; feature/snippets
+(after! yasnippet
+  ;; Don't use default snippets, use mine.
+  (setq yas-snippet-dirs
+        (append (list '+grfn-snippets-dir)
+                (delq 'yas-installed-snippets-dir yas-snippet-dirs))))
+
+(after! company
+  (setq company-idle-delay 0.2
+        company-minimum-prefix-length 1))
+
+(setq doom-modeline-height 12)
+
+
+
+;; Should really figure out which of these is correct, eventually
+
+(setq +solarized-s-base03    "#002b36"
+      +solarized-s-base02    "#073642"
+      ;; emphasized content
+      +solarized-s-base01    "#586e75"
+      ;; primary content
+      +solarized-s-base00    "#657b83"
+      +solarized-s-base0     "#839496"
+      ;; comments
+      +solarized-s-base1     "#93a1a1"
+      ;; background highlight light
+      +solarized-s-base2     "#eee8d5"
+      ;; background light
+      +solarized-s-base3     "#fdf6e3"
+
+      ;; Solarized accented colors
+      +solarized-yellow    "#b58900"
+      +solarized-orange    "#cb4b16"
+      +solarized-red       "#dc322f"
+      +solarized-magenta   "#d33682"
+      +solarized-violet    "#6c71c4"
+      +solarized-blue      "#268bd2"
+      +solarized-cyan      "#2aa198"
+      +solarized-green     "#859900"
+
+      ;; Darker and lighter accented colors
+      ;; Only use these in exceptional circumstances!
+      +solarized-yellow-d  "#7B6000"
+      +solarized-yellow-l  "#DEB542"
+      +solarized-orange-d  "#8B2C02"
+      +solarized-orange-l  "#F2804F"
+      +solarized-red-d     "#990A1B"
+      +solarized-red-l     "#FF6E64"
+      +solarized-magenta-d "#93115C"
+      +solarized-magenta-l "#F771AC"
+      +solarized-violet-d  "#3F4D91"
+      +solarized-violet-l  "#9EA0E5"
+      +solarized-blue-d    "#00629D"
+      +solarized-blue-l    "#69B7F0"
+      +solarized-cyan-d    "#00736F"
+      +solarized-cyan-l    "#69CABF"
+      +solarized-green-d   "#546E00"
+      +solarized-green-l "#B4C342")
+
+(set-cursor-color +solarized-s-base02)
+
+(after! doom-theme
+  (set-face-foreground 'font-lock-doc-face +solarized-s-base1)
+  (set-face-foreground 'org-block +solarized-s-base00)
+  (set-face-foreground 'slack-message-output-header +solarized-s-base01)
+  (set-face-attribute 'slack-message-output-header nil :underline nil)
+  (set-face-attribute 'slack-message-output-text nil :height 1.0)
+  )
+
+(after! solarized-theme
+  (set-face-foreground 'font-lock-doc-face +solarized-s-base1)
+  (set-face-foreground 'org-block +solarized-s-base00)
+
+  (set-face-foreground 'slack-message-output-header +solarized-s-base01)
+  (set-face-attribute 'slack-message-output-header nil :underline nil)
+  (set-face-attribute 'slack-message-output-text nil :height 1.0)
+  )
+
+(after! evil
+  (setq evil-shift-width 2))
+
+(after! org
+  (load! "org-query")
+  (load! "org-config"))
+
+(after! magit
+  (setq git-commit-summary-max-length 50))
+
+(after! ivy
+  ;; (setq ivy-re-builders-alist
+  ;;       '((t . ivy--regex-fuzzy)))
+  )
+
+(add-hook 'before-save-hook 'delete-trailing-whitespace)
+
+(after! paxedit
+  (add-hook! emacs-lisp-mode #'paxedit-mode)
+  (add-hook! clojure-mode #'paxedit-mode)
+  (add-hook! common-lisp-mode #'paxedit-mode))
+
+(require 'haskell)
+
+(let ((m-symbols
+      '(("`mappend`" . "⊕")
+        ("<>"        . "⊕")
+        ("`elem`"   . "∈")
+        ("`notElem`" . "∉"))))
+  (dolist (item m-symbols) (add-to-list 'haskell-font-lock-symbols-alist item)))
+
+(setq haskell-font-lock-symbols t)
+
+
+(add-hook! haskell-mode
+  ;; (intero-mode)
+  (lsp-mode)
+  ;; (flycheck-add-next-checker
+  ;;  'intero
+  ;;  'haskell-hlint)
+  (set-fill-column 80)
+  (setq evil-shift-width 2))
+
+(auth-source-pass-enable)
+
+(require 'fill-column-indicator)
+;;; * Column Marker
+(defun sanityinc/fci-enabled-p () (symbol-value 'fci-mode))
+
+(defvar sanityinc/fci-mode-suppressed nil)
+(make-variable-buffer-local 'sanityinc/fci-mode-suppressed)
+
+(defadvice popup-create (before suppress-fci-mode activate)
+  "Suspend fci-mode while popups are visible"
+  (let ((fci-enabled (sanityinc/fci-enabled-p)))
+    (when fci-enabled
+      (setq sanityinc/fci-mode-suppressed fci-enabled)
+      (turn-off-fci-mode))))
+
+(defadvice popup-delete (after restore-fci-mode activate)
+  "Restore fci-mode when all popups have closed"
+  (when (and sanityinc/fci-mode-suppressed
+             (null popup-instances))
+    (setq sanityinc/fci-mode-suppressed nil)
+    (turn-on-fci-mode)))
+
+
+;;; Javascript
+
+(require 'smartparens)
+
+(setq js-indent-level 2)
+
+(require 'prettier-js)
+(after! prettier-js
+  (add-hook! rjsx-mode #'prettier-js-mode)
+  (add-hook! js2-mode  #'prettier-js-mode)
+  (add-hook! json-mode #'prettier-js-mode)
+  (add-hook! css-mode  #'prettier-js-mode))
+
+(remove-hook 'js2-mode-hook 'tide-setup t)
+
+;; Set this to the mode you use, I use rjsx-mode
+(add-hook 'rjsx-mode-hook #'flow/set-flow-executable t)
+
+
+;; Auto-format Haskell on save, with a combination of hindent + brittany
+
+; (define-minor-mode brittany-haskell-mode
+;   :init-value nil
+;   :group 'haskell
+;   :lighter "Brittany-Haskell"
+;   :keymap '()
+;   )
+
+
+(require 'alert)
+(setq alert-default-style 'libnotify)
+
+;; (setq slack-buffer-function #'switch-to-buffer)
+
+(setq projectile-test-suffix-function
+      (lambda (project-type)
+        (case project-type
+          ('haskell-stack "Test")
+          ('npm ".test")
+          (otherwise (projectile-test-suffix project-type)))))
+
+(setq projectile-create-missing-test-files 't)
+
+(setq grfn/jira-refs-re
+      (rx line-start
+          (or "Refs" "Fixes")
+          ": "
+          "ENG-" (one-or-more digit)
+          line-end))
+
+(defun grfn/add-jira-reference-to-commit-message ()
+  (interactive)
+  (when-let* ((jira-id (grfn/org-clocked-in-jira-ticket-id)))
+    (save-excursion
+      (save-match-data
+        (goto-char (point-min))
+        ;; Don't add one if we've already got one
+        (unless (search-forward-regexp grfn/jira-refs-re nil t)
+          (or
+           (and
+            (search-forward-regexp (rx line-start "Change-Id:") nil t)
+            (forward-line -1))
+           (and
+            (search-forward-regexp (rx line-start "# Please enter") nil t)
+            (forward-line -2)))
+          (insert (format "\nRefs: %s" jira-id)))))))
+
+(defun grfn/switch-jira-refs-fixes ()
+  (interactive)
+  (save-excursion
+    (save-match-data
+      (if (not (search-forward-regexp grfn/jira-refs-re nil t))
+          (message "Could not find reference to JIRA ticket")
+        (goto-char (point-at-bol))
+        (save-restriction
+          (narrow-to-region (point-at-bol)
+                            (point-at-eol))
+          (or
+           (and (search-forward "Refs" nil t)
+                (replace-match "Fixes"))
+           (and (search-forward "Fixes" nil t)
+                (replace-match "Refs"))))))))
+
+(after! magit
+  (map! :map magit-mode-map
+        ;; :n "] ]" #'magit-section-forward
+        ;; :n "[ [" #'magit-section-backward
+        )
+
+  (define-suffix-command magit-commit-wip ()
+    (interactive)
+    (magit-commit-create '("-m" "wip")))
+
+  (transient-append-suffix
+    #'magit-commit
+    ["c"]
+    (list "W" "Commit WIP" #'magit-commit-wip))
+
+  (define-suffix-command magit-reset-head-back ()
+    (interactive)
+    (magit-reset-mixed "HEAD~"))
+
+  (define-suffix-command magit-reset-head-previous ()
+    (interactive)
+    (magit-reset-mixed "HEAD@{1}"))
+
+  (transient-append-suffix
+    #'magit-reset
+    ["f"]
+    (list "b" "Reset HEAD~"    #'magit-reset-head-back))
+  (transient-append-suffix
+    #'magit-reset
+    ["f"]
+    (list "o" "Reset HEAD@{1}" #'magit-reset-head-previous))
+
+  (defun magit-read-org-tracker-branch-name ()
+    (when-let ((issue-id (org-tracker-clocked-in-issue-id)))
+      (let ((desc
+             (magit-read-string-ns
+              (format "Issue description (to go after gs/%s/)"
+                      issue-id))))
+        (format "gs/%s/%s" issue-id desc))))
+
+  (defun magit-read-org-tracker-branch-args ()
+    (if-let ((issue-id (org-tracker-clocked-in-issue-id)))
+        (let ((start-point (magit-read-starting-point
+                            "Create and checkout branch for Tracker issue"
+                            nil
+                            "origin/master")))
+          (if (magit-rev-verify start-point)
+              (when-let ((desc (magit-read-org-tracker-branch-name)))
+                (list desc start-point))
+            (user-error "Not a valid starting point: %s" choice)))
+      (user-error "No currently clocked-in tracker issue")))
+
+  (transient-define-suffix magit-checkout-org-tracker-branch (branch start-point)
+    (interactive (magit-read-org-tracker-branch-args))
+    (magit-branch-and-checkout branch start-point))
+
+  (transient-define-suffix magit-rename-org-tracker-branch (old new)
+    (interactive
+     (let ((branch (magit-read-local-branch "Rename branch")))
+       (list branch (magit-read-org-tracker-branch-name))))
+    (when (and old new)
+      (magit-branch-rename old new)))
+
+  (transient-append-suffix
+    #'magit-branch
+    ["c"]
+    (list "C" "Checkout Tracker branch" #'magit-checkout-org-tracker-branch))
+  (transient-append-suffix
+    #'magit-branch
+    ["c"]
+    (list "M" "Rename branch to Tracker ticket" #'magit-rename-org-tracker-branch))
+
+  )
+
+(add-hook 'git-commit-setup-hook #'grfn/add-jira-reference-to-commit-message)
+(map! (:map git-commit-mode-map
+       "C-c C-f" #'grfn/switch-jira-refs-fixes))
+
+;; (defun grfn/split-window-more-sensibly (&optional window)
+;;   (let ((window (or window (selected-window))))
+;;     (or (and (window-splittable-p window)
+;;              ;; Split window vertically.
+;;              (with-selected-window window
+;;                (split-window-right)))
+;;         (and (window-splittable-p window t)
+;;              ;; Split window horizontally.
+;;              (with-selected-window window
+;;                (split-window-right)))
+;;         (and (eq window (frame-root-window (window-frame window)))
+;;              (not (window-minibuffer-p window))
+;;              ;; If WINDOW is the only window on its frame and is not the
+;;              ;; minibuffer window, try to split it vertically disregarding
+;;              ;; the value of `split-height-threshold'.
+;;              (let ((split-height-threshold 0))
+;;                (when (window-splittable-p window)
+;;                  (with-selected-window window
+;;                    (split-window-below))))))))
+
+(use-package! lsp-mode
+  :after (:any haskell-mode)
+  :config
+  (setq lsp-response-timeout 60)
+  :hook
+  (haskell-mode . lsp-mode))
+
+(use-package! lsp-ui
+  :after lsp-mode
+  :config
+  (defun +grfn/lsp-ui-doc-frame-hook (frame window)
+    (set-frame-font (if doom-big-font-mode doom-big-font doom-font)
+                    nil (list frame)))
+  (setq lsp-ui-flycheck-enable t
+        lsp-ui-doc-header nil
+        lsp-ui-doc-position 'top
+        lsp-ui-doc-alignment 'window
+        lsp-ui-doc-frame-hook '+grfn/lsp-ui-doc-frame-hook
+        lsp-ui-doc-max-width 150
+        lsp-ui-doc-max-height 13)
+  (setq imenu-auto-rescan t)
+  (set-face-background 'lsp-ui-doc-background +solarized-s-base2)
+  (set-face-background 'lsp-face-highlight-read +solarized-s-base2)
+  (set-face-background 'lsp-face-highlight-write +solarized-s-base2)
+  :hook
+  (lsp-mode . lsp-ui-mode)
+  (lsp-ui-mode . flycheck-mode))
+
+(use-package! company-lsp
+  :after (lsp-mode lsp-ui)
+  :config
+  (add-to-list #'company-backends #'company-lsp)
+  (setq company-lsp-async t))
+
+(defun +grfn/haskell-mode-setup ()
+  (interactive)
+  (flymake-mode -1)
+  (add-to-list 'flycheck-disabled-checkers 'haskell-ghc)
+
+  (flycheck-remove-next-checker 'lsp 'haskell-ghc)
+  (flycheck-add-next-checker 'lsp '(warning . haskell-hlint))
+
+  ;; If there’s a 'hie.sh' defined locally by a project
+  ;; (e.g. to run HIE in a nix-shell), use it…
+  (when-let ((project-dir (locate-dominating-file default-directory "hie.sh")))
+    (cl-flet
+        ((which (cmd)
+                (s-trim
+                 (shell-command-to-string
+                  (concat
+                   "nix-shell "
+                   (expand-file-name "shell.nix" project-dir)
+                   " --run \"which " cmd "\" 2>/dev/null")))))
+      (setq-local
+       lsp-haskell-process-path-hie (expand-file-name "hie.sh" project-dir)
+       haskell-hoogle-command (which "hoogle"))))
+  ;; … and only then setup the LSP.
+  (lsp))
+
+(defun never-flymake-mode (orig &rest args)
+  (when (and (bound-and-true-p flymake-mode))
+    (funcall orig 0)
+    (message "disabled flymake-mode")))
+(advice-add #'flymake-mode :around #'never-flymake-mode)
+
+(defun +grfn/wrap-lsp-haskell-process (argv)
+  (let* ((project-dir (locate-dominating-file
+                       (buffer-file-name)
+                       "hie.yaml"))
+         (shell-dot-nix (expand-file-name "shell.nix" project-dir)))
+    ;; (when (string-equal default-directory "/home/grfn/code/depot")
+    ;;   (debug))
+    (message "%s %s %s %s"
+             (buffer-file-name)
+             default-directory
+             project-dir
+             shell-dot-nix)
+    (if (file-exists-p shell-dot-nix)
+        `("bash" "-c"
+          ,(format "cd %s && nix-shell %s --run '%s'"
+                   project-dir
+                   shell-dot-nix
+                   (s-join " " argv)))
+      argv)))
+
+(use-package! lsp-haskell
+  :after (lsp-mode lsp-ui haskell-mode)
+  ;; :hook
+  ;; (haskell-mode . lsp-haskell-enable)
+  :config
+  (setq lsp-haskell-process-path-hie "haskell-language-server-wrapper"
+        lsp-haskell-process-args-hie
+        '("-d" "-l" "/tmp/hie.log" "+RTS" "-M4G" "-H1G" "-K4G" "-A16M" "-RTS")
+        lsp-haskell-process-wrapper-function
+        #'+grfn/wrap-lsp-haskell-process)
+  (add-hook 'haskell-mode-hook #'+grfn/haskell-mode-setup 't))
+
+(use-package! lsp-imenu
+  :after (lsp-mode lsp-ui)
+  :hook
+  (lsp-after-open . lsp-enable-imenu))
+
+;; (use-package! counsel-etags
+;;   :ensure t
+;;   :init
+;;   (add-hook 'haskell-mode-hook
+;;             (lambda ()
+;;               (add-hook 'after-save-hook
+;;                         'counsel-etags-virtual-update-tags 'append 'local)))
+;;   :config
+;;   (setq counsel-etags-update-interval 60)
+;;   ;; (push "build" counsel-etags-ignore-directories)
+;;   )
+
+;; (use-package! evil-magit
+;;   :after (magit))
+
+(use-package! writeroom-mode)
+
+(use-package! graphql-mode)
+
+
+(require 'whitespace)
+(setq whitespace-style '(face lines-tail))
+(global-whitespace-mode t)
+(add-hook 'org-mode-hook (lambda ()  (whitespace-mode -1)) t)
+
+(set-face-foreground 'whitespace-line +solarized-red)
+(set-face-attribute 'whitespace-line nil :underline 't)
+
+;; (set-face-background 'ivy-posframe +solarized-s-base3)
+;; (set-face-foreground 'ivy-posframe +solarized-s-base01)
+
+(let ((base03    "#002b36")
+      (base02    "#073642")
+      (base01    "#586e75")
+      (base00    "#657b83")
+      (base0     "#839496")
+      (base1     "#93a1a1")
+      (base2     "#eee8d5")
+      (base3     "#fdf6e3")
+      (yellow    "#b58900")
+      (orange    "#cb4b16")
+      (red       "#dc322f")
+      (magenta   "#d33682")
+      (violet    "#6c71c4")
+      (blue      "#268bd2")
+      (cyan      "#2aa198")
+      (green     "#859900"))
+  (custom-set-faces
+   `(agda2-highlight-keyword-face ((t (:foreground ,green))))
+   `(agda2-highlight-string-face ((t (:foreground ,cyan))))
+   `(agda2-highlight-number-face ((t (:foreground ,violet))))
+   `(agda2-highlight-symbol-face ((((background ,base3)) (:foreground ,base01))))
+   `(agda2-highlight-primitive-type-face ((t (:foreground ,blue))))
+   `(agda2-highlight-bound-variable-face ((t nil)))
+   `(agda2-highlight-inductive-constructor-face ((t (:foreground ,green))))
+   `(agda2-highlight-coinductive-constructor-face ((t (:foreground ,yellow))))
+   `(agda2-highlight-datatype-face ((t (:foreground ,blue))))
+   `(agda2-highlight-field-face ((t (:foreground ,red))))
+   `(agda2-highlight-function-face ((t (:foreground ,blue))))
+   `(agda2-highlight-module-face ((t (:foreground ,yellow))))
+   `(agda2-highlight-postulate-face ((t (:foreground ,blue))))
+   `(agda2-highlight-primitive-face ((t (:foreground ,blue))))
+   `(agda2-highlight-record-face ((t (:foreground ,blue))))
+   `(agda2-highlight-dotted-face ((t nil)))
+   `(agda2-highlight-operator-face ((t nil)))
+   `(agda2-highlight-error-face ((t (:foreground ,red :underline t))))
+   `(agda2-highlight-unsolved-meta-face ((t (:background ,base2))))
+   `(agda2-highlight-unsolved-constraint-face ((t (:background ,base2))))
+   `(agda2-highlight-termination-problem-face ((t (:background ,orange :foreground ,base03))))
+   `(agda2-highlight-incomplete-pattern-face ((t (:background ,orange :foreground ,base03))))
+   `(agda2-highlight-typechecks-face ((t (:background ,cyan :foreground ,base03))))))
+
+
+(after! cider
+  (setq cider-prompt-for-symbol nil
+        cider-font-lock-dynamically 't
+        cider-save-file-on-load 't)
+  )
+
+(comment
+ (setq elt (+org-clocked-in-element))
+
+ (eq 'headline (car elt))
+ (plist-get (cadr elt) :raw-value)
+ )
+
+(defun +org-headline-title (headline)
+  (when (eq 'headline (car elt))
+    (plist-get (cadr elt) :raw-value)))
+
+;; (setq +ligatures-extra-symbols
+;;       (append +ligatures-extra-symbols
+;;               '(:equal     "≡"
+;;                 :not-equal "≠"
+;;                 :is        "≣"
+;;                 :isnt      "≢"
+;;                 :lte       "≤"
+;;                 :gte       "≥"
+;;                 :subseteq  "⊆"
+;;                 )))
+
+;; (after! python
+;;   (set-pretty-symbols! 'python-mode :merge t
+;;     :equal      "=="
+;;     :not-equal "!="
+;;     :lte "<="
+;;     :gte ">="
+;;     :is  "is"
+;;     :isnt "is not"
+;;     :subseteq "issubset"
+
+;;     ;; doom builtins
+
+;;     ;; Functional
+;;     :def "def"
+;;     :lambda "lambda"
+;;     ;; Types
+;;     :null "None"
+;;     :true "True" :false "False"
+;;     :int "int" :str "str"
+;;     :float "float"
+;;     :bool "bool"
+;;     :tuple "tuple"
+;;     ;; Flow
+;;     :not "not"
+;;     :in "in" :not-in "not in"
+;;     :and "and" :or "or"
+;;     :for "for"
+;;     :return "return" :yield "yield"))
+
+(use-package! sqlup-mode
+  :hook
+  (sql-mode-hook . sqlup-mode)
+  (sql-interactive-mode-hook . sqlup-mode))
+
+(use-package! emacsql)
+(use-package! emacsql-psql
+  :after (emacsql))
+
+(use-package! pyimport
+  :after (python))
+
+(use-package! blacken
+  :after (python)
+  :init
+  (add-hook #'python-mode-hook #'blacken-mode)
+  :config
+  (setq blacken-only-if-project-is-blackened t
+        blacken-allow-py36 t
+        blacken-line-length 100))
+
+(after! python
+  (defun +python-setup ()
+    (setq-local fill-column 100
+                whitespace-line-column 100
+                flycheck-disabled-checkers '(python-flake8)
+                flycheck-checker 'python-pylint))
+
+  (add-hook #'python-mode-hook #'+python-setup)
+  (add-hook #'python-mode-hook #'lsp)
+  (remove-hook #'python-mode-hook #'pipenv-mode))
+
+; (use-package! w3m
+;   :config
+;   (setq browse-url-browser-function
+;         `(("^https://app.clubhouse.io.*" . browse-url-firefox)
+;           ("^https://github.com.*" . browse-url-firefox)
+;           (".*" . browse-url-firefox))))
+
+(use-package! ob-http
+  :config
+  (add-to-list 'org-babel-load-languages '(http . t)))
+
+;; (use-package! ob-ipython
+;;   :after (pyimport)
+;;   :config
+;;   (add-to-list 'org-babel-load-languages '(ipython . t))
+;;   (setq ob-ipython-command
+        ;; "/home/griffin/code/urb/ciml-video-classifier/bin/jupyter"))
+
+(use-package! counsel-spotify)
+
+(after! counsel
+  (map! [remap counsel-org-capture] #'org-capture
+        [remap org-capture] #'org-capture))
+
+(remove-hook 'doom-first-input-hook #'evil-snipe-mode)
+
+(use-package! rainbow-mode)
+
+(use-package! org-alert
+  :disabled t
+  :config
+  (org-alert-enable)
+  (setq alert-default-style 'libnotify
+        org-alert-headline-title "org"))
+
+(use-package! ob-async)
+
+(use-package! org-recent-headings
+  :config
+  (map! :n "SPC n r" #'org-recent-headings-ivy))
+
+(use-package! org-sticky-header
+  :after (org)
+  :hook (org-mode-hook . org-sticky-header-mode)
+  :config
+  (setq-default org-sticky-header-heading-star "●"))
+
+(enable-theme 'grfn-solarized-light)
+
+;;; this needs to be *after the theme*, or else I get no agenda items.
+;;; whuuu??
+(load! "org-config")
+
+
+;;; word-char
+(add-hook! prog-mode
+  (modify-syntax-entry ?_ "w"))
+
+(add-hook! lisp-mode
+  (modify-syntax-entry ?- "w"))
+
+(after! flycheck
+  (put 'flycheck-python-pylint-executable 'safe-local-variable (lambda (_) t))
+  (setq flycheck-error-list-minimum-level 'warn
+        flycheck-navigation-minimum-level 'warn))
+
+(defvar alembic-command "alembic"
+  "Command to execute when running alembic")
+
+(defvar alembic-dir-fun (lambda () default-directory)
+  "Reference to a function whose return value will be used as the directory to
+  run Alembic in")
+
+(put 'alembic-command 'safe-local-variable (lambda (_) t))
+(put 'alembic-dir-fun 'safe-local-variable (lambda (_) t))
+
+(defun make-alembic-command (args)
+  (if (functionp alembic-command)
+      (funcall alembic-command args)
+    (concat alembic-command " " args)))
+
+(defun +grfn/extract-alembic-migration-name (output)
+  (unless (string-match (rx (0+ anything) "Generating "
+                            (group (one-or-more (not (syntax whitespace))))
+                            " ..." (one-or-more (syntax whitespace)) "done"
+                            (0+ anything))
+                        output)
+    (user-error "Error: %s" output))
+  (match-string-no-properties 1 output))
+
+(defun -run-alembic (args)
+  (let* ((default-directory (funcall alembic-dir-fun))
+         (command (make-alembic-command args))
+         ;; (format "nix-shell --run 'alembic %s'" args)
+         ;; (format "%s %s" alembic-command args)
+         (res
+          (with-temp-buffer
+            (cons
+             (shell-command command t)
+             (s-replace-regexp
+              "^.*Nix search path entry.*$" ""
+              (buffer-string)))))
+         (exit-code (car res))
+         (out (cdr res)))
+    ;; (if (= 0 exit-code)
+    ;;     out
+    ;;   (error "Error running %s: %s" command out))
+    out
+    ))
+
+(comment
+ --exit-code
+ --bs
+ )
+
+(defun run-alembic (args)
+  (interactive "sAlembic command: ")
+  (message "%s" (-run-alembic args)))
+
+(defun generate-alembic-migration (msg &rest args)
+  (interactive "sMessage: ")
+  (->
+   (format "revision %s -m \"%s\""
+           (s-join " " args)
+           msg)
+   (-run-alembic)
+   (+grfn/extract-alembic-migration-name)
+   (find-file-other-window)))
+
+(cl-defun alembic-upgrade (&optional revision &key namespace)
+  (interactive "sRevision: ")
+  (let ((default-directory (funcall alembic-dir-fun)))
+    (run-alembic (format "%s upgrade %s"
+                         (if namespace (concat "-n " namespace) "")
+                         (or revision "head")))))
+
+(defun alembic-downgrade (revision)
+  (interactive "sRevision: ")
+  (let ((default-directory (funcall alembic-dir-fun)))
+    (run-alembic (format "downgrade %s" (or revision "head")))))
+
+(use-package! gnuplot)
+(use-package! gnuplot-mode :after gnuplot)
+(use-package! string-inflection)
+
+(after! anaconda-mode
+  ;; (set-company-backend! 'anaconda-mode #'company-yasnippet)
+  )
+
+;; (add-hook! python-mode
+;;   (capf))
+
+(cl-defstruct pull-request url number title author repository)
+
+(defun grfn/num-inbox-items ()
+  (length (org-elements-agenda-match "inbox" t)))
+
+(use-package! dhall-mode
+  :mode "\\.dhall\\'")
+
+(use-package! github-review
+  :after forge)
+
+(after! forge
+  (set-popup-rule!
+    "^\\*forge"
+    :size 0.75))
+
+(defun grfn/org-add-db-connection-params ()
+  (interactive)
+  (ivy-read
+   "DB to connect to: "
+   (-map (lambda (opts)
+           (propertize (symbol-name (car opts))
+                       'header-args (cdr opts)))
+         db-connection-param-options)
+   :require-match t
+   :action
+   (lambda (opt)
+     (let ((header-args (get-text-property 0 'header-args opt)))
+       (org-set-property "header-args" header-args)))))
+
+(use-package! kubernetes
+  :commands (kubernetes-overview))
+
+(use-package! k8s-mode
+  :hook (k8s-mode . yas-minor-mode))
+
+(use-package! sx)
+
+;; (use-package! nix-update
+;;   :config
+;;   (map! (:map nix-mode-map
+;;           (:leader
+;;             :desc "Update fetcher" :nv #'nix-update-fetch))))
+
+
+(after! lsp-haskell
+  (lsp-register-client
+   (make-lsp--client
+    :new-connection (lsp-stdio-connection (lambda () (lsp-haskell--hie-command)))
+    :major-modes '(haskell-mode)
+    :server-id 'hie
+    ;; :multi-root t
+    ;; :initialization-options 'lsp-haskell--make-init-options
+    )
+   )
+  )
+
+(solaire-global-mode -1)
+
+(use-package! wsd-mode)
+
+(use-package! metal-mercury-mode)
+(use-package! flycheck-mercury
+  :after (metal-mercury-mode flycheck-mercury))
+
+(use-package! direnv
+  :config (direnv-mode))
+
+(after! erc
+  ;; (setq erc-autojoin-channels-alist '(("freenode.net" "#nixos" "#haskell" "##tvl")))
+  )
+
+(defun evil-disable-insert-state-bindings ()
+  evil-disable-insert-state-bindings)
+
+;; (use-package! terraform-mode)
+;; (use-package! company-terraform
+;;   :after terraform-mode
+;;   :config (company-terraform-init))
+
+(use-package! znc
+  :config
+  (setq znc-servers
+        '(("znc.gws.fyi" 5000 t
+           ((freenode "glittershark" "Ompquy"))))))
+
+(use-package! jsonnet-mode
+  :config
+  (map!
+   (:map jsonnet-mode-map
+    (:n "g SPC" #'jsonnet-eval-buffer))))
+
+(add-to-list 'safe-local-variable-values
+             '(truncate-lines . t))
+
+(set-popup-rule!
+  "^\\*gud-"
+  :quit nil)
diff --git a/users/grfn/emacs.d/cpp.el b/users/grfn/emacs.d/cpp.el
new file mode 100644
index 0000000000..5b5dc8ead6
--- /dev/null
+++ b/users/grfn/emacs.d/cpp.el
@@ -0,0 +1,39 @@
+;;; -*- lexical-binding: t; -*-
+
+
+(load! "google-c-style")
+
+(after! flycheck
+  (add-to-list 'flycheck-disabled-checkers 'c/c++-gcc)
+  (add-to-list 'flycheck-disabled-checkers 'c/c++-clang))
+
+(defun +grfn/cpp-setup ()
+  (when (s-starts-with?
+         "/home/grfn/code/depot/third_party/nix"
+         (buffer-file-name))
+    (setq lsp-clients-clangd-executable "/home/grfn/code/depot/users/grfn/emacs.d/nix-clangd.sh"
+          lsp-clients-clangd-args nil)
+    (google-set-c-style)
+    (lsp)
+    (add-to-list 'flycheck-disabled-checkers 'c/c++-gcc)
+    (add-to-list 'flycheck-disabled-checkers 'c/c++-clang)))
+
+(add-hook 'c++-mode-hook #'+grfn/cpp-setup)
+
+(use-package! protobuf-mode)
+
+(use-package! clang-format+
+  :config
+  (add-hook 'c-mode-common-hook #'clang-format+-mode))
+
+(map!
+ (:map c++-mode-map
+  :leader
+  (:n "/ i" #'counsel-semantic-or-imenu)))
+
+(comment
+ (setq
+  lsp-clients-clangd-executable
+  "/home/grfn/code/depot/third_party/nix/clangd.sh"
+  lsp-clients-clangd-args nil)
+ )
diff --git a/users/grfn/emacs.d/email.el b/users/grfn/emacs.d/email.el
new file mode 100644
index 0000000000..83076898b4
--- /dev/null
+++ b/users/grfn/emacs.d/email.el
@@ -0,0 +1,42 @@
+;;; -*- lexical-binding: t; -*-
+
+(after! notmuch
+  (setq notmuch-saved-searches
+        '((:name "inbox" :query "tag:inbox tag:important not tag:trash" :key "i")
+          (:name "flagged" :query "tag:flagged" :key "f")
+          (:name "sent" :query "tag:sent" :key "s")
+          (:name "drafts" :query "tag:draft" :key "d")
+
+          (:name "work" :query "tag:inbox and tag:important and path:work/**"
+                 :key "w")
+          (:name "personal" :query "tag:inbox and tag:important and path:personal/**"
+                 :key "p"))
+        message-send-mail-function 'message-send-mail-with-sendmail
+        message-sendmail-f-is-evil 't
+        message-sendmail-envelope-from 'header
+        message-sendmail-extra-arguments '("--read-envelope-from"))
+
+  (add-hook! notmuch-message-mode-hook #'notmuch-company-setup))
+
+(setq notmuch-saved-searches
+        '((:name "inbox" :query "tag:inbox tag:important not tag:trash" :key "i")
+          (:name "flagged" :query "tag:flagged" :key "f")
+          (:name "sent" :query "tag:sent" :key "s")
+          (:name "drafts" :query "tag:draft" :key "d")
+
+          (:name "work" :query "tag:inbox and tag:important and path:work/**"
+                 :key "w")
+          (:name "personal" :query "tag:inbox and tag:important and path:personal/**"
+                 :key "p"))
+        message-send-mail-function 'message-send-mail-with-sendmail
+        message-sendmail-f-is-evil 't
+        message-sendmail-envelope-from 'header
+        message-sendmail-extra-arguments '("--read-envelope-from"))
+
+(set-popup-rule! "^\\*notmuch-saved-search-"
+  :ignore t)
+
+(set-popup-rule! (lambda (_ action)
+                   (eq (car action)
+                       'display-buffer-same-window))
+  :ignore t)
diff --git a/users/grfn/emacs.d/github-org.el b/users/grfn/emacs.d/github-org.el
new file mode 100644
index 0000000000..f4f9d2e370
--- /dev/null
+++ b/users/grfn/emacs.d/github-org.el
@@ -0,0 +1,99 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 'ghub)
+
+(defun grfn/alist->plist (alist)
+  (->> alist
+       (-mapcat (lambda (pair)
+                  (list (intern (concat ":" (symbol-name (car pair))))
+                        (cdr pair))))))
+
+;;;
+
+(cl-defstruct pull-request url number title author repository)
+
+(defun grfn/query-pulls (query)
+  (let ((resp (ghub-graphql "query reviewRequests($query: String!) {
+    reviewRequests: search(
+      type:ISSUE,
+      query: $query,
+      first: 100
+    ) {
+      issueCount
+      nodes {
+        ... on PullRequest {
+          url
+          number
+          title
+          author {
+            login
+            ... on User { name }
+          }
+          repository {
+            name
+            owner { login }
+          }
+        }
+      }
+    }
+  }" `((query . ,query)))))
+    (->> resp
+         (alist-get 'data)
+         (alist-get 'reviewRequests)
+         (alist-get 'nodes)
+         (-map
+          (lambda (pr)
+            (apply
+             #'make-pull-request
+             (grfn/alist->plist pr)))))))
+
+(defun grfn/requested-changes ())
+
+(defun grfn/pull-request->org-headline (format-string level pr)
+  (check-type format-string string)
+  (check-type level integer)
+  (check-type pr pull-request)
+  (s-format (concat (make-string level ?*) " " format-string)
+            'aget
+            `((author . ,(or (->> pr (pull-request-author) (alist-get 'name))
+                             "no author"))
+              (owner . ,(->> pr (pull-request-repository)
+                             (alist-get 'owner)
+                             (alist-get 'login)))
+              (repo . ,(->> pr (pull-request-repository) (alist-get 'name)))
+              (pr-link . ,(org-make-link-string
+                           (pull-request-url pr)
+                           (pull-request-title pr)))
+              (today . ,(format-time-string "%Y-%m-%d %a")))))
+
+(defun grfn/org-headlines-from-review-requests (level)
+  "Create org-mode headlines at LEVEL from all review-requested PRs on Github"
+  (interactive "*nLevel: ")
+  (let* ((prs (grfn/query-pulls
+               "is:open is:pr review-requested:glittershark archived:false"))
+         (text (mapconcat
+                (apply-partially
+                 #'grfn/pull-request->org-headline
+                 "TODO Review ${author}'s PR on ${owner}/${repo}: ${pr-link} :pr:
+SCHEDULED: <${today}>"
+                 level) prs "\n")))
+    (save-mark-and-excursion
+      (insert text))
+    (org-align-tags 't)))
+
+(defun grfn/org-headlines-from-requested-changes (level)
+  "Create org-mode headlines at LEVEL from all PRs with changes requested
+ on Github"
+  (interactive "*nLevel: ")
+  (let* ((prs (grfn/query-pulls
+               (concat "is:pr is:open author:glittershark archived:false "
+                       "sort:updated-desc review:changes-requested")))
+         (text (mapconcat
+                (apply-partially
+                 #'grfn/pull-request->org-headline
+                 "TODO Address review comments on ${pr-link} :pr:
+SCHEDULED: <${today}>"
+                 level) prs "\n")))
+    (save-mark-and-excursion
+      (insert text))
+    (org-align-tags 't)))
diff --git a/users/grfn/emacs.d/google-c-style.el b/users/grfn/emacs.d/google-c-style.el
new file mode 100644
index 0000000000..9bb12c61aa
--- /dev/null
+++ b/users/grfn/emacs.d/google-c-style.el
@@ -0,0 +1,151 @@
+;;; google-c-style.el --- Google's C/C++ style for c-mode
+
+;; Keywords: c, tools
+
+;; google-c-style.el is Copyright (C) 2008 Google Inc. All Rights Reserved.
+;;
+;; It is free software; you can redistribute it and/or modify it under the
+;; terms of either:
+;;
+;; a) the GNU General Public License as published by the Free Software
+;; Foundation; either version 1, or (at your option) any later version, or
+;;
+;; b) the "Artistic License".
+
+;;; Commentary:
+
+;; Provides the google C/C++ coding style. You may wish to add
+;; `google-set-c-style' to your `c-mode-common-hook' after requiring this
+;; file. For example:
+;;
+;;    (add-hook 'c-mode-common-hook 'google-set-c-style)
+;;
+;; If you want the RETURN key to go to the next line and space over
+;; to the right place, add this to your .emacs right after the load-file:
+;;
+;;    (add-hook 'c-mode-common-hook 'google-make-newline-indent)
+
+;;; Code:
+
+;; For some reason 1) c-backward-syntactic-ws is a macro and 2)  under Emacs 22
+;; bytecode cannot call (unexpanded) macros at run time:
+(eval-when-compile (require 'cc-defs))
+
+;; Wrapper function needed for Emacs 21 and XEmacs (Emacs 22 offers the more
+;; elegant solution of composing a list of lineup functions or quantities with
+;; operators such as "add")
+(defun google-c-lineup-expression-plus-4 (langelem)
+  "Indents to the beginning of the current C expression plus 4 spaces.
+
+This implements title \"Function Declarations and Definitions\"
+of the Google C++ Style Guide for the case where the previous
+line ends with an open parenthese.
+
+\"Current C expression\", as per the Google Style Guide and as
+clarified by subsequent discussions, means the whole expression
+regardless of the number of nested parentheses, but excluding
+non-expression material such as \"if(\" and \"for(\" control
+structures.
+
+Suitable for inclusion in `c-offsets-alist'."
+  (save-excursion
+    (back-to-indentation)
+    ;; Go to beginning of *previous* line:
+    (c-backward-syntactic-ws)
+    (back-to-indentation)
+    (cond
+     ;; We are making a reasonable assumption that if there is a control
+     ;; structure to indent past, it has to be at the beginning of the line.
+     ((looking-at "\\(\\(if\\|for\\|while\\)\\s *(\\)")
+      (goto-char (match-end 1)))
+     ;; For constructor initializer lists, the reference point for line-up is
+     ;; the token after the initial colon.
+     ((looking-at ":\\s *")
+      (goto-char (match-end 0))))
+    (vector (+ 4 (current-column)))))
+
+;;;###autoload
+(defconst google-c-style
+  `((c-recognize-knr-p . nil)
+    (c-enable-xemacs-performance-kludge-p . t) ; speed up indentation in XEmacs
+    (c-basic-offset . 2)
+    (indent-tabs-mode . nil)
+    (c-comment-only-line-offset . 0)
+    (c-hanging-braces-alist . ((defun-open after)
+                               (defun-close before after)
+                               (class-open after)
+                               (class-close before after)
+                               (inexpr-class-open after)
+                               (inexpr-class-close before)
+                               (namespace-open after)
+                               (inline-open after)
+                               (inline-close before after)
+                               (block-open after)
+                               (block-close . c-snug-do-while)
+                               (extern-lang-open after)
+                               (extern-lang-close after)
+                               (statement-case-open after)
+                               (substatement-open after)))
+    (c-hanging-colons-alist . ((case-label)
+                               (label after)
+                               (access-label after)
+                               (member-init-intro before)
+                               (inher-intro)))
+    (c-hanging-semi&comma-criteria
+     . (c-semi&comma-no-newlines-for-oneline-inliners
+        c-semi&comma-inside-parenlist
+        c-semi&comma-no-newlines-before-nonblanks))
+    (c-indent-comments-syntactically-p . t)
+    (comment-column . 40)
+    (c-indent-comment-alist . ((other . (space . 2))))
+    (c-cleanup-list . (brace-else-brace
+                       brace-elseif-brace
+                       brace-catch-brace
+                       empty-defun-braces
+                       defun-close-semi
+                       list-close-comma
+                       scope-operator))
+    (c-offsets-alist . ((arglist-intro google-c-lineup-expression-plus-4)
+                        (func-decl-cont . ++)
+                        (member-init-intro . ++)
+                        (inher-intro . ++)
+                        (comment-intro . 0)
+                        (arglist-close . c-lineup-arglist)
+                        (topmost-intro . 0)
+                        (block-open . 0)
+                        (inline-open . 0)
+                        (substatement-open . 0)
+                        (statement-cont
+                         .
+                         (,(when (fboundp 'c-no-indent-after-java-annotations)
+                             'c-no-indent-after-java-annotations)
+                          ,(when (fboundp 'c-lineup-assignments)
+                             'c-lineup-assignments)
+                          ++))
+                        (label . /)
+                        (case-label . +)
+                        (statement-case-open . +)
+                        (statement-case-intro . +) ; case w/o {
+                        (access-label . /)
+                        (innamespace . 0))))
+  "Google C/C++ Programming Style.")
+
+;;;###autoload
+(defun google-set-c-style ()
+  "Set the current buffer's c-style to Google C/C++ Programming
+  Style. Meant to be added to `c-mode-common-hook'."
+  (interactive)
+  (make-local-variable 'c-tab-always-indent)
+  (setq c-tab-always-indent t)
+  (c-add-style "Google" google-c-style t))
+
+;;;###autoload
+(defun google-make-newline-indent ()
+  "Sets up preferred newline behavior. Not set by default. Meant
+  to be added to `c-mode-common-hook'."
+  (interactive)
+  (define-key c-mode-base-map "\C-m" 'newline-and-indent)
+  (define-key c-mode-base-map [ret] 'newline-and-indent))
+
+(provide 'google-c-style)
+;;; google-c-style.el ends here
diff --git a/users/grfn/emacs.d/grid.el b/users/grfn/emacs.d/grid.el
new file mode 100644
index 0000000000..75776a38cd
--- /dev/null
+++ b/users/grfn/emacs.d/grid.el
@@ -0,0 +1,128 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 's)
+
+(defun grfn/all-match-groups (s)
+  (loop for n from 1
+        for x = (match-string n s)
+        while x
+        collect x))
+
+(defun projectile-grid-ff (path &optional ask)
+  "Call `find-file' function on PATH when it is not nil and the file exists.
+If file does not exist and ASK in not nil it will ask user to proceed."
+  (if (or (and path (file-exists-p path))
+          (and ask (yes-or-no-p
+                    (s-lex-format
+                     "File does not exists. Create a new buffer ${path} ?"))))
+      (find-file path)))
+
+(defun projectile-grid-goto-file (filepath &optional ask)
+  "Find FILEPATH after expanding root.  ASK is passed straight to `projectile-grid-ff'."
+  (projectile-grid-ff (projectile-expand-root filepath) ask))
+
+(defun projectile-grid-choices (ds)
+  "Uses `projectile-dir-files' function to find files in directories.
+The DIRS is list of lists consisting of a directory path and regexp to filter files from that directory.
+Optional third element can be present in the DS list. The third element will be a prefix to be placed before
+the filename in the resulting choice.
+Returns a hash table with keys being short names (choices) and values being relative paths to the files."
+  (loop with hash = (make-hash-table :test 'equal)
+        for (dir re prefix) in ds do
+        (loop for file in (projectile-dir-files (projectile-expand-root dir)) do
+              (when (string-match re file)
+                (puthash
+                 (concat (or prefix "")
+                         (s-join "/" (grfn/all-match-groups file)))
+                 (concat dir file)
+                 hash)))
+        finally return hash))
+
+(defmacro projectile-grid-find-resource (prompt dirs &optional newfile-template)
+  "Presents files from DIRS with PROMPT to the user using `projectile-completing-read'.
+If users chooses a non existant file and NEWFILE-TEMPLATE is not nil
+it will use that variable to interpolate the name for the new file.
+NEWFILE-TEMPLATE will be the argument for `s-lex-format'.
+The bound variable is \"filename\"."
+  `(lexical-let ((choices (projectile-grid-choices ,dirs)))
+     (projectile-completing-read
+      ,prompt
+      (hash-table-keys choices)
+      :action
+      (lambda (c)
+        (let* ((filepath (gethash c choices))
+               (filename c)) ;; so `s-lex-format' can interpolate FILENAME
+          (if filepath
+              (projectile-grid-goto-file filepath)
+            (when-let ((newfile-template ,newfile-template))
+              (projectile-grid-goto-file
+               (funcall newfile-template filepath)
+               ;; (cond
+               ;;  ((functionp newfile-template) (funcall newfile-template filepath))
+               ;;  ((stringp newfile-template) (s-lex-format newfile-template)))
+               t))))))))
+
+(defun projectile-grid-find-model ()
+  "Find a model."
+  (interactive)
+  (projectile-grid-find-resource
+   "model: "
+   '(("python/urbint_lib/models/"
+      "\\(.+\\)\\.py$")
+     ("python/urbint_lib/"
+      "\\(.+\\)/models/\\(.+\\).py$"))
+   (lambda (filename)
+     (pcase (s-split "/" filename)
+       (`(,model)
+        (s-lex-format "python/urbint_lib/models/${model}.py"))
+       (`(,app ,model)
+        (s-lex-format "python/urbint_lib/${app}/models/${model}.py"))))))
+
+(defun projectile-grid-find-repository ()
+  "Find a repository."
+  (interactive)
+  (projectile-grid-find-resource
+   "repository: "
+   '(("python/urbint_lib/repositories/"
+      "\\(.+\\)\\.py$")
+     ("python/urbint_lib/"
+      "\\(.+\\)/repositories/\\(.+\\).py$"))
+   (lambda (filename)
+     (pcase (s-split "/" filename)
+       (`(,repository)
+        (s-lex-format "python/urbint_lib/repositories/${repository}.py"))
+       (`(,app ,repository)
+        (s-lex-format "python/urbint_lib/${app}/repositories/${repository}.py"))))))
+
+(defun projectile-grid-find-controller ()
+  "Find a controller."
+  (interactive)
+  (projectile-grid-find-resource
+   "controller: "
+   '(("backend/src/grid/api/controllers/"
+      "\\(.+\\)\\.py$")
+     ("backend/src/grid/api/apps/"
+      "\\(.+\\)/controllers/\\(.+\\).py$"))
+   (lambda (filename)
+     (pcase (s-split "/" filename)
+       (`(,controller)
+        (s-lex-format "backend/src/grid/api/controllers/${controller}.py"))
+       (`(,app ,controller)
+        (s-lex-format "backend/src/grid/api/apps/${app}/controllers/${controller}.py"))))))
+
+(setq projectile-grid-mode-map
+  (let ((map (make-keymap)))
+    (map!
+     (:map map
+      (:leader
+       (:desc "Edit..." :prefix "e"
+        :desc "Model"      :n "m" #'projectile-grid-find-model
+        :desc "Controller" :n "c" #'projectile-grid-find-controller
+        :desc "Repository" :n "r" #'projectile-grid-find-repository))))
+    map))
+
+(define-minor-mode projectile-grid-mode
+  "Minor mode for finding files in GRID"
+  :init-value nil
+  :lighter " GRID"
+  :keymap projectile-grid-mode-map)
diff --git a/users/grfn/emacs.d/init.el b/users/grfn/emacs.d/init.el
new file mode 100644
index 0000000000..2518f2f798
--- /dev/null
+++ b/users/grfn/emacs.d/init.el
@@ -0,0 +1,172 @@
+;;; -*- lexical-binding: t; -*-
+
+(doom! :completion
+       company           ; the ultimate code completion backend
+       (ivy +fuzzy
+            +prescient)               ; a search engine for love and life
+
+       :ui
+       ;;deft              ; notational velocity for Emacs
+       doom              ; what makes DOOM look the way it does
+       ;doom-dashboard    ; a nifty splash screen for Emacs
+       doom-quit         ; DOOM quit-message prompts when you quit Emacs
+       ;fill-column       ; a `fill-column' indicator
+       hl-todo           ; highlight TODO/FIXME/NOTE tags
+       ;;indent-guides     ; highlighted indent columns
+       modeline          ; snazzy, Atom-inspired modeline, plus API
+       nav-flash         ; blink the current line after jumping
+       ;;neotree           ; a project drawer, like NERDTree for vim
+       ophints           ; highlight the region an operation acts on
+       (popup            ; tame sudden yet inevitable temporary windows
+        +all             ; catch all popups that start with an asterix
+        +defaults)       ; default popup rules
+       ;; ligatures         ; replace bits of code with pretty symbols
+       ;; tabbar            ; FIXME an (incomplete) tab bar for Emacs
+       ;; treemacs          ; a project drawer, like neotree but cooler
+       unicode           ; extended unicode support for various languages
+       vc-gutter         ; vcs diff in the fringe
+       vi-tilde-fringe   ; fringe tildes to mark beyond EOB
+       window-select     ; visually switch windows
+       workspaces        ; tab emulation, persistence & separate workspaces
+
+       :editor
+       (evil +everywhere); come to the dark side, we have cookies
+       file-templates    ; auto-snippets for empty files
+       fold              ; (nigh) universal code folding
+       ;;(format +onsave)  ; automated prettiness
+       ;;lispy             ; vim for lisp, for people who dont like vim
+       multiple-cursors  ; editing in many places at once
+       ;;parinfer          ; turn lisp into python, sort of
+       rotate-text       ; cycle region at point between text candidates
+       snippets          ; my elves. They type so I don't have to
+       word-wrap
+
+       :emacs
+       (dired            ; making dired pretty [functional]
+       ;;+ranger         ; bringing the goodness of ranger to dired
+       ;;+icons          ; colorful icons for dired-mode
+        )
+       electric          ; smarter, keyword-based electric-indent
+       ;;eshell            ; a consistent, cross-platform shell (WIP)
+       ;;term              ; terminals in Emacs
+       vc                ; version-control and Emacs, sitting in a tree
+       (undo +tree)
+
+       :tools
+       ;;ansible
+       ;;debugger          ; FIXME stepping through code, to help you add bugs
+       ;;direnv
+       docker
+       ;;editorconfig      ; let someone else argue about tabs vs spaces
+       ;; ein               ; tame Jupyter notebooks with emacs
+       (eval +overlay)              ; run code, run (also, repls)
+       gist              ; interacting with github gists
+       (lookup           ; helps you navigate your code and documentation
+        +docsets)        ; ...or in Dash docsets locally
+       lsp
+       ;;macos             ; MacOS-specific commands
+       magit             ; a git porcelain for Emacs
+       make              ; run make tasks from Emacs
+       pass              ; password manager for nerds
+       pdf               ; pdf enhancements
+       ;;prodigy           ; FIXME managing external services & code builders
+       ;;rgb               ; creating color strings
+       ;;terraform         ; infrastructure as code
+       ;;tmux              ; an API for interacting with tmux
+       ;;upload            ; map local to remote projects via ssh/ftp
+       ;;wakatime
+       ;;vterm             ; another terminals in Emacs
+
+       :checkers
+       syntax          ; tasing you for every semicolon you forget
+       ; spell           ; tasing you for misspelling mispelling
+
+       :lang
+       agda              ; types of types of types of types...
+       ;;assembly          ; assembly for fun or debugging
+       cc                ; C/C++/Obj-C madness
+       clojure           ; java with a lisp
+       common-lisp       ; if you've seen one lisp, you've seen them all
+       ; coq               ; proofs-as-programs
+       ;;crystal           ; ruby at the speed of c
+       ;;csharp            ; unity, .NET, and mono shenanigans
+       data              ; config/data formats
+       erlang            ; an elegant language for a more civilized age
+       elixir            ; erlang done right
+       ;;elm               ; care for a cup of TEA?
+       emacs-lisp        ; drown in parentheses
+       ;;ess               ; emacs speaks statistics
+       ;;go                ; the hipster dialect
+       ;; (haskell +intero) ; a language that's lazier than I am
+       haskell ; a language that's lazier than I am
+       ;;hy                ; readability of scheme w/ speed of python
+       ;; idris             ;
+       ;;(java +meghanada) ; the poster child for carpal tunnel syndrome
+       javascript        ; all(hope(abandon(ye(who(enter(here))))))
+       julia             ; a better, faster MATLAB
+       ;;kotlin            ; a better, slicker Java(Script)
+       latex             ; writing papers in Emacs has never been so fun
+       ;;ledger            ; an accounting system in Emacs
+       lua               ; one-based indices? one-based indices
+       markdown          ; writing docs for people to ignore
+       ;;nim               ; python + lisp at the speed of c
+       nix               ; I hereby declare "nix geht mehr!"
+       ;;ocaml             ; an objective camel
+       (org              ; organize your plain life in plain text
+        +dragndrop       ; drag & drop files/images into org buffers
+        +attach          ; custom attachment system
+        +babel           ; running code in org
+        +capture         ; org-capture in and outside of Emacs
+        +export          ; Exporting org to whatever you want
+        ;; +habit           ; Keep track of your habits
+        +present         ; Emacs for presentations
+        +pretty
+        +brain
+        +protocol)       ; Support for org-protocol:// links
+       ;;perl              ; write code no one else can comprehend
+       ;;php               ; perl's insecure younger brother
+       ;;plantuml          ; diagrams for confusing people more
+       purescript        ; javascript, but functional
+       (python +lsp)            ; beautiful is better than ugly
+       ;;qt                ; the 'cutest' gui framework ever
+       racket            ; a DSL for DSLs
+       rest              ; Emacs as a REST client
+       ;;ruby              ; 1.step do {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
+       rust              ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
+       ;;scala             ; java, but good
+       (sh +fish)        ; she sells (ba|z|fi)sh shells on the C xor
+       ;;solidity          ; do you need a blockchain? No.
+       ;;swift             ; who asked for emoji variables?
+       ;;terra             ; Earth and Moon in alignment for performance.
+       ;;web               ; the tubes
+       ;;vala              ; GObjective-C
+
+       ;; Applications are complex and opinionated modules that transform Emacs
+       ;; toward a specific purpose. They may have additional dependencies and
+       ;; should be loaded late.
+       :app
+       ;;(email +gmail)    ; emacs as an email client
+       irc               ; how neckbeards socialize
+       ;;(rss +org)        ; emacs as an RSS reader
+       twitter           ; twitter client https://twitter.com/vnought
+       ;;(write            ; emacs as a word processor (latex + org + markdown)
+       ;; +wordnut         ; wordnet (wn) search
+       ;; +langtool)       ; a proofreader (grammar/style check) for Emacs
+
+       :email
+       ;; (mu4e +gmail)
+       notmuch
+
+       :collab
+       ;;floobits          ; peer programming for a price
+       ;;impatient-mode    ; show off code over HTTP
+
+       :config
+       ;; For literate config users. This will tangle+compile a config.org
+       ;; literate config in your `doom-private-dir' whenever it changes.
+       ;;literate
+
+       ;; The default module sets reasonable defaults for Emacs. It also
+       ;; provides a Spacemacs-inspired keybinding scheme and a smartparens
+       ;; config. Use it as a reference for your own modules.
+       (default +bindings +smartparens))
diff --git a/users/grfn/emacs.d/irc.el b/users/grfn/emacs.d/irc.el
new file mode 100644
index 0000000000..117869599d
--- /dev/null
+++ b/users/grfn/emacs.d/irc.el
@@ -0,0 +1,131 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 'erc)
+(require 'alert)
+
+(defvar irc-servers
+  '("hackint"
+    "libera"))
+
+(defun irc-connect (server)
+  (interactive
+   (list (ivy-read "Server: " irc-servers)))
+  (let ((pw (s-trim (shell-command-to-string
+                     (format "pass irccloud/%s" server))))
+        (gnutls-verify-error nil))
+    (erc-tls :server "bnc.irccloud.com"
+             :port 6697
+             :nick "grfn"
+             :password (concat "bnc@"
+                               (s-trim (shell-command-to-string "hostname"))
+                               ":"
+                               pw))))
+
+
+(defgroup erc-alert nil
+  "Alert me using alert.el for important ERC messages"
+  :group 'erc)
+
+(defcustom erc-noise-regexp
+  "\\(Logging in:\\|Signing off\\|You're now away\\|Welcome back\\)"
+  "This regexp matches unwanted noise."
+  :type 'regexp
+  :group 'erc)
+
+(setq tvl-enabled? t)
+
+(defun disable-tvl-notifications ()
+  (interactive)
+  (setq tvl-enabled? nil))
+
+(defun enable-tvl-notifications ()
+  (interactive)
+  (setq tvl-enabled? t))
+
+(defun erc-alert-important-p (info)
+  (let ((message (plist-get info :message))
+        (erc-message (-> info (plist-get :data) (plist-get :message)))
+        (erc-channel (-> info (plist-get :data) (plist-get :channel))))
+    (and erc-message
+         (not (or (string-match "^\\** *Users on #" message)
+                  (string-match erc-noise-regexp
+                                message)))
+         (or (and tvl-enabled?
+                  (string-equal erc-channel "#tvl"))
+             (string-match "grfn" message)))))
+
+(comment
+ last-info
+ erc-noise-regexp
+ (setq tvl-enabled? nil)
+ )
+
+(defun my-erc-hook (&optional match-type nick message)
+  "Shows a notification, when user's nick was mentioned.
+If the buffer is currently not visible, makes it sticky."
+  (setq last-message message)
+  (if (or (null match-type) (not (eq match-type 'fool)))
+      (let (alert-log-messages)
+        (alert (or message (buffer-string))
+               :severity (if (string-match "grfn" (or message ""))
+                             'high 'low)
+               :title (or nick (buffer-name))
+               :data `(:message ,(or message (buffer-string))
+                                :channel ,(or nick (buffer-name)))))))
+
+(add-hook 'erc-text-matched-hook 'my-erc-hook)
+(add-hook 'erc-insert-modify-hook 'my-erc-hook)
+
+(defun my-erc-define-alerts (&rest ignore)
+  ;; Unless the user has recently typed in the ERC buffer, highlight the fringe
+  (alert-add-rule
+   :status   '(buried visible idle)
+   :severity '(moderate high urgent)
+   :mode     'erc-mode
+   :predicate
+   #'(lambda (info)
+       (and (not (eq (current-buffer) (plist-get info :buffer)))
+            (string-match "grfn:" (plist-get info :message))))
+   :persistent
+   #'(lambda (info)
+       ;; If the buffer is buried, or the user has been idle for
+       ;; `alert-reveal-idle-time' seconds, make this alert
+       ;; persistent.  Normally, alerts become persistent after
+       ;; `alert-persist-idle-time' seconds.
+       (memq (plist-get info :status) '(buried idle)))
+   :style 'message
+   :continue t)
+
+  (alert-add-rule
+   :status 'buried
+   :mode   'erc-mode
+   :predicate #'erc-alert-important-p
+   :style 'libnotify
+   :append t)
+
+  (alert-add-rule
+   :status 'buried
+   :mode   'erc-mode
+   :predicate #'erc-alert-important-p
+   :style 'message
+   :append t)
+
+  (alert-add-rule
+   :mode 'erc-mode
+   :predicate #'erc-alert-important-p
+   :style 'log
+   :append t)
+
+  (alert-add-rule :mode 'erc-mode :style 'ignore :append t))
+
+(add-hook 'erc-connect-pre-hook 'my-erc-define-alerts)
+
+(defun fix-irc-message (msg)
+  (let ((msg (s-trim msg)))
+    (if (string-equal msg ":q") "" msg)))
+
+(advice-add #'erc-user-input :filter-return #'fix-irc-message)
+
+(comment
+ (my-erc-define-alerts)
+ )
diff --git a/users/grfn/emacs.d/lisp.el b/users/grfn/emacs.d/lisp.el
new file mode 100644
index 0000000000..c45cc7e6e3
--- /dev/null
+++ b/users/grfn/emacs.d/lisp.el
@@ -0,0 +1,38 @@
+;;; -*- lexical-binding: t; -*-
+
+(defun grfn/sly-panettone ()
+  (interactive)
+  (sly
+   (concat
+    (s-trim
+     (shell-command-to-string
+      "nix-build -o sbcl -E 'with import ~/code/depot {}; nix.buildLisp.sbclWith [web.panettone]'"))
+    "/bin/sbcl")))
+
+(defun grfn/setup-lisp ()
+  (interactive)
+  (unless paxedit-mode (paxedit-mode 1))
+  (rainbow-delimiters-mode)
+  (flycheck-mode -1))
+
+(add-hook 'common-lisp-lisp-mode-hook #'grfn/setup-lisp)
+
+(defun sly-run-tests ()
+  (interactive)
+  ;; TODO: handle other test frameworks
+  (let ((orig-window (get-buffer-window)))
+    (sly-eval '(fiveam:run!))
+    (funcall-interactively #'sly-mrepl-sync)
+    (select-window orig-window)))
+
+(map!
+ (:map sly-mode-map
+  :n "g \\" #'sly-mrepl-sync
+  :n "g d" #'sly-edit-definition
+  :n "K" #'sly-documentation
+  :n "g SPC" #'sly-compile-and-load-file
+  :n "g RET" #'sly-run-tests)
+
+ (:map sly-mrepl-mode-map
+  "C-k" #'sly-mrepl-previous-prompt
+  "C-r" #'isearch-backward))
diff --git a/users/grfn/emacs.d/nix-clangd.sh b/users/grfn/emacs.d/nix-clangd.sh
new file mode 100755
index 0000000000..16f6252d8b
--- /dev/null
+++ b/users/grfn/emacs.d/nix-clangd.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CLANGD_FLAGS=--compile-commands-dir=/home/grfn/builds/tvix \
+    nix-shell /home/grfn/code/depot \
+    -A third_party.nix \
+    --run nix-clangd
diff --git a/users/grfn/emacs.d/nix.el b/users/grfn/emacs.d/nix.el
new file mode 100644
index 0000000000..ec5b474af2
--- /dev/null
+++ b/users/grfn/emacs.d/nix.el
@@ -0,0 +1,30 @@
+;;; -*- lexical-binding: t; -*-
+
+(defun nix-buffer-type ()
+  "Returns:
+
+'home-manager, if the current buffer is a home-manager module
+'nixos, if the current buffer is a nixos module
+nil, if none of the above are the case"
+  (when buffer-file-name
+    (pcase buffer-file-name
+      ((rx (0+ nonl) "system/home" (0+ nonl) ".nix" eos)
+       'home-manager)
+      ((rx (0+ nonl) "system/system" (0+ nonl) ".nix" eos)
+       'nixos))))
+
+(defun set-nix-compile-command ()
+  "Set the compile command for the current buffer based on the type of nix
+buffer it is, per `nix-buffer-type'"
+  (interactive)
+  (when-let ((btype (nix-buffer-type)))
+    (setq-local
+     compile-command
+     (case btype
+       ('home-manager "home-manager switch")
+       ('nixos "sudo nixos-rebuild switch")))))
+
+(add-hook 'nix-mode-hook #'set-nix-compile-command)
+
+(map! (:map nix-mode-map
+       (:n "g SPC" #'compile)))
diff --git a/users/grfn/emacs.d/org-alerts.el b/users/grfn/emacs.d/org-alerts.el
new file mode 100644
index 0000000000..8e6c3e0417
--- /dev/null
+++ b/users/grfn/emacs.d/org-alerts.el
@@ -0,0 +1,188 @@
+;;; -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;;; Code:
+
+(require 's)
+(require 'dash)
+(require 'alert)
+(require 'org-agenda)
+
+
+(defvar grfn/org-alert-interval 300
+  "Interval in seconds to recheck and display deadlines.")
+
+
+(defvar grfn/org-alert-notification-title "*org*"
+  "Title to be sent with notify-send.")
+
+(defvar grfn/org-alert-headline-regexp "\\(Sched.+:.+\\|Deadline:.+\\)"
+  "Regexp for headlines to search in agenda buffer.")
+
+(defun grfn/org-alert--strip-prefix (headline)
+  "Remove the scheduled/deadline prefix from HEADLINE."
+  (replace-regexp-in-string ".*:\s+" "" headline))
+
+
+(defun grfn/org-alert--unique-headlines (regexp agenda)
+  "Return unique headlines from the results of REGEXP in AGENDA."
+  (let ((matches (-distinct (-flatten (s-match-strings-all regexp agenda)))))
+    (--map (grfn/org-alert--strip-prefix it) matches)))
+
+
+(defun grfn/org-alert--get-headlines ()
+  "Return the current org agenda as text only."
+  (with-temp-buffer
+    (let ((org-agenda-sticky nil)
+          (org-agenda-buffer-tmp-name (buffer-name)))
+      (ignore-errors (org-agenda-list nil "TODAY" 1))
+      (grfn/org-alert--unique-headlines
+       grfn/org-alert-headline-regexp
+       (buffer-substring-no-properties (point-min) (point-max))))))
+
+(defun grfn/parse-range-string (str)
+  (when
+      (string-match (rx (group (repeat 2 (any digit))
+                               ":"
+                               (repeat 2 (any digit)))
+                        (optional
+                         (and
+                          "-"
+                          (group (repeat 2 (any digit))
+                                 ":"
+                                 (repeat 2 (any digit))))))
+                    str)
+    (list
+     (org-read-date nil t
+                    (match-string 1 str))
+     (when-let ((et (match-string 2 str))) (org-read-date nil t et)))))
+
+(defun grfn/start-time-from-range-string (str)
+  (pcase-let ((`(,start-time . _) (grfn/parse-range-string str)))
+    start-time))
+
+(comment
+ (org-agenda-list nil "TODAY" 1)
+
+ (grfn/org-alert--get-headlines)
+ (setq --src
+       (with-temp-buffer
+         (let ((org-agenda-sticky nil)
+               (org-agenda-buffer-tmp-name (buffer-name)))
+           (ignore-errors (org-agenda-list nil "TODAY" 1))
+           (buffer-substring-no-properties (point-min) (point-max)))))
+
+ (setq --entries
+       (with-temp-buffer
+         (let ((inhibit-redisplay t)
+               (org-agenda-sticky nil)
+               (org-agenda-buffer-tmp-name (buffer-name))
+               (org-agenda-buffer-name (buffer-name))
+               (org-agenda-buffer (current-buffer)))
+           (org-agenda-get-day-entries
+            (cadr (org-agenda-files nil 'ifmode))
+            (calendar-gregorian-from-absolute
+             (time-to-days (org-read-date nil t "TODAY")))))))
+
+ (loop for k in (text-properties-at 0 (car --entries))
+       by #'cddr
+       collect k)
+
+ (--map (substring-no-properties (get-text-property 0 'txt it)) --entries)
+ (--map (get-text-property 0 'time it) --entries)
+ (current-time)
+
+ (format-time-string "%R" (org-read-date nil t "10:00-11:00"))
+
+ (grfn/start-time-from-range-string "10:00")
+
+ (current-time-string (org-read-date nil t "10:00-11:00"))
+
+ (todo-state
+  org-habit-p
+  priority
+  warntime
+  ts-date
+  date
+  type
+  org-hd-marker
+  org-marker
+  face
+  undone-face
+  help-echo
+  mouse-face
+  done-face
+  org-complex-heading-regexp
+  org-todo-regexp
+  org-not-done-regexp
+  dotime
+  format
+  extra
+  time
+  level
+  txt
+  breadcrumbs
+  duration
+  time-of-day
+  org-lowest-priority
+  org-highest-priority
+  tags
+  org-category)
+
+ (propertize)
+
+ --src
+ )
+
+
+(defun grfn/org-alert--headline-complete? (headline)
+  "Return whether HEADLINE has been completed."
+  (--any? (s-starts-with? it headline) org-done-keywords-for-agenda))
+
+
+(defun grfn/org-alert--filter-active (deadlines)
+  "Remove any completed headings from the provided DEADLINES."
+  (-remove 'grfn/org-alert--headline-complete? deadlines))
+
+
+(defun grfn/org-alert--strip-states (deadlines)
+  "Remove the todo states from DEADLINES."
+  (--map (s-trim (s-chop-prefixes org-todo-keywords-for-agenda it)) deadlines))
+
+
+(defun grfn/org-alert-check ()
+  "Check for active, due deadlines and initiate notifications."
+  (interactive)
+  ;; avoid interrupting current command.
+  (unless (minibufferp)
+    (save-window-excursion
+      (save-excursion
+        (save-restriction
+          (let ((active (grfn/org-alert--filter-active (grfn/org-alert--get-headlines))))
+            (dolist (dl (grfn/org-alert--strip-states active))
+              (alert dl :title grfn/org-alert-notification-title))))))
+    (when (get-buffer org-agenda-buffer-name)
+      (ignore-errors
+        (with-current-buffer org-agenda-buffer-name
+          (org-agenda-redo t))))))
+
+
+(defun grfn/org-alert-enable ()
+  "Enable the notification timer.  Cancels existing timer if running."
+  (interactive)
+  (grfn/org-alert-disable)
+  (run-at-time 0 grfn/org-alert-interval 'grfn/org-alert-check))
+
+
+(defun grfn/org-alert-disable ()
+  "Cancel the running notification timer."
+  (interactive)
+  (dolist (timer timer-list)
+    (if (eq (elt timer 5) 'grfn/org-alert-check)
+        (cancel-timer timer))))
+
+
+
+(provide 'grfn/org-alert)
+;;; grfn/org-alert.el ends here
diff --git a/users/grfn/emacs.d/org-config.el b/users/grfn/emacs.d/org-config.el
new file mode 100644
index 0000000000..81fb35a1cb
--- /dev/null
+++ b/users/grfn/emacs.d/org-config.el
@@ -0,0 +1,193 @@
+;;; -*- lexical-binding: t; -*-
+
+(defun notes-file (f)
+  (concat org-directory (if (string-prefix-p "/" f) "" "/") f))
+
+(defun grfn/org-project-tag->key (tag)
+  (s-replace-regexp "^project__" "" tag))
+
+(defun grfn/org-project-tag->name (tag)
+  (s-titleized-words
+   (s-join " " (s-split "_" (grfn/org-project-tag->key tag)))))
+
+(defun grfn/org-project-tag->keys (tag)
+  (s-join "" (cons "p"
+                   (-map (lambda (s) (substring-no-properties s 0 1))
+                         (s-split "_" (grfn/org-project-tag->key tag))))))
+
+(defun grfn/org-projects->agenda-commands (project-tags)
+  (loop for tag in project-tags
+        collect `(,(grfn/org-project-tag->keys tag)
+                  ,(grfn/org-project-tag->name tag)
+                  tags-todo
+                  ,tag)))
+
+(defun grfn/org-projects ()
+  (loop for (tag) in
+        (org-global-tags-completion-table
+         (directory-files-recursively "~/notes" "\\.org$"))
+        when (s-starts-with-p "project__" tag)
+        collect tag))
+
+(comment
+ (grfn/org-projects->agenda-commands (grfn/org-projects))
+ )
+
+(setq
+ org-directory (expand-file-name "~/notes")
+ +org-dir (expand-file-name "~/notes")
+ org-default-notes-file (concat org-directory "/inbox.org")
+ +org-default-todo-file (concat org-directory "/inbox.org")
+ org-agenda-files (directory-files-recursively
+                   "~/notes" "\\.org$")
+ org-refile-targets '((org-agenda-files :maxlevel . 3))
+ org-outline-path-complete-in-steps nil
+ org-refile-use-outline-path t
+ org-file-apps `((auto-mode . emacs)
+                 (,(rx (or (and "." (optional "x") (optional "htm") (optional "l") buffer-end)
+                           (and buffer-start "http" (optional "s") "://")))
+                  . "firefox %s")
+                 (,(rx ".pdf" buffer-end) . "apvlv %s")
+                 (,(rx "." (or "png"
+                               "jpg"
+                               "jpeg"
+                               "gif"
+                               "tif"
+                               "tiff")
+                       buffer-end)
+                  . "feh %s"))
+ org-log-done 'time
+ org-archive-location "~/notes/trash::* From %s"
+ org-cycle-separator-lines 2
+ org-hidden-keywords '(title)
+ org-tags-column -130
+ org-ellipsis "…"
+ org-imenu-depth 9
+ org-capture-templates
+ `(("t" "Todo" entry
+    (file +org-default-todo-file)
+    "* TODO %?\n%i"
+    :kill-buffer t)
+
+   ("m" "Email" entry
+    (file +org-default-todo-file)
+    "* TODO [[%L][%:subject]] :email:\n%i")
+
+   ("n" "Notes" entry
+    (file +org-default-todo-file)
+    "* %U %?\n%i"
+    :prepend t
+    :kill-buffer t)
+
+   ("c" "Task note" entry
+    (clock)
+    "* %U %?\n%i[%l[Context]]\n"
+    :kill-buffer t
+    :unnarrowed t)
+
+   ("p" "Projects")
+   ("px" "Xanthous" entry
+    (file+headline ,(notes-file "xanthous.org") "Backlog")
+    "* TODO %?\nContext %a\nIn task: %K")
+   ("pt" "Tvix" entry
+    (file+headline ,(notes-file "tvix.org") "Tvix TODO")
+    "* TODO %?\nContext %a\nIn task: %K")
+   ("pw" "Windtunnel" entry
+    (file+headline ,(notes-file "windtunnel.org") "Tasks")
+    "* TODO %i%?\nContext: %a\nIn task: %K")
+
+   ("d" "Data recording")
+   )
+
+ org-capture-templates-contexts
+ `(("px" ((in-file . "/home/grfn/code/depot/users/grfn/xanthous/.*"))))
+
+ org-deadline-warning-days 1
+ org-agenda-skip-scheduled-if-deadline-is-shown 'todo
+ org-todo-keywords '((sequence "TODO(t)" "ACTIVE(a)" "|" "DONE(d)" "RUNNING(r)")
+                     (sequence "NEXT(n)" "WAITING(w)" "LATER(l)" "|" "CANCELLED(c)"))
+ org-agenda-custom-commands
+ `(("S" "Sprint Tasks" tags-todo "sprint")
+   ("i" "Inbox" tags "inbox")
+   ("r" "Running jobs" todo "RUNNING")
+   ("w" "@Work" tags-todo "@work")
+   ("n" . "Next...")
+   ("np" "Next Sprint" tags-todo "next_sprint|sprint_planning")
+
+   ("p" . "Project...")
+   ,@(grfn/org-projects->agenda-commands (grfn/org-projects)))
+
+ org-agenda-dim-blocked-tasks nil
+ org-enforce-todo-dependencies nil
+
+ org-babel-clojure-backend 'cider)
+
+
+(defun +grfn/insert-work-template ()
+  (interactive)
+  (goto-char (point-min))
+  (forward-line)
+  (insert "#+TODO: TODO(t) NEXT(n) ACTIVE(a) | DONE(d) PR(p) RUNNING(r) TESTING(D)
+#+TODO: BLOCKED(b) BACKLOG(l) PROPOSED(o) | CANCELLED(c)
+#+FILETAGS: @work
+#+FILETAGS: @work
+#+PROPERTY: Effort_ALL 0 4:00 8:00 12:00 20:00 32:00
+#+PROPERTY: ESTIMATE_ALL 0 1 2 3 5 8
+#+PROPERTY: STORY-TYPE_ALL Feature Bug Chore
+#+PROPERTY: NOBLOCKING t
+#+COLUMNS: %TODO %40ITEM(Task) %17EFFORT(Estimated){:} %CLOCKSUM(Time Spent) %17STORY-TYPE(Type) %TAGS"))
+
+(defun +grfn/insert-org-template ()
+  (interactive)
+  (pcase (buffer-file-name)
+    ((s-contains "/work/") (+grfn/insert-work-template))))
+
+;;; TODO: this doesn't work?
+(define-auto-insert "\\.org?$" #'grfn/insert-org-template t)
+
+(defun forge--post-submit-around---link-pr-to-org-item
+    (orig)
+  (let ((cb (funcall orig)))
+    (lambda (value headers status req)
+      (prog1 (funcall cb value headers status req)
+        (grfn/at-org-clocked-in-item
+         (let ((url (alist-get 'html_url value))
+               (number (alist-get 'number value)))
+           (org-set-property
+            "pull-request"
+            (org-make-link-string
+             url
+             (format "%s/%s/%d"
+                     (->> value
+                          (alist-get 'base)
+                          (alist-get 'repo)
+                          (alist-get 'name))
+                     (->> value
+                          (alist-get 'base)
+                          (alist-get 'repo)
+                          (alist-get 'owner)
+                          (alist-get 'login))
+                     number)))))))))
+
+(advice-add
+ #'forge--post-submit-callback
+ :around #'forge--post-submit-around---link-pr-to-org-item)
+
+(defun +grfn/org-setup ()
+  (setq-local truncate-lines -1)
+  (display-line-numbers-mode -1)
+  (line-number-mode -1))
+
+(add-hook 'org-mode-hook #'+grfn/org-setup)
+
+(set-face-foreground 'org-block +solarized-s-base00)
+(setq whitespace-global-modes '(not org-mode magit-mode vterm-mode))
+(setf (alist-get 'file org-link-frame-setup) 'find-file-other-window)
+(set-face-foreground 'org-block +solarized-s-base00)
+
+;; (add-hook! org-mode
+;;   (set-company-backend! 'org-mode
+;;     '(:separate company-ob-postgresql
+;;                 company-dabbrev
+;;                 company-yasnippet
+;;                 company-ispell)))
diff --git a/users/grfn/emacs.d/org-gcal.el b/users/grfn/emacs.d/org-gcal.el
new file mode 100644
index 0000000000..3e315c5e60
--- /dev/null
+++ b/users/grfn/emacs.d/org-gcal.el
@@ -0,0 +1,181 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 'aio)
+(require 'parse-time)
+
+(setq-local lexical-binding t)
+(setq plstore-cache-passphrase-for-symmetric-encryption t)
+
+(defvar gcal-client-id)
+(defvar gcal-client-secret)
+
+(defvar google-calendar-readonly-scope
+  "https://www.googleapis.com/auth/calendar.readonly")
+
+(defvar events-file "/home/grfn/notes/events.org")
+
+(defun google--get-token (scope client-id client-secret)
+  (oauth2-auth-and-store
+   "https://accounts.google.com/o/oauth2/v2/auth"
+   "https://oauth2.googleapis.com/token"
+   scope
+   client-id
+   client-secret))
+
+(cl-defun google--request (url &key method params scope)
+  (let ((p (aio-promise))
+        (auth-token (google--get-token scope gcal-client-id gcal-client-secret)))
+    (oauth2-refresh-access auth-token)
+    (oauth2-url-retrieve
+     auth-token
+     url
+     (lambda (&rest _)
+       (goto-char (point-min))
+       (re-search-forward "^$")
+       (let ((resp (json-parse-buffer :object-type 'alist)))
+         (aio-resolve p (lambda () resp))))
+     nil
+     (or method "GET")
+     params)
+    p))
+
+(cl-defun list-events (&key min-time max-time)
+  (google--request
+   (concat
+    "https://www.googleapis.com/calendar/v3/calendars/griffin@urbint.com/events"
+    "?timeMin=" (format-time-string "%Y-%m-%dT%T%z" min-time)
+    "&timeMax=" (format-time-string "%Y-%m-%dT%T%z" max-time))
+   :scope google-calendar-readonly-scope))
+
+
+(defun last-week-events ()
+  (list-events :min-time (time-subtract
+                          (current-time)
+                          (seconds-to-time
+                           (* 60 60 24 7)))
+               :max-time (current-time)))
+
+(defun next-week-events ()
+  (list-events :min-time (current-time)
+               :max-time (time-add
+                          (current-time)
+                          (seconds-to-time
+                           (* 60 60 24 7)))))
+
+(defun attending-event? (event)
+  (let* ((attendees (append (alist-get 'attendees event) nil))
+         (self (--find (alist-get 'self it) attendees)))
+    (equal "accepted" (alist-get 'responseStatus self))))
+
+(defun event->org-headline (event level)
+  (cl-flet ((make-time
+             (key)
+             (when-let ((raw-time (->> event (alist-get key) (alist-get 'dateTime))))
+               (format-time-string
+                (org-time-stamp-format t)
+                (parse-iso8601-time-string raw-time)))))
+    (if-let ((start-time (make-time 'start))
+             (end-time (make-time 'end)))
+        (s-format
+         "${headline} [[${htmlLink}][${summary}]] :event:
+${startTime}--${endTime}
+:PROPERTIES:
+${location-prop}
+:EVENT: ${htmlLink}
+:END:
+
+${description}"
+         (function
+          (lambda (k m)
+            (or (alist-get (intern k) m)
+                (format "key not found: %s" k))))
+         (append
+          event
+          `((headline . ,(make-string level ?*))
+            (startTime . ,start-time)
+            (endTime . ,end-time)
+            (location-prop
+             . ,(if-let ((location (alist-get 'location event)))
+                    (s-lex-format ":LOCATION: ${location}")
+                  "")))))
+      "")))
+
+(comment
+ (alist-get 'foo nil)
+ )
+
+(defun write-events (events)
+  (with-current-buffer (find-file-noselect events-file)
+    (save-mark-and-excursion
+      (save-restriction
+        (widen)
+        (erase-buffer)
+        (goto-char (point-min))
+        (insert "#+TITLE: Events")
+        (newline) (newline)
+        (prog1
+            (loop for event in (append events nil)
+                  when (attending-event? event)
+                  do
+                  (insert (event->org-headline event 1))
+                  (newline)
+                  sum 1)
+          (org-align-tags t))))))
+
+(defun +grfn/sync-events ()
+  (interactive)
+  (let* ((events (alist-get 'items (aio-wait-for (next-week-events))))
+         (num-written (write-events events)))
+    (message "Successfully wrote %d events" num-written)))
+
+(comment
+ ((kind . "calendar#event")
+  (etag . "\"3174776941020000\"")
+  (id . "SNIP")
+  (status . "confirmed")
+  (htmlLink . "https://www.google.com/calendar/event?eid=SNIP")
+  (created . "2020-04-01T13:30:09.000Z")
+  (updated . "2020-04-20T13:14:30.510Z")
+  (summary . "SNIP")
+  (description . "SNIP")
+  (location . "SNIP")
+  (creator
+   (email . "griffin@urbint.com")
+   (self . t))
+  (organizer
+   (email . "griffin@urbint.com")
+   (self . t))
+  (start
+   (dateTime . "2020-04-01T12:00:00-04:00")
+   (timeZone . "America/New_York"))
+  (end
+   (dateTime . "2020-04-01T12:30:00-04:00")
+   (timeZone . "America/New_York"))
+  (recurrence .
+              ["RRULE:FREQ=WEEKLY;UNTIL=20200408T035959Z;BYDAY=WE"])
+  (iCalUID . "SNIP")
+  (sequence . 0)
+  (attendees .
+             [((email . "griffin@urbint.com")
+               (organizer . t)
+               (self . t)
+               (responseStatus . "accepted"))
+              ((email . "SNIP")
+               (displayName . "SNIP")
+               (responseStatus . "needsAction"))])
+  (extendedProperties
+   (private
+    (origRecurringId . "309q48kc1dihsvbi13pnlimb5a"))
+   (shared
+    (origRecurringId . "309q48kc1dihsvbi13pnlimb5a")))
+  (reminders
+   (useDefault . t)))
+
+ (require 'icalendar)
+
+ (icalendar--convert-recurring-to-diary
+  nil
+  "RRULE:FREQ=WEEKLY;UNTIL=20200408T035959Z;BYDAY=WE"
+  )
+
+ )
diff --git a/users/grfn/emacs.d/org-query.el b/users/grfn/emacs.d/org-query.el
new file mode 100644
index 0000000000..e403c9e56f
--- /dev/null
+++ b/users/grfn/emacs.d/org-query.el
@@ -0,0 +1,131 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 'org)
+(require 'org-agenda)
+(require 'inflections)
+
+(defun grfn/org-text-element->string (elt)
+  (cond
+   ((stringp elt) elt)
+   ((and (consp elt)
+         (symbolp (car elt)))
+    (-> elt (caddr) (grfn/org-text-element->string) (s-trim) (concat " ")))))
+
+(defun grfn/org-element-title (elt)
+  (let ((title (org-element-property :title elt)))
+    (cond
+     ((stringp title) title)
+     ((listp title)
+      (->> title
+           (mapcar #'grfn/org-text-element->string)
+           (s-join "")
+           (s-trim))))))
+
+(defun grfn/org-agenda-entry->element (agenda-entry)
+  ;; ???
+  ())
+
+(defun org-elements-agenda-match (match &optional todo-only)
+  (setq match
+        (propertize match 'inherited t))
+  (with-temp-buffer
+    (let ((inhibit-redisplay (not debug-on-error))
+          (org-agenda-sticky nil)
+          (org-agenda-buffer-tmp-name (buffer-name))
+          (org-agenda-buffer-name (buffer-name))
+          (org-agenda-buffer (current-buffer))
+          (matcher (org-make-tags-matcher match))
+          result)
+      (org-agenda-prepare (concat "TAGS " match))
+      (setq match (car matcher)
+            matcher (cdr matcher))
+      (dolist (file (org-agenda-files nil 'ifmode)
+                    result)
+        (catch 'nextfile
+          (org-check-agenda-file file)
+          (when-let ((buffer (if (file-exists-p file)
+                                 (org-get-agenda-file-buffer file)
+                               (error "No such file %s" file))))
+            (with-current-buffer buffer
+              (unless (derived-mode-p 'org-mode)
+                (error "Agenda file %s is not in Org mode" file))
+              (save-excursion
+                (save-restriction
+                  (if (eq buffer org-agenda-restrict)
+                      (narrow-to-region org-agenda-restrict-begin
+                                        org-agenda-restrict-end)
+                    (widen))
+                  (setq result
+                        (append result (org-scan-tags
+                                        'agenda
+                                        matcher
+                                        todo-only))))))))))))
+
+(defun grfn/num-inbox-items ()
+  (length (org-elements-agenda-match "inbox" t)))
+
+(defun grfn/num-inbox-items-message ()
+  (let ((n (grfn/num-inbox-items)))
+    (if (zerop n) ""
+      (format "%d %s"
+              n
+              (if (= 1 n) "item" "items")))))
+
+(defmacro grfn/at-org-clocked-in-item (&rest body)
+  `(when (org-clocking-p)
+     (let ((m org-clock-marker))
+       (with-current-buffer (marker-buffer m)
+         (save-mark-and-excursion
+           (goto-char m)
+           (org-back-to-heading t)
+           ,@body)))))
+
+(defun grfn/org-element-clocked-in-task ()
+  (grfn/at-org-clocked-in-item
+   (org-element-at-point)))
+
+(comment
+ (grfn/org-element-clocked-in-task)
+ (org-element-property :title (grfn/org-element-clocked-in-task))
+ )
+
+(defun grfn/minutes->hours:minutes (minutes)
+  (format "%d:%02d"
+          (floor (/ minutes 60))
+          (mod minutes 60)))
+
+(comment
+ (grfn/minutes->hours:minutes 1)        ; => "0:01"
+ (grfn/minutes->hours:minutes 15)       ; => "0:15"
+ (grfn/minutes->hours:minutes 130)      ; => "2:10"
+ )
+
+(defun grfn/org-current-clocked-in-task-message ()
+  (if (org-clocking-p)
+      (format "(%s) [%s]"
+              (->> (grfn/org-element-clocked-in-task)
+                   (grfn/org-element-title)
+                   (substring-no-properties)
+                   (s-trim))
+              (grfn/minutes->hours:minutes
+               (org-clock-get-clocked-time)))
+    ""))
+
+(comment
+ (grfn/org-current-clocked-in-task-message)
+ )
+
+(defun grfn/org-clocked-in-jira-ticket-id ()
+  (grfn/at-org-clocked-in-item
+   (when (org-tracker-current-backend t)
+     (org-tracker-backend/extract-issue-id
+      (org-tracker-current-backend)
+      (cadr (org-element-at-point))))))
+
+(comment
+ (grfn/at-org-clocked-in-item
+  (org-tracker-backend/extract-issue-id
+   (org-tracker-current-backend)
+   (cadr (org-element-at-point))))
+
+ )
diff --git a/users/grfn/emacs.d/packages.el b/users/grfn/emacs.d/packages.el
new file mode 100644
index 0000000000..bc323c116c
--- /dev/null
+++ b/users/grfn/emacs.d/packages.el
@@ -0,0 +1,153 @@
+;; -*- no-byte-compile: t; -*-
+;;; private/grfn/packages.el
+
+(package! moody)
+
+;; Editor
+(package! solarized-theme)
+(package! fill-column-indicator)
+(package! flx)
+(package! general
+  :recipe (:host github :repo "noctuid/general.el"))
+(package! fill-column-indicator)
+(package! writeroom-mode)
+(package! dash)
+(package! w3m)
+(package! rainbow-mode)
+(package! string-inflection)
+
+;;; Org
+(package! org-tracker
+  :recipe (:host file
+           :local-repo "~/code/org-tracker"))
+(package! org-alert)
+(package! ob-http)
+(package! ob-ipython)
+(package! ob-async)
+(package! org-recent-headings)
+(package! org-sticky-header)
+(package! gnuplot)
+(package! gnuplot-mode)
+(package! org-d20)
+
+;; Presentation
+(package! epresent)
+(package! org-tree-slide)
+(package! ox-reveal)
+
+;; Slack etc
+(package! slack)
+(package! alert)
+
+;; Git
+(package! evil-magit)
+(package! marshal)
+(package! forge)
+(package!
+  github-review
+  :recipe
+  (:host github
+         :repo "charignon/github-review"
+         :files ("github-review.el")))
+
+;; Elisp
+(package! dash)
+(package! dash-functional)
+(package! s)
+(package! request)
+(package! predd
+  :recipe (:host github :repo "skeeto/predd"))
+(package! aio)
+
+;; Haskell
+(package! lsp-haskell)
+(package! counsel-etags)
+
+;;; LSP
+(package! lsp-mode)
+(package! lsp-ui :recipe (:host github :repo "emacs-lsp/lsp-ui"))
+(package! company-lsp)
+(package! lsp-treemacs)
+
+;; Rust
+(package! rustic :disable t)
+;; (package! racer :disable t)
+(package! cargo)
+
+;; Lisp
+(package! paxedit)
+
+;; Javascript
+(package! flow-minor-mode)
+(package! flycheck-flow)
+(package! company-flow)
+(package! prettier-js)
+
+;; GraphQL
+(package! graphql-mode)
+
+;; Haskell
+(package! lsp-mode)
+(package! lsp-ui)
+(package! lsp-haskell)
+(package! company-lsp)
+;; (package! lsp-imenu)
+
+;; Clojure
+(package! flycheck-clojure)
+
+;; SQL
+(package! sqlup-mode)
+(package! emacsql)
+(package! emacsql-psql)
+
+;;; Python
+(package! pyimport)
+;; (package! yapfify)
+(package! blacken)
+
+
+;;; Desktop interaction
+(package! counsel-spotify)
+
+;;; Dhall
+(package! dhall-mode)
+
+;;; Kubernetes
+(package! kubernetes)
+(package! kubernetes-evil)
+(package! k8s-mode)
+
+;;; Stack Exchange
+(package! sx)
+
+;;; Nix
+(package! nix-update
+  :recipe (:host github
+           :repo "glittershark/nix-update-el"))
+(package! direnv)
+
+;;; Sequence diagrams
+(package! wsd-mode
+  :recipe (:host github
+           :repo "josteink/wsd-mode"))
+
+;;; logic?
+(package! metal-mercury-mode
+  :recipe (:host github
+                 :repo "ahungry/metal-mercury-mode"))
+(package! flycheck-mercury)
+
+(package! terraform-mode)
+(package! company-terraform)
+
+(package! jsonnet-mode)
+
+;;;
+(package! znc
+  :recipe (:host github
+                 :repo "sshirokov/ZNC.el"))
+
+;;; cpp
+(package! protobuf-mode)
+(package! clang-format+)
diff --git a/users/grfn/emacs.d/rust.el b/users/grfn/emacs.d/rust.el
new file mode 100644
index 0000000000..1c1c75aab5
--- /dev/null
+++ b/users/grfn/emacs.d/rust.el
@@ -0,0 +1,40 @@
+;;; -*- lexical-binding: t; -*-
+
+(add-to-list 'auto-mode-alist '("\\.rs$" . rust-mode))
+
+(defun grfn/rust-setup ()
+  (interactive)
+
+  (push '(?> . ("<" . ">")) evil-surround-pairs-alist)
+  (push '(?< . ("< " . " >")) evil-surround-pairs-alist)
+
+  (setq lsp-rust-server 'rust-analyzer)
+  (setq-local whitespace-line-column 100
+              fill-column 100)
+  (setq rust-format-show-buffer nil)
+  (setq lsp-rust-analyzer-import-merge-behaviour "last"
+        lsp-rust-analyzer-cargo-watch-command "clippy"
+        lsp-rust-analyzer-cargo-watch-args ["--target-dir" "/home/grfn/code/readyset/readyset/target/rust-analyzer"]
+        lsp-ui-doc-enable t)
+  (rust-enable-format-on-save)
+  (lsp))
+
+(add-hook 'rust-mode-hook #'grfn/rust-setup)
+
+(map!
+ (:map rust-mode-map
+  :n "g RET" #'lsp-rust-analyzer-run
+  :n "g R" #'lsp-find-references
+  :n "g d" #'lsp-find-definition
+  :n "g Y" #'lsp-goto-type-definition
+  (:localleader
+   "m" #'lsp-rust-analyzer-expand-macro)))
+
+(comment
+ (flycheck-get-next-checkers 'lsp)
+ (flycheck-add-next-checker)
+ (flycheck-get-next-checkers 'lsp)
+ )
+
+(set-company-backend! 'rust-mode
+  '(:separate company-capf company-yasnippet))
diff --git a/users/grfn/emacs.d/show-matching-paren.el b/users/grfn/emacs.d/show-matching-paren.el
new file mode 100644
index 0000000000..ab65a912a8
--- /dev/null
+++ b/users/grfn/emacs.d/show-matching-paren.el
@@ -0,0 +1,61 @@
+;;; -*- lexical-binding: t; -*-
+
+;;; https://with-emacs.com/posts/ui-hacks/show-matching-lines-when-parentheses-go-off-screen/
+
+;; we will call `blink-matching-open` ourselves...
+(remove-hook 'post-self-insert-hook
+             #'blink-paren-post-self-insert-function)
+;; this still needs to be set for `blink-matching-open` to work
+(setq blink-matching-paren 'show)
+
+(let ((ov nil)) ; keep track of the overlay
+  (advice-add
+   #'show-paren-function
+   :after
+    (defun show-paren--off-screen+ (&rest _args)
+      "Display matching line for off-screen paren."
+      (when (overlayp ov)
+        (delete-overlay ov))
+      ;; check if it's appropriate to show match info,
+      ;; see `blink-paren-post-self-insert-function'
+      (when (and (overlay-buffer show-paren--overlay)
+                 (not (or cursor-in-echo-area
+                          executing-kbd-macro
+                          noninteractive
+                          (minibufferp)
+                          this-command))
+                 (and (not (bobp))
+                      (memq (char-syntax (char-before)) '(?\) ?\$)))
+                 (= 1 (logand 1 (- (point)
+                                   (save-excursion
+                                     (forward-char -1)
+                                     (skip-syntax-backward "/\\")
+                                     (point))))))
+        ;; rebind `minibuffer-message' called by
+        ;; `blink-matching-open' to handle the overlay display
+        (cl-letf (((symbol-function #'minibuffer-message)
+                   (lambda (msg &rest args)
+                     (let ((msg (apply #'format-message msg args)))
+                       (setq ov (display-line-overlay+
+                                 (window-start) msg ))))))
+          (blink-matching-open))))))
+
+(defun display-line-overlay+ (pos str &optional face)
+  "Display line at POS as STR with FACE.
+
+FACE defaults to inheriting from default and highlight."
+  (let ((ol (save-excursion
+              (goto-char pos)
+              (make-overlay (line-beginning-position)
+                            (line-end-position)))))
+    (overlay-put ol 'display str)
+    (overlay-put ol 'face
+                 (or face '(:inherit default :inherit highlight)))
+    ol))
+
+(setq show-paren-style 'paren
+      show-paren-delay 0.03
+      show-paren-highlight-openparen t
+      show-paren-when-point-inside-paren nil
+      show-paren-when-point-in-periphery t)
+(show-paren-mode 1)
diff --git a/users/grfn/emacs.d/slack-snippets.el b/users/grfn/emacs.d/slack-snippets.el
new file mode 100644
index 0000000000..b5bd4db748
--- /dev/null
+++ b/users/grfn/emacs.d/slack-snippets.el
@@ -0,0 +1,227 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 'dash)
+(require 'dash-functional)
+(require 'request)
+
+;;;
+;;; Configuration
+;;;
+
+(defvar slack/token nil
+  "Legacy (https://api.slack.com/custom-integrations/legacy-tokens) access token")
+
+(defvar slack/include-public-channels 't
+  "Whether or not to inclue public channels in the list of conversations")
+
+(defvar slack/include-private-channels 't
+  "Whether or not to inclue public channels in the list of conversations")
+
+(defvar slack/include-im 't
+  "Whether or not to inclue IMs (private messages) in the list of conversations")
+
+(defvar slack/include-mpim nil
+  "Whether or not to inclue multi-person IMs (multi-person private messages) in
+  the list of conversations")
+
+;;;
+;;; Utilities
+;;;
+
+(defmacro comment (&rest _body)
+  "Comment out one or more s-expressions"
+  nil)
+
+(defun ->list (vec) (append vec nil))
+
+(defun json-truthy? (x) (and x (not (equal :json-false x))))
+
+;;;
+;;; Generic API integration
+;;;
+
+(defvar slack/base-url "https://slack.com/api")
+
+(defun slack/get (path params &optional callback)
+  "params is an alist of query parameters"
+  (let* ((params-callback (if (functionp params) `(() . ,params) (cons params callback)))
+         (params (car params-callback)) (callback (cdr params-callback))
+         (params (append `(("token" . ,slack/token)) params))
+         (url (concat (file-name-as-directory slack/base-url) path)))
+    (request url
+             :type "GET"
+             :params params
+             :parser 'json-read
+             :success (cl-function
+                       (lambda (&key data &allow-other-keys)
+                         (funcall callback data))))))
+
+(defun slack/post (path params &optional callback)
+  (let* ((params-callback (if (functionp params) `(() . ,params) (cons params callback)))
+         (params (car params-callback)) (callback (cdr params-callback))
+         (url (concat (file-name-as-directory slack/base-url) path)))
+    (request url
+             :type "POST"
+             :data (json-encode params)
+             :headers `(("Content-Type"  . "application/json")
+                        ("Authorization" . ,(format "Bearer %s" slack/token)))
+             :success (cl-function
+                       (lambda (&key data &allow-other-keys)
+                         (funcall callback data))))))
+
+
+;;;
+;;; Specific API endpoints
+;;;
+
+;; Users
+
+(defun slack/users (cb)
+  "Returns users as (id . name) pairs"
+  (slack/get
+   "users.list"
+   (lambda (data)
+     (->> data
+          (assoc-default 'members)
+          ->list
+          (-map (lambda (user)
+                  (cons (assoc-default 'id user)
+                        (assoc-default 'real_name user))))
+          (-filter #'cdr)
+          (funcall cb)))))
+
+(comment
+ (slack/get
+  "users.list"
+  (lambda (data) (setq response-data data)))
+
+ (slack/users (lambda (data) (setq --users data)))
+
+ )
+
+;; Conversations
+
+(defun slack/conversation-types ()
+  (->>
+   (list (when slack/include-public-channels  "public_channel")
+         (when slack/include-private-channels "private_channel")
+         (when slack/include-im               "im")
+         (when slack/include-mpim             "mpim"))
+   (-filter #'identity)
+   (s-join ",")))
+
+(defun channel-label (chan users-alist)
+  (cond
+   ((json-truthy? (assoc-default 'is_channel chan))
+    (format "#%s" (assoc-default 'name chan)))
+   ((json-truthy? (assoc-default 'is_im chan))
+    (let ((user-id (assoc-default 'user chan)))
+      (format "Private message with %s" (assoc-default user-id users-alist))))
+   ((json-truthy? (assoc-default 'is_mpim chan))
+    (->> chan
+         (assoc-default 'purpose)
+         (assoc-default 'value)))))
+
+(defun slack/conversations (cb)
+  "Calls `cb' with (id . '((label . \"label\") '(topic . \"topic\") '(purpose . \"purpose\"))) pairs"
+  (slack/get
+   "conversations.list"
+   `(("types"            . ,(slack/conversation-types))
+     ("exclude-archived" . "true"))
+   (lambda (data)
+     (setq --data data)
+     (slack/users
+      (lambda (users)
+        (->> data
+             (assoc-default 'channels)
+             ->list
+             (-map
+              (lambda (chan)
+                (cons (assoc-default 'id chan)
+                      `((label   . ,(channel-label chan users))
+                        (topic   . ,(->> chan
+                                         (assoc-default 'topic)
+                                         (assoc-default 'value)))
+                        (purpose . ,(->> chan
+                                         (assoc-default 'purpose)
+                                         (assoc-default 'value)))))))
+             (funcall cb)))))))
+
+(comment
+ (slack/get
+  "conversations.list"
+  '(("types" . "public_channel,private_channel,im,mpim"))
+  (lambda (data) (setq response-data data)))
+
+ (slack/get
+  "conversations.list"
+  '(("types" . "im"))
+  (lambda (data) (setq response-data data)))
+
+ (slack/conversations
+  (lambda (convos) (setq --conversations convos)))
+
+ )
+
+;; Messages
+
+(cl-defun slack/post-message
+    (&key text channel-id (on-success #'identity))
+  (slack/post "chat.postMessage"
+              `((text    . ,text)
+                (channel . ,channel-id)
+                (as_user . t))
+              on-success))
+
+(comment
+
+ (slack/post-message
+  :text "hi slackbot"
+  :channel-id slackbot-channel-id
+  :on-success (lambda (data) (setq resp data)))
+
+ )
+
+;;;
+;;; Posting code snippets to slack
+;;;
+
+(defun prompt-for-channel (cb)
+  (slack/conversations
+   (lambda (conversations)
+     (ivy-read
+      "Select channel: "
+      ;; TODO want to potentially use purpose / topic stuff here
+      (->> conversations
+           (-filter (lambda (c) (assoc-default 'label (cdr c))))
+           (-map (lambda (chan) (let ((label (assoc-default 'label (cdr chan)))
+                                 (id (car chan)))
+                             (propertize label 'channel-id id)))))
+      :history 'slack/channel-history
+      :action (lambda (selected)
+                (let ((channel-id (get-text-property 0 'channel-id selected)))
+                  (funcall cb channel-id)
+                  (message "Sent message to %s" selected))))))
+  nil)
+
+(comment
+ (prompt-for-channel #'message)
+ (->> --convos
+      (-filter (lambda (c) (assoc-default 'label (cdr c))))
+      (-map (lambda (chan) (let ((label (assoc-default 'label (cdr chan)))
+                       (id (car chan)))
+                   (propertize label 'channel-id id)))))
+
+ (->> --convos (car) (cdr) (assoc-default 'label))
+ )
+
+(defun slack-send-code-snippet (&optional snippet-text)
+  (interactive
+   (list (buffer-substring-no-properties (mark) (point))))
+  (prompt-for-channel
+   (lambda (channel-id)
+     (slack/post-message
+      :text       (format "```\n%s```" snippet-text)
+      :channel-id channel-id))))
+
+(provide 'slack-snippets)
diff --git a/users/grfn/emacs.d/slack.el b/users/grfn/emacs.d/slack.el
new file mode 100644
index 0000000000..54d3b40b09
--- /dev/null
+++ b/users/grfn/emacs.d/slack.el
@@ -0,0 +1,24 @@
+;;; -*- lexical-binding: t; -*-
+
+(after! slack
+  (set-face-foreground 'slack-message-output-header +solarized-s-base01)
+  (set-face-attribute 'slack-message-output-header nil :underline nil)
+  (set-face-attribute 'slack-message-output-text nil :height 1.0))
+
+(require 'slack)
+(setq slack-buffer-emojify 't
+      slack-prefer-current-team 't
+      slack-thread-also-send-to-room nil)
+
+(set-popup-rule! "^\\*Slack"
+  :quit nil
+  :select t
+  :side 'bottom
+  :ttl nil
+  :size 0.5)
+
+(add-hook #'slack-message-buffer-mode-hook
+          (lambda () (toggle-truncate-lines -1)))
+
+(map! (:map slack-message-buffer-mode-map
+       :n "q" #'delete-window))
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/annotation b/users/grfn/emacs.d/snippets/haskell-mode/annotation
new file mode 100644
index 0000000000..8a2854d759
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/annotation
@@ -0,0 +1,5 @@
+# key: ann
+# name: annotation
+# expand-env: ((yas-indent-line 'fixed))
+# --
+{-# ANN ${1:module} ("${2:HLint: ignore ${3:Reduce duplication}}" :: String) #-}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/benchmark-module b/users/grfn/emacs.d/snippets/haskell-mode/benchmark-module
new file mode 100644
index 0000000000..cbb1646e41
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/benchmark-module
@@ -0,0 +1,26 @@
+# key: bench
+# name: benchmark-module
+# expand-env: ((yas-indent-line (quote fixed)))
+# --
+--------------------------------------------------------------------------------
+module ${1:`(if (not buffer-file-name) "Module"
+                (let ((name (file-name-sans-extension (buffer-file-name)))
+                      (case-fold-search nil))
+                     (if (cl-search "bench/" name)
+                         (replace-regexp-in-string "/" "."
+                           (replace-regexp-in-string "^\/[^A-Z]*" ""
+                             (car (last (split-string name "src")))))
+                         (file-name-nondirectory name))))`} ( benchmark, main ) where
+--------------------------------------------------------------------------------
+import Bench.Prelude
+--------------------------------------------------------------------------------
+import ${1:$(s-chop-suffix "Bench" yas-text)}
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain [benchmark]
+
+--------------------------------------------------------------------------------
+
+benchmark :: Benchmark
+benchmark = bgroup "${1:$(->> yas-text (s-chop-suffix "Bench") (s-split ".") -last-item)}" [bench "something dumb" $ nf (1 +) (1 :: Int)]
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/header b/users/grfn/emacs.d/snippets/haskell-mode/header
new file mode 100644
index 0000000000..fdd8250d86
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/header
@@ -0,0 +1,5 @@
+# key: hh
+# name: header
+# expand-env: ((yas-indent-line 'fixed))
+# --
+--------------------------------------------------------------------------------$2
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/hedgehog-generator b/users/grfn/emacs.d/snippets/haskell-mode/hedgehog-generator
new file mode 100644
index 0000000000..68863f7054
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/hedgehog-generator
@@ -0,0 +1,8 @@
+# key: gen
+# name: Hedgehog Generator
+# expand-env: ((yas-indent-line (quote fixed)))
+# --
+gen${1:Foo} :: Gen $1
+gen$1 = do
+  $2
+  pure $1{..}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/hedgehog-property b/users/grfn/emacs.d/snippets/haskell-mode/hedgehog-property
new file mode 100644
index 0000000000..bf39a2a3ee
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/hedgehog-property
@@ -0,0 +1,9 @@
+# -*- mode: snippet -*-
+# name: Hedgehog Property
+# key: hprop
+# expand-env: ((yas-indent-line 'fixed))
+# --
+hprop_${1:somethingIsAlwaysTrue} :: Property
+hprop_$1 = property $ do
+  ${2:x} <- forAll ${3:Gen.int $ Range.linear 1 100}
+  ${4:x === x}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/hlint b/users/grfn/emacs.d/snippets/haskell-mode/hlint
new file mode 100644
index 0000000000..74b63dc672
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/hlint
@@ -0,0 +1,8 @@
+# -*- mode: snippet -*-
+# name: hlint
+# uuid:
+# expand-env: ((yas-indent-line 'fixed))
+# key: hlint
+# condition: t
+# --
+{-# ANN module ("Hlint: ignore $1" :: String) #- }
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/import-i b/users/grfn/emacs.d/snippets/haskell-mode/import-i
new file mode 100644
index 0000000000..4a7fca2c2f
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/import-i
@@ -0,0 +1,4 @@
+# key: i
+# name: import-i
+# --
+import           ${1:Prelude}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/inl b/users/grfn/emacs.d/snippets/haskell-mode/inl
new file mode 100644
index 0000000000..6e17b83d71
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/inl
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: inl
+# key: inl
+# expand-env: ((yas-indent-line 'fixed))
+# --
+{-# INLINE $1 #-}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/inline b/users/grfn/emacs.d/snippets/haskell-mode/inline
new file mode 100644
index 0000000000..1beafbe50b
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/inline
@@ -0,0 +1,5 @@
+# key: inline
+# name: inline
+# expand-env: ((yas-indent-line 'fixed))
+# --
+{-# INLINE $1 #-}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/language pragma b/users/grfn/emacs.d/snippets/haskell-mode/language pragma
new file mode 100644
index 0000000000..6f84720f45
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/language pragma
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: language pragma
+# key: lang
+# expand-env: ((yas-indent-line 'fixed))
+# --
+{-# LANGUAGE $1 #-}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/lens.field b/users/grfn/emacs.d/snippets/haskell-mode/lens.field
new file mode 100644
index 0000000000..b22ea3d2e8
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/lens.field
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: lens.field
+# key: lens
+# expand-env: ((yas-indent-line 'fixed))
+# --
+${1:field} :: Lens' ${2:Source} ${3:Target}
+$1 = lens _${4:sourceField} $ \\${2:$(-> yas-text s-word-initials s-downcase)} ${4:$(-> yas-text s-word-initials s-downcase)} -> ${2:$(-> yas-text s-word-initials s-downcase)} { _$4 = ${4:$(-> yas-text s-word-initials s-downcase)} }
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/module b/users/grfn/emacs.d/snippets/haskell-mode/module
new file mode 100644
index 0000000000..4554d33f9b
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/module
@@ -0,0 +1,32 @@
+# -*- mode: snippet -*-
+# key: module
+# name: module
+# condition: (= (length "module") (current-column))
+# expand-env: ((yas-indent-line 'fixed))
+# contributor: Luke Hoersten <luke@hoersten.org>
+# --
+--------------------------------------------------------------------------------
+-- |
+-- Module      : $1
+-- Description : $2
+-- Maintainer  : Griffin Smith <grfn@urbint.com>
+-- Maturity    : ${3:Draft, Usable, Maintained, OR MatureAF}
+--
+-- $4
+--------------------------------------------------------------------------------
+module ${1:`(if (not buffer-file-name) "Module"
+                (let ((name (file-name-sans-extension (buffer-file-name)))
+                      (case-fold-search nil))
+                     (if (or (cl-search "src/" name)
+                             (cl-search "test/" name))
+                         (replace-regexp-in-string "/" "."
+                           (replace-regexp-in-string "^\/[^A-Z]*" ""
+                             (car (last (split-string name "src")))))
+                         (file-name-nondirectory name))))`}
+  (
+  ) where
+--------------------------------------------------------------------------------
+import Prelude
+--------------------------------------------------------------------------------
+
+$0
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/shut up, hlint b/users/grfn/emacs.d/snippets/haskell-mode/shut up, hlint
new file mode 100644
index 0000000000..fccff1d66f
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/shut up, hlint
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: shut up, hlint
+# key: dupl
+# expand-env: ((yas-indent-line 'fixed))
+# --
+{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/test-group b/users/grfn/emacs.d/snippets/haskell-mode/test-group
new file mode 100644
index 0000000000..948e90d9e0
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/test-group
@@ -0,0 +1,9 @@
+# -*- mode: snippet -*-
+# name: test-group
+# uuid:
+# key: testGroup
+# condition: t
+# --
+testGroup "${1:name}"
+[ $0
+]
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/test-module b/users/grfn/emacs.d/snippets/haskell-mode/test-module
new file mode 100644
index 0000000000..036b0ae998
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/test-module
@@ -0,0 +1,27 @@
+# -*- mode: snippet -*-
+# name: test-module
+# key: test
+# expand-env: ((yas-indent-line 'fixed))
+# --
+--------------------------------------------------------------------------------
+module ${1:`(if (not buffer-file-name) "Module"
+                (let ((name (file-name-sans-extension (buffer-file-name)))
+                      (case-fold-search nil))
+                     (if (cl-search "test/" name)
+                         (replace-regexp-in-string "/" "."
+                           (replace-regexp-in-string "^\/[^A-Z]*" ""
+                             (car (last (split-string name "src")))))
+                         (file-name-nondirectory name))))`} (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           ${1:$(s-chop-suffix "Spec" yas-text)}
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "$1"
+  [ $0
+  ]
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/haskell-mode/undefined b/users/grfn/emacs.d/snippets/haskell-mode/undefined
new file mode 100644
index 0000000000..7bcd99b571
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/undefined
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: undefined
+# key: u
+# expand-env: ((yas-indent-line 'fixed) (yas-wrap-around-region 'nil))
+# --
+undefined$1
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/action-type b/users/grfn/emacs.d/snippets/js2-mode/action-type
new file mode 100644
index 0000000000..ef8d1a3863
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/action-type
@@ -0,0 +1,4 @@
+# key: at
+# name: action-type
+# --
+export const ${1:FOO_BAR$(->> yas-text s-upcase (s-replace-all '(("-" . "_") (" " . "_"))))}: '${3:ns}/${1:$(-> yas-text s-dashed-words)}' = '$3/${1:$(-> yas-text s-dashed-words)}'$5
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/before b/users/grfn/emacs.d/snippets/js2-mode/before
new file mode 100644
index 0000000000..4569b65831
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/before
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: before
+# key: bef
+# --
+before(function() {
+                  $1
+})
diff --git a/users/grfn/emacs.d/snippets/js2-mode/context b/users/grfn/emacs.d/snippets/js2-mode/context
new file mode 100644
index 0000000000..d83809f3c3
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/context
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: context
+# key: context
+# --
+context('$1', function() {
+              $2
+})
diff --git a/users/grfn/emacs.d/snippets/js2-mode/describe b/users/grfn/emacs.d/snippets/js2-mode/describe
new file mode 100644
index 0000000000..bd0198181d
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/describe
@@ -0,0 +1,6 @@
+# key: desc
+# name: describe
+# --
+describe('$1', () => {
+  $2
+})
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/expect b/users/grfn/emacs.d/snippets/js2-mode/expect
new file mode 100644
index 0000000000..eba41ef330
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/expect
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: expect
+# key: ex
+# --
+expect($1).$2
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/function b/users/grfn/emacs.d/snippets/js2-mode/function
new file mode 100644
index 0000000000..b423044b44
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/function
@@ -0,0 +1,6 @@
+# key: f
+# name: function
+# --
+function $1($2) {
+         $3
+}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/header b/users/grfn/emacs.d/snippets/js2-mode/header
new file mode 100644
index 0000000000..3e303764cb
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/header
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: header
+# key: hh
+# expand-env: ((yas-indent-line 'fixed))
+# --
+////////////////////////////////////////////////////////////////////////////////
diff --git a/users/grfn/emacs.d/snippets/js2-mode/it b/users/grfn/emacs.d/snippets/js2-mode/it
new file mode 100644
index 0000000000..a451cfc08a
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/it
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: it
+# key: it
+# --
+it('$1', () => {
+  $2
+})
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/it-pending b/users/grfn/emacs.d/snippets/js2-mode/it-pending
new file mode 100644
index 0000000000..00da312e10
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/it-pending
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: it-pending
+# key: xi
+# --
+it('$1')$0
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/module b/users/grfn/emacs.d/snippets/js2-mode/module
new file mode 100644
index 0000000000..dc79819d89
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/module
@@ -0,0 +1,12 @@
+# key: module
+# name: module
+# expand-env: ((yas-indent-line (quote fixed)))
+# condition: (= (length "module") (current-column))
+# --
+/**
+ * @fileOverview $1
+ * @name ${2:`(file-name-nondirectory (buffer-file-name))`}
+ * @author Griffin Smith
+ * @license Proprietary
+ */
+$3
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/record b/users/grfn/emacs.d/snippets/js2-mode/record
new file mode 100644
index 0000000000..0bb0f02436
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/record
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: record
+# key: rec
+# --
+export default class $1 extends Record({
+  $2
+}) {}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/js2-mode/test b/users/grfn/emacs.d/snippets/js2-mode/test
new file mode 100644
index 0000000000..938d490a74
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/js2-mode/test
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: test
+# key: test
+# --
+test('$1', () => {
+  $2
+})
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/nix-mode/fetchFromGitHub b/users/grfn/emacs.d/snippets/nix-mode/fetchFromGitHub
new file mode 100644
index 0000000000..9b93735730
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/nix-mode/fetchFromGitHub
@@ -0,0 +1,12 @@
+# -*- mode: snippet -*-
+# name: fetchFromGitHub
+# uuid:
+# key: fetchFromGitHub
+# condition: t
+# --
+fetchFromGitHub {
+                owner = "$1";
+                repo = "$2";
+                rev = "$3";
+                sha256 = "0000000000000000000000000000000000000000000000000000";
+}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/nix-mode/pythonPackage b/users/grfn/emacs.d/snippets/nix-mode/pythonPackage
new file mode 100644
index 0000000000..0a74c21e18
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/nix-mode/pythonPackage
@@ -0,0 +1,16 @@
+# key: pypkg
+# name: pythonPackage
+# condition: t
+# --
+${1:pname} = buildPythonPackage rec {
+           name = "\${pname}-\${version}";
+           pname = "$1";
+           version = "${2:1.0.0}";
+           src = fetchPypi {
+               inherit pname version;
+               sha256 = "0000000000000000000000000000000000000000000000000000";
+           };
+           propagatedBuildInputs = with pythonSelf; [
+               $3
+           ];
+};
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/nix-mode/sha256 b/users/grfn/emacs.d/snippets/nix-mode/sha256
new file mode 100644
index 0000000000..e3d52e1c02
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/nix-mode/sha256
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: sha256
+# uuid:
+# key: sha256
+# condition: t
+# --
+sha256 = "0000000000000000000000000000000000000000000000000000";
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/org-mode/SQL source block b/users/grfn/emacs.d/snippets/org-mode/SQL source block
new file mode 100644
index 0000000000..b5d43fd6bc
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/SQL source block
@@ -0,0 +1,6 @@
+# key: sql
+# name: SQL source block
+# --
+#+BEGIN_SRC sql ${1::async}
+$2
+#+END_SRC
diff --git a/users/grfn/emacs.d/snippets/org-mode/combat b/users/grfn/emacs.d/snippets/org-mode/combat
new file mode 100644
index 0000000000..ef46062d09
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/combat
@@ -0,0 +1,13 @@
+# -*- mode: snippet -*-
+# name: combat
+# uuid:
+# key: combat
+# condition: t
+# --
+|             | initiative | max hp | current hp | status |      |
+|-------------+------------+--------+------------+--------+------|
+| Barty Barty |            |        |            |        | <--- |
+| Hectoroth   |            |        |            |        |      |
+| Xanadu      |            |        |            |        |      |
+| Aurora      |            |        |            |        |      |
+| EFB         |            |        |            |        |      |
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/org-mode/date b/users/grfn/emacs.d/snippets/org-mode/date
new file mode 100644
index 0000000000..297529cdac
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/date
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# key: date
+# name: date.org
+# --
+[`(format-time-string "%Y-%m-%d")`]$0
diff --git a/users/grfn/emacs.d/snippets/org-mode/date-time b/users/grfn/emacs.d/snippets/org-mode/date-time
new file mode 100644
index 0000000000..fde469276c
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/date-time
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: date-time
+# key: dt
+# --
+[`(format-time-string "%Y-%m-%d %H:%m:%S")`]
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/org-mode/description b/users/grfn/emacs.d/snippets/org-mode/description
new file mode 100644
index 0000000000..a43bc95cc3
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/description
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: description
+# key: desc
+# --
+:DESCRIPTION:
+$1
+:END:
diff --git a/users/grfn/emacs.d/snippets/org-mode/nologdone b/users/grfn/emacs.d/snippets/org-mode/nologdone
new file mode 100644
index 0000000000..e5be85d6b3
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/nologdone
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: nologdone
+# key: nologdone
+# --
+#+STARTUP: nologdone$0
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/org-mode/python source block b/users/grfn/emacs.d/snippets/org-mode/python source block
new file mode 100644
index 0000000000..247ae51b0b
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/python source block
@@ -0,0 +1,6 @@
+# key: py
+# name: Python source block
+# --
+#+BEGIN_SRC python
+$0
+#+END_SRC
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/org-mode/reveal b/users/grfn/emacs.d/snippets/org-mode/reveal
new file mode 100644
index 0000000000..1bdbdfa5dc
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/reveal
@@ -0,0 +1,6 @@
+# key: reveal
+# name: reveal
+# condition: t
+# --
+#+ATTR_REVEAL: :frag ${1:roll-in}
+$0
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/org-mode/transaction b/users/grfn/emacs.d/snippets/org-mode/transaction
new file mode 100644
index 0000000000..37f2dd31ca
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/transaction
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: transaction
+# key: begin
+# --
+BEGIN;
+$0
+ROLLBACK;
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/prolog-mode/use-module b/users/grfn/emacs.d/snippets/prolog-mode/use-module
new file mode 100644
index 0000000000..970391f936
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/prolog-mode/use-module
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: use-module
+# uuid:
+# key: use
+# condition: t
+# --
+:- use_module(${1:library($2)}${3:, [$4]}).
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/python-mode/add_column b/users/grfn/emacs.d/snippets/python-mode/add_column
new file mode 100644
index 0000000000..47e83850d5
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/add_column
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: add_column
+# key: op.add_column
+# --
+op.add_column('${1:table}', sa.Column('${2:name}', sa.${3:String()}))$0
diff --git a/users/grfn/emacs.d/snippets/python-mode/decorate b/users/grfn/emacs.d/snippets/python-mode/decorate
new file mode 100644
index 0000000000..9448b45c96
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/decorate
@@ -0,0 +1,15 @@
+# -*- mode: snippet -*-
+# name: decorate
+# uuid:
+# key: decorate
+# condition: t
+# --
+def wrap(inner):
+    @wraps(inner)
+    def wrapped(*args, **kwargs):
+        ret = inner(*args, **kwargs)
+        return ret
+
+    return wrapped
+
+return wrap
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/python-mode/dunder b/users/grfn/emacs.d/snippets/python-mode/dunder
new file mode 100644
index 0000000000..c49ec40a15
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/dunder
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: dunder
+# uuid:
+# key: du
+# condition: t
+# --
+__$1__$0
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/python-mode/name b/users/grfn/emacs.d/snippets/python-mode/name
new file mode 100644
index 0000000000..eca6d60b48
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/name
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: name
+# uuid:
+# key: name
+# condition: t
+# --
+__name__
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/python-mode/op.get_bind.execute b/users/grfn/emacs.d/snippets/python-mode/op.get_bind.execute
new file mode 100644
index 0000000000..aba801c6ba
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/op.get_bind.execute
@@ -0,0 +1,7 @@
+# key: exec
+# name: op.get_bind.execute
+# --
+op.get_bind().execute(
+    """
+    `(progn (sqlup-mode) "")`$1
+    """)
diff --git a/users/grfn/emacs.d/snippets/python-mode/pdb b/users/grfn/emacs.d/snippets/python-mode/pdb
new file mode 100644
index 0000000000..6b5c0bbc0a
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/pdb
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: pdb
+# uuid:
+# key: pdb
+# condition: t
+# --
+import pdb; pdb.set_trace()
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/rust-mode/#[macro_use] b/users/grfn/emacs.d/snippets/rust-mode/#[macro_use]
new file mode 100644
index 0000000000..fea942a337
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/#[macro_use]
@@ -0,0 +1,5 @@
+# key: macro_use
+# name: #[macro_use]
+# --
+#[macro_use]
+${1:extern crate} ${2:something};$0
diff --git a/users/grfn/emacs.d/snippets/rust-mode/async test b/users/grfn/emacs.d/snippets/rust-mode/async test
new file mode 100644
index 0000000000..2741075474
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/async test
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: async test
+# uuid:
+# key: atest
+# condition: t
+# --
+#[tokio::test${1:(flavor = "multi_thread")}]
+async fn ${2:test_name}() {
+   `%`$0
+}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/rust-mode/benchmark b/users/grfn/emacs.d/snippets/rust-mode/benchmark
new file mode 100644
index 0000000000..f1446923a0
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/benchmark
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: benchmark
+# uuid:
+# key: bench
+# condition: t
+# --
+#[bench]
+fn ${1:benchmark_name}(b: &mut Bencher) {
+   `%`b.iter(|| $0);
+}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/rust-mode/proptest b/users/grfn/emacs.d/snippets/rust-mode/proptest
new file mode 100644
index 0000000000..377b3cfcf6
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/proptest
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: proptest
+# uuid:
+# key: proptest
+# condition: t
+# --
+#[proptest]
+fn ${1:test_name}($2) {
+   `%`$0
+}
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/rust-mode/tests b/users/grfn/emacs.d/snippets/rust-mode/tests
new file mode 100644
index 0000000000..0a476ab586
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/tests
@@ -0,0 +1,9 @@
+# key: tests
+# name: test module
+# --
+#[cfg(test)]
+mod ${1:tests} {
+    use super::*;
+
+    $0
+}
diff --git a/users/grfn/emacs.d/snippets/snippet-mode/indent b/users/grfn/emacs.d/snippets/snippet-mode/indent
new file mode 100644
index 0000000000..d38ffceafb
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/snippet-mode/indent
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: indent
+# key: indent
+# --
+# expand-env: ((yas-indent-line 'fixed))
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/sql-mode/count(*) group by b/users/grfn/emacs.d/snippets/sql-mode/count(*) group by
new file mode 100644
index 0000000000..6acc46ff39
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/sql-mode/count(*) group by
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: count(*) group by
+# key: countby
+# --
+SELECT count(*), ${1:column} FROM ${2:table} GROUP BY $1;
diff --git a/users/grfn/emacs.d/snippets/terraform-mode/variable b/users/grfn/emacs.d/snippets/terraform-mode/variable
new file mode 100644
index 0000000000..e64175200f
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/terraform-mode/variable
@@ -0,0 +1,11 @@
+# -*- mode: snippet -*-
+# name: variable
+# uuid:
+# key: var
+# condition: t
+# --
+variable "${1:name}" {
+  type = ${2:string}
+  ${3:default = ${4:default}}
+}
+$0
\ No newline at end of file
diff --git a/users/grfn/emacs.d/snippets/text-mode/date b/users/grfn/emacs.d/snippets/text-mode/date
new file mode 100644
index 0000000000..7b94311470
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/text-mode/date
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# name: date
+# key: date
+# --
+`(format-time-string "%Y-%m-%d")`$0
\ No newline at end of file
diff --git a/users/grfn/emacs.d/splitjoin.el b/users/grfn/emacs.d/splitjoin.el
new file mode 100644
index 0000000000..dbc9704d79
--- /dev/null
+++ b/users/grfn/emacs.d/splitjoin.el
@@ -0,0 +1,192 @@
+;;; -*- lexical-binding: t; -*-
+
+(require 'dash)
+(load! "utils")
+
+;;;
+;;; Vars
+;;;
+
+(defvar +splitjoin/split-callbacks '()
+  "Alist mapping major mode symbol names to lists of split callbacks")
+
+(defvar +splitjoin/join-callbacks '()
+  "Alist mapping major mode symbol names to lists of join callbacks")
+
+
+
+;;;
+;;; Definition macros
+;;;
+
+(defmacro +splitjoin/defsplit (mode name &rest body)
+  `(setf
+    (alist-get ',name (alist-get ,mode +splitjoin/split-callbacks))
+    (λ! () ,@body)))
+
+(defmacro +splitjoin/defjoin (mode name &rest body)
+  `(setf
+    (alist-get ',name (alist-get ,mode +splitjoin/join-callbacks))
+    (λ! () ,@body)))
+
+;;;
+;;; Commands
+;;;
+
+(defun +splitjoin/split ()
+  (interactive)
+  (when-let (callbacks (->> +splitjoin/split-callbacks
+                            (alist-get major-mode)
+                            (-map #'cdr)))
+    (find-if #'funcall callbacks)))
+
+(defun +splitjoin/join ()
+  (interactive)
+  (when-let (callbacks (->> +splitjoin/join-callbacks
+                            (alist-get major-mode)
+                            (-map #'cdr)))
+    (find-if #'funcall callbacks)))
+
+
+;;;
+;;; Splits and joins
+;;; TODO: this should probably go in a file-per-language
+;;;
+
+(+splitjoin/defjoin
+ 'elixir-mode
+ join-do
+ (let* ((function-pattern (rx (and (zero-or-more whitespace)
+                                   "do"
+                                   (zero-or-more whitespace)
+                                   (optional (and "#" (zero-or-more anything)))
+                                   eol)))
+        (end-pattern (rx bol
+                         (zero-or-more whitespace)
+                         "end"
+                         (zero-or-more whitespace)
+                         eol))
+        (else-pattern (rx bol
+                         (zero-or-more whitespace)
+                         "else"
+                         (zero-or-more whitespace)
+                         eol))
+        (lineno     (line-number-at-pos))
+        (line       (thing-at-point 'line t)))
+   (when-let ((do-start-pos (string-match function-pattern line)))
+     (cond
+      ((string-match-p end-pattern (get-line (inc lineno)))
+       (modify-then-indent
+        (goto-line-char do-start-pos)
+        (insert ",")
+        (goto-char (line-end-position))
+        (insert ": nil")
+        (line-move 1)
+        (delete-line))
+       t)
+
+      ((string-match-p end-pattern (get-line (+ 2 lineno)))
+       (modify-then-indent
+        (goto-line-char do-start-pos)
+        (insert ",")
+        (goto-char (line-end-position))
+        (insert ":")
+        (join-line t)
+        (line-move 1)
+        (delete-line))
+       t)
+
+      ((and (string-match-p else-pattern (get-line (+ 2 lineno)))
+            (string-match-p end-pattern  (get-line (+ 4 lineno))))
+       (modify-then-indent
+        (goto-line-char do-start-pos)
+        (insert ",")
+        (goto-char (line-end-position))
+        (insert ":")
+        (join-line t)
+        (goto-eol)
+        (insert ",")
+        (join-line t)
+        (goto-eol)
+        (insert ":")
+        (join-line t)
+        (line-move 1)
+        (delete-line))
+       t)))))
+
+(comment
+ (string-match (rx (and bol
+                        "if "
+                        (one-or-more anything)
+                        ","
+                        (zero-or-more whitespace)
+                        "do:"
+                        (one-or-more anything)
+                        ","
+                        (zero-or-more whitespace)
+                        "else:"
+                        (one-or-more anything)))
+               "if 1, do: nil, else: nil")
+
+ )
+
+(+splitjoin/defsplit
+ 'elixir-mode
+ split-do-with-optional-else
+ (let* ((if-with-else-pattern (rx (and bol
+                                       (one-or-more anything)
+                                       ","
+                                       (zero-or-more whitespace)
+                                       "do:"
+                                       (one-or-more anything)
+                                       (optional
+                                        ","
+                                        (zero-or-more whitespace)
+                                        "else:"
+                                        (one-or-more anything)))))
+        (current-line (get-line)))
+   (when (string-match if-with-else-pattern current-line)
+     (modify-then-indent
+      (assert (goto-regex-on-line ",[[:space:]]*do:"))
+      (delete-char 1)
+      (assert (goto-regex-on-line ":"))
+      (delete-char 1)
+      (insert "\n")
+      (when (goto-regex-on-line-r ",[[:space:]]*else:")
+        (delete-char 1)
+        (insert "\n")
+        (assert (goto-regex-on-line ":"))
+        (delete-char 1)
+        (insert "\n"))
+      (goto-eol)
+      (insert "\nend"))
+     t)))
+
+(comment
+ (+splitjoin/defsplit 'elixir-mode split-def
+ (let ((function-pattern (rx (and ","
+                                  (zero-or-more whitespace)
+                                  "do:")))
+       (line (thing-at-point 'line t)))
+   (when-let (idx (string-match function-pattern line))
+     (let ((beg (line-beginning-position))
+           (orig-line-char (- (point) (line-beginning-position))))
+       (save-mark-and-excursion
+        (goto-line-char idx)
+        (delete-char 1)
+        (goto-line-char (string-match ":" (thing-at-point 'line t)))
+        (delete-char 1)
+        (insert "\n")
+        (goto-eol)
+        (insert "\n")
+        (insert "end")
+        (evil-indent beg (+ (line-end-position) 1))))
+     (goto-line-char orig-line-char)
+     t))))
+
+(+splitjoin/defjoin
+ 'elixir-mode
+ join-if-with-else
+ (let* ((current-line (thing-at-point 'line)))))
+
+(provide 'splitjoin)
diff --git a/users/grfn/emacs.d/sql-strings.el b/users/grfn/emacs.d/sql-strings.el
new file mode 100644
index 0000000000..eef397a24e
--- /dev/null
+++ b/users/grfn/emacs.d/sql-strings.el
@@ -0,0 +1,75 @@
+;;; -*- lexical-binding: t; -*-
+
+;;; https://www.emacswiki.org/emacs/StringAtPoint
+(defun ourcomments-string-or-comment-bounds-1 (what)
+  (save-restriction
+    (widen)
+    (let* ((here (point))
+           ;; Fix-me: when on end-point, how to handle that and which should be last hit point?
+           (state (parse-partial-sexp (point-min) (1+ here)))
+           (type (if (nth 3 state)
+                     'string
+                   (if (nth 4 state)
+                       'comment)))
+           (start (when type (nth 8 state)))
+           end)
+      (unless start
+        (setq state (parse-partial-sexp (point-min) here))
+        (setq type (if (nth 3 state)
+                       'string
+                     (if (nth 4 state)
+                         'comment)))
+        (setq start (when type (nth 8 state))))
+      (unless (or (not what)
+                  (eq what type))
+        (setq start nil))
+      (if (not start)
+          (progn
+            (goto-char here)
+            nil)
+        (setq state (parse-partial-sexp (1+ start) (point-max)
+                                        nil nil state 'syntax-table))
+        (setq end (point))
+        (goto-char here)
+        (cons start end)))))
+
+(defun ourcomments-bounds-of-string-at-point ()
+  "Return bounds of string at point if any."
+  (ourcomments-string-or-comment-bounds-1 'string))
+
+(put 'string 'bounds-of-thing-at-point 'ourcomments-bounds-of-string-at-point)
+
+(defun -sanitize-sql-string (str)
+  (->> str
+       (downcase)
+       (s-trim)
+       (replace-regexp-in-string
+        (rx (or (and string-start (or "\"\"\""
+                                      "\""))
+                (and (or "\"\"\""
+                         "\"")
+                     string-end)))
+        "")
+       (s-trim)))
+
+(defun sql-string-p (str)
+  "Returns 't if STR looks like a string literal for a SQL statement"
+  (setq str (-sanitize-sql-string str))
+  (or (s-starts-with? "select" str)))
+
+;;; tests
+
+(require 'ert)
+
+(ert-deftest sanitize-sql-string-test ()
+  (should (string-equal "select * from foo;"
+                        (-sanitize-sql-string
+                         "\"\"\"SELECT * FROM foo;\n\n\"\"\""))))
+
+(ert-deftest test-sql-string-p ()
+  (dolist (str '("SELECT * FROM foo;"
+                 "select * from foo;"))
+    (should (sql-string-p str)))
+
+  (dolist (str '("not a QUERY"))
+    (should-not (sql-string-p str))))
diff --git a/users/grfn/emacs.d/terraform.el b/users/grfn/emacs.d/terraform.el
new file mode 100644
index 0000000000..2d69c9bad9
--- /dev/null
+++ b/users/grfn/emacs.d/terraform.el
@@ -0,0 +1,31 @@
+;;; -*- lexical-binding: t; -*-
+
+(add-hook 'terraform-mode-hook #'terraform-format-on-save-mode)
+
+(defun packer-format-buffer ()
+  (interactive)
+  (let ((buf (get-buffer-create "*packer-fmt*")))
+    (if (zerop (call-process-region (point-min) (point-max)
+                "packer" nil buf nil "fmt" "-"))
+        (let ((point (point))
+              (window-start (window-start)))
+          (erase-buffer)
+          (insert-buffer-substring buf)
+          (goto-char point)
+          (set-window-start nil window-start))
+      (message "packer fmt failed: %s" (with-current-buffer buf (buffer-string))))
+    (kill-buffer buf)))
+
+(define-minor-mode packer-format-on-save-mode
+  "Run packer-format-buffer before saving the current buffer"
+  :lighter nil
+  (if packer-format-on-save-mode
+      (add-hook 'before-save-hook #'packer-format-buffer nil t)
+    (remove-hook 'before-save-hook #'packer-format-buffer t)))
+
+(defun maybe-init-packer ()
+  (interactive)
+  (when (s-ends-with-p ".pkr" (file-name-base (buffer-file-name)))
+    (packer-format-on-save-mode)))
+
+(add-hook 'hcl-mode-hook #'maybe-init-packer)
diff --git a/users/grfn/emacs.d/tests/splitjoin_test.el b/users/grfn/emacs.d/tests/splitjoin_test.el
new file mode 100644
index 0000000000..6495a1a595
--- /dev/null
+++ b/users/grfn/emacs.d/tests/splitjoin_test.el
@@ -0,0 +1,68 @@
+;;; private/grfn/tests/splitjoin_test.el -*- lexical-binding: t; -*-
+
+(require 'ert)
+;; (load! 'splitjoin)
+;; (load! 'utils)
+; (require 'splitjoin)
+
+;;; Helpers
+
+(defvar *test-buffer* nil)
+(make-variable-buffer-local '*test-buffer*)
+
+(defun test-buffer ()
+  (when (not *test-buffer*)
+    (setq *test-buffer* (get-buffer-create "test-buffer")))
+  *test-buffer*)
+
+(defmacro with-test-buffer (&rest body)
+  `(with-current-buffer (test-buffer)
+     ,@body))
+
+(defun set-test-buffer-mode (mode)
+  (let ((mode (if (functionp mode) mode
+                (-> mode symbol-name (concat "-mode") intern))))
+    (assert (functionp mode))
+    (with-test-buffer (funcall mode))))
+
+(defmacro set-test-buffer-contents (contents)
+  (with-test-buffer
+   (erase-buffer)
+   (insert contents)))
+
+(defun test-buffer-contents ()
+  (with-test-buffer (substring-no-properties (buffer-string))))
+
+(defmacro assert-test-buffer-contents (expected-contents)
+  `(should (equal (string-trim (test-buffer-contents))
+                  (string-trim ,expected-contents))))
+
+(defmacro should-join-to (mode original-contents expected-contents)
+  `(progn
+     (set-test-buffer-mode ,mode)
+     (set-test-buffer-contents ,original-contents)
+     (with-test-buffer (+splitjoin/join))
+     (assert-test-buffer-contents ,expected-contents)))
+
+(defmacro should-split-to (mode original-contents expected-contents)
+  `(progn
+     (set-test-buffer-mode ,mode)
+     (set-test-buffer-contents ,original-contents)
+     (with-test-buffer (+splitjoin/split))
+     (assert-test-buffer-contents ,expected-contents)))
+
+(defmacro should-splitjoin (mode joined-contents split-contents)
+  `(progn
+     (should-split-to ,mode ,joined-contents ,split-contents)
+     (should-join-to  ,mode ,split-contents  ,joined-contents)))
+
+;;; Tests
+
+;; Elixir
+(ert-deftest elixir-if-splitjoin-test ()
+  (should-splitjoin 'elixir
+   "if predicate?(), do: result"
+   "if predicate?() do
+  result
+end"))
+
diff --git a/users/grfn/emacs.d/themes/grfn-solarized-light-theme.el b/users/grfn/emacs.d/themes/grfn-solarized-light-theme.el
new file mode 100644
index 0000000000..ae00b6b5fc
--- /dev/null
+++ b/users/grfn/emacs.d/themes/grfn-solarized-light-theme.el
@@ -0,0 +1,115 @@
+(require 'solarized)
+(eval-when-compile
+  (require 'solarized-palettes))
+
+;; (defun grfn-solarized-theme ()
+;;   (custom-theme-set-faces
+;;    theme-name
+;;    `(font-lock-doc-face ((,class (:foreground ,s-base1))))
+;;    `(font-lock-preprocessor-face ((,class (:foreground ,red))))
+;;    `(font-lock-keyword-face ((,class (:foreground ,green))))
+
+;;    `(elixir-attribute-face ((,class (:foreground ,blue))))
+;;    `(elixir-atom-face ((,class (:foreground ,cyan))))))
+
+(setq +solarized-s-base03    "#002b36"
+      +solarized-s-base02    "#073642"
+      ;; emphasized content
+      +solarized-s-base01    "#586e75"
+      ;; primary content
+      +solarized-s-base00    "#657b83"
+      +solarized-s-base0     "#839496"
+      ;; comments
+      +solarized-s-base1     "#93a1a1"
+      ;; background highlight light
+      +solarized-s-base2     "#eee8d5"
+      ;; background light
+      +solarized-s-base3     "#fdf6e3"
+
+      ;; Solarized accented colors
+      +solarized-yellow    "#b58900"
+      +solarized-orange    "#cb4b16"
+      +solarized-red       "#dc322f"
+      +solarized-magenta   "#d33682"
+      +solarized-violet    "#6c71c4"
+      +solarized-blue      "#268bd2"
+      +solarized-cyan      "#2aa198"
+      +solarized-green     "#859900"
+
+      ;; Darker and lighter accented colors
+      ;; Only use these in exceptional circumstances!
+      +solarized-yellow-d  "#7B6000"
+      +solarized-yellow-l  "#DEB542"
+      +solarized-orange-d  "#8B2C02"
+      +solarized-orange-l  "#F2804F"
+      +solarized-red-d     "#990A1B"
+      +solarized-red-l     "#FF6E64"
+      +solarized-magenta-d "#93115C"
+      +solarized-magenta-l "#F771AC"
+      +solarized-violet-d  "#3F4D91"
+      +solarized-violet-l  "#9EA0E5"
+      +solarized-blue-d    "#00629D"
+      +solarized-blue-l    "#69B7F0"
+      +solarized-cyan-d    "#00736F"
+      +solarized-cyan-l    "#69CABF"
+      +solarized-green-d   "#546E00"
+      +solarized-green-l "#B4C342")
+
+
+(deftheme grfn-solarized-light "The light variant of Griffin's solarized theme")
+
+(setq grfn-solarized-faces
+      '("Griffin's solarized theme customization"
+        (custom-theme-set-faces
+         theme-name
+         `(font-lock-doc-face ((t (:foreground ,+solarized-s-base1))))
+         `(font-lock-preprocessor-face ((t (:foreground ,+solarized-red))))
+         `(font-lock-keyword-face ((t (:foreground ,+solarized-green))))
+
+         `(elixir-attribute-face ((t (:foreground ,+solarized-blue))))
+         `(elixir-atom-face ((t (:foreground ,+solarized-cyan))))
+         `(agda2-highlight-keyword-face ((t (:foreground ,green))))
+         `(agda2-highlight-string-face ((t (:foreground ,cyan))))
+         `(agda2-highlight-number-face ((t (:foreground ,violet))))
+         `(agda2-highlight-symbol-face ((((background ,base3)) (:foreground ,base01))))
+         `(agda2-highlight-primitive-type-face ((t (:foreground ,blue))))
+         `(agda2-highlight-bound-variable-face ((t nil)))
+         `(agda2-highlight-inductive-constructor-face ((t (:foreground ,green))))
+         `(agda2-highlight-coinductive-constructor-face ((t (:foreground ,yellow))))
+         `(agda2-highlight-datatype-face ((t (:foreground ,blue))))
+         `(agda2-highlight-field-face ((t (:foreground ,red))))
+         `(agda2-highlight-function-face ((t (:foreground ,blue))))
+         `(agda2-highlight-module-face ((t (:foreground ,yellow))))
+         `(agda2-highlight-postulate-face ((t (:foreground ,blue))))
+         `(agda2-highlight-primitive-face ((t (:foreground ,blue))))
+         `(agda2-highlight-record-face ((t (:foreground ,blue))))
+         `(agda2-highlight-dotted-face ((t nil)))
+         `(agda2-highlight-operator-face ((t nil)))
+         `(agda2-highlight-error-face ((t (:foreground ,red :underline t))))
+         `(agda2-highlight-unsolved-meta-face ((t (:background ,base2))))
+         `(agda2-highlight-unsolved-constraint-face ((t (:background ,base2))))
+         `(agda2-highlight-termination-problem-face ((t (:background ,orange :foreground ,base03))))
+         `(agda2-highlight-incomplete-pattern-face ((t (:background ,orange :foreground ,base03))))
+         `(agda2-highlight-typechecks-face ((t (:background ,cyan :foreground ,base03))))
+
+         `(font-lock-doc-face ((t (:foreground ,+solarized-s-base1))))
+         `(font-lock-preprocessor-face ((t (:foreground ,+solarized-red))))
+         `(font-lock-keyword-face ((t (:foreground ,+solarized-green :bold nil))))
+         `(font-lock-builtin-face ((t (:foreground ,+solarized-s-base01
+                                                  :bold t))))
+
+         `(elixir-attribute-face ((t (:foreground ,+solarized-blue))))
+         `(elixir-atom-face ((t (:foreground ,+solarized-cyan))))
+         `(linum ((t (:background ,+solarized-s-base2 :foreground ,+solarized-s-base1))))
+         `(line-number ((t (:background ,+solarized-s-base2 :foreground ,+solarized-s-base1))))
+
+         `(haskell-operator-face ((t (:foreground ,+solarized-green))))
+         `(haskell-keyword-face ((t (:foreground ,+solarized-cyan))))
+
+         `(org-drawer ((t (:foreground ,+solarized-s-base1
+                                      :bold t)))))))
+
+(solarized-with-color-variables
+  'light 'grfn-solarized-light solarized-light-color-palette-alist)
+
+(provide-theme 'grfn-solarized-light)
diff --git a/users/grfn/emacs.d/utils.el b/users/grfn/emacs.d/utils.el
new file mode 100644
index 0000000000..21192753a2
--- /dev/null
+++ b/users/grfn/emacs.d/utils.el
@@ -0,0 +1,114 @@
+;;; -*- lexical-binding: t; -*-
+
+
+;; Elisp Extras
+
+(defmacro comment (&rest _body)
+  "Comment out one or more s-expressions"
+  nil)
+
+(defun inc (x) "Returns x + 1" (+ 1 x))
+(defun dec (x) "Returns x - 1" (- x 1))
+
+(defun average (ns)
+  "Arithmetic mean of xs"
+  (if (null ns) nil
+    (/ (apply #'+ ns)
+       (length ns))))
+
+(comment
+ (average (list 1 2 3 4))
+ )
+
+;;
+;; Text editing utils
+;;
+
+;; Reading strings
+
+(defun get-char (&optional point)
+  "Get the character at the given `point' (defaulting to the current point),
+without properties"
+  (let ((point (or point (point))))
+    (buffer-substring-no-properties point (+ 1 point))))
+
+(defun get-line (&optional lineno)
+  "Read the line number `lineno', or the current line if `lineno' is nil, and
+return it as a string stripped of all text properties"
+  (let ((current-line (line-number-at-pos)))
+    (if (or (not lineno)
+            (= current-line lineno))
+        (thing-at-point 'line t)
+      (save-mark-and-excursion
+       (line-move (- lineno (line-number-at-pos)))
+       (thing-at-point 'line t)))))
+
+(defun get-line-point ()
+  "Get the position in the current line of the point"
+  (- (point) (line-beginning-position)))
+
+;; Moving in the file
+
+(defun goto-line-char (pt)
+  "Moves the point to the given position expressed as an offset from the start
+of the line"
+  (goto-char (+ (line-beginning-position) pt)))
+
+(defun goto-eol ()
+  "Moves to the end of the current line"
+  (goto-char (line-end-position)))
+
+(defun goto-regex-on-line (regex)
+  "Moves the point to the first occurrence of `regex' on the current line.
+Returns nil if the regex did not match, non-nil otherwise"
+  (when-let ((current-line (get-line))
+             (line-char (string-match regex current-line)))
+    (goto-line-char line-char)))
+
+(defun goto-regex-on-line-r (regex)
+  "Moves the point to the *last* occurrence of `regex' on the current line.
+Returns nil if the regex did not match, non-nil otherwise"
+  (when-let ((current-line (get-line))
+             (modified-regex (concat ".*\\(" regex "\\)"))
+             (_ (string-match modified-regex current-line))
+             (match-start (match-beginning 1)))
+    (goto-line-char match-start)))
+
+(comment
+ (progn
+   (string-match (rx (and (zero-or-more anything)
+                          (group "foo" "foo")))
+                 "foofoofoo")
+   (match-beginning 1)))
+
+;; Changing file contents
+
+(defun delete-line ()
+  "Remove the line at the current point"
+  (delete-region (line-beginning-position)
+                 (inc (line-end-position))))
+
+(defmacro modify-then-indent (&rest body)
+  "Modify text in the buffer according to body, then re-indent from where the
+  cursor started to where the cursor ended up, then return the cursor to where
+  it started."
+  `(let ((beg (line-beginning-position))
+         (orig-line-char (- (point) (line-beginning-position))))
+     (atomic-change-group
+       (save-mark-and-excursion
+        ,@body
+        (evil-indent beg (+ (line-end-position) 1))))
+     (goto-line-char orig-line-char)))
+
+(pcase-defmacro s-starts-with (prefix)
+  `(pred (s-starts-with-p ,prefix)))
+
+(pcase-defmacro s-contains (needle &optional ignore-case)
+  `(pred (s-contains-p ,needle
+                       ,@(when ignore-case (list ignore-case)))))
+
+(comment
+ (pcase "foo"
+   ((s-contains "bar") 1)
+   ((s-contains "o") 2))
+ )
diff --git a/users/grfn/emacs.d/vterm.el b/users/grfn/emacs.d/vterm.el
new file mode 100644
index 0000000000..a7fdea46da
--- /dev/null
+++ b/users/grfn/emacs.d/vterm.el
@@ -0,0 +1,24 @@
+;;; -*- lexical-binding: t; -*-
+
+(defun require-vterm ()
+  (add-to-list
+   'load-path
+   (concat
+    (s-trim
+     (shell-command-to-string
+      "nix-build --no-out-link ~/code/depot -A third_party.emacs.vterm"))
+    "/share/emacs/site-lisp/elpa/vterm-20200515.1412"))
+  (require 'vterm))
+
+(defun +grfn/vterm-setup ()
+  (hide-mode-line-mode)
+  (setq-local evil-collection-vterm-send-escape-to-vterm-p t))
+
+(add-hook 'vterm-mode-hook #'+grfn/vterm-setup)
+
+(map! (:map vterm-mode-map
+       "<C-escape>" #'evil-normal-state))
+
+(comment
+ (require-vterm)
+ )
diff --git a/users/grfn/gws.fyi/.envrc b/users/grfn/gws.fyi/.envrc
new file mode 100644
index 0000000000..be81feddb1
--- /dev/null
+++ b/users/grfn/gws.fyi/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
\ No newline at end of file
diff --git a/users/grfn/gws.fyi/.gitignore b/users/grfn/gws.fyi/.gitignore
new file mode 100644
index 0000000000..2b72eaed29
--- /dev/null
+++ b/users/grfn/gws.fyi/.gitignore
@@ -0,0 +1,3 @@
+result
+letsencrypt
+index.html
diff --git a/users/grfn/gws.fyi/Makefile b/users/grfn/gws.fyi/Makefile
new file mode 100644
index 0000000000..d6c9f40c95
--- /dev/null
+++ b/users/grfn/gws.fyi/Makefile
@@ -0,0 +1,31 @@
+.PHONY: deploy
+
+deploy:
+	@$(shell nix-build `git rev-parse --show-toplevel` -A 'users.grfn."gws.fyi"')
+
+renew:
+	@echo Renewing...
+	@certbot certonly \
+		--manual \
+		--domain www.gws.fyi \
+		--preferred-challenges dns \
+		--server https://acme-v02.api.letsencrypt.org/directory \
+		--agree-tos \
+		--work-dir $(shell pwd)/letsencrypt/work \
+		--logs-dir $(shell pwd)/letsencrypt/logs \
+		--config-dir $(shell pwd)/letsencrypt/config
+	@echo "Reimporting certificate"
+	@aws acm import-certificate \
+	    --profile personal \
+	    --certificate file://letsencrypt/config/live/www.gws.fyi/cert.pem \
+	    --certificate-chain file://letsencrypt/config/live/www.gws.fyi/fullchain.pem \
+	    --private-key file://letsencrypt/config/live/www.gws.fyi/privkey.pem \
+	    --certificate-arn arn:aws:acm:us-east-1:797089351721:certificate/628e54f3-55f9-49c0-811a-eba516b68e30 \
+		--region us-east-1
+
+backup:
+	@tarsnap -cf $(shell uname -n)-letsencrypt-$(shell date +%Y-%m-%d_%H-%M-%S) \
+		letsencrypt/
+
+open:
+	$$BROWSER "https://www.gws.fyi"
diff --git a/users/grfn/gws.fyi/config.el b/users/grfn/gws.fyi/config.el
new file mode 100644
index 0000000000..b05d897d3d
--- /dev/null
+++ b/users/grfn/gws.fyi/config.el
@@ -0,0 +1,6 @@
+(require 'org)
+
+(setq org-html-postamble nil)
+
+(defadvice org-export-grab-title-from-buffer
+    (around org-export-grab-title-from-buffer-disable activate))
diff --git a/users/grfn/gws.fyi/default.nix b/users/grfn/gws.fyi/default.nix
new file mode 100644
index 0000000000..5ab3614d79
--- /dev/null
+++ b/users/grfn/gws.fyi/default.nix
@@ -0,0 +1,37 @@
+args@{ pkgs, depot, ... }:
+with pkgs;
+let
+  site = import ./site.nix args;
+  resume = import ../resume args;
+  bucket = "s3://gws.fyi";
+  distributionID = "E2ST43JNBH8C64";
+
+  css = runCommand "main.css"
+    {
+      buildInputs = [ pkgs.minify ];
+    } ''
+    minify --type css < ${./main.css} > $out
+  '';
+
+  keys = runCommand "ssh-keys" { } ''
+    touch $out
+    echo "${depot.users.grfn.keys.main}" >> $out
+  '';
+
+  website =
+    runCommand "gws.fyi" { } ''
+      mkdir -p $out
+      cp ${css} $out/main.css
+      cp ${site.index} $out/index.html
+      cp -r ${site.recipes} $out/recipes
+      cp ${resume} $out/resume.pdf
+      cp ${keys} $out/keys
+    '';
+
+in
+(writeShellScript "deploy.sh" ''
+  ${awscli2}/bin/aws --profile personal s3 sync ${website}/ ${bucket}
+  echo "Deployed to http://gws.fyi"
+'') // {
+  inherit website site;
+}
diff --git a/users/grfn/gws.fyi/index.org b/users/grfn/gws.fyi/index.org
new file mode 100644
index 0000000000..4ba8461ee5
--- /dev/null
+++ b/users/grfn/gws.fyi/index.org
@@ -0,0 +1,40 @@
+#+OPTIONS: title:nil toc:nil num:nil
+#+HTML_HEAD: <title>griffin smith</title>
+#+HTML_HEAD: <link rel="stylesheet" href="./main.css">
+
+my name is griffin ward smith (aka grfn, glittershark, gws) and i'm a software
+engineer and musician
+
+* code
+
+- [[https://github.com/glittershark/][github]]
+- [[https://cs.tvl.fyi/depot/-/tree/users/grfn][my directory in the tvl monorepo]]
+
+* work
+
+i work on database internals at [[https://readyset.io/][readyset]], an incrementally maintained, partially
+stateful materialized view maintenance system for sql that's wire-compatible
+with postgresql and mysql, based on [[https://github.com/mit-pdos/noria][noria]]. readyset is hiring - if you're
+interested, send me an email
+
+* projects
+
+- [[https://windtunnel.ci/][windtunnel]], a continuous benchmarking software-as-a-service currently accepting early alpha users (send me an email if you want to try it out!)
+- [[https://cs.tvl.fyi/depot/-/tree/users/grfn/achilles][achilles]], a compiler for (what I plan to become) a dependently typed, low-level functional programming language targeting LLVM
+- [[https://github.com/glittershark/org-clubhouse][org-clubhouse]], an emacs package for lightweight integration between [[https://orgmode.org/][org-mode]] and [[https://clubhouse.io/][the clubhouse project management tool]]
+- [[https://cs.tvl.fyi/depot/-/tree/users/grfn/xanthous][xanthous]], a terminal roguelike in haskell that I work on intermittently and exclusively for fun \\
+  you can now try xanthous out over ssh by running ~ssh -p 2222 xanthous.gws.fyi~. if you do so I'd love if you send me an email about it
+
+* music
+
+- https://sacrosanct.bandcamp.com/, a post-rock project with a [[https://bandcamp.com/h34rken][friend of mine]]
+- [[https://soundcloud.com/missingggg][my current soundcloud]], releasing instrumental hip-hop under the name *missing*
+- you can also find a log of all the music I listen to [[https://www.last.fm/user/wildgriffin45][on last.fm]]
+
+* contact
+
+- [[mailto:web@gws.fyi][web@gws.fyi]]
+- [[https://twitter.com/glittershark1][twitter]]
+- https://keybase.io/glittershark
+- grfn on IRC (hackint or libera.chat)
+- [[http://keys.gnupg.net/pks/lookup?op=get&search=0x44EF5B5E861C09A7][gpg key: 0F11A989879E8BBBFDC1E23644EF5B5E861C09A7]]
diff --git a/users/grfn/gws.fyi/main.css b/users/grfn/gws.fyi/main.css
new file mode 100644
index 0000000000..cdcd440766
--- /dev/null
+++ b/users/grfn/gws.fyi/main.css
@@ -0,0 +1,139 @@
+@import url(https://fonts.googleapis.com/css?family=Inconsolata|Inter&display=swap);
+
+body {
+  margin-top: 40px;
+  max-width: 900px;
+  line-height: 1.6;
+  font-size: 16px;
+  background: #f8f3ff;
+  color: #3a1616;
+  padding: 0 10px;
+  font-family: Inter, sans-serif;
+}
+
+@media (min-width: 1050px) {
+  body {
+    margin-left: 150px;
+  }
+}
+
+@media (min-width: 2000px) {
+  body {
+    margin-left: 300px;
+  }
+}
+
+input {
+  padding: 10px 16px;
+  margin: 2px 0;
+  box-sizing: border-box;
+  border: 2px solid #dabebe;
+  border-radius: 6px;
+  background: #f8f3ff;
+  color: #3a1616;
+  font-size: 16px;
+  -webkit-transition: 0.5s;
+  transition: 0.5s;
+  outline: 0;
+}
+
+input:focus {
+  border: 2px solid #3a1616;
+}
+
+.button {
+  background-color: #f8f3ff;
+  border: none;
+  color: #000;
+  padding: 6px 14px;
+  text-align: center;
+  text-decoration: none;
+  display: inline-block;
+  font-size: 16px;
+  margin: 4px 2px;
+  transition-duration: 0.4s;
+  cursor: pointer;
+  border: 2px solid #3a1616;
+  border-radius: 6px;
+}
+
+.button:hover {
+  background-color: #3a1616;
+  color: #fff;
+}
+
+.isa_error,
+.isa_info,
+.isa_success,
+.isa_warning {
+  width: 90%;
+  margin: 10px 0;
+  padding: 12px;
+}
+
+.isa_info {
+  color: #00529b;
+  background-color: #bde5f8;
+}
+
+.isa_success {
+  color: #4f8a10;
+  background-color: #dff2bf;
+}
+
+.isa_warning {
+  color: #9f6000;
+  background-color: #feefb3;
+}
+
+.isa_error {
+  color: #d8000c;
+  background-color: #ffd2d2;
+}
+
+h1,
+h2,
+h3 {
+  line-height: 1.2;
+  font-family: Inter, sans-serif;
+}
+
+h1.title,
+h2.title,
+h3.title {
+  text-align: left;
+  margin-bottom: 1.5em;
+}
+
+h2 {
+  font-size: 18px;
+}
+
+img {
+  max-width: 750px;
+  border-radius: 10px;
+}
+
+a {
+  cursor: pointer;
+  color: #217ab7;
+  line-height: inherit;
+}
+
+a:hover {
+  background-color: #e3d6ff;
+}
+
+a:visited {
+  color: #43458b;
+  border-color: #43458b;
+}
+
+pre {
+  font-family: Inconsolata, monospace;
+}
+
+::selection {
+  color: #fff;
+  background: #ff4081;
+}
diff --git a/users/grfn/gws.fyi/orgExportHTML.nix b/users/grfn/gws.fyi/orgExportHTML.nix
new file mode 100644
index 0000000000..aac4e32e7a
--- /dev/null
+++ b/users/grfn/gws.fyi/orgExportHTML.nix
@@ -0,0 +1,67 @@
+{ pkgs, depot, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  emacs = pkgs.emacs28;
+
+in
+
+opts:
+
+let
+  src = if isAttrs opts then opts.src else opts;
+  headline = if isAttrs opts then opts.headline else null;
+
+  bn = builtins.baseNameOf src;
+  filename = elemAt (splitString "." bn) 0;
+
+  outName =
+    if isNull headline
+    then
+      let
+        bn = builtins.baseNameOf src;
+        filename = elemAt (splitString "." bn) 0;
+      in
+      if depot.nix.utils.isDirectory src
+      then filename
+      else filename + ".html"
+    else "${filename}-${replaceStrings [" "] ["-"] filename}.html";
+
+  escapeDoubleQuotes = replaceStrings [ "\"" ] [ "\\\"" ];
+
+  navToHeadline = optionalString (! isNull headline) ''
+    (search-forward "${escapeDoubleQuotes headline}")
+    (org-narrow-to-subtree)
+  '';
+
+in
+
+runCommand outName { inherit src; } ''
+  buildFile() {
+    cp "$1" file.org
+    ${emacs}/bin/emacs --batch \
+      --load ${./config.el} \
+      --visit file.org \
+      --eval "(progn
+        ${escapeDoubleQuotes navToHeadline}
+        (org-html-export-to-html))" \
+      --kill
+    rm file.org
+    substitute file.html "$2" \
+      --replace '<title>&lrm;</title>' ""
+    rm file.html
+  }
+
+  if [ -d $src ]; then
+    for file in $src/*; do
+      result=''${file/$src/$out}
+      mkdir -p $(dirname $result)
+      buildFile $file ''${result/.org/.html}
+    done
+  else
+    buildFile $src $out
+  fi
+''
diff --git a/users/grfn/gws.fyi/recipes/tomato-sauce.org b/users/grfn/gws.fyi/recipes/tomato-sauce.org
new file mode 100644
index 0000000000..74e9b103c6
--- /dev/null
+++ b/users/grfn/gws.fyi/recipes/tomato-sauce.org
@@ -0,0 +1,102 @@
+#+TITLE: Tomato Sauce
+#+OPTIONS: toc:nil num:nil
+#+HTML_HEAD: <link rel="stylesheet" href="../main.css">
+
+This is a general, all-purpose framework for turning some form of tomatoes into
+some form of sauce. You can use fresh tomatoes or canned (the latter are really
+quite surprisingly good sometimes), and include or omit garlic, basil, or other
+add-ins. The only real non-negotiable ingredients are tomatoes (duh), onion, and
+some kind of fat (I prefer butter).
+
+* Sauce
+
+1. *Prep*. If starting with canned tomatoes, skip this step. if starting with
+   whole tomatoes (which you should really only ever do if you grew them
+   yourself or got them fresh at a farmers market, grocery store tomatoes are
+   kinda sad), first, peel the tomatoes. The easiest way to do this is to score
+   them with an X pattern cut as shallow as possible while still breaking the
+   skin, trying to cover the whole surface area of the tomato, blanch them
+   briefly in boiling water, then dunk into an ice bath. After this, the skins
+   will slip right off.  After peeling, cut out the stem, core, and any green or
+   brown bits, and go to the next step
+
+2. *Base layer*. Couple of variables here, though a perfectly good (in fact, my
+   usual go-to) tomato sauce can also skip this entire step:
+   - If you want meat with your sauce (pancetta/guanciale/bacon for an
+     amatriciana, ground beef or pork for a bolognese) you'll start out by
+     sautéing that in some sort of fat (probably olive oil), less fat for meat
+     with a lot of fat already in it, to brown and render out fat from the meat
+   - If you want onion in the final sauce, you'll chop them finely and sauté
+     them with whatever fat you've got (either from the meat, or olive oil or
+     butter if you're not making a meat sauce). Remember to always add a *bit*
+     of salt when sautéing onion like this, not for flavor but to draw out the
+     moisture. If you just want onion flavor but not bits of onion in the final
+     sauce, it's added whole later (so ignore this bullet point).
+   - If you feel like it (sometimes I do, usually I don't) you can also mince
+     garlic here and sauté that in with everything else. Add a little after the
+     onion, as garlic cooks slower than onion unless you want something roastier
+     (usually you don't for tomato sauce)
+   - The traditional (so I'm told) thing to do with amatriciana, but also nice
+     with all variations, is to add in a little crushed red pepper with the
+     fat to flavor it slightly, but do this late so it doesn't burn
+   - If you have tomato paste on hand and feel like using it, it's also nice to
+     fry that in the oil for a little bit - usually I'd do that around the same
+     time as the garlic
+
+   If you're making tomato *paste* from your sauce, skip all of this - paste is
+   an ingredient, not a sauce on its own, so imo should be as neutral as
+   possible (i.e. just tomato).
+
+3. *Tomato layer*. Not a whole lot to do here, just add all of your tomatoes -
+   either your peeled and de-cored tomatoes from step 1 if you're using whole
+   tomatoes, or an entire can of whole, peeled san marzano tomatoes, including
+   the juice in the can - to a pot over medium-high heat. If you need more fat
+   or if you skipped step 2, this is where you'd add it - a classic and my
+   personal favorite is like 2/3rds to 3/4ths of a stick of butter, but you can
+   also go with olive oil. If you skipped the onion in step 2, add that here
+   too - usually that'd just be a fist-sized amount of onion or so peeled but
+   left with the stem on so you can fish it out from your final sauce later (and
+   snack on it!). Also salt here, again not to taste but primarily to draw out
+   moisture from the various ingredients.
+
+4. You can cook that for a wide variety of times, especially depending on how
+   hot you make your stove - there ends up being *lot* of liquid in there, so
+   you can go (in my experience) a reasonable amount hotter than you expect
+   without burning the sauce, though obviously your mileage may vary. The main
+   thing you're looking for is the whole chunks of tomato to break down, and the
+   whole sauce to get a texture that looks like it'll end up sticking to pasta
+   nicely. In all versions of this, stir pretty regularly with a wooden spoon,
+   and use the spoon to crush the big chunks of tomato occasionally.
+
+5. *Final layer*. Usually I don't do anything here - but if you feel
+   like it, usually right as you take stuff off the heat is where you'd add
+   basil, if you're using it. You can also add sugar to balance out too much
+   acidity from an especially acidic tomato here - I'm not going to tell anyone.
+   Also salt, but make sure to account for the extra salt you're gonna get from
+   the pasta water (see step 6)
+
+6. *Pasta*. You know how to cook pasta, I'm not going to tell you that. But,
+   like, salt your water until it tastes too salty, and remember to move the
+   pasta itself *directly* into the sauce pot from the pasta pot before it's
+   completely done cooking and without straining, bringing along some of the
+   pasta water (and a little extra for good measure) then finishing the pasta in
+   the sauce. You know, the thing you do for pasta. Remember the pasta water
+   will have salt in it, so adjust for that when salting the sauce overall (I
+   have made this mistake and ended up with too-salty pasta sauce).
+
+* Paste
+
+Start with the above recipe for tomato sauce, noting especially that (in my
+opinion) you should skip step 2 entirely. Keep cooking the sauce until it's
+*too* thick for pasta sauce (but don't burn it!), then spread it out across some
+sort of lined sheet pan (like a silpat, if you've got one) and bake in the oven
+at like 250-300 degrees for a *hell* of a long time - I've seen this take like
+10 hours, for an especially juicy batch of tomatoes, but obviously keep a close
+eye on it because it *definitely will burn* eventually. You're looking for the
+end result to be the texture of tomato paste, because that's what the recipe is
+for.  Especially if you're using garden-grown or otherwise fresh tomatoes,
+you'll notice quite a few seeds in the final product - don't worry too much
+about those, they've never bothered me. Once everything's done and cooled down,
+store in a jar in a fridge, topped with olive oil to seal things off and prevent
+oxidation. Use in all your future endeavors, including the tomato sauce recipe
+above itself. Tomato sauce is a beautiful oroborous.
diff --git a/users/grfn/gws.fyi/shell.nix b/users/grfn/gws.fyi/shell.nix
new file mode 100644
index 0000000000..846bdb6677
--- /dev/null
+++ b/users/grfn/gws.fyi/shell.nix
@@ -0,0 +1,9 @@
+with import <nixpkgs> { config.allowUnfree = true; };
+mkShell {
+  buildInputs = [
+    awscli
+    gnumake
+    letsencrypt
+    tarsnap
+  ];
+}
diff --git a/users/grfn/gws.fyi/site.nix b/users/grfn/gws.fyi/site.nix
new file mode 100644
index 0000000000..057c4d3ee6
--- /dev/null
+++ b/users/grfn/gws.fyi/site.nix
@@ -0,0 +1,12 @@
+args@{ pkgs ? import <nixpkgs> { }, ... }:
+
+let
+
+  orgExportHTML = import ./orgExportHTML.nix args;
+
+in
+
+{
+  index = orgExportHTML ./index.org;
+  recipes = orgExportHTML ./recipes;
+}
diff --git a/users/grfn/keyboard/.gitignore b/users/grfn/keyboard/.gitignore
new file mode 100644
index 0000000000..b2be92b7db
--- /dev/null
+++ b/users/grfn/keyboard/.gitignore
@@ -0,0 +1 @@
+result
diff --git a/users/grfn/keyboard/README.org b/users/grfn/keyboard/README.org
new file mode 100644
index 0000000000..b085883a10
--- /dev/null
+++ b/users/grfn/keyboard/README.org
@@ -0,0 +1,10 @@
+This repository contains the source of the keyboard layout for my Ergodox EZ,
+plus build tooling based on Nix.
+
+To flash to an Ergodox EZ that's connected to your computer via USB, run:
+
+#+BEGIN_SRC shell
+./flash
+#+END_SRC
+
+then press the reset switch on the keyboard.
diff --git a/users/grfn/keyboard/default.nix b/users/grfn/keyboard/default.nix
new file mode 100644
index 0000000000..39b21b8766
--- /dev/null
+++ b/users/grfn/keyboard/default.nix
@@ -0,0 +1,63 @@
+{ pkgs, ... }:
+
+with pkgs;
+
+let avrlibc = pkgsCross.avr.libcCross; in
+
+rec {
+  qmkSource = fetchgit {
+    url = "https://github.com/qmk/qmk_firmware";
+    rev = "ab1650606c36f85018257aba65d9c3ff8ec42e71";
+    sha256 = "1k59flkvhjzmfl0yz9z37lqhvad7m9r5wy1p1sjk5274rsmylh79";
+    fetchSubmodules = true;
+  };
+
+  layout = stdenv.mkDerivation rec {
+    name = "ergodox_ez_grfn.hex";
+
+    src = qmkSource;
+
+    buildInputs = [
+      dfu-programmer
+      dfu-util
+      diffutils
+      git
+      python3
+      pkgsCross.avr.buildPackages.binutils
+      pkgsCross.avr.buildPackages.gcc
+      avrlibc
+      avrdude
+    ];
+
+    AVR_CFLAGS = [
+      "-isystem ${avrlibc}/avr/include"
+      "-L${avrlibc}/avr/lib/avr5"
+    ];
+
+    AVR_ASFLAGS = AVR_CFLAGS;
+
+    patches = [ ./increase-tapping-delay.patch ];
+
+    postPatch = ''
+      mkdir keyboards/ergodox_ez/keymaps/grfn
+      cp ${./keymap.c} keyboards/ergodox_ez/keymaps/grfn/keymap.c
+    '';
+
+    buildPhase = ''
+      make ergodox_ez:grfn
+    '';
+
+    installPhase = ''
+      cp ergodox_ez_grfn.hex $out
+    '';
+  };
+
+  flash = writeShellScript "flash.sh" ''
+    ${teensy-loader-cli}/bin/teensy-loader-cli \
+      -v \
+      --mcu=atmega32u4 \
+      -w ${layout}
+  '';
+
+  meta.ci.targets = [ "layout" ];
+}
diff --git a/users/grfn/keyboard/flash b/users/grfn/keyboard/flash
new file mode 100755
index 0000000000..76def36f9c
--- /dev/null
+++ b/users/grfn/keyboard/flash
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec "$(nix-build --no-out-link ../../.. -A users.grfn.keyboard.flash)"
diff --git a/users/grfn/keyboard/increase-tapping-delay.patch b/users/grfn/keyboard/increase-tapping-delay.patch
new file mode 100644
index 0000000000..316c435fed
--- /dev/null
+++ b/users/grfn/keyboard/increase-tapping-delay.patch
@@ -0,0 +1,13 @@
+diff --git a/keyboards/ergodox_ez/config.h b/keyboards/ergodox_ez/config.h
+index ae70c4f2e..776110c09 100644
+--- a/keyboards/ergodox_ez/config.h
++++ b/keyboards/ergodox_ez/config.h
+@@ -45,7 +45,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ /* define if matrix has ghost */
+ //#define MATRIX_HAS_GHOST
+ 
+-#define TAPPING_TERM    200
++#define TAPPING_TERM    150
+ #define IGNORE_MOD_TAP_INTERRUPT // this makes it possible to do rolling combos (zx) with keys that convert to other keys on hold (z becomes ctrl when you hold it, and when this option isn't enabled, z rapidly followed by x actually sends Ctrl-x. That's bad.)
+ 
+ /* Mechanical locking support. Use KC_LCAP, KC_LNUM or KC_LSCR instead in keymap */
diff --git a/users/grfn/keyboard/keymap.c b/users/grfn/keyboard/keymap.c
new file mode 100644
index 0000000000..079f08a9a3
--- /dev/null
+++ b/users/grfn/keyboard/keymap.c
@@ -0,0 +1,206 @@
+#include QMK_KEYBOARD_H
+#include "debug.h"
+#include "action_layer.h"
+#include "version.h"
+
+
+#include "keymap_german.h"
+
+#include "keymap_nordic.h"
+
+
+
+enum custom_keycodes {
+  PLACEHOLDER = SAFE_RANGE, // can always be here
+  EPRM,
+  VRSN,
+  RGB_SLD,
+
+  EX_PIPE, // |>
+  THIN_ARROW, // ->
+  FAT_ARROW, // =>
+};
+
+
+
+#define LAMBDA UC(0x03BB)
+
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
+
+  [0] = LAYOUT_ergodox(
+      KC_EQUAL,       KC_1,           KC_2,   KC_3,   KC_4,   KC_5,   KC_LEFT,
+      KC_TAB,         KC_Q,           KC_W,   KC_E,   KC_R,   KC_T,   KC_LALT,
+      KC_ESCAPE,      KC_A,           KC_S,   KC_D,   KC_F,   KC_G,
+      KC_RSFT,        CTL_T(KC_Z),    KC_X,   KC_C,   KC_V,   KC_B,   KC_TAB,
+      LT(1,KC_GRAVE), KC_QUOTE,       LALT(KC_LSHIFT),KC_LEFT,KC_RIGHT,
+                                        ALT_T(KC_APPLICATION),      KC_SPACE,
+                                                                    KC_LBRACKET,
+                                        KC_LGUI, LSFT_T(KC_BSPACE),    KC_COLN,
+
+      KC_MY_COMPUTER, KC_6,   KC_7,   KC_8,       KC_9,       KC_0,               KC_MINUS,
+      KC_RALT,      KC_Y,   KC_U,   KC_I,       KC_O,       KC_P,               KC_BSLASH,
+                    KC_H,   KC_J,   KC_K,       KC_L,       LT(2,KC_SCOLON),    LT(1,KC_QUOTE),
+      KC_MINUS,     KC_N,   KC_M,   KC_COMMA,   KC_DOT,     CTL_T(KC_SLASH),    KC_RSFT,
+                    KC_DOWN,KC_UP,  KC_LBRACKET,KC_RBRACKET,MO(1),
+
+      KC_PAUSE,  TG(3),
+      KC_RBRACKET,
+      KC_COLN,  RSFT_T(KC_ENTER),   KC_SPACE
+   ),
+
+  [1] = LAYOUT_ergodox(
+      KC_ESCAPE,        KC_F1,          KC_F2,          KC_F3,          KC_F4,      KC_F5,          KC_TRANSPARENT,
+      KC_TRANSPARENT,   KC_EXLM,        KC_AT,          KC_LCBR,        KC_RCBR,    KC_PIPE,        KC_RABK,
+      KC_TRANSPARENT,   KC_HASH,        KC_DLR,         KC_LPRN,        KC_RPRN,    KC_UNDERSCORE,
+      KC_LABK,          KC_PERC,          KC_CIRC,        KC_LBRACKET,    KC_RBRACKET,    KC_TILD,    KC_TRANSPARENT,
+      KC_TRANSPARENT,   KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+                                                        RGB_MOD,  KC_TRANSPARENT,
+                                                                  KC_TRANSPARENT,
+                                                        RGB_VAD,    RGB_VAI, EX_PIPE,
+
+      KC_TRANSPARENT,   KC_F6,          KC_F7,          KC_F8,          KC_F9,      KC_F10,         KC_F11,
+      KC_PGUP,          KC_UP,          KC_7,           KC_8,           KC_9,       KC_ASTR,        KC_F12,
+                        KC_DOWN,        KC_4,           KC_5,           KC_6,       KC_PLUS,        KC_TRANSPARENT,
+      KC_PGDOWN,        KC_AMPR,        KC_1,           KC_2,           KC_3,       KC_BSLASH,      KC_TRANSPARENT,
+                                        KC_TRANSPARENT, KC_DOT,         KC_0,       KC_EQUAL,       KC_TRANSPARENT,
+      RGB_TOG,          RGB_SLD,
+      THIN_ARROW,
+      EX_PIPE,          RGB_HUD,    RGB_HUI
+  ),
+
+  [2] = LAYOUT_ergodox(
+      KC_SCROLLLOCK,  KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_MS_UP,       KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_MS_LEFT,     KC_MS_DOWN,     KC_MS_RIGHT,    KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_MS_BTN1,     KC_MS_BTN2,
+                                                       KC_TRANSPARENT,                 KC_TRANSPARENT,
+                                                                                       KC_TRANSPARENT,
+                                                       KC_MS_BTN1,     KC_MS_BTN2,     KC_TRANSPARENT,
+
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,    KC_TRANSPARENT,      KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,    KC_TRANSPARENT,      KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+                      KC_TRANSPARENT, KC_MS_WH_DOWN,     KC_MS_WH_UP,         KC_TRANSPARENT,      KC_TRANSPARENT, KC_MEDIA_PLAY_PAUSE,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,    KC_MEDIA_PREV_TRACK, KC_MEDIA_NEXT_TRACK, KC_TRANSPARENT, KC_TRANSPARENT,
+                                      KC_AUDIO_VOL_DOWN, KC_AUDIO_VOL_UP,     KC_AUDIO_MUTE,       KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_WWW_BACK),
+
+  // FPS layout
+  [3] = LAYOUT_ergodox(
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,
+                                                      KC_TRANSPARENT,           KC_TRANSPARENT,
+                                                                                KC_TRANSPARENT,
+                                                      KC_SPACE, KC_TRANSPARENT, KC_TRANSPARENT,
+
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+                      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+                                      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT,      KC_TRANSPARENT, KC_TRANSPARENT,
+      KC_TRANSPARENT, TG(3),
+      KC_TRANSPARENT,
+      KC_TRANSPARENT, KC_TRANSPARENT, KC_TRANSPARENT),
+};
+
+const uint16_t PROGMEM fn_actions[] = {
+  [1] = ACTION_LAYER_TAP_TOGGLE(1)
+};
+
+// leaving this in place for compatibilty with old keymaps cloned and re-compiled.
+const macro_t *action_get_macro(keyrecord_t *record, uint8_t id, uint8_t opt)
+{
+      switch(id) {
+        case 0:
+        if (record->event.pressed) {
+          SEND_STRING (QMK_KEYBOARD "/" QMK_KEYMAP " @ " QMK_VERSION);
+        }
+        break;
+      }
+    return MACRO_NONE;
+};
+
+bool process_record_user(uint16_t keycode, keyrecord_t *record) {
+  switch (keycode) {
+    // dynamically generate these.
+    case EPRM:
+      if (record->event.pressed) {
+        eeconfig_init();
+      }
+      return false;
+      break;
+    case VRSN:
+      if (record->event.pressed) {
+        SEND_STRING (QMK_KEYBOARD "/" QMK_KEYMAP " @ " QMK_VERSION);
+      }
+      return false;
+      break;
+    case RGB_SLD:
+      if (record->event.pressed) {
+        rgblight_mode(1);
+      }
+      return false;
+      break;
+    case EX_PIPE:
+      if (record->event.pressed) {
+        SEND_STRING ( "|> " );
+      }
+      return false;
+      break;
+    case THIN_ARROW:
+      if (record->event.pressed) {
+        SEND_STRING ( "-> " );
+      }
+      return false;
+      break;
+
+
+  }
+  return true;
+}
+
+void matrix_scan_user(void) {
+
+    uint8_t layer = biton32(layer_state);
+
+    ergodox_board_led_off();
+    ergodox_right_led_1_off();
+    ergodox_right_led_2_off();
+    ergodox_right_led_3_off();
+    switch (layer) {
+        case 1:
+            ergodox_right_led_1_on();
+            break;
+        case 2:
+            ergodox_right_led_2_on();
+            break;
+        case 3:
+            ergodox_right_led_3_on();
+            break;
+        case 4:
+            ergodox_right_led_1_on();
+            ergodox_right_led_2_on();
+            break;
+        case 5:
+            ergodox_right_led_1_on();
+            ergodox_right_led_3_on();
+            break;
+        case 6:
+            ergodox_right_led_2_on();
+            ergodox_right_led_3_on();
+            break;
+        case 7:
+            ergodox_right_led_1_on();
+            ergodox_right_led_2_on();
+            ergodox_right_led_3_on();
+            break;
+        default:
+            break;
+    }
+
+};
diff --git a/users/grfn/keys.nix b/users/grfn/keys.nix
new file mode 100644
index 0000000000..29d5a3fa63
--- /dev/null
+++ b/users/grfn/keys.nix
@@ -0,0 +1,6 @@
+{ ... }:
+{
+  whitby = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDIwl+xQYRCk6Ijz/Ll8eXKZrcTH9/7xwlvIowiuqDSFtGkf+73QJkwVJ0YiKHWAPwIUWMzCEO/Ab2g6j4PcR+XYu8kXbrwT5aW65L/AK1oaav2RfV1bnQEVUP9FRPL52BN42J0ibI2QJZKJVws9JF7vxTWPPG0V0eoxcaRMk1ZEqq+/k3GuN8D69VSV8xo9lB8yZEvTxs0YQRiiF7Q6t/3jhYtz6lCdazQviRcSEOj5AVsDjcf1XIAPOcLK4Q4OEXL49T3UaitSYMyKIO8hzNLiyGAUlSbshAnutPXdyNBypkCs6FrSPSRdBfFjzUVE/a+JWCPmx0q0xAVd497Efxby+Vsa2/TPMp7tSisPaqk3MpPmjBS7eI/y4Pl2GpAB4OVANEBNd1Q6K2/37Pk+PrZtIUBiRG8sM0Od36BjwLCxvG0G5P/UYZ93aC8GzqkRf4evOBMiJCvR2o9CDEDycNyTm1y5dyJzQewOTWX9nsiF1rllc92W0ZALvpO03+W2+k= grfn@chupacabra";
+  main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMcBGBoWd5pPIIQQP52rcFOQN3wAY0J/+K2fuU6SffjA";
+  old = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHPiNpPB6Uqs/VSW/C8tR/Z5wCQxKppNL2iETb1ucsYsFf1B2apG5txj06NMT6IWXwWpZXq7ld+/sA+a2I03lO2INP7S1Dto5nAwpNhhKN/UBXk76qYTdY5tEvb9J89S2ZzfQWR30aZ0CEDDrcbc+YktU1eSLdluu6QH+M/uPBweSiVn5wNHkc5sRdbyiVsZSQJ41MO7PQrzGpe7Pxola/ghOHdEFlESJMKA5uoRpCGboxtDE9tMJwG5MxNwHERpfI9FjvvLsJRrp9dRf6A/RQjlV/nb1GmpX0I8pvrXEPxm/l0rOAgE81VSsM+BxJ7ZvCe8u/YqMYJ8xVfskzlVsf griffin@MacBook-Pro";
+}
diff --git a/users/grfn/org-clubhouse/.gitignore b/users/grfn/org-clubhouse/.gitignore
new file mode 100644
index 0000000000..2a7dd97deb
--- /dev/null
+++ b/users/grfn/org-clubhouse/.gitignore
@@ -0,0 +1,3 @@
+# Spacemacs
+org-clubhouse-autoloads.el
+org-clubhouse-pkg.el
diff --git a/users/grfn/org-clubhouse/CODE_OF_CONDUCT.org b/users/grfn/org-clubhouse/CODE_OF_CONDUCT.org
new file mode 100644
index 0000000000..f15e387d54
--- /dev/null
+++ b/users/grfn/org-clubhouse/CODE_OF_CONDUCT.org
@@ -0,0 +1,101 @@
+* Contributor Covenant Code of Conduct
+  :PROPERTIES:
+  :CUSTOM_ID: contributor-covenant-code-of-conduct
+  :END:
+
+** Our Pledge
+   :PROPERTIES:
+   :CUSTOM_ID: our-pledge
+   :END:
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our
+project and our community a harassment-free experience for everyone,
+regardless of age, body size, disability, ethnicity, sex
+characteristics, gender identity and expression, level of experience,
+education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+** Our Standards
+   :PROPERTIES:
+   :CUSTOM_ID: our-standards
+   :END:
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+- Using welcoming and inclusive language
+- Being respectful of differing viewpoints and experiences
+- Gracefully accepting constructive criticism
+- Focusing on what is best for the community
+- Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+- The use of sexualized language or imagery and unwelcome sexual
+  attention or advances
+- Trolling, insulting/derogatory comments, and personal or political
+  attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or
+  electronic address, without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+** Our Responsibilities
+   :PROPERTIES:
+   :CUSTOM_ID: our-responsibilities
+   :END:
+
+Project maintainers are responsible for clarifying the standards of
+acceptable behavior and are expected to take appropriate and fair
+corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit,
+or reject comments, commits, code, wiki edits, issues, and other
+contributions that are not aligned to this Code of Conduct, or to ban
+temporarily or permanently any contributor for other behaviors that they
+deem inappropriate, threatening, offensive, or harmful.
+
+** Scope
+   :PROPERTIES:
+   :CUSTOM_ID: scope
+   :END:
+
+This Code of Conduct applies within all project spaces, and it also
+applies when an individual is representing the project or its community
+in public spaces. Examples of representing a project or community
+include using an official project e-mail address, posting via an
+official social media account, or acting as an appointed representative
+at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+** Enforcement
+   :PROPERTIES:
+   :CUSTOM_ID: enforcement
+   :END:
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may
+be reported by contacting the project team at root@gws.fyi. All
+complaints will be reviewed and investigated and will result in a
+response that is deemed necessary and appropriate to the circumstances.
+The project team is obligated to maintain confidentiality with regard to
+the reporter of an incident. Further details of specific enforcement
+policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in
+good faith may face temporary or permanent repercussions as determined
+by other members of the project's leadership.
+
+** Attribution
+   :PROPERTIES:
+   :CUSTOM_ID: attribution
+   :END:
+
+This Code of Conduct is adapted from the
+[[https://www.contributor-covenant.org][Contributor Covenant]], version
+1.4, available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/users/grfn/org-clubhouse/LICENSE b/users/grfn/org-clubhouse/LICENSE
new file mode 100644
index 0000000000..1777f0fac3
--- /dev/null
+++ b/users/grfn/org-clubhouse/LICENSE
@@ -0,0 +1,7 @@
+Copyright (C) 2018 Off Market Data, Inc. DBA Urbint
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/users/grfn/org-clubhouse/README.org b/users/grfn/org-clubhouse/README.org
new file mode 100644
index 0000000000..9cd8fbe892
--- /dev/null
+++ b/users/grfn/org-clubhouse/README.org
@@ -0,0 +1,142 @@
+#+TITLE:Org-Clubhouse
+
+Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]]
+issue tracker
+
+(This used to be at urbint/org-clubhouse, by the way, but moved here as it's
+more of a personal project than a company one)
+
+* Installation
+
+** [[https://github.com/quelpa/quelpa][Quelpa]]
+
+#+BEGIN_SRC emacs-lisp
+(quelpa '(org-clubhouse
+          :fetcher github
+          :repo "glittershark/org-clubhouse"))
+#+END_SRC
+
+** [[https://github.com/hlissner/doom-emacs/][DOOM Emacs]]
+
+#+BEGIN_SRC emacs-lisp
+;; in packages.el
+(package! org-clubhouse
+  :recipe (:fetcher github
+           :repo "glittershark/org-clubhouse"
+           :files ("*")))
+
+;; in config.el
+(def-package! org-clubhouse)
+#+END_SRC
+
+** [[http://spacemacs.org/][Spacemacs]]
+#+BEGIN_SRC emacs-lisp
+;; in .spacemacs (SPC+fed)
+   dotspacemacs-additional-packages
+    '((org-clubhouse :location (recipe :fetcher github :repo "glittershark/org-clubhouse")))
+#+END_SRC
+
+
+* Setup
+
+Once installed, you'll need to set three global config vars:
+
+#+BEGIN_SRC emacs-lisp
+(setq org-clubhouse-auth-token "<your-token>"
+      org-clubhouse-team-name "<your-team-name>"
+      org-clubhouse-username "<your-username>")
+#+END_SRC
+
+You can generate a new personal API token by going to the "API Tokens" tab on
+the "Settings" page in the clubhouse UI.
+
+Note that ~org-clubhouse-username~ needs to be set to your *mention name*, not
+your username, as currently there's no way to get the ID of a user given their
+username in the clubhouse API
+
+* Usage
+
+** Reading from clubhouse
+
+- ~org-clubhouse-headlines-from-query~
+  Create org-mode headlines from a [[https://help.clubhouse.io/hc/en-us/articles/360000046646-Searching-in-Clubhouse-Story-Search][clubhouse query]] at the cursor's current
+  position, prompting for the headline indentation level and clubhouse query
+  text
+- ~org-clubhouse-headline-from-story~
+  Prompts for headline indentation level and the title of a story (which will
+  complete using the titles of all stories in your Clubhouse workspace) and
+  creates an org-mode headline from that story
+- ~org-clubhouse-headline-from-story-id~
+  Creates an org-mode headline directly from the ID of a clubhouse story
+
+** Writing to clubhouse
+
+- ~org-clubhouse-create-story~
+  Creates a new Clubhouse story from the current headline, or if a region of
+  headlines is selected bulk-creates stories with all those headlines
+- ~org-clubhouse-create-epic~
+  Creates a new Clubhouse epic from the current headline, or if a region of
+  headlines is selected bulk-creates epics with all those headlines
+- ~org-clubhouse-create-story-with-task-list~
+  Creates a Clubhouse story from the current headline, making all direct
+  children of the headline into tasks in the task list of the story
+- ~org-clubhouse-push-task-list~
+  Writes each child element of the current clubhouse element as a task list
+  item of the associated clubhouse ID.
+- ~org-clubhouse-update-story-title~
+  Updates the title of the Clubhouse story linked to the current headline with
+  the text of the headline
+- ~org-clubhouse-update-description~
+  Update the status of the Clubhouse story linked to the current element with
+  the contents of a drawer inside the element called DESCRIPTION, if any exists
+- ~org-clubhouse-claim~
+  Adds the user configured in ~org-clubhouse-username~ as the owner of the
+  clubhouse story associated with the headline at point
+
+*** Automatically updating Clubhouse story statuses
+
+Org-clubhouse can be configured to update the status of stories as you update
+their todo-keyword in org-mode. To opt-into this behavior, set the
+~org-clubhouse-mode~ minor-mode:
+
+#+BEGIN_SRC emacs-lisp
+(add-hook 'org-mode-hook #'org-clubhouse-mode nil nil)
+#+END_SRC
+
+The mapping from org-mode todo-keywords is configured via the
+~org-clubhouse-state-alist~ variable, which should be an [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Association-Lists.html][alist]] mapping (string)
+[[https://orgmode.org/manual/Workflow-states.html][org-mode todo-keywords]] to the (string) names of their corresponding workflow
+state. You can have todo-keywords that don't map to a workflow state (I use this
+in my workflow extensively) and org-clubhouse will just preserve the previous
+state of the story when moving to that state.
+
+An example config:
+
+#+BEGIN_SRC emacs-lisp
+(setq org-clubhouse-state-alist
+      '(("TODO"   . "To Do")
+        ("ACTIVE" . "In Progress")
+        ("DONE"   . "Done")))
+#+END_SRC
+
+* Philosophy
+
+I use org-mode every single day to manage tasks, notes, literate programming,
+etc. Part of what that means for me is that I already have a system for the
+structure of my .org files, and I don't want to sacrifice that system for any
+external tool. Updating statuses, ~org-clubhouse-create-story~, and
+~org-clubhouse-headline-from-story~ are my bread and butter for that reason -
+rather than having some sort of bidirectional sync that pulls down full lists of
+all the stories in Clubhouse (or whatever issue tracker / project management
+tool I'm using at the time). I can be in a mode where I'm taking meeting notes,
+think of something that I need to do, make it a TODO headline, and make that
+TODO headline a clubhouse story. That's the same reason for the DESCRIPTION
+drawers rather than just sending the entire contents of a headline to
+Clubhouse - I almost always want to write things like personal notes, literate
+code, etc inside of the tasks I'm working on, and don't always want to share
+that with Clubhouse.
+
+* Configuration
+
+Refer to the beginning of the [[https://github.com/urbint/org-clubhouse/blob/master/org-clubhouse.el][org-clubhouse.el]] file in this repository for
+documentation on all supported configuration variables
diff --git a/users/grfn/org-clubhouse/org-clubhouse.el b/users/grfn/org-clubhouse/org-clubhouse.el
new file mode 100644
index 0000000000..e6e29b5751
--- /dev/null
+++ b/users/grfn/org-clubhouse/org-clubhouse.el
@@ -0,0 +1,1241 @@
+;;; org-clubhouse.el --- Simple, unopinionated integration between org-mode and
+;;; Clubhouse
+
+;;; Copyright (C) 2018 Off Market Data, Inc. DBA Urbint
+;;; Permission is hereby granted, free of charge, to any person obtaining a copy
+;;; of this software and associated documentation files (the "Software"), to
+;;; deal in the Software without restriction, including without limitation the
+;;; rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+;;; sell copies of the Software, and to permit persons to whom the Software is
+;;; furnished to do so, subject to the following conditions:
+
+;;; The above copyright notice and this permission notice shall be included in
+;;; all copies or substantial portions of the Software.
+
+;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+;;; IN THE SOFTWARE.
+
+;;; Commentary:
+;;; org-clubhouse provides simple, unopinionated integration between Emacs's
+;;; org-mode and the Clubhouse issue tracker
+;;;
+;;; To configure org-clubhouse, create an authorization token in Cluhbouse's
+;;; settings, then place the following configuration somewhere private:
+;;;
+;;;   (setq org-clubhouse-auth-token "<auth_token>"
+;;;         org-clubhouse-team-name  "<team-name>")
+;;;
+
+;;; Code:
+
+(require 'cl-macs)
+(require 'dash)
+(require 'dash-functional)
+(require 's)
+(require 'org)
+(require 'org-element)
+(require 'subr-x)
+(require 'ivy)
+(require 'json)
+
+;;;
+;;; Configuration
+;;;
+
+(defvar org-clubhouse-auth-token nil
+  "Authorization token for the Clubhouse API.")
+
+(defvar org-clubhouse-username nil
+  "Username for the current Clubhouse user.
+
+Unfortunately, the Clubhouse API doesn't seem to provide this via the API given
+an API token, so we need to configure this for
+`org-clubhouse-claim-story-on-status-updates' to work")
+
+(defvar org-clubhouse-team-name nil
+  "Team name to use in links to Clubhouse.
+ie https://app.clubhouse.io/<TEAM_NAME>/stories")
+
+(defvar org-clubhouse-project-ids nil
+  "Specific list of project IDs to synchronize with clubhouse.
+If unset all projects will be synchronized")
+
+(defvar org-clubhouse-workflow-name "Default")
+
+(defvar org-clubhouse-default-story-type nil
+  "Sets the default story type. If set to 'nil', it will interactively prompt
+the user each and every time a new story is created. If set to 'feature',
+'bug', or 'chore', that value will be used as the default and the user will
+not be prompted")
+
+(defvar org-clubhouse-state-alist
+  '(("LATER"  . "Unscheduled")
+    ("[ ]"    . "Ready for Development")
+    ("TODO"   . "Ready for Development")
+    ("OPEN"   . "Ready for Development")
+    ("ACTIVE" . "In Development")
+    ("PR"     . "Review")
+    ("DONE"   . "Merged")
+    ("[X]"    . "Merged")
+    ("CLOSED" . "Merged"))
+  "Alist mapping org-mode todo keywords to their corresponding states in
+  Clubhouse. In `org-clubhouse-mode', moving headlines to these todo keywords
+  will update to the corresponding status in Clubhouse")
+
+(defvar org-clubhouse-story-types
+  '(("feature" . "Feature")
+    ("bug"     . "Bug")
+    ("chore"   . "Chore")))
+
+(defvar org-clubhouse-default-story-types
+  '(("feature" . "Feature")
+    ("bug"     . "Bug")
+    ("chore"   . "Chore")
+    ("prompt"  . "**Prompt each time (do not set a default story type)**")))
+
+(defvar org-clubhouse-default-state "Proposed"
+  "Default state to create all new stories in.")
+
+(defvar org-clubhouse-claim-story-on-status-update 't
+  "Controls the assignee behavior of stories on status update.
+
+If set to 't, will mark the current user as the owner of any clubhouse
+stories on any update to the status.
+
+If set to nil, will never automatically update the assignee of clubhouse
+stories.
+
+If set to a list of todo-state's, will mark the current user as the owner of
+clubhouse stories whenever updating the status to one of those todo states.")
+
+(defvar org-clubhouse-create-stories-with-labels nil
+  "Controls the way org-clubhouse creates stories with labels based on org tags.
+
+If set to 't, will create labels for all org tags on headlines when stories are
+created.
+
+If set to 'existing, will set labels on created stories only if the label
+already exists in clubhouse
+
+If set to nil, will never create stories with labels")
+
+;;;
+;;; Utilities
+;;;
+
+(defmacro comment (&rest _)
+  "Comment out one or more s-expressions."
+  nil)
+
+(defun ->list (vec) (append vec nil))
+
+(defun reject-archived (item-list)
+  (-reject (lambda (item) (equal :json-true (alist-get 'archived item))) item-list))
+
+(defun alist->plist (key-map alist)
+  (->> key-map
+       (-map (lambda (key-pair)
+               (let ((alist-key (car key-pair))
+                     (plist-key (cdr key-pair)))
+                 (list plist-key (alist-get alist-key alist)))))
+       (-flatten-n 1)))
+
+(defun alist-get-equal (key alist)
+  "Like `alist-get', but uses `equal' instead of `eq' for comparing keys"
+  (->> alist
+       (-find (lambda (pair) (equal key (car pair))))
+       (cdr)))
+
+(defun invert-alist (alist)
+  "Invert the keys and values of ALIST."
+  (-map (lambda (cell) (cons (cdr cell) (car cell))) alist))
+
+(comment
+
+ (alist->plist
+  '((foo . :foo)
+    (bar . :something))
+
+  '((foo . "foo") (bar . "bar") (ignored . "ignoreme!")))
+ ;; => (:foo "foo" :something "bar")
+
+ )
+
+(defun find-match-in-alist (target alist)
+  (->> alist
+       (-find (lambda (key-value)
+                   (string-equal (cdr key-value) target)))
+       car))
+
+(defun org-clubhouse-collect-headlines (beg end)
+  "Collects the headline at point or the headlines in a region. Returns a list."
+  (if (and beg end)
+      (org-clubhouse-get-headlines-in-region beg end)
+    (list (org-element-find-headline))))
+
+
+(defun org-clubhouse-get-headlines-in-region (beg end)
+  "Collects the headlines from BEG to END"
+  (save-excursion
+    ;; This beg/end clean up pulled from `reverse-region`.
+    ;; it expands the region to include the full lines from the selected region.
+
+    ;; put beg at the start of a line and end and the end of one --
+    ;; the largest possible region which fits this criteria
+    (goto-char beg)
+    (or (bolp) (forward-line 1))
+    (setq beg (point))
+    (goto-char end)
+    ;; the test for bolp is for those times when end is on an empty line;
+    ;; it is probably not the case that the line should be included in the
+    ;; reversal; it isn't difficult to add it afterward.
+    (or (and (eolp) (not (bolp))) (progn (forward-line -1) (end-of-line)))
+    (setq end (point-marker))
+
+    ;; move to the beginning
+    (goto-char beg)
+    ;; walk by line until past end
+    (let ((headlines '())
+          (before-end 't))
+      (while before-end
+        (add-to-list 'headlines (org-element-find-headline))
+        (let ((before (point)))
+          (org-forward-heading-same-level 1)
+          (setq before-end (and (not (eq before (point))) (< (point) end)))))
+      (reverse headlines))))
+
+;;;
+;;; Org-element interaction
+;;;
+
+;; (defun org-element-find-headline ()
+;;   (let ((current-elt (org-element-at-point)))
+;;     (if (equal 'headline (car current-elt))
+;;         current-elt
+;;       (let* ((elt-attrs (cadr current-elt))
+;;              (parent (plist-get elt-attrs :post-affiliated)))
+;;         (goto-char parent)
+;;         (org-element-find-headline)))))
+
+(defun org-element-find-headline ()
+  (save-mark-and-excursion
+    (when (not (outline-on-heading-p)) (org-back-to-heading))
+    (let ((current-elt (org-element-at-point)))
+      (when (equal 'headline (car current-elt))
+        (cadr current-elt)))))
+
+(defun org-element-extract-clubhouse-id (elt &optional property)
+  (when-let* ((clubhouse-id-link (plist-get elt (or property :CLUBHOUSE-ID))))
+    (cond
+     ((string-match
+       (rx "[[" (one-or-more anything) "]"
+           "[" (group (one-or-more digit)) "]]")
+       clubhouse-id-link)
+      (string-to-number (match-string 1 clubhouse-id-link)))
+     ((string-match-p
+       (rx buffer-start
+           (one-or-more digit)
+           buffer-end)
+       clubhouse-id-link)
+      (string-to-number clubhouse-id-link)))))
+
+(comment
+ (let ((strn "[[https://app.clubhouse.io/example/story/2330][2330]]"))
+   (string-match
+    (rx "[[" (one-or-more anything) "]"
+        "[" (group (one-or-more digit)) "]]")
+    strn)
+   (string-to-number (match-string 1 strn)))
+ )
+
+(defun org-element-clubhouse-id ()
+  (org-element-extract-clubhouse-id
+   (org-element-find-headline)))
+
+(defun org-clubhouse-clocked-in-story-id ()
+  "Return the clubhouse story-id of the currently clocked-in org entry, if any."
+  (save-mark-and-excursion
+    (save-current-buffer
+      (when (org-clocking-p)
+        (set-buffer (marker-buffer org-clock-marker))
+        (save-restriction
+          (when (or (< org-clock-marker (point-min))
+                    (> org-clock-marker (point-max)))
+            (widen))
+          (goto-char org-clock-marker)
+          (org-element-clubhouse-id))))))
+
+(comment
+ (org-clubhouse-clocked-in-story-id)
+ )
+
+(defun org-element-and-children-at-point ()
+  (let* ((elt (org-element-find-headline))
+         (contents-begin (or (plist-get elt :contents-begin)
+                             (plist-get elt :begin)))
+         (end   (plist-get elt :end))
+         (level (plist-get elt :level))
+         (children '()))
+    (save-excursion
+      (goto-char (+ contents-begin (length (plist-get elt :title))))
+      (while (< (point) end)
+        (let* ((next-elt (org-element-at-point))
+               (elt-type (car next-elt))
+               (elt      (cadr next-elt)))
+          (when (and (eql 'headline elt-type)
+                     (eql (+ 1 level) (plist-get elt :level)))
+            (push elt children))
+          (goto-char (plist-get elt :end)))))
+    (append elt `(:children ,(reverse children)))))
+
+(defun +org-element-contents (elt)
+  (if-let ((begin (plist-get (cadr elt) :contents-begin))
+           (end (plist-get (cadr elt) :contents-end)))
+      (buffer-substring-no-properties begin end)
+    ""))
+
+(defun org-clubhouse-find-description-drawer ()
+  "Try to find a DESCRIPTION drawer in the current element."
+  (let ((elt (org-element-at-point)))
+    (cl-case (car elt)
+      ('drawer (+org-element-contents elt))
+      ('headline
+       (when-let ((drawer-pos (string-match
+                               ":DESCRIPTION:"
+                               (+org-element-contents elt))))
+         (save-excursion
+           (goto-char (+ (plist-get (cadr elt) :contents-begin)
+                         drawer-pos))
+           (org-clubhouse-find-description-drawer)))))))
+
+(defun org-clubhouse--labels-for-elt (elt)
+  "Return the Clubhouse labels based on the tags of ELT and the user's config."
+  (unless (eq nil org-clubhouse-create-stories-with-labels)
+    (let ((tags (org-get-tags (plist-get elt :contents-begin))))
+      (-map (lambda (l) `((name . ,l)))
+            (cl-case org-clubhouse-create-stories-with-labels
+              ('t tags)
+              ('existing (-filter (lambda (tag) (-some (lambda (l)
+                                                    (string-equal tag (cdr l)))
+                                                  (org-clubhouse-labels)))
+                                  tags)))))))
+
+;;;
+;;; API integration
+;;;
+
+(defvar org-clubhouse-base-url* "https://api.clubhouse.io/api/v3")
+
+(defun org-clubhouse-auth-url (url &optional params)
+ (concat url
+         "?"
+         (url-build-query-string
+          (cons `("token" ,org-clubhouse-auth-token) params))))
+
+(defun org-clubhouse-baseify-url (url)
+ (if (s-starts-with? org-clubhouse-base-url* url) url
+   (concat org-clubhouse-base-url*
+           (if (s-starts-with? "/" url) url
+             (concat "/" url)))))
+
+(cl-defun org-clubhouse-request (method url &key data (params '()))
+ (message "%s %s %s" method url (prin1-to-string data))
+ (let* ((url-request-method method)
+        (url-request-extra-headers
+         '(("Content-Type" . "application/json")))
+        (url-request-data data)
+        (buf))
+
+   (setq url (-> url
+                 org-clubhouse-baseify-url
+                 (org-clubhouse-auth-url params)))
+
+   (setq buf (url-retrieve-synchronously url))
+
+   (with-current-buffer buf
+     (goto-char url-http-end-of-headers)
+     (prog1 (json-read) (kill-buffer)))))
+
+(cl-defun to-id-name-pairs
+    (seq &optional (id-attr 'id) (name-attr 'name))
+  (->> seq
+       ->list
+       (-map (lambda (resource)
+          (cons (alist-get id-attr   resource)
+                (alist-get name-attr resource))))))
+
+(cl-defun org-clubhouse-fetch-as-id-name-pairs
+    (resource &optional
+              (id-attr 'id)
+              (name-attr 'name))
+  "Returns the given resource from clubhouse as (id . name) pairs"
+  (let ((resp-json (org-clubhouse-request "GET" resource)))
+    (-> resp-json
+        ->list
+        reject-archived
+        (to-id-name-pairs id-attr name-attr))))
+
+(defun org-clubhouse-get-story
+    (clubhouse-id)
+  (org-clubhouse-request "GET" (format "/stories/%s" clubhouse-id)))
+
+(defun org-clubhouse-link-to-story (story-id)
+  (format "https://app.clubhouse.io/%s/story/%d"
+          org-clubhouse-team-name
+          story-id))
+
+(defun org-clubhouse-link-to-epic (epic-id)
+  (format "https://app.clubhouse.io/%s/epic/%d"
+          org-clubhouse-team-name
+          epic-id))
+
+(defun org-clubhouse-link-to-milestone (milestone-id)
+  (format "https://app.clubhouse.io/%s/milestone/%d"
+          org-clubhouse-team-name
+          milestone-id))
+
+(defun org-clubhouse-link-to-project (project-id)
+  (format "https://app.clubhouse.io/%s/project/%d"
+          org-clubhouse-team-name
+          project-id))
+
+;;;
+;;; Caching
+;;;
+
+(comment
+ (defcache org-clubhouse-projects
+   (org-sync-clubhouse-fetch-as-id-name-pairs "projectx"))
+
+ (clear-org-clubhouse-projects-cache)
+ (clear-org-clubhouse-cache)
+ )
+
+(defvar org-clubhouse-cache-clear-functions ())
+
+(defmacro defcache (name &optional docstring &rest body)
+  (let* ((doc (when docstring (list docstring)))
+         (cache-var-name (intern (concat (symbol-name name)
+                                         "-cache")))
+         (clear-cache-function-name
+          (intern (concat "clear-" (symbol-name cache-var-name)))))
+    `(progn
+       (defvar ,cache-var-name :no-cache)
+       (defun ,name ()
+         ,@doc
+         (when (equal :no-cache ,cache-var-name)
+           (setq ,cache-var-name (progn ,@body)))
+         ,cache-var-name)
+       (defun ,clear-cache-function-name ()
+         (interactive)
+         (setq ,cache-var-name :no-cache))
+
+       (push (quote ,clear-cache-function-name)
+             org-clubhouse-cache-clear-functions))))
+
+(defun org-clubhouse-clear-cache ()
+  (interactive)
+  (-map #'funcall org-clubhouse-cache-clear-functions))
+
+;;;
+;;; API resource functions
+;;;
+
+(defcache org-clubhouse-projects
+  "Returns projects as (project-id . name)"
+  (org-clubhouse-fetch-as-id-name-pairs "projects"))
+
+(defcache org-clubhouse-epics
+  "Returns epics as (epic-id . name)"
+  (org-clubhouse-fetch-as-id-name-pairs "epics"))
+
+(defcache org-clubhouse-milestones
+  "Returns milestone-id . name)"
+  (org-clubhouse-fetch-as-id-name-pairs "milestones"))
+
+(defcache org-clubhouse-workflow-states
+  "Returns worflow states as (name . id) pairs"
+  (let* ((resp-json (org-clubhouse-request "GET" "workflows"))
+         (workflows (->list resp-json))
+         ;; just assume it exists, for now
+         (workflow  (-find (lambda (workflow)
+                             (equal org-clubhouse-workflow-name
+                                    (alist-get 'name workflow)))
+                           workflows))
+         (states    (->list (alist-get 'states workflow))))
+    (to-id-name-pairs states
+                      'name
+                      'id)))
+
+(defcache org-clubhouse-labels
+  "Returns labels as (label-id . name)"
+  (org-clubhouse-fetch-as-id-name-pairs "labels"))
+
+(defcache org-clubhouse-whoami
+  "Returns the ID of the logged in user"
+  (->> (org-clubhouse-request
+        "GET"
+        "/members")
+       ->list
+       (find-if (lambda (m)
+                  (->> m
+                       (alist-get 'profile)
+                       (alist-get 'mention_name)
+                       (equal org-clubhouse-username))))
+       (alist-get 'id)))
+
+(defcache org-clubhouse-iterations
+  "Returns iterations as (iteration-id . name)"
+  (org-clubhouse-fetch-as-id-name-pairs "iterations"))
+
+(defun org-clubhouse-stories-in-project (project-id)
+  "Return the stories in the given PROJECT-ID as org headlines."
+  (let ((resp-json (org-clubhouse-request "GET" (format "/projects/%d/stories" project-id))))
+    (->> resp-json ->list reject-archived
+         (-reject (lambda (story) (equal :json-true (alist-get 'completed story))))
+         (-map (lambda (story)
+                 (cons
+                  (cons 'status
+                        (cond
+                         ((equal :json-true (alist-get 'started story))
+                          'started)
+                         ((equal :json-true (alist-get 'completed story))
+                          'completed)
+                         ('t
+                          'open)))
+                  story)))
+         (-map (-partial #'alist->plist
+                         '((name . :title)
+                           (id . :id)
+                           (status . :status)))))))
+
+(defun org-clubhouse-workflow-state-id-to-todo-keyword (workflow-state-id)
+  "Convert the named clubhouse WORKFLOW-STATE-ID to an org todo keyword."
+  (let* ((state-name (alist-get-equal
+                      workflow-state-id
+                      (invert-alist (org-clubhouse-workflow-states))))
+         (inv-state-name-alist
+          (-map (lambda (cell) (cons (cdr cell) (car cell)))
+                org-clubhouse-state-alist)))
+    (or (alist-get-equal state-name inv-state-name-alist)
+        (if state-name (s-upcase state-name) "UNKNOWN"))))
+
+;;;
+;;; Prompting
+;;;
+
+(defun org-clubhouse-prompt-for-project (cb)
+  (ivy-read
+   "Select a project: "
+   (-map #'cdr (org-clubhouse-projects))
+   :require-match t
+   :history 'org-clubhouse-project-history
+   :action (lambda (selected)
+             (let ((project-id
+                    (find-match-in-alist selected (org-clubhouse-projects))))
+               (funcall cb project-id)))))
+
+(defun org-clubhouse-prompt-for-epic (cb)
+  "Prompt the user for an epic using ivy and call CB with its ID."
+  (ivy-read
+   "Select an epic: "
+   (-map #'cdr (append '((nil . "No Epic")) (org-clubhouse-epics)))
+   :history 'org-clubhouse-epic-history
+   :action (lambda (selected)
+             (let ((epic-id
+                    (find-match-in-alist selected (org-clubhouse-epics))))
+               (funcall cb epic-id)))))
+
+(defun org-clubhouse-prompt-for-milestone (cb)
+  "Prompt the user for a milestone using ivy and call CB with its ID."
+  (ivy-read
+   "Select a milestone: "
+   (-map #'cdr (append '((nil . "No Milestone")) (org-clubhouse-milestones)))
+   :require-match t
+   :history 'org-clubhouse-milestone-history
+   :action (lambda (selected)
+             (let ((milestone-id
+                    (find-match-in-alist selected (org-clubhouse-milestones))))
+               (funcall cb milestone-id)))))
+
+(defun org-clubhouse-prompt-for-story-type (cb)
+  (ivy-read
+   "Select a story type: "
+   (-map #'cdr org-clubhouse-story-types)
+   :history 'org-clubhouse-story-history
+   :action (lambda (selected)
+             (let ((story-type
+                    (find-match-in-alist selected org-clubhouse-story-types)))
+               (funcall cb story-type)))))
+
+(defun org-clubhouse-prompt-for-default-story-type ()
+  (interactive)
+  (ivy-read
+   "Select a default story type: "
+   (-map #'cdr org-clubhouse-default-story-types)
+   :history 'org-clubhouse-default-story-history
+   :action (lambda (selected)
+             (let ((story-type
+                    (find-match-in-alist selected org-clubhouse-default-story-types)))
+                  (if (string-equal story-type "prompt")
+                      (setq org-clubhouse-default-story-type nil)
+                      (setq org-clubhouse-default-story-type story-type))))))
+
+;;;
+;;; Epic creation
+;;;
+
+(cl-defun org-clubhouse-create-epic-internal
+    (title &key milestone-id)
+  (cl-assert (and (stringp title)
+                  (or (null milestone-id)
+                      (integerp milestone-id))))
+  (org-clubhouse-request
+   "POST"
+   "epics"
+   :data
+   (json-encode
+    `((name . ,title)
+      (milestone_id . ,milestone-id)))))
+
+(defun org-clubhouse-populate-created-epic (elt epic)
+  (let ((elt-start  (plist-get elt :begin))
+        (epic-id    (alist-get 'id epic))
+        (milestone-id (alist-get 'milestone_id epic)))
+    (save-excursion
+      (goto-char elt-start)
+
+      (org-set-property "clubhouse-epic-id"
+                        (org-link-make-string
+                         (org-clubhouse-link-to-epic epic-id)
+                         (number-to-string epic-id)))
+
+      (when milestone-id
+        (org-set-property "clubhouse-milestone"
+                          (org-link-make-string
+                           (org-clubhouse-link-to-milestone milestone-id)
+                           (alist-get milestone-id (org-clubhouse-milestones))))))))
+
+(defun org-clubhouse-create-epic (&optional beg end)
+  "Creates a clubhouse epic using selected headlines.
+Will pull the title from the headline at point, or create epics for all the
+headlines in the selected region.
+
+All epics are added to the same milestone, as selected via a prompt.
+If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored."
+  (interactive
+   (when (use-region-p)
+     (list (region-beginning region-end))))
+
+  (let* ((elts (org-clubhouse-collect-headlines beg end))
+         (elts (-remove (lambda (elt) (plist-get elt :CLUBHOUSE-EPIC-ID)) elts)))
+    (org-clubhouse-prompt-for-milestone
+     (lambda (milestone-id)
+       (dolist (elt elts)
+         (let* ((title (plist-get elt :title))
+                (epic  (org-clubhouse-create-epic-internal
+                        title
+                        :milestone-id milestone-id)))
+           (org-clubhouse-populate-created-epic elt epic))
+         elts)))))
+
+;;;
+;;; Story creation
+;;;
+
+(defun org-clubhouse-default-state-id ()
+  (alist-get-equal org-clubhouse-default-state (org-clubhouse-workflow-states)))
+
+(cl-defun org-clubhouse-create-story-internal
+    (title &key project-id epic-id story-type description labels)
+  (cl-assert (and (stringp title)
+               (integerp project-id)
+               (or (null epic-id) (integerp epic-id))
+               (or (null description) (stringp description))))
+  (let ((workflow-state-id (org-clubhouse-default-state-id))
+        (params `((name . ,title)
+                  (project_id . ,project-id)
+                  (epic_id . ,epic-id)
+                  (story_type . ,story-type)
+                  (description . ,(or description ""))
+                  (labels . ,labels))))
+
+    (when workflow-state-id
+      (push `(workflow_state_id . ,workflow-state-id) params))
+
+    (org-clubhouse-request
+     "POST"
+     "stories"
+     :data
+     (json-encode params))))
+
+(cl-defun org-clubhouse-populate-created-story (elt story &key extra-properties)
+  (let ((elt-start  (plist-get elt :begin))
+        (story-id   (alist-get 'id story))
+        (epic-id    (alist-get 'epic_id story))
+        (project-id (alist-get 'project_id story))
+        (story-type (alist-get 'story_type story)))
+
+    (save-excursion
+      (goto-char elt-start)
+
+      (org-set-property "clubhouse-id"
+                        (org-link-make-string
+                         (org-clubhouse-link-to-story story-id)
+                         (number-to-string story-id)))
+      (when epic-id
+        (org-set-property "clubhouse-epic"
+                          (org-link-make-string
+                           (org-clubhouse-link-to-epic epic-id)
+                           (alist-get epic-id (org-clubhouse-epics)))))
+
+      (org-set-property "clubhouse-project"
+                        (org-link-make-string
+                         (org-clubhouse-link-to-project project-id)
+                         (alist-get project-id (org-clubhouse-projects))))
+
+      (org-set-property "story-type"
+                        (alist-get-equal story-type org-clubhouse-story-types))
+
+      (dolist (extra-prop extra-properties)
+        (org-set-property (car extra-prop)
+                          (alist-get (cdr extra-prop) story)))
+
+      (org-todo "TODO"))))
+
+(defun org-clubhouse-create-story (&optional beg end &key then)
+  "Creates a clubhouse story using selected headlines.
+
+Will pull the title from the headline at point,
+or create cards for all the headlines in the selected region.
+
+All stories are added to the same project and epic, as selected via two prompts.
+If the stories already have a CLUBHOUSE-ID, they are filtered and ignored."
+  (interactive
+   (when (use-region-p)
+     (list (region-beginning) (region-end))))
+
+  (let* ((elts     (org-clubhouse-collect-headlines beg end))
+         (new-elts (-remove (lambda (elt) (plist-get elt :CLUBHOUSE-ID)) elts)))
+    (org-clubhouse-prompt-for-project
+     (lambda (project-id)
+       (when project-id
+         (org-clubhouse-prompt-for-epic
+          (lambda (epic-id)
+            (let ((create-story
+                   (lambda (story-type)
+                     (-map
+                      (lambda (elt)
+                        (let* ((title (plist-get elt :title))
+                               (description
+                                (save-mark-and-excursion
+                                  (goto-char (plist-get elt :begin))
+                                  (org-clubhouse-find-description-drawer)))
+                               (labels (org-clubhouse--labels-for-elt elt))
+                               (story (org-clubhouse-create-story-internal
+                                       title
+                                       :project-id project-id
+                                       :epic-id epic-id
+                                       :story-type story-type
+                                       :description description
+                                       :labels labels)))
+                          (org-clubhouse-populate-created-story elt story)
+                          (when (functionp then)
+                            (funcall then story))))
+                      new-elts))))
+              (if org-clubhouse-default-story-type
+                  (funcall create-story org-clubhouse-default-story-type)
+                (org-clubhouse-prompt-for-story-type create-story))))))))))
+
+(defun org-clubhouse-create-story-with-task-list (&optional beg end)
+  "Creates a clubhouse story using the selected headline, making all direct
+children of that headline into tasks in the task list of the story."
+  (interactive
+   (when (use-region-p)
+     (list (region-beginning) (region-end))))
+
+  (let* ((elt (org-element-and-children-at-point)))
+    (org-clubhouse-create-story nil nil
+     :then (lambda (story)
+             (pp story)
+             (org-clubhouse-push-task-list
+              (alist-get 'id story)
+              (plist-get elt :children))))))
+
+;;;
+;;; Task creation
+;;;
+
+(cl-defun org-clubhouse-create-task (title &key story-id)
+  (cl-assert (and (stringp title)
+               (integerp story-id)))
+  (org-clubhouse-request
+   "POST"
+   (format "/stories/%d/tasks" story-id)
+   :data (json-encode `((description . ,title)))))
+
+(defun org-clubhouse-push-task-list (&optional parent-clubhouse-id child-elts)
+  "Writes each child of the element at point as a task list item.
+
+When called as (org-clubhouse-push-task-list PARENT-CLUBHOUSE-ID CHILD-ELTS),
+allows manually passing a clubhouse ID and list of org-element plists to write"
+  (interactive)
+  (let* ((elt (org-element-and-children-at-point))
+         (parent-clubhouse-id (or parent-clubhouse-id
+                                  (org-element-extract-clubhouse-id elt)))
+         (child-elts (or child-elts (plist-get elt :children)))
+         (story (org-clubhouse-get-story parent-clubhouse-id))
+         (existing-tasks (alist-get 'tasks story))
+         (task-exists
+          (lambda (task-name)
+            (cl-some (lambda (task)
+                    (string-equal task-name (alist-get 'description task)))
+                  existing-tasks)))
+         (elts-with-starts
+          (-map (lambda (e) (cons (set-marker (make-marker)
+                                         (plist-get e :begin))
+                             e))
+                child-elts)))
+    (dolist (child-elt-and-start elts-with-starts)
+      (let* ((start (car child-elt-and-start))
+             (child-elt (cdr child-elt-and-start))
+             (task-name (plist-get child-elt :title)))
+        (unless (funcall task-exists task-name)
+          (let ((task (org-clubhouse-create-task
+                       task-name
+                       :story-id parent-clubhouse-id)))
+            (org-clubhouse-populate-created-task child-elt task start)))))))
+
+(defun org-clubhouse-populate-created-task (elt task &optional begin)
+  (let ((elt-start (or begin (plist-get elt :begin)))
+        (task-id   (alist-get 'id task))
+        (story-id  (alist-get 'story_id task)))
+
+    (save-excursion
+      (goto-char elt-start)
+
+      (org-set-property "clubhouse-task-id" (format "%d" task-id))
+
+      (org-set-property "clubhouse-story-id"
+                        (org-link-make-string
+                         (org-clubhouse-link-to-story story-id)
+                         (number-to-string story-id)))
+
+      (org-todo "TODO"))))
+
+;;;
+;;; Task Updates
+;;;
+
+(cl-defun org-clubhouse-update-task-internal
+    (story-id task-id &rest attrs)
+  (cl-assert (and (integerp story-id)
+                  (integerp task-id)
+                  (listp attrs)))
+  (org-clubhouse-request
+   "PUT"
+   (format "stories/%d/tasks/%d" story-id task-id)
+   :data
+   (json-encode attrs)))
+
+;;;
+;;; Story updates
+;;;
+
+(cl-defun org-clubhouse-update-story-internal
+    (story-id &rest attrs)
+  (cl-assert (and (integerp story-id)
+               (listp attrs)))
+  (org-clubhouse-request
+   "PUT"
+   (format "stories/%d" story-id)
+   :data
+   (json-encode attrs)))
+
+(cl-defun org-clubhouse-update-story-at-point (&rest attrs)
+  (when-let* ((clubhouse-id (org-element-clubhouse-id)))
+    (apply
+     #'org-clubhouse-update-story-internal
+     (cons clubhouse-id attrs))
+    t))
+
+(defun org-clubhouse-update-story-title ()
+  "Update the title of the Clubhouse story linked to the current headline.
+
+Update the title of the story linked to the current headline with the text of
+the headline."
+  (interactive)
+
+  (let* ((elt (org-element-find-headline))
+         (title (plist-get elt :title))
+         (clubhouse-id (org-element-clubhouse-id)))
+    (and
+     (org-clubhouse-update-story-at-point
+      clubhouse-id
+      :name title)
+     (message "Successfully updated story title to \"%s\""
+              title))))
+
+(defun org-clubhouse-update-status ()
+  "Update the status of the Clubhouse story linked to the current element.
+
+Update the status of the Clubhouse story linked to the current element with the
+entry in `org-clubhouse-state-alist' corresponding to the todo-keyword of the
+element."
+  (interactive)
+  (let* ((elt (org-element-find-headline))
+         (todo-keyword (-> elt
+                           (plist-get :todo-keyword)
+                           (substring-no-properties)))
+
+         (clubhouse-id (org-element-extract-clubhouse-id elt))
+         (task-id (plist-get elt :CLUBHOUSE-TASK-ID)))
+    (cond
+     (clubhouse-id
+      (let* ((todo-keyword (-> elt
+                               (plist-get :todo-keyword)
+                               (substring-no-properties))))
+        (when-let* ((clubhouse-workflow-state
+                     (alist-get-equal todo-keyword org-clubhouse-state-alist))
+                    (workflow-state-id
+                     (alist-get-equal clubhouse-workflow-state
+                                      (org-clubhouse-workflow-states))))
+          (let ((update-assignee?
+                 (if (or (eq 't org-clubhouse-claim-story-on-status-update)
+                         (member todo-keyword
+                                 org-clubhouse-claim-story-on-status-update))
+                     (if org-clubhouse-username
+                         't
+                       (warn "Not claiming story since `org-clubhouse-username'
+                       is not set")
+                       nil))))
+
+            (if update-assignee?
+                (org-clubhouse-update-story-internal
+                 clubhouse-id
+                 :workflow_state_id workflow-state-id
+                 :owner_ids (if update-assignee?
+                                (list (org-clubhouse-whoami))
+                              (list)))
+              (org-clubhouse-update-story-internal
+                 clubhouse-id
+                 :workflow_state_id workflow-state-id))
+            (message
+             (if update-assignee?
+                 "Successfully claimed story and updated clubhouse status to \"%s\""
+               "Successfully updated clubhouse status to \"%s\"")
+             clubhouse-workflow-state)))))
+
+     (task-id
+      (let ((story-id (org-element-extract-clubhouse-id
+                       elt
+                       :CLUBHOUSE-STORY-ID))
+            (done? (member todo-keyword org-done-keywords)))
+        (org-clubhouse-update-task-internal
+         story-id
+         (string-to-number task-id)
+         :complete (if done? 't :json-false))
+        (message "Successfully marked clubhouse task status as %s"
+                 (if done? "complete" "incomplete")))))))
+
+(defun org-clubhouse-update-description ()
+  "Update the description of the Clubhouse story linked to the current element.
+
+Update the status of the Clubhouse story linked to the current element with the
+contents of a drawer inside the element called DESCRIPTION, if any."
+  (interactive)
+  (when-let* ((new-description (org-clubhouse-find-description-drawer)))
+    (and
+     (org-clubhouse-update-story-at-point
+      :description new-description)
+     (message "Successfully updated story description"))))
+
+(defun org-clubhouse-update-labels ()
+  "Update the labels of the Clubhouse story linked to the current element.
+
+Will use the value of `org-clubhouse-create-stories-with-labels' to determine
+which labels to set."
+  (interactive)
+  (when-let* ((elt (org-element-find-headline))
+              (new-labels (org-clubhouse--labels-for-elt elt)))
+    (and
+     (org-clubhouse-update-story-at-point
+      :labels new-labels)
+     (message "Successfully updated story labels to :%s:"
+              (->> new-labels
+                   (-map #'cdar)
+                   (s-join ":"))))))
+
+
+;;;
+;;; Creating headlines from existing stories
+;;;
+
+(defun org-clubhouse--task-to-headline-text (level task)
+  (format "%s %s %s
+:PROPERTIES:
+:clubhouse-task-id: %s
+:clubhouse-story-id: %s
+:END:"
+          (make-string level ?*)
+          (if (equal :json-false (alist-get 'complete task))
+              "TODO" "DONE")
+          (alist-get 'description task)
+          (alist-get 'id task)
+          (let ((story-id (alist-get 'story_id task)))
+            (org-link-make-string
+             (org-clubhouse-link-to-story story-id)
+             story-id))))
+
+(defun org-clubhouse--story-to-headline-text (level story)
+  (let ((story-id (alist-get 'id story)))
+    (format
+     "%s %s %s %s
+:PROPERTIES:
+:clubhouse-id: %s
+:END:
+%s
+%s
+"
+     (make-string level ?*)
+     (org-clubhouse-workflow-state-id-to-todo-keyword
+      (alist-get 'workflow_state_id story))
+     (alist-get 'name story)
+     (if-let ((labels (->> story
+                             (alist-get 'labels)
+                             ->list
+                             (-map (apply-partially #'alist-get 'name)))))
+         (format ":%s:" (s-join ":" labels))
+       "")
+     (org-link-make-string
+      (org-clubhouse-link-to-story story-id)
+      (number-to-string story-id))
+     (let ((desc (alist-get 'description story)))
+       (if (= 0 (length desc)) ""
+         (format ":DESCRIPTION:\n%s\n:END:" desc)))
+     (if-let ((tasks (seq-sort-by
+                      (apply-partially #'alist-get 'position)
+                      #'<
+                      (or (alist-get 'tasks story)
+                          (alist-get 'tasks
+                                     (org-clubhouse-get-story story-id))))))
+         (mapconcat (apply-partially #'org-clubhouse--task-to-headline-text
+                                     (1+ level))
+                    tasks
+                    "\n")
+       ""))))
+
+(defun org-clubhouse-headline-from-my-tasks (level)
+  "Prompt my active stories and create a single `org-mode' headline at LEVEL."
+  (interactive "*nLevel: \n")
+  (if org-clubhouse-username
+      (let* ((story-list (org-clubhouse--search-stories
+                          (format "owner:%s !is:done !is:archived"
+                                  org-clubhouse-username)))
+             (stories (to-id-name-pairs story-list)))
+        (org-clubhouse-headline-from-story-id level
+                                              (find-match-in-alist
+                                               (ivy-read "Select Story: "
+                                                         (-map #'cdr stories))
+                                               stories)))
+    (warn "Can't fetch my tasks if `org-clubhouse-username' is unset")))
+
+(defun org-clubhouse-headline-from-story-id (level story-id)
+  "Create a single `org-mode' headline at LEVEL based on the given clubhouse STORY-ID."
+  (interactive "*nLevel: \nnStory ID: ")
+  (let* ((story (org-clubhouse-get-story story-id)))
+    (if (equal '((message . "Resource not found.")) story)
+        (message "Story ID not found: %d" story-id)
+      (save-mark-and-excursion
+        (insert (org-clubhouse--story-to-headline-text level story))
+        (org-align-tags)))))
+
+(defun org-clubhouse--search-stories (query)
+  (unless (string= "" query)
+    (-> (org-clubhouse-request "GET" "search/stories" :params `((query ,query)))
+        cdadr
+        (append nil)
+        reject-archived)))
+
+(defun org-clubhouse-prompt-for-iteration (cb)
+  "Prompt for iteration and call CB with that iteration"
+  (ivy-read
+   "Select an interation: "
+   (-map #'cdr (org-clubhouse-iterations))
+   :require-match t
+   :history 'org-clubhouse-iteration-history
+   :action (lambda (selected)
+             (let ((iteration-id
+                    (find-match-in-alist selected (org-clubhouse-iterations))))
+               (funcall cb iteration-id)))))
+
+(defun org-clubhouse--get-iteration (iteration-id)
+  (-> (org-clubhouse-request "GET" (format "iterations/%d/stories" iteration-id))
+      (append nil)))
+
+(defun org-clubhouse-headlines-from-iteration (level)
+  "Create `org-mode' headlines from a clubhouse iteration.
+
+Create `org-mode' headlines from all the resulting stories at headline level LEVEL."
+  (interactive "*nLevel: ")
+  (org-clubhouse-prompt-for-iteration
+   (lambda (iteration-id)
+     (let ((story-list (org-clubhouse--get-iteration iteration-id)))
+       (if (null story-list)
+           (message "Iteration id returned no stories: %d" iteration-id)
+         (let ((text (mapconcat (apply-partially
+                                 #'org-clubhouse--story-to-headline-text
+                                 level)
+                                (reject-archived story-list) "\n")))
+               (save-mark-and-excursion
+                 (insert text)
+                 (org-align-all-tags))
+             text))))))
+
+(defun org-clubhouse-headlines-from-query (level query)
+  "Create `org-mode' headlines from a clubhouse query.
+
+Submits QUERY to clubhouse, and creates `org-mode' headlines from all the
+resulting stories at headline level LEVEL."
+  (interactive
+   "*nLevel: \nMQuery: ")
+  (let* ((story-list (org-clubhouse--search-stories query)))
+    (if (null story-list)
+        (message "Query returned no stories: %s" query)
+      (let ((text (mapconcat (apply-partially
+                              #'org-clubhouse--story-to-headline-text
+                              level)
+                             (reject-archived story-list) "\n")))
+        (if (called-interactively-p)
+            (save-mark-and-excursion
+              (insert text)
+              (org-align-all-tags))
+          text)))))
+
+(defun org-clubhouse-prompt-for-story (cb)
+  "Prompt the user for a clubhouse story, then call CB with the full story."
+  (ivy-read "Story title: "
+            (lambda (search-term)
+              (let* ((stories (org-clubhouse--search-stories
+                               (if search-term (format "\"%s\"" search-term)
+                                 ""))))
+                (-map (lambda (story)
+                        (propertize (alist-get 'name story) 'story story))
+                      stories)))
+            :dynamic-collection t
+            :history 'org-clubhouse-story-prompt
+            :action (lambda (s) (funcall cb (get-text-property 0 'story s)))
+            :require-match t))
+
+(defun org-clubhouse-headline-from-story (level)
+  "Prompt for a story, and create an org headline at LEVEL from that story."
+  (interactive "*nLevel: ")
+  (org-clubhouse-prompt-for-story
+   (lambda (story)
+     (save-mark-and-excursion
+       (insert (org-clubhouse--story-to-headline-text level story))
+       (org-align-tags)))))
+
+
+(defun org-clubhouse-link ()
+  "Link the current `org-mode' headline with an existing clubhouse story."
+  (interactive)
+  (org-clubhouse-prompt-for-story
+   (lambda (story)
+     (org-clubhouse-populate-created-story
+      (org-element-find-headline)
+      story
+      :extra-properties '(("clubhouse-story-name" . name)))
+     (org-todo
+      (org-clubhouse-workflow-state-id-to-todo-keyword
+       (alist-get 'workflow_state_id story))))))
+
+(defun org-clubhouse-claim ()
+  "Assign the clubhouse story associated with the headline at point to yourself."
+  (interactive)
+  (if org-clubhouse-username
+      (and
+       (org-clubhouse-update-story-at-point
+        :owner_ids (list (org-clubhouse-whoami)))
+       (message "Successfully claimed story"))
+    (warn "Can't claim story if `org-clubhouse-username' is unset")))
+
+(defun org-clubhouse-sync-status (&optional beg end)
+  "Pull the status(es) for the story(ies) in region and update the todo state.
+
+Uses `org-clubhouse-state-alist'. Operates over stories from BEG to END"
+  (interactive
+   (when (use-region-p)
+     (list (region-beginning) (region-end))))
+  (let ((elts (-filter (lambda (e) (plist-get e :CLUBHOUSE-ID))
+                       (org-clubhouse-collect-headlines beg end))))
+    (save-mark-and-excursion
+      (dolist (e elts)
+        (goto-char (plist-get e :begin))
+        (let* ((clubhouse-id (org-element-extract-clubhouse-id e))
+               (story (org-clubhouse-get-story clubhouse-id))
+               (workflow-state-id (alist-get 'workflow_state_id story))
+               (todo-keyword (org-clubhouse-workflow-state-id-to-todo-keyword
+                              workflow-state-id)))
+          (let ((org-after-todo-state-change-hook
+                 (remove 'org-clubhouse-update-status
+                         org-after-todo-state-change-hook)))
+            (org-todo todo-keyword)))))
+    (message "Successfully synchronized status of %d stories from Clubhouse"
+             (length elts))))
+
+(cl-defun org-clubhouse-set-epic (&optional story-id epic-id cb &key beg end)
+  "Set the epic of clubhouse story STORY-ID to EPIC-ID, then call CB.
+
+When called interactively, prompt for an epic and set the story of the clubhouse
+stor{y,ies} at point or region"
+  (interactive
+   (when (use-region-p)
+     (list nil nil nil
+           :beg (region-beginning)
+           :end (region-end))))
+  (if (and story-id epic-id)
+      (progn
+        (org-clubhouse-update-story-internal
+         story-id :epic-id epic-id)
+        (when cb (funcall cb)))
+    (let ((elts (-filter (lambda (elt) (plist-get elt :CLUBHOUSE-ID))
+                         (org-clubhouse-collect-headlines beg end))))
+      (org-clubhouse-prompt-for-epic
+       (lambda (epic-id)
+         (-map
+          (lambda (elt)
+            (let ((story-id (org-element-extract-clubhouse-id elt)))
+              (org-clubhouse-set-epic
+               story-id epic-id
+               (lambda ()
+                 (org-set-property
+                  "clubhouse-epic"
+                  (org-link-make-string
+                   (org-clubhouse-link-to-epic epic-id)
+                   (alist-get epic-id (org-clubhouse-epics))))
+                 (message "Successfully set the epic on story %d to %d"
+                          story-id epic-id))))))
+         elts)))))
+
+;;;
+
+(define-minor-mode org-clubhouse-mode
+  "If enabled, updates to the todo keywords on org headlines will update the
+linked ticket in Clubhouse."
+  :group 'org
+  :lighter "Org-Clubhouse"
+  :keymap '()
+  (add-hook 'org-after-todo-state-change-hook
+            'org-clubhouse-update-status
+            nil
+            t))
+
+(provide 'org-clubhouse)
+
+;;; org-clubhouse.el ends here
diff --git a/users/grfn/resume/chimera.png b/users/grfn/resume/chimera.png
new file mode 100644
index 0000000000..6dde989c53
--- /dev/null
+++ b/users/grfn/resume/chimera.png
Binary files differdiff --git a/users/grfn/resume/collection.sty b/users/grfn/resume/collection.sty
new file mode 100644
index 0000000000..4f1540a9d2
--- /dev/null
+++ b/users/grfn/resume/collection.sty
@@ -0,0 +1,85 @@
+%% start of file `collection.sty'.

+%% Copyright 2013-2013 Xavier Danaux (xdanaux@gmail.com).

+%

+% This work may be distributed and/or modified under the

+% conditions of the LaTeX Project Public License version 1.3c,

+% available at http://www.latex-project.org/lppl/.

+

+

+%-------------------------------------------------------------------------------

+%                identification

+%-------------------------------------------------------------------------------

+\NeedsTeXFormat{LaTeX2e}

+\ProvidesPackage{collection}[2013/03/28 v1.0.0 collections]

+

+

+%-------------------------------------------------------------------------------

+%                requirements

+%-------------------------------------------------------------------------------

+

+

+\RequirePackage{ifthen}

+

+

+%-------------------------------------------------------------------------------

+%                code

+%-------------------------------------------------------------------------------

+

+% creates a new collection

+% usage: \collectionnew{<collection name>}

+\newcommand*{\collectionnew}[1]{%

+  \newcounter{collection@#1@count}}

+

+% adds an item to a collection

+% usage: \collectionadd[<optional key>]{<collection name>}{<item to add>}

+\newcommand*{\collectionadd}[3][]{%

+  \expandafter\def\csname collection@#2@item\roman{collection@#2@count}\endcsname{#3}%

+  \if\relax\noexpand#1\relax% if #1 is empty

+    \else\expandafter\def\csname collection@#2@key\roman{collection@#2@count}\endcsname{#1}\fi%

+  \stepcounter{collection@#2@count}}

+

+% returns the number of items in a collection

+% usage: \collectioncount{<collection name>}

+\newcommand*{\collectioncount}[1]{%

+  \value{collection@#1@count}}

+

+% gets an item from a collection

+% usage: \collectiongetitem{<collection name>}{<element id>}

+% where <element id> is an integer between 0 and (collectioncount-1)

+\newcommand*{\collectiongetitem}[2]{%

+  \csname collection@#1@item\romannumeral #2\endcsname}

+

+% gets a key from a collection

+% usage: \collectiongetkey{<collection name>}{<element id>}

+% where <element id> is an integer between 0 and (collectioncount-1)

+\newcommand*{\collectiongetkey}[2]{%

+  \csname collection@#1@key\romannumeral #2\endcsname}

+

+% loops through a collection and perform the given operation on every element

+% usage: \collectionloop{<collection name>}{<operation sequence>}

+% where <operation sequence> is the code sequence to be evaluated for each collection item,

+%   code which can refer to \collectionloopid, \collectionloopkey, \collectionloopitem and

+%   \collectionloopbreak

+\newcounter{collection@iterator}

+\newcommand*{\collectionloopbreak}{\let\iterate\relax}

+\newcommand*{\collectionloop}[2]{%

+  \setcounter{collection@iterator}{0}%

+  \loop\ifnum\value{collection@iterator}<\value{collection@#1@count}%

+    \def\collectionloopid{\arabic{collection@iterator}}%

+    \def\collectionloopitem{\collectiongetitem{#1}{\collectionloopid}}%

+    \def\collectionloopkey{\collectiongetkey{#1}{\collectionloopid}}%

+    #2%

+    \stepcounter{collection@iterator}%

+    \repeat}

+

+% loops through a collection and finds the (first) element matching the given key

+% usage: \collectionfindbykey{<collection name>}{key>}

+\newcommand*{\collectionfindbykey}[2]{%

+  \collectionloop{#1}{%

+    \ifthenelse{\equal{\collectionloopkey}{#2}}{\collectionloopitem\collectionloopbreak}{}}}

+

+

+\endinput

+

+

+%% end of file `collection.cls'.

diff --git a/users/grfn/resume/default.nix b/users/grfn/resume/default.nix
new file mode 100644
index 0000000000..21801ad9e7
--- /dev/null
+++ b/users/grfn/resume/default.nix
@@ -0,0 +1,40 @@
+{ pkgs, ... }:
+
+with pkgs.lib;
+
+pkgs.runCommandNoCC "resume.pdf"
+{
+  buildInputs = [
+    (pkgs.texlive.combine {
+      inherit (pkgs.texlive)
+        capt-of
+        collection-fontsrecommended
+        enumitem
+        etoolbox
+        fancyvrb
+        float
+        fncychap
+        framed
+        l3packages
+        microtype
+        needspace
+        parskip
+        scheme-basic
+        tabulary
+        titlesec
+        ulem
+        upquote
+        varwidth
+        wrapfig
+        xcolor
+        ;
+    })
+  ];
+} ''
+  cp ${builtins.filterSource (path: type:
+    type == "regular" &&
+    any (ext: hasSuffix ext path) [".sty" ".cls" ".tex" ".png"]
+  ) ./.}/* .
+  pdflatex ./resume.tex
+  cp resume.pdf $out
+''
diff --git a/users/grfn/resume/helvetica.sty b/users/grfn/resume/helvetica.sty
new file mode 100644
index 0000000000..dacc129a10
--- /dev/null
+++ b/users/grfn/resume/helvetica.sty
@@ -0,0 +1,32 @@
+%% 
+%% This is file `helvetica.sty', based on helvet.sty extended to include 
+%% definitions for rm and tt.  This means commands such as \textbf, \textit,
+%% etc. will appear in Helvetica.  
+%% Changes added by Harriet Borton on <1995/12/11> 
+%% 
+%% The original source files were:
+%% 
+%% psfonts.dtx  (with options: `helvet')
+%% 
+%% Copyright (C) 1994 Sebastian Rahtz 
+%% All rights reserved. 
+%% 
+%% The original file is part of the PSNFSS2e package. 
+%% ----------------------------------------- 
+%% 
+%% This is a generated file. Permission is granted to to customize the 
+%% declarations in this file to serve the needs of your installation. 
+%% However, no permission is granted to distribute a modified version of 
+%% this file under its original name. 
+\def\fileversion{4.2}
+\def\filedate{94/11/11}
+\def\docdate {94/11/06}
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{helvetica}[\filedate\space\fileversion\space
+Helvetica PSNFSS2e package]
+\renewcommand{\sfdefault}{phv}
+\renewcommand{\rmdefault}{phv}
+\renewcommand{\ttdefault}{pcr}
+\endinput
+%% 
+%% End of file `helvetica.sty'.
diff --git a/users/grfn/resume/moderncv.cls b/users/grfn/resume/moderncv.cls
new file mode 100644
index 0000000000..a40f807337
--- /dev/null
+++ b/users/grfn/resume/moderncv.cls
@@ -0,0 +1,585 @@
+%% start of file `moderncv.cls'.

+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).

+%

+% This work may be distributed and/or modified under the

+% conditions of the LaTeX Project Public License version 1.3c,

+% available at http://www.latex-project.org/lppl/.

+

+

+%-------------------------------------------------------------------------------

+%                identification

+%-------------------------------------------------------------------------------

+\NeedsTeXFormat{LaTeX2e}

+\ProvidesClass{moderncv}[2013/02/09 v1.3.0 modern curriculum vitae and letter document class]

+

+

+%-------------------------------------------------------------------------------

+%                class options

+%

+% (need to be done before the external package loading, for example because

+% we need \paperwidth, \paperheight and \@ptsize to be defined before loading

+% geometry and fancyhdr)

+%-------------------------------------------------------------------------------

+% paper size option

+\DeclareOption{a4paper}{

+  \setlength\paperheight{297mm}

+  \setlength\paperwidth{210mm}}

+\DeclareOption{a5paper}{

+  \setlength\paperheight{210mm}

+  \setlength\paperwidth{148mm}}

+\DeclareOption{b5paper}{

+  \setlength\paperheight{250mm}

+  \setlength\paperwidth{176mm}}

+\DeclareOption{letterpaper}{

+  \setlength\paperheight{11in}

+  \setlength\paperwidth{8.5in}}

+\DeclareOption{legalpaper}{

+  \setlength\paperheight{14in}

+  \setlength\paperwidth{8.5in}}

+\DeclareOption{executivepaper}{

+  \setlength\paperheight{10.5in}

+  \setlength\paperwidth{7.25in}}

+\DeclareOption{landscape}{

+  \setlength\@tempdima{\paperheight}

+  \setlength\paperheight{\paperwidth}

+  \setlength\paperwidth{\@tempdima}}

+

+% font size options

+\newcommand\@ptsize{}

+\DeclareOption{10pt}{\renewcommand\@ptsize{0}}

+\DeclareOption{11pt}{\renewcommand\@ptsize{1}}

+\DeclareOption{12pt}{\renewcommand\@ptsize{2}}

+

+% font type options

+\DeclareOption{sans}{\AtBeginDocument{\renewcommand{\familydefault}{\sfdefault}}}

+\DeclareOption{roman}{\AtBeginDocument{\renewcommand{\familydefault}{\rmdefault}}}

+

+% draft/final option

+\DeclareOption{draft}{\setlength\overfullrule{5pt}}

+\DeclareOption{final}{\setlength\overfullrule{0pt}}

+

+% execute default options

+\ExecuteOptions{a4paper,11pt,final}

+

+% process given options

+\ProcessOptions\relax

+\input{size1\@ptsize.clo}

+

+

+%-------------------------------------------------------------------------------

+%                required packages

+%-------------------------------------------------------------------------------

+% \AtEndPreamble hook (loading etoolbox instead of defining the macro, as to avoid incompatibilities with etoolbox (and packages relying on it) defining the macro too)

+\RequirePackage{etoolbox}

+%\let\@endpreamblehook\@empty

+%\def\AtEndPreamble{\g@addto@macro\@endpreamblehook}

+%\let\document@original\document

+%\def\document{\endgroup\@endpreamblehook\begingroup\document@original}

+

+% if... then... else... constructs

+\RequirePackage{ifthen}

+% TODO: move to xifthen and \isempty{<arg>} instead of \equal{<arg>}{}

+

+% color

+\RequirePackage{xcolor}

+

+% font loading

+%\RequirePackage{ifxetex,ifluatex}

+%\newif\ifxetexorluatex

+%\ifxetex

+%  \xetexorluatextrue

+%\else

+%  \ifluatex

+%    \xetexorluatextrue

+%  \else

+%    \xetexorluatexfalse

+%  \fi

+%\fi

+% automatic loading of latin modern fonts

+%\ifxetexorluatex

+%  \RequirePackage{fontspec}

+%  \defaultfontfeatures{Ligatures=TeX}

+%  \RequirePackage{unicode-math}

+%  \setmainfont{Latin Modern}

+%  \setsansfont{Latin Modern Sans}

+%  \setmathfont{Latin Modern Math}

+%\else

+  \RequirePackage[T1]{fontenc}

+  \IfFileExists{lmodern.sty}%

+    {\RequirePackage{lmodern}}%

+    {}

+%\fi

+

+% hyper links (hyperref is loaded at the end of the preamble to pass options required by loaded packages such as CJK)

+\newcommand*\pdfpagemode{UseNone}% do not show thumbnails or bookmarks on opening (on supporting browsers); set \pdfpagemode to "UseOutlines" to show bookmarks

+\RequirePackage{url}

+\urlstyle{tt}

+\AtEndPreamble{

+  \pagenumbering{arabic}% has to be issued before loading hyperref, as to set \thepage and hence to avoid hyperref issuing a warning and setting pdfpagelabels=false

+  \RequirePackage[unicode]{hyperref}% unicode is required for unicode pdf metadata

+  \hypersetup{

+    breaklinks,

+    baseurl       = http://,

+    pdfborder     = 0 0 0,

+    pdfpagemode   = \pdfpagemode,

+    pdfstartpage  = 1,

+    pdfcreator    = {\LaTeX{} with 'moderncv' package},

+%    pdfproducer   = {\LaTeX{}},% will/should be set automatically to the correct TeX engine used

+    bookmarksopen = true,

+    bookmarksdepth= 2,% to show sections and subsections

+    pdfauthor     = {\@firstname{}~\@lastname{}},

+    pdftitle      = {\@firstname{}~\@lastname{} -- \@title{}},

+    pdfsubject    = {Resum\'{e} of \@firstname{}~\@lastname{}},

+    pdfkeywords   = {\@firstname{}~\@lastname{}, curriculum vit\ae{}, resum\'{e}}}}

+

+% graphics

+\RequirePackage{graphicx}

+

+% headers and footers

+\RequirePackage{fancyhdr}

+\fancypagestyle{plain}{

+  \renewcommand{\headrulewidth}{0pt}

+  \renewcommand{\footrulewidth}{0pt}

+  \fancyhf{}}

+% page numbers in footer if more than 1 page

+\newif\if@displaypagenumbers\@displaypagenumberstrue

+\newcommand*{\nopagenumbers}{\@displaypagenumbersfalse}

+\AtEndPreamble{%

+  \AtBeginDocument{%

+    \if@displaypagenumbers%

+      \@ifundefined{r@lastpage}{}{%

+        \ifthenelse{\pageref{lastpage}>1}{%

+          \newlength{\pagenumberwidth}%

+          \settowidth{\pagenumberwidth}{\color{color2}\addressfont\itshape\strut\thepage/\pageref{lastpage}}%

+          \fancypagestyle{plain}{%

+            \fancyfoot[r]{\parbox[b]{\pagenumberwidth}{\color{color2}\pagenumberfont\strut\thepage/\pageref{lastpage}}}}% the parbox is required to ensure alignment with a possible center footer (e.g., as in the casual style)

+          \pagestyle{plain}}{}}%

+      \AtEndDocument{\label{lastpage}}\else\fi}}

+\pagestyle{plain}

+

+% reduced list spacing

+% package providing hooks into lists

+%   originally developped by Jakob Schiøtz (see http://dcwww.camd.dtu.dk/~schiotz/comp/LatexTips/tweaklist.sty)

+%   modified and distributed with moderncv(not available otherwise on ctan)

+\RequirePackage{tweaklist}

+\renewcommand*{\itemhook}{%

+  \@minipagetrue% removes spacing before lists as they use \addvspace, which doesn't add vertical space inside minipages

+  \@noparlisttrue% removes spacing at end of lists, caused by \par

+  \setlength{\topsep}{0pt}% normally not required thanks to \@minipagetrue

+  \setlength{\partopsep}{0pt}% normally not required thanks to \@minipagetrue

+  \setlength{\parsep}{0pt}% not required when \itemsep and \parskip are set to 0pt (?)

+  \setlength{\parskip}{0pt}%

+  \setlength{\itemsep}{0pt}}

+\renewcommand*{\enumhook}{\itemhook{}}

+\renewcommand*{\deschook}{\itemhook{}}

+

+% lengths calculations

+\RequirePackage{calc}

+

+% advanced command arguments (LaTeX 3)

+\RequirePackage{xparse}

+% TODO (?): replace all \newcommand by \NewDocumentCommand

+

+% micro-typography (e.g., character protrusion, font expansion, hyphenatable letterspacing)

+\RequirePackage{microtype}

+

+% compatibility package with older versions of moderncv

+\RequirePackageWithOptions{moderncvcompatibility}

+

+

+%-------------------------------------------------------------------------------

+%                class definition

+%-------------------------------------------------------------------------------

+% minimal base settings

+\setlength\lineskip{1\p@}

+\setlength\normallineskip{1\p@}

+\renewcommand\baselinestretch{}

+\setlength{\parindent}{0\p@}

+\setlength{\parskip}{0\p@}

+\setlength\columnsep{10\p@}

+\setlength\columnseprule{0\p@}

+\setlength\fboxsep{3\p@}

+\setlength\fboxrule{.4\p@}

+\setlength\arrayrulewidth{.4\p@}

+\setlength\doublerulesep{2\p@}

+

+% not set on purpose

+%\setlength\arraycolsep{5\p@}

+%\setlength\tabcolsep{6\p@}

+%\setlength\tabbingsep{\labelsep}

+

+\raggedbottom

+\onecolumn

+

+

+%-------------------------------------------------------------------------------

+%                overall design commands definitions

+%-------------------------------------------------------------------------------

+% elements

+% defines one's name

+% usage: \name{<firstname>}{<lastname>}

+\newcommand*{\name}[2]{\def\@firstname{#1}\def\@lastname{#2}}

+% defines one's title (optional)

+% usage: \title{<title>}

+\renewcommand*{\title}[1]{\def\@title{#1}}

+% defines one's address (optional)

+% usage: \address{<street>}{<city>}{<country>}

+% where the <city> and <country> arguments can be omitted or provided empty

+\NewDocumentCommand{\address}{mG{}G{}}{\def\@addressstreet{#1}\def\@addresscity{#2}\def\@addresscountry{#3}}

+% adds a mobile/fixed/fax number to one's personal information (optional)

+% usage: \phone[<optional type>]{<number>}

+% where <optional type> should be either "mobile", "fixed" or "fax

+\RequirePackage{collection}

+\collectionnew{phones}

+\newcommand*{\phone}[2][fixed]{\collectionadd[#1]{phones}{#2}}

+\newcommand*{\email}[1]{\def\@email{#1}}

+% defines one's home page (optional)

+% usage: \homepage{<url>}

+\newcommand*{\homepage}[1]{\def\@homepage{#1}}

+% defines one's github (optional)

+% usage: \homepage{<url>}

+\newcommand*{\github}[1]{\def\@github{#1}}

+% defines additional personal information (optional)

+% usage: \extrainfo{<text>}

+\newcommand*{\extrainfo}[1]{\def\@extrainfo{#1}}

+

+% colors

+\definecolor{color0}{rgb}{0,0,0}% main default color, normally left to black

+\definecolor{color1}{rgb}{0,0,0}% primary theme color

+\definecolor{color2}{rgb}{0,0,0}% secondary theme color

+\definecolor{color3}{rgb}{0,0,0}% tertiary theme color

+

+% symbols

+%   itemize labels (the struts were added to correct inter-item spacing (works for single line items, until a solution is found for multi-line ones...)

+\newcommand*{\labelitemi}{\strut\textcolor{color1}{\large\rmfamily\textbullet}}% the \rmfamily is required to force Latin Modern fonts when using sans serif, as OMS/lmss/m/n is not defined and gets substituted by OMS/cmsy/m/n

+\newcommand*{\labelitemii}{\strut\textcolor{color1}{\large\bfseries-}}

+\newcommand*{\labelitemiii}{\strut\textcolor{color1}{\rmfamily\textperiodcentered}}% alternative: \textasteriskcentered; the \rmfamily is required to force Latin Modern fonts when using sans serif, as OMS/lmss/m/n is not defined and gets substituted by OMS/cmsy/m/n

+\newcommand*{\labelitemiv}{\labelitemiii}

+%   enumerate labels

+\renewcommand{\theenumi}{\@arabic\c@enumi}

+\renewcommand{\theenumii}{\@alph\c@enumii}

+\renewcommand{\theenumiii}{\@roman\c@enumiii}

+\renewcommand{\theenumiv}{\@Alph\c@enumiv}

+%   other symbols

+\newcommand*{\listitemsymbol}{\labelitemi~}

+\newcommand*{\addresssymbol}{}

+\newcommand*{\mobilephonesymbol}{}

+\newcommand*{\fixedphonesymbol}{}

+\newcommand*{\faxphonesymbol}{}

+\newcommand*{\emailsymbol}{}

+\newcommand*{\homepagesymbol}{}

+

+% fonts

+\AtBeginDocument{\normalfont\color{color0}}

+

+% strings for internationalisation

+\newcommand*{\refname}{Publications}

+\newcommand*{\enclname}{Enclosure}

+

+% makes the footer (normally used both for the resume and the letter)

+% usage: \makefooter

+\newcommand*{\makefooter}{}%

+

+% loads a style variant

+% usage: \moderncvstyle{<style variant name>}

+\newcommand*{\moderncvstyle}[1]{

+  \RequirePackage{moderncvstyle#1}}

+  

+% loads a color scheme

+% usage: \moderncvcolor{<color scheme name>}

+\newcommand*{\moderncvcolor}[1]{

+  \RequirePackage{moderncvcolor#1}}

+

+% loads an icons set

+% usage: \moderncvicons{<icon set name>}

+\newcommand*{\moderncvicons}[1]{

+  \RequirePackage{moderncvicons#1}}

+

+% recomputes all automatic lengths

+\newcommand*{\recomputelengths}{\recomputecvlengths}

+\AtBeginDocument{\recomputelengths{}}

+

+% creates a length if not yet defined

+\newcommand*{\@initializelength}[1]{%

+  \ifdefined#1\else\newlength{#1}\fi}

+

+

+%-------------------------------------------------------------------------------

+%                resume design commands definitions

+%-------------------------------------------------------------------------------

+% elements

+% defines one's picture (optional)

+% usage: photo[<picture width>][<picture frame thickness>]{<picture filename>}

+\NewDocumentCommand{\photo}{O{64pt}O{0.4pt}m}{\def\@photowidth{#1}\def\@photoframewidth{#2}\def\@photo{#3}}

+\newcommand*{\quote}[1]{\def\@quote{#1}}

+

+% fonts

+\newcommand*{\namefont}{}

+\newcommand*{\titlefont}{}

+\newcommand*{\addressfont}{}

+\newcommand*{\quotefont}{}

+\newcommand*{\sectionfont}{}

+\newcommand*{\subsectionfont}{}

+\newcommand*{\hintfont}{}

+\newcommand*{\pagenumberfont}{\addressfont\itshape}

+

+% styles

+\newcommand*{\namestyle}[1]{{\namefont#1}}

+\newcommand*{\titlestyle}[1]{{\titlefont#1}}

+\newcommand*{\addressstyle}[1]{{\addressfont#1}}

+\newcommand*{\quotestyle}[1]{{\quotefont#1}}

+\newcommand*{\sectionstyle}[1]{{\sectionfont#1}}

+\newcommand*{\subsectionstyle}[1]{{\subsectionfont#1}}

+\newcommand*{\hintstyle}[1]{{\hintfont#1}}

+\newcommand*{\pagenumberstyle}[1]{{\pagenumberfont#1}}

+

+% recompute all resume lengths

+\newcommand*{\recomputecvlengths}{}

+

+% internal maketitle command to issue a new line only when required

+\newif\if@firstdetailselement\@firstdetailselementtrue

+\newcommand*{\makenewline}{

+  \if@firstdetailselement%

+    \strut% to ensure baseline alignment, e.g. with when put in the margin vs sections that also contains a \strut

+  \else%

+    \\\fi%

+  \@firstdetailselementfalse}

+

+% makes the resume title

+% usage: \makecvtitle

+\newcommand*{\makecvtitle}{}

+

+% makes the resume footer

+% usage: \makecvfooter

+\newcommand*{\makecvfooter}{\makefooter}

+

+% makes a resume section

+% usage: \section{<title>}

+% identical starred and non-starred variants should be defined for compatibility with other packages (e.g. with natbib, that uses \section*{} for the bibliography header)

+\NewDocumentCommand{\section}{sm}{}

+

+% makes a resume subsection

+% usage: \subsection{title}

+\NewDocumentCommand{\subsection}{sm}{}

+

+% makes a resume line with a header and a corresponding text

+% usage: \cvitem[spacing]{header}{text}

+\newcommand*{\cvitem}[3][.25em]{}

+

+% makes a resume line 2 headers and their corresponding text

+% usage: \cvdoubleitem[spacing]{header1}{text1}{header2}{text2}

+\newcommand*{\cvdoubleitem}[5][.25em]{}

+

+% makes a resume line with a list item

+% usage: \cvlistitem[label]{item}

+\newcommand*{\cvlistitem}[2][\listitemsymbol]{}

+

+% makes a resume line with 2 list items

+% usage: \cvlistdoubleitem[label]{item1}{item2}

+\newcommand*{\cvlistdoubleitem}[3][\listitemsymbol]{}

+

+% makes a typical resume job / education entry

+% usage: \cventry[spacing]{years}{degree/job title}{institution/employer}{localization}{optionnal: grade/...}{optional: comment/job description}

+\newcommand*{\cventry}[7][.25em]{}

+

+% makes a resume entry with a proficiency comment

+% usage: \cvitemwithcomment[spacing]{header}{text}{comment}

+\newcommand*{\cvitemwithcomment}[4][.25em]{}

+

+% makes a generic hyperlink

+% usage: \link[optional text]{link}

+\newcommand*{\link}[2][]{%

+  \ifthenelse{\equal{#1}{}}%

+    {\href{#2}{#2}}%

+    {\href{#2}{#1}}}

+

+% makes a http hyperlink

+% usage: \httplink[optional text]{link}

+\newcommand*{\httplink}[2][]{%

+  \ifthenelse{\equal{#1}{}}%

+    {\href{http://#2}{#2}}%

+    {\href{http://#2}{#1}}}

+

+% makes an email hyperlink

+% usage: \emaillink[optional text]{link}

+\newcommand*{\emaillink}[2][]{%

+  \ifthenelse{\equal{#1}{}}%

+    {\href{mailto:#2}{#2}}%

+    {\href{mailto:#2}{#1}}}

+

+% cvcolumns environment, where every column is created through \cvcolumn

+% usage: \begin{cvcolumns}

+%          \cvcolumn[width]{head}{content}

+%          \cvcolumn[width]{head}{content}

+%          ...

+%        \end{cvcolumns}

+% where "width" is the width as a fraction of the line length (between 0 and 1), "head" is the column header and "content" its content

+\newcounter{cvcolumnscounter}% counter for the number of columns

+\newcounter{cvcolumnsautowidthcounter}% counter for the number of columns with no column width provided, and which will then be equally distributed

+\newcounter{tmpiteratorcounter}% counter for any temporary purpose (e.g., iterating loops)

+\newlength{\cvcolumnsdummywidth}\setlength{\cvcolumnsdummywidth}{1000pt}% dummy width for total width, in order to enable arithmetics (TeX has no float variables, only integer counters or lengths)

+\newlength{\cvcolumnswidth}% total width available for head / content

+\newlength{\cvcolumnsautowidth}% total width of columns with no explicit width provided

+\newlength{\cvcolumnautowidth}% width of one of the columns with no explicit width provided (based on equal distribution of remaining space)

+\newif\if@cvcolumns@head@empty% whether or not at least one of the columns has a header

+\newenvironment*{cvcolumns}%

+  {% at environment opening: reset counters, lengths and ifs

+    \setcounter{cvcolumnscounter}{0}%

+    \setcounter{cvcolumnsautowidthcounter}{0}%

+    \setlength{\cvcolumnsautowidth}{\cvcolumnsdummywidth}%

+    \setlength{\cvcolumnautowidth}{0pt}%

+    \@cvcolumns@head@emptytrue}%

+  {% at environment closing: typeset environment

+    % compute the width of each cvcolumn, considering a spacing of \separatorcolumnwidth and the columns with set width

+    \ifnum\thecvcolumnscounter>0%

+      \setlength{\cvcolumnswidth}{\maincolumnwidth-\value{cvcolumnscounter}\separatorcolumnwidth+\separatorcolumnwidth}%

+      \setlength{\cvcolumnautowidth}{\cvcolumnswidth*\ratio{\cvcolumnsautowidth}{\cvcolumnsdummywidth}/\value{cvcolumnsautowidthcounter}}\fi%

+    % pre-aggregate the tabular definition, heading and content (required before creating the tabular, as the tabular environment doesn't like loops --- probably because "&" generates a \endgroup)

+    % - the tabular definition is the aggregation of the different "\cvcolumn<i>@def" (by default "p{\cvcolumnautowidth}"), separated by "@{\hspace*{\separatorcolumnwidth}}"

+    % - the tabular heading is the aggregation of the different "\cvcolumn<i>@head", separated by "&"

+    % - the tabular content is the aggregation of the different "\cvcolumn<i>@content", separated by "&"

+    % to aggregate the different elements, \protected@edef or \g@addto@macro is required to avoid that \cvcolumns@def, -@head and -@content get expanded in subsequent redefinitions, which would cause errors due to the expansions of \hspace, of \subsectionstyle and possibly of user content/argument such as font commands

+    \def\cvcolumns@def{}%

+    \def\cvcolumns@head{}%

+    \def\cvcolumns@content{}%

+    \setcounter{tmpiteratorcounter}{0}%

+    % loop based on \g@addto@macro

+    \loop\ifnum\thetmpiteratorcounter<\thecvcolumnscounter%

+      \ifnum\thetmpiteratorcounter=0\else%

+        \g@addto@macro\cvcolumns@def{@{\hspace*{\separatorcolumnwidth}}}%

+        \g@addto@macro\cvcolumns@head{&}%

+        \g@addto@macro\cvcolumns@content{&}\fi%

+      \expandafter\g@addto@macro\expandafter\cvcolumns@def\expandafter{\csname cvcolumn\roman{tmpiteratorcounter}@def\endcsname}%

+      \expandafter\g@addto@macro\expandafter\cvcolumns@head\expandafter{\csname cvcolumn\roman{tmpiteratorcounter}@head\endcsname}%

+      \expandafter\g@addto@macro\expandafter\cvcolumns@content\expandafter{\csname cvcolumn\roman{tmpiteratorcounter}@content\endcsname}%

+      \stepcounter{tmpiteratorcounter}%

+      \repeat%

+%    % same loop based on \protected@edef

+%    \loop\ifnum\thetmpiteratorcounter<\thecvcolumnscounter%

+%      \ifnum\thetmpiteratorcounter=0\else%

+%        \protected@edef\cvcolumns@def{\cvcolumns@def @{\hspace*{\separatorcolumnwidth}}}%

+%        \protected@edef\cvcolumns@head{\cvcolumns@head &}%

+%        \protected@edef\cvcolumns@content{\cvcolumns@content &}\fi%

+%      \expandafter\protected@edef\expandafter\cvcolumns@def\expandafter{\expandafter\cvcolumns@def\expandafter\protect\csname cvcolumn\roman{tmpiteratorcounter}@def\endcsname}%

+%      \expandafter\protected@edef\expandafter\cvcolumns@head\expandafter{\expandafter\cvcolumns@head\expandafter\protect\csname cvcolumn\roman{tmpiteratorcounter}@head\endcsname}%

+%      \expandafter\protected@edef\expandafter\cvcolumns@content\expandafter{\expandafter\cvcolumns@content\expandafter\protect\csname cvcolumn\roman{tmpiteratorcounter}@content\endcsname}%

+%      \stepcounter{tmpiteratorcounter}%

+%      \repeat%

+    % create the tabular

+    \cvitem{}{%

+      \begin{tabular}{\cvcolumns@def}%

+        \if@cvcolumns@head@empty\else%

+          \cvcolumns@head\\[-.8em]%

+          {\color{color1}\rule{\maincolumnwidth}{.25pt}}\\\fi%

+        \cvcolumns@content%

+      \end{tabular}}}

+

+% cvcolumn command, to create a column inside a cvcolumns environment

+% usage: \cvcolumn[width]{head}{content}

+% where "width" is the width as a fraction of the line length (between 0 and 1), "head" is the column header and "content" its content ("head" and "content" can contain "\\", "\newline" or any other paragraph command such as "itemize")

+\newcommand*{\cvcolumn}[3][\cvcolumnautowidth]{%

+%  \def\cvcolumn@width{}%

+  \ifthenelse{\equal{#1}{\cvcolumnautowidth}}%

+    {% if no width fraction is provided, count this column as auto-adjusted and set its width to \cvcolumnsautowidth

+      \stepcounter{cvcolumnsautowidthcounter}%

+      \expandafter\expandafter\expandafter\def\expandafter\csname cvcolumn\roman{cvcolumnscounter}@def\endcsname{p{\cvcolumnautowidth}}%

+      \expandafter\expandafter\expandafter\def\expandafter\csname cvcolumn\roman{cvcolumnscounter}@head\endcsname{\protect\parbox[b]{\cvcolumnautowidth}{\protect\subsectionstyle{#2}}}}%

+    {% if a width is provided, set the width of the column to it and decrease the available space for auto-adjusted columns

+      \addtolength{\cvcolumnsautowidth}{-#1\cvcolumnsdummywidth}%

+      \expandafter\expandafter\expandafter\def\expandafter\csname cvcolumn\roman{cvcolumnscounter}@def\endcsname{p{#1\cvcolumnswidth}}%

+      \expandafter\expandafter\expandafter\def\expandafter\csname cvcolumn\roman{cvcolumnscounter}@head\endcsname{\protect\parbox[b]{#1\cvcolumnswidth}{\protect\subsectionstyle{#2}}}}%

+  \ifthenelse{\equal{#2}{}}{}{\@cvcolumns@head@emptyfalse}%

+  \expandafter\expandafter\expandafter\def\expandafter\csname cvcolumn\roman{cvcolumnscounter}@content\endcsname{\protect\cvcolumncell{#3}}%

+  \stepcounter{cvcolumnscounter}}

+

+% internal cvcolumncell command, that enables a cvcolumn cell to contain paragraph commands (lists, newlines, etc)

+\newcommand*{\cvcolumncell}[1]{{% put cell inside a group, so that command redefinitions are only local

+  % roughly restore \\ to its regular definition (outside of tabular)

+  \renewcommand*{\\}{\newline}%

+  % enclose the contents of the cell inside a vertical box, to allow paragraph commands

+  \protect\vtop{#1}}}

+

+% thebibliography environment, for use with BibTeX and possibly multibib

+\newlength{\bibindent}

+\setlength{\bibindent}{1.5em}

+% bibliography item label

+\newcommand*{\bibliographyitemlabel}{}% use \@biblabel{\arabic{enumiv}} for BibTeX labels

+%\newif\if@multibibfirstbib\@multibibfirstbibfalse

+% bibliography head (section, etc}, depending on whether multibib is used

+\newcommand*{\bibliographyhead}[1]{\section{#1}}

+\AtEndPreamble{\@ifpackageloaded{multibib}{\renewcommand*{\bibliographyhead}[1]{\subsection{#1}}}{}}

+% thebibliography environment definition

+\newenvironment{thebibliography}[1]{}{}

+\newcommand*{\newblock}{\hskip .11em\@plus.33em\@minus.07em}

+\let\@openbib@code\@empty

+

+% itemize, enumerate and description environment

+\setlength{\leftmargini}   {1em}

+\leftmargin\leftmargini

+\setlength{\leftmarginii}  {\leftmargini}

+\setlength{\leftmarginiii} {\leftmargini}

+\setlength{\leftmarginiv}  {\leftmargini}

+\setlength{\leftmarginv}   {\leftmargini}

+\setlength{\leftmarginvi}  {\leftmargini}

+\setlength{\labelsep}      {.5em}% this is the distance between the label and the body, but it pushes the label to the left rather than pushing the body to the right (to do the latter, modify \leftmargin(i)

+\setlength{\labelwidth}    {\leftmargini}% unfortunately, \labelwidth is not defined by item level (i.e. no \labeliwidth, \labeliiwidth, etc)

+\addtolength{\labelwidth}  {-\labelsep}

+\@beginparpenalty -\@lowpenalty

+\@endparpenalty   -\@lowpenalty

+\@itempenalty     -\@lowpenalty

+\newcommand\labelenumi{\theenumi.}

+\newcommand\labelenumii{(\theenumii)}

+\newcommand\labelenumiii{\theenumiii.}

+\newcommand\labelenumiv{\theenumiv.}

+\renewcommand\p@enumii{\theenumi}

+\renewcommand\p@enumiii{\p@enumii(\theenumii)}

+\renewcommand\p@enumiv{\p@enumiii\theenumiii}

+% description label

+\newcommand*\descriptionlabel[1]{\hspace\labelsep\normalfont\bfseries#1}

+

+% classical \today definition

+\def\today{\ifcase\month\or

+  January\or February\or March\or April\or May\or June\or

+  July\or August\or September\or October\or November\or December\fi

+  \space\number\day, \number\year}

+

+%\newcommand{\widthofautobox}[1]{%

+%  \widthof{\begin{tabular}{@{}l@{}}#1\end{tabular}}}

+

+%\newcommand{\autobox}[2][b]{%

+%  \parbox[#1]{\widthofautobox{#2}}{#2}}

+

+

+%-------------------------------------------------------------------------------

+%                letter design commands definitions

+%-------------------------------------------------------------------------------

+% elements

+\newcommand*{\recipient}[2]{\def\@recipientname{#1}\def\@recipientaddress{#2}}

+\renewcommand*{\date}[1]{\def\@date{#1}}\date{\today}

+\newcommand*{\opening}[1]{\def\@opening{#1}}

+\newcommand*{\closing}[1]{\def\@closing{#1}}

+\newcommand*{\enclosure}[2][]{%

+  % if an optional argument is provided, use it to redefine \enclname

+  \ifthenelse{\equal{#1}{}}{}{\renewcommand*{\enclname}{#1}}%

+  \def\@enclosure{#2}}

+

+% recompute all letter lengths

+\newcommand*{\recomputeletterlengths}{}

+

+% makes the letter title

+% usage: \makelettertitle

+\newcommand*{\makelettertitle}{}

+

+% makes the letter footer

+% usage: \makeletterfooter

+\newcommand*{\makeletterfooter}{\makefooter}

+

+% makes the letter closing

+% usage: \makeletterclosing

+\newcommand*{\makeletterclosing}{}

+

+

+\endinput

+

+

+%% end of file `moderncv.cls'.

diff --git a/users/grfn/resume/moderncvcolorblack.sty b/users/grfn/resume/moderncvcolorblack.sty
new file mode 100644
index 0000000000..3a6e1477f3
--- /dev/null
+++ b/users/grfn/resume/moderncvcolorblack.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolorblack.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolorblack}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: black]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0,0,0}% black
+\definecolor{color2}{rgb}{0,0,0}% black
+
+
+\endinput
+
+
+%% end of file `moderncvcolorblack.sty'.
diff --git a/users/grfn/resume/moderncvcolorblue.sty b/users/grfn/resume/moderncvcolorblue.sty
new file mode 100644
index 0000000000..7b949c704a
--- /dev/null
+++ b/users/grfn/resume/moderncvcolorblue.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolorblue.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolorblue}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: blue]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0.22,0.45,0.70}% light blue
+\definecolor{color2}{rgb}{0.45,0.45,0.45}% dark grey
+
+
+\endinput
+
+
+%% end of file `moderncvcolorblue.sty'.
diff --git a/users/grfn/resume/moderncvcolorgreen.sty b/users/grfn/resume/moderncvcolorgreen.sty
new file mode 100644
index 0000000000..4de7f848a0
--- /dev/null
+++ b/users/grfn/resume/moderncvcolorgreen.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolorgreen.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolorgreen}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: green]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0.35,0.70,0.30}% green
+\definecolor{color2}{rgb}{0.45,0.45,0.45}% dark grey
+
+
+\endinput
+
+
+%% end of file `moderncvcolorgreen.sty'.
diff --git a/users/grfn/resume/moderncvcolorgrey.sty b/users/grfn/resume/moderncvcolorgrey.sty
new file mode 100644
index 0000000000..9018726a23
--- /dev/null
+++ b/users/grfn/resume/moderncvcolorgrey.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolorgrey.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolorgrey}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: grey]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0.55,0.55,0.55}% dark grey
+\definecolor{color2}{rgb}{0.55,0.55,0.55}% dark grey
+
+
+\endinput
+
+
+%% end of file `moderncvcolorgrey.sty'.
diff --git a/users/grfn/resume/moderncvcolororange.sty b/users/grfn/resume/moderncvcolororange.sty
new file mode 100644
index 0000000000..134ae24011
--- /dev/null
+++ b/users/grfn/resume/moderncvcolororange.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolororange.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolororange}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: orange]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0.95,0.55,0.15}% orange
+\definecolor{color2}{rgb}{0.45,0.45,0.45}% dark grey
+
+
+\endinput
+
+
+%% end of file `moderncvcolororange.sty'.
diff --git a/users/grfn/resume/moderncvcolorpurple.sty b/users/grfn/resume/moderncvcolorpurple.sty
new file mode 100644
index 0000000000..d3dc5345b0
--- /dev/null
+++ b/users/grfn/resume/moderncvcolorpurple.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolorpurple.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolorpurple}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: purple]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0.50,0.33,0.80}% purple
+\definecolor{color2}{rgb}{0.45,0.45,0.45}% dark grey
+
+
+\endinput
+
+
+%% end of file `moderncvcolorpurple.sty'.
diff --git a/users/grfn/resume/moderncvcolorred.sty b/users/grfn/resume/moderncvcolorred.sty
new file mode 100644
index 0000000000..681181997d
--- /dev/null
+++ b/users/grfn/resume/moderncvcolorred.sty
@@ -0,0 +1,27 @@
+%% start of file `moderncvcolorred.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcolorred}[2013/02/09 v1.3.0 modern curriculum vitae and letter color scheme: red]
+
+
+%-------------------------------------------------------------------------------
+%                color scheme definition
+%-------------------------------------------------------------------------------
+\definecolor{color0}{rgb}{0,0,0}% black
+\definecolor{color1}{rgb}{0.95,0.20,0.20}% red
+\definecolor{color2}{rgb}{0.45,0.45,0.45}% dark grey
+
+
+\endinput
+
+
+%% end of file `moderncvcolorred.sty'.
diff --git a/users/grfn/resume/moderncvcompatibility.sty b/users/grfn/resume/moderncvcompatibility.sty
new file mode 100644
index 0000000000..1fc53f2180
--- /dev/null
+++ b/users/grfn/resume/moderncvcompatibility.sty
@@ -0,0 +1,104 @@
+%% start of file `moderncvcompatibility.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvcompatibility}[2013/02/09 v1.3.0 modern curriculum vitae and letter compatibility patches]
+
+
+%-------------------------------------------------------------------------------
+%                required packages
+%-------------------------------------------------------------------------------
+
+
+%-------------------------------------------------------------------------------
+%                package options
+%-------------------------------------------------------------------------------
+% old casual option (version 0.1)
+%\DeclareOption{casual}{\input{moderncvstylecasual.sty}}
+
+% old classic option (version 0.1)
+%\DeclareOption{classic}{\input{moderncvstyleclassic.sty}}
+
+\DeclareOption*{}
+
+% process given options
+\ProcessOptions\relax
+
+%-------------------------------------------------------------------------------
+%                definitions
+%-------------------------------------------------------------------------------
+% compatibility with version 0.1
+\newcommand*{\cvresume}[2]{\cvlistdoubleitem{#1}{#2}}
+
+% compatibility with versions <= 0.2
+% section, cvline, ... with width argument...
+%\newcommand*{\section}[2][0.825]{%
+%  \closesection{}%
+%  \@sectionopentrue%
+%  \addcontentsline{toc}{part}{#2}
+%  \begin{longtable}[t]{@{}r@{\hspace{.025\textwidth}}@{}p{#1\textwidth}@{}}%
+%%  \colorrule{.15\textwidth}&\mbox{\color{sectiontitlecolor}\sectionfont#2}\\[1ex]}%
+%  {\color{sectionrectanglecolor}\rule{0.15\textwidth}{1ex}}&\mbox{\color{sectiontitlecolor}\sectionfont#2}\\[1ex]}%
+%\newcommand*{\cvline}[3][.825]{%
+%  \begin{minipage}[t]{\hintscolumnwidth}\raggedleft\small\sffamily#2\end{minipage}&\begin{minipage}[t]{\maincolumnwidth}#3\end{minipage}\\}
+%\newcommand*{\cvitem}[3][.825]{%
+%  \cvline[#1]{#2}{#3\vspace*{.75em}}}   % the \vspace*{} inside the cvline environment is a hack... (should conceptually be outside the environment)
+
+% compatibility with versions <= 0.5
+%\newcommand*{\cvitem}[2]{\cvline{#1}{#2}}
+%\newcommand*{\moderncvstyle}[1]{\moderncvtheme{#1}}
+
+% compatibility with versions <= 0.7
+\newcommand*{\closesection}{}
+\newcommand*{\emptysection}{}
+\newcommand*{\sethintscolumnlength}[1]{%
+  \setlength{\hintscolumnwidth}{#1}%
+  \recomputelengths}
+\newcommand*{\sethintscolumntowidth}[1]{%
+  \settowidth{\hintscolumnwidth}{#1}%
+  \recomputelengths}
+
+% compatibility with versions <= 0.15
+\newcommand*{\cvline}[2]{\cvitem{#1}{#2}}
+\newcommand*{\cvlanguage}[3]{\cvitemwithcomment{#1}{#2}{#3}}
+\newcommand*{\cvcomputer}[4]{\cvdoubleitem{#1}{\small#2}{#3}{\small#4}}
+\newcommand*{\moderncvtheme}[2][blue]{%
+  \moderncvcolor{#1}%
+  \moderncvstyle{#2}}
+
+% compatibility with versions <= 0.19
+\newcommand*{\maketitle}{\makecvtitle}%
+\title{}% to avoid LaTeX complaining that \maketitle is a called without first a call to \title
+\newcommand*{\maketitlenamewidth}{\makecvtitlenamewidth}
+
+% compatibility with versions <= 1.3.0
+\newcommand*{\firstname}[1]{\def\@firstname{#1}}
+\newcommand*{\lastname}[1]{\def\@lastname{#1}}
+\newcommand*{\givenname}[1]{\def\@firstname{#1}}
+\newcommand*{\familyname}[1]{\def\@lastname{#1}}
+\def\@familyname{\@lastname}
+
+% compatibility with versions <= 1.4.0
+\newcommand*{\mobile}[1]{\collectionadd[mobile]{phones}{#1}}
+%\newcommand*{\phone}[1]{\collectionadd[fixed]{phones}{#1}}% implicit, as \phone{...} defaults to \phone[fixed]{...}
+\newcommand*{\fax}[1]{\collectionadd[fax]{phones}{#1}}
+\newcommand*{\@mobile}{\collectionfindbykey{phones}{mobile}}
+\newcommand*{\@phone}{\collectionfindbykey{phones}{fixed}}
+\newcommand*{\@fax}{\collectionfindbykey{phones}{fax}}
+\newcommand*{\phonesymbol}{\fixedphonesymbol}
+\newcommand*{\mobilesymbol}{\mobilephonesymbol}
+\newcommand*{\faxsymbol}{\faxphonesymbol}
+
+
+\endinput
+
+
+%% end of file `moderncvcompatibility.sty'.
diff --git a/users/grfn/resume/moderncviconsletters.sty b/users/grfn/resume/moderncviconsletters.sty
new file mode 100644
index 0000000000..0a4e2864be
--- /dev/null
+++ b/users/grfn/resume/moderncviconsletters.sty
@@ -0,0 +1,50 @@
+%% start of file `moderncviconsletters.sty'.
+%% Copyright 2013-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncviconsmarvosym}[2013/02/09 v1.3.0 modern curriculum vitae and letter icons: letters]
+
+
+%-------------------------------------------------------------------------------
+%                required packages
+%-------------------------------------------------------------------------------
+% MarVoSym font
+%\RequirePackage{marvosym}
+\newcommand*{\marvosymbol}[1]{}
+%\ifxetexorluatex
+%  \renewcommand*{\marvosymbol}[1]{{\fontspec{MarVoSym}\char#1}}
+%\else
+  \renewcommand*{\marvosymbol}[1]{{\fontfamily{mvs}\fontencoding{U}\fontseries{m}\fontshape{n}\selectfont\char#1}}
+%\fi
+
+
+%-------------------------------------------------------------------------------
+%                symbols definition
+%-------------------------------------------------------------------------------
+\renewcommand*{\labelitemi}{\strut\textcolor{color1}{\marvosymbol{123}}}% equivalent to \Neutral from marvosym package; alternative: \fontencoding{U}\fontfamily{ding}\selectfont\tiny\symbol{'102}
+%\renewcommand*{\labelitemii}{\strut\textcolor{color1}{\large\bfseries-}}% no change from default in moderncv.cls
+%\renewcommand*{\labelitemiii}{\strut\textcolor{color1}{\rmfamily\textperiodcentered}}% no change from default in moderncv.cls
+%\renewcommand*{\labelitemiv}{\labelitemiii}% no change from default in moderncv.cls
+
+\renewcommand*{\addresssymbol}{}
+\renewcommand*{\mobilephonesymbol}{\textbf{M}~}
+\renewcommand*{\fixedphonesymbol}{\textbf{T}~}
+\renewcommand*{\faxphonesymbol}{\textbf{F}~}
+\renewcommand*{\emailsymbol}{\textbf{E}~}
+\renewcommand*{\homepagesymbol}{}
+
+\renewcommand*{\listitemsymbol}{\labelitemi~}
+
+
+\endinput
+
+
+%% end of file `moderncviconsletters.sty'.
diff --git a/users/grfn/resume/moderncviconsmarvosym.sty b/users/grfn/resume/moderncviconsmarvosym.sty
new file mode 100644
index 0000000000..eb1b1ec727
--- /dev/null
+++ b/users/grfn/resume/moderncviconsmarvosym.sty
@@ -0,0 +1,48 @@
+%% start of file `moderncviconsmarvosym.sty'.
+%% Copyright 2013-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncviconsmarvosym}[2013/02/09 v1.3.0 modern curriculum vitae and letter icons: marvosym]
+
+
+%-------------------------------------------------------------------------------
+%                required packages
+%-------------------------------------------------------------------------------
+% MarVoSym font
+%\RequirePackage{marvosym}
+\newcommand*{\marvosymbol}[1]{}
+%\ifxetexorluatex
+%  \renewcommand*{\marvosymbol}[1]{{\fontspec{MarVoSym}\char#1}}
+%\else
+  \renewcommand*{\marvosymbol}[1]{{\fontfamily{mvs}\fontencoding{U}\fontseries{m}\fontshape{n}\selectfont\char#1}}
+%\fi
+
+
+%-------------------------------------------------------------------------------
+%                symbols definition
+%-------------------------------------------------------------------------------
+\renewcommand*{\labelitemi}{\strut\textcolor{color1}{\marvosymbol{123}}}% equivalent to \Neutral from marvosym package; alternative: \fontencoding{U}\fontfamily{ding}\selectfont\tiny\symbol{'102}
+%\renewcommand*{\labelitemii}{\strut\textcolor{color1}{\large\bfseries-}}% no change from default in moderncv.cls
+%\renewcommand*{\labelitemiii}{\strut\textcolor{color1}{\rmfamily\textperiodcentered}}% no change from default in moderncv.cls
+%\renewcommand*{\labelitemiv}{\labelitemiii}% no change from default in moderncv.cls
+
+\renewcommand*{\addresssymbol}{}
+\renewcommand*{\mobilephonesymbol}{\marvosymbol{72}~}
+\renewcommand*{\fixedphonesymbol}{\marvosymbol{84}~}
+\renewcommand*{\faxphonesymbol}{\marvosymbol{117}~}
+\renewcommand*{\emailsymbol}{\marvosymbol{66}~}
+\renewcommand*{\homepagesymbol}{{\Large\marvosymbol{205}}~}
+
+
+\endinput
+
+
+%% end of file `moderncviconsmarvosym.sty'.
diff --git a/users/grfn/resume/moderncvstylebanking.sty b/users/grfn/resume/moderncvstylebanking.sty
new file mode 100644
index 0000000000..fb0b70fdcd
--- /dev/null
+++ b/users/grfn/resume/moderncvstylebanking.sty
@@ -0,0 +1,287 @@
+%% start of file `moderncvstylebanking.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvstylebanking}[2013/02/09 v1.3.0 modern curriculum vitae and letter style scheme: banking]
+
+
+%-------------------------------------------------------------------------------
+%                required packages
+%-------------------------------------------------------------------------------
+
+
+%-------------------------------------------------------------------------------
+%                overall style definition
+%-------------------------------------------------------------------------------
+% fonts
+%\ifxetexorluatex
+%  \setmainfont{Tex-Gyre Pagella}
+%  \setsansfont{Tex-Gyre Pagella}
+%  \setmathfont{Tex-Gyre Pagella}
+%  \setmathfont[range=\mathit,\mathsfit]{Tex-Gyre Pagella Italic}
+%  \setmathfont[range=\mathbfup,\mathbfsfup]{Tex-Gyre Pagella Bold}
+%  \setmathfont[range=\mathbfit,\mathbfsfit]{Tex-Gyre Pagella Bold Italic}
+%\else
+  \IfFileExists{tgpagella.sty}%
+    {%
+      \RequirePackage{tgpagella}%
+      \renewcommand*{\familydefault}{\rmdefault}}%
+    {}
+%\fi
+
+% symbols
+\moderncvicons{marvosym}
+
+% commands
+\newcommand*{\maketitlesymbol}{%
+    {~~~{\rmfamily\textbullet}~~~}}% the \rmfamily is required to force Latin Modern fonts when using sans serif, as OMS/lmss/m/n is not defined and gets substituted by OMS/cmsy/m/n
+%   internal command to add an element to the footer
+%   it collects the elements in a temporary box, and checks when to flush the box
+\newsavebox{\maketitlebox}%
+\newsavebox{\maketitletempbox}%
+\newlength{\maketitlewidth}%
+\newlength{\maketitleboxwidth}%
+\newif\if@firstmaketitleelement\@firstmaketitleelementtrue%
+%   adds an element to the maketitle, separated by maketitlesymbol
+%   usage: \addtomaketitle[maketitlesymbol]{element}
+\newcommand*{\addtomaketitle}[2][\maketitlesymbol]{%
+  \if@firstmaketitleelement%
+    \savebox{\maketitletempbox}{\usebox{\maketitlebox}#2}%
+  \else%
+    \savebox{\maketitletempbox}{\usebox{\maketitlebox}#1#2}\fi%
+  \settowidth{\maketitleboxwidth}{\usebox{\maketitletempbox}}%
+  \ifnum\maketitleboxwidth<\maketitlewidth%
+    \savebox{\maketitlebox}{\usebox{\maketitletempbox}}%
+    \@firstmaketitleelementfalse%
+  \else%
+    \flushmaketitle{}\\%
+    \savebox{\maketitlebox}{#2}%
+    \savebox{\maketitletempbox}{#2}%
+    \settowidth{\maketitleboxwidth}{\usebox{\maketitlebox}}%
+    \@firstmaketitleelementfalse\fi}
+%   internal command to flush the maketitle
+\newcommand*{\flushmaketitle}{%
+  \strut\usebox{\maketitlebox}%
+  \savebox{\maketitlebox}{}%
+  \savebox{\maketitletempbox}{}%
+  \setlength{\maketitleboxwidth}{0pt}}
+\renewcommand*{\maketitle}{%
+  \setlength{\maketitlewidth}{0.8\textwidth}%
+  \hfil%
+  \parbox{\maketitlewidth}{%
+    \centering%
+    % name and title
+    \namestyle{\@firstname~\@lastname}%
+    \ifthenelse{\equal{\@title}{}}{}{\titlestyle{~|~\@title}}\\% \isundefined doesn't work on \@title, as LaTeX itself defines \@title (before it possibly gets redefined by \title) 
+    % detailed information
+    \addressfont\color{color2}%
+    \ifthenelse{\isundefined{\@addressstreet}}{}{\addtomaketitle{\addresssymbol\@addressstreet}%
+      \ifthenelse{\equal{\@addresscity}{}}{}{\addtomaketitle[~--~]{\@addresscity}}% if \addresstreet is defined, \addresscity and \addresscountry will always be defined but could be empty
+      \ifthenelse{\equal{\@addresscountry}{}}{}{\addtomaketitle[~--~]{\@addresscountry}}%
+      \flushmaketitle\@firstmaketitleelementtrue\\}%
+    \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number
+      \addtomaketitle{\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}}%
+    \ifthenelse{\isundefined{\@email}}{}{\addtomaketitle{\emailsymbol\emaillink{\@email}}}%
+    \ifthenelse{\isundefined{\@homepage}}{}{\addtomaketitle{\homepagesymbol\httplink{\@homepage}}}%
+    \ifthenelse{\isundefined{\@extrainfo}}{}{\addtomaketitle{\@extrainfo}}%
+    \flushmaketitle}\\[2.5em]}% need to force a \par after this to avoid weird spacing bug at the first section if no blank line is left after \maketitle
+
+
+%-------------------------------------------------------------------------------
+%                resume style definition
+%-------------------------------------------------------------------------------
+% fonts
+\renewcommand*{\namefont}{\Huge\bfseries\upshape}
+\renewcommand*{\titlefont}{\Huge\mdseries\upshape}
+\renewcommand*{\addressfont}{\normalsize\mdseries\upshape}
+\renewcommand*{\quotefont}{\large\slshape}
+\renewcommand*{\sectionfont}{\Large\bfseries\upshape}
+\renewcommand*{\subsectionfont}{\large\upshape\fontseries{sb}\selectfont}
+\renewcommand*{\hintfont}{\bfseries}
+
+% styles
+\renewcommand*{\namestyle}[1]{{\namefont\textcolor{color1}{#1}}}
+\renewcommand*{\titlestyle}[1]{{\titlefont\textcolor{color2!85}{#1}}}
+\renewcommand*{\addressstyle}[1]{{\addressfont\textcolor{color1}{#1}}}
+\renewcommand*{\quotestyle}[1]{{\quotefont\textcolor{color1}{#1}}}
+\renewcommand*{\sectionstyle}[1]{{\sectionfont\textcolor{color1}{#1}}}
+\renewcommand*{\subsectionstyle}[1]{{\subsectionfont\textcolor{color1}{#1}}}
+\renewcommand*{\hintstyle}[1]{{\hintfont\textcolor{color0}{#1}}}
+
+% lengths
+\newlength{\quotewidth}
+\newlength{\hintscolumnwidth}
+\setlength{\hintscolumnwidth}{0.3\textwidth}%
+\newlength{\separatorcolumnwidth}
+\setlength{\separatorcolumnwidth}{0.025\textwidth}%
+\newlength{\maincolumnwidth}
+\newlength{\doubleitemcolumnwidth}
+\newlength{\listitemsymbolwidth}
+\settowidth{\listitemsymbolwidth}{\listitemsymbol}
+\newlength{\listitemmaincolumnwidth}
+\newlength{\listdoubleitemmaincolumnwidth}
+
+% commands
+\renewcommand*{\recomputecvlengths}{%
+  \setlength{\quotewidth}{0.65\textwidth}%
+  % main lenghts
+  \setlength{\maincolumnwidth}{\textwidth}%
+  % listitem lengths
+  \setlength{\listitemmaincolumnwidth}{\maincolumnwidth-\listitemsymbolwidth}%
+  % doubleitem lengths
+  \setlength{\doubleitemcolumnwidth}{\maincolumnwidth-\separatorcolumnwidth}%
+  \setlength{\doubleitemcolumnwidth}{0.5\doubleitemcolumnwidth}%
+  % listdoubleitem lengths
+  \setlength{\listdoubleitemmaincolumnwidth}{\maincolumnwidth-\listitemsymbolwidth-\separatorcolumnwidth-\listitemsymbolwidth}%
+  \setlength{\listdoubleitemmaincolumnwidth}{0.5\listdoubleitemmaincolumnwidth}%
+  % fancyhdr lengths
+  \renewcommand{\headwidth}{\textwidth}%
+  % regular lengths
+  \setlength{\parskip}{0\p@}}
+
+\renewcommand*{\makecvtitle}{%
+  % recompute lengths (in case we are switching from letter to resume, or vice versa)
+  \recomputecvlengths%
+  \maketitle%
+  % optional quote
+  \ifthenelse{\isundefined{\@quote}}%
+    {}%
+    {{\centering\begin{minipage}{\quotewidth}\centering\quotestyle{\@quote}\end{minipage}\\[2.5em]}}%
+  \par}% to avoid weird spacing bug at the first section if no blank line is left after \maketitle}
+
+\RenewDocumentCommand{\section}{sm}{%
+  \par\addvspace{2.5ex}%
+  \phantomsection{}% reset the anchor for hyperrefs
+  \addcontentsline{toc}{section}{#2}%
+  \strut\sectionstyle{#2}%
+  {\color{color1}\hrule}%
+  \par\nobreak\addvspace{1ex}\@afterheading}
+
+\newcommand{\subsectionfill}{\xleaders\hbox to 0.35em{\scriptsize.}\hfill}% different subsectionfills will not be perfectly aligned, but remaining space at the end of the fill will be distributed evenly between leaders, so it will be barely visible
+\RenewDocumentCommand{\subsection}{sm}{%
+  \par\addvspace{1ex}%
+  \phantomsection{}%
+  \addcontentsline{toc}{subsection}{#2}%
+  \strut\subsectionstyle{#2}{\color{color1}{\subsectionfill}}%
+  \par\nobreak\addvspace{0.5ex}\@afterheading}
+
+\renewcommand*{\cvitem}[3][.25em]{%
+  \ifthenelse{\equal{#2}{}}{}{\hintstyle{#2}: }{#3}%
+  \par\addvspace{#1}}
+
+\renewcommand*{\cvdoubleitem}[5][.25em]{%
+  \begin{minipage}[t]{\doubleitemcolumnwidth}\hintstyle{#2}: #3\end{minipage}%
+  \hfill% fill of \separatorcolumnwidth
+  \begin{minipage}[t]{\doubleitemcolumnwidth}\ifthenelse{\equal{#4}{}}{}{\hintstyle{#4}: }#5\end{minipage}%
+  \par\addvspace{#1}}
+
+\renewcommand*{\cvlistitem}[2][.25em]{%
+  \listitemsymbol\begin{minipage}[t]{\listitemmaincolumnwidth}#2\end{minipage}%
+  \par\addvspace{#1}}
+
+\renewcommand*{\cvlistdoubleitem}[3][.25em]{%
+  \cvitem[#1]{}{\listitemsymbol\begin{minipage}[t]{\listdoubleitemmaincolumnwidth}#2\end{minipage}%
+  \hfill% fill of \separatorcolumnwidth
+  \ifthenelse{\equal{#3}{}}%
+    {}%
+    {\listitemsymbol\begin{minipage}[t]{\listdoubleitemmaincolumnwidth}#3\end{minipage}}}}
+
+\renewcommand*{\cventry}[7][.25em]{
+  \begin{tabular*}{\textwidth}{l@{\extracolsep{\fill}}r}%
+	  {\bfseries #4} & {\bfseries #5} \\%
+	  {\itshape #3\ifthenelse{\equal{#6}{}}{}{, #6}} & {\itshape #2}\\%
+  \end{tabular*}%
+  \ifx&#7&%
+    \else{\\\vbox{\small#7}}\fi%
+  \par\addvspace{#1}}
+
+\newbox{\cvitemwithcommentmainbox}
+\newlength{\cvitemwithcommentmainlength}
+\newlength{\cvitemwithcommentcommentlength}
+\renewcommand*{\cvitemwithcomment}[4][.25em]{%
+  \savebox{\cvitemwithcommentmainbox}{\ifthenelse{\equal{#2}{}}{}{\hintstyle{#2}: }#3}%
+  \setlength{\cvitemwithcommentmainlength}{\widthof{\usebox{\cvitemwithcommentmainbox}}}%
+  \setlength{\cvitemwithcommentcommentlength}{\maincolumnwidth-\separatorcolumnwidth-\cvitemwithcommentmainlength}%
+  \begin{minipage}[t]{\cvitemwithcommentmainlength}\ifthenelse{\equal{#2}{}}{}{\hintstyle{#2}: }#3\end{minipage}%
+  \hfill% fill of \separatorcolumnwidth
+  \begin{minipage}[t]{\cvitemwithcommentcommentlength}\raggedleft\small\itshape#4\end{minipage}%
+  \par\addvspace{#1}}
+
+\renewenvironment{thebibliography}[1]%
+  {%
+    \bibliographyhead{\refname}%
+%    \small%
+    \begin{list}{\bibliographyitemlabel}%
+      {%
+        \setlength{\topsep}{0pt}%
+        \setlength{\labelwidth}{0pt}%
+        \setlength{\labelsep}{0pt}%
+        \leftmargin\labelwidth%
+        \advance\leftmargin\labelsep%
+        \@openbib@code%
+        \usecounter{enumiv}%
+        \let\p@enumiv\@empty%
+        \renewcommand\theenumiv{\@arabic\c@enumiv}}%
+        \sloppy\clubpenalty4000\widowpenalty4000%
+%        \sfcode`\.\@m%
+%        \sfcode `\=1000\relax%
+  }%
+  {%
+    \def\@noitemerr{\@latex@warning{Empty `thebibliography' environment}}%
+    \end{list}%
+  }
+
+
+%-------------------------------------------------------------------------------
+%                letter style definition
+%-------------------------------------------------------------------------------
+% commands
+\renewcommand*{\recomputeletterlengths}{
+  \recomputecvlengths%
+  \setlength{\parskip}{6\p@}}
+
+\renewcommand*{\makelettertitle}{%
+  % recompute lengths (in case we are switching from letter to resume, or vice versa)
+  \recomputeletterlengths%
+  % sender block
+  \maketitle%
+  \par%
+   % recipient block
+  \begin{minipage}[t]{.5\textwidth}
+    \raggedright%
+    \addressfont%
+    {\bfseries\upshape\@recipientname}\\%
+    \@recipientaddress%
+  \end{minipage}
+  % date
+  \hfill % US style
+%  \\[1em] % UK style
+  \@date\\[2em]% US informal style: "April 6, 2006"; UK formal style: "05/04/2006"
+  % opening
+  \raggedright%
+  \@opening\\[1.5em]%
+  % ensure no extra spacing after \makelettertitle due to a possible blank line
+%  \ignorespacesafterend% not working
+  \hspace{0pt}\par\vspace{-\baselineskip}\vspace{-\parskip}}
+
+\renewcommand*{\makeletterclosing}{
+  \@closing\\[3em]%
+  {\bfseries \@firstname~\@lastname}%
+  \ifthenelse{\isundefined{\@enclosure}}{}{%
+    \\%
+    \vfill%
+    {\color{color2}\itshape\enclname: \@enclosure}}}
+
+
+\endinput
+
+
+%% end of file `moderncvstylebanking.sty'.
diff --git a/users/grfn/resume/moderncvstylecasual.sty b/users/grfn/resume/moderncvstylecasual.sty
new file mode 100644
index 0000000000..e375e7612a
--- /dev/null
+++ b/users/grfn/resume/moderncvstylecasual.sty
@@ -0,0 +1,182 @@
+%% start of file `moderncvstylecasual.sty'.

+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).

+%

+% This work may be distributed and/or modified under the

+% conditions of the LaTeX Project Public License version 1.3c,

+% available at http://www.latex-project.org/lppl/.

+

+

+%-------------------------------------------------------------------------------

+%                identification

+%-------------------------------------------------------------------------------

+\NeedsTeXFormat{LaTeX2e}

+\ProvidesPackage{moderncvstylecasual}[2013/02/09 v1.3.0 modern curriculum vitae and letter style scheme: casual]

+

+

+%-------------------------------------------------------------------------------

+%                required packages

+%-------------------------------------------------------------------------------

+\RequirePackage{moderncvstyleclassic}

+

+

+%-------------------------------------------------------------------------------

+%                overall style definition

+%-------------------------------------------------------------------------------

+% commands

+%   footer symbol used to separate footer elements

+\newcommand*{\footersymbol}{%

+    {~~~{\rmfamily\textbullet}~~~}}% the \rmfamily is required to force Latin Modern fonts when using sans serif, as OMS/lmss/m/n is not defined and gets substituted by OMS/cmsy/m/n

+%   internal command to add an element to the footer

+%   it collects the elements in a temporary box, and checks when to flush the box

+\newsavebox{\footerbox}%

+\newsavebox{\footertempbox}%

+\newlength{\footerwidth}%

+\newlength{\footerboxwidth}%

+\newif\if@firstfooterelement\@firstfooterelementtrue%

+%   adds an element to the footer, separated by footersymbol

+%   usage: \addtofooter[footersymbol]{element}

+\newcommand*{\addtofooter}[2][\footersymbol]{%

+  \if@firstfooterelement%

+    \savebox{\footertempbox}{\usebox{\footerbox}#2}%

+  \else%

+    \savebox{\footertempbox}{\usebox{\footerbox}#1#2}\fi%

+  \settowidth{\footerboxwidth}{\usebox{\footertempbox}}%

+  \ifnum\footerboxwidth<\footerwidth%

+    \savebox{\footerbox}{\usebox{\footertempbox}}%

+    \@firstfooterelementfalse%

+  \else%

+    \flushfooter\\%

+    \savebox{\footerbox}{#2}%

+    \savebox{\footertempbox}{#2}%

+    \settowidth{\footerboxwidth}{\usebox{\footerbox}}%

+    \@firstfooterelementfalse\fi}

+%   internal command to flush the footer

+\newcommand*{\flushfooter}{%

+  \strut\usebox{\footerbox}%

+  \savebox{\footerbox}{}%

+  \savebox{\footertempbox}{}%

+  \setlength{\footerboxwidth}{0pt}}

+

+

+%-------------------------------------------------------------------------------

+%                resume style definition

+%-------------------------------------------------------------------------------

+% fonts

+\renewcommand*{\namefont}{\fontsize{38}{40}\mdseries\upshape}

+\renewcommand*{\addressfont}{\normalsize\mdseries\slshape}

+

+% commands

+\renewcommand*{\makecvtitle}{%

+  % recompute lengths (in case we are switching from letter to resume, or vice versa)

+  \recomputecvlengths%

+  % ensure footer with personal information

+  \makecvfooter%

+  % optional picture

+  \newbox{\makecvtitlepicturebox}%

+  \savebox{\makecvtitlepicturebox}{%

+    \ifthenelse{\isundefined{\@photo}}%

+      {}%

+      {%

+       \setlength\fboxrule{\@photoframewidth}%

+       \ifdim\@photoframewidth=0pt%

+         \setlength{\fboxsep}{0pt}\fi%

+       {\color{color1}\framebox{\includegraphics[width=\@photowidth]{\@photo}}}}}%

+  \usebox{\makecvtitlepicturebox}%

+  % name

+  \@initializelength{\makecvtitlepicturewidth}%

+  \settowidth{\makecvtitlepicturewidth}{\usebox{\makecvtitlepicturebox}}%

+  \parbox[b]{\textwidth-\makecvtitlepicturewidth}{%

+    \raggedleft\namefont{\color{color2!50}\@firstname} {\color{color2}\@lastname}}\\[-.35em]% alternate design: \MakeLowercase and no space

+  {\color{color2!50}\rule{\textwidth}{.25ex}}%

+  % optional title

+  \ifthenelse{\equal{\@title}{}}{}{\\[1.25em]\null\hfill\titlestyle{\@title}}\\[2.5em]% \null is required as there is no box on the line after \\, so glue (and leaders) disappears; this is in contrast to after \par, where the next line starts with an indent box (even after \noindent).

+  % optional quote

+  \ifthenelse{\isundefined{\@quote}}%

+    {}%

+    {{\null\hfill\begin{minipage}{\quotewidth}\centering\quotestyle{\@quote}\end{minipage}\hfill\null\\[2.5em]}}%

+  \par}% to avoid weird spacing bug at the first section if no blank line is left after \maketitle

+

+\renewcommand*{\makecvfooter}{%

+  \setlength{\footerwidth}{0.8\textwidth}%

+  \fancypagestyle{plain}{%

+    \fancyfoot[c]{%

+      \parbox[b]{\footerwidth}{%

+        \centering%

+        \color{color2}\addressfont%

+        \ifthenelse{\isundefined{\@addressstreet}}{}{\addtofooter[]{\addresssymbol\@addressstreet}%

+          \ifthenelse{\equal{\@addresscity}{}}{}{\addtofooter[~--~]{\@addresscity}}% if \addresstreet is defined, \addresscity and \addresscountry will always be defined but could be empty

+          \ifthenelse{\equal{\@addresscountry}{}}{}{\addtofooter[~--~]{\@addresscountry}}%

+          \flushfooter\@firstfooterelementtrue\\}%

+        \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number

+          \addtofooter{\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}}%

+        \ifthenelse{\isundefined{\@email}}{}{\addtofooter{\emailsymbol\emaillink{\@email}}}%

+        \ifthenelse{\isundefined{\@homepage}}{}{\addtofooter{\homepagesymbol\httplink{\@homepage}}}%

+        \ifthenelse{\isundefined{\@github}}{}{\addtofooter{\httplink{http://github.com/\@github}}}%

+        \ifthenelse{\isundefined{\@extrainfo}}{}{\addtofooter{\@extrainfo}}%

+        \ifthenelse{\lengthtest{\footerboxwidth=0pt}}{}{\flushfooter}% the lengthtest is required to avoid flushing an empty footer, which could cause a blank line due to the \\ after the address, if no other personal info is used

+        }}}%

+  \pagestyle{plain}}

+

+

+%-------------------------------------------------------------------------------

+%                letter style definition

+%-------------------------------------------------------------------------------

+\renewcommand*{\makelettertitle}{%

+  % recompute lengths (in case we are switching from letter to resume, or vice versa)

+  \recomputeletterlengths%

+  % ensure footer with personal information

+  \makeletterfooter%

+  % recipient block

+  \begin{minipage}[t]{.5\textwidth}

+    \raggedright%

+    \addressfont%

+    {\bfseries\upshape\@recipientname}\\%

+    \@recipientaddress%

+  \end{minipage}

+  % date

+  \hfill% US style

+%  \\[1em]% UK style

+  \@date\\[2em]% US informal style: "April 6, 2006"; UK formal style: "05/04/2006"

+  % opening

+  \raggedright%

+  \@opening\\[1.5em]%

+  % ensure no extra spacing after \makelettertitle due to a possible blank line

+%  \ignorespacesafterend% not working

+  \hspace{0pt}\par\vspace{-\baselineskip}\vspace{-\parskip}}

+

+\renewcommand*{\makeletterfooter}{%

+  \setlength{\footerwidth}{0.8\textwidth}%

+  \fancypagestyle{plain}{%

+    \fancyfoot[c]{%

+      \parbox[b]{\footerwidth}{%

+        \centering%

+        \addressfont\color{color2}%

+        \vspace{-\baselineskip}% to cancel out the extra vertical space taken by the name (below) and ensure perfect alignment of letter and cv footers

+        \strut{\bfseries\upshape\@firstname~\@lastname}\\% the \strut is required to ensure the line is exactly \baselineskip tall

+        \ifthenelse{\isundefined{\@addressstreet}}{}{\addtofooter[]{\addresssymbol\@addressstreet}%

+          \ifthenelse{\equal{\@addresscity}{}}{}{\addtofooter[~--~]{\@addresscity}}% if \addresstreet is defined, \addresscity and addresscountry will always be defined but could be empty

+          \ifthenelse{\equal{\@addresscountry}{}}{}{\addtofooter[~--~]{\@addresscountry}}%

+          \flushfooter\@firstfooterelementtrue\\}%

+        \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number

+          \addtofooter{\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}}%

+        \ifthenelse{\isundefined{\@email}}{}{\addtofooter{\emailsymbol\emaillink{\@email}}}%

+        \ifthenelse{\isundefined{\@homepage}}{}{\addtofooter{\homepagesymbol\httplink{\@homepage}}}%

+        \ifthenelse{\isundefined{\@extrainfo}}{}{\addtofooter{\@extrainfo}}%

+        \ifthenelse{\lengthtest{\footerboxwidth=0pt}}{}{\flushfooter}% the lengthtest is required to avoid flushing an empty footer, which could cause a blank line due to the \\ after the address, if no other personal info is used

+        }}}%

+  \pagestyle{plain}}

+

+\renewcommand*{\makeletterclosing}{

+  \@closing\\[3em]%

+  {\bfseries\@firstname~\@lastname}%

+  \ifthenelse{\isundefined{\@enclosure}}{}{%

+    \\%

+    \vfil%

+    {\color{color2}\itshape\enclname: \@enclosure}}%

+    \vfil}

+

+

+\endinput

+

+

+%% end of file `moderncvstylecasual.sty'.

diff --git a/users/grfn/resume/moderncvstyleclassic.sty b/users/grfn/resume/moderncvstyleclassic.sty
new file mode 100644
index 0000000000..63cf97aa3b
--- /dev/null
+++ b/users/grfn/resume/moderncvstyleclassic.sty
@@ -0,0 +1,294 @@
+%% start of file `moderncvstyleclassic.sty'.

+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).

+%

+% This work may be distributed and/or modified under the

+% conditions of the LaTeX Project Public License version 1.3c,

+% available at http://www.latex-project.org/lppl/.

+

+

+%-------------------------------------------------------------------------------

+%                identification

+%-------------------------------------------------------------------------------

+\NeedsTeXFormat{LaTeX2e}

+\ProvidesPackage{moderncvstyleclassic}[2013/02/09 v1.3.0 modern curriculum vitae and letter style scheme: classic]

+

+

+%-------------------------------------------------------------------------------

+%                required packages

+%-------------------------------------------------------------------------------

+% Latin Modern fonts

+%\ifxetexorluatex

+%  \setmainfont{Latin Modern Roman}

+%  \setsansfont{Latin Modern Sans}

+%  \setmathfont{Latin Modern Math}

+%\else

+  \IfFileExists{lmodern.sty}%

+    {\RequirePackage{lmodern}}%

+    {}

+%\fi

+

+

+%-------------------------------------------------------------------------------

+%                overall style definition

+%-------------------------------------------------------------------------------

+% symbols

+\moderncvicons{marvosym}

+

+

+%-------------------------------------------------------------------------------

+%                resume style definition

+%-------------------------------------------------------------------------------

+% fonts

+\renewcommand*{\namefont}{\fontsize{34}{36}\mdseries\upshape}

+\renewcommand*{\titlefont}{\LARGE\mdseries\slshape}

+\renewcommand*{\addressfont}{\small\mdseries\slshape}

+\renewcommand*{\quotefont}{\large\slshape}

+\renewcommand*{\sectionfont}{\Large\mdseries\upshape}

+\renewcommand*{\subsectionfont}{\large\mdseries\upshape}

+\renewcommand*{\hintfont}{}

+

+% styles

+\renewcommand*{\namestyle}[1]{{\namefont\textcolor{color0}{#1}}}

+\renewcommand*{\titlestyle}[1]{{\titlefont\textcolor{color2}{#1}}}

+\renewcommand*{\addressstyle}[1]{{\addressfont\textcolor{color1}{#1}}}

+\renewcommand*{\quotestyle}[1]{{\quotefont\textcolor{color1}{#1}}}

+\renewcommand*{\sectionstyle}[1]{{\sectionfont\textcolor{color1}{#1}}}

+\renewcommand*{\subsectionstyle}[1]{{\subsectionfont\textcolor{color1}{#1}}}

+\renewcommand*{\hintstyle}[1]{{\hintfont\textcolor{color0}{#1}}}

+

+% lengths

+\newlength{\quotewidth}

+\newlength{\hintscolumnwidth}

+\setlength{\hintscolumnwidth}{0.175\textwidth}

+\newlength{\separatorcolumnwidth}

+\setlength{\separatorcolumnwidth}{0.025\textwidth}

+\newlength{\maincolumnwidth}

+\newlength{\doubleitemmaincolumnwidth}

+\newlength{\listitemsymbolwidth}

+\settowidth{\listitemsymbolwidth}{\listitemsymbol}

+\newlength{\listitemmaincolumnwidth}

+\newlength{\listdoubleitemmaincolumnwidth}

+

+% commands

+\renewcommand*{\recomputecvlengths}{%

+  \setlength{\quotewidth}{0.65\textwidth}%

+  % main lenghts

+  \setlength{\maincolumnwidth}{\textwidth-\separatorcolumnwidth-\hintscolumnwidth}%

+  % listitem lengths

+  \setlength{\listitemmaincolumnwidth}{\maincolumnwidth-\listitemsymbolwidth}%

+  % doubleitem lengths

+  \setlength{\doubleitemmaincolumnwidth}{\maincolumnwidth-\hintscolumnwidth-\separatorcolumnwidth-\separatorcolumnwidth}%

+  \setlength{\doubleitemmaincolumnwidth}{0.5\doubleitemmaincolumnwidth}%

+  % listdoubleitem lengths

+  \setlength{\listdoubleitemmaincolumnwidth}{\maincolumnwidth-\listitemsymbolwidth-\separatorcolumnwidth-\listitemsymbolwidth}%

+  \setlength{\listdoubleitemmaincolumnwidth}{0.5\listdoubleitemmaincolumnwidth}%

+  % fancyhdr lengths

+  \renewcommand{\headwidth}{\textwidth}%

+  % regular lengths

+  \setlength{\parskip}{0\p@}}

+

+% optional maketitle width to force a certain width (if set to 0pt, the width is calculated automatically)

+\newlength{\makecvtitlenamewidth}

+\setlength{\makecvtitlenamewidth}{0pt}% dummy value

+\renewcommand*{\makecvtitle}{%

+  % recompute lengths (in case we are switching from letter to resume, or vice versa)

+  \recomputecvlengths%

+  % optional detailed information (pre-rendering)

+  \def\phonesdetails{}%

+  \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number

+    \protected@edef\phonesdetails{\phonesdetails\protect\makenewline\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}}%

+  \newbox{\makecvtitledetailsbox}%

+  \savebox{\makecvtitledetailsbox}{%

+    \addressfont\color{color2}%

+    \begin{tabular}[b]{@{}r@{}}%

+      \ifthenelse{\isundefined{\@addressstreet}}{}{\makenewline\addresssymbol\@addressstreet%

+        \ifthenelse{\equal{\@addresscity}{}}{}{\makenewline\@addresscity}% if \addresstreet is defined, \addresscity and addresscountry will always be defined but could be empty

+        \ifthenelse{\equal{\@addresscountry}{}}{}{\makenewline\@addresscountry}}%

+      \phonesdetails% needed to be pre-rendered as loops and tabulars seem to conflict

+      \ifthenelse{\isundefined{\@email}}{}{\makenewline\emailsymbol\emaillink{\@email}}%

+      \ifthenelse{\isundefined{\@homepage}}{}{\makenewline\homepagesymbol\httplink{\@homepage}}%

+      \ifthenelse{\isundefined{\@extrainfo}}{}{\makenewline\@extrainfo}%

+    \end{tabular}

+  }%

+  % optional photo (pre-rendering)

+  \newbox{\makecvtitlepicturebox}%

+  \savebox{\makecvtitlepicturebox}{%

+    \ifthenelse{\isundefined{\@photo}}%

+    {}%

+    {%

+      \hspace*{\separatorcolumnwidth}%

+      \color{color1}%

+      \setlength{\fboxrule}{\@photoframewidth}%

+      \ifdim\@photoframewidth=0pt%

+        \setlength{\fboxsep}{0pt}\fi%

+      \framebox{\includegraphics[width=\@photowidth]{\@photo}}}}%

+  % name and title

+  \newlength{\makecvtitledetailswidth}\settowidth{\makecvtitledetailswidth}{\usebox{\makecvtitledetailsbox}}%

+  \newlength{\makecvtitlepicturewidth}\settowidth{\makecvtitlepicturewidth}{\usebox{\makecvtitlepicturebox}}%

+  \ifthenelse{\lengthtest{\makecvtitlenamewidth=0pt}}% check for dummy value (equivalent to \ifdim\makecvtitlenamewidth=0pt)

+    {\setlength{\makecvtitlenamewidth}{\textwidth-\makecvtitledetailswidth-\makecvtitlepicturewidth}}%

+    {}%

+  \begin{minipage}[b]{\makecvtitlenamewidth}%

+    \namestyle{\@firstname\ \@lastname}%

+    \ifthenelse{\equal{\@title}{}}{}{\\[1.25em]\titlestyle{\@title}}%

+  \end{minipage}%

+  \hfill%

+  % optional detailed information (rendering)

+  \llap{\usebox{\makecvtitledetailsbox}}% \llap is used to suppress the width of the box, allowing overlap if the value of makecvtitlenamewidth is forced

+  % optional photo (rendering)

+  \usebox{\makecvtitlepicturebox}\\[2.5em]%

+  % optional quote

+  \ifthenelse{\isundefined{\@quote}}%

+    {}%

+    {{\centering\begin{minipage}{\quotewidth}\centering\quotestyle{\@quote}\end{minipage}\\[2.5em]}}%

+  \par}% to avoid weird spacing bug at the first section if no blank line is left after \makecvtitle

+

+\newlength{\baseletterheight}

+\settoheight{\baseletterheight}{\sectionstyle{o}}

+\setlength{\baseletterheight}{\baseletterheight-0.95ex}

+\RenewDocumentCommand{\section}{sm}{%

+  \par\addvspace{2.5ex}%

+  \phantomsection{}% reset the anchor for hyperrefs

+  \addcontentsline{toc}{section}{#2}%

+  \parbox[t]{\hintscolumnwidth}{\strut\raggedleft\raisebox{\baseletterheight}{\color{color1}\rule{\hintscolumnwidth}{0.95ex}}}%

+  \hspace{\separatorcolumnwidth}%

+  \parbox[t]{\maincolumnwidth}{\strut\sectionstyle{#2}}%

+  \par\nobreak\addvspace{1ex}\@afterheading}% to avoid a pagebreak after the heading

+

+\RenewDocumentCommand{\subsection}{sm}{%

+  \par\addvspace{1ex}%

+  \phantomsection{}% reset the anchor for hyperrefs

+  \addcontentsline{toc}{subsection}{#2}%

+  \begin{tabular}{@{}p{\hintscolumnwidth}@{\hspace{\separatorcolumnwidth}}p{\maincolumnwidth}@{}}%

+    \raggedleft\hintstyle{} &{\strut\subsectionstyle{#2}}%

+  \end{tabular}%

+  \par\nobreak\addvspace{0.5ex}\@afterheading}% to avoid a pagebreak after the heading

+

+\renewcommand*{\cvitem}[3][.25em]{%

+  \begin{tabular}{@{}p{\hintscolumnwidth}@{\hspace{\separatorcolumnwidth}}p{\maincolumnwidth}@{}}%

+    \raggedleft\hintstyle{#2} &{#3}%

+  \end{tabular}%

+  \par\addvspace{#1}}

+

+\renewcommand*{\cvdoubleitem}[5][.25em]{%

+ \cvitem[#1]{#2}{%

+   \begin{minipage}[t]{\doubleitemmaincolumnwidth}#3\end{minipage}%

+   \hfill% fill of \separatorcolumnwidth

+   \begin{minipage}[t]{\hintscolumnwidth}\raggedleft\hintstyle{#4}\end{minipage}%

+   \hspace*{\separatorcolumnwidth}%

+   \begin{minipage}[t]{\doubleitemmaincolumnwidth}#5\end{minipage}}}

+

+\renewcommand*{\cvlistitem}[2][.25em]{%

+  \cvitem[#1]{}{\listitemsymbol\begin{minipage}[t]{\listitemmaincolumnwidth}#2\end{minipage}}}

+

+\renewcommand*{\cvlistdoubleitem}[3][.25em]{%

+  \cvitem[#1]{}{\listitemsymbol\begin{minipage}[t]{\listdoubleitemmaincolumnwidth}#2\end{minipage}%

+  \hfill% fill of \separatorcolumnwidth

+  \ifthenelse{\equal{#3}{}}%

+    {}%

+    {\listitemsymbol\begin{minipage}[t]{\listdoubleitemmaincolumnwidth}#3\end{minipage}}}}

+

+\renewcommand*{\cventry}[7][.25em]{%

+  \cvitem[#1]{#2}{%

+    {\bfseries#3}%

+    \ifthenelse{\equal{#4}{}}{}{, {\slshape#4}}%

+    \ifthenelse{\equal{#5}{}}{}{, #5}%

+    \ifthenelse{\equal{#6}{}}{}{, #6}%

+    .\strut%

+    \ifx&#7&%

+      \else{\newline{}\begin{minipage}[t]{\linewidth}\small#7\end{minipage}}\fi}}

+

+\newbox{\cvitemwithcommentmainbox}

+\newlength{\cvitemwithcommentmainlength}

+\newlength{\cvitemwithcommentcommentlength}

+\renewcommand*{\cvitemwithcomment}[4][.25em]{%

+  \savebox{\cvitemwithcommentmainbox}{{\bfseries#3}}%

+  \setlength{\cvitemwithcommentmainlength}{\widthof{\usebox{\cvitemwithcommentmainbox}}}%

+  \setlength{\cvitemwithcommentcommentlength}{\maincolumnwidth-\separatorcolumnwidth-\cvitemwithcommentmainlength}%

+  \cvitem[#1]{#2}{%

+    \begin{minipage}[t]{\cvitemwithcommentmainlength}\bfseries#3\end{minipage}%

+    \hfill% fill of \separatorcolumnwidth

+    \begin{minipage}[t]{\cvitemwithcommentcommentlength}\raggedleft\small\itshape#4\end{minipage}}}

+

+\renewenvironment{thebibliography}[1]%

+  {%

+    \bibliographyhead{\refname}%

+%    \small%

+    \begin{list}{\bibliographyitemlabel}%

+      {%

+        \setlength{\topsep}{0pt}%

+        \setlength{\labelwidth}{\hintscolumnwidth}%

+        \setlength{\labelsep}{\separatorcolumnwidth}%

+        \leftmargin\labelwidth%

+        \advance\leftmargin\labelsep%

+        \@openbib@code%

+        \usecounter{enumiv}%

+        \let\p@enumiv\@empty%

+        \renewcommand\theenumiv{\@arabic\c@enumiv}}%

+        \sloppy\clubpenalty4000\widowpenalty4000%

+%        \sfcode`\.\@m%

+%        \sfcode `\=1000\relax%

+  }%

+  {%

+    \def\@noitemerr{\@latex@warning{Empty `thebibliography' environment}}%

+    \end{list}%

+  }

+

+

+%-------------------------------------------------------------------------------

+%                letter style definition

+%-------------------------------------------------------------------------------

+% commands

+\renewcommand*{\recomputeletterlengths}{%

+  \recomputecvlengths%

+  \setlength{\parskip}{6\p@}}

+

+\renewcommand*{\makelettertitle}{%

+  % recompute lengths (in case we are switching from letter to resume, or vice versa)

+  \recomputeletterlengths%

+  % sender contact info

+  \hfill%

+  \begin{minipage}{.5\textwidth}%

+    \raggedleft%

+    \addressfont\textcolor{color2}{%

+      {\bfseries\upshape\@firstname~\@lastname}\@firstdetailselementfalse%

+      \ifthenelse{\isundefined{\@addressstreet}}{}{\makenewline\addresssymbol\@addressstreet%

+        \ifthenelse{\equal{\@addresscity}{}}{}{\makenewline\@addresscity}% if \addresstreet is defined, \addresscity and addresscountry will always be defined but could be empty

+        \ifthenelse{\equal{\@addresscountry}{}}{}{\makenewline\@addresscountry}}%

+      \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number

+        \makenewline\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}%

+      \ifthenelse{\isundefined{\@email}}{}{\makenewline\emailsymbol\emaillink{\@email}}%

+      \ifthenelse{\isundefined{\@homepage}}{}{\makenewline\homepagesymbol\httplink{\@homepage}}%

+      \ifthenelse{\isundefined{\@extrainfo}}{}{\makenewline\@extrainfo}}%

+    \end{minipage}\\[1em]

+  % recipient block

+  \begin{minipage}[t]{.5\textwidth}

+    \raggedright%

+    \addressfont%

+    {\bfseries\upshape\@recipientname}\\%

+    \@recipientaddress%

+  \end{minipage}

+  % date

+  \hfill% US style

+%  \\[1em]% UK style

+  \@date\\[2em]% US informal style: "January 1, 1900"; UK formal style: "01/01/1900"

+  % opening

+  \raggedright%

+  \@opening\\[1.5em]%

+  % ensure no extra spacing after \makelettertitle due to a possible blank line

+%  \ignorespacesafterend% not working

+  \hspace{0pt}\par\vspace{-\baselineskip}\vspace{-\parskip}}

+

+\renewcommand*{\makeletterclosing}{

+  \@closing\\[3em]%

+  {\bfseries \@firstname~\@lastname}%

+  \ifthenelse{\isundefined{\@enclosure}}{}{%

+    \\%

+    \vfill%

+    {\color{color2}\itshape\enclname: \@enclosure}}}

+

+

+\endinput

+

+

+%% end of file `moderncvstyleclassic.sty'.

diff --git a/users/grfn/resume/moderncvstyleempty.sty b/users/grfn/resume/moderncvstyleempty.sty
new file mode 100644
index 0000000000..85932464d1
--- /dev/null
+++ b/users/grfn/resume/moderncvstyleempty.sty
@@ -0,0 +1,34 @@
+%% start of file `moderncvstyleempty.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvstyleempty}[2013/02/09 v1.3.0 modern curriculum vitae scheme: empty]
+
+
+%-------------------------------------------------------------------------------
+%                required packages
+%-------------------------------------------------------------------------------
+
+
+%-------------------------------------------------------------------------------
+%                package options
+%-------------------------------------------------------------------------------
+
+
+%-------------------------------------------------------------------------------
+%                style definition
+%-------------------------------------------------------------------------------
+% see moderncv.cls for command declarations that needs to be implemented, e.g. \maketitle, \section, \subsections, \cvline, etc
+
+\endinput
+
+
+%% end of file `moderncvstyleempty.sty'.
diff --git a/users/grfn/resume/moderncvstyleoldstyle.sty b/users/grfn/resume/moderncvstyleoldstyle.sty
new file mode 100644
index 0000000000..ff732f4e2a
--- /dev/null
+++ b/users/grfn/resume/moderncvstyleoldstyle.sty
@@ -0,0 +1,306 @@
+%% start of file `moderncvstyleoldstyle.sty'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+%-------------------------------------------------------------------------------
+%                identification
+%-------------------------------------------------------------------------------
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{moderncvstyleoldstyle}[2013/02/09 v1.3.0 modern curriculum vitae and letter style scheme: oldstyle]
+
+
+%-------------------------------------------------------------------------------
+%                required packages
+%-------------------------------------------------------------------------------
+% change the layout of the page on the fly, for resume or letter layout
+\RequirePackage{changepage}
+
+
+%-------------------------------------------------------------------------------
+%                overall style definition
+%-------------------------------------------------------------------------------
+% fonts
+%\ifxetexorluatex
+%  \setmainfont[Numbers={OldStyle,Proportional}, BoldFont={Kurier Bold}, ItalicFont={Kurier Light Italic}, BoldItalicFont={Kurier Bold Italic}]{Kurier Light}
+%  \setsansfont[Numbers={OldStyle,Proportional}, BoldFont={Kurier Bold}, ItalicFont={Kurier Light Italic}, BoldItalicFont={Kurier Bold Italic}]{Kurier Light}
+%  \setmathfont{Kurier Light}
+%  \setmathfont[range=\mathit,\mathsfit]{Kurier Light Italic}
+%  \setmathfont[range=\mathbfup,\mathbfsfup]{Kurier Bold}
+%  \setmathfont[range=\mathbfit,\mathbfsfit]{Kurier Bold Italic}
+%\else
+  \IfFileExists{kurier.sty}%
+    {\RequirePackage[light,math]{kurier}}%
+    {}
+%\fi
+
+% symbols
+\moderncvicons{letters}
+
+
+%-------------------------------------------------------------------------------
+%                resume style definition
+%-------------------------------------------------------------------------------
+% fonts
+\renewcommand*{\namefont}{\fontsize{34}{36}\mdseries\upshape}
+\renewcommand*{\titlefont}{\LARGE\mdseries\slshape}
+\renewcommand*{\addressfont}{\small\mdseries}
+\renewcommand*{\quotefont}{\large\itshape}
+\renewcommand*{\sectionfont}{\Large\bfseries\upshape}
+\renewcommand*{\subsectionfont}{\large\bfseries\itshape}
+\renewcommand*{\hintfont}{\bfseries}
+
+% styles
+\renewcommand*{\namestyle}[1]{{\namefont\textcolor{color0}{#1}}}
+\renewcommand*{\titlestyle}[1]{{\titlefont\textcolor{color2}{#1}}}
+\renewcommand*{\addressstyle}[1]{{\addressfont\textcolor{color2}{#1}}}
+\renewcommand*{\quotestyle}[1]{{\quotefont\textcolor{color1}{#1}}}
+\renewcommand*{\sectionstyle}[1]{{\sectionfont\textcolor{color1}{#1}}}
+\renewcommand*{\subsectionstyle}[1]{{\subsectionfont\textcolor{color1}{#1}}}
+\renewcommand*{\hintstyle}[1]{{\hintfont\textcolor{color0}{#1}}}
+
+% lengths
+\newlength{\quotewidth}
+\newlength{\hintscolumnwidth}
+\setlength{\hintscolumnwidth}{0.3\textwidth}%
+\newlength{\separatorcolumnwidth}
+\setlength{\separatorcolumnwidth}{0.025\textwidth}%
+\newlength{\maincolumnwidth}
+\newlength{\doubleitemcolumnwidth}
+\newlength{\listitemsymbolwidth}
+\settowidth{\listitemsymbolwidth}{\listitemsymbol}
+\newlength{\listitemmaincolumnwidth}
+\newlength{\listdoubleitemmaincolumnwidth}
+
+% commands
+\setlength{\marginparwidth}{0\p@}%
+\setlength{\marginparsep}{0\p@}
+\renewcommand*{\recomputecvlengths}{%
+  % regular lengths
+  \changepage{}{+\marginparwidth+\marginparsep}{}{}{}{}{}{}{}% if a letter was typeset before the resume, \marginparwidth and \marginparsep will be non-zero; otherwise, this has no effect
+  \setlength{\marginparwidth}{0\p@}%
+  \setlength{\marginparsep}{0\p@}
+  \setlength{\parskip}{0\p@}%
+  % maketitle lengths
+  \setlength{\quotewidth}{0.65\textwidth}%
+  % main lenghts
+  \setlength{\maincolumnwidth}{\textwidth-\hintscolumnwidth-\separatorcolumnwidth}%
+  % listitem lengths
+  \setlength{\listitemmaincolumnwidth}{\maincolumnwidth-\listitemsymbolwidth}%
+  % doubleitem lengths
+  \setlength{\doubleitemcolumnwidth}{\maincolumnwidth-\separatorcolumnwidth}%
+  \setlength{\doubleitemcolumnwidth}{0.5\doubleitemcolumnwidth}%
+  % listdoubleitem lengths
+  \setlength{\listdoubleitemmaincolumnwidth}{\maincolumnwidth-\listitemsymbolwidth-\separatorcolumnwidth-\listitemsymbolwidth}%
+  \setlength{\listdoubleitemmaincolumnwidth}{0.5\listdoubleitemmaincolumnwidth}%
+  % fancyhdr lengths
+  \renewcommand{\headwidth}{\textwidth}}
+
+\newcommand{\makecvinfo}[1]{%
+  \newbox{\makecvinfobox}%
+  \savebox{\makecvinfobox}{\parbox[t]{\hintscolumnwidth}{#1}}%
+  \newlength{\makecvinfoheight}%
+  \setlength{\makecvinfoheight}{\totalheightof{\usebox{\makecvinfobox}}}% the total height of the parbox is the sum of its height (\the\ht\makeinfobox) and its depth (\the\dp\makeinfobox); the \totalheightof command is provided by the "calc" package
+  \usebox{\makecvinfobox}\vspace{-\makecvinfoheight}%
+  \newlength{\leftcolumnwidth}%
+  \setlength{\leftcolumnwidth}{\hintscolumnwidth+\separatorcolumnwidth}%
+  \par\vspace{-\baselineskip}\vspace{-\parskip}\leftskip=\leftcolumnwidth}
+
+\renewcommand*{\makecvtitle}{
+  % recompute lengths (in case we are switching from letter to resume, or vice versa)
+  \recomputecvlengths%
+  % optional picture box
+  \newbox{\makecvtitlepicturebox}%
+  \savebox{\makecvtitlepicturebox}{%
+    \ifthenelse{\isundefined{\@photo}}%
+    {}%
+    {%
+      \color{color1}%
+      \setlength\fboxrule{\@photoframewidth}%
+      \ifdim\@photoframewidth=0pt%
+        \setlength{\fboxsep}{0pt}\fi%
+      \framebox{\includegraphics[width=\@photowidth]{\@photo}}}}%
+  % name and title
+  \newlength{\makecvtitlepicturewidth}\settowidth{\makecvtitlepicturewidth}{\usebox{\makecvtitlepicturebox}}%
+  \newlength{\makecvtitlenamewidth}\setlength{\makecvtitlenamewidth}{\textwidth-\makecvtitlepicturewidth}%
+  \begin{minipage}[b]{\makecvtitlenamewidth}%
+    \namestyle{\@firstname\ \@lastname}%
+    \ifthenelse{\equal{\@title}{}}{}{\\[1.25em]\titlestyle{\@title}}%
+  \end{minipage}%
+  % optional photo
+  \usebox{\makecvtitlepicturebox}\\[2.5em]%
+   % optional quote
+  \ifthenelse{\isundefined{\@quote}}%
+    {}%
+    {{\centering\begin{minipage}{\quotewidth}\centering\quotestyle{\@quote}\end{minipage}\\[2.5em]}}%
+  % optional details
+  \makecvinfo{%
+    \addressfont\color{color2}%
+    \ifthenelse{\isundefined{\@addressstreet}}{}{\makenewline\addresssymbol\@addressstreet%
+      \ifthenelse{\equal{\@addresscity}{}}{}{\makenewline\@addresscity}% if \addresstreet is defined, \addresscity and \addresscountry will always be defined but could be empty
+      \ifthenelse{\equal{\@addresscountry}{}}{}{\makenewline\@addresscountry}}%
+    \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number
+      \makenewline\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}%
+    \ifthenelse{\isundefined{\@email}}{}{\makenewline\emailsymbol\emaillink{\@email}}%
+    \ifthenelse{\isundefined{\@homepage}}{}{\makenewline\homepagesymbol\httplink{\@homepage}}%
+    \ifthenelse{\isundefined{\@extrainfo}}{}{\makenewline\@extrainfo}}}
+
+\RenewDocumentCommand{\section}{sm}{%
+  \par\addvspace{2.5ex}%
+  \phantomsection{}% reset the anchor for hyperrefs
+  \addcontentsline{toc}{section}{#2}%
+  \strut\sectionstyle{#2}%
+  \par\nobreak\addvspace{1ex}\@afterheading}% to avoid a pagebreak after the heading
+
+\RenewDocumentCommand{\subsection}{sm}{%
+  \par\addvspace{1ex}%
+  \phantomsection{}% reset the anchor for hyperrefs
+  \addcontentsline{toc}{subsection}{#2}%
+  \strut\subsectionstyle{#2}%
+  \par\nobreak\addvspace{0.5ex}\@afterheading}% to avoid a pagebreak after the heading
+
+\renewcommand*{\cvitem}[3][.25em]{%
+  \ifthenelse{\equal{#2}{}}{}{\hintstyle{#2}: }{#3}%
+  \par\addvspace{#1}}
+
+\renewcommand*{\cvdoubleitem}[5][.25em]{%
+  \begin{minipage}[t]{\doubleitemcolumnwidth}\hintstyle{#2}: #3\end{minipage}%
+  \hfill% fill of \separatorcolumnwidth
+  \begin{minipage}[t]{\doubleitemcolumnwidth}\ifthenelse{\equal{#4}{}}{}{\hintstyle{#4}: }#5\end{minipage}%
+  \par\addvspace{#1}}
+
+\renewcommand*{\cvlistitem}[2][.25em]{%
+  \cvitem[#1]{}{\listitemsymbol\begin{minipage}[t]{\listitemmaincolumnwidth}#2\end{minipage}}}
+
+\renewcommand*{\cvlistdoubleitem}[3][.25em]{%
+  \cvitem[#1]{}{\listitemsymbol\begin{minipage}[t]{\listdoubleitemmaincolumnwidth}#2\end{minipage}%
+  \hfill% fill of \separatorcolumnwidth
+  \ifthenelse{\equal{#3}{}}%
+    {}%
+    {\listitemsymbol\begin{minipage}[t]{\listdoubleitemmaincolumnwidth}#3\end{minipage}}}}
+
+\newbox{\cventryyearbox}
+\newlength{\cventrytitleboxwidth}
+\renewcommand*{\cventry}[7][.25em]{%
+  \savebox{\cventryyearbox}{%
+    \hspace*{2\separatorcolumnwidth}%
+    \hintstyle{#2}}%
+  \setlength{\cventrytitleboxwidth}{\widthof{\usebox{\cventryyearbox}}}%
+  \setlength{\cventrytitleboxwidth}{\maincolumnwidth-\cventrytitleboxwidth}%
+  \begin{minipage}{\maincolumnwidth}%
+    \parbox[t]{\cventrytitleboxwidth}{%
+      \strut%
+      {\bfseries#3}%
+      \ifthenelse{\equal{#4}{}}{}{, {\slshape#4}}%
+      \ifthenelse{\equal{#5}{}}{}{, #5}%
+      \ifthenelse{\equal{#6}{}}{}{, #6}%
+      .\strut}%
+    \usebox{\cventryyearbox}%
+  \end{minipage}%
+  \ifx&#7&%
+    \else{%
+      \newline{}%
+      \begin{minipage}[t]{\maincolumnwidth}%
+        \small%
+        #7%
+      \end{minipage}}\fi%
+  \par\addvspace{#1}}
+
+\newbox{\cvitemwithcommentmainbox}
+\newlength{\cvitemwithcommentmainlength}
+\newlength{\cvitemwithcommentcommentlength}
+\renewcommand*{\cvitemwithcomment}[4][.25em]{%
+  \savebox{\cvitemwithcommentmainbox}{\ifthenelse{\equal{#2}{}}{}{\hintstyle{#2}: }#3}%
+  \setlength{\cvitemwithcommentmainlength}{\widthof{\usebox{\cvitemwithcommentmainbox}}}%
+  \setlength{\cvitemwithcommentcommentlength}{\maincolumnwidth-\separatorcolumnwidth-\cvitemwithcommentmainlength}%
+  \begin{minipage}[t]{\cvitemwithcommentmainlength}\ifthenelse{\equal{#2}{}}{}{\hintstyle{#2}: }#3\end{minipage}%
+  \hfill% fill of \separatorcolumnwidth
+  \begin{minipage}[t]{\cvitemwithcommentcommentlength}\raggedleft\small\itshape#4\end{minipage}%
+  \par\addvspace{#1}}
+
+\renewenvironment{thebibliography}[1]%
+  {%
+    \bibliographyhead{\refname}%
+%    \small%
+    \begin{list}{\bibliographyitemlabel}%
+      {%
+        \setlength{\topsep}{0pt}%
+        \setlength{\labelwidth}{\hintscolumnwidth}%
+        \setlength{\labelsep}{\separatorcolumnwidth}%
+        \leftmargin\labelwidth%
+        \advance\leftmargin\labelsep%
+        \@openbib@code%
+        \usecounter{enumiv}%
+        \let\p@enumiv\@empty%
+        \renewcommand\theenumiv{\@arabic\c@enumiv}}%
+        \sloppy\clubpenalty4000\widowpenalty4000%
+%        \sfcode`\.\@m%
+%        \sfcode `\=1000\relax%
+  }%
+  {%
+    \def\@noitemerr{\@latex@warning{Empty `thebibliography' environment}}%
+    \end{list}%
+  }
+
+
+%-------------------------------------------------------------------------------
+%                letter style definition
+%-------------------------------------------------------------------------------
+% commands
+%\newlength{\textwidthdelta}%
+\renewcommand*{\recomputeletterlengths}{%
+  \recomputecvlengths%
+  \setlength{\parskip}{6\p@}%
+  \leftskip=0pt%
+%  \setlength{\textwidthdelta}{+\marginparwidth+\marginparsep}%
+  \setlength{\marginparwidth}{\hintscolumnwidth}%
+  \setlength{\marginparsep}{2\separatorcolumnwidth}%
+%  \addtolength{\textwidthdelta}{-\marginparwidth-\marginparsep}%
+%  \changepage{}{\textwidthdelta}{-\textwidthdelta}{}{}{}{}{}{}%\changepage{<textheight>}{<textwidth>}{<evensidemargin>}{<oddsidemargin>}{<columnsep>}{<topmargin>}{<headheight>}{<headsep>}{<footskip>}
+  \changepage{}{-\marginparwidth-\marginparsep}{}{}{}{}{}{}{}%\changepage{<textheight>}{<textwidth>}{<evensidemargin>}{<oddsidemargin>}{<columnsep>}{<topmargin>}{<headheight>}{<headsep>}{<footskip>}
+  }
+
+\renewcommand*{\makelettertitle}{%
+  % recompute lengths (in case we are switching from letter to resume, or vice versa)
+  \recomputeletterlengths%
+  % recipient block
+  {\addressfont%
+    {\bfseries\upshape\@recipientname}\\%
+    \@recipientaddress}\\[1em]%
+  % date
+  \@date\\[2em]%
+  % opening
+  \@opening\\[1.5em]%
+  % sender contact info
+  \hspace{0pt}%
+  \marginpar{%
+    \addressfont\textcolor{color2}{%
+      {\bfseries\@firstname~\@lastname}\@firstdetailselementfalse%
+      \ifthenelse{\isundefined{\@addressstreet}}{}{\makenewline\addresssymbol\@addressstreet%
+        \ifthenelse{\equal{\@addresscity}{}}{}{\makenewline\@addresscity}% if \addresstreet is defined, \addresscity and \addresscountry will always be defined but could be empty
+        \ifthenelse{\equal{\@addresscountry}{}}{}{\makenewline\@addresscountry}}%
+      \collectionloop{phones}{% the key holds the phone type (=symbol command prefix), the item holds the number
+        \makenewline\csname\collectionloopkey phonesymbol\endcsname\collectionloopitem}%
+      \ifthenelse{\isundefined{\@email}}{}{\makenewline\emailsymbol\emaillink{\@email}}%
+      \ifthenelse{\isundefined{\@homepage}}{}{\makenewline\homepagesymbol\httplink{\@homepage}}%
+      \ifthenelse{\isundefined{\@extrainfo}}{}{\makenewline\@extrainfo}}}%
+  % ensure no extra spacing after \makelettertitle due to a possible blank line
+%  \ignorespacesafterend% not working
+  \par\vspace{-\baselineskip}\vspace{-\parskip}}
+
+\renewcommand*{\makeletterclosing}{
+  \@closing\\[3em]%
+  {\bfseries\@firstname~\@lastname}%
+  \ifthenelse{\isundefined{\@enclosure}}{}{%
+    \\%
+    \vfill%
+    {\color{color2}\itshape\enclname: \@enclosure}}}
+
+
+\endinput
+
+
+%% end of file `moderncvstyleoldstyle.sty'.
diff --git a/users/grfn/resume/picture.png b/users/grfn/resume/picture.png
new file mode 100644
index 0000000000..63b21b5320
--- /dev/null
+++ b/users/grfn/resume/picture.png
Binary files differdiff --git a/users/grfn/resume/resume.tex b/users/grfn/resume/resume.tex
new file mode 100644
index 0000000000..933558d570
--- /dev/null
+++ b/users/grfn/resume/resume.tex
@@ -0,0 +1,212 @@
+%% start of file `template.tex'.
+%% Copyright 2006-2013 Xavier Danaux (xdanaux@gmail.com).
+%% Copyright 2014-2020 Griffin Smith (wildgriffin45@gmail.com).
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+\documentclass[10pt,a4paper,sans]{moderncv}        % possible options include font size ('10pt', '11pt' and '12pt'), paper size ('a4paper', 'letterpaper', 'a5paper', 'legalpaper', 'executivepaper' and 'landscape') and font family ('sans' and 'roman')
+
+\usepackage[inline]{enumitem}
+
+
+% moderncv themes
+% style options are 'casual' (default), 'classic', 'oldstyle' and 'banking'
+\moderncvstyle{casual}
+% color options 'blue' (default), 'orange', 'green', 'red', 'purple', 'grey' and 'black'
+\moderncvcolor{black}
+% to set the default font; use '\sfdefault' for the default sans serif font,
+% '\rmdefault' for the default roman one, or any tex font name
+%\renewcommand{\familydefault}{\sfdefault}
+\nopagenumbers{}
+
+\usepackage[utf8]{inputenc}
+
+\usepackage[scale=0.8, margin=0.65in]{geometry}
+\setlength{\hintscolumnwidth}{2.6cm}
+
+\name{Griffin}{Smith}
+\title{Software Engineer}
+\phone[mobile]{(720) 206-7218}
+\email{grfn@gws.fyi}
+\homepage{https://www.gws.fyi}
+\extrainfo{References available upon request}
+
+\begin{document}
+\makecvtitle{}
+\section{Skills}
+\cvitem{Clojure}{Extensive experience architecting, deploying, and building
+complex web applications in Clojure and Clojurescript, with a focus on
+Re-Frame and Reagent.}
+\cvitem{Haskell}{Passionate love for pure functional programming as a hobbyist
+pursuit, but also practical experience building production systems in Haskell at
+scale, and using Haskell's advanced type system extensions where appropriate to
+deliver increased ergonomics and safety.}
+\cvitem{Nix}{Experience with adopting and teaching nix at scale in a production
+stack both for local development dependencies and for configuring and building
+production software. Core contributer to a fork of the nix implementation itself
+(tvix) aimed at providing increased safety, performance, and flexibility.}
+\cvitem{Scala}{Understanding of Scala from the perspective of a functional
+programmer rather than a Java programmer. Experience building production
+big-data processing systems using Akka, and deep programming with Scala's type
+system using Shapeless.}
+\cvitem{Unix/Linux}{Experience with administrating highly available distributed
+systems. Passion for the Unix philosophy of discrete, composable units of
+functionality.}
+\cvitem{Ruby}{Experience building both full-stack applications with Ruby on
+Rails in addition to smaller microservices and custom frameworks. Deep
+understanding of the internals of the Ruby interpreter and object system.}
+\cvitem{Javascript}{Experience developing real-time responsive single-page web
+applications using React, in addition to significant contributions to the React
+open-source community.}
+\cvitem{SQL}{Deep understanding of relational databases, including experience
+designing the database schema in Postgres for an application with over a decade
+of usage, hundreds of gigabytes of data, complex, multi-tiered hierarchical data
+structures, as well as experience writing and optimizing large, complex queries
+against that database.}
+
+\subsection{Additional Tools}
+\cvitem{}{\footnotesize
+    \begin{itemize*}
+        \item Vim
+        \item Kubernetes
+        \item Git
+        \item Puppet
+        \item AWS
+        \item Reagent
+        \item Datomic
+        \item Elasticsearch
+        \item Redis
+        \item DynamoDB
+        \item Docker
+        \item JIRA
+        \item Java
+        \item QuickCheck (and similar tools)
+        \item Python
+        \item Elixir
+    \end{itemize*}
+    \newline
+    \textbf{Novice Level:}
+    \begin{itemize*}
+        \item Rust
+        \item C++
+        \item Erlang
+        \item Prolog
+        \item Idris
+        \item Agda
+        \item Tensorflow
+    \end{itemize*}}
+
+\section{Experience}
+\subsection{Employment}
+\cventry{2019-present}{Engineering Manager}{Urbint}{New York, NY}{}
+{\begin{itemize}
+   \item Lead of the platform team with two direct reports - a senior SRE and
+     a senior software engineer.
+   \item Performed user research on developers, project managers, product
+     managers, and other internal stakeholders to build the roadmap for the
+     platform team.
+   \item Built and maintained a system to deploy one-off full stack
+     application instances from pull requests to enable easier testing.
+   \item Led a large, multi-project migration between CI systems that resulted
+     in a decrease of average build times from 2 hours to less than 10 minutes.
+   \item Maintained and extended Nix-based build and development
+     infrastructure for both software engineers and machine learning engineers.
+ \end{itemize}}
+\cventry{2018--2019}{Senior Software Engineer}{Urbint}{New York, NY}{}
+{\begin{itemize}
+   \item Built, trained, and maintained a large, deep-learning-based
+     image-detection model for semi-automated (human-in-the-loop) video
+     classification.
+   \item Designed, built, and maintained a novel in-house tool for collection of
+     training data.
+   \item Maintained and guaranteed reliability of a large data pipeline for
+     video processing and classification.
+ \end{itemize}}
+\cventry{2017--2018}{Senior Software Engineer}{Urbint}{New York, NY}{}
+{\begin{itemize}
+   \item Integral in the architecture of a novel, serializable ACID
+     transactional graph database built on RocksDB, first in Elixir then in
+     Haskell.
+   \item Helped ship customer deliverables involving multi-day data
+     processing jobs for disparate data sources.
+   \item Instructed other developers in the use of and theory behind Haskell
+   \item Brought computational graph theory to bear on the problem of unifying
+     disparate, highly heterogeneous data sources across the world of open data.
+ \end{itemize}}
+\cventry{2016--2017}{Senior Software Engineer}{SecurityScorecard, Inc.}{New York, NY}{}
+{Lead frontend developer for a rapidly-moving and growing security software startup.
+  \begin{itemize}
+    \item Took part in collaborative product design meetings to make UX
+      tradeoffs with product designers and managers.
+    \item Drove application architecture for a large, complex, data-driven frontend
+      application.
+    \item Championed increased use of production monitoring and alerting.
+    \item Worked with business stakeholders to set long- and short-term priorities for
+      application development.
+    \item Mentored junior team members.
+  \end{itemize}}
+\cventry{2015--2016}{Lead Developer}{Nomi, Inc.}{New York, NY}{}
+{Lead web services developer transitioning to a full-stack role implementing
+  shared software components and architecting a large, complex microservices
+  application ingesting hundreds of gigabytes of IoT data per week.
+  \begin{itemize}
+    \item Lead application architecture of the majority of the backend services to
+      encourage consistent REST API design and code sharing.
+    \item Championed the use of Haskell for rapid, safe development of the API Gateway
+      service.
+    \item Took ownership of operations and server maintenance of a >100-instance AWS
+      account using Puppet.
+  \end{itemize}}
+\cventry{2014--2015}{Lead Developer}{LandlordsNY, LLC}{New York, NY}{}
+{Sole engineer for a small startup connecting landlords and property managers and
+  facilitating the online sharing of information in a historically technology-averse
+  industry.
+  \begin{itemize}
+    \item Drove product design, visual design, and UX architecture for a major revamping
+      of the core product.
+    \item Interfaced with customers to set priorities for new feature development.
+    \item Conducted hiring and recruiting to build out an engineering team.
+  \end{itemize}}
+\cventry{2012--2014}{Associate Developer}{Visionlink Inc.}{Boulder, CO}{}
+{Integral member of an agile development team building the nation's most-used Information
+  and Referral platform for organizations such as United Way Worldwide and the American Red
+  Cross.
+  \begin{itemize}
+    \item Refactored and revamped legacy code to increase performance and long-term
+      maintainablity.
+    \item Worked on several triage-teams to rapidly fix production bugs with strict deadlines.
+    \item Built a complex, yet highly-performant tool for searching human services by category.
+    \item Acted as a core designer and developer of a major product revamp.
+      \begin{itemize}
+        \item Drove a complete rethinking of the data model in the product, leading to greater
+          unification, simplicity, and consistency;
+        \item Championed the adoption of a test-driven-development model;
+        \item Drove product documentation and code standardization.
+      \end{itemize}
+  \end{itemize}}
+
+\section{Project Highlights}
+\newcommand{\project}[3]{\item \textbf{#1} -- \textit{#2}\newline{}#3}
+\cvitem{}{\begin{itemize}
+  \project{Github Bug Bounty}{https://bounty.github.com/researchers/glittershark.html}{
+    Discovered and responsibly disclosed a persistent XSS on Github's main
+    website}
+  \project{Tvix}{https://cs.tvl.fyi/depot/-/blob/third\_party/nix/README.md}{
+    Fork of the Nix build tool delivering increased reliability, code
+    quality, and pluggability}
+  \project{Panettone}{https://cs.tvl.fyi/depot/-/tree/web/panettone}{
+    Aggressively simple bug-tracker developed in Common Lisp for the community
+    involved in the development of Tvix. Hosted at https://b.tvl.fyi}
+  \project{Org-Clubhouse}{https://github.com/glittershark/org-clubhouse}{
+    Emacs library for integration between org-mode and the Clubhouse issue
+    tracker}
+  \project{core-async-storage}{https://github.com/glittershark/core-async-storage}{
+    Simple Clojurescript wrapper around React Native's AsyncStorage using
+    core.async}
+\end{itemize}}
+
+\end{document}
+% vim: set tw=95 colorcolumn=-1:
diff --git a/users/grfn/resume/tweaklist.sty b/users/grfn/resume/tweaklist.sty
new file mode 100644
index 0000000000..adc9398932
--- /dev/null
+++ b/users/grfn/resume/tweaklist.sty
@@ -0,0 +1,56 @@
+%% start of file `tweaklist.sty'.
+%% Original by Jakob Schiøtz, downloaded from http://dcwww.camd.dtu.dk/~schiotz/comp/LatexTips/tweaklist.sty; not found on ctan.
+%% Modified by Xavier Danaux (xdanaux@gmail.com).
+%
+% The tweaklist.sty package redefines the itemize, enumerate and description packages, so that all parameters can be adjusted.
+% This was done by copying the original definitions, and adding "hook commands" that are executed when entering the environment.
+% The hook commands are initially empty, but can be redefined with \renewcommand.
+%
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License version 1.3c,
+% available at http://www.latex-project.org/lppl/.
+
+
+% hooks for the itemize environment
+\def\itemhook{}
+\def\itemhooki{}
+\def\itemhookii{}
+\def\itemhookiii{}
+\def\itemhookiv{}
+% hooks for the enumerate environment
+\def\enumhook{}
+\def\enumhooki{}
+\def\enumhookii{}
+\def\enumhookiii{}
+\def\enumhookiv{}
+% hook for the description environment
+\def\deschook{}
+% original environment definitions, with hooks added
+\def\enumerate{%
+  \ifnum \@enumdepth >\thr@@\@toodeep\else
+    \advance\@enumdepth\@ne
+    \edef\@enumctr{enum\romannumeral\the\@enumdepth}%
+      \expandafter
+      \list
+        \csname label\@enumctr\endcsname
+        {%
+          \enumhook \csname enumhook\romannumeral\the\@enumdepth\endcsname%
+          \usecounter\@enumctr\def\makelabel##1{\hss\llap{##1}}%
+        }%
+  \fi}
+\def\itemize{%
+  \ifnum \@itemdepth >\thr@@\@toodeep\else
+    \advance\@itemdepth\@ne
+    \edef\@itemitem{labelitem\romannumeral\the\@itemdepth}%
+    \expandafter
+    \list
+      \csname\@itemitem\endcsname
+      {%
+        \itemhook \csname itemhook\romannumeral\the\@itemdepth\endcsname%
+        \def\makelabel##1{\hss\llap{##1}}%
+      }%
+  \fi}
+\newenvironment{description}
+  {\list{}{\deschook\labelwidth\z@ \itemindent-\leftmargin
+           \let\makelabel\descriptionlabel}}
+  {\endlist}
diff --git a/users/grfn/secrets/.envrc b/users/grfn/secrets/.envrc
new file mode 100644
index 0000000000..051d09d292
--- /dev/null
+++ b/users/grfn/secrets/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
diff --git a/users/grfn/secrets/bbbg.age b/users/grfn/secrets/bbbg.age
new file mode 100644
index 0000000000..6c15dcdf73
--- /dev/null
+++ b/users/grfn/secrets/bbbg.age
@@ -0,0 +1,12 @@
+age-encryption.org/v1
+-> ssh-ed25519 CpJBgQ 6vLlq2WEcn6TE0rgahQyl7CYhCF3uiBD3hOnZkHswmM
+BSUiKPdDWMhYbi/+j9Kw5YDEOvjaickYQuhpWkhLktQ
+-> ssh-ed25519 LfBFbQ mQJfyk35Ghd7UWouPlq4kTIFFwlRGh24r0kvJUgUbBw
+eYpBJEG9Cdc2qHI0maFpp9/2o30R0KGLRSQ7DzsVaZ0
+-> ssh-ed25519 lZtaEQ npyXpqTMWITvRVfPwEQ1rXJ0sxnJvurLOfeiE07m92E
+oCXVRGOegBgQUJof8UHJsDdMyNsx6X575Rd4mWZ9LRk
+-> ;^O0_l-grease
+sseb4RnQz93Wlgs5B0PE+j7AzFyMkzHjFbn9sCn0UA
+--- Bqq0uedob5/kJOSoavN7Aq1fH7QVNW134M3uS6u2lFA
+r3?ujF !_R/#BQ
+Nה"V1mwly's P^6K{t3m%'zOo8^SJxꨯR=ŕzEz*Ѥ>g뻐%>\)Lj05VQ8/HXOGs7gDGIsNpiuYFj?x;]
\ No newline at end of file
diff --git a/users/grfn/secrets/buildkite-ssh-key.age b/users/grfn/secrets/buildkite-ssh-key.age
new file mode 100644
index 0000000000..0ae5aa5502
--- /dev/null
+++ b/users/grfn/secrets/buildkite-ssh-key.age
Binary files differdiff --git a/users/grfn/secrets/buildkite-token.age b/users/grfn/secrets/buildkite-token.age
new file mode 100644
index 0000000000..9e9e370f1b
--- /dev/null
+++ b/users/grfn/secrets/buildkite-token.age
@@ -0,0 +1,12 @@
+age-encryption.org/v1
+-> ssh-ed25519 CpJBgQ tz7tudrJYQw2Ftnk7iNbSd/De2UJ0GAafFJjPwUo8xM
+bUBNO94Pjf79FErPxv92XnpXWFEgethREU+/U+xjWBc
+-> ssh-ed25519 LfBFbQ yPjXk6XlJoGyVaCWMcPzfNXzb1cBNZhjYy+wsQtMhTI
+qk6hZMl1oeKLniGb/bKIxSb6ocVRCQsmQPcwxnlYfno
+-> \'q-grease
+nYvpKokvFbVXfATzlQ7SPQa9Gw99E84SPRFdR7ey+HSCB705Q9uYwBpr9hjpiIod
+9PJIi88ENWf9/XAmm2d7daE+YPRYhln4U6w
+--- EuyCLA6GvtbGI+EoC1z2dbpfyxo4ebXX1nY+9rsgUVY
+[hΩЪ`1?NC@uBl8*ՈsZ~PА?8
+
+O~{G}0q.AW
\ No newline at end of file
diff --git a/users/grfn/secrets/cloudflare.age b/users/grfn/secrets/cloudflare.age
new file mode 100644
index 0000000000..e2f6e93603
--- /dev/null
+++ b/users/grfn/secrets/cloudflare.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> ssh-ed25519 CpJBgQ tWx7wXCFjOOfD0wKRHHvLUdR+SF0i43xvnQG9GKurnk
+NRh7kSn7wqw80Y9EFr9Ccft+zYMadXZhYNPEaQlQXtQ
+-> ssh-ed25519 LfBFbQ SPQMLC3Ehw00IG1CcbcLFZI2tHy89fjRgVgH4Iw2iBM
+oo2gT9472/DFRoZ6TYxhnM9ylRUNzoS8mLQYvn+4OSM
+-> D[7+*-grease `>j ~Jk Dz%o vaKET3
+TkKVm8IpqfiVzETAi9+zuUtCdkReB+lHtthwNw
+--- 3iOmY4TNICMi/Fz7k8pmoZlFym9uQBWNtHNlizoAMaM
+ZPzQ65ATI;;Зy5]k^!`t$RւtK)<k_#XmASpU1@)cֺqj1z,Hg:
\ No newline at end of file
diff --git a/users/grfn/secrets/ddclient-password.age b/users/grfn/secrets/ddclient-password.age
new file mode 100644
index 0000000000..0de8707105
--- /dev/null
+++ b/users/grfn/secrets/ddclient-password.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> ssh-ed25519 CpJBgQ 1Yw1EllkiG38qEQ03eN1p8WzC74zKb6YIuZMb3RD0ik
+P7iAo2rCex8XkCyWKjTSecAZDg5wokrfBLRk/Nl479w
+-> ssh-ed25519 LfBFbQ /cMWDtN+SlCs7WsomkngPpPK/4RHpCDZr2bg+jyqHEs
+bgnNTXhxYtW84twA7ty9RWgycABW0MI9OEk69TRT2ro
+-> Ujl-grease l_8cO.F
+pw/kCMvXCg4my4M
+--- m3jMrTCJFA1bGgKERiAhAYvXt/++wWzva0CVdtz3cgQ
+]>'C[**j!`7<QmI7tGW ϳ;{Janpש`bt
\ No newline at end of file
diff --git a/users/grfn/secrets/default.nix b/users/grfn/secrets/default.nix
new file mode 100644
index 0000000000..26b1998f56
--- /dev/null
+++ b/users/grfn/secrets/default.nix
@@ -0,0 +1,2 @@
+{ depot, ... }:
+depot.ops.secrets.mkSecrets ./. (import ./secrets.nix)
diff --git a/users/grfn/secrets/secrets.nix b/users/grfn/secrets/secrets.nix
new file mode 100644
index 0000000000..986ad181b8
--- /dev/null
+++ b/users/grfn/secrets/secrets.nix
@@ -0,0 +1,13 @@
+let
+  grfn = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMcBGBoWd5pPIIQQP52rcFOQN3wAY0J/+K2fuU6SffjA";
+  mugwump = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFE2fxPgWO+zeQoLBTgsgxP7Vg7QNHlrQ+Rb3fHFTomB";
+  bbbg = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/VzrNEY47KPTce3dgfORkAbweWkr4BI8j54BAIs7bG";
+in
+
+{
+  "bbbg.age".publicKeys = [ grfn mugwump bbbg ];
+  "cloudflare.age".publicKeys = [ grfn mugwump ];
+  "ddclient-password.age".publicKeys = [ grfn mugwump ];
+  "buildkite-ssh-key.age".publicKeys = [ grfn mugwump ];
+  "buildkite-token.age".publicKeys = [ grfn mugwump ];
+}
diff --git a/users/grfn/secrets/shell.nix b/users/grfn/secrets/shell.nix
new file mode 100644
index 0000000000..6e70458d19
--- /dev/null
+++ b/users/grfn/secrets/shell.nix
@@ -0,0 +1,8 @@
+let
+  depot = import ../../.. { };
+in
+depot.third_party.nixpkgs.mkShell {
+  buildInputs = [
+    depot.third_party.agenix.cli
+  ];
+}
diff --git a/users/grfn/system/.gitignore b/users/grfn/system/.gitignore
new file mode 100644
index 0000000000..41fbeb02c4
--- /dev/null
+++ b/users/grfn/system/.gitignore
@@ -0,0 +1 @@
+**/result
diff --git a/users/grfn/system/home/.skip-subtree b/users/grfn/system/home/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/grfn/system/home/.skip-subtree
diff --git a/users/grfn/system/home/common/solarized.nix b/users/grfn/system/home/common/solarized.nix
new file mode 100644
index 0000000000..554ee0523e
--- /dev/null
+++ b/users/grfn/system/home/common/solarized.nix
@@ -0,0 +1,18 @@
+rec {
+  base03 = "#002B36";
+  base02 = "#073642";
+  base01 = "#586e75";
+  base00 = "#657b83";
+  base0 = "#839496";
+  base1 = "#93a1a1";
+  base2 = "#eee8d5";
+  base3 = "#fdf6e3";
+  yellow = "#b58900";
+  orange = "#cb4b16";
+  red = "#dc322f";
+  magenta = "#d33682";
+  violet = "#6c71c4";
+  blue = "#268bd2";
+  cyan = "#2aa198";
+  green = "#859900";
+}
diff --git a/users/grfn/system/home/default.nix b/users/grfn/system/home/default.nix
new file mode 100644
index 0000000000..874d7b2cb2
--- /dev/null
+++ b/users/grfn/system/home/default.nix
@@ -0,0 +1,32 @@
+{ pkgs, depot, lib, ... }:
+
+with lib;
+
+rec {
+  home = confPath: (import (pkgs.home-manager.src + "/modules") {
+    inherit pkgs;
+
+    configuration = { config, lib, ... }: {
+      imports = [ confPath ];
+      lib.depot = depot;
+
+      # home-manager exposes no API to override the package set that
+      # is used, unless called from the NixOS module.
+      #
+      # To get around it, the module argument is overridden here.
+      _module.args.pkgs = mkForce pkgs;
+    };
+  });
+
+  dobharchu = home ./machines/dobharchu.nix;
+
+  dobharchuHome = dobharchu.activation-script;
+
+  yeren = home ./machines/yeren.nix;
+
+  yerenHome = yeren.activation-script;
+
+  meta.ci.targets = [
+    "yerenHome"
+  ];
+}
diff --git a/users/grfn/system/home/home.nix b/users/grfn/system/home/home.nix
new file mode 100644
index 0000000000..39045c147d
--- /dev/null
+++ b/users/grfn/system/home/home.nix
@@ -0,0 +1,20 @@
+{ config, pkgs, ... }:
+
+{
+  imports = [
+    (throw "Pick a machine from ./machines")
+  ];
+
+  # Let Home Manager install and manage itself.
+  programs.home-manager.enable = true;
+
+  # This value determines the Home Manager release that your
+  # configuration is compatible with. This helps avoid breakage
+  # when a new Home Manager release introduces backwards
+  # incompatible changes.
+  #
+  # You can update Home Manager without changing this value. See
+  # the Home Manager release notes for a list of state version
+  # changes in each release.
+  home.stateVersion = "19.09";
+}
diff --git a/users/grfn/system/home/machines/dobharchu.nix b/users/grfn/system/home/machines/dobharchu.nix
new file mode 100644
index 0000000000..0b8503a00e
--- /dev/null
+++ b/users/grfn/system/home/machines/dobharchu.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../platforms/darwin.nix
+    ../modules/common.nix
+    ../modules/games.nix
+  ];
+
+  home.packages = with pkgs; [
+    coreutils
+    gnupg
+    nix-prefetch-github
+    pass
+    pinentry_mac
+  ];
+}
diff --git a/users/grfn/system/home/machines/roswell.nix b/users/grfn/system/home/machines/roswell.nix
new file mode 100644
index 0000000000..ee3a557fa0
--- /dev/null
+++ b/users/grfn/system/home/machines/roswell.nix
@@ -0,0 +1,55 @@
+{ pkgs, lib, config, ... }:
+
+let
+  laptopKeyboardId = "5";
+in
+
+{
+  imports = [
+    ../platforms/linux.nix
+    ../modules/shell.nix
+    ../modules/development.nix
+    ../modules/emacs.nix
+    ../modules/vim.nix
+  ];
+
+  home.packages = with pkgs; [
+    # System utilities
+    bat
+    htop
+    killall
+    bind
+    zip
+    unzip
+    tree
+    nmap
+    bc
+    pv
+
+    # Security
+    gnupg
+    keybase
+    openssl
+
+    # Nix things
+    nixfmt
+    nix-prefetch-github
+    nix-review
+    cachix
+  ];
+
+  programs.password-store.enable = true;
+
+  programs.home-manager.enable = true;
+  home.stateVersion = "20.03";
+
+  xsession.enable = lib.mkForce false;
+
+  services.lorri.enable = true;
+
+  programs.direnv = {
+    enable = true;
+    enableBashIntegration = true;
+    enableZshIntegration = true;
+  };
+}
diff --git a/users/grfn/system/home/machines/yeren.nix b/users/grfn/system/home/machines/yeren.nix
new file mode 100644
index 0000000000..7f5b610f9b
--- /dev/null
+++ b/users/grfn/system/home/machines/yeren.nix
@@ -0,0 +1,82 @@
+{ pkgs, lib, config, ... }:
+
+let
+  inherit (builtins) pathExists;
+  laptopKeyboardId = "5";
+in
+
+{
+  imports = [
+    ../platforms/linux.nix
+    ../modules/common.nix
+    ../modules/development/readyset.nix
+  ] ++ (lib.optional (pathExists ../modules/private.nix) ../modules/private.nix);
+
+  # for when hacking
+  programs.home-manager.enable = true;
+  home.stateVersion = "20.03";
+
+  system.machine = {
+    wirelessInterface = "wlp0s20f3";
+    i3FontSize = 9;
+  };
+
+  home.packages = with pkgs; [
+    zoom-us
+    slack
+    mysql
+    graphviz
+    gnuplot
+    mypaint
+    xdot
+    tdesktop
+    subsurface
+
+    (discord.override rec {
+      version = "0.0.16";
+      src = fetchurl {
+        url = "https://dl.discordapp.net/apps/linux/${version}/discord-${version}.tar.gz";
+        sha256 = "1s9qym58cjm8m8kg3zywvwai2i3adiq6sdayygk2zv72ry74ldai";
+      };
+    })
+
+    steam
+  ];
+
+  systemd.user.services.laptop-keyboard = {
+    Unit = {
+      Description = "Swap caps+escape and alt+super, but only on the built-in laptop keyboard";
+      After = [ "graphical-session-pre.target" ];
+      PartOf = [ "graphical-session.target" ];
+    };
+
+    Install = { WantedBy = [ "graphical-session.target" ]; };
+
+    Service = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStart = (
+        "${pkgs.xorg.setxkbmap}/bin/setxkbmap "
+        + "-device ${laptopKeyboardId} "
+        + "-option caps:swapescape "
+        + "-option compose:ralt "
+        + "-option altwin:swap_alt_win"
+      );
+    };
+  };
+
+  xsession.windowManager.i3.config.keybindings.F9 = "exec lock";
+
+  xdg.mimeApps.defaultApplications."x-scheme-handler/tg" =
+    "telegramdesktop.desktop";
+
+  programs.zsh.shellAliases = {
+    "graph" = "curl -s localhost:6033/graph | dot -Tpng | feh -";
+  };
+
+  programs.ssh.matchBlocks."grfn-dev" = {
+    host = "grfn-dev";
+    forwardAgent = true;
+    user = "ubuntu";
+  };
+}
diff --git a/users/grfn/system/home/modules/alacritty.nix b/users/grfn/system/home/modules/alacritty.nix
new file mode 100644
index 0000000000..67d6638a31
--- /dev/null
+++ b/users/grfn/system/home/modules/alacritty.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+{
+  programs.alacritty = {
+    enable = true;
+    settings = {
+      font.size = 6;
+      font.normal.family = "Meslo LGSDZ Nerd Font";
+
+      draw_bold_text_with_bright_colors = false;
+
+      key_bindings = [
+        {
+          key = "Escape";
+          mods = "Control";
+          action = "ToggleViMode";
+        }
+      ];
+
+      colors = with import ../common/solarized.nix; rec {
+        # Default colors
+        primary = {
+          background = base3;
+          foreground = base00;
+        };
+
+        cursor = {
+          text = base3;
+          cursor = base00;
+        };
+
+        # Normal colors
+        normal = {
+          inherit red green yellow blue magenta cyan;
+          black = base02;
+          white = base2;
+        };
+
+        # Bright colors
+        # bright = normal;
+        bright = {
+          black = base03;
+          red = orange;
+          green = base01;
+          yellow = base00;
+          blue = base0;
+          magenta = violet;
+          cyan = base1;
+          white = base3;
+        };
+
+        vi_mode_cursor.cursor = red;
+      };
+    };
+  };
+}
diff --git a/users/grfn/system/home/modules/alsi.nix b/users/grfn/system/home/modules/alsi.nix
new file mode 100644
index 0000000000..204f9c8e14
--- /dev/null
+++ b/users/grfn/system/home/modules/alsi.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+{
+  home.packages = [ config.lib.depot.third_party.alsi ];
+
+  xdg.configFile."alsi/alsi.logo" = {
+    source = ./nixos-logo.txt;
+    force = true;
+  };
+
+  xdg.configFile."alsi/alsi.conf" = {
+    force = true;
+    text = ''
+      #!${pkgs.perl}/bin/perl
+
+      scalar {
+        ALSI_VERSION         => "0.4.8",
+        COLORS_FILE          => "/home/grfn/.config/alsi/alsi.colors",
+        DE_FILE              => "/home/grfn/.config/alsi/alsi.de",
+        DEFAULT_COLOR_BOLD   => "blue",
+        DEFAULT_COLOR_NORMAL => "blue",
+        DF_COMMAND           => "df -Th -x sys -x tmpfs -x devtmpfs &>/dev/stdout",
+        GTK2_RC_FILE         => "/home/grfn/.gtkrc-2.0",
+        GTK3_RC_FILE         => "/home/grfn/.config/gtk-3.0/settings.ini",
+        LOGO_FILE            => "/home/grfn/.config/alsi/alsi.logo",
+        OUTPUT_FILE          => "/home/grfn/.config/alsi/alsi.output",
+        # PACKAGES_PATH      => "/var/lib/pacman/local/",
+        PS_COMMAND           => "ps -A",
+        USAGE_COLORS         => 0,
+        USAGE_COLORS_BOLD    => 0,
+        USAGE_PRECENT_GREEN  => 50,
+        USAGE_PRECENT_RED    => 100,
+        USAGE_PRECENT_YELLOW => 85,
+        USE_LOGO_FROM_FILE   => 1,
+        USE_VALUES_COLOR     => 0,
+        WM_FILE              => "/home/grfn/.config/alsi/alsi.wm",
+      }
+    '';
+  };
+
+  xdg.configFile."alsi/alsi.colors".text = ''
+    #!${pkgs.perl}/bin/perl
+
+    # Colors for alsi
+
+    scalar {
+       black   => {normal => "\e[0;30m", bold => "\e[1;30m"},
+       red     => {normal => "\e[0;31m", bold => "\e[1;31m"},
+       green   => {normal => "\e[0;32m", bold => "\e[1;32m"},
+       yellow  => {normal => "\e[0;33m", bold => "\e[1;33m"},
+       default => {normal => "\e[0;34m", bold => "\e[1;34m"},
+       blue    => {normal => "\e[0;34m", bold => "\e[1;34m"},
+       purple  => {normal => "\e[0;35m", bold => "\e[1;35m"},
+       cyan    => {normal => "\e[0;36m", bold => "\e[1;36m"},
+       white   => {normal => "\e[0;37m", bold => "\e[1;37m"},
+       reset   => "\e[0m",
+    }
+  '';
+}
diff --git a/users/grfn/system/home/modules/common.nix b/users/grfn/system/home/modules/common.nix
new file mode 100644
index 0000000000..a24c5961de
--- /dev/null
+++ b/users/grfn/system/home/modules/common.nix
@@ -0,0 +1,109 @@
+{ config, lib, pkgs, ... }:
+
+# Everything in here needs to work on linux or darwin
+
+{
+  imports = [
+    ../modules/shell.nix
+    ../modules/development.nix
+    ../modules/emacs.nix
+    ../modules/vim.nix
+    ../modules/tarsnap.nix
+    ../modules/twitter.nix
+    ../modules/lib/cloneRepo.nix
+  ];
+
+  home.username = "grfn";
+  home.homeDirectory = "/home/grfn";
+
+  programs.password-store.enable = true;
+
+  grfn.impure.clonedRepos.passwordStore = {
+    github = "glittershark/pass";
+    path = ".local/share/password-store";
+  };
+
+  home.packages = with pkgs; [
+    # System utilities
+    bat
+    htop
+    killall
+    bind
+    zip
+    unzip
+    tree
+    nmap
+    bc
+    pv
+
+    # Security
+    gnupg
+    keybase
+    openssl
+
+    # Nix things
+    nixfmt
+    nix-prefetch-github
+    nix-review
+    cachix
+    (writeShellScriptBin "rebuild-mugwump" ''
+      set -eo pipefail
+      cd ~/code/depot
+      nix build -f . users.grfn.system.system.mugwumpSystem -o /tmp/mugwump
+      nix copy -f . users.grfn.system.system.mugwumpSystem \
+        --to ssh://mugwump
+      system=$(readlink -ef /tmp/mugwump)
+      ssh mugwump sudo nix-env -p /nix/var/nix/profiles/system --set $system
+      ssh mugwump sudo $system/bin/switch-to-configuration switch
+    '')
+    (writeShellScriptBin "rebuild-home" ''
+      set -eo pipefail
+      cd ~/code/depot
+      nix build -f . users.grfn.system.home.$(hostname)Home -o /tmp/home
+      /tmp/home/activate
+    '')
+  ];
+
+  programs.ssh = {
+    enable = true;
+
+    matchBlocks = {
+      "home" = {
+        host = "home.gws.fyi";
+        forwardAgent = true;
+      };
+
+      "dobharchu" = {
+        host = "dobharchu";
+        hostname = "172.16.0.4";
+        forwardAgent = true;
+        user = "griffin";
+      };
+
+      "cerberus" = {
+        host = "cerberus";
+        hostname = "172.16.0.3";
+        forwardAgent = true;
+        user = "griffin";
+      };
+
+      "mugwump" = {
+        host = "mugwump";
+        hostname = "172.16.0.5";
+        forwardAgent = true;
+      };
+
+      "roswell" = {
+        host = "roswell";
+        hostname = "18.223.118.13";
+        forwardAgent = true;
+      };
+    };
+  };
+
+  programs.direnv = {
+    enable = true;
+    enableBashIntegration = true;
+    enableZshIntegration = true;
+  };
+}
diff --git a/users/grfn/system/home/modules/development.nix b/users/grfn/system/home/modules/development.nix
new file mode 100644
index 0000000000..3420fd6522
--- /dev/null
+++ b/users/grfn/system/home/modules/development.nix
@@ -0,0 +1,210 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  clj2nix = pkgs.callPackage
+    (pkgs.fetchFromGitHub {
+      owner = "hlolli";
+      repo = "clj2nix";
+      rev = "3ab3480a25e850b35d1f532a5e4e7b3202232383";
+      sha256 = "1lry026mlpxp1j563qs13nhxf37i2zpl7lh0lgfdwc44afybqka6";
+    })
+    { };
+
+  pg-dump-upsert = pkgs.buildGoModule rec {
+    pname = "pg-dump-upsert";
+    version = "165258deaebded5e9b88f7a0acf3a4b7350e7bf4";
+
+    src = pkgs.fetchFromGitHub {
+      owner = "tomyl";
+      repo = "pg-dump-upsert";
+      rev = version;
+      sha256 = "1an4h8jjbj3r618ykjwk9brii4h9cxjqy47c4c8rivnvhimgf4wm";
+    };
+
+    vendorSha256 = "1a5fx6mrv30cl46kswicd8lf5i5shn1fykchvbnbhdpgxhbz6qi4";
+  };
+
+in
+
+with lib;
+
+{
+  imports = [
+    ./lib/zshFunctions.nix
+    ./development/kube.nix
+    # TODO(grfn): agda build is broken in the nixpkgs checkout
+    # ./development/agda.nix
+    ./development/rust.nix
+  ];
+
+  home.packages = with pkgs; [
+    jq
+    yq
+    gron
+    gitAndTools.tig
+    gitAndTools.gh
+    shellcheck
+    httpie
+    entr
+    gnumake
+    inetutils
+    tokei
+    jsonnet
+    ngrok
+    amber
+
+    gdb
+    lldb
+    hyperfine
+    clang-tools_11
+    rr
+
+    clj2nix
+    clojure
+    leiningen
+    clj-kondo
+
+    pg-dump-upsert
+
+    nodePackages.prettier
+
+    linuxPackages.perf
+  ] ++ optionals (stdenv.isLinux) [
+    # TODO(grfn): replace with stable again once the current julia debacle
+    # is resolved upstream, see https://github.com/NixOS/nixpkgs/pull/121114
+    julia_16-bin
+    valgrind
+  ];
+
+  programs.git = {
+    enable = true;
+    package = pkgs.gitFull;
+    userEmail = "root@gws.fyi";
+    userName = "Griffin Smith";
+    ignores = [
+      "*.sw*"
+      ".classpath"
+      ".project"
+      ".settings/"
+      ".dir-locals.el"
+      ".stack-work-profiling"
+      ".projectile"
+    ];
+    extraConfig = {
+      github.user = "glittershark";
+      merge.conflictstyle = "diff3";
+      rerere.enabled = "true";
+      advice.skippedCherryPicks = "false";
+    };
+
+    delta = {
+      enable = true;
+      options = {
+        syntax-theme = "Solarized (light)";
+        hunk-style = "plain";
+        commit-style = "box";
+      };
+    };
+  };
+
+  home.file.".gdbinit".text = ''
+    set history filename ~/.gdb_history
+    set history save on
+    set history size unlimited
+    set history remove-duplicates unlimited
+    set history expansion on
+  '';
+
+  home.file.".psqlrc".text = ''
+    \set QUIET 1
+    \timing
+    \set ON_ERROR_ROLLBACK interactive
+    \set VERBOSITY verbose
+    \x auto
+    \set PROMPT1 '%[%033[1m%]%M/%/%R%[%033[0m%]%# '
+    \set PROMPT2 '...%# '
+    \set HISTFILE ~/.psql_history- :DBNAME
+    \set HISTCONTROL ignoredups
+    \pset null [null]
+    \unset QUIET
+  '';
+
+  programs.readline = {
+    enable = true;
+    extraConfig = ''
+      set editing-mode vi
+    '';
+  };
+
+  programs.zsh = {
+    shellAliases = {
+      # Git
+      "gwip" = "git add . && git commit -am wip";
+      "gpr" = "g pull-request";
+      "gcl" = "git clone";
+      "grs" = "gr --soft";
+      "grhh" = "grh HEAD";
+      "grh" = "gr --hard";
+      "gr" = "git reset";
+      "gcb" = "gc -b";
+      "gco" = "gc";
+      "gcd" = "gc development";
+      "gcm" = "gc master";
+      "gcc" = "gc canon";
+      "gc" = "git checkout";
+      "gbg" = "git branch | grep";
+      "gba" = "git branch -a";
+      "gb" = "git branch";
+      "gcv" = "git commit --verbose";
+      "gci" = "git commit";
+      "gm" = "git merge";
+      "gdc" = "gd --cached";
+      "gd" = "git diff";
+      "gsl" = "git stash list";
+      "gss" = "git show stash";
+      "gsad" = "git stash drop";
+      "gsa" = "git stash";
+      "gst" = "gs";
+      "gs" = "git status";
+      "gg" = "gl --decorate --oneline --graph --date-order --all";
+      "gl" = "git log";
+      "gf" = "git fetch";
+      "gur" = "gu --rebase";
+      "gu" = "git pull";
+      "gpf" = "gp -f";
+      "gpa" = "gp --all";
+      "gpu" = "git push -u origin \"$(git symbolic-ref --short HEAD)\"";
+      "gp" = "git push";
+      "ganw" = "git diff -w --no-color | git apply --cached --ignore-whitespace";
+      "ga" = "git add";
+      "gnp" = "git --no-pager";
+      "g" = "git";
+      "grim" = "git fetch && git rebase -i --autostash origin/master";
+      "grom" = "git fetch && git rebase --autostash origin/master";
+      "groc" = "git fetch && git rebase --autostash origin/canon";
+      "grc" = "git rebase --continue";
+      "gcan" = "git commit --amend --no-edit";
+      "grl" = "git reflog";
+
+      # Haskell
+      "crl" = "cabal repl";
+      "cr" = "cabal run";
+      "cnb" = "cabal new-build";
+      "cob" = "cabal old-build";
+      "cnr" = "cabal new-run";
+      "cor" = "cabal old-run";
+      "ho" = "hoogle";
+    };
+
+    functions = {
+      gdelmerged = ''
+        git branch --merged | egrep -v 'master' | tr -d '+ ' | xargs git branch -d
+      '';
+
+      gref = ''
+        git show -s --pretty=reference "$1" | xclip -selection clipboard
+      '';
+    };
+  };
+}
diff --git a/users/grfn/system/home/modules/development/agda.nix b/users/grfn/system/home/modules/development/agda.nix
new file mode 100644
index 0000000000..afd22a306d
--- /dev/null
+++ b/users/grfn/system/home/modules/development/agda.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+let
+  agda-categories = with pkgs.agdaPackages; mkDerivation rec {
+    pname = "agda-categories";
+    version = "2128fab";
+    src = pkgs.fetchFromGitHub {
+      owner = "agda";
+      repo = "agda-categories";
+      rev = version;
+      sha256 = "08mc20qaz9vp5rhi60rh8wvjkg5aby3bgwwdhfnxha1663qf1q24";
+    };
+
+    buildInputs = [ standard-library ];
+  };
+
+in
+
+{
+  imports = [
+    ../lib/cloneRepo.nix
+  ];
+
+  home.packages = with pkgs; [
+    (pkgs.agda.withPackages
+      (p: with p; [
+        p.standard-library
+
+      ]))
+  ];
+
+  grfn.impure.clonedRepos = {
+    agda-stdlib = {
+      github = "agda/agda-stdlib";
+      path = "code/agda-stdlib";
+    };
+
+    agda-categories = {
+      github = "agda/agda-categories";
+      path = "code/agda-categories";
+    };
+
+    categories-examples = {
+      github = "agda/categories-examples";
+      path = "code/categories-examples";
+    };
+  };
+
+  home.file.".agda/defaults".text = ''
+    standard-library
+  '';
+
+  home.file.".agda/libraries".text = ''
+    /home/grfn/code/agda-stdlib/standard-library.agda-lib
+    /home/grfn/code/agda-categories/agda-categories.agda-lib
+  '';
+
+}
diff --git a/users/grfn/system/home/modules/development/kube.nix b/users/grfn/system/home/modules/development/kube.nix
new file mode 100644
index 0000000000..876b0c08df
--- /dev/null
+++ b/users/grfn/system/home/modules/development/kube.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+{
+  home.packages = with pkgs; [
+    kubectl
+    kubetail
+    sops
+    kubie
+    # pkgs-unstable.argocd # provided by urbos
+  ];
+
+  programs.zsh.shellAliases = {
+    "kc" = "kubectl";
+    "kg" = "kc get";
+    "kga" = "kc get --all-namespaces";
+    "kpd" = "kubectl get pods";
+    "kpa" = "kubectl get pods --all-namespaces";
+    "klf" = "kubectl logs -f";
+    "kdep" = "kubectl get deployments";
+    "ked" = "kubectl edit deployment";
+    "kpw" = "kubectl get pods -w";
+    "kew" = "kubectl get events -w";
+    "kdel" = "kubectl delete";
+    "knw" = "kubectl get nodes -w";
+    "kev" = "kubectl get events --sort-by='.metadata.creationTimestamp'";
+
+    "arsy" = "argocd app sync --prune";
+  };
+
+  home.file.".kube/kubie.yaml".text = ''
+    shell: zsh
+    prompt:
+      zsh_use_rps1: true
+  '';
+}
diff --git a/users/grfn/system/home/modules/development/readyset.nix b/users/grfn/system/home/modules/development/readyset.nix
new file mode 100644
index 0000000000..7b1b836837
--- /dev/null
+++ b/users/grfn/system/home/modules/development/readyset.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./rust.nix
+  ];
+
+  home.packages = with pkgs; [
+    # This goes in $PATH so I can run it from rofi and parent to my WM
+    (writeShellScriptBin "dotclip" "xclip -out -selection clipboard | dot -Tpng | feh -")
+    (buildGoModule rec {
+      pname = "rain";
+      version = "1.2.0";
+
+      src = fetchFromGitHub {
+        owner = "aws-cloudformation";
+        repo = pname;
+        rev = "v${version}";
+        sha256 = "168gkchshl5f1awqi1cgvdkm6q707702rnn0v4i5djqxmg5rk0p9";
+      };
+
+      vendorSha256 = "16bx7cjh5cq9zlis8lf28i016avgqf3j9fmcvkqzd8db2vxpqx3v";
+    })
+    awscli2
+    amazon-ecr-credential-helper
+  ];
+
+  programs.zsh.shellAliases = {
+    "tf" = "terraform";
+  };
+}
diff --git a/users/grfn/system/home/modules/development/rust.nix b/users/grfn/system/home/modules/development/rust.nix
new file mode 100644
index 0000000000..4ae5bc3bcc
--- /dev/null
+++ b/users/grfn/system/home/modules/development/rust.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+
+{
+  home.packages = with pkgs; [
+    rustup
+    rust-analyzer
+    cargo-edit
+    cargo-expand
+    cargo-flamegraph
+    cargo-rr
+    cargo-udeps
+    cargo-bloat
+    sccache
+    evcxr
+  ];
+
+  programs.zsh.shellAliases = {
+    "cg" = "cargo";
+    "cb" = "cargo build";
+    "ct" = "cargo test";
+    "ctw" = "fd -e rs | entr cargo test";
+    "cch" = "cargo check";
+  };
+
+  home.file.".cargo/config".text = ''
+    [build]
+    rustc-wrapper = "${pkgs.sccache}/bin/sccache"
+
+    [target.x86_64-unknown-linux-gnu]
+    linker = "clang"
+    rustflags = ["-C", "link-arg=-fuse-ld=${pkgs.mold}/bin/mold"]
+  '';
+}
diff --git a/users/grfn/system/home/modules/emacs.nix b/users/grfn/system/home/modules/emacs.nix
new file mode 100644
index 0000000000..f3d08a49ce
--- /dev/null
+++ b/users/grfn/system/home/modules/emacs.nix
@@ -0,0 +1,109 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  # doom-emacs = pkgs.callPackage (builtins.fetchTarball {
+  #   url = https://github.com/vlaci/nix-doom-emacs/archive/master.tar.gz;
+  # }) {
+  #   doomPrivateDir = ./doom.d;  # Directory containing your config.el init.el
+  #                               # and packages.el files
+  # };
+
+  depot = config.lib.depot;
+
+in
+{
+  imports = [
+    ./lib/cloneRepo.nix
+  ];
+
+  # home.packages = [ doom-emacs ];
+  # home.file.".emacs.d/init.el".text = ''
+  #     (load "default.el")
+  # '';
+  #
+
+  config = mkMerge [
+    {
+      home.packages = with pkgs; [
+        # LaTeX (for org export)
+        (pkgs.texlive.combine {
+          inherit (pkgs.texlive)
+            capt-of
+            collection-fontsrecommended
+            dvipng
+            fancyvrb
+            float
+            fncychap
+            framed
+            mathpartir
+            needspace
+            parskip
+            scheme-basic
+            semantic
+            tabulary
+            titlesec
+            ulem
+            upquote
+            varwidth
+            wrapfig
+            ;
+        })
+
+        ispell
+
+        ripgrep
+        coreutils
+        fd
+        clang
+        gnutls
+        emacsPackages.telega
+      ];
+
+      programs.emacs = {
+        enable = true;
+        package = pkgs.emacsUnstable;
+        extraPackages = (epkgs:
+          (with epkgs; [
+            tvlPackages.dottime
+            tvlPackages.tvl
+            vterm
+            telega
+          ])
+        );
+      };
+
+      grfn.impure.clonedRepos = {
+        orgClubhouse = {
+          github = "glittershark/org-clubhouse";
+          path = "code/org-clubhouse";
+        };
+
+        doomEmacs = {
+          github = "hlissner/doom-emacs";
+          path = ".emacs.d";
+          after = [ "emacs.d" ];
+          onClone = "bin/doom install";
+        };
+
+        "emacs.d" = {
+          github = "glittershark/emacs.d";
+          path = ".doom.d";
+          after = [ "orgClubhouse" ];
+        };
+      };
+
+      programs.zsh.shellAliases = {
+        "ec" = "emacsclient";
+      };
+    }
+    (mkIf pkgs.stdenv.isLinux {
+      # Notes
+      services.syncthing = {
+        enable = true;
+        tray.enable = true;
+      };
+    })
+  ];
+}
diff --git a/users/grfn/system/home/modules/email.nix b/users/grfn/system/home/modules/email.nix
new file mode 100644
index 0000000000..63dfeeb6f4
--- /dev/null
+++ b/users/grfn/system/home/modules/email.nix
@@ -0,0 +1,93 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+
+  # from home-manager/modules/services/lieer.nix
+  escapeUnitName = name:
+    let
+      good = upperChars ++ lowerChars ++ stringToCharacters "0123456789-_";
+      subst = c: if any (x: x == c) good then c else "-";
+    in
+    stringAsChars subst name;
+
+  accounts = {
+    personal = {
+      primary = true;
+      address = "root@gws.fyi";
+      aliases = [ "grfn@gws.fyi" ];
+      passEntry = "root-gws-msmtp";
+    };
+
+    work = {
+      address = "griffin@readyset.io";
+      passEntry = "readyset/msmtp";
+    };
+
+  };
+
+in
+{
+  programs.lieer.enable = true;
+  programs.notmuch.enable = true;
+  services.lieer.enable = true;
+  programs.msmtp.enable = true;
+
+  home.packages = with pkgs; [
+    mu
+    msmtp
+  ];
+
+  systemd.user.services = mapAttrs'
+    (name: account: {
+      name = escapeUnitName "lieer-${name}";
+      value.Service = {
+        ExecStart = mkForce "${pkgs.writeShellScript "sync-${name}" ''
+        ${pkgs.gmailieer}/bin/gmi sync --path ~/mail/${name}
+      ''}";
+        Environment = "NOTMUCH_CONFIG=${config.home.sessionVariables.NOTMUCH_CONFIG}";
+      };
+
+    })
+    accounts;
+
+  # xdg.configFile."notifymuch/notifymuch.cfg".text = generators.toINI {} {
+  #   notifymuch = {
+  #     query = "is:unread and is:important";
+  #     mail_client = "";
+  #     recency_interval_hours = "48";
+  #     hidden_tags = "inbox unread attachment replied sent encrypted signed";
+  #   };
+  # };
+
+  accounts.email.maildirBasePath = "mail";
+  accounts.email.accounts = mapAttrs
+    (_: params@{ passEntry, ... }: {
+      realName = "Griffin Smith";
+      passwordCommand = "pass ${passEntry}";
+
+      flavor = "gmail.com";
+
+      imapnotify = {
+        enable = true;
+        boxes = [ "Inbox" ];
+      };
+
+      gpg = {
+        key = "0F11A989879E8BBBFDC1E23644EF5B5E861C09A7";
+        signByDefault = true;
+      };
+
+      notmuch.enable = true;
+      lieer = {
+        enable = true;
+        sync = {
+          enable = true;
+          frequency = "*:*";
+        };
+      };
+      msmtp.enable = true;
+    } // builtins.removeAttrs params [ "passEntry" ])
+    accounts;
+}
diff --git a/users/grfn/system/home/modules/firefox.nix b/users/grfn/system/home/modules/firefox.nix
new file mode 100644
index 0000000000..c7e78685a5
--- /dev/null
+++ b/users/grfn/system/home/modules/firefox.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+{
+
+  xdg.mimeApps = rec {
+    enable = true;
+    defaultApplications = {
+      "text/html" = [ "firefox.desktop" ];
+      "x-scheme-handler/http" = [ "firefox.desktop" ];
+      "x-scheme-handler/https" = [ "firefox.desktop" ];
+      "x-scheme-handler/ftp" = [ "firefox.desktop" ];
+      "x-scheme-handler/chrome" = [ "firefox.desktop" ];
+      "application/x-extension-htm" = [ "firefox.desktop" ];
+      "application/x-extension-html" = [ "firefox.desktop" ];
+      "application/x-extension-shtml" = [ "firefox.desktop" ];
+      "application/xhtml+xml" = [ "firefox.desktop" ];
+      "application/x-extension-xhtml" = [ "firefox.desktop" ];
+      "application/x-extension-xht" = [ "firefox.desktop" ];
+    };
+    associations.added = defaultApplications;
+  };
+}
diff --git a/users/grfn/system/home/modules/games.nix b/users/grfn/system/home/modules/games.nix
new file mode 100644
index 0000000000..5272ac9330
--- /dev/null
+++ b/users/grfn/system/home/modules/games.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  df-orig = dwarf-fortress-packages.dwarf-fortress-original;
+
+  df-full = (dwarf-fortress-packages.dwarf-fortress-full.override {
+    theme = null;
+    enableIntro = false;
+    enableFPS = true;
+    enableDFHack = false; # Fails to build currently
+  });
+
+  init = runCommand "init.txt" { } ''
+    substitute "${df-orig}/data/init/init.txt" $out \
+      --replace "[INTRO:YES]" "[INTRO:NO]" \
+      --replace "[VOLUME:255]" "[VOLUME:0]" \
+      --replace "[FPS:NO]" "[FPS:YES]"
+  '';
+
+  d_init = runCommand "d_init.txt" { } ''
+    substitute "${df-orig}/data/init/d_init.txt" $out \
+      --replace "[AUTOSAVE:NONE]" "[AUTOSAVE:SEASONAL]" \
+      --replace "[AUTOSAVE_PAUSE:NO]" "[AUTOSAVE_PAUSE:YES]" \
+      --replace "[INITIAL_SAVE:NO]" "[INITIAL_SAVE:YES]" \
+      --replace "[EMBARK_WARNING_ALWAYS:NO]" "[EMBARK_WARNING_ALWAYS:YES]" \
+      --replace "[VARIED_GROUND_TILES:YES]" "[VARIED_GROUND_TILES:NO]" \
+      --replace "[SHOW_FLOW_AMOUNTS:NO]" "[SHOW_FLOW_AMOUNTS:YES]"
+  '';
+
+  df = runCommand "dwarf-fortress" { } ''
+    mkdir -p $out/bin
+    sed \
+      -e '4icp -f ${init} "$DF_DIR/data/init/init.txt"' \
+      -e '4icp -f ${d_init} "$DF_DIR/data/init/d_init.txt"' \
+      < "${df-full}/bin/dwarf-fortress" >"$out/bin/dwarf-fortress"
+
+    shopt -s extglob
+    ln -s ${df-full}/bin/!(dwarf-fortress) $out/bin
+
+    chmod +x $out/bin/dwarf-fortress
+  '';
+
+in
+mkMerge [
+  {
+    home.packages = [
+      crawl
+      xonotic
+    ];
+  }
+  (mkIf stdenv.isLinux {
+    home.packages = [
+      df
+      polymc
+    ];
+  })
+]
diff --git a/users/grfn/system/home/modules/i3.nix b/users/grfn/system/home/modules/i3.nix
new file mode 100644
index 0000000000..c6e198c98a
--- /dev/null
+++ b/users/grfn/system/home/modules/i3.nix
@@ -0,0 +1,380 @@
+{ config, lib, pkgs, ... }:
+let
+  mod = "Mod4";
+  solarized = import ../common/solarized.nix;
+  # TODO pull this out into lib
+  emacsclient = eval: pkgs.writeShellScript "emacsclient-eval" ''
+    msg=$(emacsclient --eval '${eval}' 2>&1)
+    echo "''${msg:1:-1}"
+  '';
+  screenlayout = {
+    home = pkgs.writeShellScript "screenlayout_home.sh" ''
+      xrandr \
+        --output eDP-1 --mode 1920x1200 --pos 0x960 --rotate normal \
+        --output DP-3 --primary --mode 3840x2160 --pos 1920x0 --rotate normal \
+        --output DP-1 --off \
+        --output DP-2 --off \
+        --output DP-4 --off
+    '';
+  };
+
+  inherit (builtins) map;
+  inherit (lib) mkMerge range;
+in
+{
+  options = with lib; {
+    system.machine.wirelessInterface = mkOption {
+      description = ''
+        Name of the primary wireless interface. Used by i3status, etc.
+      '';
+      default = "wlp3s0";
+      type = types.str;
+    };
+
+    system.machine.i3FontSize = mkOption {
+      description = "Font size to use in i3 window decorations etc.";
+      default = 6;
+      type = types.int;
+    };
+  };
+
+  config =
+    let
+      fontName = "MesloLGSDZ";
+      fontSize = config.system.machine.i3FontSize;
+      fonts = {
+        names = [ fontName ];
+        size = fontSize * 1.0;
+      };
+      decorationFont = "${fontName} ${toString fontSize}";
+    in
+    {
+      home.packages = with pkgs; [
+        rofi
+        rofi-pass
+        python3Packages.py3status
+        i3lock
+        i3status
+        dconf # for gtk
+
+        # Screenshots
+        maim
+
+        # GIFs
+        picom
+        peek
+
+        (pkgs.writeShellScriptBin "lock" ''
+          playerctl pause
+          ${pkgs.i3lock}/bin/i3lock -c 222222
+        '')
+      ];
+
+      xsession.scriptPath = ".xsession";
+
+      xsession.windowManager.i3 = {
+        enable = true;
+        config = {
+          modifier = mod;
+          keybindings =
+            mkMerge (
+              (map
+                (n: {
+                  "${mod}+${toString n}" =
+                    "workspace ${toString n}";
+                  "${mod}+Shift+${toString n}" =
+                    "move container to workspace ${toString n}";
+                })
+                (range 0 9))
+              ++ [
+                (rec {
+                  "${mod}+h" = "focus left";
+                  "${mod}+j" = "focus down";
+                  "${mod}+k" = "focus up";
+                  "${mod}+l" = "focus right";
+                  "${mod}+semicolon" = "focus parent";
+
+                  "${mod}+Shift+h" = "move left";
+                  "${mod}+Shift+j" = "move down";
+                  "${mod}+Shift+k" = "move up";
+                  "${mod}+Shift+l" = "move right";
+
+                  "${mod}+Shift+x" = "kill";
+
+                  "${mod}+Return" = "exec alacritty";
+
+                  "${mod}+Shift+s" = "split h";
+                  "${mod}+Shift+v" = "split v";
+                  "${mod}+e" = "layout toggle split";
+                  "${mod}+w" = "layout tabbed";
+                  "${mod}+s" = "layout stacking";
+
+                  "${mod}+f" = "fullscreen";
+
+                  "${mod}+Shift+r" = "restart";
+
+                  "${mod}+r" = "mode resize";
+
+                  # Marks
+                  "${mod}+Shift+m" = ''exec i3-input -F "mark %s" -l 1 -P 'Mark: ' '';
+                  "${mod}+m" = ''exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Go to: ' '';
+
+                  # Screenshots
+                  "${mod}+q" = "exec \"maim | xclip -selection clipboard -t image/png\"";
+                  "${mod}+Shift+q" = "exec \"maim -s | xclip -selection clipboard -t image/png\"";
+                  "${mod}+Ctrl+q" = "exec ${pkgs.writeShellScript "peek.sh" ''
+              ${pkgs.picom}/bin/picom &
+              picom_pid=$!
+              ${pkgs.peek}/bin/peek || true
+              kill -SIGINT $picom_pid
+            ''}";
+
+                  # Launching applications
+                  "${mod}+u" = "exec ${pkgs.writeShellScript "rofi" ''
+              rofi \
+                -modi 'combi' \
+                -combi-modi "window,drun,ssh,run" \
+                -font '${decorationFont}' \
+                -show combi
+            ''}";
+
+                  # Passwords
+                  "${mod}+p" = "exec rofi-pass -font '${decorationFont}'";
+
+                  # Media
+                  "XF86AudioPlay" = "exec playerctl play-pause";
+                  "XF86AudioNext" = "exec playerctl next";
+                  "XF86AudioPrev" = "exec playerctl previous";
+                  "XF86AudioRaiseVolume" = "exec pulseaudio-ctl up";
+                  "XF86AudioLowerVolume" = "exec pulseaudio-ctl down";
+                  "XF86AudioMute" = "exec pulseaudio-ctl mute";
+
+                  # Lock
+                  Pause = "exec lock";
+
+                  # Brightness
+                  "XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl -q s 5%-";
+                  "XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl -q s 5%+";
+
+                  # Sleep/hibernate
+                  # "${mod}+Escape" = "exec systemctl suspend";
+                  # "${mod}+Shift+Escape" = "exec systemctl hibernate";
+
+                  # Scratch buffer
+                  "${mod}+minus" = "scratchpad show";
+                  "${mod}+Shift+minus" = "move scratchpad";
+                  "${mod}+space" = "focus mode_toggle";
+                  "${mod}+Shift+space" = "floating toggle";
+
+                  # Screen Layout
+                  "${mod}+Shift+t" = "exec xrandr --auto";
+                  "${mod}+t" = "exec ${screenlayout.home}";
+                  "${mod}+Ctrl+t" = "exec ${pkgs.writeShellScript "fix_term.sh" ''
+              xrandr --output eDP-1 --off && ${screenlayout.home}
+            ''}";
+
+                  # Notifications
+                  "${mod}+Shift+n" = "exec killall -SIGUSR1 .dunst-wrapped";
+                  "${mod}+n" = "exec killall -SIGUSR2 .dunst-wrapped";
+                  "Control+space" = "exec ${pkgs.dunst}/bin/dunstctl close";
+                  "Control+Shift+space" = "exec ${pkgs.dunst}/bin/dunstctl close-all";
+                  "Control+grave" = "exec ${pkgs.dunst}/bin/dunstctl history-pop";
+                  "Control+Shift+period" = "exec ${pkgs.dunst}/bin/dunstctl action";
+                })
+              ]
+            );
+
+          inherit fonts;
+
+          colors = with solarized; rec {
+            focused = {
+              border = base01;
+              background = base01;
+              text = base3;
+              indicator = red;
+              childBorder = base02;
+            };
+            focusedInactive = focused // {
+              border = base03;
+              background = base03;
+              # text = base1;
+            };
+            unfocused = focusedInactive;
+            background = base03;
+          };
+
+          modes.resize = {
+            l = "resize shrink width 5 px or 5 ppt";
+            k = "resize grow height 5 px or 5 ppt";
+            j = "resize shrink height 5 px or 5 ppt";
+            h = "resize grow width 5 px or 5 ppt";
+
+            Return = "mode \"default\"";
+          };
+
+          bars = [{
+            statusCommand =
+              let
+                i3status-conf = pkgs.writeText "i3status.conf" ''
+                  general {
+                      output_format = i3bar
+                      colors = true
+                      color_good = "#859900"
+
+                      interval = 1
+                  }
+
+                  order += "external_script current_task"
+                  order += "external_script inbox"
+                  order += "spotify"
+                  order += "volume_status"
+                  order += "wireless ${config.system.machine.wirelessInterface}"
+                  # order += "ethernet enp3s0f0"
+                  order += "cpu_usage"
+                  order += "battery 0"
+                  # order += "volume master"
+                  order += "time"
+                  order += "tztime utc"
+
+                  mpd {
+                      format = "%artist - %album - %title"
+                  }
+
+                  wireless ${config.system.machine.wirelessInterface} {
+                      format_up = "W: (%quality - %essid - %bitrate) %ip"
+                      format_down = "W: -"
+                  }
+
+                  ethernet enp3s0f0 {
+                      format_up = "E: %ip"
+                      format_down = "E: -"
+                  }
+
+                  battery 0 {
+                      format = "%status %percentage"
+                      path = "/sys/class/power_supply/BAT%d/uevent"
+                      low_threshold = 10
+                  }
+
+                  cpu_usage {
+                      format = "CPU: %usage"
+                  }
+
+                  load {
+                      format = "%5min"
+                  }
+
+                  time {
+                      format = "    %a %h %d ⌚   %I:%M     "
+                  }
+
+                  spotify {
+                      color_playing = "#fdf6e3"
+                      color_paused = "#93a1a1"
+                      format_stopped = ""
+                      format_down = ""
+                      format = "{title} - {artist} ({album})"
+                  }
+
+                  external_script inbox {
+                      script_path = '${emacsclient "(grfn/num-inbox-items-message)"}'
+                      format = 'Inbox: {output}'
+                      cache_timeout = 120
+                      color = "#93a1a1"
+                  }
+
+                  external_script current_task {
+                      script_path = '${emacsclient "(grfn/org-current-clocked-in-task-message)"}'
+                      # format = '{output}'
+                      cache_timeout = 60
+                      color = "#93a1a1"
+                  }
+
+                  tztime utc {
+                      timezone = "UTC"
+                      format = "    %H·%M    "
+                  }
+
+                  volume_status {
+                      format = "☊ {percentage}"
+                      format_muted = "☊ X"
+                      # device = "default"
+                      # mixer_idx = 0
+                  }
+                '';
+              in
+              "py3status -c ${i3status-conf}";
+            inherit fonts;
+            position = "top";
+            colors = with solarized; rec {
+              background = base03;
+              statusline = base3;
+              separator = base1;
+              activeWorkspace = {
+                border = base03;
+                background = base1;
+                text = base3;
+              };
+              focusedWorkspace = activeWorkspace;
+              inactiveWorkspace = activeWorkspace // {
+                background = base01;
+              };
+              urgentWorkspace = activeWorkspace // {
+                background = red;
+              };
+            };
+          }];
+        };
+      };
+
+      services.dunst = {
+        enable = true;
+        settings = with solarized; {
+          global = {
+            font = "MesloLGSDZ ${toString (config.system.machine.i3FontSize * 1.5)}";
+            allow_markup = true;
+            format = "<b>%s</b>\n%b";
+            sort = true;
+            alignment = "left";
+            geometry = "600x15-40+40";
+            idle_threshold = 120;
+            separator_color = "frame";
+            separator_height = 1;
+            word_wrap = true;
+            padding = 8;
+            horizontal_padding = 8;
+            max_icon_size = 45;
+          };
+
+          frame = {
+            width = 0;
+            color = "#aaaaaa";
+          };
+
+          urgency_low = {
+            background = base03;
+            foreground = base3;
+            timeout = 5;
+          };
+
+          urgency_normal = {
+            background = base02;
+            foreground = base3;
+            timeout = 7;
+          };
+
+          urgency_critical = {
+            background = red;
+            foreground = base3;
+            timeout = 0;
+          };
+        };
+      };
+
+      gtk = {
+        enable = true;
+        iconTheme.name = "Adwaita";
+        theme.name = "Adwaita";
+      };
+    };
+}
diff --git a/users/grfn/system/home/modules/lib/cloneRepo.nix b/users/grfn/system/home/modules/lib/cloneRepo.nix
new file mode 100644
index 0000000000..54992bd69b
--- /dev/null
+++ b/users/grfn/system/home/modules/lib/cloneRepo.nix
@@ -0,0 +1,73 @@
+{ lib, config, ... }:
+with lib;
+{
+  options = {
+    grfn.impure.clonedRepos = mkOption {
+      description = "Repositories to clone";
+      default = { };
+      type = with types; loaOf (
+        let
+          sm = submodule {
+            options = {
+              url = mkOption {
+                type = nullOr str;
+                description = "URL of repository to clone";
+                default = null;
+              };
+
+              github = mkOption {
+                type = nullOr str;
+                description = "Github owner/repo of repository to clone";
+                default = null;
+              };
+
+              path = mkOption {
+                type = str;
+                description = "Path to clone to";
+              };
+
+              onClone = mkOption {
+                type = str;
+                description = ''
+                  Shell command to run after cloning the repo for the first time.
+                  Runs inside the repo itself.
+                '';
+                default = "";
+              };
+
+              after = mkOption {
+                type = listOf str;
+                description = "Activation hooks that this repository must be cloned after";
+                default = [ ];
+              };
+            };
+          };
+        in
+        addCheck sm (cr: (! isNull cr.url || ! isNull cr.github))
+      );
+    };
+  };
+
+  config = {
+    home.activation =
+      mapAttrs
+        (_: { url
+            , path
+            , github
+            , onClone
+            , after
+            , ...
+            }:
+          let repoURL = if isNull url then "git@github.com:${github}" else url;
+          in hm.dag.entryAfter ([ "writeBoundary" ] ++ after) ''
+            $DRY_RUN_CMD mkdir -p $(dirname "${path}")
+            if [[ ! -d ${path} ]]; then
+              $DRY_RUN_CMD git clone "${repoURL}" "${path}"
+              pushd ${path}
+              $DRY_RUN_CMD ${onClone}
+              popd
+            fi
+          '')
+        config.grfn.impure.clonedRepos;
+  };
+}
diff --git a/users/grfn/system/home/modules/lib/zshFunctions.nix b/users/grfn/system/home/modules/lib/zshFunctions.nix
new file mode 100644
index 0000000000..228dc6379f
--- /dev/null
+++ b/users/grfn/system/home/modules/lib/zshFunctions.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+    programs.zsh.functions = mkOption {
+      description = "An attribute set that maps function names to their source";
+      default = { };
+      type = with types; attrsOf (either str path);
+    };
+  };
+
+  config.programs.zsh.initExtra = concatStringsSep "\n" (
+    mapAttrsToList
+      (name: funSrc: ''
+        function ${name}() {
+          ${funSrc}
+        }
+      '')
+      config.programs.zsh.functions
+  );
+}
diff --git a/users/grfn/system/home/modules/nixos-logo.txt b/users/grfn/system/home/modules/nixos-logo.txt
new file mode 100644
index 0000000000..d4b16b44f0
--- /dev/null
+++ b/users/grfn/system/home/modules/nixos-logo.txt
@@ -0,0 +1,26 @@
+                 ((((((          ###%######       ##%###/
+               ,(((((((/(          #%#%#%#%#    .#%#%#%#%#
+                 ((((((///          %#######%. #####%###/
+                  (((((/(//,         /##%###%###%######
+                    (((//////          #####%########(
+         .(((((((((((((((///////////////#%%%########          ((
+        (((((((((((((((///////////////////#########         .((((
+       ((((((((((((((((/(//////////////////##########      ((((((((
+                   (#########                #########    (((((((((
+                  #########                   #########/((((((((((
+                *#########                     .#######(((((((((
+ ###%###################                         ####(//((((((((((((((((
+####%##################                           .#////////((((((((((((((
+%%%%%%%%%%%%%%#######((                           ////////////((((((((((((
+ ###%#######%#######////.                        ///////////////////((((
+         ###%###%#///////(                      /////////
+       .####%#### /////////                   /////////,
+      %#%#%#%#%*   /////////(                /////////
+      .#####%#       ////////(######################%#######%#####,
+        %####         (////////#####################%###%###%###%
+         .#          (//////(//((###################%#######%##%
+                    (//(((((((((((          #####%%%%(
+                  //(/((((((((((((((          ######%##
+                 (((((((((  (((((((((          #####%###/
+                (((((((((    /(((((((((         .###%####%
+                 ((((((        (((((((((          %#%#%#/
diff --git a/users/grfn/system/home/modules/obs.nix b/users/grfn/system/home/modules/obs.nix
new file mode 100644
index 0000000000..e85a7ba9a0
--- /dev/null
+++ b/users/grfn/system/home/modules/obs.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+
+let
+  libuiohook = stdenv.mkDerivation rec {
+    pname = "libuiohook";
+    version = "1.1";
+    src = fetchFromGitHub {
+      owner = "kwhat";
+      repo = "libuiohook";
+      rev = version;
+      sha256 = "1isfxn3cfrdqq22d3mlz2lzm4asf9gprs7ww2xy9c3j3srk9kd7r";
+    };
+
+    preConfigure = ''
+      ./bootstrap.sh
+    '';
+
+    nativeBuildInputs = [ pkg-config ];
+    buildInputs = [
+      libtool
+      autoconf
+      automake
+      xlibsWrapper
+      xorg.libXtst
+      xorg.libXinerama
+      xorg.libxkbfile
+      libxkbcommon
+    ];
+  };
+
+  obs-input-overlay = stdenv.mkDerivation rec {
+    pname = "obs-input-overlay";
+    version = "4.8";
+    src = fetchFromGitHub {
+      owner = "univrsal";
+      repo = "input-overlay";
+      rev = "v${version}";
+      sha256 = "1dklg0dx9ijwyhgwcaqz859rbpaivmqxqvh9w3h4byrh5pnkz8bf";
+      fetchSubmodules = true;
+    };
+
+    nativeBuildInputs = [ cmake ];
+    buildInputs = [ obs-studio libuiohook ];
+
+    postPatch = ''
+      sed -i CMakeLists.txt \
+        -e '2iinclude(${obs-studio.src}/cmake/Modules/ObsHelpers.cmake)' \
+        -e '2ifind_package(LibObs REQUIRED)'
+    '';
+
+    cmakeFlags = [
+      "-Wno-dev"
+    ];
+  };
+in
+{
+  home.packages = [
+    obs-studio
+    obs-input-overlay
+  ];
+
+  xdg.configFile."obs-studio/plugins/input-overlay/bin/64bit/input-overlay.so".source =
+    "${obs-input-overlay}/lib/obs-plugins/input-overlay.so";
+  xdg.configFile."obs-studio/plugins/input-overlay/data".source =
+    "${obs-input-overlay}/share/obs/obs-plugins/input-overlay";
+}
diff --git a/users/grfn/system/home/modules/ptt.nix b/users/grfn/system/home/modules/ptt.nix
new file mode 100644
index 0000000000..436c8f2617
--- /dev/null
+++ b/users/grfn/system/home/modules/ptt.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  pttKeycode = "152";
+  sourceID = "3";
+
+  mute = pkgs.writeShellScript "mute-mic" ''
+    xset -r ${pttKeycode}
+    ${pkgs.pulseaudio}/bin/pactl set-source-mute ${sourceID} 1
+  '';
+
+  unmute = pkgs.writeShellScript "unmute-mic" ''
+    xset -r ${pttKeycode}
+    ${pkgs.pulseaudio}/bin/pactl set-source-mute ${sourceID} 0
+  '';
+
+in
+
+{
+  home.packages = with pkgs; [
+    xbindkeys
+  ];
+
+
+  home.file.".xbindkeysrc.scm".text = ''
+    (xbindkey '("c:${pttKeycode}") "${unmute}")
+    (xbindkey '(release "c:${pttKeycode}") "${mute}")
+  '';
+
+  systemd.user.services."xbindkeys" = {
+    Unit = {
+      Description = "Keybind daemon for push-to-talk";
+      After = [ "graphical-session-pre.target" ];
+      PartOf = [ "graphical-session.target" ];
+    };
+
+    Install = { WantedBy = [ "graphical-session.target" ]; };
+
+    Service = {
+      ExecStart = "${pkgs.xbindkeys}/bin/xbindkeys -n -v";
+    };
+  };
+}
diff --git a/users/grfn/system/home/modules/pure.zsh-theme b/users/grfn/system/home/modules/pure.zsh-theme
new file mode 100755
index 0000000000..b4776e8159
--- /dev/null
+++ b/users/grfn/system/home/modules/pure.zsh-theme
@@ -0,0 +1,151 @@
+#!/bin/zsh -f
+# vim: ft=zsh:
+# MIT License
+# For my own and others sanity
+# git:
+# %b => current branch
+# %a => current action (rebase/merge)
+# prompt:
+# %F => color dict
+# %f => reset color
+# %~ => current path
+# %* => time
+# %n => username
+# %m => shortname host
+# %(?..) => prompt conditional - %(condition.true.false)
+
+# turns seconds into human readable time
+# 165392 => 1d 21h 56m 32s
+prompt_pure_human_time() {
+	local tmp=$1
+	local days=$(( tmp / 60 / 60 / 24 ))
+	local hours=$(( tmp / 60 / 60 % 24 ))
+	local minutes=$(( tmp / 60 % 60 ))
+	local seconds=$(( tmp % 60 ))
+	(( $days > 0 )) && echo -n "${days}d "
+	(( $hours > 0 )) && echo -n "${hours}h "
+	(( $minutes > 0 )) && echo -n "${minutes}m "
+	echo "${seconds}s"
+}
+
+is_git_repo() {
+	command git rev-parse --is-inside-work-tree &>/dev/null
+	return $?
+}
+
+# fastest possible way to check if repo is dirty
+prompt_pure_git_dirty() {
+	# check if we're in a git repo
+	is_git_repo || return
+	# check if it's dirty
+	[[ "$PURE_GIT_UNTRACKED_DIRTY" == 0 ]] && local umode="-uno" || local umode="-unormal"
+	command test -n "$(git status --porcelain --ignore-submodules ${umode})"
+
+	(($? == 0)) && echo '*'
+}
+
+prompt_pure_git_wip() {
+	is_git_repo || return
+	local subject="$(command git show --pretty=%s --quiet HEAD 2>/dev/null)"
+	[ "$subject" == 'wip' ] && echo '[WIP]'
+}
+
+# displays the exec time of the last command if set threshold was exceeded
+prompt_pure_cmd_exec_time() {
+	local stop=$EPOCHSECONDS
+	local start=${cmd_timestamp:-$stop}
+	integer elapsed=$stop-$start
+	(($elapsed > ${PURE_CMD_MAX_EXEC_TIME:=5})) && prompt_pure_human_time $elapsed
+}
+
+prompt_pure_preexec() {
+	cmd_timestamp=$EPOCHSECONDS
+
+	# shows the current dir and executed command in the title when a process is active
+	print -Pn "\e]0;"
+	echo -nE "$PWD:t: $2"
+	print -Pn "\a"
+}
+
+# string length ignoring ansi escapes
+prompt_pure_string_length() {
+	echo ${#${(S%%)1//(\%([KF1]|)\{*\}|\%[Bbkf])}}
+}
+
+prompt_pure_nix_info() {
+	local packages_info=''
+	if [[ -z $NIX_SHELL_PACKAGES ]]; then
+		packages_info='[nix-shell]'
+	else
+		packages_info="{ $NIX_SHELL_PACKAGES }"
+	fi
+
+	case $IN_NIX_SHELL in
+		'pure')
+			echo "$fg_bold[green][nix-shell] "
+			;;
+		'impure')
+			echo "$fg_bold[magenta][nix-shell] "
+			;;
+		*) ;;
+	esac
+}
+
+prompt_pure_precmd() {
+	# shows the full path in the title
+	print -Pn '\e]0;%~\a'
+
+	# git info
+	vcs_info
+
+	local prompt_pure_preprompt="\n$(prompt_pure_nix_info)$fg_bold[green]$prompt_pure_username%F{blue}%~%F{yellow}$vcs_info_msg_0_`prompt_pure_git_dirty` $fg_no_bold[red]`prompt_pure_git_wip`%f %F{yellow}`prompt_pure_cmd_exec_time`%f "
+	print -P $prompt_pure_preprompt
+
+	# check async if there is anything to pull
+	# (( ${PURE_GIT_PULL:-1} )) && {
+	# 	# check if we're in a git repo
+	# 	command git rev-parse --is-inside-work-tree &>/dev/null &&
+	# 	# make sure working tree is not $HOME
+	# 	[[ "$(command git rev-parse --show-toplevel)" != "$HOME" ]] &&
+	# 	# check check if there is anything to pull
+	# 	command git fetch &>/dev/null &&
+	# 	# check if there is an upstream configured for this branch
+	# 	command git rev-parse --abbrev-ref @'{u}' &>/dev/null && {
+	# 		local arrows=''
+	# 		(( $(command git rev-list --right-only --count HEAD...@'{u}' 2>/dev/null) > 0 )) && arrows='⇣'
+	# 		(( $(command git rev-list --left-only --count HEAD...@'{u}' 2>/dev/null) > 0 )) && arrows+='⇡'
+	# 		print -Pn "\e7\e[A\e[1G\e[`prompt_pure_string_length $prompt_pure_preprompt`C%F{cyan}${arrows}%f\e8"
+	# 	}
+	# } &!
+
+	# reset value since `preexec` isn't always triggered
+	unset cmd_timestamp
+}
+
+
+prompt_pure_setup() {
+	# prevent percentage showing up
+	# if output doesn't end with a newline
+	export PROMPT_EOL_MARK=''
+
+	prompt_opts=(cr subst percent)
+
+	zmodload zsh/datetime
+	autoload -Uz add-zsh-hook
+	autoload -Uz vcs_info
+
+	add-zsh-hook precmd prompt_pure_precmd
+	add-zsh-hook preexec prompt_pure_preexec
+
+	zstyle ':vcs_info:*' enable git
+	zstyle ':vcs_info:git*' formats ' %b'
+	zstyle ':vcs_info:git*' actionformats ' %b|%a'
+
+	# show username@host if logged in through SSH
+	[[ "$SSH_CONNECTION" != '' ]] && prompt_pure_username='%n@%m '
+
+	# prompt turns red if the previous command didn't exit with 0
+	PROMPT='%(?.%F{green}.%F{red})❯%f '
+}
+
+prompt_pure_setup "$@"
diff --git a/users/grfn/system/home/modules/rtlsdr.nix b/users/grfn/system/home/modules/rtlsdr.nix
new file mode 100644
index 0000000000..c8a404a1f4
--- /dev/null
+++ b/users/grfn/system/home/modules/rtlsdr.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  nixpkgs-gnuradio = import
+    (pkgs.fetchFromGitHub {
+      owner = "doronbehar";
+      repo = "nixpkgs";
+      rev = "712561aa5f10bfe6112a1726a912585612a70d1f";
+      sha256 = "04yqflbwjcfl9vlplphpj82csqqz9k6m3nj1ybhwgmsc4by7vivl";
+    })
+    { };
+
+in
+
+{
+  home.packages = with pkgs; [
+    rtl-sdr
+    nixpkgs-gnuradio.gnuradio
+    nixpkgs-gnuradio.gnuradio.plugins.osmosdr
+    nixpkgs-gnuradio.gqrx
+  ];
+}
diff --git a/users/grfn/system/home/modules/shell.nix b/users/grfn/system/home/modules/shell.nix
new file mode 100644
index 0000000000..ed82292b0a
--- /dev/null
+++ b/users/grfn/system/home/modules/shell.nix
@@ -0,0 +1,185 @@
+{ config, lib, pkgs, ... }:
+let
+  shellAliases = rec {
+    # NixOS stuff
+    hms = "home-manager switch";
+    nor = "sudo nixos-rebuild switch";
+    nrs = nor;
+    nrb = "sudo nixos-rebuild boot";
+    ncg = "nix-collect-garbage";
+    vihome = "vim ~/.config/nixpkgs/home.nix && home-manager switch";
+    virc = "vim ~/code/system/home/modules/shell.nix && home-manager switch && source ~/.zshrc";
+    visystem = "sudo vim /etc/nixos/configuration.nix && sudo nixos-rebuild switch";
+
+    # Nix
+    ns = "nix-shell";
+    nb = "nix build -f .";
+    nbl = "nix build -f . --builders ''"; # nix build local
+    lwo = "lorri watch --once";
+
+    # Docker and friends
+    "dcu" = "docker-compose up";
+    "dcud" = "docker-compose up -d";
+    "dc" = "docker-compose";
+    "dcr" = "docker-compose restart";
+    "dclf" = "docker-compose logs -f";
+    "dck" = "docker";
+    "dockerclean" = "dockercleancontainers && dockercleanimages";
+    "dockercleanimages" = "docker images -a --no-trunc | grep none | awk '{print \$$3}' | xargs -L 1 -r docker rmi";
+    "dockercleancontainers" = "docker ps -a --no-trunc| grep 'Exit' | awk '{print \$$1}' | xargs -L 1 -r docker rm";
+
+    # Directories
+    stck = "dirs -v";
+    b = "cd ~1";
+    ".." = "cd ..";
+    "..." = "cd ../..";
+    "...." = "cd ../../..";
+    "....." = "cd ../../../..";
+
+    # Aliases from old config
+    "http" = "http --style solarized";
+    "grep" = "grep $GREP_OPTIONS";
+    "bak" = "~/bin/backup.sh";
+    "xmm" = "xmodmap ~/.Xmodmap";
+    "asdflkj" = "asdf";
+    "asdf" = "asdfghjkl";
+    "asdfghjkl" = "echo \"Having some trouble?\"";
+    "ift" = "sudo iftop -i wlp3s0";
+    "first" = "awk '{print \$$1}'";
+    "cmt" = "git log --oneline | fzf-tmux | awk '{print \$$1}'";
+    "workmon" = "xrandr --output DP-2 --pos 1440x900 --primary";
+    "vi" = "vim";
+    "adbdev" = "adb devices";
+    "adbcon" = "adb connect $GNEX_IP";
+    "mpalb" = "mpc search album";
+    "mpart" = "mpc search artist";
+    "mps" = "mpc search";
+    "mpa" = "mpc add";
+    "mpt" = "mpc toggle";
+    "mpl" = "mpc playlist";
+    "dsstore" = "find . -name '*.DS_Store' -type f -ls -delete";
+    "df" = "df -h";
+    "fs" = "stat -f '%z bytes'";
+    "ll" = "ls -al";
+    "la" = "ls -a";
+  };
+in
+{
+  home.packages = with pkgs; [
+    zsh
+    autojump
+    ntfy
+  ];
+
+  home.sessionVariables = {
+    EDITOR = "vim";
+    LS_COLORS = "no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.ogg=01;35:*.mp3=01;35:*.wav=01;35:";
+    BROWSER = "firefox";
+    BAT_THEME = "ansi-light";
+  };
+
+  programs.bash = {
+    enable = true;
+    inherit shellAliases;
+  };
+
+  programs.zsh = {
+    enable = true;
+    enableAutosuggestions = true;
+    autocd = true;
+
+    inherit shellAliases;
+
+    history = rec {
+      save = 100000;
+      size = save;
+    };
+
+    oh-my-zsh = {
+      enable = true;
+
+      plugins = [
+        "battery"
+        "colorize"
+        "command-not-found"
+        "github"
+        "gitignore"
+        "postgres"
+        "systemd"
+        "themes"
+        "vi-mode"
+      ];
+
+      custom = "${pkgs.stdenv.mkDerivation {
+        name = "oh-my-zsh-custom";
+        unpackPhase = ":";
+        installPhase = ''
+          mkdir -p $out/themes
+          mkdir -p $out/custom/plugins
+          ln -s ${./pure.zsh-theme} $out/themes/pure.zsh-theme
+        '';
+      }}";
+
+      theme = "pure";
+    };
+
+    plugins = [{
+      name = "pure-theme";
+      src = pkgs.fetchFromGitHub {
+        owner = "sindresorhus";
+        repo = "pure";
+        rev = "0a92b02dd4172f6c64fdc9b81fe6cd4bddb0a23b";
+        sha256 = "0l8jqhmmjn7p32hdjnv121xsjnqd2c0plhzgydv2yzrmqgyvx7cc";
+      };
+    }];
+
+    initExtraBeforeCompInit = ''
+      zstyle ':completion:*' completer _complete _ignored _correct _approximate
+      zstyle ':completion:*' matcher-list \'\' 'm:{[:lower:]}={[:upper:]} m:{[:lower:][:upper:]}={[:upper:][:lower:]} r:|[._- :]=** r:|=**' 'l:|=* r:|=*'
+      zstyle ':completion:*' max-errors 5
+      zstyle ':completion:*' use-cache yes
+      zstyle ':completion::complete:grunt::options:' expire 1
+      zstyle ':completion:*' prompt '%e errors'
+      # zstyle :compinstall filename '~/.zshrc'
+      autoload -Uz compinit
+    '';
+
+    initExtra = ''
+      source ${./zshrc}
+      source ${pkgs.fetchFromGitHub {
+        owner = "zsh-users";
+        repo = "zsh-syntax-highlighting";
+        rev = "7678a8a22780141617f809002eeccf054bf8f448";
+        sha256 = "0xh4fbd54kvwwpqvabk8lpw7m80phxdzrd75q3y874jw0xx1a9q6";
+      }}/zsh-syntax-highlighting.zsh
+      source ${pkgs.autojump}/share/autojump/autojump.zsh
+      source ${pkgs.fetchFromGitHub {
+        owner = "chisui";
+        repo = "zsh-nix-shell";
+        rev = "a65382a353eaee5a98f068c330947c032a1263bb";
+        sha256 = "0l41ac5b7p8yyjvpfp438kw7zl9dblrpd7icjg1v3ig3xy87zv0n";
+      }}/nix-shell.plugin.zsh
+
+      eval "$(${pkgs.ntfy}/bin/ntfy shell-integration)"
+
+      export RPS1=""
+      autoload -U promptinit; promptinit
+      prompt pure
+
+      if [[ "$TERM" == "dumb" ]]; then
+        unsetopt zle
+        unsetopt prompt_cr
+        unsetopt prompt_subst
+        unfunction precmd
+        unfunction preexec
+        export PS1='$ '
+      fi
+    '';
+  };
+
+  programs.fzf = {
+    enable = true;
+    enableBashIntegration = true;
+    enableZshIntegration = true;
+  };
+}
diff --git a/users/grfn/system/home/modules/tarsnap.nix b/users/grfn/system/home/modules/tarsnap.nix
new file mode 100644
index 0000000000..87002610cb
--- /dev/null
+++ b/users/grfn/system/home/modules/tarsnap.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, ... }:
+
+{
+  home.packages = with pkgs; [
+    tarsnap
+  ];
+
+  home.file.".tarsnaprc".text = ''
+    ### Recommended options
+
+    # Tarsnap cache directory
+    cachedir /home/grfn/.cache/tarsnap
+
+    # Tarsnap key file
+    keyfile /home/grfn/.private/tarsnap.key
+
+    # Don't archive files which have the nodump flag set.
+    nodump
+
+    # Print statistics when creating or deleting archives.
+    print-stats
+
+    # Create a checkpoint once per GB of uploaded data.
+    checkpoint-bytes 1G
+
+    ### Commonly useful options
+
+    # Use SI prefixes to make numbers printed by --print-stats more readable.
+    humanize-numbers
+
+    ### Other options, not applicable to most systems
+
+    # Aggressive network behaviour: Use multiple TCP connections when
+    # writing archives.  Use of this option is recommended only in
+    # cases where TCP congestion control is known to be the limiting
+    # factor in upload performance.
+    #aggressive-networking
+
+    # Exclude files and directories matching specified patterns.
+    # Only one file or directory per command; multiple "exclude"
+    # commands may be given.
+    #exclude
+
+    # Include only files and directories matching specified patterns.
+    # Only one file or directory per command; multiple "include"
+    # commands may be given.
+    #include
+
+    # Attempt to reduce tarsnap memory consumption.  This option
+    # will slow down the process of creating archives, but may help
+    # on systems where the average size of files being backed up is
+    # less than 1 MB.
+    #lowmem
+
+    # Try even harder to reduce tarsnap memory consumption.  This can
+    # significantly slow down tarsnap, but reduces its memory usage
+    # by an additional factor of 2 beyond what the lowmem option does.
+    #verylowmem
+
+    # Snapshot time.  Use this option if you are backing up files
+    # from a filesystem snapshot rather than from a "live" filesystem.
+    #snaptime <file>
+  '';
+}
diff --git a/users/grfn/system/home/modules/twitter.nix b/users/grfn/system/home/modules/twitter.nix
new file mode 100644
index 0000000000..3cb2e90adc
--- /dev/null
+++ b/users/grfn/system/home/modules/twitter.nix
@@ -0,0 +1,23 @@
+{ pkgs, lib, ... }:
+
+{
+  home.packages = with pkgs; [
+    t
+  ];
+
+  home.sessionVariables = {
+    TWITTER_WHOAMI = "glittershark1";
+  };
+
+  programs.zsh = {
+    shellAliases = {
+      "mytl" = "t tl $TWITTER_WHOAMI";
+    };
+
+    functions = {
+      favelast = "t fave $(t tl -l $1 | head -n1 | cut -d' ' -f1)";
+      rtlast = "t rt $(t tl -l $1 | head -n1 | cut -d' ' -f1)";
+      tthread = "t reply $(t tl -l $TWITTER_WHOAMI | head -n1 | cut -d' ' -f1) $@";
+    };
+  };
+}
diff --git a/users/grfn/system/home/modules/vim.nix b/users/grfn/system/home/modules/vim.nix
new file mode 100644
index 0000000000..b87cb09ad1
--- /dev/null
+++ b/users/grfn/system/home/modules/vim.nix
@@ -0,0 +1,48 @@
+{ config, pkgs, ... }:
+{
+  programs.neovim = {
+    enable = true;
+    viAlias = true;
+    vimAlias = true;
+    plugins = with pkgs.vimPlugins; [
+      ctrlp
+      deoplete-nvim
+      syntastic
+      vim-abolish
+      vim-airline
+      vim-airline-themes
+      vim-bufferline
+      vim-closetag
+      # vim-colors-solarized
+      # solarized
+      (pkgs.vimUtils.buildVimPlugin {
+        pname = "vim-colors-solarized";
+        version = "git";
+        src = pkgs.fetchFromGitHub {
+          owner = "glittershark";
+          repo = "vim-colors-solarized";
+          rev = "4857c3221ec3f2693a45855154cb61a2cefb514d";
+          sha256 = "0kqp5w14g7adaiinmixm7z3x4w74lv1lcgbqjbirx760f0wivf9y";
+        };
+      })
+      vim-commentary
+      vim-dispatch
+      vim-endwise
+      vim-repeat
+      vim-fugitive
+      vim-markdown
+      vim-nix
+      vim-rhubarb
+      vim-sexp
+      vim-sexp-mappings-for-regular-people
+      vim-sleuth
+      vim-startify
+      vim-surround
+      vim-unimpaired
+      vinegar
+    ];
+    extraConfig = ''
+      source ${./vimrc}
+    '';
+  };
+}
diff --git a/users/grfn/system/home/modules/vimrc b/users/grfn/system/home/modules/vimrc
new file mode 100644
index 0000000000..3e33b5e2be
--- /dev/null
+++ b/users/grfn/system/home/modules/vimrc
@@ -0,0 +1,1121 @@
+" vim:set fdm=marker fmr={{{,}}} ts=2 sts=2 sw=2 expandtab:
+
+
+" Basic Options {{{
+set nocompatible
+set modeline
+set modelines=10
+syntax enable
+filetype plugin indent on
+set ruler
+set showcmd
+set number
+set incsearch
+set smartcase
+set ignorecase
+set scrolloff=10
+set tabstop=4
+set shiftwidth=4
+set softtabstop=4
+set nosmartindent
+set expandtab
+set noerrorbells visualbell t_vb=
+set laststatus=2
+set hidden
+let mapleader = ','
+let maplocalleader = '\'
+set undofile
+" set undodir=~/.vim/undo
+set wildignore=*.pyc,*.o,.git
+set clipboard=unnamedplus
+" set backupdir=$HOME/.vim/backup
+" set directory=$HOME/.vim/tmp
+set foldmarker={{{,}}}
+set colorcolumn=+1
+set concealcursor=
+set formatoptions+=j
+set wildmenu
+set wildmode=longest,list:full
+set noincsearch
+" }}}
+
+" GUI options {{{
+set go-=m
+set go-=T
+set go-=r
+set go-=L
+set go-=e
+set guifont=Meslo\ LG\ S\ DZ\ 9
+" }}}
+
+" Colors {{{
+" set t_Co=256
+
+fu! ReverseBackground()
+  if &bg=="light"
+    se bg=dark
+  else
+    se bg=light
+  endif
+endf
+com! BgToggle call ReverseBackground()
+nm <F12> :BgToggle<CR>
+
+set background=light
+colorscheme solarized
+" }}}
+
+" ---------------------------------------------------------------------------
+
+" CtrlP {{{
+let g:ctrlp_custom_ignore = {
+      \ 'dir': '(node_modules|target)'
+      \ }
+let g:ctrlp_max_files = 0
+let g:ctrlp_max_depth = 100
+" }}}
+
+" YouCompleteMe {{{
+let g:ycm_semantic_triggers =  {
+      \   'c' : ['->', '.'],
+      \   'objc' : ['->', '.'],
+      \   'ocaml' : ['.', '#'],
+      \   'cpp,objcpp' : ['->', '.', '::'],
+      \   'perl' : ['->'],
+      \   'php' : ['->', '::'],
+      \   'cs,java,javascript,d,python,perl6,scala,vb,elixir,go' : ['.'],
+      \   'vim' : ['re![_a-zA-Z]+[_\w]*\.'],
+      \   'lua' : ['.', ':'],
+      \   'erlang' : [':'],
+      \   'clojure' : [],
+      \   'haskell' : ['re!.*', '.', ' ', '(']
+      \ }
+      " \   'haskell' : ['.', '(', ' ']
+      " \   'ruby' : ['.', '::'],
+      " \   'clojure' : ['(', '.', '/', '[']
+" }}}
+
+" Neocomplete {{{
+if !has('nvim')
+  " Use neocomplete.
+  let g:neocomplete#enable_at_startup = 1
+  " Use smartcase.
+  let g:neocomplete#enable_smart_case = 1
+  " Set minimum syntax keyword length.
+  let g:neocomplete#sources#syntax#min_keyword_length = 3
+  let g:neocomplete#lock_buffer_name_pattern = '\*ku\*'
+
+  " Define dictionary.
+  " let g:neocomplete#sources#dictionary#dictionaries = {
+  "     \ 'default' : '',
+  "     \ 'vimshell' : $HOME.'/.vimshell_hist',
+  "     \ 'scheme' : $HOME.'/.gosh_completions'
+  "     \ }
+
+  " Define keyword.
+  if !exists('g:neocomplete#keyword_patterns')
+      let g:neocomplete#keyword_patterns = {}
+  endif
+  let g:neocomplete#keyword_patterns['default'] = '\h\w*'
+
+  " Plugin key-mappings.
+  inoremap <expr><C-g>     neocomplete#undo_completion()
+  inoremap <expr><C-l>     neocomplete#complete_common_string()
+
+  " Recommended key-mappings.
+  " <CR>: close popup and save indent.
+  inoremap <silent> <CR> <C-r>=<SID>my_cr_function()<CR>
+  function! s:my_cr_function()
+    return (pumvisible() ? "\<C-y>" : "" ) . "\<CR>"
+    " For no inserting <CR> key.
+    "return pumvisible() ? "\<C-y>" : "\<CR>"
+  endfunction
+  " <TAB>: completion.
+  inoremap <expr><TAB>  pumvisible() ? "\<C-n>" : "\<TAB>"
+  " <C-h>, <BS>: close popup and delete backword char.
+  inoremap <expr><C-h> neocomplete#smart_close_popup()."\<C-h>"
+  inoremap <expr><BS> neocomplete#smart_close_popup()."\<C-h>"
+  " Close popup by <Space>.
+  "inoremap <expr><Space> pumvisible() ? "\<C-y>" : "\<Space>"
+
+  " AutoComplPop like behavior.
+  "let g:neocomplete#enable_auto_select = 1
+
+  " Shell like behavior(not recommended).
+  "set completeopt+=longest
+  "let g:neocomplete#enable_auto_select = 1
+  "let g:neocomplete#disable_auto_complete = 1
+  "inoremap <expr><TAB>  pumvisible() ? "\<Down>" : "\<C-x>\<C-u>"
+
+  " Enable omni completion.
+  " autocmd FileType css setlocal omnifunc=csscomplete#CompleteCSS
+  " autocmd FileType html,markdown setlocal omnifunc=htmlcomplete#CompleteTags
+  " autocmd FileType javascript setlocal omnifunc=javascriptcomplete#CompleteJS
+  " autocmd FileType python setlocal omnifunc=pythoncomplete#Complete
+  " autocmd FileType xml setlocal omnifunc=xmlcomplete#CompleteTags
+
+  " Enable heavy omni completion.
+  if !exists('g:neocomplete#sources#omni#input_patterns')
+    let g:neocomplete#sources#omni#input_patterns = {}
+  endif
+endif
+" }}}
+
+" Deoplete {{{
+if has('nvim')
+  let g:deoplete#enable_at_startup = 1
+
+  inoremap <silent> <CR> <C-r>=<SID>my_cr_function()<CR>
+  function! s:my_cr_function()
+    return (pumvisible() ? "\<C-y>" : "" ) . "\<CR>"
+    " For no inserting <CR> key.
+    "return pumvisible() ? "\<C-y>" : "\<CR>"
+  endfunction
+  " <TAB>: completion.
+  inoremap <expr><TAB> pumvisible() ? "\<C-n>" : "\<TAB>"
+  inoremap <expr><S-TAB> pumvisible() ? "\<C-p>" : "\<TAB>"
+endif
+" }}}
+
+" Neovim Terminal mode {{{
+if has('nvim')
+  tnoremap <Esc> <C-\><C-n>
+  nnoremap \\ :tabedit term://zsh<CR>
+  nnoremap q\ :call <SID>OpenRepl()<CR>
+
+  if !exists('g:repl_size')
+    let g:repl_size=9
+  endif
+
+  function! s:OpenRepl() " {{{
+    " Check if buffer exists and is open
+    if exists('s:repl_bufname') && bufexists(s:repl_bufname) && bufwinnr(s:repl_bufname) >=? 0
+      " If so, just switch to it
+      execute bufwinnr(s:repl_bufname) . 'wincmd' 'w'
+      norm i
+      return
+    endif
+
+    if !exists('b:console')
+      let b:console=$SHELL
+    endif
+
+    let l:console_cmd = b:console
+
+    execute 'bot' g:repl_size . 'new'
+    set winfixheight nobuflisted
+    call termopen(l:console_cmd)
+    let s:repl_bufname = bufname('%')
+    norm i
+  endfunction " }}}
+endif
+" }}}
+
+" Tagbar options {{{
+let g:tagbar_autoclose = 1
+let g:tagbar_autofocus = 1
+let g:tagbar_compact = 1
+" }}}
+
+" delimitMate options {{{
+let g:delimitMate_expand_cr = 1
+" }}}
+
+" UltiSnips options {{{
+let g:UltiSnipsExpandTrigger = '<c-j>'
+   "g:UltiSnipsJumpForwardTrigger          <c-j>
+   "g:UltiSnipsJumpBackwardTrigger         <c-k>
+" }}}
+
+" VDebug Options {{{
+let g:vdebug_options = {'server': '192.168.56.1'}
+" }}}
+
+" Statusline {{{
+let g:airline_powerline_fonts=1
+
+if !exists('g:airline_symbols')
+  let g:airline_symbols = {}
+endif
+let g:airline_symbols.space = "\ua0"
+
+let g:airline#extensions#tagbar#flags = 'f'
+let g:airline#extensions#tabline#enabled = 1
+let g:airline#extensions#tabline#show_buffers = 0
+let g:airline#extensions#tabline#show_tabs = 1
+let g:airline#extensions#tabline#tab_min_count = 2
+let g:airline#extensions#tmuxline#enabled = 0
+
+let g:tmuxline_theme = 'airline'
+let g:tmuxline_preset = 'full'
+
+"set statusline=
+"set statusline+=%2*[%n%H%M%R%W]%*\              " flags and buf no
+"set statusline+=%-40f%<\                        " path
+"set statusline+=%=%40{fugitive#statusline()}\   " Vim status
+"set statusline+=%1*%y%*%*\                      " file type
+"set statusline+=%10((%l,%c)%)\                  " line and column
+"set statusline+=%P                              " percentage of file
+" }}}
+
+" Code review mode {{{
+fun! GetFontName()
+  return substitute(&guifont, '^\(.\{-}\)[0-9]*$', '\1', '')
+endfun
+
+fun! <SID>CodeReviewMode()
+  let &guifont = GetFontName() . ' 15'
+endfun
+com! CodeReviewMode call <SID>CodeReviewMode()
+" }}}
+
+" Syntastic {{{
+let g:syntastic_enable_signs = 0
+
+" Python {{{
+let g:syntastic_python_checkers = ['flake8']
+let g:syntastic_python_flake8_post_args = "--ignore=E101,E223,E224,E301,E302,E303,E501,E701,W,F401,E111,E261"
+
+" }}}
+" Javascript {{{
+let g:syntastic_javascript_checkers = ['eslint']
+let g:flow#autoclose = 1
+let g:flow#enable = 1
+
+" augroup syntastic_javascript_jsx
+"   autocmd!
+"   autocmd BufReadPre,BufNewFile *.js
+"   autocmd BufReadPre,BufNewFile *.jsx
+"         \ let g:syntastic_javascript_checkers = ['jsxhint']
+" augroup END
+
+" }}}
+" Haml {{{
+let g:syntastic_haml_checkers = ['haml_lint']
+
+" }}}
+" Html {{{
+let g:syntastic_html_checkers = []
+
+" }}}
+" Ruby {{{
+let g:syntastic_ruby_checkers = ['rubocop']
+" }}}
+" SASS/SCSS {{{
+let g:syntastic_scss_checkers = ['scss_lint']
+" }}}
+" Haskell {{{
+" let g:syntastic_haskell_checkers = ['ghc-mod']
+" }}}
+" Elixir {{{
+let g:syntastic_elixir_checkers = ['elixir']
+let g:syntastic_enable_elixir_checker = 1
+" }}}
+" }}}
+
+" Bufferline {{{
+let g:bufferline_echo=0
+" }}}
+
+" Eclim {{{
+let g:EclimCompletionMethod = 'omnifunc'
+augroup eclim
+  au!
+  au FileType java call <SID>JavaSetup()
+  au FileType java set textwidth=120
+augroup END
+
+function! s:JavaSetup() abort
+  noremap <C-I> :JavaImport<CR>
+  nnoremap K :JavaDocPreview<CR>
+  nnoremap ]d :JavaSearchContext<CR>
+  nnoremap [d :JavaSearchContext<CR>
+  nnoremap g<CR> :JUnit<CR>
+  nnoremap g\ :Mvn test<CR>
+endfunction
+" }}}
+
+" Signify options {{{
+let g:signify_mapping_next_hunk = ']h'
+let g:signify_mapping_prev_hunk = '[h'
+let g:signify_vcs_list          = ['git']
+let g:signify_sign_change       = '~'
+let g:signify_sign_delete       = '-'
+" }}}
+
+" Simplenote {{{
+let g:SimplenoteFiletype = 'markdown'
+let g:SimplenoteSortOrder = 'pinned,modifydate,tagged,createdate'
+let g:SimplenoteVertical = 1
+
+nnoremap <Leader>nn :Simplenote -n<CR>
+nnoremap <Leader>nl :Simplenote -l<CR>
+nnoremap <Leader>nw :Simplenote -l work<CR>
+nnoremap <Leader>nt :Simplenote -t<CR>
+" }}}
+
+" Emmet {{{
+" Expand abbreviation
+let g:user_emmet_leader_key = '<C-y>'
+" }}}
+
+" Startify {{{
+let g:startify_bookmarks=[ '~/.vimrc',  '~/.zshrc' ]
+" }}}
+
+" Abolish {{{
+let g:abolish_save_file = expand('~/.vim/after/plugin/abolish.vim')
+" }}}
+
+" Rails projections {{{
+
+if !exists('g:rails_projections')
+  let g:rails_projections = {}
+endif
+
+call extend(g:rails_projections, {
+      \ "config/routes.rb": { "command": "routes" },
+      \ "config/structure.sql": { "command": "structure" }
+      \ }, 'keep')
+
+if !exists('g:rails_gem_projections')
+  let g:rails_gem_projections = {}
+endif
+
+call extend(g:rails_gem_projections, {
+      \ "active_model_serializers": {
+      \   "app/serializers/*_serializer.rb": {
+      \     "command": "serializer",
+      \     "template": "class %SSerializer < ActiveModel::Serializer\nend",
+      \     "affinity": "model"}},
+      \ "react-rails": {
+      \   "app/assets/javascripts/components/*.jsx": {
+      \     "command": "component",
+      \     "template": "var %S = window.%S = React.createClass({\n  render: function() {\n  }\n});",
+      \     "alternate": "spec/javascripts/components/%s_spec.jsx" },
+      \   "spec/javascripts/components/*_spec.jsx": {
+      \     "alternate": "app/assets/javascripts/components/{}.jsx" }},
+      \ "rspec": {
+      \    "spec/**/support/*.rb": {
+      \      "command": "support"}},
+      \ "cucumber": {
+      \   "features/*.feature": {
+      \     "command": "feature",
+      \     "template": "Feature: %h"},
+      \   "features/support/*.rb": {
+      \     "command": "support"},
+      \   "features/support/env.rb": {
+      \     "command": "support"},
+      \   "features/step_definitions/*_steps.rb": {
+      \     "command": "steps"}},
+      \ "carrierwave": {
+      \   "app/uploaders/*_uploader.rb": {
+      \     "command": "uploader",
+      \     "template": "class %SUploader < CarrierWave::Uploader::Base\nend"}},
+      \ "draper": {
+      \   "app/decorators/*_decorator.rb": {
+      \     "command": "decorator",
+      \     "affinity": "model",
+      \     "template": "class %SDecorator < Draper::Decorator\nend"}},
+      \ "fabrication": {
+      \   "spec/fabricators/*_fabricator.rb": {
+      \     "command": ["fabricator", "factory"],
+      \     "alternate": "app/models/%s.rb",
+      \     "related": "db/schema.rb#%p",
+      \     "test": "spec/models/%s_spec.rb",
+      \     "template": "Fabricator :%s do\nend",
+      \     "affinity": "model"}},
+      \ "factory_girl": {
+      \   "spec/factories/*.rb": {
+      \     "command": "factory",
+      \     "alternate": "app/models/%i.rb",
+      \     "related": "db/structure.sql#%s",
+      \     "test": "spec/models/%s_spec.rb",
+      \     "template": "FactoryGirl.define do\n  factory :%i do\n  end\nend",
+      \     "affinity": "model"},
+      \   "spec/factories.rb": {
+      \      "command": "factory"},
+      \   "test/factories.rb": {
+      \      "command": "factory"}}
+      \ }, 'keep')
+" }}}
+
+" Other projections {{{
+let g:projectionist_heuristics = {
+      \ "config.ru&docker-compose.yml&app/&config/&OWNERS": {
+      \   "app/jobs/*.rb": {
+      \     "type": "job",
+      \     "alternate": "spec/jobs/{}_spec.rb"
+      \   },
+      \   "app/models/*.rb": {
+      \     "type": "model",
+      \     "alternate": "spec/models/{}_spec.rb"
+      \   },
+      \   "app/resources/*_resource.rb": {
+      \     "type": "resource",
+      \     "alternate": "spec/resources/{}_resource_spec.rb"
+      \   },
+      \   "config/*.yml": {
+      \     "type": "config"
+      \   },
+      \   "spec/*_spec.rb": {
+      \     "type": "spec",
+      \     "alternate": "app/{}.rb"
+      \   },
+      \   "spec/factories/*.rb": {
+      \     "type": "factory",
+      \   }
+      \ },
+      \ "svc-gateway.cabal": {
+      \   "src/*.hs": {
+      \     "type": "src",
+      \     "alternate": "test/{}Spec.hs"
+      \  },
+      \   "test/*Spec.hs": {
+      \     "type": "spec",
+      \     "alternate": "src/{}.hs",
+      \     "template": [
+      \       "module Gateway.Resource.HierarchySpec (main, spec) where",
+      \       "",
+      \       "import Prelude",
+      \       "import Test.Hspec",
+      \       "import Data.Aeson",
+      \       "",
+      \       "import Gateway.Resource.Hierarchy",
+      \       "",
+      \       "main :: IO ()",
+      \       "main = hspec spec",
+      \       "",
+      \       "spec :: Spec",
+      \       "spec = do",
+      \       "    describe \"something\" $ undefined"
+      \    ]
+      \  },
+      \  "svc-gateway.cabal": {
+      \    "type": "cabal"
+      \  }
+      \ },
+      \ "package.json&.flowconfig": {
+      \   "src/*.*": {
+      \     "type": "src",
+      \     "alternate": "test/{}_spec.js"
+      \   }
+      \ },
+      \ "pom.xml&src/main/clj/|src/main/cljs": {
+      \   "*": {
+      \     "start": "USE_NREPL=1 bin/run -m elephant.dev-system" ,
+      \     "connect": "nrepl://localhost:5554",
+      \     "piggieback": "(figwheel-sidecar.repl-api/repl-env)"
+      \   },
+      \   "pom.xml": { "type": "pom" },
+      \   "src/main/clj/*.clj": {
+      \     "alternate": "src/test/clj/{}_test.clj",
+      \     "template": ["(ns {dot|hyphenate})"]
+      \   },
+      \   "src/test/clj/*_test.clj": {
+      \     "alternate": "src/main/clj/{}.clj",
+      \     "dispatch": ":RunTests {dot|hyphenate}-test",
+      \     "template": ["(ns {dot|hyphenate}-test",
+      \                  "  (:require [clojure.test :refer :all]))"]
+      \   },
+      \   "src/main/cljs/*.cljs": {
+      \     "alternate": "src/test/cljs/{}_test.cljs"
+      \   },
+      \   "src/main/cljs/*_test.cljs": {
+      \     "alternate": "src/main/cljs/{}.cljs",
+      \     "dispatch": ":RunTests {dot|hyphenate}-test"
+      \   },
+      \   "src/main/clj/*.cljc": {
+      \     "alternate": "src/test/clj/{}_test.cljc"
+      \   },
+      \   "src/main/clj/*_test.cljc": {
+      \     "alternate": "src/test/clj/{}.cljc",
+      \     "dispatch": ":RunTests {dot|hyphenate}-test"
+      \   }
+      \ }}
+" }}}
+
+" AutoPairs {{{
+let g:AutoPairsCenterLine = 0
+" }}}
+
+" Filetypes {{{
+
+" Python {{{
+aug Python
+  au!
+  au FileType python set tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+aug END
+let g:python_highlight_all=1
+" }}}
+
+" PHP {{{
+aug PHP
+  au!
+  "au FileType php setlocal fdm=marker fmr={{{,}}}
+aug END " }}}
+
+" Mail {{{
+aug Mail
+  au FileType mail setlocal spell
+aug END " }}}
+
+" Haskell {{{
+let g:haskell_conceal_wide = 1
+let g:haskellmode_completion_ghc = 0
+let g:necoghc_enable_detailed_browse = 1
+
+augroup Haskell
+  autocmd!
+  autocmd FileType haskell setlocal textwidth=110 shiftwidth=2
+  autocmd FileType haskell setlocal omnifunc=necoghc#omnifunc
+  autocmd FileType haskell call <SID>HaskellSetup()
+  autocmd FileType haskell setlocal keywordprg=hoogle\ -cie
+augroup END
+
+function! s:HaskellSetup()
+  set sw=4
+  " compiler cabal
+  " let b:start='cabal run'
+  " let b:console='cabal repl'
+  " let b:dispatch='cabal test'
+  compiler stack
+  let b:start='stack run'
+  let b:console='stack ghci'
+  let b:dispatch='stack test'
+  nnoremap <buffer> gy :HdevtoolsType<CR>
+  nnoremap <buffer> yu :HdevtoolsClear<CR>
+endfunction
+" }}}
+
+" Ruby {{{
+
+function! s:RSpecSyntax()
+  syn keyword rspecMethod describe context it its specify shared_context
+        \ shared_examples shared_examples_for shared_context include_examples
+        \ include_context it_should_behave_like it_behaves_like before after
+        \ around fixtures controller_name helper_name scenario feature
+        \ background given described_class
+  syn match rspecMethod '\<let\>!\='
+  syn match rspecMethod '\<subject\>!\='
+  syn keyword rspecMethod violated pending expect expect_any_instance_of allow
+        \ allow_any_instance_of double instance_double mock mock_model
+        \ stub_model xit
+  syn match rspecMethod '\.\@<!\<stub\>!\@!'
+
+  call s:RSpecHiDefaults()
+endfunction
+
+function! s:RSpecHiDefaults()
+  hi def link rspecMethod rubyFunction
+endfunction
+
+augroup Ruby
+  au!
+  " au FileType ruby let b:surround_114 = "\\(module|class,def,if,unless,case,while,until,begin,do) \r end"
+  " au FileType ruby set fdm=syntax
+  au FileType ruby set tw=110
+  au FileType ruby set omnifunc=
+  au FileType ruby nnoremap <buffer> gy orequire 'pry'; binding.pry<ESC>^
+  au FileType ruby nnoremap <buffer> gY Orequire 'pry'; binding.pry<ESC>^
+  au FileType ruby nnoremap <buffer> yu :g/require 'pry'; binding.pry/d<CR>
+  au BufNewFile,BufRead *_spec.rb call <SID>RSpecSyntax()
+augroup END
+
+let ruby_operators = 1
+let ruby_space_errors = 1
+
+let g:rubycomplete_rails = 1
+command! -range ConvertHashSyntax <line1>,<line2>s/:(\S{-})(\s{-})=> /\1:\2/
+" }}}
+
+" Clojure {{{
+
+aug Clojure
+  au!
+  autocmd FileType clojure nnoremap <C-S> :Slamhound<CR>
+  autocmd FileType clojure nnoremap <silent> gr :w <bar> Require <bar> e<CR>
+  let g:clojure_align_multiline_strings = 1
+  let g:clojure_fuzzy_indent_patterns =
+        \ ['^with', '^def', '^let', '^fact']
+  let g:clojure_special_indent_words =
+        \ 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn,html'
+
+  autocmd FileType clojure setlocal textwidth=80
+  autocmd FileType clojure setlocal lispwords+=GET,POST,PATCH,PUT,DELETE |
+        \ setlocal lispwords+=context,select
+  autocmd BufNewFile,BufReadPost *.cljx setfiletype clojure
+  autocmd BufNewFile,BufReadPost *.cljx setlocal omnifunc=
+  autocmd BufNewFile,BufReadPost *.cljs setlocal omnifunc=
+  autocmd FileType clojure call <SID>TangentInit()
+  autocmd FileType clojure call <SID>sexp_mappings()
+  autocmd BufRead *.cljc ClojureHighlightReferences
+  autocmd FileType clojure let b:AutoPairs = {
+        \ '"': '"',
+        \ '{': '}',
+        \ '(': ')',
+        \ '[': ']'}
+        " Don't auto-pair quote reader macros
+        " \'`': '`',
+        " \ '''': '''',
+
+  autocmd User ProjectionistActivate call s:projectionist_connect()
+
+  function! s:projectionist_connect() abort
+    let connected = !empty(fireplace#path())
+    if !connected
+      for [root, value] in projectionist#query('connect')
+        try
+          silent execute "FireplaceConnect" value root
+          let connected = 1
+          break
+        catch /.*Connection refused.*/
+        endtry
+      endfor
+    endif
+
+    " if connected && exists(':Piggieback')
+    "   for [root, value] in projectionist#query('piggieback')
+    "     silent execute "Piggieback" value
+    "     break
+    "   endfor
+    " endif
+  endfunction
+
+  " autocmd BufNewFile,BufReadPost *.cljx setlocal omnifunc=
+  " autocmd BufNewFile,BufReadPost *.cljs setlocal omnifunc=
+
+  autocmd FileType clojure let b:console='lein repl'
+  autocmd FileType clojure call <SID>ClojureMaps()
+
+  function! s:ClojureMaps() abort
+    nnoremap <silent> <buffer> [m :call search('^(def', 'Wzb')<CR>
+    nnoremap <silent> <buffer> ]m :call search('^(def', 'Wz')<CR>
+  endfunction
+
+  command! Scratch call <SID>OpenScratch()
+  autocmd FileType clojure nnoremap <buffer> \s :Scratch<CR>
+
+  let g:scratch_buffer_name = 'SCRATCH'
+
+  function! s:OpenScratch()
+    if bufwinnr(g:scratch_buffer_name) > 0
+      execute bufwinnr(g:scratch_buffer_name) . 'wincmd' 'w'
+      return
+    endif
+
+    vsplit SCRATCH
+    set buftype=nofile
+    set filetype=clojure
+    let b:scratch = 1
+  endfunction
+aug END
+
+function! s:sexp_mappings() abort
+  if !exists('g:sexp_loaded')
+    return
+  endif
+
+  nmap <buffer> cfo <Plug>(sexp_raise_list)
+  nmap <buffer> cfO <Plug>(sexp_raise_element)
+  nmap <buffer> cfe <Plug>(sexp_raise_element)
+endfunction
+
+function! s:TangentInit() abort
+  set textwidth=80
+  command! TReset    call fireplace#session_eval('(user/reset)')
+  command! TGo       call fireplace#session_eval('(user/go)')
+  command! TMigrate  call fireplace#session_eval('(user/migrate)')
+  command! TRollback call fireplace#session_eval('(user/rollback)')
+  nnoremap g\ :TReset<CR>
+endfunction
+
+" }}}
+
+" Go {{{
+
+let g:go_highlight_functions = 1
+let g:go_highlight_methods = 1
+let g:go_highlight_structs = 1
+let g:go_highlight_operators = 1
+let g:go_highlight_build_constraints = 1
+
+augroup Go
+  autocmd!
+  autocmd FileType go setlocal omnifunc=go#complete#Complete
+  autocmd FileType go setlocal foldmethod=syntax
+  autocmd FileType go setlocal foldlevel=100
+  autocmd FileType go nnoremap <buffer> <F9> :GoTest<CR>
+  autocmd FileType go inoremap <buffer> <F9> <ESC>:GoTest<CR>i
+augroup END
+
+" }}}
+
+" RAML {{{
+
+function! s:buffer_syntax() " {{{
+  syn keyword ramlRAML          RAML             contained
+  syn match   ramlVersionString '^#%RAML \d\.\d' contains=ramlRAML
+endfunction " }}}
+
+augroup RAML
+  autocmd!
+  autocmd BufRead,BufNewFile *.raml set filetype=yaml
+  autocmd BufRead,BufNewFile *.raml call s:buffer_syntax()
+augroup END
+
+hi def link ramlVersionString Special
+hi def link ramlRAML Error
+" }}}
+
+" Mustache/Handlebars {{{
+let g:mustache_abbreviations = 1
+" }}}
+
+" Netrw {{{
+augroup netrw
+  autocmd!
+  autocmd FileType netrw nnoremap <buffer> Q :Rexplore<CR>
+
+  " Hee hee, oil and vinegar
+  function! s:setup_oil() abort
+    nnoremap <buffer> q <C-6>
+    xnoremap <buffer> q <C-6>
+  endfunction
+augroup END
+" }}}
+" }}}
+
+" Remove trailing whitespace {{{
+fun! <SID>StripTrailingWhitespaces()
+  let l = line(".")
+  let c = col(".")
+  %s/\s\+$//e
+  call cursor(l, c)
+endfun
+
+augroup striptrailingwhitespaces " {{{
+autocmd FileType c,cpp,java,php,ruby,python,sql,javascript,sh,jst,less,haskell,haml,coffee,scss,clojure,objc,elixir,yaml,json,eruby
+  \ autocmd BufWritePre <buffer> :call <SID>StripTrailingWhitespaces()
+augroup END " }}}
+
+" }}}
+
+" Goyo {{{
+let g:limelight_conceal_ctermfg = "10"
+let g:limelight_conceal_guifg = "#586e75"
+autocmd! User GoyoEnter Limelight
+autocmd! User GoyoLeave Limelight!
+" }}}
+
+"-----------------------------------------------------------------------------
+
+" Commands {{{
+
+" Edit temporary SQL files {{{
+let s:curr_sql = 0
+fun! <SID>EditSqlTempFile()
+  let l:fname = '/tmp/q' . s:curr_sql . '.sql'
+  execute 'edit' l:fname
+  let s:curr_sql = s:curr_sql + 1
+endfun
+com! EditSqlTempFile call <SID>EditSqlTempFile()
+" }}}
+
+" Double Indentation
+command! -range DoubleIndentation <line1>,<line2>s/^\(\s.\{-}\)\(\S\)/\1\1\2/
+
+" Quick-and-dirty fix capitalization of sql files
+command! -range FixSqlCapitalization <line1>,<line2>v/\v(^\s*--.*$)|(TG_)/norm guu
+
+" VimPipe Commands {{{
+" let g:sql_type_default = 'pgsql'
+command! SqlLive let b:vimpipe_command="vagrant ssh -c '~/mysql'"
+command! SqlRails let b:vimpipe_command="bin/rails dbconsole"
+command! SqlHeroku let b:vimpipe_command="heroku pg:psql"
+command! SqlEntities let b:vimpipe_command="psql -h 127.1 entities nomi"
+command! SqlUsers let b:vimpipe_command="psql -h 127.1 users nomi"
+command! SqlTangent let b:vimpipe_command="psql -h local.docker tangent super"
+" }}}
+
+" Git commands {{{
+command! -nargs=* Gpf Gpush -f <args>
+command! -nargs=* Gcv Gcommit --verbose <args>
+" }}}
+
+" Focus dispatch to only the last failures
+command! -nargs=* FocusFailures FocusDispatch rspec --only-failures <args>
+
+" }}}
+
+" Autocommands {{{
+
+augroup fugitive " {{{
+  au!
+  autocmd BufNewFile,BufRead fugitive://* set bufhidden=delete
+augroup END " }}}
+
+augroup omni " {{{
+  au!
+  " autocmd FileType javascript setlocal omnifunc=tern#Complete
+  "autocmd FileType python setlocal omnifunc=pythoncomplete#Complete
+  autocmd FileType php setlocal omnifunc=
+augroup END " }}}
+
+augroup sql " {{{
+  au!
+  autocmd FileType sql                 let b:vimpipe_command="psql -h 127.0.0.1 landlordsny_development landlordsny"
+  autocmd FileType sql                 let b:vimpipe_filetype="postgresql"
+  autocmd FileType sql                 set syntax=postgresql
+  autocmd FileType postgresql          set nowrap
+  autocmd BufNewFile,BufReadPost *.sql set syntax=pgsql
+augroup END " }}}
+
+augroup markdown " {{{
+  au!
+  autocmd FileType markdown let b:vimpipe_command='markdown'
+  autocmd FileType markdown let b:vimpipe_filetype='html'
+  autocmd FileType markdown set tw=80
+augroup END " }}}
+
+augroup typescript " {{{
+  au!
+  autocmd FileType typescript let b:vimpipe_command='tsc'
+  autocmd FileType typescript let b:vimpipe_filetype='javascript'
+  autocmd FileType typescript TSSstarthere
+  autocmd FileType typescript nnoremap <buffer> gd :TSSdef<CR>
+augroup END " }}}
+
+augroup jsx " {{{
+  au!
+  " autocmd FileType jsx set syntax=javascript
+  autocmd FileType javascript set filetype=javascript.jsx
+augroup END " }}}
+
+augroup nicefoldmethod " {{{
+  au!
+  " Don't screw up folds when inserting text that might affect them, until
+  " leaving insert mode. Foldmethod is local to the window. Protect against
+  " screwing up folding when switching between windows.
+  autocmd InsertEnter *
+    \ if !exists('w:last_fdm') |
+    \   let w:last_fdm=&foldmethod |
+    \   setlocal foldmethod=manual |
+    \ endif
+  autocmd InsertLeave,WinLeave *
+    \ if exists('w:last_fdm') |
+    \    let &l:foldmethod=w:last_fdm |
+    \    unlet w:last_fdm |
+    \ endif
+augroup END " }}}
+
+augroup visualbell " {{{
+  au!
+  autocmd GUIEnter * set visualbell t_vb=
+augroup END
+" }}}
+
+augroup quickfix " {{{
+  au!
+  autocmd QuickFixCmdPost grep cwindow
+augroup END " }}}
+
+augroup php " {{{
+  au!
+augroup END  "}}}
+
+augroup rubylang " {{{
+  au!
+  autocmd FileType ruby compiler rake
+augroup END " }}}
+
+augroup javascript "{{{
+  au!
+  autocmd FileType javascript let &errorformat =
+        \ '%E%.%#%n) %s:,' .
+        \ '%C%.%#Error: %m,' .
+        \ '%C%.%#at %s (%f:%l:%c),' .
+        \ '%Z%.%#at %s (%f:%l:%c),' .
+        \ '%-G%.%#,'
+augroup END " }}}
+
+augroup git " {{{
+  autocmd!
+  autocmd FileType gitcommit set textwidth=72
+augroup END
+" }}}
+" }}}
+
+" Leader commands {{{
+
+" Edit specific files {{{
+nnoremap <silent> <leader>ev :split $MYVIMRC<CR>
+nnoremap <silent> <leader>eb :split ~/.vim_bundles<CR>
+nnoremap <silent> <leader>es :UltiSnipsEdit<CR>
+nnoremap <silent> <leader>ea :split ~/.vim/after/plugin/abolish.vim<CR>
+
+nnoremap <silent> <leader>sv :so $MYVIMRC<CR>
+nnoremap <silent> <leader>sb :so ~/.vim_bundles<CR>
+nnoremap <silent> <leader>sa :so ~/.vim/after/plugin/abolish.vim<CR>
+
+nnoremap <Leader>el :EditSqlTempFile<CR>
+" }}}
+
+" Toggle navigation panels {{{
+nnoremap <Leader>l :TagbarToggle<CR>
+nnoremap <Leader>mb :MBEToggle<CR>
+nnoremap <Leader>u :GundoToggle<CR>
+
+nnoremap <Leader>t :CtrlP<CR>
+nnoremap <Leader>z :FZF<CR>
+nnoremap <Leader>b :CtrlPBuffer<CR>
+nnoremap <Leader>a :CtrlPTag<CR>
+nnoremap <Leader>r :CtrlPGitBranch<CR>
+" }}}
+
+" CtrlP {{{
+let g:ctrlp_custom_ignore = {
+      \ 'dir': 'node_modules',
+      \ }
+" }}}
+
+" Git leader commands {{{
+noremap <Leader>g :Git<SPACE>
+noremap <Leader>gu :Gpull<CR>
+noremap <Leader>gp :Gpush<CR>
+noremap <Leader>s :Gstatus<CR>
+noremap <Leader>cv :Gcommit --verbose<CR>
+noremap <Leader>ca :Gcommit --verbose --amend<CR>
+
+nnoremap <Leader>dl :diffg LOCAL<CR>
+nnoremap <Leader>dr :diffg REMOTE<CR>
+nnoremap <Leader>db :diffg BASE<CR>
+nnoremap <Leader>du :diffu<CR>
+nnoremap <Leader>dg :diffg<CR>
+
+nnoremap <Leader>d2 :diffg //2<CR>:diffu<CR>
+nnoremap <Leader>d3 :diffg //3<CR>:diffu<CR>
+
+nnoremap <Leader>yt :SignifyToggle<CR>
+" }}}
+
+" Breakpoint Leader Commands {{{
+nnoremap <Leader>x :Breakpoint<CR>
+nnoremap <Leader>dx :BreakpointRemove *<CR>
+" }}}
+
+" Tabularize {{{
+  " Leader Commands {{{
+  nnoremap <localleader>t= :Tabularize /=<CR>
+  vmap <localleader>t= :Tabularize /=<CR>
+
+  nnoremap <localleader>t> :Tabularize /=><CR>
+  vmap <localleader>t> :Tabularize /=><CR>
+  " }}}
+
+  " => Aligning {{{
+  function! s:rocketalign()
+    let l:p = '^.*=>\s.*$'
+    echo l:p
+    if exists(':Tabularize') && getline('.') =~# '^.*=' &&
+                \ (getline(line('.')-1) =~# l:p || getline(line('.')+1) =~# l:p)
+      let column = strlen(substitute(getline('.')[0:col('.')],'[^=>]','','g'))
+      let position = strlen(matchstr(getline('.')[0:col('.')],'.*=>\s*\zs.*'))
+      Tabularize/=>/l1
+      normal! $
+      call search(repeat('[^=>]*=>',column).'\s\{-\}'.repeat('.',position),'ce',line('.'))
+    endif
+  endfunction
+  "inoremap <buffer> <space>=><space> =><Esc>:call <SID>rocketalign()<CR>a
+  " }}}
+
+  " = Aligning {{{
+  function! s:eqalign()
+    let l:p = '^.*=\s.*$'
+    if exists(':Tabularize') && getline('.') =~# '^.*=' &&
+                \ (getline(line('.')-1) =~# l:p || getline(line('.')+1) =~# l:p)
+      let column = strlen(substitute(getline('.')[0:col('.')],'[^=]','','g'))
+      let position = strlen(matchstr(getline('.')[0:col('.')],'.*=\s*\zs.*'))
+      Tabularize/=/l1
+      normal! $
+      call search(repeat('[^=]*=',column).'\s\{-\}'.repeat('.',position),'ce',line('.'))
+    endif
+  endfunction
+  "inoremap <buffer><silent> <space>=<space> =<Esc>:call <SID>eqalign()<CR>a
+  " }}}
+
+  " : Aligning {{{
+  function! s:colonalign()
+    let l:p : '^.*:\s.*$'
+    if exists(':Tabularize') && getline('.') :~# '^.*:' &&
+                \ (getline(line('.')-1) :~# l:p || getline(line('.')+1) :~# l:p)
+      let column : strlen(substitute(getline('.')[0:col('.')],'[^:]','','g'))
+      let position : strlen(matchstr(getline('.')[0:col('.')],'.*:\s*\zs.*'))
+      Tabularize/:/l1
+      normal! $
+      call search(repeat('[^:]*:',column).'\s\{-\}'.repeat('.',position),'ce',line('.'))
+    endif
+  endfunction
+  "inoremap <buffer><silent> <space>:<space> :<Esc>:call <SID>colonalign()<CR>a
+  " }}}
+" }}}
+
+" }}}
+
+" Mappings {{{
+" 'delete current'
+nnoremap dc 0d$
+nnoremap com :silent !tmux set status<CR>
+nnoremap <F9>  :Make<CR>
+nnoremap g<CR> :Dispatch<CR>
+nnoremap g\ :Start<CR>
+inoremap <F9> <ESC>:Make<CR>i
+
+" Navigate buffers {{{
+nnoremap gb :bn<CR>
+nnoremap gB :bp<CR>
+" }}}
+
+" Window Navigation {{{
+nnoremap <space>w <C-w>
+nnoremap <space>h <C-w>h
+nnoremap <space>j <C-w>j
+nnoremap <space>k <C-w>k
+nnoremap <space>l <C-w>l
+nnoremap <space>z <C-w>z
+" }}}
+
+
+" Sort with motion {{{
+if !exists("g:sort_motion_flags")
+  let g:sort_motion_flags = ""
+endif
+function! s:sort_motion(mode) abort
+  if a:mode == 'line'
+    execute "'[,']sort " . g:sort_motion_flags
+  elseif a:mode == 'char'
+    execute "normal! `[v`]y"
+    let sorted = join(sort(split(@@, ', ')), ', ')
+    execute "normal! v`]c" . sorted
+  elseif a:mode == 'V' || a:mode == ''
+    execute "'<,'>sort " . g:sort_motion_flags
+  endif
+endfunction
+
+function! s:sort_lines()
+  let beginning = line('.')
+  let end = v:count + beginning - 1
+  execute beginning . ',' . end . 'sort'
+endfunction
+
+xnoremap <silent> <Plug>SortMotionVisual :<C-U>call <SID>sort_motion(visualmode())<CR>
+nnoremap <silent> <Plug>SortMotion :<C-U>set opfunc=<SID>sort_motion<CR>g@
+nnoremap <silent> <Plug>SortLines :<C-U>call <SID>sort_lines()<CR>
+
+map go <Plug>SortMotion
+vmap go <Plug>SortMotionVisual
+map goo <Plug>SortLines
+" }}}
+" }}}
+
+let g:hare_executable = 'cabal exec -- ghc-hare'
diff --git a/users/grfn/system/home/modules/zshrc b/users/grfn/system/home/modules/zshrc
new file mode 100644
index 0000000000..a12173d684
--- /dev/null
+++ b/users/grfn/system/home/modules/zshrc
@@ -0,0 +1,327 @@
+#!/usr/bin/zsh
+# vim: set fdm=marker fmr={{{,}}}:
+
+stty -ixon
+
+# Compinstall {{{
+zstyle ':completion:*' completer _complete _ignored _correct _approximate
+zstyle ':completion:*' matcher-list '' 'm:{[:lower:]}={[:upper:]} m:{[:lower:][:upper:]}={[:upper:][:lower:]} r:|[._- :]=** r:|=**' 'l:|=* r:|=*'
+zstyle ':completion:*' max-errors 5
+zstyle ':completion:*' use-cache yes
+zstyle ':completion::complete:grunt::options:' expire 1
+zstyle ':completion:*' prompt '%e errors'
+zstyle :compinstall filename '~/.zshrc'
+autoload -Uz compinit
+compinit
+# }}}
+
+# Zsh-newuser-install {{{
+HISTFILE=~/.histfile
+HISTSIZE=1000
+SAVEHIST=1000
+setopt appendhistory autocd extendedglob notify autopushd
+unsetopt beep nomatch
+bindkey -v
+# }}}
+
+# Basic options {{{
+set -o vi
+umask 022
+export VIRTUAL_ENV_DISABLE_PROMPT=1
+# export PATH=~/.local/bin:~/.cabal/bin:$PATH:~/code/go/bin:~/bin:~/npm/bin:~/.gem/ruby/2.1.0/bin:~/.gem/ruby/2.0.0/bin:/home/smith/bin
+# }}}
+
+# Zsh highlight highlighters {{{
+ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern root)
+# }}}
+
+# More basic options {{{
+setopt no_hist_verify
+setopt histignorespace
+# }}}
+
+# Utility Functions {{{
+
+# Set the terminal's title bar.
+function titlebar() {
+echo -ne "\033]0;$*\007"
+}
+
+function quiet() {
+"$@" >/dev/null
+}
+
+function quieter() {
+"$@" >/dev/null 2>&1
+}
+
+# From http://stackoverflow.com/questions/370047/#370255
+function path_remove() {
+IFS=:
+# convert it to an array
+t=($PATH)
+unset IFS
+# perform any array operations to remove elements from the array
+t=(${t[@]%%$1})
+IFS=:
+# output the new array
+echo "${t[*]}"
+}
+
+# }}}
+
+# Force screen to use zsh {{{
+# }}}
+
+# Environment {{{
+# }}}
+
+# Directory Stuff {{{
+
+# Always use color output for `ls`
+
+# Directory listing
+
+# Easier navigation: .., ..., -
+
+# File size
+
+# Recursively delete `.DS_Store` files
+
+# Create a new directory and enter it
+function md() {
+  mkdir -p "$@" && cd "$@"
+}
+
+# }}}
+
+# MPD/MPC stuff {{{
+function mp() {
+# Test if drive is already mounted
+if ! lsblk | grep /media/external >/dev/null; then
+  if ! sudo mount /media/external; then
+    echo "External drive not plugged in, or could not mount"
+    return 1
+  fi
+fi
+if (mpc >/dev/null 2>&1); then
+  ncmpcpp
+else
+  mpd &&
+    (pgrep mpdscribble || mpdscribble) &&
+    ncmpcpp
+fi
+}
+
+# kill mp
+function kmp() {
+killall ncmpcpp
+mpd --kill
+
+local files
+
+if (files=$(lsof 2>&1 | grep -v docker | grep external)); then
+  echo
+  echo "==> Still processes using external drive:"
+  echo
+  echo $files
+else
+  sudo umount /media/external
+fi
+}
+
+
+function mppal() {
+mpc search album "$1" | mpc add &&
+  mpc play;
+}
+# }}}
+
+# Git stuff {{{
+# function ga() { git add "${@:-.}"; } # Add all files by default
+# Add non-whitespace changes
+# function gc() { git checkout "${@:-master}"; } # Checkout master by default
+
+# open all changed files (that still actually exist) in the editor
+function ged() {
+local files=()
+for f in $(git diff --name-only "$@"); do
+  [[ -e "$f" ]] && files=("${files[@]}" "$f")
+done
+local n=${#files[@]}
+echo "Opening $n $([[ "$@" ]] || echo "modified ")file$([[ $n != 1 ]] && \
+  echo s)${@:+ modified in }$@"
+q "${files[@]}"
+}
+
+# git find-replace
+function gfr() {
+if [[ "$#" == "0" ]]; then
+  echo 'Usage:'
+  echo ' gg_replace term replacement file_mask'
+  echo
+  echo 'Example:'
+  echo ' gg_replace cappuchino cappuccino *.html'
+  echo
+else
+  find=$1; shift
+  replace=$1; shift
+
+  ORIG_GLOBIGNORE=$GLOBIGNORE
+  GLOBIGNORE=*.*
+  if [[ "$#" = "0" ]]; then
+    set -- ' ' $@
+  fi
+
+  while [[ "$#" -gt "0" ]]; do
+    for file in `git grep -l $find -- $1`; do
+      sed -e "s/$find/$replace/g" -i'' $file
+    done
+    shift
+  done
+
+  GLOBIGNORE=$ORIG_GLOBIGNORE
+fi
+}
+
+function vconflicts() {
+$EDITOR $(git status --porcelain | awk '/^UU/ { print $2 }')
+}
+# }}}
+
+# fzf {{{
+v() {
+  local file
+  file=$(fzf-tmux --query="$1" --select-1 --exit-0)
+  [ -n "$file" ] && ${EDITOR:-vim} "$file"
+}
+
+c() {
+  local dir
+  dir=$(find ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && cd "$dir"
+}
+
+co() {
+  local branch
+  branch=$(git branch -a | sed -s "s/\s*\**//g" | fzf --query="$1" --select-1 --exit-0) && git checkout "$branch"
+}
+
+
+# fh - repeat history
+# h() {
+#   eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//')
+# }
+
+# fkill - kill process
+fkill() {
+  ps -ef | sed 1d | fzf-tmux -m | awk '{print $2}' | xargs kill -${1:-9}
+}
+# }}}
+
+# Tmux utils {{{
+kill_detached() {
+  for sess in $(tmux ls | grep -v attached | sed -s "s/:.*$//"); do
+    tmux kill-session -t $sess;
+  done
+}
+# }}}
+
+# Docker {{{
+
+
+# dbp foo/bar .
+function dbp () {
+  docker build -t $1 ${@:2} && docker push $1
+}
+
+# }}}
+
+# Twitter! {{{
+
+
+# favelast <username>
+function favelast() {
+  t fave $(t tl -l $1 | head -n1 | first)
+}
+
+function rtlast() {
+  t rt $(t tl -l $1 | head -n1 | first)
+}
+
+function tthread() {
+  t reply $(t tl -l $TWITTER_WHOAMI | head -n1 | first) $@
+}
+# }}}
+
+# Geeknote {{{
+gnc() {
+  gn create --title $1 --content '' &&
+    gn find --count=1 "$1"
+    gn edit 1
+}
+# }}}
+
+# Misc aliases {{{
+
+function fw() { # fix white
+  local substitution
+  local substitution='s/\x1b\[90m/\x1b[92m/g'
+  $@ > >(perl -pe "$substitution") 2> >(perl -pe "$substitution" 1>&2)
+}
+# }}}
+
+# Grep options {{{
+unset GREP_OPTIONS
+export GREP_OPTIONS=
+# }}}
+
+
+# Run docker containers {{{
+    # -d \
+    # -v $HOME/.pentadactyl:/home/firefox/.pentadactyl:rw \
+    # -v $HOME/.pentadactylrc:/home/firefox/.pentadactylrc:rw \
+    # -v $HOME/.mozilla:/home/firefox/.mozilla:rw \
+    # -v $HOME/.config:/home/firefox/.config \
+    # -v $HOME/Downloads:/home/firefox/Downloads:rw \
+    # -v /etc/fonts:/etc/fonts \
+    # -v /tmp/.X11-unix:/tmp/.X11-unix \
+    # -v /dev/snd:/dev/snd \
+    # --net=host \
+    # -v $XDG_RUNTIME_DIR:$XDG_RUNTIME_DIR \
+    # -e uid=$(id -u) \
+    # -e gid=$(id -g) \
+    # -e DISPLAY=$DISPLAY \
+    # -e XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR \
+    # --name firefox \
+    # --rm -it \
+    # glittershark/firefox
+# }}}
+
+# Change cursor shape on insert/normal mode {{{
+# (https://unix.stackexchange.com/q/433273/64261)
+
+KEYTIMEOUT=5
+
+_fix_cursor() {
+   echo -ne '\e[5 q'
+}
+
+precmd_functions+=(_fix_cursor)
+
+function zle-keymap-select {
+  if [[ ${KEYMAP} == vicmd ]] ||
+       [[ $1 = 'block' ]]; then
+  echo -ne '\e[1 q'
+
+  elif [[ ${KEYMAP} == main ]] ||
+         [[ ${KEYMAP} == viins ]] ||
+         [[ ${KEYMAP} = '' ]] ||
+         [[ $1 = 'beam' ]]; then
+  echo -ne '\e[5 q'
+  fi
+}
+zle -N zle-keymap-select
+
+# }}}
+
+[ -f ./.localrc ] && source ./.localrc
diff --git a/users/grfn/system/home/platforms/darwin.nix b/users/grfn/system/home/platforms/darwin.nix
new file mode 100644
index 0000000000..f98b80f269
--- /dev/null
+++ b/users/grfn/system/home/platforms/darwin.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  config = {
+    home.packages = with pkgs; [
+      coreutils
+      gnupg
+      pinentry_mac
+    ];
+
+    home.activation.linkApplications = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
+      $DRY_RUN_CMD ln -sf $VERBOSE_ARG \
+        ~/.nix-profile/Applications/* ~/Applications/
+    '';
+
+    programs.zsh.initExtra = ''
+      export NIX_PATH=$HOME/.nix-defexpr/channels:$NIX_PATH
+
+      if [[ "$TERM" == "alacritty" ]]; then
+        export TERM="xterm-256color"
+      fi
+    '';
+  };
+}
diff --git a/users/grfn/system/home/platforms/linux.nix b/users/grfn/system/home/platforms/linux.nix
new file mode 100644
index 0000000000..decc8b8c2e
--- /dev/null
+++ b/users/grfn/system/home/platforms/linux.nix
@@ -0,0 +1,92 @@
+{ config, pkgs, ... }:
+
+let
+
+  depot = config.lib.depot;
+
+in
+
+{
+  imports = [
+    ../modules/alacritty.nix
+    ../modules/alsi.nix
+    ../modules/development.nix
+    ../modules/emacs.nix
+    ../modules/email.nix
+    ../modules/firefox.nix
+    ../modules/games.nix
+    ../modules/obs.nix
+    ../modules/i3.nix
+    ../modules/shell.nix
+    ../modules/tarsnap.nix
+    ../modules/vim.nix
+  ];
+
+  xsession.enable = true;
+
+  home.packages = with pkgs; [
+    # Desktop stuff
+    arandr
+    firefox
+    feh
+    chromium
+    xclip
+    xorg.xev
+    picom
+    peek
+    signal-desktop
+    apvlv # pdf viewer
+    vlc
+    irssi
+    gnutls
+    pandoc
+    barrier
+    depot.tools.nsfv-setup
+    glimpse # fork of gimp
+
+    # System utilities
+    powertop
+    usbutils
+    pciutils
+    gdmap
+    lsof
+    tree
+    nmap
+    iftop
+
+    # Security
+    gnupg
+    keybase
+    openssl
+    yubikey-manager
+    yubikey-manager-qt
+
+    # Spotify...etc
+    spotify
+    playerctl
+  ];
+
+  services.redshift = {
+    enable = true;
+    provider = "geoclue2";
+  };
+
+  services.pasystray.enable = true;
+
+  services.gpg-agent = {
+    enable = true;
+  };
+
+  gtk = {
+    enable = true;
+    gtk3.bookmarks = [
+      "file:///home/grfn/code"
+    ];
+  };
+
+  programs.zsh.initExtra = ''
+    [[ ! $IN_NIX_SHELL ]] && alsi -l
+  '';
+
+  services.lorri.enable = true;
+}
diff --git a/users/grfn/system/install b/users/grfn/system/install
new file mode 100755
index 0000000000..a9a45953da
--- /dev/null
+++ b/users/grfn/system/install
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+if [[ -f /etc/nixos/.system-installed ]]; then
+    echo "=== System config already installed, skipping"
+else
+    echo "==> Installing system config"
+
+    [[ -d /etc/nixos ]] && sudo mv /etc/nixos{,.bak}
+    sudo mkdir -p /etc/nixos
+    sudo cp /etc/nixos.bak/hardware-configuration.nix /etc/nixos
+
+    sudo cp ./system/configuration.nix /etc/nixos/
+    sudo ln -s $(pwd)/system/{machines,modules,pkgs} /etc/nixos
+    sudo touch /etc/nixos/.system-installed
+
+    echo "==> System config installed, your old configuration is at /etc/nixos.bak"
+fi
+echo
+
+if [[ -f ~/.config/nixpkgs/system-installed ]]; then
+    echo "=== home-manager config already installed, skipping"
+else
+    echo "==> Installing home-manager config"
+    nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager
+    nix-channel --update
+    # nix-shell '<home-manager>' -A install
+
+    [[ -d ~/.config/nixpkgs ]] && mv ~/.config/{nixpkgs,nixpkgs.bak}
+    mkdir -p ~/.config/nixpkgs
+    ln -s $(pwd)/home/* ~/.config/nixpkgs
+
+    echo "==> home-manager config installed"
+fi
diff --git a/users/grfn/system/system/.skip-subtree b/users/grfn/system/system/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/grfn/system/system/.skip-subtree
diff --git a/users/grfn/system/system/configuration.nix b/users/grfn/system/system/configuration.nix
new file mode 100644
index 0000000000..eae567015b
--- /dev/null
+++ b/users/grfn/system/system/configuration.nix
@@ -0,0 +1,11 @@
+{ config, pkgs, ... }:
+
+let machine = throw "Pick a machine from ./machines"; in
+{
+  imports =
+    [
+      /etc/nixos/hardware-configuration.nix
+      ./modules/common.nix
+      machine
+    ];
+}
diff --git a/users/grfn/system/system/default.nix b/users/grfn/system/system/default.nix
new file mode 100644
index 0000000000..9196758e64
--- /dev/null
+++ b/users/grfn/system/system/default.nix
@@ -0,0 +1,38 @@
+args @ { depot, pkgs, ... }:
+
+rec {
+  mugwump = import ./machines/mugwump.nix;
+
+  mugwumpSystem = (depot.ops.nixos.nixosFor mugwump).system;
+
+  roswell = import ./machines/roswell.nix;
+
+  roswellSystem = (depot.ops.nixos.nixosFor ({ ... }: {
+    imports = [
+      ./machines/roswell.nix
+      "${pkgs.home-manager.src}/nixos"
+    ];
+
+    # Use the same nixpkgs as everything else
+    home-manager.useGlobalPkgs = true;
+
+    home-manager.users.grfn = { config, lib, ... }: {
+      imports = [ ../home/machines/roswell.nix ];
+      lib.depot = depot;
+    };
+  })).system;
+
+  yeren = import ./machines/yeren.nix;
+
+  yerenSystem = (depot.ops.nixos.nixosFor yeren).system;
+
+  iso = import ./iso.nix args;
+
+  meta.ci.targets = [
+    "mugwumpSystem"
+    "roswellSystem"
+    "yerenSystem"
+
+    "iso"
+  ];
+}
diff --git a/users/grfn/system/system/iso.nix b/users/grfn/system/system/iso.nix
new file mode 100644
index 0000000000..9fa8e7ec7e
--- /dev/null
+++ b/users/grfn/system/system/iso.nix
@@ -0,0 +1,18 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  configuration = { ... }: {
+    imports = [
+      (pkgs.path + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
+      (pkgs.path + "/nixos/modules/installer/cd-dvd/channel.nix")
+    ];
+
+    networking.networkmanager.enable = true;
+    networking.useDHCP = false;
+    networking.firewall.enable = false;
+    networking.wireless.enable = lib.mkForce false;
+  };
+in
+(depot.third_party.nixos {
+  inherit configuration;
+}).config.system.build.isoImage
diff --git a/users/grfn/system/system/machines/bumblebee.nix b/users/grfn/system/system/machines/bumblebee.nix
new file mode 100644
index 0000000000..0fec214092
--- /dev/null
+++ b/users/grfn/system/system/machines/bumblebee.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+{
+  imports = [
+    ../modules/reusable/battery.nix
+  ];
+
+  networking.hostName = "bumblebee";
+
+  powerManagement = {
+    enable = true;
+    cpuFreqGovernor = "powersave";
+    powertop.enable = true;
+  };
+
+  # Hibernate on low battery
+  laptop.onLowBattery = {
+    enable = true;
+    action = "hibernate";
+    thresholdPercentage = 5;
+  };
+
+  services.xserver.xkbOptions = "caps:swapescape";
+}
diff --git a/users/grfn/system/system/machines/mugwump.nix b/users/grfn/system/system/machines/mugwump.nix
new file mode 100644
index 0000000000..0774e06119
--- /dev/null
+++ b/users/grfn/system/system/machines/mugwump.nix
@@ -0,0 +1,291 @@
+{ config, lib, pkgs, modulesPath, depot, ... }:
+
+with lib;
+
+{
+  imports = [
+    ../modules/common.nix
+    (modulesPath + "/installer/scan/not-detected.nix")
+    (depot.path.origSrc + "/ops/modules/prometheus-fail2ban-exporter.nix")
+    (depot.path.origSrc + "/users/grfn/xanthous/server/module.nix")
+    (depot.third_party.agenix.src + "/modules/age.nix")
+    (depot.path.origSrc + "/users/grfn/bbbg/module.nix")
+  ];
+
+  networking.hostName = "mugwump";
+
+  boot = {
+    loader.systemd-boot.enable = true;
+
+    kernelModules = [ "kvm-intel" ];
+    extraModulePackages = [ ];
+
+    initrd = {
+      availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
+      kernelModules = [
+        "uas"
+        "usbcore"
+        "usb_storage"
+        "vfat"
+        "nls_cp437"
+        "nls_iso8859_1"
+      ];
+
+      postDeviceCommands = pkgs.lib.mkBefore ''
+        mkdir -m 0755 -p /key
+        sleep 2
+        mount -n -t vfat -o ro `findfs UUID=9048-A9D5` /key
+      '';
+
+      luks.devices."cryptroot" = {
+        device = "/dev/disk/by-uuid/803a9028-339c-4617-a213-4fe138161f6d";
+        keyFile = "/key/keyfile";
+        preLVM = false;
+      };
+    };
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "/dev/mapper/cryptroot";
+      fsType = "btrfs";
+    };
+    "/boot" = {
+      device = "/dev/disk/by-uuid/7D74-0E4B";
+      fsType = "vfat";
+    };
+  };
+
+  networking.interfaces = {
+    enp0s25.useDHCP = false;
+    wlp2s0.useDHCP = false;
+  };
+
+  networking.firewall.enable = true;
+  networking.firewall.allowedTCPPorts = [ 22 80 443 ];
+
+  security.sudo.extraRules = [{
+    groups = [ "wheel" ];
+    commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
+  }];
+
+  nix.gc.dates = "monthly";
+
+  age.secrets =
+    let
+      secret = name: depot.users.grfn.secrets."${name}.age";
+    in
+    {
+      bbbg.file = secret "bbbg";
+      cloudflare.file = secret "cloudflare";
+      ddclient-password.file = secret "ddclient-password";
+
+      buildkite-ssh-key = {
+        file = secret "buildkite-ssh-key";
+        group = "keys";
+        mode = "0440";
+      };
+
+      buildkite-token = {
+        file = secret "buildkite-token";
+        group = "keys";
+        mode = "0440";
+      };
+    };
+
+  services.fail2ban = {
+    enable = true;
+    ignoreIP = [
+      "172.16.0.0/16"
+    ];
+  };
+
+  services.openssh = {
+    allowSFTP = false;
+    passwordAuthentication = false;
+    permitRootLogin = "no";
+  };
+
+  services.grafana = {
+    enable = true;
+    port = 3000;
+    domain = "metrics.gws.fyi";
+    rootUrl = "https://metrics.gws.fyi";
+    dataDir = "/var/lib/grafana";
+    analytics.reporting.enable = false;
+
+    provision = {
+      enable = true;
+      datasources = [{
+        name = "Prometheus";
+        type = "prometheus";
+        url = "http://localhost:9090";
+      }];
+    };
+  };
+
+  security.acme.defaults.email = "root@gws.fyi";
+  security.acme.acceptTerms = true;
+
+  services.nginx = {
+    enable = true;
+    statusPage = true;
+    recommendedGzipSettings = true;
+    recommendedOptimisation = true;
+    recommendedTlsSettings = true;
+    recommendedProxySettings = true;
+
+    virtualHosts = {
+      "metrics.gws.fyi" = {
+        enableACME = true;
+        forceSSL = true;
+        locations."/" = {
+          proxyPass = "http://localhost:${toString config.services.grafana.port}";
+        };
+      };
+    };
+  };
+
+  services.ddclient = {
+    enable = true;
+    domains = [ "home.gws.fyi" ];
+    interval = "1d";
+    zone = "gws.fyi";
+    protocol = "cloudflare";
+    username = "root@gws.fyi";
+    passwordFile = config.age.secretsDir + "/ddclient-password";
+    quiet = true;
+  };
+
+  systemd.services.ddclient.serviceConfig.DynamicUser = lib.mkForce false;
+
+  security.acme.certs."metrics.gws.fyi" = {
+    dnsProvider = "cloudflare";
+    credentialsFile = config.age.secretsDir + "/cloudflare";
+    webroot = mkForce null;
+  };
+
+  services.prometheus = {
+    enable = true;
+    exporters = {
+      node = {
+        enable = true;
+        openFirewall = false;
+
+        enabledCollectors = [
+          "processes"
+          "systemd"
+          "tcpstat"
+          "wifi"
+        ];
+      };
+
+      nginx = {
+        enable = true;
+        openFirewall = true;
+        sslVerify = false;
+        constLabels = [ "host=mugwump" ];
+      };
+
+      blackbox = {
+        enable = true;
+        openFirewall = true;
+        configFile = pkgs.writeText "blackbox-exporter.yaml" (builtins.toJSON {
+          modules = {
+            https_2xx = {
+              prober = "http";
+              http = {
+                method = "GET";
+                fail_if_ssl = false;
+                fail_if_not_ssl = true;
+                preferred_ip_protocol = "ip4";
+              };
+            };
+          };
+        });
+      };
+    };
+
+    scrapeConfigs = [
+      {
+        job_name = "node";
+        scrape_interval = "5s";
+        static_configs = [{
+          targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
+        }];
+      }
+      {
+        job_name = "nginx";
+        scrape_interval = "5s";
+        static_configs = [{
+          targets = [ "localhost:${toString config.services.prometheus.exporters.nginx.port}" ];
+        }];
+      }
+      {
+        job_name = "xanthous_server";
+        scrape_interval = "1s";
+        static_configs = [{
+          targets = [ "localhost:${toString config.services.xanthous-server.metricsPort}" ];
+        }];
+      }
+      {
+        job_name = "blackbox";
+        metrics_path = "/probe";
+        params.module = [ "https_2xx" ];
+        scrape_interval = "5s";
+        static_configs = [{
+          targets = [
+            "https://gws.fyi"
+            "https://windtunnel.ci"
+            "https://app.windtunnel.ci"
+            "https://metrics.gws.fyi"
+          ];
+        }];
+        relabel_configs = [{
+          source_labels = [ "__address__" ];
+          target_label = "__param_target";
+        }
+          {
+            source_labels = [ "__param_target" ];
+            target_label = "instance";
+          }
+          {
+            target_label = "__address__";
+            replacement = "localhost:${toString config.services.prometheus.exporters.blackbox.port}";
+          }];
+      }
+    ];
+  };
+
+  services.xanthous-server.enable = true;
+
+  services.bbbg.enable = true;
+  services.bbbg.domain = "staging.bbbg.gws.fyi";
+  services.bbbg.database.enable = true;
+  services.bbbg.proxy.enable = true;
+
+  virtualisation.docker.enable = true;
+
+  services.buildkite-agents = listToAttrs (map
+    (n: rec {
+      name = "mugwump-${toString n}";
+      value = {
+        inherit name;
+        enable = true;
+        tokenPath = config.age.secretsDir + "/buildkite-token";
+        privateSshKeyPath = config.age.secretsDir + "/buildkite-ssh-key";
+        runtimePackages = with pkgs; [
+          docker
+          nix
+          gnutar
+          gzip
+        ];
+      };
+    })
+    (range 1 1));
+
+  users.users."buildkite-agent-mugwump-1" = {
+    isSystemUser = true;
+    extraGroups = [ "docker" ];
+  };
+}
diff --git a/users/grfn/system/system/machines/roswell.nix b/users/grfn/system/system/machines/roswell.nix
new file mode 100644
index 0000000000..6eb4a510b8
--- /dev/null
+++ b/users/grfn/system/system/machines/roswell.nix
@@ -0,0 +1,17 @@
+{ depot, config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports = [
+    ../modules/common.nix
+    "${modulesPath}/installer/scan/not-detected.nix"
+    "${modulesPath}/virtualisation/amazon-image.nix"
+  ];
+
+  ec2.hvm = true;
+
+  networking.hostName = "roswell";
+
+  users.users.grfn.openssh.authorizedKeys.keys = [
+    depot.users.grfn.keys.main
+  ];
+}
diff --git a/users/grfn/system/system/machines/yeren.nix b/users/grfn/system/system/machines/yeren.nix
new file mode 100644
index 0000000000..228631c8cc
--- /dev/null
+++ b/users/grfn/system/system/machines/yeren.nix
@@ -0,0 +1,137 @@
+{ depot, modulesPath, config, lib, pkgs, ... }:
+
+{
+  imports = [
+    (modulesPath + "/installer/scan/not-detected.nix")
+    ../modules/common.nix
+    ../modules/laptop.nix
+    ../modules/xserver.nix
+    ../modules/fonts.nix
+    ../modules/sound.nix
+    ../modules/tvl.nix
+    ../modules/development.nix
+    ../modules/work/kolide.nix
+  ];
+
+  networking.hostName = "yeren";
+
+  system.stateVersion = "21.03";
+
+  boot = {
+    initrd = {
+      availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
+      kernelModules = [ ];
+
+      luks.devices = {
+        "cryptroot".device = "/dev/disk/by-uuid/dcfbc22d-e0d2-411b-8dd3-96704d3aae2e";
+      };
+    };
+
+    kernelPackages = pkgs.linuxPackages_5_15;
+
+    kernelModules = [ "kvm-intel" ];
+    blacklistedKernelModules = [ "psmouse" ];
+    extraModulePackages = [
+      config.boot.kernelPackages.digimend
+    ];
+    kernelParams = [
+      "i915.preliminary_hw_support=1"
+      "pcie_aspm=force"
+    ];
+
+    # https://bbs.archlinux.org/viewtopic.php?pid=1933643#p1933643
+    extraModprobeConfig = ''
+      options snd-intel-dspcfg dsp_driver=1
+    '';
+
+    kernel.sysctl = {
+      "kernel.perf_event_paranoid" = -1;
+    };
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "/dev/mapper/cryptroot";
+      fsType = "btrfs";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/53A9-248B";
+      fsType = "vfat";
+    };
+  };
+
+  swapDevices = [{
+    device = "/dev/disk/by-uuid/b627cb0e-0451-4f25-94d0-6497e01f0da4";
+  }];
+
+  services.earlyoom = {
+    enable = true;
+    freeMemThreshold = 5;
+  };
+
+  services.xserver = {
+    exportConfiguration = true;
+    extraConfig = ''
+      Section "Device"
+        Identifier  "Intel Graphics"
+        Driver      "intel"
+        Option      "TripleBuffer" "true"
+        Option      "TearFree"     "true"
+        Option      "DRI"          "true"
+        Option      "AccelMethod"  "sna"
+      EndSection
+    '';
+  };
+
+  hardware.firmware = with pkgs; [
+    alsa-firmware
+    sof-firmware
+  ];
+
+  hardware.opengl.extraPackages = with pkgs; [
+    vaapiIntel
+    vaapiVdpau
+    libvdpau-va-gl
+    intel-media-driver
+  ];
+
+  # Disabled for now until libfprint-tod can get a version bump
+  # services.fprintd = {
+  #   enable = true;
+  #   package = pkgs.fprintd-tod;
+  # };
+
+  systemd.services.fprintd.environment.FP_TOD_DRIVERS_DIR =
+    "${pkgs.libfprint-2-tod1-goodix}/usr/lib/libfprint-2/tod-1";
+
+  security.pam.loginLimits = [
+    {
+      domain = "grfn";
+      type = "soft";
+      item = "nofile";
+      value = "65535";
+    }
+  ];
+
+  security.pam.services = {
+    login.fprintAuth = true;
+    sudo.fprintAuth = true;
+    i3lock.fprintAuth = false;
+    i3lock-color.fprintAuth = false;
+    lightdm.fprintAuth = true;
+    lightdm-greeter.fprintAuth = true;
+  };
+
+  hardware.opengl.driSupport32Bit = true;
+
+  hardware.pulseaudio.extraConfig = ''
+    load-module module-remap-source source_name=KompleteAudio6_1 source_properties=device.description=KompleteAudio6Input1 master=alsa_input.usb-Native_Instruments_Komplete_Audio_6_458E0FFD-00.multichannel-input remix=no channels=1 master_channel_map=front-left channel_map=mono
+    load-module module-remap-source source_name=KompleteAudio6_2 source_properties=device.description=KompleteAudio6Input2 master=alsa_input.usb-Native_Instruments_Komplete_Audio_6_458E0FFD-00.multichannel-input remix=no channels=1 master_channel_map=front-right channel_map=mono
+    load-module module-remap-sink sink_name=KompleteAudio6_12 sink_properties=device.description=KompleteAudio6_12 remix=no master=alsa_output.usb-Native_Instruments_Komplete_Audio_6_458E0FFD-00.analog-surround-21 channels=2 master_channel_map=front-left,front-right channel_map=front-left,front-right
+  '';
+
+  services.fwupd.enable = true;
+
+  services.tailscale.enable = true;
+}
diff --git a/users/grfn/system/system/modules/common.nix b/users/grfn/system/system/modules/common.nix
new file mode 100644
index 0000000000..a7d5a62e68
--- /dev/null
+++ b/users/grfn/system/system/modules/common.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  depot = import ../../../../.. { };
+
+in
+
+with lib;
+
+{
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  networking.useDHCP = false;
+  networking.networkmanager.enable = true;
+
+  i18n = {
+    defaultLocale = "en_US.UTF-8";
+  };
+
+  time.timeZone = lib.mkDefault "America/New_York";
+
+  environment.systemPackages = with pkgs; [
+    wget
+    vim
+    zsh
+    git
+    w3m
+    libnotify
+    file
+    lm_sensors
+    dnsutils
+    htop
+    man-pages
+    man-pages-posix
+  ];
+
+  documentation.dev.enable = true;
+  documentation.man.generateCaches = true;
+
+  services.openssh.enable = true;
+
+  programs.ssh.startAgent = true;
+
+  networking.firewall.enable = mkDefault false;
+
+  users.mutableUsers = true;
+  programs.zsh.enable = true;
+  environment.pathsToLink = [ "/share/zsh" ];
+  users.users.grfn = {
+    isNormalUser = true;
+    initialPassword = "password";
+    extraGroups = [
+      "wheel"
+      "networkmanager"
+      "audio"
+      "docker"
+    ];
+    shell = pkgs.zsh;
+  };
+
+  nix = {
+    settings.trusted-users = [ "grfn" ];
+    distributedBuilds = true;
+
+    gc = {
+      automatic = true;
+      dates = mkDefault "weekly";
+      options = "--delete-older-than 30d";
+    };
+  };
+
+  services.udev.packages = with pkgs; [
+    yubikey-personalization
+  ];
+
+  services.pcscd.enable = true;
+
+  services.udev.extraRules = ''
+    # UDEV rules for Teensy USB devices
+    ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", ENV{ID_MM_DEVICE_IGNORE}="1"
+    ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789A]?", ENV{MTP_NO_PROBE}="1"
+    SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789ABCD]?", MODE:="0666"
+    KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", MODE:="0666"
+  '';
+}
diff --git a/users/grfn/system/system/modules/desktop.nix b/users/grfn/system/system/modules/desktop.nix
new file mode 100644
index 0000000000..3adbd9d9b0
--- /dev/null
+++ b/users/grfn/system/system/modules/desktop.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./xserver.nix
+    ./fonts.nix
+    ./sound.nix
+    ./kernel.nix
+  ];
+
+  programs.nm-applet.enable = true;
+
+  users.users.grfn.extraGroups = [
+    "audio"
+    "video"
+  ];
+
+  services.geoclue2.enable = true;
+}
diff --git a/users/grfn/system/system/modules/development.nix b/users/grfn/system/system/modules/development.nix
new file mode 100644
index 0000000000..bfa0e22cff
--- /dev/null
+++ b/users/grfn/system/system/modules/development.nix
@@ -0,0 +1,6 @@
+{ config, lib, pkgs, ... }:
+
+{
+  virtualisation.docker.enable = true;
+  users.users.grfn.extraGroups = [ "docker" ];
+}
diff --git a/users/grfn/system/system/modules/fcitx.nix b/users/grfn/system/system/modules/fcitx.nix
new file mode 100644
index 0000000000..812f598f9f
--- /dev/null
+++ b/users/grfn/system/system/modules/fcitx.nix
@@ -0,0 +1,10 @@
+{ config, lib, pkgs, ... }:
+
+{
+  i18n.inputMethod = {
+    enabled = "fcitx";
+    fcitx.engines = with pkgs.fcitx-engines; [
+      cloudpinyin
+    ];
+  };
+}
diff --git a/users/grfn/system/system/modules/fonts.nix b/users/grfn/system/system/modules/fonts.nix
new file mode 100644
index 0000000000..f30600b28b
--- /dev/null
+++ b/users/grfn/system/system/modules/fonts.nix
@@ -0,0 +1,12 @@
+{ config, lib, pkgs, ... }:
+{
+  fonts = {
+    fonts = with pkgs; [
+      nerdfonts
+      noto-fonts-emoji
+      twitter-color-emoji
+    ];
+
+    fontconfig.defaultFonts.emoji = [ "Twitter Color Emoji" ];
+  };
+}
diff --git a/users/grfn/system/system/modules/laptop.nix b/users/grfn/system/system/modules/laptop.nix
new file mode 100644
index 0000000000..05c5333e51
--- /dev/null
+++ b/users/grfn/system/system/modules/laptop.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./reusable/battery.nix
+  ];
+
+  laptop.onLowBattery.enable = true;
+
+  services.logind.extraConfig = ''
+    HandlePowerKey=hibernate
+  '';
+
+  services.tlp.enable = true;
+}
diff --git a/users/grfn/system/system/modules/reusable/README.org b/users/grfn/system/system/modules/reusable/README.org
new file mode 100644
index 0000000000..34d9bfdcb7
--- /dev/null
+++ b/users/grfn/system/system/modules/reusable/README.org
@@ -0,0 +1,2 @@
+This directory contains things I'm eventually planning on contributing upstream
+to nixpkgs
diff --git a/users/grfn/system/system/modules/reusable/battery.nix b/users/grfn/system/system/modules/reusable/battery.nix
new file mode 100644
index 0000000000..151c2a246f
--- /dev/null
+++ b/users/grfn/system/system/modules/reusable/battery.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+with lib;
+{
+  options = {
+    laptop.onLowBattery = {
+      enable = mkEnableOption "Perform action on low battery";
+
+      thresholdPercentage = mkOption {
+        description = "Threshold battery percentage on which to perform the action";
+        default = 8;
+        type = types.int;
+      };
+
+      action = mkOption {
+        description = "Action to perform on low battery";
+        default = "hibernate";
+        type = types.enum [ "hibernate" "suspend" "suspend-then-hibernate" ];
+      };
+    };
+  };
+
+  config =
+    let cfg = config.laptop.onLowBattery;
+    in mkIf cfg.enable {
+      services.udev.extraRules = concatStrings [
+        ''SUBSYSTEM=="power_supply", ''
+        ''ATTR{status}=="Discharging", ''
+        ''ATTR{capacity}=="[0-${toString cfg.thresholdPercentage}]", ''
+        ''RUN+="${pkgs.systemd}/bin/systemctl ${cfg.action}"''
+      ];
+    };
+}
diff --git a/users/grfn/system/system/modules/rtlsdr.nix b/users/grfn/system/system/modules/rtlsdr.nix
new file mode 100644
index 0000000000..ce58ebb0dc
--- /dev/null
+++ b/users/grfn/system/system/modules/rtlsdr.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+{
+
+  environment.systemPackages = with pkgs; [
+    rtl-sdr
+  ];
+
+  services.udev.packages = with pkgs; [
+    rtl-sdr
+  ];
+
+  # blacklist for rtl-sdr
+  boot.blacklistedKernelModules = [
+    "dvb_usb_rtl28xxu"
+  ];
+}
diff --git a/users/grfn/system/system/modules/sound.nix b/users/grfn/system/system/modules/sound.nix
new file mode 100644
index 0000000000..07a67a1ec4
--- /dev/null
+++ b/users/grfn/system/system/modules/sound.nix
@@ -0,0 +1,16 @@
+{ config, lib, pkgs, ... }:
+
+{
+  # Enable sound.
+  sound.enable = true;
+  hardware.pulseaudio.enable = true;
+
+  environment.systemPackages = with pkgs; [
+    pulseaudio-ctl
+    paprefs
+    pasystray
+    pavucontrol
+  ];
+
+  hardware.pulseaudio.package = pkgs.pulseaudioFull;
+}
diff --git a/users/grfn/system/system/modules/tvl.nix b/users/grfn/system/system/modules/tvl.nix
new file mode 100644
index 0000000000..3c0326c664
--- /dev/null
+++ b/users/grfn/system/system/modules/tvl.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+{
+  nix = {
+    buildMachines = [{
+      hostName = "whitby.tvl.fyi";
+      sshUser = "grfn";
+      sshKey = "/root/.ssh/id_rsa";
+      system = "x86_64-linux";
+      maxJobs = 64;
+      supportedFeatures = [ "big-parallel" "kvm" "nixos-test" "benchmark" ];
+    }];
+
+    extraOptions = ''
+      builders-use-substitutes = true
+    '';
+
+    settings = {
+      substituters = [
+        "https://cache.nixos.org"
+        "ssh://nix-ssh@whitby.tvl.fyi"
+      ];
+      trusted-substituters = [
+        "https://cache.nixos.org"
+        "ssh://nix-ssh@whitby.tvl.fyi"
+      ];
+      trusted-public-keys = [
+        "cache.tvl.fyi:fd+9d1ceCPvDX/xVhcfv8nAa6njEhAGAEe+oGJDEeoc="
+      ];
+    };
+  };
+
+  programs.ssh.knownHosts.whitby = {
+    extraHostNames = [ "whitby" "whitby.tvl.fyi" "49.12.129.211" ];
+    publicKeyFile = pkgs.writeText "whitby.pub" ''
+      ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILNh/w4BSKov0jdz3gKBc98tpoLta5bb87fQXWBhAl2I
+    '';
+  };
+}
diff --git a/users/grfn/system/system/modules/work/kolide.deb b/users/grfn/system/system/modules/work/kolide.deb
new file mode 100644
index 0000000000..a319a5806f
--- /dev/null
+++ b/users/grfn/system/system/modules/work/kolide.deb
Binary files differdiff --git a/users/grfn/system/system/modules/work/kolide.nix b/users/grfn/system/system/modules/work/kolide.nix
new file mode 100644
index 0000000000..e4ee786f0c
--- /dev/null
+++ b/users/grfn/system/system/modules/work/kolide.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+let
+  deb = ./kolide.deb;
+
+  kolide = pkgs.runCommand "kolide-data"
+    {
+      buildInputs = [ pkgs.binutils-unwrapped ];
+    } ''
+    cp ${deb} ./kolide.deb
+    ar x kolide.deb
+    mkdir result
+    tar xzf data.tar.gz -C result
+    patchelf \
+      --set-interpreter ${pkgs.glibc}/lib/ld-linux-x86-64.so.2 \
+      --set-rpath "${lib.makeLibraryPath (with pkgs; [
+        zlib
+      ])}" \
+      result/usr/local/kolide-k2/bin/osqueryd
+    mv result $out
+  '';
+
+in
+{
+  systemd.services."launcher.kolide-k2" = {
+    wantedBy = [ "multi-user.target" ];
+    after = [ "network.target" "syslog.service" ];
+    description = "The Kolide Launcher";
+    serviceConfig = {
+      ExecStart = ''
+        ${kolide}/usr/local/kolide-k2/bin/launcher \
+          -config \
+          ${pkgs.writeText "launcher.flags" ''
+            with_initial_runner
+            control
+            autoupdate
+            root_directory /var/lib/kolide
+            osqueryd_path ${kolide}/usr/local/kolide-k2/bin/osqueryd
+            enroll_secret_path ${kolide}/etc/kolide-k2/secret
+            control_hostname k2control.kolide.com
+            update_channel stable
+            transport jsonrpc
+            hostname k2device.kolide.com
+          ''}
+      '';
+      StateDirectory = "kolide";
+      Restart = "on-failure";
+      RestartSec = 3;
+    };
+  };
+}
diff --git a/users/grfn/system/system/modules/xserver.nix b/users/grfn/system/system/modules/xserver.nix
new file mode 100644
index 0000000000..35ee44112e
--- /dev/null
+++ b/users/grfn/system/system/modules/xserver.nix
@@ -0,0 +1,16 @@
+{ config, pkgs, ... }:
+{
+  # Enable the X11 windowing system.
+  services.xserver = {
+    enable = true;
+    layout = "us";
+
+    libinput.enable = true;
+
+    displayManager = {
+      defaultSession = "none+i3";
+    };
+
+    windowManager.i3.enable = true;
+  };
+}
diff --git a/users/grfn/terraform/globals.nix b/users/grfn/terraform/globals.nix
new file mode 100644
index 0000000000..c6bc24c22b
--- /dev/null
+++ b/users/grfn/terraform/globals.nix
@@ -0,0 +1,27 @@
+{ pkgs, ... }:
+
+{
+  provider.aws = map
+    (region: {
+      inherit region;
+      alias = region;
+      profile = "personal";
+    }) [
+    "us-east-1"
+    "us-east-2"
+    "us-west-2"
+  ];
+
+  data.external.cloudflare_api_key = {
+    program = [
+      (pkgs.writeShellScript "cloudflare_api_key" ''
+        jq -n --arg api_key "$(pass cloudflare-api-key)" '{"api_key":$api_key}'
+      '')
+    ];
+  };
+
+  provider.cloudflare = {
+    email = "root@gws.fyi";
+    api_key = "\${data.external.cloudflare_api_key.result.api_key}";
+  };
+}
diff --git a/users/grfn/terraform/nixosMachine.nix b/users/grfn/terraform/nixosMachine.nix
new file mode 100644
index 0000000000..23cd838804
--- /dev/null
+++ b/users/grfn/terraform/nixosMachine.nix
@@ -0,0 +1,208 @@
+{ depot, pkgs, lib, ... }:
+
+# mostly stolen from espes
+
+{ name
+, instanceType
+, configuration
+, prefix ? "${name}_"
+, region ? "us-east-2"
+, rootVolumeSizeGb ? 50
+, securityGroupId ? null
+, extraIngressPorts ? [ ]
+}:
+
+let
+  os = depot.ops.nixos.nixosFor ({ modulesPath, ... }: {
+    imports = [
+      (pkgs.path + "/nixos/modules/virtualisation/amazon-image.nix")
+      configuration
+    ];
+
+    ec2.hvm = true;
+    networking.hostName = name;
+    # TODO: remove this once the terraform tls provider supports ed25519 keys
+    # https://github.com/hashicorp/terraform-provider-tls/issues/26
+    services.openssh.extraConfig = ''
+      PubkeyAcceptedKeyTypes=+ssh-rsa
+      PubkeyAcceptedAlgorithms=+ssh-rsa
+    '';
+  });
+
+  targetUser = "root";
+
+  ec2Amis = import "${pkgs.path}/nixos/modules/virtualisation/ec2-amis.nix";
+
+  osRoot = os.config.system.build.toplevel;
+
+  osRootPath = builtins.unsafeDiscardStringContext (toString osRoot.outPath);
+  drvPath = builtins.unsafeDiscardStringContext (toString osRoot.drvPath);
+
+  machineResource = "aws_instance.${prefix}machine";
+
+  recursiveMerge = builtins.foldl' lib.recursiveUpdate { };
+
+  securityGroupId' =
+    if isNull securityGroupId
+    then "\${aws_security_group.${prefix}group.id}"
+    else securityGroupId;
+in
+recursiveMerge [
+  (lib.optionalAttrs (isNull securityGroupId) {
+    resource.aws_security_group."${prefix}group" = {
+      provider = "aws.${region}";
+      vpc_id = null;
+
+      # terraform isn't good about knowing what other resources depend on
+      # security groups
+      lifecycle.create_before_destroy = true;
+    };
+
+    resource.aws_security_group_rule.all_egress = {
+      provider = "aws.${region}";
+      security_group_id = securityGroupId';
+      type = "egress";
+      protocol = "-1";
+      from_port = 0;
+      to_port = 0;
+      cidr_blocks = [ "0.0.0.0/0" ];
+      ipv6_cidr_blocks = [ "::/0" ];
+
+      description = null;
+      prefix_list_ids = null;
+      self = null;
+    };
+  })
+  rec {
+    data.external.my_ip = {
+      program = [
+        (pkgs.writeShellScript "my_ip" ''
+          ${pkgs.jq}/bin/jq \
+            -n \
+            --arg ip "$(curl ifconfig.me)" \
+            '{"ip":$ip}'
+        '')
+      ];
+    };
+
+    resource.aws_security_group_rule.provision_ssh_access = {
+      provider = "aws.${region}";
+      security_group_id = securityGroupId';
+      type = "ingress";
+      protocol = "TCP";
+      from_port = 22;
+      to_port = 22;
+      cidr_blocks = [ "\${data.external.my_ip.result.ip}/32" ];
+      ipv6_cidr_blocks = [ ];
+      description = null;
+      prefix_list_ids = null;
+      self = null;
+    };
+
+    resource.tls_private_key."${prefix}key" = {
+      algorithm = "RSA";
+    };
+
+    resource.aws_key_pair."${prefix}generated_key" = {
+      provider = "aws.${region}";
+      key_name = "generated-key-\${sha256(tls_private_key.${prefix}key.public_key_openssh)}";
+      public_key = "\${tls_private_key.${prefix}key.public_key_openssh}";
+    };
+
+    resource.aws_instance."${prefix}machine" = {
+      provider = "aws.${region}";
+      ami = ec2Amis."21.05"."${region}".hvm-ebs;
+      instance_type = instanceType;
+      vpc_security_group_ids = [ securityGroupId' ];
+      key_name = "\${aws_key_pair.${prefix}generated_key.key_name}";
+      root_block_device = {
+        volume_size = rootVolumeSizeGb;
+        tags.Name = name;
+      };
+      tags.Name = name;
+    };
+
+    resource.null_resource."${prefix}deploy_nixos" = {
+      triggers = {
+        # deploy if the machine is recreated
+        machine_id = "\${${machineResource}.id}";
+
+        # deploy on os changes
+        os_drv = drvPath;
+      };
+
+      connection = {
+        type = "ssh";
+        host = "\${${machineResource}.public_ip}";
+        user = targetUser;
+        private_key = "\${tls_private_key.${prefix}key.private_key_pem}";
+      };
+
+      # do the actual deployment
+      provisioner = [
+        # wait till ssh is up
+        { remote-exec.inline = [ "true" ]; }
+
+        # copy the nixos closure
+        {
+          local-exec.command = ''
+            export PATH="${pkgs.openssh}/bin:$PATH"
+
+            scratch="$(mktemp -d)"
+            trap 'rm -rf -- "$scratch"' EXIT
+
+            # write out ssh key
+            echo -n "''${tls_private_key.${prefix}key.private_key_pem}" > $scratch/id_rsa.pem
+            chmod 0600 $scratch/id_rsa.pem
+
+            export NIX_SSHOPTS="\
+                -o StrictHostKeyChecking=no\
+                -o UserKnownHostsFile=/dev/null\
+                -o GlobalKnownHostsFile=/dev/null\
+                -o IdentityFile=$scratch/id_rsa.pem"
+
+            nix-build ${drvPath}
+            nix-copy-closure \
+              --to ${targetUser}@''${${machineResource}.public_ip} \
+              ${osRootPath} \
+              --gzip \
+              --use-substitutes
+          '';
+        }
+
+        # activate it
+        {
+          remote-exec.inline = [
+            # semicolons mandatory
+            ''
+              set -e;
+              nix-env --profile /nix/var/nix/profiles/system --set ${osRootPath};
+              ${osRootPath}/bin/switch-to-configuration switch;
+            ''
+          ];
+        }
+      ];
+    };
+  }
+
+  {
+    resource.aws_security_group_rule = builtins.listToAttrs (map
+      (port: {
+        name = "ingress_${toString port}";
+        value = {
+          provider = "aws.${region}";
+          security_group_id = securityGroupId';
+          type = "ingress";
+          protocol = "TCP";
+          from_port = port;
+          to_port = port;
+          cidr_blocks = [ "0.0.0.0/0" ];
+          ipv6_cidr_blocks = [ ];
+          description = null;
+          prefix_list_ids = null;
+          self = null;
+        };
+      })
+      extraIngressPorts);
+  }
+]
diff --git a/users/grfn/terraform/workspace.nix b/users/grfn/terraform/workspace.nix
new file mode 100644
index 0000000000..92bf6e4ec1
--- /dev/null
+++ b/users/grfn/terraform/workspace.nix
@@ -0,0 +1,107 @@
+{ pkgs, depot, ... }:
+name: { plugins }: module_tf:
+
+let
+
+  inherit (pkgs) lib runCommandNoCC writeText writeScript;
+  inherit (lib) filterAttrsRecursive;
+
+  allPlugins = (p: plugins p ++ (with p; [
+    external
+    local
+    tls
+    p.null
+  ]));
+
+  tf = pkgs.terraform.withPlugins allPlugins;
+
+  cleanTerraform = filterAttrsRecursive (k: _: ! (builtins.elem k [
+    "__readTree"
+    "__readTreeChildren"
+  ]));
+
+  plugins_tf = {
+    terraform.required_providers = (builtins.listToAttrs (map
+      (p: {
+        name = lib.last (lib.splitString "/" p.provider-source-address);
+        value = {
+          source = p.provider-source-address;
+          version = p.version;
+        };
+      })
+      (allPlugins pkgs.terraform.plugins)));
+  };
+
+
+  module_tf' = module_tf // {
+    inherit (depot.users.grfn.terraform) globals;
+    plugins = plugins_tf;
+  };
+
+  module = runCommandNoCC "module" { } ''
+    mkdir $out
+    ${lib.concatStrings (lib.mapAttrsToList (k: config_tf:
+      (let
+        # TODO: filterAttrsRecursive?
+        configJson = writeText "${k}.tf.json"
+          (builtins.toJSON (cleanTerraform config_tf));
+      in ''
+        ${pkgs.jq}/bin/jq . ${configJson} > $out/${lib.escapeShellArg k}.tf.json
+      ''))
+      (cleanTerraform module_tf'))}
+  '';
+
+
+  tfcmd = writeScript "${name}-tfcmd" ''
+    set -e
+    dir="''${TF_STATE_ROOT:-$HOME/tfstate}/${name}"
+    cd "$dir"
+    rm -f *.json
+    cp ${module}/*.json .
+    exec ${tf}/bin/terraform "$(basename "$0")"
+  '';
+
+  init = writeScript "${name}-init" ''
+    set -e
+    dir="''${TF_STATE_ROOT:-$HOME/tfstate}/${name}"
+    [ -d "$dir" ] || mkdir -p "$dir"
+    cd "$dir"
+    rm -f *.json
+    cp ${module}/*.json .
+    exec ${tf}/bin/terraform init
+  '';
+
+  # TODO: import (-config)
+  tfcmds = runCommandNoCC "${name}-tfcmds" { } ''
+    mkdir -p $out/bin
+    ln -s ${init} $out/bin/init
+    ln -s ${tfcmd} $out/bin/validate
+    ln -s ${tfcmd} $out/bin/plan
+    ln -s ${tfcmd} $out/bin/apply
+    ln -s ${tfcmd} $out/bin/destroy
+  '';
+
+in
+{
+  inherit name module;
+  terraform = tf;
+  cmds = tfcmds;
+
+  # run = {
+  #   init = depot.nix.nixRunWrapper "init" tfcmds;
+  #   validate = depot.nix.nixRunWrapper "validate" tfcmds;
+  #   plan = depot.nix.nixRunWrapper "plan" tfcmds;
+  #   apply = depot.nix.nixRunWrapper "apply" tfcmds;
+  #   destroy = depot.nix.nixRunWrapper "destroy" tfcmds;
+  # };
+
+  test = runCommandNoCC "${name}-test" { } ''
+    set -e
+    export TF_STATE_ROOT=$(pwd)
+    ${tfcmds}/bin/init
+    ${tfcmds}/bin/validate
+    touch $out
+  '';
+
+  meta.targets = [ "module" "test" ];
+}
diff --git a/users/grfn/wigglydonke.rs/index.html b/users/grfn/wigglydonke.rs/index.html
new file mode 100644
index 0000000000..4fd7f25fcf
--- /dev/null
+++ b/users/grfn/wigglydonke.rs/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Kids Love Wiggly Donkers!</title>
+    <style type="text/css">
+      #wiggly-donkers {
+          width: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <a href="https://tvl.fyi">
+      <img src="/wd.png" id="wiggly-donkers"/>
+    </a>
+  </body>
+</html>
diff --git a/users/grfn/wigglydonke.rs/wd.png b/users/grfn/wigglydonke.rs/wd.png
new file mode 100644
index 0000000000..217443e2df
--- /dev/null
+++ b/users/grfn/wigglydonke.rs/wd.png
Binary files differdiff --git a/users/grfn/xanthous/.envrc b/users/grfn/xanthous/.envrc
new file mode 100644
index 0000000000..be81feddb1
--- /dev/null
+++ b/users/grfn/xanthous/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
\ No newline at end of file
diff --git a/users/grfn/xanthous/.github/actions/nix-build/Dockerfile b/users/grfn/xanthous/.github/actions/nix-build/Dockerfile
new file mode 100644
index 0000000000..cfe8e35df0
--- /dev/null
+++ b/users/grfn/xanthous/.github/actions/nix-build/Dockerfile
@@ -0,0 +1,23 @@
+FROM lnl7/nix:2.1.2
+
+LABEL name="Nix Build for GitHub Actions"
+LABEL version="1.0"
+LABEL repository="http://github.com/glittershark/xanthous"
+LABEL homepage="http://github.com/glittershark/xanthous"
+LABEL maintainer="Griffin Smith <root at gws dot fyi>"
+
+LABEL "com.github.actions.name"="Nix Build"
+LABEL "com.github.actions.description"="Runs 'nix-build'"
+LABEL "com.github.actions.icon"="cpu"
+LABEL "com.github.actions.color"="purple"
+
+RUN nix-env -iA \
+  nixpkgs.gnutar nixpkgs.gzip \
+  nixpkgs.gnugrep nixpkgs.git && \
+  mkdir -p /etc/nix && \
+  (echo "binary-caches = https://cache.nixos.org/" | tee -a /etc/nix/nix.conf) && \
+  (echo "trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" | tee -a /etc/nix/nix.conf)
+
+COPY entrypoint.sh /entrypoint.sh
+ENTRYPOINT [ "/entrypoint.sh" ]
+CMD [ "--help" ]
diff --git a/users/grfn/xanthous/.github/actions/nix-build/entrypoint.sh b/users/grfn/xanthous/.github/actions/nix-build/entrypoint.sh
new file mode 100755
index 0000000000..cb7aca541a
--- /dev/null
+++ b/users/grfn/xanthous/.github/actions/nix-build/entrypoint.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+# Entrypoint that runs nix-build and, optionally, copies Docker image tarballs
+# to real files. The reason this is necessary is because once a Nix container
+# exits, you must copy out the artifacts to the working directory before exit.
+
+[ "$DEBUG" = "1" ] && set -x
+[ "$QUIET" = "1" ] && QUIET_ARG="-Q"
+
+set -e
+
+# file to build (e.g. release.nix)
+file="$1"
+
+[ "$file" = "" ] && echo "No .nix file to build specified!" && exit 1
+[ ! -e "$file" ] && echo "File $file not exist!" && exit 1
+
+echo "Building all attrs in $file..."
+nix-build --no-link ${QUIET_ARG} "$file" "${@:2}"
+
+echo "Copying build closure to $(pwd)/store..."
+mapfile -t storePaths < <(nix-build ${QUIET_ARG} --no-link "$file" | grep -v cache-deps)
+printf '%s\n' "${storePaths[@]}" > store.roots
+nix copy --to "file://$(pwd)/store" "${storePaths[@]}"
diff --git a/users/grfn/xanthous/.github/workflows/haskell.yml b/users/grfn/xanthous/.github/workflows/haskell.yml
new file mode 100644
index 0000000000..df82de3e8c
--- /dev/null
+++ b/users/grfn/xanthous/.github/workflows/haskell.yml
@@ -0,0 +1,15 @@
+name: Haskell CI
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Nix Build
+      with:
+        args: default.nix --arg failOnWarnings true
+      uses: ./.github/actions/nix-build
diff --git a/users/grfn/xanthous/.gitignore b/users/grfn/xanthous/.gitignore
new file mode 100644
index 0000000000..2ad31c01d4
--- /dev/null
+++ b/users/grfn/xanthous/.gitignore
@@ -0,0 +1,37 @@
+dist
+dist-*
+cabal-dev
+*.o
+*.hi
+*.hie
+*.chi
+*.chs.h
+*.dyn_o
+*.dyn_hi
+.hpc
+.hsenv
+.cabal-sandbox/
+cabal.sandbox.config
+*.prof
+*.aux
+*.hp
+*.eventlog
+.stack-work/
+cabal.project.local
+cabal.project.local~
+cabal.project.local~*
+.HTF/
+.ghc.environment.*
+
+
+# from nix-build
+result
+
+# grr
+*_flymake.hs
+
+# app-specific
+debug.log
+data
+*.save
+.tasty-rerun-log
diff --git a/users/grfn/xanthous/LICENSE b/users/grfn/xanthous/LICENSE
new file mode 100644
index 0000000000..45644ff764
--- /dev/null
+++ b/users/grfn/xanthous/LICENSE
@@ -0,0 +1,674 @@
+              GNU GENERAL PUBLIC LICENSE
+                Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                     Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+              END OF TERMS AND CONDITIONS
+
+     How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/users/grfn/xanthous/README.org b/users/grfn/xanthous/README.org
new file mode 100644
index 0000000000..7e1fedb069
--- /dev/null
+++ b/users/grfn/xanthous/README.org
@@ -0,0 +1,36 @@
+#+TITLE: Xanthous
+
+* Building
+
+#+BEGIN_SRC shell
+$ nix build
+#+END_SRC
+
+* Running
+
+#+BEGIN_SRC shell
+$ ./result/bin/xanthous [--help]
+#+END_SRC
+
+** Keyboard commands
+
+Keyboard commands are currently undocumented, but can be found in [[[https://github.com/glittershark/xanthous/blob/master/src/Xanthous/Command.hs#L26][this file]].
+Movement uses the nethack-esque hjklybnu.
+
+* Development
+
+Use [[https://github.com/target/lorri][lorri]], or run everything in a ~nix-shell~
+
+#+BEGIN_SRC shell
+# Build (for dev)
+$ cabal new-build
+
+# Run the game
+$ cabal new-run xanthous
+
+# Run tests
+$ cabal new-run test
+
+# Run a repl
+$ cabal new-repl
+#+END_SRC
diff --git a/users/grfn/xanthous/Setup.hs b/users/grfn/xanthous/Setup.hs
new file mode 100644
index 0000000000..9a994af677
--- /dev/null
+++ b/users/grfn/xanthous/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/users/grfn/xanthous/app/Main.hs b/users/grfn/xanthous/app/Main.hs
new file mode 100644
index 0000000000..c771a0d932
--- /dev/null
+++ b/users/grfn/xanthous/app/Main.hs
@@ -0,0 +1,171 @@
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Main ( main ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (finally)
+import           Brick
+import qualified Brick.BChan
+import qualified Graphics.Vty as Vty
+import qualified Options.Applicative as Opt
+import           System.Random
+import           Control.Monad.Random (getRandom)
+import           Control.Exception (finally)
+import           System.Exit (die)
+--------------------------------------------------------------------------------
+import qualified Xanthous.Game as Game
+import           Xanthous.Game.Env (GameEnv(..))
+import qualified Xanthous.Game.Env as Game
+import           Xanthous.App
+import           Xanthous.Generators.Level
+                 ( GeneratorInput
+                 , parseGeneratorInput
+                 , generateFromInput
+                 , showCells
+                 )
+import qualified Xanthous.Entities.Character as Character
+import           Xanthous.Generators.Level.Util (regions)
+import           Xanthous.Generators.Level.LevelContents
+import           Xanthous.Data (Dimensions, Dimensions'(Dimensions))
+import           Data.Array.IArray ( amap )
+--------------------------------------------------------------------------------
+
+parseGameConfig :: Opt.Parser Game.Config
+parseGameConfig = Game.Config
+  <$> Opt.switch
+      ( Opt.long "disable-saving"
+      <> Opt.help "Disallow saving games"
+      )
+
+data RunParams = RunParams
+  { seed :: Maybe Int
+  , characterName :: Maybe Text
+  , gameConfig :: Game.Config
+  }
+  deriving stock (Show, Eq)
+
+parseRunParams :: Opt.Parser RunParams
+parseRunParams = RunParams
+  <$> optional (Opt.option Opt.auto
+      ( Opt.long "seed"
+      <> Opt.help "Random seed for the game."
+      ))
+  <*> optional (Opt.strOption
+      ( Opt.short 'n'
+      <> Opt.long "name"
+      <> Opt.help
+        ( "Name for the character. If not set on the command line, "
+        <> "will be prompted for at runtime"
+        )
+      ))
+  <*> parseGameConfig
+
+data Command
+  = Run RunParams
+  | Load FilePath
+  | Generate GeneratorInput Dimensions (Maybe Int)
+
+parseDimensions :: Opt.Parser Dimensions
+parseDimensions = Dimensions
+  <$> Opt.option Opt.auto
+       ( Opt.short 'w'
+       <> Opt.long "width"
+       <> Opt.metavar "TILES"
+       )
+  <*> Opt.option Opt.auto
+       ( Opt.short 'h'
+       <> Opt.long "height"
+       <> Opt.metavar "TILES"
+       )
+
+
+parseCommand :: Opt.Parser Command
+parseCommand = (<|> Run <$> parseRunParams) $ Opt.subparser
+  $ Opt.command "run"
+      (Opt.info
+       (Run <$> parseRunParams)
+       (Opt.progDesc "Run the game"))
+  <> Opt.command "load"
+      (Opt.info
+       (Load <$> Opt.argument Opt.str (Opt.metavar "FILE"))
+       (Opt.progDesc "Load a saved game"))
+  <> Opt.command "generate"
+      (Opt.info
+       (Generate
+        <$> parseGeneratorInput
+        <*> parseDimensions
+        <*> optional
+            (Opt.option Opt.auto (Opt.long "seed"))
+        <**> Opt.helper
+       )
+       (Opt.progDesc "Generate a sample level"))
+
+optParser :: Opt.ParserInfo Command
+optParser = Opt.info
+  (parseCommand <**> Opt.helper)
+  (Opt.header "Xanthous: a WIP TUI RPG")
+
+thanks :: IO ()
+thanks = putStr "\n\n" >> putStrLn "Thanks for playing Xanthous!"
+
+newGame :: RunParams -> IO ()
+newGame rparams = do
+  gameSeed <- maybe getRandom pure $ seed rparams
+  when (isNothing $ seed rparams)
+    . putStrLn
+    $ "Seed: " <> tshow gameSeed
+  let initialState = Game.initialStateFromSeed gameSeed &~ do
+        for_ (characterName rparams) $ \cn ->
+          Game.character . Character.characterName ?= cn
+  runGame NewGame (gameConfig rparams) initialState `finally` do
+    thanks
+    when (isNothing $ seed rparams)
+      . putStrLn
+      $ "Seed: " <> tshow gameSeed
+    putStr "\n\n"
+
+loadGame :: FilePath -> IO ()
+loadGame saveFile = do
+  gameState <- maybe (die "Invalid save file!") pure . Game.loadGame  . fromStrict
+              =<< readFile @IO saveFile
+  gameState `deepseq` runGame (LoadGame saveFile) Game.defaultConfig gameState
+
+runGame :: RunType -> Game.Config -> Game.GameState -> IO ()
+runGame rt _config gameState = do
+  _eventChan <- Brick.BChan.newBChan 10
+  let gameEnv = GameEnv {..}
+  app <- makeApp gameEnv rt
+  let buildVty = Vty.mkVty Vty.defaultConfig
+  initialVty <- buildVty
+  _game' <- customMain
+    initialVty
+    buildVty
+    (Just _eventChan)
+    app
+    gameState
+  pure ()
+
+runGenerate :: GeneratorInput -> Dimensions -> Maybe Int -> IO ()
+runGenerate input dims mSeed = do
+  putStrLn "Generating..."
+  genSeed <- maybe getRandom pure mSeed
+  let randGen = mkStdGen genSeed
+      res = generateFromInput input dims randGen
+      rs = regions $ amap not res
+  when (isNothing mSeed)
+    . putStrLn
+    $ "Seed: " <> tshow genSeed
+  putStr "num regions: "
+  print $ length rs
+  putStr "region lengths: "
+  print $ length <$> rs
+  putStr "character position: "
+  print =<< chooseCharacterPosition res
+  putStrLn $ showCells res
+
+runCommand :: Command -> IO ()
+runCommand (Run runParams) = newGame runParams
+runCommand (Load saveFile) = loadGame saveFile
+runCommand (Generate input dims mSeed) = runGenerate input dims mSeed
+
+main :: IO ()
+main = runCommand =<< Opt.execParser optParser
diff --git a/users/grfn/xanthous/bench/Bench.hs b/users/grfn/xanthous/bench/Bench.hs
new file mode 100644
index 0000000000..5889618ee4
--- /dev/null
+++ b/users/grfn/xanthous/bench/Bench.hs
@@ -0,0 +1,12 @@
+--------------------------------------------------------------------------------
+module Main where
+--------------------------------------------------------------------------------
+import Bench.Prelude
+--------------------------------------------------------------------------------
+import qualified Xanthous.RandomBench
+import qualified Xanthous.Generators.UtilBench
+
+main :: IO ()
+main = defaultMain
+  [ Xanthous.Generators.UtilBench.benchmark
+  ]
diff --git a/users/grfn/xanthous/bench/Bench/Prelude.hs b/users/grfn/xanthous/bench/Bench/Prelude.hs
new file mode 100644
index 0000000000..c553abd6d5
--- /dev/null
+++ b/users/grfn/xanthous/bench/Bench/Prelude.hs
@@ -0,0 +1,9 @@
+--------------------------------------------------------------------------------
+module Bench.Prelude
+  ( module Xanthous.Prelude
+  , module Criterion.Main
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+import Criterion.Main
+--------------------------------------------------------------------------------
diff --git a/users/grfn/xanthous/bench/Xanthous/Generators/UtilBench.hs b/users/grfn/xanthous/bench/Xanthous/Generators/UtilBench.hs
new file mode 100644
index 0000000000..56310e691c
--- /dev/null
+++ b/users/grfn/xanthous/bench/Xanthous/Generators/UtilBench.hs
@@ -0,0 +1,37 @@
+--------------------------------------------------------------------------------
+module Xanthous.Generators.UtilBench (benchmark, main) where
+--------------------------------------------------------------------------------
+import           Bench.Prelude
+--------------------------------------------------------------------------------
+import           Data.Array.IArray
+import           Data.Array.Unboxed
+import           System.Random (getStdGen)
+--------------------------------------------------------------------------------
+import           Xanthous.Generators.Util
+import qualified Xanthous.Generators.CaveAutomata as CaveAutomata
+import           Xanthous.Data (Dimensions'(..))
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain [benchmark]
+
+--------------------------------------------------------------------------------
+
+benchmark :: Benchmark
+benchmark = bgroup "Generators.Util"
+  [ bgroup "floodFill"
+    [ env (NFWrapper <$> cells) $ \(NFWrapper ir) ->
+        bench "checkerboard" $ nf (floodFill ir) (1,0)
+    ]
+  ]
+  where
+    cells :: IO Cells
+    cells = CaveAutomata.generate
+      CaveAutomata.defaultParams
+      (Dimensions 50 50)
+      <$> getStdGen
+
+newtype NFWrapper a = NFWrapper a
+
+instance NFData (NFWrapper a) where
+  rnf (NFWrapper x) = x `seq` ()
diff --git a/users/grfn/xanthous/bench/Xanthous/RandomBench.hs b/users/grfn/xanthous/bench/Xanthous/RandomBench.hs
new file mode 100644
index 0000000000..fae4af92a7
--- /dev/null
+++ b/users/grfn/xanthous/bench/Xanthous/RandomBench.hs
@@ -0,0 +1,32 @@
+--------------------------------------------------------------------------------
+module Xanthous.RandomBench (benchmark, main) where
+--------------------------------------------------------------------------------
+import Bench.Prelude
+--------------------------------------------------------------------------------
+import Control.Parallel.Strategies
+import Control.Monad.Random
+--------------------------------------------------------------------------------
+import Xanthous.Random
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain [benchmark]
+
+--------------------------------------------------------------------------------
+
+benchmark :: Benchmark
+benchmark = bgroup "Random"
+  [ bgroup "chooseSubset"
+    [ bench "serially" $
+      nf (evalRand $ chooseSubset (0.5 :: Double) [1 :: Int ..1000000])
+         (mkStdGen 1234)
+    ]
+  , bgroup "choose weightedBy"
+    [ bench "serially" $
+      nf (evalRand
+          . choose
+          . weightedBy (\n -> product [n, pred n .. 1])
+          $ [1 :: Int ..1000000])
+         (mkStdGen 1234)
+    ]
+  ]
diff --git a/users/grfn/xanthous/build/generic-arbitrary-export-garbitrary.patch b/users/grfn/xanthous/build/generic-arbitrary-export-garbitrary.patch
new file mode 100644
index 0000000000..f0c936bfca
--- /dev/null
+++ b/users/grfn/xanthous/build/generic-arbitrary-export-garbitrary.patch
@@ -0,0 +1,12 @@
+diff --git a/src/Test/QuickCheck/Arbitrary/Generic.hs b/src/Test/QuickCheck/Arbitrary/Generic.hs
+index fed6ab3..91f59f1 100644
+--- a/src/Test/QuickCheck/Arbitrary/Generic.hs
++++ b/src/Test/QuickCheck/Arbitrary/Generic.hs
+@@ -23,6 +23,7 @@ The generated 'arbitrary' method is equivalent to
+ 
+ module Test.QuickCheck.Arbitrary.Generic
+   ( Arbitrary(..)
++  , GArbitrary
+   , genericArbitrary
+   , genericShrink
+   ) where
diff --git a/users/grfn/xanthous/build/hgeometry-fix-haddock.patch b/users/grfn/xanthous/build/hgeometry-fix-haddock.patch
new file mode 100644
index 0000000000..748c65b3e0
--- /dev/null
+++ b/users/grfn/xanthous/build/hgeometry-fix-haddock.patch
@@ -0,0 +1,13 @@
+diff --git a/src/Data/Geometry/PlanarSubdivision/Merge.hs b/src/Data/Geometry/PlanarSubdivision/Merge.hs
+index 1136114..3f4e7bb 100644
+--- a/src/Data/Geometry/PlanarSubdivision/Merge.hs
++++ b/src/Data/Geometry/PlanarSubdivision/Merge.hs
+@@ -153,7 +153,7 @@ mergeWith' mergeFaces p1 p2 = PlanarSubdivision cs vd rd rf
+         -- we have to shift the number of the *Arcs*. Since every dart
+         -- consists of two arcs, we have to shift by numDarts / 2
+         -- Furthermore, we take numFaces - 1 since we want the first
+-        -- *internal* face of p2 (the one with FaceId 1) to correspond with the first free
++        -- /internal/ face of p2 (the one with FaceId 1) to correspond with the first free
+         -- position (at index numFaces)
+
+     cs = p1^.components <> p2'^.components
diff --git a/users/grfn/xanthous/build/update-comonad-extras.patch b/users/grfn/xanthous/build/update-comonad-extras.patch
new file mode 100644
index 0000000000..cd1dbe24d3
--- /dev/null
+++ b/users/grfn/xanthous/build/update-comonad-extras.patch
@@ -0,0 +1,92 @@
+diff --git a/comonad-extras.cabal b/comonad-extras.cabal
+index fc3745a..77a2f0d 100644
+--- a/comonad-extras.cabal
++++ b/comonad-extras.cabal
+@@ -1,7 +1,7 @@
+ name:          comonad-extras
+ category:      Control, Comonads
+-version:       4.0
++version:       5.0
+ x-revision: 1
+ license:       BSD3
+ cabal-version: >= 1.6
+ license-file:  LICENSE
+@@ -34,8 +34,8 @@ library
+   build-depends:
+     array                >= 0.3   && < 0.6,
+-    base                 >= 4     && < 4.7,
+-    containers           >= 0.4   && < 0.6,
+-    comonad              >= 4     && < 5,
++    base                 >= 4     && < 5,
++    containers           >= 0.6   && < 0.7,
++    comonad              >= 5     && < 6,
+     distributive         >= 0.3.2 && < 1,
+-    semigroupoids        >= 4     && < 5,
+-    transformers         >= 0.2   && < 0.4
++    semigroupoids        >= 5     && < 6,
++    transformers         >= 0.5   && < 0.6
+
+   exposed-modules:
+     Control.Comonad.Store.Zipper
+diff --git a/src/Control/Comonad/Store/Pointer.hs b/src/Control/Comonad/Store/Pointer.hs
+index 5044a1e..8d4c62d 100644
+--- a/src/Control/Comonad/Store/Pointer.hs
++++ b/src/Control/Comonad/Store/Pointer.hs
+@@ -41,7 +41,6 @@ module Control.Comonad.Store.Pointer
+   , module Control.Comonad.Store.Class
+   ) where
+
+-import Control.Applicative
+ import Control.Comonad
+ import Control.Comonad.Hoist.Class
+ import Control.Comonad.Trans.Class
+@@ -51,27 +50,8 @@ import Control.Comonad.Env.Class
+ import Data.Functor.Identity
+ import Data.Functor.Extend
+ import Data.Array
+-
+ #ifdef __GLASGOW_HASKELL__
+ import Data.Typeable
+-instance (Typeable i, Typeable1 w) => Typeable1 (PointerT i w) where
+-  typeOf1 diwa = mkTyConApp storeTTyCon [typeOf (i diwa), typeOf1 (w diwa)]
+-    where
+-      i :: PointerT i w a -> i
+-      i = undefined
+-      w :: PointerT i w a -> w a
+-      w = undefined
+-
+-instance (Typeable i, Typeable1 w, Typeable a) => Typeable (PointerT i w a) where
+-  typeOf = typeOfDefault
+-
+-storeTTyCon :: TyCon
+-#if __GLASGOW_HASKELL__ < 704
+-storeTTyCon = mkTyCon "Control.Comonad.Trans.Store.Pointer.PointerT"
+-#else
+-storeTTyCon = mkTyCon3 "comonad-extras" "Control.Comonad.Trans.Store.Pointer" "PointerT"
+-#endif
+-{-# NOINLINE storeTTyCon #-}
+ #endif
+
+ type Pointer i = PointerT i Identity
+@@ -83,6 +63,9 @@ runPointer :: Pointer i a -> (Array i a, i)
+ runPointer (PointerT (Identity f) i) = (f, i)
+
+ data PointerT i w a = PointerT (w (Array i a)) i
++#ifdef __GLASGOW_HASKELL__
++  deriving Typeable
++#endif
+
+ runPointerT :: PointerT i w a -> (w (Array i a), i)
+ runPointerT (PointerT g i) = (g, i)
+diff --git a/src/Control/Comonad/Store/Zipper.hs b/src/Control/Comonad/Store/Zipper.hs
+index 3b70c86..decc378 100644
+--- a/src/Control/Comonad/Store/Zipper.hs
++++ b/src/Control/Comonad/Store/Zipper.hs
+@@ -15,7 +15,6 @@
+ module Control.Comonad.Store.Zipper
+   ( Zipper, zipper, zipper1, unzipper, size) where
+
+-import Control.Applicative
+ import Control.Comonad (Comonad(..))
+ import Data.Functor.Extend
+ import Data.Foldable
diff --git a/users/grfn/xanthous/default.nix b/users/grfn/xanthous/default.nix
new file mode 100644
index 0000000000..049c92fb4c
--- /dev/null
+++ b/users/grfn/xanthous/default.nix
@@ -0,0 +1,27 @@
+{ depot ? (import ../../../. { })
+, pkgs ? depot.third_party.nixpkgs
+, ...
+}:
+
+let
+  ignore = depot.third_party.gitignoreSource.gitignoreFilter ./.;
+  src = builtins.path {
+    name = "xanthous-source";
+    path = ./.;
+    filter = path: type:
+      !(type == "directory" && builtins.baseNameOf path == "server")
+      && !(type == "directory" && builtins.baseNameOf path == "docs")
+      && (ignore path type
+      || builtins.baseNameOf path == "package.yaml");
+  };
+  # generated by cabal2nix
+  basePkg = pkgs.haskell.packages.ghc8107.callPackage ./pkg.nix { };
+in
+
+pkgs.haskell.lib.overrideCabal basePkg (default: {
+  inherit src;
+  version = "canon";
+  configureFlags = [
+    "--ghc-option=-Wall --ghc-option=-Werror"
+  ] ++ (default.configureFlags or [ ]);
+})
diff --git a/users/grfn/xanthous/docs/raw-types.org b/users/grfn/xanthous/docs/raw-types.org
new file mode 100644
index 0000000000..e5bcda0426
--- /dev/null
+++ b/users/grfn/xanthous/docs/raw-types.org
@@ -0,0 +1,24 @@
+#+TITLE: Raw Types (WIP)
+
+
+* Raw Types
+** Item
+*** Attributes
+| name            | type                      | commentary                                                       |
+|-----------------+---------------------------+------------------------------------------------------------------|
+| name            | string                    |                                                                  |
+| description     | string                    | Not capitalized, should usually start with an indefinite article |
+| longDescription | string                    | Capitalized, should usually start with an indefinite article     |
+| char            | [[*EntityChar][EntityChar]]                |                                                                  |
+| wieldable       | [[*EntityWieldable][EntityWieldable]]           |                                                                  |
+| density         | number , [number, number] | Density, or range for random density, in g/m³                    |
+| volume          | number , [number, number] | Volume, or range for random volume, in m³                        |
+* Data Types
+** EntityChar
+*** Attributes
+| name  | type | commentary                                            |
+|-------+------+-------------------------------------------------------|
+| char  | char | How the entity is displayed when dropped on the floor |
+| style | Attr |                                                       |
+** TODO EntityWieldable
+** TODO Attr
diff --git a/users/grfn/xanthous/hie.yaml b/users/grfn/xanthous/hie.yaml
new file mode 100644
index 0000000000..e7cf01d158
--- /dev/null
+++ b/users/grfn/xanthous/hie.yaml
@@ -0,0 +1,10 @@
+cradle:
+  cabal:
+    - path: './src'
+      component: 'lib:xanthous'
+    - path: './test'
+      component: 'test:test'
+    - path: './app'
+      component: 'exe:xanthous'
+    - path: './bench'
+      component: 'bench:benchmark'
diff --git a/users/grfn/xanthous/nixpkgs.nix b/users/grfn/xanthous/nixpkgs.nix
new file mode 100644
index 0000000000..7d7c164405
--- /dev/null
+++ b/users/grfn/xanthous/nixpkgs.nix
@@ -0,0 +1,3 @@
+args:
+let pkgs = (import ../../../. args).third_party;
+in pkgs // { inherit pkgs; }
diff --git a/users/grfn/xanthous/package.yaml b/users/grfn/xanthous/package.yaml
new file mode 100644
index 0000000000..630dc69c11
--- /dev/null
+++ b/users/grfn/xanthous/package.yaml
@@ -0,0 +1,156 @@
+name:                xanthous
+version:             0.1.0.0
+github:              "glittershark/xanthous"
+license:             GPL-3
+author:              "Griffin Smith"
+maintainer:          "root@gws.fyi"
+copyright:           "2019 Griffin Smith"
+
+extra-source-files:
+- README.org
+
+synopsis:            A WIP TUI RPG
+category:            Game
+
+description:         Please see the README on GitHub at <https://github.com/glittershark/xanthous>
+
+dependencies:
+- base
+
+- aeson
+- array
+- async
+- QuickCheck
+- quickcheck-text
+- quickcheck-instances
+- brick
+- bifunctors
+- checkers
+- classy-prelude
+- comonad
+- comonad-extras
+- constraints
+- containers
+- criterion
+- data-default
+- data-interval
+- deepseq
+- directory
+- fgl
+- fgl-arbitrary
+- file-embed
+- filepath
+- generic-arbitrary
+- generic-lens
+- groups
+- hgeometry
+- hgeometry-combinatorial
+- JuicyPixels
+- lens
+- lifted-async
+- linear
+- megaparsec
+- mmorph
+- monad-control
+- MonadRandom
+- mtl
+- optparse-applicative
+- parallel
+- parser-combinators
+- pointed
+- random
+- random-fu
+- random-extras
+- random-source
+- raw-strings-qq
+- reflection
+- Rasterific
+- splitmix
+- streams
+- stache
+- semigroups
+- semigroupoids
+- tomland
+- transformers
+- text
+- text-zipper
+- vector
+- vty
+- witherable
+- yaml
+- zlib
+
+default-extensions:
+- BlockArguments
+- ConstraintKinds
+- DataKinds
+- DeriveAnyClass
+- DeriveGeneric
+- DerivingStrategies
+- DerivingVia
+- FlexibleContexts
+- FlexibleInstances
+- FunctionalDependencies
+- GADTSyntax
+- GeneralizedNewtypeDeriving
+- KindSignatures
+- StandaloneKindSignatures
+- LambdaCase
+- MultiWayIf
+- NoImplicitPrelude
+- NoStarIsType
+- OverloadedStrings
+- PolyKinds
+- RankNTypes
+- ScopedTypeVariables
+- TupleSections
+- TypeApplications
+- TypeFamilies
+- TypeOperators
+- ViewPatterns
+
+ghc-options:
+- -Wall
+
+library:
+  source-dirs: src
+
+executable:
+  source-dirs: app
+  main: Main.hs
+  dependencies:
+  - xanthous
+  ghc-options:
+  - -threaded
+  - -rtsopts
+  - -with-rtsopts=-N
+  - -O2
+
+tests:
+  test:
+    main:                Spec.hs
+    source-dirs:         test
+    ghc-options:
+    - -threaded
+    - -rtsopts
+    - -with-rtsopts=-N
+    - -O0
+    dependencies:
+    - xanthous
+    - tasty
+    - tasty-hunit
+    - tasty-quickcheck
+    - tasty-rerun
+    - lens-properties
+
+benchmarks:
+  benchmark:
+    main: Bench.hs
+    source-dirs: bench
+    ghc-options:
+    - -threaded
+    - -rtsopts
+    - -with-rtsopts=-N
+    dependencies:
+    - xanthous
+    - criterion
diff --git a/users/grfn/xanthous/pkg.nix b/users/grfn/xanthous/pkg.nix
new file mode 100644
index 0000000000..f8364c467a
--- /dev/null
+++ b/users/grfn/xanthous/pkg.nix
@@ -0,0 +1,349 @@
+{ mkDerivation
+, aeson
+, array
+, async
+, base
+, bifunctors
+, brick
+, checkers
+, classy-prelude
+, comonad
+, comonad-extras
+, constraints
+, containers
+, criterion
+, data-default
+, data-interval
+, deepseq
+, directory
+, fgl
+, fgl-arbitrary
+, file-embed
+, filepath
+, generic-arbitrary
+, generic-lens
+, groups
+, hgeometry
+, hgeometry-combinatorial
+, hpack
+, JuicyPixels
+, lens
+, lens-properties
+, lib
+, lifted-async
+, linear
+, megaparsec
+, mmorph
+, monad-control
+, MonadRandom
+, mtl
+, optparse-applicative
+, parallel
+, parser-combinators
+, pointed
+, QuickCheck
+, quickcheck-instances
+, quickcheck-text
+, random
+, random-extras
+, random-fu
+, random-source
+, Rasterific
+, raw-strings-qq
+, reflection
+, semigroupoids
+, semigroups
+, splitmix
+, stache
+, streams
+, tasty
+, tasty-hunit
+, tasty-quickcheck
+, tasty-rerun
+, text
+, text-zipper
+, tomland
+, transformers
+, vector
+, vty
+, witherable
+, yaml
+, zlib
+}:
+mkDerivation {
+  pname = "xanthous";
+  version = "0.1.0.0";
+  src = ./.;
+  isLibrary = true;
+  isExecutable = true;
+  libraryHaskellDepends = [
+    aeson
+    array
+    async
+    base
+    bifunctors
+    brick
+    checkers
+    classy-prelude
+    comonad
+    comonad-extras
+    constraints
+    containers
+    criterion
+    data-default
+    data-interval
+    deepseq
+    directory
+    fgl
+    fgl-arbitrary
+    file-embed
+    filepath
+    generic-arbitrary
+    generic-lens
+    groups
+    hgeometry
+    hgeometry-combinatorial
+    JuicyPixels
+    lens
+    lifted-async
+    linear
+    megaparsec
+    mmorph
+    monad-control
+    MonadRandom
+    mtl
+    optparse-applicative
+    parallel
+    parser-combinators
+    pointed
+    QuickCheck
+    quickcheck-instances
+    quickcheck-text
+    random
+    random-extras
+    random-fu
+    random-source
+    Rasterific
+    raw-strings-qq
+    reflection
+    semigroupoids
+    semigroups
+    splitmix
+    stache
+    streams
+    text
+    text-zipper
+    tomland
+    transformers
+    vector
+    vty
+    witherable
+    yaml
+    zlib
+  ];
+  libraryToolDepends = [ hpack ];
+  executableHaskellDepends = [
+    aeson
+    array
+    async
+    base
+    bifunctors
+    brick
+    checkers
+    classy-prelude
+    comonad
+    comonad-extras
+    constraints
+    containers
+    criterion
+    data-default
+    data-interval
+    deepseq
+    directory
+    fgl
+    fgl-arbitrary
+    file-embed
+    filepath
+    generic-arbitrary
+    generic-lens
+    groups
+    hgeometry
+    hgeometry-combinatorial
+    JuicyPixels
+    lens
+    lifted-async
+    linear
+    megaparsec
+    mmorph
+    monad-control
+    MonadRandom
+    mtl
+    optparse-applicative
+    parallel
+    parser-combinators
+    pointed
+    QuickCheck
+    quickcheck-instances
+    quickcheck-text
+    random
+    random-extras
+    random-fu
+    random-source
+    Rasterific
+    raw-strings-qq
+    reflection
+    semigroupoids
+    semigroups
+    splitmix
+    stache
+    streams
+    text
+    text-zipper
+    tomland
+    transformers
+    vector
+    vty
+    witherable
+    yaml
+    zlib
+  ];
+  testHaskellDepends = [
+    aeson
+    array
+    async
+    base
+    bifunctors
+    brick
+    checkers
+    classy-prelude
+    comonad
+    comonad-extras
+    constraints
+    containers
+    criterion
+    data-default
+    data-interval
+    deepseq
+    directory
+    fgl
+    fgl-arbitrary
+    file-embed
+    filepath
+    generic-arbitrary
+    generic-lens
+    groups
+    hgeometry
+    hgeometry-combinatorial
+    JuicyPixels
+    lens
+    lens-properties
+    lifted-async
+    linear
+    megaparsec
+    mmorph
+    monad-control
+    MonadRandom
+    mtl
+    optparse-applicative
+    parallel
+    parser-combinators
+    pointed
+    QuickCheck
+    quickcheck-instances
+    quickcheck-text
+    random
+    random-extras
+    random-fu
+    random-source
+    Rasterific
+    raw-strings-qq
+    reflection
+    semigroupoids
+    semigroups
+    splitmix
+    stache
+    streams
+    tasty
+    tasty-hunit
+    tasty-quickcheck
+    tasty-rerun
+    text
+    text-zipper
+    tomland
+    transformers
+    vector
+    vty
+    witherable
+    yaml
+    zlib
+  ];
+  benchmarkHaskellDepends = [
+    aeson
+    array
+    async
+    base
+    bifunctors
+    brick
+    checkers
+    classy-prelude
+    comonad
+    comonad-extras
+    constraints
+    containers
+    criterion
+    data-default
+    data-interval
+    deepseq
+    directory
+    fgl
+    fgl-arbitrary
+    file-embed
+    filepath
+    generic-arbitrary
+    generic-lens
+    groups
+    hgeometry
+    hgeometry-combinatorial
+    JuicyPixels
+    lens
+    lifted-async
+    linear
+    megaparsec
+    mmorph
+    monad-control
+    MonadRandom
+    mtl
+    optparse-applicative
+    parallel
+    parser-combinators
+    pointed
+    QuickCheck
+    quickcheck-instances
+    quickcheck-text
+    random
+    random-extras
+    random-fu
+    random-source
+    Rasterific
+    raw-strings-qq
+    reflection
+    semigroupoids
+    semigroups
+    splitmix
+    stache
+    streams
+    text
+    text-zipper
+    tomland
+    transformers
+    vector
+    vty
+    witherable
+    yaml
+    zlib
+  ];
+  prePatch = "hpack";
+  homepage = "https://github.com/glittershark/xanthous#readme";
+  description = "A WIP TUI RPG";
+  license = lib.licenses.gpl3Only;
+}
diff --git a/users/grfn/xanthous/server/.envrc b/users/grfn/xanthous/server/.envrc
new file mode 100644
index 0000000000..051d09d292
--- /dev/null
+++ b/users/grfn/xanthous/server/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
diff --git a/users/grfn/xanthous/server/.gitignore b/users/grfn/xanthous/server/.gitignore
new file mode 100644
index 0000000000..2f7896d1d1
--- /dev/null
+++ b/users/grfn/xanthous/server/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/users/grfn/xanthous/server/Cargo.lock b/users/grfn/xanthous/server/Cargo.lock
new file mode 100644
index 0000000000..e1ef44357a
--- /dev/null
+++ b/users/grfn/xanthous/server/Cargo.lock
@@ -0,0 +1,1841 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aes"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+ "ctr",
+ "opaque-debug",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atomic-shim"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64ct"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c"
+
+[[package]]
+name = "bcrypt-pbkdf"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c38c03b9506bd92bf1ef50665a81eda156f615438f7654bffba58907e6149d7"
+dependencies = [
+ "blowfish",
+ "crypto-mac",
+ "pbkdf2",
+ "sha2",
+ "zeroize",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-modes"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e"
+dependencies = [
+ "block-padding",
+ "cipher",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
+
+[[package]]
+name = "blowfish"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab"
+dependencies = [
+ "byteorder",
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "cipher"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "clap"
+version = "3.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "indexmap",
+ "lazy_static",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "color-eyre"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7"
+dependencies = [
+ "backtrace",
+ "color-spantrace",
+ "eyre",
+ "indenter",
+ "once_cell",
+ "owo-colors",
+ "tracing-error",
+]
+
+[[package]]
+name = "color-spantrace"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1"
+dependencies = [
+ "once_cell",
+ "owo-colors",
+ "tracing-core",
+ "tracing-error",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "lazy_static",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
+dependencies = [
+ "cfg-if",
+ "lazy_static",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "cryptovec"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccc7fa13a6bbb2322d325292c57f4c8e7291595506f8289968a0eb61c3130bdf"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "ctr"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "dashmap"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
+dependencies = [
+ "cfg-if",
+ "num_cpus",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dirs"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
+[[package]]
+name = "eyre"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
+dependencies = [
+ "indenter",
+ "once_cell",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "futures"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
+
+[[package]]
+name = "futures-task"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+
+[[package]]
+name = "futures-util"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gimli"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "indenter"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
+
+[[package]]
+name = "indexmap"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "js-sys"
+version = "0.3.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
+
+[[package]]
+name = "libsodium-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "walkdir",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "mach"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matchers"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metrics"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55586aa936c35f34ba8aa5d97356d554311206e1ce1f9e68fe7b07288e5ad827"
+dependencies = [
+ "ahash",
+ "metrics-macros",
+]
+
+[[package]]
+name = "metrics-exporter-prometheus"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "343a5ceb38235928e7a5687412590f07e6d281522dcd9ff51246f8856eef5fe5"
+dependencies = [
+ "hyper",
+ "ipnet",
+ "metrics",
+ "metrics-util",
+ "parking_lot",
+ "quanta",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "metrics-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0daa0ab3a0ae956d0e2c1f42511422850e577d36a255357d1a7d08d45ee3a2f1"
+dependencies = [
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn",
+]
+
+[[package]]
+name = "metrics-util"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1174223789e331d9d47a4a953dac36e397db60fa8d2a111ac505388c6c7fe32e"
+dependencies = [
+ "ahash",
+ "aho-corasick",
+ "atomic-shim",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "dashmap",
+ "hashbrown",
+ "indexmap",
+ "metrics",
+ "num_cpus",
+ "ordered-float",
+ "parking_lot",
+ "quanta",
+ "radix_trie",
+ "sketches-ddsketch",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
+dependencies = [
+ "libc",
+ "log",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys",
+]
+
+[[package]]
+name = "nibble_vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "nix"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.28.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "ordered-float"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+
+[[package]]
+name = "owo-colors"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "pbkdf2"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa"
+dependencies = [
+ "base64ct",
+ "crypto-mac",
+ "hmac",
+ "password-hash",
+ "sha2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quanta"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8"
+dependencies = [
+ "crossbeam-utils",
+ "libc",
+ "mach",
+ "once_cell",
+ "raw-cpuid",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radix_trie"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
+dependencies = [
+ "endian-type",
+ "nibble_vec",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "raw-cpuid"
+version = "10.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "738bc47119e3eeccc7e94c4a506901aea5e7b4944ecd0829cbebf4af04ceda12"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "sketches-ddsketch"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a77a8fd93886010f05e7ea0720e569d6d16c65329dbe3ec033bbbccccb017b"
+
+[[package]]
+name = "slab"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "thrussh"
+version = "0.33.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e6540238a9adf83df6e66541c182a52acf892ab335595ca965c229ade8536f8"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "cryptovec",
+ "digest",
+ "flate2",
+ "futures",
+ "generic-array",
+ "log",
+ "rand",
+ "sha2",
+ "thiserror",
+ "thrussh-keys",
+ "thrussh-libsodium",
+ "tokio",
+]
+
+[[package]]
+name = "thrussh-keys"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a72cc51a2932b18d92f7289332d8564cec4a5014063722a9d3fdca52c5d8f5ab"
+dependencies = [
+ "aes",
+ "bcrypt-pbkdf",
+ "bit-vec",
+ "block-modes",
+ "byteorder",
+ "cryptovec",
+ "data-encoding",
+ "dirs",
+ "futures",
+ "hmac",
+ "log",
+ "md5",
+ "num-bigint",
+ "num-integer",
+ "pbkdf2",
+ "rand",
+ "serde",
+ "serde_derive",
+ "sha2",
+ "thiserror",
+ "thrussh-libsodium",
+ "tokio",
+ "tokio-stream",
+ "yasna",
+]
+
+[[package]]
+name = "thrussh-libsodium"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe89c70d27b1cb92e13bc8af63493e890d0de46dae4df0e28233f62b4ed9500"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "libsodium-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "tokio"
+version = "1.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
+dependencies = [
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
+dependencies = [
+ "lazy_static",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-error"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24"
+dependencies = [
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71"
+dependencies = [
+ "ansi_term",
+ "chrono",
+ "lazy_static",
+ "matchers",
+ "regex",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
+
+[[package]]
+name = "web-sys"
+version = "0.3.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "xanthous-server"
+version = "0.1.0"
+dependencies = [
+ "base64ct",
+ "clap",
+ "color-eyre",
+ "eyre",
+ "futures",
+ "libc",
+ "metrics",
+ "metrics-exporter-prometheus",
+ "nix",
+ "pbkdf2",
+ "tempfile",
+ "thrussh",
+ "thrussh-keys",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "yasna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75"
+dependencies = [
+ "bit-vec",
+ "num-bigint",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
diff --git a/users/grfn/xanthous/server/Cargo.toml b/users/grfn/xanthous/server/Cargo.toml
new file mode 100644
index 0000000000..d4a064beb6
--- /dev/null
+++ b/users/grfn/xanthous/server/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "xanthous-server"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+clap = { version = "3.0", features = [ "derive", "env" ] }
+color-eyre = "0.5.11"
+eyre = "0.6.5"
+thrussh = "0.33.5"
+thrussh-keys = "0.21.0"
+tracing = "0.1.29"
+tracing-subscriber = "0.2.25"
+metrics = "0.17.0"
+metrics-exporter-prometheus = "0.6.1"
+futures = "0.3.17"
+libc = "0.2.103"
+nix = "0.23.0"
+
+# Pins for rust 1.55 (2018 edition) until we have 1.56 in nixpkgs-unstable
+pbkdf2 = "<0.9"
+base64ct = "<1.2"
+
+[dependencies.tokio]
+version = "1.13"
+features = ["rt", "rt-multi-thread", "macros", "net", "process", "fs", "signal"]
+
+[dev-dependencies]
+tempfile = "3.2.0"
diff --git a/users/grfn/xanthous/server/default.nix b/users/grfn/xanthous/server/default.nix
new file mode 100644
index 0000000000..fbb5ccd269
--- /dev/null
+++ b/users/grfn/xanthous/server/default.nix
@@ -0,0 +1,21 @@
+args@{ depot ? import ../../../.. { }
+, pkgs ? depot.third_party.nixpkgs
+, ...
+}:
+
+depot.third_party.naersk.buildPackage {
+  name = "xanthous-server";
+  version = "0.0.1";
+  src = depot.third_party.gitignoreSource ./.;
+
+  # Workaround for a potential Nix bug related to restricted eval.
+  # See https://github.com/nix-community/naersk/issues/169
+  root = depot.nix.sparseTree ./. [
+    ./Cargo.toml
+    ./Cargo.lock
+  ];
+
+  passthru = {
+    docker = import ./docker.nix args;
+  };
+}
diff --git a/users/grfn/xanthous/server/docker.nix b/users/grfn/xanthous/server/docker.nix
new file mode 100644
index 0000000000..09054cb00f
--- /dev/null
+++ b/users/grfn/xanthous/server/docker.nix
@@ -0,0 +1,21 @@
+{ depot ? import ../../../.. { }
+, pkgs ? depot.third_party.nixpkgs
+, ...
+}:
+
+let
+  inherit (depot.users.grfn) xanthous;
+  xanthous-server = xanthous.server;
+in
+pkgs.dockerTools.buildLayeredImage {
+  name = "xanthous-server";
+  tag = "latest";
+  contents = [ xanthous xanthous-server ];
+  config = {
+    Cmd = [
+      "${xanthous-server}/bin/xanthous-server"
+      "--xanthous-binary-path"
+      "${xanthous}/bin/xanthous"
+    ];
+  };
+}
diff --git a/users/grfn/xanthous/server/module.nix b/users/grfn/xanthous/server/module.nix
new file mode 100644
index 0000000000..82de6e38e1
--- /dev/null
+++ b/users/grfn/xanthous/server/module.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, depot, ... }:
+
+let
+  cfg = config.services.xanthous-server;
+in
+{
+  options = with lib; {
+    services.xanthous-server = {
+      enable = mkEnableOption "xanthous server";
+
+      port = mkOption {
+        type = types.int;
+        default = 2222;
+        description = "Port to listen to for SSH connections";
+      };
+
+      metricsPort = mkOption {
+        type = types.int;
+        default = 9000;
+        description = "Port to listen to for prometheus metrics";
+      };
+
+      image = mkOption {
+        type = types.package;
+        default = depot.users.grfn.xanthous.server.docker;
+        description = "OCI image file to run";
+      };
+
+      ed25519SecretKeyFile = mkOption {
+        type = with types; uniq string;
+        description = "Path to the ed25519 secret key for the server";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    virtualisation.oci-containers.containers."xanthous-server" = {
+      autoStart = true;
+      image = "${cfg.image.imageName}:${cfg.image.imageTag}";
+      imageFile = cfg.image;
+      ports = [
+        "${toString cfg.port}:22"
+        "${toString cfg.metricsPort}:9000"
+      ];
+      environment.SECRET_KEY_FILE = "/secret-key";
+      volumes = [ "/etc/secrets/xanthous-server-secret-key:/secret-key" ];
+    };
+  };
+}
diff --git a/users/grfn/xanthous/server/shell.nix b/users/grfn/xanthous/server/shell.nix
new file mode 100644
index 0000000000..e01c0316a6
--- /dev/null
+++ b/users/grfn/xanthous/server/shell.nix
@@ -0,0 +1,11 @@
+let
+  depot = import ../../../.. { };
+  pkgs = depot.third_party.nixpkgs;
+in
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    rustup
+    rust-analyzer
+  ];
+}
diff --git a/users/grfn/xanthous/server/src/main.rs b/users/grfn/xanthous/server/src/main.rs
new file mode 100644
index 0000000000..1b2c1c104b
--- /dev/null
+++ b/users/grfn/xanthous/server/src/main.rs
@@ -0,0 +1,385 @@
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::process::Command;
+use std::str;
+use std::sync::Arc;
+
+use clap::Parser;
+use color_eyre::eyre::Result;
+use eyre::{bail, Context};
+use futures::future::{ready, Ready};
+use futures::Future;
+use metrics_exporter_prometheus::PrometheusBuilder;
+use nix::pty::Winsize;
+use pty::ChildHandle;
+use thrussh::server::{self, Auth, Session};
+use thrussh::{ChannelId, CryptoVec};
+use thrussh_keys::decode_secret_key;
+use thrussh_keys::key::KeyPair;
+use tokio::fs::File;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net::TcpListener;
+use tokio::select;
+use tokio::time::Instant;
+use tracing::{debug, error, info, info_span, trace, warn, Instrument};
+use tracing_subscriber::EnvFilter;
+
+use crate::pty::WaitPid;
+
+mod metrics;
+mod pty;
+
+use crate::metrics::reported::*;
+use crate::metrics::{decrement_gauge, histogram, increment_counter, increment_gauge};
+
+/// SSH-compatible server for playing Xanthous
+#[derive(Parser, Debug)]
+struct Opts {
+    /// Address to bind to
+    #[clap(long, short = 'a', default_value = "0.0.0.0:22")]
+    address: String,
+
+    /// Address to listen to for metrics
+    #[clap(long, default_value = "0.0.0.0:9000")]
+    metrics_address: SocketAddr,
+
+    /// Format to use when emitting log events
+    #[clap(
+        long,
+        env = "LOG_FORMAT",
+        default_value = "full",
+        possible_values = &["compact", "full", "pretty", "json"]
+    )]
+    log_format: String,
+
+    /// Full path to the xanthous binary
+    #[clap(long, env = "XANTHOUS_BINARY_PATH")]
+    xanthous_binary_path: String,
+
+    /// Path to a file containing the ed25519 secret key for the server
+    #[clap(long, env = "SECRET_KEY_FILE")]
+    secret_key_file: PathBuf,
+
+    /// Level to log at
+    #[clap(long, env = "LOG_LEVEL", default_value = "info")]
+    log_level: String,
+}
+
+impl Opts {
+    async fn read_secret_key(&self) -> Result<KeyPair> {
+        let mut file = File::open(&self.secret_key_file)
+            .await
+            .context("Reading secret key file")?;
+        let mut secret_key = Vec::with_capacity(464);
+        file.read_to_end(&mut secret_key).await?;
+        Ok(decode_secret_key(str::from_utf8(&secret_key)?, None)?)
+    }
+
+    async fn ssh_server_config(&self) -> Result<server::Config> {
+        let key_pair = self.read_secret_key().await?;
+
+        Ok(server::Config {
+            server_id: "SSH-2.0-xanthous".to_owned(),
+            keys: vec![key_pair],
+            ..Default::default()
+        })
+    }
+
+    fn init_logging(&self) -> Result<()> {
+        let filter = EnvFilter::try_new(&self.log_level)?;
+        let s = tracing_subscriber::fmt().with_env_filter(filter);
+
+        match self.log_format.as_str() {
+            "compact" => s.compact().init(),
+            "full" => s.init(),
+            "pretty" => s.pretty().init(),
+            "json" => s.json().with_current_span(true).init(),
+            f => bail!("Invalid log format `{}`", f),
+        }
+
+        Ok(())
+    }
+}
+
+struct Handler {
+    address: SocketAddr,
+    xanthous_binary_path: &'static str,
+    username: Option<String>,
+    child: Option<ChildHandle>,
+}
+
+async fn run_child(
+    mut child: pty::Child,
+    mut server_handle: server::Handle,
+    channel_id: ChannelId,
+) -> Result<()> {
+    let mut buf = [0; 2048];
+    loop {
+        select! {
+            r = child.tty.read(&mut buf)  => {
+                let read_bytes = r?;
+                if read_bytes == 0 {
+                    info!("EOF received from process");
+                    let _ = server_handle.close(channel_id).await;
+                    return Ok(())
+                } else {
+                    trace!(?read_bytes, "read bytes from child");
+                    let _ = server_handle.data(channel_id, CryptoVec::from_slice(&buf[..read_bytes])).await;
+                }
+            }
+            status = WaitPid::new(child.pid) => {
+                match status {
+                    Ok(_status) => info!("Child exited"),
+                    Err(error) => error!(%error, "Child failed"),
+                }
+                let _ = server_handle.close(channel_id).await;
+                return Ok(())
+            }
+        }
+    }
+}
+
+impl Handler {
+    async fn spawn_shell(
+        &mut self,
+        mut handle: server::Handle,
+        channel_id: ChannelId,
+        term: String,
+        winsize: Winsize,
+    ) -> Result<()> {
+        let mut cmd = Command::new(self.xanthous_binary_path);
+        cmd.env("TERM", term);
+        if let Some(username) = &self.username {
+            cmd.args(["--name", username]);
+        }
+        cmd.arg("--disable-saving");
+
+        let child = pty::spawn(cmd, Some(winsize), None).await?;
+        info!(pid = %child.pid, "Spawned child");
+        increment_gauge!(RUNNING_PROCESSES, 1.0);
+        self.child = Some(child.handle().await?);
+        tokio::spawn(
+            async move {
+                let span = info_span!("child", pid = %child.pid);
+                if let Err(error) = run_child(child, handle.clone(), channel_id)
+                    .instrument(span.clone())
+                    .await
+                {
+                    span.in_scope(|| error!(%error, "Error running child"));
+                    let _ = handle.close(channel_id).await;
+                }
+                decrement_gauge!(RUNNING_PROCESSES, 1.0);
+            }
+            .in_current_span(),
+        );
+        Ok(())
+    }
+}
+
+#[allow(clippy::type_complexity)]
+impl server::Handler for Handler {
+    type Error = eyre::Error;
+    type FutureAuth = Ready<Result<(Self, Auth)>>;
+    type FutureUnit = Pin<Box<dyn Future<Output = Result<(Self, Session)>> + Send + 'static>>;
+    type FutureBool = Ready<Result<(Self, Session, bool)>>;
+
+    fn finished_auth(self, auth: Auth) -> Self::FutureAuth {
+        ready(Ok((self, auth)))
+    }
+
+    fn finished_bool(self, b: bool, session: Session) -> Self::FutureBool {
+        ready(Ok((self, session, b)))
+    }
+
+    fn finished(self, session: Session) -> Self::FutureUnit {
+        Box::pin(ready(Ok((self, session))))
+    }
+
+    fn auth_none(mut self, username: &str) -> Self::FutureAuth {
+        info!(%username, "Accepted new connection");
+        self.username = Some(username.to_owned());
+        self.finished_auth(Auth::Accept)
+    }
+
+    fn auth_password(mut self, username: &str, _password: &str) -> Self::FutureAuth {
+        info!(%username, "Accepted new connection");
+        self.username = Some(username.to_owned());
+        self.finished_auth(Auth::Accept)
+    }
+
+    fn auth_publickey(
+        mut self,
+        username: &str,
+        _: &thrussh_keys::key::PublicKey,
+    ) -> Self::FutureAuth {
+        info!(%username, "Accepted new connection");
+        self.username = Some(username.to_owned());
+        self.finished_auth(Auth::Accept)
+    }
+
+    fn pty_request(
+        mut self,
+        channel: thrussh::ChannelId,
+        term: &str,
+        col_width: u32,
+        row_height: u32,
+        pix_width: u32,
+        pix_height: u32,
+        modes: &[(thrussh::Pty, u32)],
+        session: Session,
+    ) -> Self::FutureUnit {
+        let term = term.to_owned();
+        let modes = modes.to_vec();
+        Box::pin(async move {
+            debug!(
+                %term,
+                %col_width,
+                %row_height,
+                %pix_width,
+                %pix_height,
+                ?modes,
+                "PTY Requested"
+            );
+
+            self.spawn_shell(
+                session.handle(),
+                channel,
+                term,
+                Winsize {
+                    ws_row: row_height as _,
+                    ws_col: col_width as _,
+                    ws_xpixel: pix_width as _,
+                    ws_ypixel: pix_height as _,
+                },
+            )
+            .await?;
+
+            Ok((self, session))
+        })
+    }
+
+    fn window_change_request(
+        mut self,
+        _channel: ChannelId,
+        col_width: u32,
+        row_height: u32,
+        pix_width: u32,
+        pix_height: u32,
+        session: Session,
+    ) -> Self::FutureUnit {
+        Box::pin(async move {
+            if let Some(child) = self.child.as_mut() {
+                trace!(%row_height, %col_width, "Window resize request received");
+                child
+                    .resize_window(Winsize {
+                        ws_row: row_height as _,
+                        ws_col: col_width as _,
+                        ws_xpixel: pix_width as _,
+                        ws_ypixel: pix_height as _,
+                    })
+                    .await?;
+            } else {
+                warn!("Resize request received without child process; ignoring");
+            }
+
+            Ok((self, session))
+        })
+    }
+
+    fn data(
+        mut self,
+        _channel: thrussh::ChannelId,
+        data: &[u8],
+        session: Session,
+    ) -> Self::FutureUnit {
+        trace!(data = %String::from_utf8_lossy(data), raw_data = ?data);
+        let data = data.to_owned();
+        Box::pin(async move {
+            if let Some(child) = self.child.as_mut() {
+                child.write_all(&data).await?;
+            } else {
+                warn!("Data received without child process; ignoring");
+            }
+
+            Ok((self, session))
+        })
+    }
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    color_eyre::install()?;
+    let opts = Box::leak::<'static>(Box::new(Opts::parse()));
+    opts.init_logging()?;
+    PrometheusBuilder::new()
+        .listen_address(opts.metrics_address)
+        .install()?;
+    metrics::register();
+
+    let config = Arc::new(opts.ssh_server_config().await?);
+    info!(address = %opts.address, "Listening for new SSH connections");
+    let listener = TcpListener::bind(&opts.address).await?;
+
+    loop {
+        let (stream, address) = listener.accept().await?;
+        increment_counter!(CONNECTIONS_ACCEPTED);
+        increment_gauge!(ACTIVE_CONNECTIONS, 1.0);
+        let config = config.clone();
+        let handler = Handler {
+            xanthous_binary_path: &opts.xanthous_binary_path,
+            address,
+            username: None,
+            child: None,
+        };
+        tokio::spawn(async move {
+            let span = info_span!("client", address = %handler.address);
+            let start = Instant::now();
+            if let Err(error) = server::run_stream(config, stream, handler)
+                .instrument(span.clone())
+                .await
+            {
+                span.in_scope(|| error!(%error));
+            }
+            let duration = start.elapsed();
+            span.in_scope(|| info!(duration_ms = %duration.as_millis(), "Client disconnected"));
+            histogram!(CONNECTION_DURATION, duration);
+            decrement_gauge!(ACTIVE_CONNECTIONS, 1.0);
+        });
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use tempfile::NamedTempFile;
+
+    use super::*;
+
+    #[tokio::test]
+    async fn read_secret_key() {
+        use std::io::Write;
+
+        let mut file = NamedTempFile::new().unwrap();
+        file.write_all(
+            b"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAYz80xcK7jYxZMAl6apIHKRtB0Z2U78gG39c1QaIhgMwAAAJB9vxK9fb8S
+vQAAAAtzc2gtZWQyNTUxOQAAACAYz80xcK7jYxZMAl6apIHKRtB0Z2U78gG39c1QaIhgMw
+AAAEDNZ0d3lLNBGU6Im4JOpr490TOjm+cB7kMVXjVg3iCowBjPzTFwruNjFkwCXpqkgcpG
+0HRnZTvyAbf1zVBoiGAzAAAACHRlc3Qta2V5AQIDBAU=
+-----END OPENSSH PRIVATE KEY-----
+",
+        )
+        .unwrap();
+
+        let opts: Opts = Opts::parse_from(&[
+            "xanthous-server".as_ref(),
+            "--xanthous-binary-path".as_ref(),
+            "/bin/xanthous".as_ref(),
+            "--secret-key-file".as_ref(),
+            file.path().as_os_str(),
+        ]);
+        opts.read_secret_key().await.unwrap();
+    }
+}
diff --git a/users/grfn/xanthous/server/src/metrics.rs b/users/grfn/xanthous/server/src/metrics.rs
new file mode 100644
index 0000000000..6912cdd9c9
--- /dev/null
+++ b/users/grfn/xanthous/server/src/metrics.rs
@@ -0,0 +1,24 @@
+pub use ::metrics::*;
+
+pub mod reported {
+    /// Counter: Connections accepted on the TCP listener
+    pub const CONNECTIONS_ACCEPTED: &str = "ssh.connections.accepted";
+
+    /// Histogram: Connection duration
+    pub const CONNECTION_DURATION: &str = "ssh.connections.duration";
+
+    /// Gauge: Currently active connections
+    pub const ACTIVE_CONNECTIONS: &str = "ssh.connections.active";
+
+    /// Gauge: Currently running xanthous processes
+    pub const RUNNING_PROCESSES: &str = "ssh.child.processes";
+}
+
+pub fn register() {
+    use reported::*;
+
+    register_counter!(CONNECTIONS_ACCEPTED);
+    register_histogram!(CONNECTION_DURATION);
+    register_gauge!(ACTIVE_CONNECTIONS);
+    register_gauge!(RUNNING_PROCESSES);
+}
diff --git a/users/grfn/xanthous/server/src/pty.rs b/users/grfn/xanthous/server/src/pty.rs
new file mode 100644
index 0000000000..234ecd8f23
--- /dev/null
+++ b/users/grfn/xanthous/server/src/pty.rs
@@ -0,0 +1,172 @@
+use std::io::{self};
+use std::os::unix::prelude::{AsRawFd, CommandExt, FromRawFd};
+use std::pin::Pin;
+use std::process::{abort, Command};
+use std::task::{Context, Poll};
+
+use eyre::{bail, Result};
+use futures::Future;
+use nix::pty::{forkpty, Winsize};
+use nix::sys::termios::Termios;
+use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
+use nix::unistd::{ForkResult, Pid};
+use tokio::fs::File;
+use tokio::io::{AsyncRead, AsyncWrite};
+use tokio::signal::unix::{signal, Signal, SignalKind};
+use tokio::task::spawn_blocking;
+
+mod ioctl {
+    use super::Winsize;
+    use libc::TIOCSWINSZ;
+    use nix::ioctl_write_ptr_bad;
+
+    ioctl_write_ptr_bad!(tiocswinsz, TIOCSWINSZ, Winsize);
+}
+
+async fn asyncify<F, T>(f: F) -> Result<T>
+where
+    F: FnOnce() -> Result<T> + Send + 'static,
+    T: Send + 'static,
+{
+    match spawn_blocking(f).await {
+        Ok(res) => res,
+        Err(_) => bail!("background task failed",),
+    }
+}
+
+pub struct Child {
+    pub tty: File,
+    pub pid: Pid,
+}
+
+pub struct ChildHandle {
+    pub tty: File,
+}
+
+pub struct WaitPid {
+    pid: Pid,
+    signal: Signal,
+}
+
+impl WaitPid {
+    pub fn new(pid: Pid) -> Self {
+        Self {
+            pid,
+            signal: signal(SignalKind::child()).unwrap(),
+        }
+    }
+}
+
+impl Future for WaitPid {
+    type Output = nix::Result<WaitStatus>;
+
+    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let _ = self.signal.poll_recv(cx);
+        match waitpid(self.pid, Some(WaitPidFlag::WNOHANG)) {
+            Ok(WaitStatus::StillAlive) => Poll::Pending,
+            result => Poll::Ready(result),
+        }
+    }
+}
+
+impl Child {
+    pub async fn handle(&self) -> io::Result<ChildHandle> {
+        Ok(ChildHandle {
+            tty: self.tty.try_clone().await?,
+        })
+    }
+}
+
+impl ChildHandle {
+    pub async fn resize_window(&mut self, winsize: Winsize) -> Result<()> {
+        let fd = self.tty.as_raw_fd();
+        asyncify(move || unsafe {
+            ioctl::tiocswinsz(fd, &winsize as *const Winsize)?;
+            Ok(())
+        })
+        .await
+    }
+}
+
+pub async fn spawn(
+    mut cmd: Command,
+    winsize: Option<Winsize>,
+    termios: Option<Termios>,
+) -> Result<Child> {
+    asyncify(move || unsafe {
+        let res = forkpty(winsize.as_ref(), termios.as_ref())?;
+        match res.fork_result {
+            ForkResult::Parent { child } => Ok(Child {
+                pid: child,
+                tty: File::from_raw_fd(res.master),
+            }),
+            ForkResult::Child => {
+                cmd.exec();
+                abort();
+            }
+        }
+    })
+    .await
+}
+
+impl AsyncRead for Child {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &mut tokio::io::ReadBuf<'_>,
+    ) -> Poll<io::Result<()>> {
+        Pin::new(&mut self.tty).poll_read(cx, buf)
+    }
+}
+
+impl AsyncWrite for Child {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<Result<usize, io::Error>> {
+        Pin::new(&mut self.tty).poll_write(cx, buf)
+    }
+
+    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
+        Pin::new(&mut self.tty).poll_flush(cx)
+    }
+
+    fn poll_shutdown(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+    ) -> Poll<Result<(), io::Error>> {
+        Pin::new(&mut self.tty).poll_shutdown(cx)
+    }
+}
+
+impl AsyncRead for ChildHandle {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &mut tokio::io::ReadBuf<'_>,
+    ) -> Poll<io::Result<()>> {
+        Pin::new(&mut self.tty).poll_read(cx, buf)
+    }
+}
+
+impl AsyncWrite for ChildHandle {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<Result<usize, io::Error>> {
+        Pin::new(&mut self.tty).poll_write(cx, buf)
+    }
+
+    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
+        Pin::new(&mut self.tty).poll_flush(cx)
+    }
+
+    fn poll_shutdown(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+    ) -> Poll<Result<(), io::Error>> {
+        Pin::new(&mut self.tty).poll_shutdown(cx)
+    }
+}
diff --git a/users/grfn/xanthous/shell.nix b/users/grfn/xanthous/shell.nix
new file mode 100644
index 0000000000..2c41cb4aa8
--- /dev/null
+++ b/users/grfn/xanthous/shell.nix
@@ -0,0 +1,23 @@
+let
+  depot = import ../../../. { };
+  inherit (depot) third_party;
+  pkgs = third_party.nixpkgs;
+in
+
+(pkgs.haskell.packages.ghc8107.extend (pkgs.haskell.lib.packageSourceOverrides {
+  xanthous = third_party.gitignoreSource ./.;
+})).shellFor {
+  packages = p: [ p.xanthous ];
+  withHoogle = true;
+  doBenchmark = true;
+  buildInputs = (with pkgs.haskell.packages.ghc8107; [
+    cabal-install
+    ghc-prof-flamegraph
+    hp2pretty
+    hlint
+    haskell-language-server
+    cabal2nix
+  ]) ++ (with pkgs; [
+    qpdf
+  ]);
+}
diff --git a/users/grfn/xanthous/src/Data/Aeson/Generic/DerivingVia.hs b/users/grfn/xanthous/src/Data/Aeson/Generic/DerivingVia.hs
new file mode 100644
index 0000000000..e89fcd6211
--- /dev/null
+++ b/users/grfn/xanthous/src/Data/Aeson/Generic/DerivingVia.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE ConstraintKinds, DataKinds, DeriveGeneric, DerivingVia    #-}
+{-# LANGUAGE ExplicitNamespaces, FlexibleContexts, FlexibleInstances   #-}
+{-# LANGUAGE GADTs, GeneralizedNewtypeDeriving, MultiParamTypeClasses  #-}
+{-# LANGUAGE PolyKinds, ScopedTypeVariables, StandaloneDeriving        #-}
+{-# LANGUAGE TypeApplications, TypeFamilies, TypeInType, TypeOperators #-}
+{-# LANGUAGE UndecidableInstances                                      #-}
+{-# OPTIONS_GHC -Wall #-}
+-- | https://gist.github.com/konn/27c00f784dd883ec2b90eab8bc84a81d
+module Data.Aeson.Generic.DerivingVia
+     ( StrFun(..), Setting(..), SumEncoding'(..), DefaultOptions, WithOptions(..)
+     , -- Utility type synonyms to save ticks (') before promoted data constructors
+       type Drop, type CamelTo2, type UserDefined
+     , type TaggedObj, type UntaggedVal, type ObjWithSingleField, type TwoElemArr
+     , type FieldLabelModifier
+     , type ConstructorTagModifier
+     , type AllNullaryToStringTag
+     , type OmitNothingFields
+     , type SumEnc
+     , type UnwrapUnaryRecords
+     , type TagSingleConstructors
+     )
+  where
+
+import           Prelude
+import           Data.Aeson      (FromJSON (..), GFromJSON, GToJSON,
+                                  ToJSON (..))
+import           Data.Aeson      (Options (..), Zero, camelTo2,
+                                  genericParseJSON)
+import           Data.Aeson      (defaultOptions, genericToJSON)
+import qualified Data.Aeson      as Aeson
+import           Data.Kind       (Constraint, Type)
+import           Data.Proxy      (Proxy (..))
+import           Data.Reflection (Reifies (..))
+import           GHC.Generics    (Generic, Rep)
+import           GHC.TypeLits    (KnownNat, KnownSymbol, natVal, symbolVal)
+import           GHC.TypeLits    (Nat, Symbol)
+
+newtype WithOptions options a = WithOptions { runWithOptions :: a }
+
+data StrFun = Drop     Nat
+            | CamelTo2 Symbol
+            | forall p. UserDefined p
+
+type Drop = 'Drop
+type CamelTo2 = 'CamelTo2
+type UserDefined = 'UserDefined
+
+type family Demoted a where
+  Demoted Symbol  = String
+  Demoted StrFun  = String -> String
+  Demoted [a]     = [Demoted a]
+  Demoted Setting = Options -> Options
+  Demoted SumEncoding' = Aeson.SumEncoding
+  Demoted a = a
+
+data SumEncoding' = TaggedObj {tagFieldName' :: Symbol, contentsFieldName :: Symbol }
+                  | UntaggedVal
+                  | ObjWithSingleField
+                  | TwoElemArr
+
+type TaggedObj          = 'TaggedObj
+type UntaggedVal        = 'UntaggedVal
+type ObjWithSingleField = 'ObjWithSingleField
+type TwoElemArr         = 'TwoElemArr
+
+data Setting = FieldLabelModifier     [StrFun]
+             | ConstructorTagModifier [StrFun]
+             | AllNullaryToStringTag  Bool
+             | OmitNothingFields      Bool
+             | SumEnc                 SumEncoding'
+             | UnwrapUnaryRecords     Bool
+             | TagSingleConstructors  Bool
+
+type FieldLabelModifier     = 'FieldLabelModifier
+type ConstructorTagModifier = 'ConstructorTagModifier
+-- | If 'True' the constructors of a datatype, with all nullary constructors,
+-- will be encoded to just a string with the constructor tag. If 'False' the
+-- encoding will always follow the 'SumEncoding'.
+type AllNullaryToStringTag  = 'AllNullaryToStringTag
+type OmitNothingFields      = 'OmitNothingFields
+type SumEnc                 = 'SumEnc
+-- | Hide the field name when a record constructor has only one field, like a
+-- newtype.
+type UnwrapUnaryRecords     = 'UnwrapUnaryRecords
+-- | Encode types with a single constructor as sums, so that
+-- 'AllNullaryToStringTag' and 'SumEncoding' apply.
+type TagSingleConstructors  = 'TagSingleConstructors
+
+class Demotable (a :: k) where
+  demote :: proxy a -> Demoted k
+
+type All :: (Type -> Constraint) -> [Type] -> Constraint
+type family All p xs where
+  All p '[] = ()
+  All p (x ': xs) = (p x, All p xs)
+
+instance Reifies f (String -> String) => Demotable ('UserDefined f) where
+  demote _ = reflect @f Proxy
+
+instance KnownSymbol sym => Demotable sym where
+  demote = symbolVal
+
+instance (KnownSymbol s, KnownSymbol t) => Demotable ('TaggedObj s t) where
+  demote _ = Aeson.TaggedObject (symbolVal @s Proxy) (symbolVal @t Proxy)
+
+instance Demotable 'UntaggedVal where
+  demote _ = Aeson.UntaggedValue
+
+instance Demotable 'ObjWithSingleField where
+  demote _ = Aeson.ObjectWithSingleField
+
+instance Demotable 'TwoElemArr where
+  demote _ = Aeson.TwoElemArray
+
+instance Demotable xs => Demotable ('FieldLabelModifier xs) where
+  demote _ o = o { fieldLabelModifier = foldr (.) id (demote (Proxy @xs)) }
+
+instance Demotable xs => Demotable ('ConstructorTagModifier xs) where
+  demote _ o = o { constructorTagModifier = foldr (.) id (demote (Proxy @xs)) }
+
+instance Demotable b => Demotable ('AllNullaryToStringTag b) where
+  demote _ o = o { allNullaryToStringTag = demote (Proxy @b) }
+
+instance Demotable b => Demotable ('OmitNothingFields b) where
+  demote _ o = o { omitNothingFields = demote (Proxy @b) }
+
+instance Demotable b => Demotable ('UnwrapUnaryRecords b) where
+  demote _ o = o { unwrapUnaryRecords = demote (Proxy @b) }
+
+instance Demotable b => Demotable ('TagSingleConstructors b) where
+  demote _ o = o { tagSingleConstructors = demote (Proxy @b) }
+
+instance Demotable b => Demotable ('SumEnc b) where
+  demote _ o = o { sumEncoding = demote (Proxy @b) }
+
+instance Demotable 'True where
+  demote _ = True
+
+instance Demotable 'False where
+  demote _ = False
+
+instance KnownNat n => Demotable ('Drop n) where
+  demote _ = drop (fromIntegral $ natVal (Proxy :: Proxy n))
+
+instance KnownSymbol sym => Demotable ('CamelTo2 sym) where
+  demote _ = camelTo2 $ head $ symbolVal @sym Proxy
+
+instance {-# OVERLAPPING #-} Demotable ('[] :: [k]) where
+  demote _ = []
+
+instance (Demotable (x :: k), Demotable (xs :: [k])) => Demotable (x ': xs) where
+  demote _ = demote (Proxy @x) : demote (Proxy @xs)
+
+type DefaultOptions = ('[] :: [Setting])
+
+reflectOptions :: forall xs proxy. Demotable (xs :: [Setting]) => proxy xs -> Options
+reflectOptions pxy = foldr (.) id (demote pxy) defaultOptions
+
+instance (Demotable (options :: [Setting])) => Reifies options Options where
+  reflect = reflectOptions
+
+instance (Generic a, GToJSON Zero (Rep a), Reifies (options :: k) Options)
+       => ToJSON (WithOptions options a) where
+  toJSON = genericToJSON (reflect (Proxy @options)) . runWithOptions
+
+instance (Generic a, GFromJSON Zero (Rep a), Reifies (options :: k) Options)
+       => FromJSON (WithOptions options a) where
+  parseJSON = fmap WithOptions . genericParseJSON (reflect (Proxy @options))
diff --git a/users/grfn/xanthous/src/Xanthous/AI/Gormlak.hs b/users/grfn/xanthous/src/Xanthous/AI/Gormlak.hs
new file mode 100644
index 0000000000..1f2b513ffe
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/AI/Gormlak.hs
@@ -0,0 +1,201 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE UndecidableInstances #-}
+--------------------------------------------------------------------------------
+module Xanthous.AI.Gormlak
+  ( HasVisionRadius(..)
+  , GormlakBrain(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (lines)
+--------------------------------------------------------------------------------
+import           Control.Monad.State
+import           Control.Monad.Random
+import           Data.Aeson (object)
+import qualified Data.Aeson as A
+import           Data.Generics.Product.Fields
+--------------------------------------------------------------------------------
+import           Xanthous.Data
+                 ( Positioned(..), positioned, position, _Position
+                 , diffPositions, stepTowards, isUnit
+                 , Ticks, (|*|), invertedRate
+                 )
+import           Xanthous.Data.EntityMap
+import           Xanthous.Entities.Creature.Hippocampus
+import           Xanthous.Entities.Character (Character)
+import qualified Xanthous.Entities.Character as Character
+import qualified Xanthous.Entities.RawTypes as Raw
+import           Xanthous.Entities.RawTypes
+                 ( CreatureType, HasLanguage(language), getLanguage
+                 , HasAttacks (attacks), creatureAttackMessage
+                 )
+import           Xanthous.Entities.Common
+                 ( wielded, Inventory, wieldedItems, WieldedItem (WieldedItem) )
+import           Xanthous.Game.State
+import           Xanthous.Game.Lenses
+                 ( entitiesCollision, collisionAt
+                 , character, characterPosition, positionIsCharacterVisible
+                 , hearingRadius
+                 )
+import           Xanthous.Data.EntityMap.Graphics (linesOfSight, canSee)
+import           Xanthous.Random
+import           Xanthous.Monad (say, message)
+import           Xanthous.Generators.Speech (word)
+import qualified Linear.Metric as Metric
+import qualified Xanthous.Messages as Messages
+--------------------------------------------------------------------------------
+
+--  TODO move the following two classes to a more central location
+
+class HasVisionRadius a where visionRadius :: a -> Word
+
+type IsCreature entity =
+  ( HasVisionRadius entity
+  , HasField "_hippocampus" entity entity Hippocampus Hippocampus
+  , HasField "_creatureType" entity entity CreatureType CreatureType
+  , HasField "_inventory" entity entity Inventory Inventory
+  , A.ToJSON entity
+  )
+
+--------------------------------------------------------------------------------
+
+stepGormlak
+  :: forall entity m.
+    ( MonadState GameState m, MonadRandom m
+    , IsCreature entity
+    )
+  => Ticks
+  -> Positioned entity
+  -> m (Positioned entity)
+stepGormlak ticks pe@(Positioned pos creature) = do
+  canSeeCharacter <- uses entities $ canSee (entityIs @Character) pos vision
+
+  let selectDestination pos' creature' = destinationFromPos <$> do
+        if canSeeCharacter
+          then do
+            charPos <- use characterPosition
+            if isUnit (pos' `diffPositions` charPos)
+              then attackCharacter $> pos'
+              else pure $ pos' `stepTowards` charPos
+        else do
+          lines <- map (takeWhile (isNothing . entitiesCollision . map snd . snd)
+                      -- the first item on these lines is always the creature itself
+                      . fromMaybe mempty . tailMay)
+                  . linesOfSight pos' (visionRadius creature')
+                  <$> use entities
+          line <- choose $ weightedBy length lines
+          pure $ fromMaybe pos' $ fmap fst . headMay =<< line
+
+  pe' <- if canSeeCharacter && not (creature ^. creatureGreeted)
+        then yellAtCharacter $> (pe & positioned . creatureGreeted .~ True)
+        else pure pe
+
+  dest <- maybe (selectDestination pos creature) pure
+         . mfilter (\(Destination p _) -> p /= pos)
+         $ creature ^. hippocampus . destination
+  let progress' =
+        dest ^. destinationProgress
+        + creature ^. creatureType . Raw.speed . invertedRate |*| ticks
+  if progress' < 1
+    then pure
+         $ pe'
+         & positioned . hippocampus . destination
+         ?~ (dest & destinationProgress .~ progress')
+    else do
+      let newPos = dest ^. destinationPosition
+          remainingSpeed = progress' - 1
+      newDest <- selectDestination newPos creature
+                <&> destinationProgress +~ remainingSpeed
+      let pe'' = pe' & positioned . hippocampus . destination ?~ newDest
+      collisionAt newPos >>= \case
+        Nothing -> pure $ pe'' & position .~ newPos
+        Just Stop -> pure pe''
+        Just Combat -> do
+          ents <- use $ entities . atPosition newPos
+          when (any (entityIs @Character) ents) attackCharacter
+          pure pe'
+  where
+    vision = visionRadius creature
+    attackCharacter = do
+      dmg <- case creature ^? inventory . wielded . wieldedItems of
+        Just (WieldedItem item wi) -> do
+          let msg = fromMaybe
+                    (Messages.lookup ["combat", "creatureAttack", "genericWeapon"])
+                    $ wi ^. creatureAttackMessage
+          message msg $ object [ "creature" A..= creature
+                               , "item" A..= item
+                               ]
+          pure $ wi ^. Raw.damage
+        Nothing -> do
+          attack <- choose $ creature ^. creatureType . attacks
+          attackDescription <- Messages.render (attack ^. Raw.description)
+                              $ object []
+          say ["combat", "creatureAttack", "natural"]
+              $ object [ "creature" A..= creature
+                       , "attackDescription" A..= attackDescription
+                       ]
+          pure $ attack ^. Raw.damage
+
+      character %= Character.damage dmg
+
+    yellAtCharacter = for_ (creature ^. creatureType . language)
+      $ \lang -> do
+          utterance <- fmap (<> "!") . word $ getLanguage lang
+          creatureSaysText pe utterance
+
+    creatureGreeted :: Lens' entity Bool
+    creatureGreeted = hippocampus . greetedCharacter
+
+
+-- | A creature sends some text
+--
+-- If that creature is visible to the character, its description will be
+-- included, otherwise if it's within earshot the character will just hear the
+-- sound
+creatureSaysText
+  :: (MonadState GameState m, MonadRandom m, IsCreature entity)
+  => Positioned entity
+  -> Text
+  -> m ()
+creatureSaysText ent txt = do
+  let entPos = ent ^. position . _Position . to (fmap fromIntegral)
+  charPos <- use $ characterPosition . _Position . to (fmap fromIntegral)
+  let dist :: Int
+      dist = round $ Metric.distance @_ @Double entPos charPos
+      audible = dist <= fromIntegral hearingRadius
+  when audible $ do
+    visible <- positionIsCharacterVisible $ ent ^. position
+    let path = ["entities", "say", "creature"]
+               <> [if visible then "visible" else "invisible"]
+        params = object [ "creature" A..= (ent ^. positioned)
+                        , "message" A..= txt
+                        ]
+    say path params
+
+newtype GormlakBrain entity = GormlakBrain { _unGormlakBrain :: entity }
+
+instance (IsCreature entity) => Brain (GormlakBrain entity) where
+  step ticks
+    = fmap (fmap GormlakBrain)
+    . stepGormlak ticks
+    . fmap _unGormlakBrain
+  entityCanMove = const True
+
+hippocampus :: HasField "_hippocampus" s t a b => Lens s t a b
+hippocampus = field @"_hippocampus"
+
+creatureType :: HasField "_creatureType" s t a b => Lens s t a b
+creatureType = field @"_creatureType"
+
+inventory :: HasField "_inventory" s t a b => Lens s t a b
+inventory = field @"_inventory"
+
+--------------------------------------------------------------------------------
+
+-- instance Brain Creature where
+--   step = brainVia GormlakBrain
+--   entityCanMove = const True
+
+-- instance Entity Creature where
+--   blocksVision _ = False
+--   description = view $ Creature.creatureType . Raw.description
+--   entityChar = view $ Creature.creatureType . char
diff --git a/users/grfn/xanthous/src/Xanthous/App.hs b/users/grfn/xanthous/src/Xanthous/App.hs
new file mode 100644
index 0000000000..426230cdc2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/App.hs
@@ -0,0 +1,647 @@
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE RecordWildCards      #-}
+--------------------------------------------------------------------------------
+{-# OPTIONS_GHC -Wno-deferred-type-errors #-}
+module Xanthous.App
+  ( makeApp
+  , RunType(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Brick hiding (App, halt, continue, raw)
+import qualified Brick
+import           Graphics.Vty.Attributes (defAttr)
+import           Graphics.Vty.Input.Events (Event(EvKey))
+import           Control.Monad.State (get, gets)
+import           Control.Monad.State.Class (modify)
+import           Data.Aeson (object, ToJSON)
+import qualified Data.Aeson as A
+import qualified Data.Vector as V
+import           System.Exit
+import           System.Directory (doesFileExist)
+import           Data.List.NonEmpty (NonEmpty(..))
+import           Data.Vector.Lens (toVectorOf)
+--------------------------------------------------------------------------------
+import           Xanthous.App.Common
+import           Xanthous.App.Time
+import           Xanthous.App.Prompt
+import           Xanthous.App.Autocommands
+import           Xanthous.Command
+import           Xanthous.Data
+                 ( move
+                 , Dimensions'(Dimensions)
+                 , positioned
+                 , position
+                 , Position
+                 , (|*|)
+                 , Tiles(..), Hitpoints, fromScalar
+                 )
+import           Xanthous.Data.App (ResourceName, Panel(..), AppEvent(..))
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Data.Levels (prevLevel, nextLevel)
+import qualified Xanthous.Data.Levels as Levels
+import           Xanthous.Data.Entities (blocksObject)
+import           Xanthous.Game
+import           Xanthous.Game.State
+import           Xanthous.Game.Env
+import           Xanthous.Game.Draw (drawGame)
+import           Xanthous.Game.Prompt hiding (Fire)
+import qualified Xanthous.Messages as Messages
+import           Xanthous.Random
+import           Xanthous.Util (removeVectorIndex, useListOf)
+import           Xanthous.Util.Inflection (toSentence)
+import           Xanthous.Physics (throwDistance, bluntThrowDamage)
+import           Xanthous.Data.EntityMap.Graphics (lineOfSight)
+import           Xanthous.Data.EntityMap (EntityID)
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.Common
+                 ( InventoryPosition, describeInventoryPosition, backpack
+                 , wieldableItem, wieldedItems, wielded, itemsWithPosition
+                 , removeItemFromPosition, asWieldedItem
+                 , wieldedItem, items, Hand (..), describeHand, wieldInHand
+                 , WieldedItem, Wielded (..)
+                 )
+import qualified Xanthous.Entities.Character as Character
+import           Xanthous.Entities.Character hiding (pickUpItem)
+import           Xanthous.Entities.Item (Item, weight)
+import qualified Xanthous.Entities.Item as Item
+import           Xanthous.Entities.Creature (Creature)
+import qualified Xanthous.Entities.Creature as Creature
+import           Xanthous.Entities.Environment
+                 (Door, open, closed, locked, GroundMessage(..), Staircase(..))
+import           Xanthous.Entities.RawTypes
+                 ( edible, eatMessage, hitpointsHealed
+                 , attackMessage
+                 )
+import           Xanthous.Generators.Level
+import qualified Xanthous.Generators.Level.CaveAutomata as CaveAutomata
+import qualified Xanthous.Generators.Level.Dungeon as Dungeon
+--------------------------------------------------------------------------------
+
+type App = Brick.App GameState AppEvent ResourceName
+
+data RunType = NewGame | LoadGame FilePath
+  deriving stock (Eq)
+
+makeApp :: GameEnv -> RunType -> IO App
+makeApp env rt = pure $ Brick.App
+  { appDraw = drawGame
+  , appChooseCursor = const headMay
+  , appHandleEvent = \game event -> runAppM (handleEvent event) env game
+  , appStartEvent = case rt of
+      NewGame -> runAppM (startEvent >> get) env
+      LoadGame save -> pure . (savefile ?~ save)
+  , appAttrMap = const $ attrMap defAttr []
+  }
+
+runAppM :: AppM a -> GameEnv -> GameState -> EventM ResourceName a
+runAppM appm ge = fmap fst . runAppT appm ge
+
+startEvent :: AppM ()
+startEvent = do
+  initLevel
+  modify updateCharacterVision
+  use (character . characterName) >>= \case
+    Nothing -> prompt_ @'StringPrompt ["character", "namePrompt"] Uncancellable
+      $ \(StringResult s) -> do
+        character . characterName ?= s
+        say ["welcome"] =<< use character
+    Just n -> say ["welcome"] $ object [ "characterName" A..= n ]
+
+initLevel :: AppM ()
+initLevel = do
+  level <- genLevel 0
+  entities <>= levelToEntityMap level
+  characterPosition .= level ^. levelCharacterPosition
+
+--------------------------------------------------------------------------------
+
+handleEvent :: BrickEvent ResourceName AppEvent -> AppM (Next GameState)
+handleEvent ev = use promptState >>= \case
+  NoPrompt -> handleNoPromptEvent ev
+  WaitingPrompt msg pr -> handlePromptEvent msg pr ev
+
+
+handleNoPromptEvent :: BrickEvent ResourceName AppEvent -> AppM (Next GameState)
+handleNoPromptEvent (VtyEvent (EvKey k mods))
+  | Just command <- commandFromKey k mods
+  = do messageHistory %= nextTurn
+       cancelAutocommand
+       handleCommand command
+handleNoPromptEvent (AppEvent AutoContinue) = do
+  preuse (autocommand . _ActiveAutocommand . _1) >>= traverse_ autoStep
+  continue
+handleNoPromptEvent _ = continue
+
+handleCommand :: Command -> AppM (Next GameState)
+handleCommand Quit = confirm_ ["quit", "confirm"] (liftIO exitSuccess) >> continue
+
+handleCommand Help = showPanel HelpPanel >> continue
+
+handleCommand (Move dir) = do
+  newPos <- uses characterPosition $ move dir
+  collisionAt newPos >>= \case
+    Nothing -> do
+      characterPosition .= newPos
+      stepGameBy =<< uses (character . speed) (|*| Tiles 1)
+      describeEntitiesAt newPos
+    Just Combat -> attackAt newPos
+    Just Stop -> pure ()
+  continue
+
+handleCommand PickUp = do
+  pos <- use characterPosition
+  uses entities (entitiesAtPositionWithType @Item pos) >>= \case
+    [] -> say_ ["pickUp", "nothingToPickUp"]
+    [item] -> pickUpItem item
+    items' ->
+      menu_ ["pickUp", "menu"] Cancellable (entityMenu_ items')
+      $ \(MenuResult item) -> pickUpItem item
+  continue
+  where
+    pickUpItem (itemID, item) = do
+      character %= Character.pickUpItem item
+      entities . at itemID .= Nothing
+      say ["pickUp", "pickUp"] $ object [ "item" A..= item ]
+      stepGameBy 100 -- TODO
+
+handleCommand Drop = do
+  takeItemFromInventory_ ["drop", "menu"] Cancellable id
+    (say_ ["drop", "nothing"])
+    $ \(MenuResult item) -> do
+      entitiesAtCharacter %= (SomeEntity item <|)
+      say ["drop", "dropped"] $ object [ "item" A..= item ]
+  continue
+
+handleCommand PreviousMessage = do
+  messageHistory %= previousMessage
+  continue
+
+handleCommand Open = do
+  prompt_ @'DirectionPrompt ["open", "prompt"] Cancellable
+    $ \(DirectionResult dir) -> do
+      pos <- move dir <$> use characterPosition
+      doors <- uses entities $ entitiesAtPositionWithType @Door pos
+      if | null doors -> say_ ["open", "nothingToOpen"]
+         | any (view $ _2 . locked) doors -> say_ ["open", "locked"]
+         | all (view $ _2 . open) doors   -> say_ ["open", "alreadyOpen"]
+         | otherwise -> do
+             for_ doors $ \(eid, _) ->
+               entities . ix eid . positioned . _SomeEntity . open .= True
+             say_ ["open", "success"]
+      pure ()
+  stepGame -- TODO
+  continue
+
+handleCommand Close = do
+  prompt_ @'DirectionPrompt ["close", "prompt"] Cancellable
+    $ \(DirectionResult dir) -> do
+      pos <- move dir <$> use characterPosition
+      (nonDoors, doors) <- uses entities
+        $ partitionEithers
+        . toList
+        . map ( (matching . aside $ _SomeEntity @Door)
+              . over _2 (view positioned)
+              )
+        . EntityMap.atPositionWithIDs pos
+      if | null doors -> say_ ["close", "nothingToClose"]
+         | all (view $ _2 . closed) doors -> say_ ["close", "alreadyClosed"]
+         | any (view blocksObject . entityAttributes . snd) nonDoors ->
+           say ["close", "blocked"]
+           $ object [ "entityDescriptions"
+                      A..= ( toSentence
+                           . map description
+                           . filter (view blocksObject . entityAttributes)
+                           . map snd
+                           ) nonDoors
+                    , "blockOrBlocks"
+                      A..= ( if length nonDoors == 1
+                             then "blocks"
+                             else "block"
+                           :: Text)
+                    ]
+         | otherwise -> do
+             for_ doors $ \(eid, _) ->
+               entities . ix eid . positioned . _SomeEntity . closed .= True
+             for_ nonDoors $ \(eid, _) ->
+               entities . ix eid . position %= move dir
+             say_ ["close", "success"]
+      pure ()
+  stepGame -- TODO
+  continue
+
+handleCommand Look = do
+  prompt_ @'PointOnMap ["look", "prompt"] Cancellable
+    $ \(PointOnMapResult pos) -> revealedEntitiesAtPosition pos >>= \case
+        Empty -> say_ ["look", "nothing"]
+        ents -> describeEntities ents
+  continue
+
+handleCommand Wait = stepGame >> continue
+
+handleCommand Eat = do
+  uses (character . inventory . backpack)
+       (V.mapMaybe (\item -> (item,) <$> item ^. Item.itemType . edible))
+    >>= \case
+      Empty -> say_ ["eat", "noFood"]
+      food ->
+        let foodMenuItem idx (item, edibleItem)
+              = ( item ^. Item.itemType . char . char
+                , MenuOption (description item) (idx, item, edibleItem))
+                -- TODO refactor to use entityMenu_
+            menuItems = mkMenuItems $ imap foodMenuItem food
+        in menu_ ["eat", "menuPrompt"] Cancellable menuItems
+          $ \(MenuResult (idx, item, edibleItem)) -> do
+            character . inventory . backpack %= removeVectorIndex idx
+            let msg = fromMaybe (Messages.lookup ["eat", "eat"])
+                      $ edibleItem ^. eatMessage
+            character . characterHitpoints' +=
+              edibleItem ^. hitpointsHealed . to fromIntegral
+            message msg $ object ["item" A..= item]
+            stepGame -- TODO
+  continue
+
+handleCommand Read = do
+  -- TODO allow reading things in the inventory (combo direction+menu prompt?)
+  prompt_ @'DirectionPrompt ["read", "prompt"] Cancellable
+    $ \(DirectionResult dir) -> do
+      pos <- uses characterPosition $ move dir
+      uses entities
+        (fmap snd . entitiesAtPositionWithType @GroundMessage pos) >>= \case
+          Empty -> say_ ["read", "nothing"]
+          GroundMessage msg :< Empty ->
+            say ["read", "result"] $ object ["message" A..= msg]
+          msgs ->
+            let readAndContinue Empty = pure ()
+                readAndContinue (msg :< msgs') =
+                  prompt @'Continue
+                    ["read", "result"]
+                    (object ["message" A..= msg])
+                    Cancellable
+                  . const
+                  $ readAndContinue msgs'
+                readAndContinue _ = error "this is total"
+            in readAndContinue msgs
+  continue
+
+handleCommand ShowInventory = showPanel InventoryPanel >> continue
+
+handleCommand DescribeInventory = do
+  selectItemFromInventory_ ["inventory", "describe", "select"] Cancellable id
+    (say_ ["inventory", "describe", "nothing"])
+    $ \(MenuResult (invPos, item)) -> showPanel . ItemDescriptionPanel
+        $ Item.fullDescription item
+        <> "\n\n" <> describeInventoryPosition invPos
+  continue
+
+
+handleCommand Wield = do
+  hs <- use $ character . inventory . wielded
+  selectItem $ \(MenuResult (invPos, (item :: WieldedItem))) -> do
+    selectHand hs $ \(MenuResult hand) -> do
+      character . inventory
+        %= removeItemFromPosition invPos (asWieldedItem # item)
+      prevItems <- character . inventory . wielded %%= wieldInHand hand item
+      character . inventory . backpack
+        <>= fromList (map (view wieldedItem) prevItems)
+      say ["wield", "wielded"] $ object [ "item" A..= item
+                                        , "hand" A..= describeHand hand
+                                        ]
+  continue
+  where
+    selectItem =
+      selectItemFromInventory_ ["wield", "menu"] Cancellable asWieldedItem
+        (say_ ["wield", "nothing"])
+    selectHand hs = menu_ ["wield", "hand"] Cancellable $ handsMenu hs
+    itemsInHand (Hands i _) LeftHand       = toList i
+    itemsInHand (DoubleHanded _) LeftHand  = []
+    itemsInHand (Hands _ i) RightHand      = toList i
+    itemsInHand (DoubleHanded _) RightHand = []
+    itemsInHand (Hands l r) BothHands      = toList l <> toList r
+    itemsInHand (DoubleHanded i) BothHands = [i]
+    describeItems [] = ""
+    describeItems is
+      = " (currently holding "
+      <> (intercalate " and" $ map (view $ wieldedItem . to description) is)
+      <> ")"
+    handsMenu hs = mapFromList
+      . map (second $ \hand ->
+                MenuOption
+                ( describeHand hand
+                <> describeItems (itemsInHand hs hand)
+                )
+                hand
+            )
+      $ [ ('l', LeftHand)
+        , ('r', RightHand)
+        , ('b', BothHands)
+        ]
+
+handleCommand Fire = do
+  selectItemFromInventory_ ["fire", "menu"] Cancellable id
+    (say_ ["fire", "nothing"])
+    $ \(MenuResult (invPos, item)) ->
+      let wt = weight item
+          dist = throwDistance wt
+          dam = bluntThrowDamage wt
+      in if dist < fromScalar 1
+         then say_ ["fire", "zeroRange"]
+         else firePrompt_ ["fire", "target"] Cancellable dist $
+          \(FireResult targetPos) -> do
+              charPos <- use characterPosition
+              mTarget <- uses entities $ firstEnemy . lineOfSight charPos targetPos
+              case mTarget of
+                Just target -> do
+                  creature' <- damageCreature target dam
+                  unless (Creature.isDead creature') $
+                    let msgPath = ["fire", "fired"] <> [if dam == 0
+                                                        then "noDamage"
+                                                        else "someDamage"]
+                    in say msgPath $ object [ "item" A..= item
+                                            , "creature" A..= creature'
+                                            ]
+                Nothing ->
+                  say ["fire", "fired", "noTarget"] $ object [ "item" A..= item ]
+              character . inventory %= removeItemFromPosition invPos item
+              entities . EntityMap.atPosition targetPos %= (SomeEntity item <|)
+              stepGame -- TODO(grfn): should this be based on distance?
+  continue
+  where
+    firstEnemy
+      :: [(Position, Vector (EntityID, SomeEntity))]
+      -> Maybe (EntityID, Creature)
+    firstEnemy los =
+      let enemies = los >>= \(_, es) -> toList $ headMay es
+      in enemies ^? folded . below _SomeEntity
+
+handleCommand Save =
+  view (config . disableSaving) >>= \case
+    True -> say_ ["save", "disabled"] >> continue
+    False -> do
+      -- TODO default save locations / config file?
+      use savefile >>= \case
+        Just filepath ->
+          stringPromptWithDefault_
+            ["save", "location"]
+            Cancellable
+            (pack filepath)
+            promptCallback
+        Nothing -> prompt_ @'StringPrompt ["save", "location"] Cancellable promptCallback
+      continue
+      where
+        promptCallback :: PromptResult 'StringPrompt -> AppM ()
+        promptCallback (StringResult filename) = do
+          sf <- use savefile
+          exists <- liftIO . doesFileExist $ unpack filename
+          if exists && sf /= Just (unpack filename)
+          then confirm ["save", "overwrite"] (object ["filename" A..= filename])
+              $ doSave filename
+          else doSave filename
+        doSave filename = do
+          src <- gets saveGame
+          lift . liftIO $ do
+            writeFile (unpack filename) $ toStrict src
+            exitSuccess
+
+handleCommand GoUp = do
+  hasStairs <- uses entitiesAtCharacter $ elem (SomeEntity UpStaircase)
+  if hasStairs
+  then uses levels prevLevel >>= \case
+    Just levs' -> do
+      cEID <- use characterEntityID
+      pCharacter <- entities . at cEID <<.= Nothing
+      levels .= levs'
+      charPos <- use characterPosition
+      entities . at cEID .= pCharacter
+      characterPosition .= charPos
+    Nothing ->
+      -- TODO in nethack, this leaves the game. Maybe something similar here?
+      say_ ["cant", "goUp"]
+  else say_ ["cant", "goUp"]
+
+  continue
+
+handleCommand GoDown = do
+  hasStairs <- uses entitiesAtCharacter $ elem (SomeEntity DownStaircase)
+
+  if hasStairs
+  then do
+    levs <- use levels
+    let newLevelNum = Levels.pos levs + 1
+    levs' <- nextLevel (levelToGameLevel <$> genLevel newLevelNum) levs
+    cEID <- use characterEntityID
+    pCharacter <- entities . at cEID <<.= Nothing
+    levels .= levs'
+    entities . at cEID .= pCharacter
+    characterPosition .= extract levs' ^. upStaircasePosition
+  else say_ ["cant", "goDown"]
+
+  continue
+
+handleCommand (StartAutoMove dir) = do
+  runAutocommand $ AutoMove dir
+  continue
+
+handleCommand Rest = do
+  say_ ["autocommands", "resting"]
+  runAutocommand AutoRest
+  continue
+
+--
+
+handleCommand ToggleRevealAll = do
+  val <- debugState . allRevealed <%= not
+  say ["debug", "toggleRevealAll"] $ object [ "revealAll" A..= val ]
+  continue
+
+--------------------------------------------------------------------------------
+attackAt :: Position -> AppM ()
+attackAt pos =
+  uses entities (entitiesAtPositionWithType @Creature pos) >>= \case
+    Empty               -> say_ ["combat", "nothingToAttack"]
+    (creature :< Empty) -> attackCreature creature
+    creatures ->
+      menu_ ["combat", "menu"] Cancellable (entityMenu_ creatures)
+      $ \(MenuResult creature) -> attackCreature creature
+ where
+  attackCreature creature = do
+    charDamage <- uses character characterDamage
+    creature' <- damageCreature creature charDamage
+    unless (Creature.isDead creature') $ writeAttackMessage creature'
+    whenM (uses character $ isNothing . weapon) handleFists
+    stepGame
+  weapon chr = chr ^? inventory . wielded . wieldedItems . wieldableItem
+  writeAttackMessage creature = do
+    let params = object ["creature" A..= creature]
+    attackMessages <- uses character getAttackMessages
+    msg <- intercalate " and " <$> for attackMessages (`Messages.render` params)
+    writeMessage $ "You " <> msg
+  getAttackMessages chr =
+    case chr ^.. inventory . wielded . wieldedItems . wieldableItem of
+      [] -> [Messages.lookup ["combat", "hit", "fists"]]
+      is ->
+        is
+        <&> \wi ->
+              fromMaybe (Messages.lookup ["combat", "hit", "generic"])
+              $ wi ^. attackMessage
+
+
+  handleFists = do
+    damageChance <- use $ character . body . knuckles . to fistDamageChance
+    whenM (chance damageChance) $ do
+      damageAmount <- use $ character . body . knuckles . to fistfightingDamage
+      say_ [ "combat" , if damageAmount > 1
+                        then "fistExtraSelfDamage"
+                        else "fistSelfDamage" ]
+      character %= Character.damage damageAmount
+      character . body . knuckles %= damageKnuckles
+
+damageCreature :: (EntityID, Creature) -> Hitpoints -> AppM Creature
+damageCreature (creatureID, creature) dam = do
+  let creature' = Creature.damage dam creature
+      msgParams = object ["creature" A..= creature']
+  if Creature.isDead creature'
+    then do
+      say ["combat", "killed"] msgParams
+      floorItems <- useListOf
+                   $ entities
+                   . ix creatureID
+                   . positioned
+                   . _SomeEntity @Creature
+                   . inventory
+                   . items
+      mCreaturePos <- preuse $ entities . ix creatureID . position
+      entities . at creatureID .= Nothing
+      for_ mCreaturePos $ \creaturePos ->
+        entities . EntityMap.atPosition creaturePos
+          %= (<> fromList (SomeEntity <$> floorItems))
+    else entities . ix creatureID . positioned .= SomeEntity creature'
+  pure creature'
+
+
+entityMenu_
+  :: (Comonad w, Entity entity)
+  => [w entity]
+  -> Map Char (MenuOption (w entity))
+entityMenu_ = mkMenuItems @[_] . map entityMenuItem
+  where
+    entityMenuItem wentity
+      = let entity = extract wentity
+      in (entityMenuChar entity, MenuOption (description entity) wentity)
+
+
+entityMenuChar :: Entity a => a -> Char
+entityMenuChar entity
+  = let ec = entityChar entity ^. char
+    in if ec `elem` (['a'..'z'] ++ ['A'..'Z'])
+        then ec
+        else 'a'
+
+-- | Prompt with an item to select out of the inventory and call callback with
+-- it
+selectItemFromInventory
+  :: forall item params.
+    (ToJSON params)
+  => [Text]            -- ^ Menu message
+  -> params            -- ^ Menu message params
+  -> PromptCancellable -- ^ Is the menu cancellable?
+  -> Prism' Item item  -- ^ Attach some extra information to the item, in a
+                      --   recoverable fashion. Prism vs iso so we can discard
+                      --   items.
+  -> AppM ()            -- ^ Action to take if there are no items matching
+  -> (PromptResult ('Menu (InventoryPosition, item)) -> AppM ())
+  -> AppM ()
+selectItemFromInventory msgPath msgParams cancellable extraInfo onEmpty cb = do
+  uses (character . inventory)
+       (V.mapMaybe (_2 $ preview extraInfo) . toVectorOf itemsWithPosition)
+    >>= \case
+      Empty -> onEmpty
+      items' -> menu msgPath msgParams cancellable (itemMenu items') cb
+  where
+    itemMenu = mkMenuItems . map itemMenuItem
+    itemMenuItem (invPos, extraInfoItem) =
+      let item = extraInfo # extraInfoItem
+      in ( entityMenuChar item
+         , MenuOption
+           (description item <> " (" <> describeInventoryPosition invPos <> ")")
+           (invPos, extraInfoItem)
+         )
+
+-- | Prompt with an item to select out of the inventory and call callback with
+-- it
+selectItemFromInventory_
+  :: forall item.
+    [Text]            -- ^ Menu message
+  -> PromptCancellable -- ^ Is the menu cancellable?
+  -> Prism' Item item  -- ^ Attach some extra information to the item, in a
+                      --   recoverable fashion. Prism vs iso so we can discard
+                      --   items.
+  -> AppM ()            -- ^ Action to take if there are no items matching
+  -> (PromptResult ('Menu (InventoryPosition, item)) -> AppM ())
+  -> AppM ()
+selectItemFromInventory_ msgPath = selectItemFromInventory msgPath ()
+
+-- | Prompt with an item to select out of the inventory, remove it from the
+-- inventory, and call callback with it
+takeItemFromInventory
+  :: forall item params.
+    (ToJSON params)
+  => [Text]            -- ^ Menu message
+  -> params            -- ^ Menu message params
+  -> PromptCancellable -- ^ Is the menu cancellable?
+  -> Prism' Item item  -- ^ Attach some extra information to the item, in a
+                      --   recoverable fashion. Prism vs iso so we can discard
+                      --   items.
+  -> AppM ()            -- ^ Action to take if there are no items matching
+  -> (PromptResult ('Menu item) -> AppM ())
+  -> AppM ()
+takeItemFromInventory msgPath msgParams cancellable extraInfo onEmpty cb =
+  selectItemFromInventory msgPath msgParams cancellable extraInfo onEmpty
+    $ \(MenuResult (invPos, item)) -> do
+      character . inventory
+        %= removeItemFromPosition invPos (item ^. re extraInfo)
+      cb $ MenuResult item
+
+takeItemFromInventory_
+  :: forall item.
+    [Text]            -- ^ Menu message
+  -> PromptCancellable -- ^ Is the menu cancellable?
+  -> Prism' Item item  -- ^ Attach some extra information to the item, in a
+                      --   recoverable fashion. Prism vs iso so we can discard
+                      --   items.
+  -> AppM ()            -- ^ Action to take if there are no items matching
+  -> (PromptResult ('Menu item) -> AppM ())
+  -> AppM ()
+takeItemFromInventory_ msgPath = takeItemFromInventory msgPath ()
+
+-- entityMenu :: Entity entity => [entity] -> Map Char (MenuOption entity)
+-- entityMenu = map (map runIdentity) . entityMenu_ . fmap Identity
+
+showPanel :: Panel -> AppM ()
+showPanel panel = do
+  activePanel ?= panel
+  prompt_ @'Continue ["generic", "continue"] Uncancellable
+    . const
+    $ activePanel .= Nothing
+
+--------------------------------------------------------------------------------
+
+genLevel
+  :: Word -- ^ Level number, starting at 0
+  -> AppM Level
+genLevel num = do
+  let dims = Dimensions 80 80
+  generator <- choose $ CaveAutomata :| [Dungeon]
+  let
+    doGen = case generator of
+      CaveAutomata -> generateLevel SCaveAutomata CaveAutomata.defaultParams
+      Dungeon -> generateLevel SDungeon Dungeon.defaultParams
+  level <- doGen dims num
+  pure $!! level
+
+levelToGameLevel :: Level -> GameLevel
+levelToGameLevel level =
+  let _levelEntities = levelToEntityMap level
+      _upStaircasePosition = level ^. levelCharacterPosition
+      _levelRevealedPositions = mempty
+  in GameLevel {..}
diff --git a/users/grfn/xanthous/src/Xanthous/App/Autocommands.hs b/users/grfn/xanthous/src/Xanthous/App/Autocommands.hs
new file mode 100644
index 0000000000..5d4db1a474
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/App/Autocommands.hs
@@ -0,0 +1,76 @@
+--------------------------------------------------------------------------------
+module Xanthous.App.Autocommands
+  ( runAutocommand
+  , autoStep
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Control.Concurrent (threadDelay)
+import qualified Data.Aeson as A
+import           Data.Aeson (object)
+import           Data.List.NonEmpty (nonEmpty)
+import qualified Data.List.NonEmpty as NE
+import           Control.Monad.State (gets)
+--------------------------------------------------------------------------------
+import           Xanthous.App.Common
+import           Xanthous.App.Time
+import           Xanthous.Data
+import           Xanthous.Data.App
+import           Xanthous.Entities.Character (speed, isFullyHealed)
+import           Xanthous.Entities.Creature (Creature, creatureType)
+import           Xanthous.Entities.RawTypes (hostile)
+import           Xanthous.Game.State
+--------------------------------------------------------------------------------
+
+-- | Step the given autocommand forward once
+autoStep :: Autocommand -> AppM ()
+autoStep (AutoMove dir) = do
+  newPos <- uses characterPosition $ move dir
+  collisionAt newPos >>= \case
+    Nothing -> do
+      characterPosition .= newPos
+      stepGameBy =<< uses (character . speed) (|*| (1 :: Tiles))
+      describeEntitiesAt newPos
+      cancelIfDanger
+    Just _ -> cancelAutocommand
+
+autoStep AutoRest = do
+  done <- uses character isFullyHealed
+  if done
+    then say_ ["autocommands", "doneResting"] >> cancelAutocommand
+    else stepGame >> cancelIfDanger
+
+-- | Cancel the autocommand if the character is in danger
+cancelIfDanger :: AppM ()
+cancelIfDanger = do
+  maybeVisibleEnemies <- nonEmpty <$> enemiesInSight
+  for_ maybeVisibleEnemies $ \visibleEnemies -> do
+    say ["autocommands", "enemyInSight"]
+      $ object [ "firstEntity" A..= NE.head visibleEnemies ]
+    cancelAutocommand
+  where
+    enemiesInSight :: AppM [Creature]
+    enemiesInSight = do
+      ents <- gets characterVisibleEntities
+      pure $ ents
+          ^.. folded
+            . _SomeEntity @Creature
+            . filtered (view $ creatureType . hostile)
+
+--------------------------------------------------------------------------------
+
+autocommandIntervalμs :: Int
+autocommandIntervalμs = 1000 * 50 -- 50 ms
+
+runAutocommand :: Autocommand -> AppM ()
+runAutocommand ac = do
+  env <- ask
+  tid <- liftIO . async $ runReaderT go env
+  autocommand .= ActiveAutocommand ac tid
+  where
+    go = everyμs autocommandIntervalμs $ sendEvent AutoContinue
+
+-- | Perform 'act' every μs microseconds forever
+everyμs :: MonadIO m => Int -> m () -> m ()
+everyμs μs act = act >> liftIO (threadDelay μs) >> everyμs μs act
diff --git a/users/grfn/xanthous/src/Xanthous/App/Common.hs b/users/grfn/xanthous/src/Xanthous/App/Common.hs
new file mode 100644
index 0000000000..69ba6f0e05
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/App/Common.hs
@@ -0,0 +1,67 @@
+--------------------------------------------------------------------------------
+module Xanthous.App.Common
+  ( describeEntities
+  , describeEntitiesAt
+  , entitiesAtPositionWithType
+
+    -- * Re-exports
+  , MonadState
+  , MonadRandom
+  , EntityMap
+  , module Xanthous.Game.Lenses
+  , module Xanthous.Monad
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.Aeson (object)
+import qualified Data.Aeson as A
+import           Control.Monad.State (MonadState)
+import           Control.Monad.Random (MonadRandom)
+--------------------------------------------------------------------------------
+import           Xanthous.Data (Position, positioned)
+import           Xanthous.Data.EntityMap (EntityMap)
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Game
+import           Xanthous.Game.Lenses
+import           Xanthous.Game.State
+import           Xanthous.Monad
+import           Xanthous.Entities.Character (Character)
+import           Xanthous.Util.Inflection (toSentence)
+--------------------------------------------------------------------------------
+
+entitiesAtPositionWithType
+  :: forall a. (Entity a, Typeable a)
+  => Position
+  -> EntityMap SomeEntity
+  -> [(EntityMap.EntityID, a)]
+entitiesAtPositionWithType pos em =
+  let someEnts = EntityMap.atPositionWithIDs pos em
+  in flip foldMap someEnts $ \(eid, view positioned -> se) ->
+    case downcastEntity @a se of
+      Just e  -> [(eid, e)]
+      Nothing -> []
+
+describeEntitiesAt :: (MonadState GameState m, MonadRandom m) => Position -> m ()
+describeEntitiesAt pos =
+  use ( entities
+      . EntityMap.atPosition pos
+      . to (filter (not . entityIs @Character))
+      ) >>= \case
+        Empty -> pure ()
+        ents  -> describeEntities ents
+
+describeEntities
+  :: ( Entity entity
+    , MonadRandom m
+    , MonadState GameState m
+    , MonoFoldable (f Text)
+    , Functor f
+    , Element (f Text) ~ Text
+    )
+  => f entity
+  -> m ()
+describeEntities ents =
+  let descriptions = description <$> ents
+  in say ["entities", "description"]
+     $ object ["entityDescriptions" A..= toSentence descriptions]
diff --git a/users/grfn/xanthous/src/Xanthous/App/Prompt.hs b/users/grfn/xanthous/src/Xanthous/App/Prompt.hs
new file mode 100644
index 0000000000..799281a1c2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/App/Prompt.hs
@@ -0,0 +1,228 @@
+{-# LANGUAGE UndecidableInstances #-}
+--------------------------------------------------------------------------------
+module Xanthous.App.Prompt
+  ( handlePromptEvent
+  , clearPrompt
+  , prompt
+  , prompt_
+  , stringPromptWithDefault
+  , stringPromptWithDefault_
+  , confirm_
+  , confirm
+  , menu
+  , menu_
+  , firePrompt_
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Brick (BrickEvent(..), Next)
+import           Brick.Widgets.Edit (handleEditorEvent)
+import           Data.Aeson (ToJSON, object)
+import           Graphics.Vty.Input.Events (Event(EvKey), Key(..))
+--------------------------------------------------------------------------------
+import           Xanthous.App.Common
+import           Xanthous.Data (move, Tiles, Position, positioned, _Position)
+import qualified Xanthous.Data as Data
+import           Xanthous.Command (directionFromChar)
+import           Xanthous.Data.App (ResourceName, AppEvent)
+import           Xanthous.Game.Prompt
+import           Xanthous.Game.State
+import qualified Xanthous.Messages as Messages
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Entities.Creature (creatureType, Creature)
+import           Xanthous.Entities.RawTypes (hostile)
+import qualified Linear.Metric as Metric
+--------------------------------------------------------------------------------
+
+handlePromptEvent
+  :: Text -- ^ Prompt message
+  -> Prompt AppM
+  -> BrickEvent ResourceName AppEvent
+  -> AppM (Next GameState)
+
+handlePromptEvent _ (Prompt Cancellable _ _ _ _) (VtyEvent (EvKey KEsc []))
+  = clearPrompt >> continue
+handlePromptEvent _ pr (VtyEvent (EvKey KEnter []))
+  = clearPrompt >> submitPrompt pr >> continue
+
+handlePromptEvent _ pr@(Prompt _ SConfirm _ _ _) (VtyEvent (EvKey (KChar 'y') []))
+  = clearPrompt >> submitPrompt pr >> continue
+
+handlePromptEvent _ (Prompt _ SConfirm _ _ _) (VtyEvent (EvKey (KChar 'n') []))
+  = clearPrompt >> continue
+
+handlePromptEvent
+  msg
+  (Prompt c SStringPrompt (StringPromptState edit) pri cb)
+  (VtyEvent ev)
+  = do
+    edit' <- lift $ handleEditorEvent ev edit
+    let prompt' = Prompt c SStringPrompt (StringPromptState edit') pri cb
+    promptState .= WaitingPrompt msg prompt'
+    continue
+
+handlePromptEvent _ (Prompt _ SDirectionPrompt _ _ cb)
+  (VtyEvent (EvKey (KChar (directionFromChar -> Just dir)) []))
+  = clearPrompt >> cb (DirectionResult dir) >> continue
+handlePromptEvent _ (Prompt _ SDirectionPrompt _ _ _) _ = continue
+
+handlePromptEvent _ (Prompt _ SMenu _ items' cb) (VtyEvent (EvKey (KChar chr) []))
+  | Just (MenuOption _ res) <- items' ^. at chr
+  = clearPrompt >> cb (MenuResult res) >> continue
+  | otherwise
+  = continue
+
+handlePromptEvent
+  msg
+  (Prompt c SPointOnMap (PointOnMapPromptState pos) pri cb)
+  (VtyEvent (EvKey (KChar (directionFromChar -> Just dir)) []))
+  = let pos' = move dir pos
+        prompt' = Prompt c SPointOnMap (PointOnMapPromptState pos') pri cb
+    in promptState .= WaitingPrompt msg prompt'
+       >> continue
+handlePromptEvent _ (Prompt _ SPointOnMap _ _ _) _ = continue
+
+handlePromptEvent
+  msg
+  (Prompt c SFire (FirePromptState pos) pri@(origin, range) cb)
+  (VtyEvent (EvKey (KChar (directionFromChar -> Just dir)) []))
+  = do
+  let pos' = move dir pos
+      prompt' = Prompt c SFire (FirePromptState pos') pri cb
+  when (Data.distance origin pos' <= range) $
+    promptState .= WaitingPrompt msg prompt'
+  continue
+
+handlePromptEvent
+  _
+  (Prompt Cancellable _ _ _ _)
+  (VtyEvent (EvKey (KChar 'q') []))
+  = clearPrompt >> continue
+handlePromptEvent _ _ _ = continue
+
+clearPrompt :: AppM ()
+clearPrompt = promptState .= NoPrompt
+
+type PromptParams :: PromptType -> Type
+type family PromptParams pt where
+  PromptParams ('Menu a) = Map Char (MenuOption a) -- Menu items
+  PromptParams 'Fire     = Tiles -- Range
+  PromptParams _         = ()
+
+prompt
+  :: forall (pt :: PromptType) (params :: Type).
+    (ToJSON params, SingPromptType pt, PromptParams pt ~ ())
+  => [Text]                     -- ^ Message key
+  -> params                     -- ^ Message params
+  -> PromptCancellable
+  -> (PromptResult pt -> AppM ()) -- ^ Prompt promise handler
+  -> AppM ()
+prompt msgPath params cancellable cb = do
+  let pt = singPromptType @pt
+  msg <- Messages.message msgPath params
+  mp :: Maybe (Prompt AppM) <- case pt of
+    SPointOnMap -> do
+      charPos <- use characterPosition
+      pure . Just $ mkPointOnMapPrompt cancellable charPos cb
+    SStringPrompt -> pure . Just $ mkStringPrompt cancellable cb
+    SConfirm -> pure . Just $ mkPrompt cancellable pt cb
+    SDirectionPrompt -> pure . Just $ mkPrompt cancellable pt cb
+    SContinue -> pure . Just $ mkPrompt cancellable pt cb
+  for_ mp $ \p -> promptState .= WaitingPrompt msg p
+
+prompt_
+  :: forall (pt :: PromptType).
+    (SingPromptType pt, PromptParams pt ~ ())
+  => [Text] -- ^ Message key
+  -> PromptCancellable
+  -> (PromptResult pt -> AppM ()) -- ^ Prompt promise handler
+  -> AppM ()
+prompt_ msg = prompt msg $ object []
+
+stringPromptWithDefault
+  :: forall (params :: Type). (ToJSON params)
+  => [Text]                                -- ^ Message key
+  -> params                                -- ^ Message params
+  -> PromptCancellable
+  -> Text                                  -- ^ Prompt default
+  -> (PromptResult 'StringPrompt -> AppM ()) -- ^ Prompt promise handler
+  -> AppM ()
+stringPromptWithDefault msgPath params cancellable def cb = do
+  msg <- Messages.message msgPath params
+  let p = mkStringPromptWithDefault cancellable def cb
+  promptState .= WaitingPrompt msg p
+
+stringPromptWithDefault_
+  :: [Text]                                -- ^ Message key
+  -> PromptCancellable
+  -> Text                                  -- ^ Prompt default
+  -> (PromptResult 'StringPrompt -> AppM ()) -- ^ Prompt promise handler
+  -> AppM ()
+stringPromptWithDefault_ msg = stringPromptWithDefault msg $ object []
+
+confirm
+  :: ToJSON params
+  => [Text] -- ^ Message key
+  -> params
+  -> AppM ()
+  -> AppM ()
+confirm msgPath params
+  = prompt @'Confirm msgPath params Cancellable . const
+
+confirm_ :: [Text] -> AppM () -> AppM ()
+confirm_ msgPath = confirm msgPath $ object []
+
+menu :: forall (a :: Type) (params :: Type).
+       (ToJSON params)
+     => [Text]                            -- ^ Message key
+     -> params                            -- ^ Message params
+     -> PromptCancellable
+     -> Map Char (MenuOption a)           -- ^ Menu items
+     -> (PromptResult ('Menu a) -> AppM ()) -- ^ Menu promise handler
+     -> AppM ()
+menu msgPath params cancellable items' cb = do
+  msg <- Messages.message msgPath params
+  let p = mkMenu cancellable items' cb
+  promptState .= WaitingPrompt msg p
+
+menu_ :: forall (a :: Type).
+        [Text]                            -- ^ Message key
+      -> PromptCancellable
+      -> Map Char (MenuOption a)           -- ^ Menu items
+      -> (PromptResult ('Menu a) -> AppM ()) -- ^ Menu promise handler
+      -> AppM ()
+menu_ msgPath = menu msgPath $ object []
+
+firePrompt_
+  :: [Text]                        -- ^ Message key
+  -> PromptCancellable
+  -> Tiles                         -- ^ Range
+  -> (PromptResult 'Fire -> AppM ()) -- ^ Promise handler
+  -> AppM ()
+firePrompt_ msgPath cancellable range cb = do
+  msg <- Messages.message msgPath $ object []
+  initialPos <- maybe (use characterPosition) pure =<< nearestEnemyPosition
+  let p = mkFirePrompt cancellable initialPos range cb
+  promptState .= WaitingPrompt msg p
+
+-- | Returns the position of the nearest visible hostile creature, if any
+nearestEnemyPosition :: AppM (Maybe Position)
+nearestEnemyPosition = do
+  charPos <- use characterPosition
+  em <- use entities
+  ps <- characterVisiblePositions
+  let candidates = toList ps >>= \p ->
+        let ents = EntityMap.atPositionWithIDs p em
+        in ents
+           ^.. folded
+           . _2
+           . positioned
+           . _SomeEntity @Creature
+           . creatureType
+           . filtered (view hostile)
+           . to (const (distance charPos p, p))
+  pure . headMay . fmap snd $ sortOn fst candidates
+  where
+    distance :: Position -> Position -> Double
+    distance = Metric.distance `on` (fmap fromIntegral . view _Position)
diff --git a/users/grfn/xanthous/src/Xanthous/App/Time.hs b/users/grfn/xanthous/src/Xanthous/App/Time.hs
new file mode 100644
index 0000000000..cca352858d
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/App/Time.hs
@@ -0,0 +1,42 @@
+--------------------------------------------------------------------------------
+module Xanthous.App.Time
+  ( stepGame
+  , stepGameBy
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           System.Exit
+--------------------------------------------------------------------------------
+import           Xanthous.Data (Ticks)
+import           Xanthous.App.Prompt
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Entities.Character (isDead)
+import           Xanthous.Game.State
+import           Xanthous.Game.Prompt
+import           Xanthous.Game.Lenses
+import           Control.Monad.State (modify)
+import qualified Xanthous.Game.Memo as Memo
+--------------------------------------------------------------------------------
+
+
+stepGameBy :: Ticks -> AppM ()
+stepGameBy ticks = do
+  ents <- uses entities EntityMap.toEIDsAndPositioned
+  for_ ents $ \(eid, pEntity) -> do
+    pEntity' <- step ticks pEntity
+    entities . ix eid .= pEntity'
+
+  clearMemo Memo.characterVisiblePositions
+  modify updateCharacterVision
+
+  whenM (uses character isDead)
+    . prompt_ @'Continue ["dead"] Uncancellable
+    . const . lift . liftIO
+    $ exitSuccess
+
+ticksPerTurn :: Ticks
+ticksPerTurn = 100
+
+stepGame :: AppM ()
+stepGame = stepGameBy ticksPerTurn
diff --git a/users/grfn/xanthous/src/Xanthous/Command.hs b/users/grfn/xanthous/src/Xanthous/Command.hs
new file mode 100644
index 0000000000..6e6274a02c
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Command.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Command
+  ( -- * Commands
+    Command(..)
+  , commandIsHidden
+    -- * Keybindings
+  , Keybinding(..)
+  , keybindings
+  , commands
+  , commandFromKey
+  , directionFromChar
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude hiding (Left, Right, Down, try)
+--------------------------------------------------------------------------------
+import           Graphics.Vty.Input (Key(..), Modifier(..))
+import qualified Data.Char as Char
+import           Data.Aeson (FromJSON (parseJSON), FromJSONKey, FromJSONKeyFunction (FromJSONKeyTextParser))
+import qualified Data.Aeson as A
+import           Data.Aeson.Generic.DerivingVia
+import           Text.Megaparsec (Parsec, errorBundlePretty, parse, eof, try)
+import           Text.Megaparsec.Char (string', char', printChar)
+import           Data.FileEmbed (embedFile)
+import qualified Data.Yaml as Yaml
+import           Test.QuickCheck.Arbitrary
+import           Data.Aeson.Types (Parser)
+--------------------------------------------------------------------------------
+import           Xanthous.Data (Direction(..))
+import           Xanthous.Util.QuickCheck (GenericArbitrary(..))
+--------------------------------------------------------------------------------
+
+data Command
+  = Quit
+  | Help
+  | Move !Direction
+  | StartAutoMove !Direction
+  | PreviousMessage
+  | PickUp
+  | Drop
+  | Open
+  | Close
+  | Wait
+  | Eat
+  | Look
+  | Save
+  | Read
+  | ShowInventory
+  | DescribeInventory
+  | Wield
+  | Fire
+  | GoUp
+  | GoDown
+  | Rest
+
+    -- | TODO replace with `:` commands
+  | ToggleRevealAll
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (Hashable, NFData)
+  deriving Arbitrary via GenericArbitrary Command
+  deriving (FromJSON)
+       via WithOptions '[ SumEnc UntaggedVal ]
+           Command
+
+-- | Should the command be hidden from the help menu?
+--
+-- Note that this is true for both debug commands and movement commands, as the
+-- latter is documented non-automatically
+commandIsHidden :: Command -> Bool
+commandIsHidden (Move _) = True
+commandIsHidden (StartAutoMove _) = True
+commandIsHidden ToggleRevealAll = True
+commandIsHidden _ = False
+
+--------------------------------------------------------------------------------
+
+data Keybinding = Keybinding !Key ![Modifier]
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (Hashable, NFData)
+
+parseKeybindingFromText :: Text -> Parser Keybinding
+parseKeybindingFromText
+  = either (fail . errorBundlePretty) pure
+  . parse keybinding "<JSON>"
+  where
+    key :: Parsec Void Text Key
+    key = KUp <$ string' "<up>"
+      <|> KDown <$ string' "<down>"
+      <|> KLeft <$ string' "<left>"
+      <|> KRight <$ string' "<right>"
+      <|> KChar <$> printChar
+
+    modifier :: Parsec Void Text Modifier
+    modifier = modf <* char' '-'
+      where
+        modf = MAlt <$ char' 'a'
+          <|> MMeta <$ char' 'm'
+          <|> MCtrl  <$ char' 'c'
+          <|> MShift  <$ char' 's'
+
+    keybinding :: Parsec Void Text Keybinding
+    keybinding = do
+      mods <- many (try modifier)
+      k <- key
+      eof
+      pure $ Keybinding k mods
+
+instance FromJSON Keybinding where
+  parseJSON = A.withText "Keybinding" parseKeybindingFromText
+
+instance FromJSONKey Keybinding where
+  fromJSONKey = FromJSONKeyTextParser parseKeybindingFromText
+
+rawKeybindings :: ByteString
+rawKeybindings = $(embedFile "src/Xanthous/keybindings.yaml")
+
+keybindings :: HashMap Keybinding Command
+keybindings = either (error . Yaml.prettyPrintParseException) id
+  $ Yaml.decodeEither' rawKeybindings
+
+commands :: HashMap Command Keybinding
+commands = mapFromList . map swap . itoList $ keybindings
+
+commandFromKey :: Key -> [Modifier] -> Maybe Command
+commandFromKey (KChar (directionFromChar -> Just dir)) [] = Just $ Move dir
+commandFromKey (KChar c) []
+  | Char.isUpper c
+  , Just dir <- directionFromChar $ Char.toLower c
+  = Just $ StartAutoMove dir
+commandFromKey k mods = keybindings ^. at keybinding
+  where keybinding = Keybinding k mods
+
+--------------------------------------------------------------------------------
+
+directionFromChar :: Char -> Maybe Direction
+directionFromChar 'h' = Just Left
+directionFromChar 'j' = Just Down
+directionFromChar 'k' = Just Up
+directionFromChar 'l' = Just Right
+directionFromChar 'y' = Just UpLeft
+directionFromChar 'u' = Just UpRight
+directionFromChar 'b' = Just DownLeft
+directionFromChar 'n' = Just DownRight
+directionFromChar '.' = Just Here
+directionFromChar _   = Nothing
diff --git a/users/grfn/xanthous/src/Xanthous/Data.hs b/users/grfn/xanthous/src/Xanthous/Data.hs
new file mode 100644
index 0000000000..c11ceb55aa
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data.hs
@@ -0,0 +1,817 @@
+{-# LANGUAGE PartialTypeSignatures  #-}
+{-# LANGUAGE StandaloneDeriving     #-}
+{-# LANGUAGE RoleAnnotations        #-}
+{-# LANGUAGE RecordWildCards        #-}
+{-# LANGUAGE DeriveTraversable      #-}
+{-# LANGUAGE TemplateHaskell        #-}
+{-# LANGUAGE NoTypeSynonymInstances #-}
+{-# LANGUAGE DuplicateRecordFields  #-}
+{-# LANGUAGE QuantifiedConstraints  #-}
+{-# LANGUAGE UndecidableInstances   #-}
+{-# LANGUAGE AllowAmbiguousTypes    #-}
+--------------------------------------------------------------------------------
+-- | Common data types for Xanthous ------------------------------------------------------------------------------
+module Xanthous.Data
+  ( Opposite(..)
+
+    -- *
+  , Position'(..)
+  , Position
+  , x
+  , y
+
+    -- **
+  , Positioned(..)
+  , _Positioned
+  , position
+  , positioned
+  , loc
+  , _Position
+  , positionFromPair
+  , positionFromV2
+  , addPositions
+  , diffPositions
+  , stepTowards
+  , isUnit
+  , distance
+
+    -- * Boxes
+  , Box(..)
+  , topLeftCorner
+  , bottomRightCorner
+  , setBottomRightCorner
+  , dimensions
+  , inBox
+  , boxIntersects
+  , boxCenter
+  , boxEdge
+  , module Linear.V2
+
+    -- * Unit math
+  , Scalar(..)
+  , Per(..)
+  , invertRate
+  , invertedRate
+  , (|+|)
+  , (|*|)
+  , (|/|)
+  , (:+:)
+  , (:*:)
+  , (:/:)
+  , (:**:)(..)
+  , Ticks(..)
+  , Tiles(..)
+  , TicksPerTile
+  , TilesPerTick
+  , timesTiles
+  , Square(..)
+  , squared
+  , Cubic(..)
+  , Grams
+  , Meters
+  , Uno(..)
+  , Unit(..)
+  , UnitSymbol(..)
+
+    -- *
+  , Dimensions'(..)
+  , Dimensions
+  , HasWidth(..)
+  , HasHeight(..)
+
+    -- *
+  , Direction(..)
+  , move
+  , asPosition
+  , directionOf
+  , Cardinal(..)
+
+    -- *
+  , Corner(..)
+  , Edge(..)
+  , cornerEdges
+
+    -- *
+  , Neighbors(..)
+  , edges
+  , neighborDirections
+  , neighborPositions
+  , neighborCells
+  , arrayNeighbors
+  , rotations
+  , HasTopLeft(..)
+  , HasTop(..)
+  , HasTopRight(..)
+  , HasLeft(..)
+  , HasRight(..)
+  , HasBottomLeft(..)
+  , HasBottom(..)
+  , HasBottomRight(..)
+
+    -- *
+  , Hitpoints(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (Left, Down, Right, (.=), elements)
+--------------------------------------------------------------------------------
+import           Linear.V2 hiding (_x, _y)
+import qualified Linear.V2 as L
+import           Linear.V4 hiding (_x, _y)
+import           Test.QuickCheck (CoArbitrary, Function, elements)
+import           Test.QuickCheck.Arbitrary.Generic
+import           Data.Group
+import           Brick (Location(Location), Edges(..))
+import           Data.Monoid (Product(..), Sum(..))
+import           Data.Array.IArray
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Aeson
+                 ( ToJSON(..), FromJSON(..), object, (.=), (.:), withObject)
+import           Data.Random (Distribution)
+import           Data.Coerce
+import           Data.Proxy (Proxy(Proxy))
+--------------------------------------------------------------------------------
+import           Xanthous.Util (EqEqProp(..), EqProp, between)
+import           Xanthous.Orphans ()
+import           Xanthous.Util.Graphics
+import qualified Linear.Metric as Metric
+--------------------------------------------------------------------------------
+
+-- | opposite ∘ opposite ≡ id
+class Opposite x where
+  opposite :: x -> x
+
+--------------------------------------------------------------------------------
+
+-- fromScalar ∘ scalar ≡ id
+class Scalar a where
+  scalar :: a -> Double
+  fromScalar :: Double -> a
+
+instance Scalar Double where
+  scalar = id
+  fromScalar = id
+
+newtype ScalarIntegral a = ScalarIntegral a
+  deriving newtype (Eq, Ord, Num, Enum, Real, Integral)
+instance Integral a => Scalar (ScalarIntegral a) where
+  scalar = fromIntegral
+  fromScalar = floor
+
+deriving via (ScalarIntegral Integer) instance Scalar Integer
+deriving via (ScalarIntegral Word) instance Scalar Word
+
+-- | Units of measure
+class Unit a where
+  unitSuffix :: Text
+type UnitSymbol :: Symbol -> Type -> Type
+newtype UnitSymbol suffix a = UnitSymbol a
+instance KnownSymbol suffix => Unit (UnitSymbol suffix a) where
+  unitSuffix = pack $ symbolVal @suffix Proxy
+
+newtype ShowUnitSuffix a b = ShowUnitSuffix a
+instance (Show b, Unit a, Coercible a b) => Show (ShowUnitSuffix a b) where
+  show a = show (coerce @_ @b a) <> " " <> unpack (unitSuffix @a)
+
+--------------------------------------------------------------------------------
+
+data Position' a where
+  Position :: { _x :: a
+             , _y :: a
+             } -> (Position' a)
+  deriving stock (Show, Eq, Generic, Ord, Functor, Foldable, Traversable)
+  deriving anyclass (NFData, Hashable, CoArbitrary, Function)
+  deriving EqProp via EqEqProp (Position' a)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       (Position' a)
+
+x, y :: Lens' (Position' a) a
+x = lens (\(Position xx _) -> xx) (\(Position _ yy) xx -> Position xx yy)
+y = lens (\(Position _ yy) -> yy) (\(Position xx _) yy -> Position xx yy)
+
+type Position = Position' Int
+
+instance Arbitrary a => Arbitrary (Position' a) where
+  arbitrary = genericArbitrary
+  shrink (Position px py) = Position <$> shrink px <*> shrink py
+
+
+instance Num a => Semigroup (Position' a) where
+  (Position x₁ y₁) <> (Position x₂ y₂) = Position (x₁ + x₂) (y₁ + y₂)
+
+instance Num a => Monoid (Position' a) where
+  mempty = Position 0 0
+
+instance Num a => Group (Position' a) where
+  invert (Position px py) = Position (negate px) (negate py)
+
+-- | Positions convert to scalars by discarding their orientation and just
+-- measuring the length from the origin
+instance (Ord a, Num a, Scalar a) => Scalar (Position' a) where
+  scalar = fromIntegral . length . line 0 . view _Position
+  fromScalar n = Position (fromScalar n) (fromScalar n)
+
+data Positioned a where
+  Positioned :: Position -> a -> Positioned a
+  deriving stock (Show, Eq, Ord, Functor, Foldable, Traversable, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+type role Positioned representational
+
+_Positioned :: Iso (Position, a) (Position, b) (Positioned a) (Positioned b)
+_Positioned = iso hither yon
+  where
+    hither (pos, a) = Positioned pos a
+    yon (Positioned pos b) = (pos, b)
+
+instance Arbitrary a => Arbitrary (Positioned a) where
+  arbitrary = Positioned <$> arbitrary <*> arbitrary
+
+instance ToJSON a => ToJSON (Positioned a) where
+  toJSON (Positioned pos val) = object
+    [ "position" .= pos
+    , "data" .= val
+    ]
+
+instance FromJSON a => FromJSON (Positioned a) where
+  parseJSON = withObject "Positioned" $ \obj ->
+    Positioned <$> obj .: "position" <*> obj .: "data"
+
+position :: Lens' (Positioned a) Position
+position = lens
+  (\(Positioned pos _) -> pos)
+  (\(Positioned _ a) pos -> Positioned pos a)
+
+positioned :: Lens (Positioned a) (Positioned b) a b
+positioned = lens
+  (\(Positioned _ x') -> x')
+  (\(Positioned pos _) x' -> Positioned pos x')
+
+loc :: Iso' Position Location
+loc = iso hither yon
+  where
+    hither (Position px py) = Location (px, py)
+    yon (Location (lx, ly)) = Position lx ly
+
+_Position :: Iso' (Position' a) (V2 a)
+_Position = iso hither yon
+  where
+    hither (Position px py) = V2 px py
+    yon (V2 lx ly) = Position lx ly
+
+positionFromPair :: (Num a, Integral i, Integral j) => (i, j) -> Position' a
+positionFromPair (i, j) = Position (fromIntegral i) (fromIntegral j)
+
+positionFromV2 :: (Num a, Integral i) => V2 i -> Position' a
+positionFromV2 (V2 xx yy) = Position (fromIntegral xx) (fromIntegral yy)
+
+-- | Add two positions
+--
+-- Operation for the additive group on positions
+addPositions :: Num a => Position' a -> Position' a -> Position' a
+addPositions = (<>)
+
+-- | Subtract two positions.
+--
+-- diffPositions pos₁ pos₂ = pos₁ `addPositions` (invert pos₂)
+diffPositions :: Num a => Position' a -> Position' a -> Position' a
+diffPositions (Position x₁ y₁) (Position x₂ y₂) = Position (x₁ - x₂) (y₁ - y₂)
+
+-- | Is this position a unit position? or: When taken as a difference, does this
+-- position represent a step of one tile?
+--
+-- ∀ dir :: Direction. isUnit ('asPosition' dir)
+isUnit :: (Eq a, Num a) => Position' a -> Bool
+isUnit (Position px py) =
+  abs px `elem` [0,1] && abs py `elem` [0, 1] && (px, py) /= (0, 0)
+
+--------------------------------------------------------------------------------
+
+data Dimensions' a = Dimensions
+  { _width :: a
+  , _height :: a
+  }
+  deriving stock (Show, Eq, Functor, Generic)
+  deriving anyclass (CoArbitrary, Function)
+makeFieldsNoPrefix ''Dimensions'
+
+instance Arbitrary a => Arbitrary (Dimensions' a) where
+  arbitrary = Dimensions <$> arbitrary <*> arbitrary
+
+type Dimensions = Dimensions' Word
+
+--------------------------------------------------------------------------------
+
+data Direction where
+  Up        :: Direction
+  Down      :: Direction
+  Left      :: Direction
+  Right     :: Direction
+  UpLeft    :: Direction
+  UpRight   :: Direction
+  DownLeft  :: Direction
+  DownRight :: Direction
+  Here      :: Direction
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (CoArbitrary, Function, NFData, ToJSON, FromJSON, Hashable)
+  deriving Arbitrary via GenericArbitrary Direction
+
+instance Opposite Direction where
+  opposite Up        = Down
+  opposite Down      = Up
+  opposite Left      = Right
+  opposite Right     = Left
+  opposite UpLeft    = DownRight
+  opposite UpRight   = DownLeft
+  opposite DownLeft  = UpRight
+  opposite DownRight = UpLeft
+  opposite Here      = Here
+
+move :: Num a => Direction -> Position' a -> Position' a
+move Up        = y -~ 1
+move Down      = y +~ 1
+move Left      = x -~ 1
+move Right     = x +~ 1
+move UpLeft    = move Up . move Left
+move UpRight   = move Up . move Right
+move DownLeft  = move Down . move Left
+move DownRight = move Down . move Right
+move Here      = id
+
+asPosition :: Direction -> Position
+asPosition dir = move dir mempty
+
+-- | Returns the direction that a given position is from a given source position
+directionOf
+  :: Position -- ^ Source
+  -> Position -- ^ Target
+  -> Direction
+directionOf (Position x₁ y₁) (Position x₂ y₂) =
+  case (x₁ `compare` x₂, y₁ `compare` y₂) of
+    (EQ, EQ) -> Here
+    (EQ, LT) -> Down
+    (EQ, GT) -> Up
+    (LT, EQ) -> Right
+    (GT, EQ) -> Left
+
+    (LT, LT) -> DownRight
+    (GT, LT) -> DownLeft
+
+    (LT, GT) -> UpRight
+    (GT, GT) -> UpLeft
+
+-- | Take one (potentially diagonal) step towards the given position
+--
+-- ∀ src tgt. isUnit (src `diffPositions` (src `stepTowards tgt`))
+stepTowards
+  :: Position -- ^ Source
+  -> Position -- ^ Target
+  -> Position
+stepTowards (view _Position -> p₁) (view _Position -> p₂)
+  | p₁ == p₂ = _Position # p₁
+  | otherwise =
+    let (_:p:_) = line p₁ p₂
+    in _Position # p
+
+-- | Newtype controlling arbitrary generation to only include cardinal
+-- directions ('Up', 'Down', 'Left', 'Right')
+newtype Cardinal = Cardinal { getCardinal :: Direction }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, Function, CoArbitrary)
+  deriving newtype (Opposite)
+
+instance Arbitrary Cardinal where
+  arbitrary = Cardinal <$> elements [Up, Down, Left, Right]
+
+--------------------------------------------------------------------------------
+
+data Corner
+  = TopLeft
+  | TopRight
+  | BottomLeft
+  | BottomRight
+  deriving stock (Show, Eq, Ord, Enum, Bounded, Generic)
+  deriving Arbitrary via GenericArbitrary Corner
+
+instance Opposite Corner where
+  opposite TopLeft = BottomRight
+  opposite TopRight = BottomLeft
+  opposite BottomLeft = TopRight
+  opposite BottomRight = TopLeft
+
+data Edge
+  = TopEdge
+  | LeftEdge
+  | RightEdge
+  | BottomEdge
+  deriving stock (Show, Eq, Ord, Enum, Bounded, Generic)
+  deriving Arbitrary via GenericArbitrary Edge
+
+instance Opposite Edge where
+  opposite TopEdge = BottomEdge
+  opposite BottomEdge = TopEdge
+  opposite LeftEdge = RightEdge
+  opposite RightEdge = LeftEdge
+
+cornerEdges :: Corner -> (Edge, Edge)
+cornerEdges TopLeft = (TopEdge, LeftEdge)
+cornerEdges TopRight = (TopEdge, RightEdge)
+cornerEdges BottomLeft = (BottomEdge, LeftEdge)
+cornerEdges BottomRight = (BottomEdge, RightEdge)
+
+--------------------------------------------------------------------------------
+
+data Neighbors a = Neighbors
+  { _topLeft
+  , _top
+  , _topRight
+  , _left
+  , _right
+  , _bottomLeft
+  , _bottom
+  , _bottomRight :: a
+  }
+  deriving stock (Show, Eq, Ord, Functor, Foldable, Traversable, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function, MonoFoldable)
+  deriving Arbitrary via GenericArbitrary (Neighbors a)
+
+type instance Element (Neighbors a) = a
+
+makeFieldsNoPrefix ''Neighbors
+
+instance Applicative Neighbors where
+  pure α = Neighbors
+    { _topLeft     = α
+    , _top         = α
+    , _topRight    = α
+    , _left        = α
+    , _right       = α
+    , _bottomLeft  = α
+    , _bottom      = α
+    , _bottomRight = α
+    }
+  nf <*> nx = Neighbors
+    { _topLeft     = nf ^. topLeft     $ nx ^. topLeft
+    , _top         = nf ^. top         $ nx ^. top
+    , _topRight    = nf ^. topRight    $ nx ^. topRight
+    , _left        = nf ^. left        $ nx ^. left
+    , _right       = nf ^. right       $ nx ^. right
+    , _bottomLeft  = nf ^. bottomLeft  $ nx ^. bottomLeft
+    , _bottom      = nf ^. bottom      $ nx ^. bottom
+    , _bottomRight = nf ^. bottomRight $ nx ^. bottomRight
+    }
+
+edges :: Neighbors a -> Edges a
+edges neighs = Edges
+  { eTop = neighs ^. top
+  , eBottom = neighs ^. bottom
+  , eLeft = neighs ^. left
+  , eRight = neighs ^. right
+  }
+
+neighborDirections :: Neighbors Direction
+neighborDirections = Neighbors
+  { _topLeft     = UpLeft
+  , _top         = Up
+  , _topRight    = UpRight
+  , _left        = Left
+  , _right       = Right
+  , _bottomLeft  = DownLeft
+  , _bottom      = Down
+  , _bottomRight = DownRight
+  }
+
+neighborPositions :: Num a => Position' a -> Neighbors (Position' a)
+neighborPositions pos = (`move` pos) <$> neighborDirections
+
+neighborCells :: Num a => V2 a -> Neighbors (V2 a)
+neighborCells = map (view _Position) . neighborPositions . review _Position
+
+arrayNeighbors
+  :: (IArray a e, Ix i, Num i)
+  => a (V2 i) e
+  -> V2 i
+  -> Neighbors (Maybe e)
+arrayNeighbors arr center = arrLookup <$> neighborPositions (_Position # center)
+  where
+    arrLookup (view _Position -> pos)
+      | inRange (bounds arr) pos = Just $ arr ! pos
+      | otherwise                = Nothing
+
+-- | Returns a list of all 4 90-degree rotations of the given neighbors
+rotations :: Neighbors a -> V4 (Neighbors a)
+rotations orig@(Neighbors tl t tr l r bl b br) = V4
+   orig                            -- tl t  tr
+                                   -- l     r
+                                   -- bl b  br
+
+   (Neighbors bl l tl b t br r tr) -- bl l tl
+                                   -- b    t
+                                   -- br r tr
+
+   (Neighbors br b bl r l tr t tl) -- br b bl
+                                   -- r    l
+                                   -- tr t tl
+
+   (Neighbors tr r br t b tl l bl) -- tr r br
+                                   -- t    b
+                                   -- tl l bl
+
+--------------------------------------------------------------------------------
+
+newtype Per a b = Rate Double
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (Arbitrary, Num, Ord, Enum, Real, Fractional, ToJSON, FromJSON)
+       via Double
+  deriving (Semigroup, Monoid) via Product Double
+  deriving Show via ShowUnitSuffix (Per a b) Double
+deriving via Double
+  instance ( Distribution d Double
+           , forall xx yy. Coercible xx yy => Coercible (d xx) (d yy)
+           )
+  => Distribution d (Per a b)
+
+instance (Unit a, Unit b) => Unit (a `Per` b) where
+  unitSuffix = unitSuffix @a <> "/" <> unitSuffix @b
+
+invertRate :: a `Per` b -> b `Per` a
+invertRate (Rate p) = Rate $ 1 / p
+
+invertedRate :: Iso (a `Per` b) (b' `Per` a') (b `Per` a) (a' `Per` b')
+invertedRate = iso invertRate invertRate
+
+type (:+:) :: Type -> Type -> Type
+type family (:+:) a b where
+  a :+: a       = a
+  a :+: (Uno b) = a
+
+infixl 6 |+|
+class AddUnit a b where
+  (|+|) :: a -> b -> a :+: b
+
+instance Scalar a => AddUnit a a where
+  x' |+| y' = fromScalar $ scalar x' + scalar y'
+
+instance (Scalar a, Scalar b) => AddUnit a (Uno b) where
+  x' |+| y' = fromScalar $ scalar x' + scalar y'
+
+type (:*:) :: Type -> Type -> Type
+type family (:*:) a b where
+  (a `Per` b) :*: b     = a
+  (Square a)  :*: a     = Cubic a
+  a           :*: a     = Square a
+  a           :*: Uno b = a
+  a           :*: b     = a :**: b
+
+infixl 7 |*|
+class MulUnit a b where
+  (|*|) :: a -> b -> a :*: b
+
+instance (Scalar a, Scalar b) => MulUnit (a `Per` b) b where
+  (Rate rate) |*| b = fromScalar $ rate * scalar b
+
+instance forall a. (Scalar a, a :*: a ~ Square a) => MulUnit a a where
+  x' |*| y' = Square @a . fromScalar $ scalar x' * scalar y'
+
+instance forall a. (Scalar a) => MulUnit (Square a) a where
+  x' |*| y' = Cubic @a . fromScalar $ scalar x' * scalar y'
+
+instance {-# INCOHERENT #-} forall a b.
+  (Scalar a, Scalar b, Scalar (a :*: Uno b))
+    => MulUnit a (Uno b) where
+  x' |*| y' = fromScalar $ scalar x' * scalar y'
+
+type (:/:) :: Type -> Type -> Type
+type family (:/:) a b where
+  (Square a) :/: a          = a
+  (Cubic a)  :/: a          = Square a
+  (Cubic a)  :/: (Square a) = a
+  (a :**: b) :/: b          = a
+  (a :**: b) :/: a          = b
+  a          :/: Uno b      = a
+  a          :/: b          = a `Per` b
+
+infixl 7 |/|
+class DivUnit a b where
+  (|/|) :: a -> b -> a :/: b
+
+instance Scalar a => DivUnit (Square a) a where
+  (Square a) |/| b = fromScalar $ scalar a / scalar b
+
+instance Scalar a => DivUnit (Cubic a) a where
+  (Cubic a) |/| b = fromScalar $ scalar a / scalar b
+
+instance (Scalar a, Cubic a :/: Square a ~ a)
+       => DivUnit (Cubic a) (Square a) where
+  (Cubic a) |/| (Square b) = fromScalar $ scalar a / scalar b
+
+instance (Scalar a, Scalar b) => DivUnit (a :**: b) b where
+  (Times a) |/| b = fromScalar $ scalar a / scalar b
+
+instance (Scalar a, Scalar b) => DivUnit (a :**: b) a where
+  (Times a) |/| b = fromScalar $ scalar a / scalar b
+
+instance {-# INCOHERENT #-} forall a b.
+  (Scalar a, Scalar b, Scalar (a :/: Uno b))
+    => DivUnit a (Uno b) where
+  x' |/| y' = fromScalar $ scalar x' / scalar y'
+
+-- | Dimensionless quantitites (mass per unit mass, radians, etc)
+--
+-- see <https://en.wikipedia.org/wiki/Parts-per_notation#Uno>
+newtype Uno a = Uno a
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving ( Arbitrary, Num, Ord, Enum, Real, Fractional, ToJSON, FromJSON
+           , Scalar, Show
+           )
+       via a
+  deriving Unit via UnitSymbol "" (Uno a)
+
+newtype Square a = Square a
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving ( Arbitrary, Num, Ord, Enum, Real, Fractional, ToJSON, FromJSON
+           , Scalar
+           )
+       via a
+deriving via (a :: Type)
+  instance ( Distribution d a
+           , forall xx yy. Coercible xx yy => Coercible (d xx) (d yy)
+           )
+  => Distribution d (Square a)
+
+instance Unit a => Unit (Square a) where
+  unitSuffix = unitSuffix @a <> "²"
+
+instance Show a => Show (Square a) where
+  show (Square n) = show n <> "²"
+
+squared :: (Scalar a, a :*: a ~ Square a) => a -> Square a
+squared v = v |*| v
+
+newtype Cubic a = Cubic a
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving ( Arbitrary, Num, Ord, Enum, Real, Fractional, ToJSON, FromJSON
+           , Scalar
+           )
+       via a
+deriving via (a :: Type)
+  instance ( Distribution d a
+           , forall xx yy. Coercible xx yy => Coercible (d xx) (d yy)
+           )
+  => Distribution d (Cubic a)
+
+instance Unit a => Unit (Cubic a) where
+  unitSuffix = unitSuffix @a <> "³"
+
+instance Show a => Show (Cubic a) where
+  show (Cubic n) = show n <> "³"
+
+newtype (:**:) a b = Times Double
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (Arbitrary, Num, Ord, Enum, Real, Fractional, ToJSON, FromJSON)
+       via Double
+  deriving (Semigroup, Monoid) via Sum Double
+  deriving Show via ShowUnitSuffix (a :**: b) Double
+deriving via Double
+  instance ( Distribution d Double
+           , forall xx yy. Coercible xx yy => Coercible (d xx) (d yy)
+           )
+  => Distribution d (a :**: b)
+
+instance (Unit a, Unit b) => Unit (a :**: b) where
+  unitSuffix = unitSuffix @a <> " " <> unitSuffix @b
+
+--------------------------------------------------------------------------------
+
+newtype Ticks = Ticks Word
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (Num, Ord, Bounded, Enum, Integral, Real, ToJSON, FromJSON) via Word
+  deriving (Semigroup, Monoid) via (Sum Word)
+  deriving Scalar via ScalarIntegral Ticks
+  deriving Arbitrary via GenericArbitrary Ticks
+  deriving Unit via UnitSymbol "ticks" Ticks
+  deriving Show via ShowUnitSuffix Ticks Word
+deriving via Word
+  instance ( Distribution d Word
+           , forall xx yy. Coercible xx yy => Coercible (d xx) (d yy)
+           )
+  => Distribution d Ticks
+
+newtype Tiles = Tiles Double
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (Num, Ord, Enum, Real, ToJSON, FromJSON, Scalar) via Double
+  deriving (Semigroup, Monoid) via (Sum Double)
+  deriving Arbitrary via GenericArbitrary Tiles
+  deriving Unit via UnitSymbol "m" Tiles
+  deriving Show via ShowUnitSuffix Tiles Double
+deriving via Double
+  instance ( Distribution d Double
+           , forall xx yy. Coercible xx yy => Coercible (d xx) (d yy)
+           )
+  => Distribution d Tiles
+
+type TicksPerTile = Ticks `Per` Tiles
+type TilesPerTick = Tiles `Per` Ticks
+
+timesTiles :: TicksPerTile -> Tiles -> Ticks
+timesTiles = (|*|)
+
+-- | Calculate the (cartesian) distance between two 'Position's, floored and
+-- represented as a number of 'Tile's
+--
+-- Note that this is imprecise, and may be different than the length of a
+-- bresenham's line between the points
+distance :: Position -> Position -> Tiles
+distance
+  = (fromScalar .) . (Metric.distance `on` (fmap fromIntegral . view _Position))
+
+--------------------------------------------------------------------------------
+
+newtype Hitpoints = Hitpoints Word
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving ( Arbitrary, Num, Ord, Bounded, Enum, Integral, Real, Scalar
+           , ToJSON, FromJSON
+           )
+       via Word
+  deriving (Semigroup, Monoid) via Sum Word
+  deriving Unit via UnitSymbol "hp" Hitpoints
+  deriving Show via ShowUnitSuffix Hitpoints Word
+
+--------------------------------------------------------------------------------
+
+-- | Grams, the fundamental measure of weight in Xanthous.
+newtype Grams = Grams Double
+  deriving stock (Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving ( Arbitrary, Num, Ord, Enum, Real, Floating, Fractional, RealFloat
+           , RealFrac, Scalar, ToJSON, FromJSON
+           )
+       via Double
+  deriving (Semigroup, Monoid) via Sum Double
+  deriving Unit via UnitSymbol "g" Grams
+  deriving Show via ShowUnitSuffix Grams Double
+
+-- | Every tile is 1 meter
+type Meters = Tiles
+
+--------------------------------------------------------------------------------
+
+data Box a = Box
+  { _topLeftCorner :: V2 a
+  , _dimensions    :: V2 a
+  }
+  deriving stock (Show, Eq, Ord, Functor, Generic)
+  deriving Arbitrary via GenericArbitrary (Box a)
+makeFieldsNoPrefix ''Box
+
+bottomRightCorner :: Num a => Box a -> V2 a
+bottomRightCorner box =
+  V2 (box ^. topLeftCorner . L._x + box ^. dimensions . L._x)
+     (box ^. topLeftCorner . L._y + box ^. dimensions . L._y)
+
+setBottomRightCorner :: (Num a, Ord a) => Box a -> V2 a -> Box a
+setBottomRightCorner box br@(V2 brx bry)
+  | brx < box ^. topLeftCorner . L._x || bry < box ^. topLeftCorner . L._y
+  = box & topLeftCorner .~ br
+        & dimensions . L._x .~ ((box ^. topLeftCorner . L._x) - brx)
+        & dimensions . L._y .~ ((box ^. topLeftCorner . L._y) - bry)
+  | otherwise
+  = box & dimensions . L._x .~ (brx - (box ^. topLeftCorner . L._x))
+        & dimensions . L._y .~ (bry - (box ^. topLeftCorner . L._y))
+
+inBox :: (Ord a, Num a) => Box a -> V2 a -> Bool
+inBox box pt = flip all [L._x, L._y] $ \component ->
+  between (box ^. topLeftCorner . component)
+          (box ^. to bottomRightCorner . component)
+          (pt ^. component)
+
+boxIntersects :: (Ord a, Num a) => Box a -> Box a -> Bool
+boxIntersects box₁ box₂
+  = any (inBox box₁) [box₂ ^. topLeftCorner, bottomRightCorner box₂]
+
+boxCenter :: (Fractional a) => Box a -> V2 a
+boxCenter box = V2 cx cy
+ where
+   cx = box ^. topLeftCorner . L._x + (box ^. dimensions . L._x / 2)
+   cy = box ^. topLeftCorner . L._y + (box ^. dimensions . L._y / 2)
+
+boxEdge :: (Enum a, Num a) => Box a -> Edge -> [V2 a]
+boxEdge box LeftEdge =
+  V2 (box ^. topLeftCorner . L._x)
+  <$> [box ^. topLeftCorner . L._y .. box ^. to bottomRightCorner . L._y]
+boxEdge box RightEdge =
+  V2 (box ^. to bottomRightCorner . L._x)
+  <$> [box ^. to bottomRightCorner . L._y .. box ^. to bottomRightCorner . L._y]
+boxEdge box TopEdge =
+  flip V2 (box ^. topLeftCorner . L._y)
+  <$> [box ^. topLeftCorner . L._x .. box ^. to bottomRightCorner . L._x]
+boxEdge box BottomEdge =
+  flip V2 (box ^. to bottomRightCorner . L._y)
+  <$> [box ^. topLeftCorner . L._x .. box ^. to bottomRightCorner . L._x]
diff --git a/users/grfn/xanthous/src/Xanthous/Data/App.hs b/users/grfn/xanthous/src/Xanthous/Data/App.hs
new file mode 100644
index 0000000000..13c4b5d610
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/App.hs
@@ -0,0 +1,47 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.App
+  ( Panel(..)
+  , ResourceName(..)
+  , AppEvent(..)
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Test.QuickCheck
+import Test.QuickCheck.Instances.Text ()
+import Data.Aeson (ToJSON, FromJSON)
+--------------------------------------------------------------------------------
+import Xanthous.Util.QuickCheck
+--------------------------------------------------------------------------------
+
+-- | Enum for "panels" displayed in the game's UI.
+data Panel
+  = -- | A panel providing help with the game's commands
+    HelpPanel
+  | -- | A panel displaying the character's inventory
+    InventoryPanel
+  | -- | A panel describing an item in the inventory in detail
+    --
+    -- The argument is the full description of the item
+    ItemDescriptionPanel Text
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function, ToJSON, FromJSON)
+  deriving Arbitrary via GenericArbitrary Panel
+
+
+data ResourceName
+  = MapViewport -- ^ The main viewport where we display the game content
+  | Character   -- ^ The character
+  | MessageBox  -- ^ The box where we display messages to the user
+  | Prompt      -- ^ The game's prompt
+  | Panel Panel -- ^ A panel in the game
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function, ToJSON, FromJSON)
+  deriving Arbitrary via GenericArbitrary ResourceName
+
+data AppEvent
+  = AutoContinue -- ^ Continue whatever autocommand has been requested by the
+                 --   user
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function, ToJSON, FromJSON)
+  deriving Arbitrary via GenericArbitrary AppEvent
diff --git a/users/grfn/xanthous/src/Xanthous/Data/Entities.hs b/users/grfn/xanthous/src/Xanthous/Data/Entities.hs
new file mode 100644
index 0000000000..39953410f2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/Entities.hs
@@ -0,0 +1,68 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.Entities
+  ( -- * Collisions
+    Collision(..)
+  , _Stop
+  , _Combat
+    -- * Entity Attributes
+  , EntityAttributes(..)
+  , blocksVision
+  , blocksObject
+  , collision
+  , defaultEntityAttributes
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.Aeson (ToJSON(..), FromJSON(..), (.:?), (.!=), withObject)
+import           Data.Aeson.Generic.DerivingVia
+import           Xanthous.Util.QuickCheck (GenericArbitrary(..))
+import           Test.QuickCheck
+--------------------------------------------------------------------------------
+
+data Collision
+  = Stop   -- ^ Can't move through this
+  | Combat -- ^ Moving into this equates to hitting it with a stick
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Collision
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ AllNullaryToStringTag 'True ]
+           Collision
+makePrisms ''Collision
+
+-- | Attributes of an entity
+data EntityAttributes = EntityAttributes
+  { _blocksVision :: Bool
+    -- | Does this entity block a large object from being put in the same tile as
+    -- it - eg a a door being closed on it
+  , _blocksObject :: Bool
+    -- | What type of collision happens when moving into this entity?
+  , _collision :: Collision
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary EntityAttributes
+  deriving (ToJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           EntityAttributes
+makeLenses ''EntityAttributes
+
+instance FromJSON EntityAttributes where
+  parseJSON = withObject "EntityAttributes" $ \o -> do
+    _blocksVision <- o .:? "blocksVision"
+                      .!= _blocksVision defaultEntityAttributes
+    _blocksObject <- o .:? "blocksObject"
+                      .!= _blocksObject defaultEntityAttributes
+    _collision    <- o .:? "collision"
+                      .!= _collision defaultEntityAttributes
+    pure EntityAttributes {..}
+
+defaultEntityAttributes :: EntityAttributes
+defaultEntityAttributes = EntityAttributes
+  { _blocksVision = False
+  , _blocksObject = False
+  , _collision    = Stop
+  }
diff --git a/users/grfn/xanthous/src/Xanthous/Data/EntityChar.hs b/users/grfn/xanthous/src/Xanthous/Data/EntityChar.hs
new file mode 100644
index 0000000000..855a3462da
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/EntityChar.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE RoleAnnotations      #-}
+{-# LANGUAGE RecordWildCards      #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE GADTs                #-}
+{-# LANGUAGE AllowAmbiguousTypes  #-}
+{-# LANGUAGE TemplateHaskell      #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntityChar
+  ( EntityChar(..)
+  , HasChar(..)
+  , HasStyle(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding ((.=))
+--------------------------------------------------------------------------------
+import qualified Graphics.Vty.Attributes as Vty
+import           Test.QuickCheck
+import           Data.Aeson
+--------------------------------------------------------------------------------
+import           Xanthous.Orphans ()
+import           Xanthous.Util.QuickCheck (GenericArbitrary(..))
+--------------------------------------------------------------------------------
+
+
+class HasChar s a | s -> a where
+  char :: Lens' s a
+  {-# MINIMAL char #-}
+
+data EntityChar = EntityChar
+  { _char :: Char
+  , _style :: Vty.Attr
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary EntityChar
+makeFieldsNoPrefix ''EntityChar
+
+instance FromJSON EntityChar where
+  parseJSON (String (chr :< Empty)) = pure $ EntityChar chr Vty.defAttr
+  parseJSON (Object o) = do
+    (EntityChar _char _) <- o .: "char"
+    _style <- o .:? "style" .!= Vty.defAttr
+    pure EntityChar {..}
+  parseJSON _ = fail "Invalid type, expected string or object"
+
+instance ToJSON EntityChar where
+  toJSON (EntityChar chr styl)
+    | styl == Vty.defAttr = String $ chr <| Empty
+    | otherwise = object
+      [ "char" .= chr
+      , "style" .= styl
+      ]
+
+instance IsString EntityChar where
+  fromString [ch] = EntityChar ch Vty.defAttr
+  fromString _ = error "Entity char must only be a single character"
diff --git a/users/grfn/xanthous/src/Xanthous/Data/EntityMap.hs b/users/grfn/xanthous/src/Xanthous/Data/EntityMap.hs
new file mode 100644
index 0000000000..33a98f1ae5
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/EntityMap.hs
@@ -0,0 +1,276 @@
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE DeriveTraversable  #-}
+{-# LANGUAGE TupleSections      #-}
+{-# LANGUAGE TemplateHaskell    #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE DeriveFunctor      #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntityMap
+  ( EntityMap
+  , _EntityMap
+  , EntityID
+  , emptyEntityMap
+  , insertAt
+  , insertAtReturningID
+  , fromEIDsAndPositioned
+  , toEIDsAndPositioned
+  , atPosition
+  , atPositionWithIDs
+  , positions
+  , lookup
+  , lookupWithPosition
+  , positionOf
+  -- , positionedEntities
+  , neighbors
+  , Deduplicate(..)
+
+  -- * debug
+  , byID
+  , byPosition
+  , lastID
+
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude hiding (lookup)
+import Xanthous.Data
+  ( Position
+  , Positioned(..)
+  , positioned
+  , Neighbors(..)
+  , neighborPositions, position
+  )
+import Xanthous.Data.VectorBag
+import Xanthous.Orphans ()
+import Xanthous.Util (EqEqProp(..))
+--------------------------------------------------------------------------------
+import Data.Monoid (Endo(..))
+import Test.QuickCheck (Arbitrary(..), CoArbitrary, Function)
+import Test.QuickCheck.Checkers (EqProp)
+import Test.QuickCheck.Instances.UnorderedContainers ()
+import Test.QuickCheck.Instances.Vector ()
+import Text.Show (showString, showParen)
+import Data.Aeson
+--------------------------------------------------------------------------------
+
+type EntityID = Word32
+type NonNullSet a = NonNull (Set a)
+
+data EntityMap a where
+  EntityMap ::
+    { _byPosition :: Map Position (NonNullSet EntityID)
+    , _byID       :: HashMap EntityID (Positioned a)
+    , _lastID     :: EntityID
+    } -> EntityMap a
+  deriving stock (Functor, Foldable, Traversable, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+deriving via (EqEqProp (EntityMap a)) instance (Eq a, Ord a) => EqProp (EntityMap a)
+makeLenses ''EntityMap
+
+instance ToJSON a => ToJSON (EntityMap a) where
+  toJSON = toJSON . toEIDsAndPositioned
+
+
+instance FromJSON a => FromJSON (EntityMap a) where
+  parseJSON = fmap (fromEIDsAndPositioned @[_]) . parseJSON
+
+byIDInvariantError :: forall a. a
+byIDInvariantError = error $ "Invariant violation: All EntityIDs in byPosition "
+  <> "must point to entityIDs in byID"
+
+instance (Ord a, Eq a) => Eq (EntityMap a) where
+  -- em₁ == em₂ = em₁ ^. _EntityMap == em₂ ^. _EntityMap
+  (==) = (==) `on` view (_EntityMap . to sort)
+
+deriving stock instance (Ord a) => Ord (EntityMap a)
+
+instance Show a => Show (EntityMap a) where
+  showsPrec pr em
+    = showParen (pr > 10)
+    $ showString
+    . ("fromEIDsAndPositioned " <>)
+    . show
+    . toEIDsAndPositioned
+    $ em
+
+instance Arbitrary a => Arbitrary (EntityMap a) where
+  arbitrary = review _EntityMap <$> arbitrary
+  shrink em = review _EntityMap <$> shrink (em ^. _EntityMap)
+
+type instance Index (EntityMap a) = EntityID
+type instance IxValue (EntityMap a) = (Positioned a)
+instance Ixed (EntityMap a) where ix eid = at eid . traverse
+
+instance At (EntityMap a) where
+  at eid = lens (view $ byID . at eid) setter
+    where
+      setter :: EntityMap a -> Maybe (Positioned a) -> EntityMap a
+      setter m Nothing = fromMaybe m $ do
+        Positioned pos _ <- m ^. byID . at eid
+        pure $ m
+          & removeEIDAtPos pos
+          & byID . at eid .~ Nothing
+      setter m (Just pe@(Positioned pos _)) = m
+        & (case lookupWithPosition eid m of
+             Nothing -> id
+             Just (Positioned origPos _) -> removeEIDAtPos origPos
+          )
+        & byID . at eid ?~ pe
+        & byPosition . at pos %~ \case
+            Nothing -> Just $ opoint eid
+            Just es -> Just $ ninsertSet eid es
+      removeEIDAtPos pos =
+        byPosition . at pos %~ (>>= fromNullable . ndeleteSet eid)
+
+instance Semigroup (EntityMap a) where
+  em₁ <> em₂ = alaf Endo foldMap (uncurry insertAt) (em₂ ^. _EntityMap) em₁
+
+instance Monoid (EntityMap a) where
+  mempty = emptyEntityMap
+
+instance FunctorWithIndex EntityID EntityMap
+
+instance FoldableWithIndex EntityID EntityMap
+
+instance TraversableWithIndex EntityID EntityMap where
+  itraverse = itraverseOf itraversed
+
+type instance Element (EntityMap a) = a
+instance MonoFoldable (EntityMap a)
+
+emptyEntityMap :: EntityMap a
+emptyEntityMap = EntityMap mempty mempty 0
+
+newtype Deduplicate a = Deduplicate (EntityMap a)
+  deriving stock (Show, Traversable, Generic)
+  deriving newtype (Eq, Functor, Foldable, EqProp, Arbitrary)
+
+instance Semigroup (Deduplicate a) where
+  (Deduplicate em₁) <> (Deduplicate em₂) =
+    let _byID = em₁ ^. byID <> em₂ ^. byID
+        _byPosition = mempty &~ do
+          ifor_ _byID $ \eid (Positioned pos _) ->
+            at pos %= \case
+              Just eids -> Just $ ninsertSet eid eids
+              Nothing -> Just $ opoint eid
+        _lastID = fromMaybe 1 $ maximumOf (ifolded . asIndex) _byID
+    in Deduplicate EntityMap{..}
+
+
+--------------------------------------------------------------------------------
+
+_EntityMap :: Iso' (EntityMap a) [(Position, a)]
+_EntityMap = iso hither yon
+  where
+    hither :: EntityMap a -> [(Position, a)]
+    hither em = do
+       (pos, eids) <- em ^. byPosition . _Wrapped
+       eid <- toList eids
+       ent <- em ^.. byID . at eid . folded . positioned
+       pure (pos, ent)
+    yon :: [(Position, a)] -> EntityMap a
+    yon poses = alaf Endo foldMap (uncurry insertAt) poses emptyEntityMap
+
+
+insertAtReturningID :: forall a. Position -> a -> EntityMap a -> (EntityID, EntityMap a)
+insertAtReturningID pos e em =
+  let (eid, em') = em & lastID <+~ 1
+  in em'
+     & byID . at eid ?~ Positioned pos e
+     & byPosition . at pos %~ \case
+       Nothing -> Just $ opoint eid
+       Just es -> Just $ ninsertSet eid es
+     & (eid, )
+
+insertAt :: forall a. Position -> a -> EntityMap a -> EntityMap a
+insertAt pos e = snd . insertAtReturningID pos e
+
+atPosition :: forall a. (Ord a, Show a) => Position -> Lens' (EntityMap a) (VectorBag a)
+atPosition pos = lens getter setter
+  where
+    getter em =
+      let eids :: VectorBag EntityID
+          eids = maybe mempty (VectorBag . toVector . toNullable)
+                 $ em ^. byPosition . at pos
+      in getEIDAssume em <$> eids
+    setter em Empty = em & byPosition . at pos .~ Nothing
+    setter em (sort -> entities) =
+      let origEIDs = maybe Empty toNullable $ em ^. byPosition . at pos
+          origEntitiesWithIDs =
+            sortOn snd $ toList origEIDs <&> \eid -> (eid, getEIDAssume em eid)
+          go alles₁@((eid, e₁) :< es₁) -- orig
+             (e₂ :< es₂)               -- new
+            | e₁ == e₂
+              -- same, do nothing
+            = let (eids, lastEID, byID') = go es₁ es₂
+              in (insertSet eid eids, lastEID, byID')
+            | otherwise
+              -- e₂ is new, generate a new ID for it
+            = let (eids, lastEID, byID') = go alles₁ es₂
+                  eid' = succ lastEID
+              in (insertSet eid' eids, eid', byID' & at eid' ?~ Positioned pos e₂)
+          go Empty Empty = (mempty, em ^. lastID, em ^. byID)
+          go orig Empty =
+            let byID' = foldr deleteMap (em ^. byID) $ map fst orig
+            in (mempty, em ^. lastID, byID')
+          go Empty (new :< news) =
+            let (eids, lastEID, byID') = go Empty news
+                eid' = succ lastEID
+            in (insertSet eid' eids, eid', byID' & at eid' ?~ Positioned pos new)
+          go _ _ = error "unreachable"
+          (eidsAtPosition, newLastID, newByID) = go origEntitiesWithIDs entities
+      in em & byPosition . at pos .~ fromNullable eidsAtPosition
+            & byID .~ newByID
+            & lastID .~ newLastID
+
+getEIDAssume :: EntityMap a -> EntityID -> a
+getEIDAssume em eid = fromMaybe byIDInvariantError
+  $ em ^? byID . ix eid . positioned
+
+atPositionWithIDs :: Position -> EntityMap a -> Vector (EntityID, Positioned a)
+atPositionWithIDs pos em =
+  let eids = maybe mempty (toVector . toNullable)
+             $ em ^. byPosition . at pos
+  in (id &&& Positioned pos . getEIDAssume em) <$> eids
+
+fromEIDsAndPositioned
+  :: forall mono a. (MonoFoldable mono, Element mono ~ (EntityID, Positioned a))
+  => mono
+  -> EntityMap a
+fromEIDsAndPositioned eps = newLastID $ alaf Endo foldMap insert' eps mempty
+  where
+    insert' (eid, pe@(Positioned pos _))
+      = (byID . at eid ?~ pe)
+      . (byPosition . at pos %~ \case
+            Just eids -> Just $ ninsertSet eid eids
+            Nothing   -> Just $ opoint eid
+        )
+    newLastID em = em & lastID
+      .~ fromMaybe 1
+         (maximumOf (ifolded . asIndex) (em ^. byID))
+
+toEIDsAndPositioned :: EntityMap a -> [(EntityID, Positioned a)]
+toEIDsAndPositioned = itoListOf $ byID . ifolded
+
+positions :: EntityMap a -> [Position]
+positions = toListOf $ byPosition . to keys . folded
+
+lookupWithPosition :: EntityID -> EntityMap a -> Maybe (Positioned a)
+lookupWithPosition eid = view $ byID . at eid
+
+lookup :: EntityID -> EntityMap a -> Maybe a
+lookup eid = fmap (view positioned) . lookupWithPosition eid
+
+-- unlawful :(
+-- positionedEntities :: IndexedTraversal EntityID (EntityMap a) (EntityMap b) (Positioned a) (Positioned b)
+-- positionedEntities = byID . itraversed
+
+neighbors :: (Ord a, Show a) => Position -> EntityMap a -> Neighbors (VectorBag a)
+neighbors pos em = (\p -> view (atPosition p) em) <$> neighborPositions pos
+
+-- | Traversal to the position of the entity with the given ID
+positionOf :: EntityID -> Traversal' (EntityMap a) Position
+positionOf eid = ix eid . position
+
+--------------------------------------------------------------------------------
+makeWrapped ''Deduplicate
diff --git a/users/grfn/xanthous/src/Xanthous/Data/EntityMap/Graphics.hs b/users/grfn/xanthous/src/Xanthous/Data/EntityMap/Graphics.hs
new file mode 100644
index 0000000000..1398c611cf
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/EntityMap/Graphics.hs
@@ -0,0 +1,72 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntityMap.Graphics
+  ( visiblePositions
+  , visibleEntities
+  , lineOfSight
+  , linesOfSight
+  , canSee
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude hiding (lines)
+--------------------------------------------------------------------------------
+import Xanthous.Util (takeWhileInclusive)
+import Xanthous.Data
+import Xanthous.Data.Entities
+import Xanthous.Data.EntityMap
+import Xanthous.Game.State
+import Xanthous.Util.Graphics (circle, line)
+--------------------------------------------------------------------------------
+
+-- | Returns a set of positions that are visible, when taking into account
+-- 'blocksVision', from the given position, within the given radius.
+visiblePositions
+  :: Entity e
+  => Position
+  -> Word -- ^ Vision radius
+  -> EntityMap e
+  -> Set Position
+visiblePositions pos radius
+  = setFromList . positions . visibleEntities pos radius
+
+-- | Returns a list of entities on the *line of sight* from the first position
+-- to the second position
+lineOfSight
+  :: forall e. Entity e
+  => Position -- ^ Origin
+  -> Position -- ^ Destination
+  -> EntityMap e
+  -> [(Position, Vector (EntityID, e))]
+lineOfSight (view _Position -> origin) (view _Position -> destination) em =
+  takeWhileInclusive (none (view blocksVision . entityAttributes . snd) . snd)
+    $ getPositionedAt <$> line origin destination
+  where
+    getPositionedAt :: V2 Int -> (Position, Vector (EntityID, e))
+    getPositionedAt (review _Position -> p) =
+      (p, over _2 (view positioned) <$> atPositionWithIDs p em)
+
+-- | Returns a list of individual lines of sight, each of which is a list of
+-- entities at positions on that line of sight
+linesOfSight
+  :: forall e. Entity e
+  => Position    -- ^ Centerpoint
+  -> Word        -- ^ Radius
+  -> EntityMap e
+  -> [[(Position, Vector (EntityID, e))]]
+linesOfSight pos visionRadius em =
+  radius <&> \edge -> lineOfSight pos (_Position # edge) em
+  where
+    radius = circle (pos ^. _Position) $ fromIntegral visionRadius
+
+-- | Given a point and a radius of vision, returns a list of all entities that
+-- are *visible* (eg, not blocked by an entity that obscures vision) from that
+-- point
+visibleEntities :: Entity e => Position -> Word -> EntityMap e -> EntityMap e
+visibleEntities pos visionRadius
+  = fromEIDsAndPositioned
+  . foldMap (\(p, es) -> over _2 (Positioned p) <$> es)
+  . fold
+  . linesOfSight pos visionRadius
+
+canSee :: Entity e => (e -> Bool) -> Position -> Word -> EntityMap e -> Bool
+canSee match pos radius = any match . visibleEntities pos radius
+-- ^ this might be optimizable
diff --git a/users/grfn/xanthous/src/Xanthous/Data/Levels.hs b/users/grfn/xanthous/src/Xanthous/Data/Levels.hs
new file mode 100644
index 0000000000..13251d8afd
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/Levels.hs
@@ -0,0 +1,180 @@
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.Levels
+  ( Levels
+  , allLevels
+  , numLevels
+  , nextLevel
+  , prevLevel
+  , mkLevels1
+  , mkLevels
+  , oneLevel
+  , current
+  , ComonadStore(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding ((<.>), Empty, foldMap)
+import           Xanthous.Util (between, EqProp, EqEqProp(..))
+import           Xanthous.Util.Comonad (current)
+import           Xanthous.Orphans ()
+--------------------------------------------------------------------------------
+import           Control.Comonad.Store
+import           Control.Comonad.Store.Zipper
+import           Data.Aeson (ToJSON(..), FromJSON(..))
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Functor.Apply
+import           Data.Foldable (foldMap)
+import           Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NE
+import           Data.Maybe (fromJust)
+import           Data.Sequence (Seq((:<|), Empty))
+import           Data.Semigroup.Foldable.Class
+import           Data.Text (replace)
+import           Test.QuickCheck
+--------------------------------------------------------------------------------
+
+-- | Collection of levels plus a pointer to the current level
+--
+-- Navigation is via the 'Comonad' instance. We can get the current level with
+-- 'extract':
+--
+--     extract @Levels :: Levels level -> level
+--
+-- For access to and modification of the level, use
+-- 'Xanthous.Util.Comonad.current'
+newtype Levels a = Levels { levelZipper :: Zipper Seq a }
+    deriving stock (Generic)
+    deriving (Functor, Comonad, Foldable) via (Zipper Seq)
+
+type instance Element (Levels a) = a
+instance MonoFoldable (Levels a)
+instance MonoFunctor (Levels a)
+instance MonoTraversable (Levels a)
+
+instance ComonadStore Word Levels where
+  pos = toEnum . pos . levelZipper
+  peek i = peek (fromEnum i) . levelZipper
+
+instance Traversable Levels where
+  traverse f (Levels z) = Levels <$> traverse f z
+
+instance Foldable1 Levels
+
+instance Traversable1 Levels where
+  traverse1 f levs@(Levels z) = seek (pos levs) . partialMkLevels <$> go (unzipper z)
+    where
+      go Empty = error "empty seq, unreachable"
+      go (x :<| xs) = (<|) <$> f x <.> go xs
+
+-- | Always takes the position of the latter element
+instance Semigroup (Levels a) where
+  levs₁ <> levs₂
+    = seek (pos levs₂)
+    . partialMkLevels
+    $ allLevels levs₁ <> allLevels levs₂
+
+-- | The number of levels stored in 'Levels'
+--
+-- Equivalent to 'Data.Foldable.length', but likely faster
+numLevels :: Levels a -> Word
+numLevels = toEnum . size . levelZipper
+
+-- | Make Levels from a Seq. Throws an error if the seq is not empty
+partialMkLevels :: Seq a -> Levels a
+partialMkLevels = Levels . fromJust . zipper
+
+-- | Make Levels from a possibly-empty structure
+mkLevels :: Foldable1 f => f level -> Maybe (Levels level)
+mkLevels = fmap Levels . zipper . foldMap pure
+
+-- | Make Levels from a non-empty structure
+mkLevels1 :: Foldable1 f => f level -> Levels level
+mkLevels1 = fromJust . mkLevels
+
+oneLevel :: a -> Levels a
+oneLevel = mkLevels1 . Identity
+
+-- | Get a sequence of all the levels
+allLevels :: Levels a -> Seq a
+allLevels = unzipper . levelZipper
+
+-- | Step to the next level, generating a new level if necessary using the given
+-- applicative action
+nextLevel
+  :: Applicative m
+  => m level -- ^ Generate a new level, if necessary
+  -> Levels level
+  -> m (Levels level)
+nextLevel genLevel levs
+  | succ (pos levs) < numLevels levs
+  = pure $ seeks succ levs
+  | otherwise
+  = genLevel <&> \level ->
+      seek (pos levs + 1) . partialMkLevels $ allLevels levs |> level
+
+-- | Go to the previous level. Returns Nothing if 'pos' is 0
+prevLevel :: Levels level -> Maybe (Levels level)
+prevLevel levs | pos levs == 0 = Nothing
+               | otherwise = Just $ seeks pred levs
+
+--------------------------------------------------------------------------------
+
+-- | alternate, slower representation of Levels we can Iso into to perform
+-- various operations
+data AltLevels a = AltLevels
+  { _levels :: NonEmpty a
+  , _currentLevel :: Word -- ^ invariant: is within the bounds of _levels
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           (AltLevels a)
+makeLenses ''AltLevels
+
+alt :: Iso (Levels a) (Levels b) (AltLevels a) (AltLevels b)
+alt = iso hither yon
+  where
+    hither levs = AltLevels (NE.fromList . toList $ allLevels levs) (pos levs)
+    yon (AltLevels levs curr) = seek curr $ mkLevels1 levs
+
+instance Eq a => Eq (Levels a) where
+  (==) = (==) `on` view alt
+
+deriving via EqEqProp (Levels a) instance Eq a => EqProp (Levels a)
+
+instance Show a => Show (Levels a) where
+  show = unpack . replace "AltLevels" "Levels" . pack . show . view alt
+
+instance NFData a => NFData (Levels a) where
+  rnf = rnf . view alt
+
+instance ToJSON a => ToJSON (Levels a) where
+  toJSON = toJSON . view alt
+
+instance FromJSON a => FromJSON (Levels a) where
+  parseJSON = fmap (review alt) . parseJSON
+
+instance Arbitrary a => Arbitrary (AltLevels a) where
+  arbitrary = do
+    _levels <- arbitrary
+    _currentLevel <- choose (0, pred . toEnum . length $ _levels)
+    pure AltLevels {..}
+  shrink als = do
+    _levels <- shrink $ als ^. levels
+    _currentLevel <- filter (between 0 $ pred . toEnum . length $ _levels)
+                    $ shrink $ als ^. currentLevel
+    pure AltLevels {..}
+
+
+instance Arbitrary a => Arbitrary (Levels a) where
+  arbitrary = review alt <$> arbitrary
+  shrink = fmap (review alt) . shrink . view alt
+
+instance CoArbitrary a => CoArbitrary (Levels a) where
+  coarbitrary = coarbitrary . view alt
+
+instance Function a => Function (Levels a) where
+  function = functionMap (view alt) (review alt)
diff --git a/users/grfn/xanthous/src/Xanthous/Data/Memo.hs b/users/grfn/xanthous/src/Xanthous/Data/Memo.hs
new file mode 100644
index 0000000000..2b2ee0f960
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/Memo.hs
@@ -0,0 +1,98 @@
+--------------------------------------------------------------------------------
+-- | Memoized values
+--------------------------------------------------------------------------------
+module Xanthous.Data.Memo
+  ( Memoized(UnMemoized)
+  , memoizeWith
+  , getMemoized
+  , runMemoized
+  , fillWith
+  , fillWithM
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+import Data.Aeson (FromJSON, ToJSON)
+import Test.QuickCheck (Arbitrary (arbitrary), oneof, CoArbitrary, Function)
+import Test.QuickCheck.Checkers (EqProp)
+import Xanthous.Util (EqEqProp(EqEqProp))
+import Control.Monad.State.Class (MonadState)
+--------------------------------------------------------------------------------
+
+-- | A memoized value, keyed by a key
+--
+-- If key is different than what is stored here, then val is invalid
+data Memoized key val = Memoized key val | UnMemoized
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (Hashable, FromJSON, ToJSON, NFData, CoArbitrary, Function)
+  deriving EqProp via EqEqProp (Memoized key val)
+
+instance (Arbitrary k, Arbitrary v) => Arbitrary (Memoized k v) where
+  arbitrary = oneof [ pure UnMemoized
+                    , Memoized <$> arbitrary <*> arbitrary
+                    ]
+
+-- | Construct a memoized value with the given key
+memoizeWith :: forall key val. key -> val -> Memoized key val
+memoizeWith = Memoized
+{-# INLINE memoizeWith #-}
+
+-- | Retrieve a memoized value providing the key. If the value is unmemoized or
+-- the keys do not match, returns Nothing.
+--
+-- >>> getMemoized 1 (memoizeWith @Int @Int 1 2)
+-- Just 2
+--
+-- >>> getMemoized 2 (memoizeWith @Int @Int 1 2)
+-- Nothing
+--
+-- >>> getMemoized 1 (UnMemoized :: Memoized Int Int)
+-- Nothing
+getMemoized :: Eq key => key -> Memoized key val -> Maybe val
+getMemoized key (Memoized key' v)
+  | key == key' = Just v
+  | otherwise = Nothing
+getMemoized _ UnMemoized = Nothing
+{-# INLINE getMemoized #-}
+
+-- | Get a memoized value using an applicative action to obtain the key
+runMemoized
+  :: (Eq key, Applicative m)
+  => Memoized key val
+  -> m key
+  -> m (Maybe val)
+runMemoized m mk = getMemoized <$> mk <*> pure m
+
+-- | In a monadic state containing a 'MemoState', look up the current memoized
+-- target of some lens keyed by k, filling it with v if not present and
+-- returning either the new or old value
+fillWith
+  :: forall m s k v.
+    (MonadState s m, Eq k)
+  => Lens' s (Memoized k v)
+  -> k
+  -> v
+  -> m v
+fillWith l k v' = do
+  uses l (getMemoized k) >>= \case
+    Just v -> pure v
+    Nothing -> do
+      l .= memoizeWith k v'
+      pure v'
+
+-- | In a monadic state, look up the current memoized target of some lens keyed
+-- by k, filling it with the result of some monadic action v if not present and
+-- returning either the new or old value
+fillWithM
+  :: forall m s k v.
+    (MonadState s m, Eq k)
+  => Lens' s (Memoized k v)
+  -> k
+  -> m v
+  -> m v
+fillWithM l k mv = do
+  uses l (getMemoized k) >>= \case
+    Just v -> pure v
+    Nothing -> do
+      v' <- mv
+      l .= memoizeWith k v'
+      pure v'
diff --git a/users/grfn/xanthous/src/Xanthous/Data/NestedMap.hs b/users/grfn/xanthous/src/Xanthous/Data/NestedMap.hs
new file mode 100644
index 0000000000..1b875d4483
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/NestedMap.hs
@@ -0,0 +1,227 @@
+{-# LANGUAGE PartialTypeSignatures #-}
+{-# LANGUAGE UndecidableInstances  #-}
+{-# LANGUAGE QuantifiedConstraints #-}
+{-# LANGUAGE StandaloneDeriving    #-}
+{-# LANGUAGE PolyKinds             #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.NestedMap
+  ( NestedMapVal(..)
+  , NestedMap(..)
+  , lookup
+  , lookupVal
+  , insert
+
+    -- *
+  , (:->)
+  , BifunctorFunctor'(..)
+  , BifunctorMonad'(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (lookup, foldMap)
+import qualified Xanthous.Prelude as P
+--------------------------------------------------------------------------------
+import           Test.QuickCheck
+import           Data.Aeson
+import           Data.Function (fix)
+import           Data.Foldable (Foldable(..))
+import           Data.List.NonEmpty (NonEmpty(..))
+import qualified Data.List.NonEmpty as NE
+--------------------------------------------------------------------------------
+
+-- | Natural transformations on bifunctors
+type (:->) p q = forall a b. p a b -> q a b
+infixr 0 :->
+
+class (forall b. Bifunctor b => Bifunctor (t b)) => BifunctorFunctor' t where
+  bifmap' :: (Bifunctor p, Bifunctor q) => (p :-> q) -> t p :-> t q
+
+class BifunctorFunctor' t => BifunctorMonad' t where
+  bireturn' :: (Bifunctor p) => p :-> t p
+
+  bibind' :: (Bifunctor p, Bifunctor q) => (p :-> t q) -> t p :-> t q
+  bibind' f = bijoin' . bifmap' f
+
+  bijoin' :: (Bifunctor p) => t (t p) :-> t p
+  bijoin' = bibind' id
+
+  {-# MINIMAL bireturn', (bibind' | bijoin') #-}
+
+--------------------------------------------------------------------------------
+
+data NestedMapVal m k v = Val v | Nested (NestedMap m k v)
+
+deriving stock instance
+  ( forall k' v'. (Show k', Show v') => Show (m k' v')
+  , Show k
+  , Show v
+  ) => Show (NestedMapVal m k v)
+
+deriving stock instance
+  ( forall k' v'. (Eq k', Eq v') => Eq (m k' v')
+  , Eq k
+  , Eq v
+  ) => Eq (NestedMapVal m k v)
+
+instance
+  forall m k v.
+  ( Arbitrary (m k v)
+  , Arbitrary (m k (NestedMapVal m k v))
+  , Arbitrary k
+  , Arbitrary v
+  , IsMap (m k (NestedMapVal m k v))
+  , MapValue (m k (NestedMapVal m k v)) ~ (NestedMapVal m k v)
+  , ContainerKey (m k (NestedMapVal m k v)) ~ k
+  ) => Arbitrary (NestedMapVal m k v) where
+  arbitrary = sized . fix $ \gen n ->
+    let nst = fmap (NestedMap . mapFromList)
+            . listOf
+            $ (,) <$> arbitrary @k <*> gen (n `div` 2)
+    in if n == 0
+       then Val <$> arbitrary
+       else oneof [ Val <$> arbitrary
+                  , Nested <$> nst]
+  shrink (Val v) = Val <$> shrink v
+  shrink (Nested mkv) = Nested <$> shrink mkv
+
+instance Functor (m k) => Functor (NestedMapVal m k) where
+  fmap f (Val v) = Val $ f v
+  fmap f (Nested m) = Nested $ fmap f m
+
+instance Bifunctor m => Bifunctor (NestedMapVal m) where
+  bimap _ g (Val v) = Val $ g v
+  bimap f g (Nested m) = Nested $ bimap f g m
+
+instance BifunctorFunctor' NestedMapVal where
+  bifmap' _ (Val v) = Val v
+  bifmap' f (Nested m) = Nested $ bifmap' f m
+
+instance (ToJSONKey k, ToJSON v, ToJSON (m k (NestedMapVal m k v)))
+       => ToJSON (NestedMapVal m k v) where
+  toJSON (Val v) = toJSON v
+  toJSON (Nested m) = toJSON m
+
+instance Foldable (m k) => Foldable (NestedMapVal m k) where
+  foldMap f (Val v) = f v
+  foldMap f (Nested m) = foldMap f m
+
+-- _NestedMapVal
+--   :: forall m k v m' k' v'.
+--     ( IsMap (m k v), IsMap (m' k' v')
+--     , IsMap (m [k] v), IsMap (m' [k'] v')
+--     , ContainerKey (m k v) ~ k, ContainerKey (m' k' v') ~ k'
+--     , ContainerKey (m [k] v) ~ [k], ContainerKey (m' [k'] v') ~ [k']
+--     , MapValue (m k v) ~ v, MapValue (m' k' v') ~ v'
+--     , MapValue (m [k] v) ~ v, MapValue (m' [k'] v') ~ v'
+--     )
+--   => Iso (NestedMapVal m k v)
+--         (NestedMapVal m' k' v')
+--         (m [k] v)
+--         (m' [k'] v')
+-- _NestedMapVal = iso hither yon
+--   where
+--     hither :: NestedMapVal m k v -> m [k] v
+--     hither (Val v) = singletonMap [] v
+--     hither (Nested m) = bimap _ _ $ m ^. _NestedMap
+--     yon = _
+
+--------------------------------------------------------------------------------
+
+newtype NestedMap m k v = NestedMap (m k (NestedMapVal m k v))
+
+deriving stock instance
+  ( forall k' v'. (Eq k', Eq v') => Eq (m k' v')
+  , Eq k
+  , Eq v
+  ) => Eq (NestedMap m k v)
+
+deriving stock instance
+  ( forall k' v'. (Show k', Show v') => Show (m k' v')
+  , Show k
+  , Show v
+  ) => Show (NestedMap m k v)
+
+instance Arbitrary (m k (NestedMapVal m k v))
+       => Arbitrary (NestedMap m k v) where
+  arbitrary = NestedMap <$> arbitrary
+  shrink (NestedMap m) = NestedMap <$> shrink m
+
+instance Functor (m k) => Functor (NestedMap m k) where
+  fmap f (NestedMap m) = NestedMap $ fmap (fmap f) m
+
+instance Bifunctor m => Bifunctor (NestedMap m) where
+  bimap f g (NestedMap m) = NestedMap $ bimap f (bimap f g) m
+
+instance BifunctorFunctor' NestedMap where
+  bifmap' f (NestedMap m) = NestedMap . f $ bimap id (bifmap' f) m
+
+instance (ToJSONKey k, ToJSON v, ToJSON (m k (NestedMapVal m k v)))
+       => ToJSON (NestedMap m k v) where
+  toJSON (NestedMap m) = toJSON m
+
+instance Foldable (m k) => Foldable (NestedMap m k) where
+  foldMap f (NestedMap m) = foldMap (foldMap f) m
+
+--------------------------------------------------------------------------------
+
+lookup
+  :: ( IsMap (m k (NestedMapVal m k v))
+    , MapValue (m k (NestedMapVal m k v)) ~ (NestedMapVal m k v)
+    , ContainerKey (m k (NestedMapVal m k v)) ~ k
+    )
+  => NonEmpty k
+  -> NestedMap m k v
+  -> Maybe (NestedMapVal m k v)
+lookup (p :| []) (NestedMap vs) = P.lookup p vs
+lookup (p :| (p₁ : ps)) (NestedMap vs) = P.lookup p vs >>= \case
+  (Val _) -> Nothing
+  (Nested vs') -> lookup (p₁ :| ps) vs'
+
+lookupVal
+  :: ( IsMap (m k (NestedMapVal m k v))
+    , MapValue (m k (NestedMapVal m k v)) ~ (NestedMapVal m k v)
+    , ContainerKey (m k (NestedMapVal m k v)) ~ k
+    )
+  => NonEmpty k
+  -> NestedMap m k v
+  -> Maybe v
+lookupVal ks m
+  | Just (Val v) <- lookup ks m = Just v
+  | otherwise                  = Nothing
+
+insert
+  :: ( IsMap (m k (NestedMapVal m k v))
+    , MapValue (m k (NestedMapVal m k v)) ~ (NestedMapVal m k v)
+    , ContainerKey (m k (NestedMapVal m k v)) ~ k
+    )
+  => NonEmpty k
+  -> v
+  -> NestedMap m k v
+  -> NestedMap m k v
+insert (k :| []) v (NestedMap m) = NestedMap $ P.insertMap k (Val v) m
+insert (k₁ :| (k₂ : ks)) v (NestedMap m) = NestedMap $ alterMap upd k₁ m
+  where
+    upd (Just (Nested nm)) = Just . Nested $ insert (k₂ :| ks) v nm
+    upd _ = Just $
+      let (kΩ :| ks') = NE.reverse (k₂ :| ks)
+      in P.foldl'
+         (\m' k -> Nested . NestedMap . singletonMap k $ m')
+         (Nested . NestedMap . singletonMap kΩ $ Val v)
+         ks'
+
+-- _NestedMap
+--   :: ( IsMap (m k v), IsMap (m' k' v')
+--     , IsMap (m (NonEmpty k) v), IsMap (m' (NonEmpty k') v')
+--     , ContainerKey (m k v) ~ k, ContainerKey (m' k' v') ~ k'
+--     , ContainerKey (m (NonEmpty k) v) ~ (NonEmpty k)
+--     , ContainerKey (m' (NonEmpty k') v') ~ (NonEmpty k')
+--     , MapValue (m k v) ~ v, MapValue (m' k' v') ~ v'
+--     , MapValue (m (NonEmpty k) v) ~ v, MapValue (m' (NonEmpty k') v') ~ v'
+--     )
+--   => Iso (NestedMap m k v)
+--         (NestedMap m' k' v')
+--         (m (NonEmpty k) v)
+--         (m' (NonEmpty k') v')
+-- _NestedMap = iso undefined yon
+--   where
+--     hither (NestedMap m) = undefined . mapToList $ m
+--     yon mkv = undefined
diff --git a/users/grfn/xanthous/src/Xanthous/Data/VectorBag.hs b/users/grfn/xanthous/src/Xanthous/Data/VectorBag.hs
new file mode 100644
index 0000000000..2e6d48062a
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data/VectorBag.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.VectorBag
+  (VectorBag(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Data.Aeson
+import qualified Data.Vector as V
+import           Test.QuickCheck
+import           Test.QuickCheck.Instances.Vector ()
+--------------------------------------------------------------------------------
+
+-- | Acts exactly like a Vector, except ignores order when testing for equality
+newtype VectorBag a = VectorBag (Vector a)
+  deriving stock
+    ( Traversable
+    , Generic
+    )
+  deriving newtype
+    ( Show
+    , Read
+    , Foldable
+    , FromJSON
+    , FromJSON1
+    , ToJSON
+    , Reversing
+    , Applicative
+    , Functor
+    , Monad
+    , Monoid
+    , Semigroup
+    , Arbitrary
+    , CoArbitrary
+    , Filterable
+    )
+makeWrapped ''VectorBag
+
+instance Function a => Function (VectorBag a) where
+  function = functionMap (\(VectorBag v) -> v) VectorBag
+
+type instance Element (VectorBag a) = a
+deriving via (Vector a) instance MonoFoldable (VectorBag a)
+deriving via (Vector a) instance GrowingAppend (VectorBag a)
+deriving via (Vector a) instance SemiSequence (VectorBag a)
+deriving via (Vector a) instance MonoPointed (VectorBag a)
+deriving via (Vector a) instance MonoFunctor (VectorBag a)
+
+instance Cons (VectorBag a) (VectorBag b) a b where
+  _Cons = prism (\(x, VectorBag xs) -> VectorBag $ x <| xs) $ \(VectorBag v) ->
+    if V.null v
+    then Left (VectorBag mempty)
+    else Right (V.unsafeHead v, VectorBag $ V.unsafeTail v)
+
+instance AsEmpty (VectorBag a) where
+  _Empty = prism' (const $ VectorBag Empty) $ \case
+    (VectorBag Empty) -> Just ()
+    _ -> Nothing
+
+instance Witherable VectorBag where
+  wither f (VectorBag v) = VectorBag <$> wither f v
+  witherM f (VectorBag v) = VectorBag <$> witherM f v
+  filterA p (VectorBag v) = VectorBag <$> filterA p v
+
+{-
+    TODO:
+    , Ixed
+    , FoldableWithIndex
+    , FunctorWithIndex
+    , TraversableWithIndex
+    , Snoc
+    , Each
+-}
+
+instance Ord a => Eq (VectorBag a) where
+  (==) = (==) `on` (view _Wrapped . sort)
+
+instance Ord a => Ord (VectorBag a) where
+  compare = compare  `on` (view _Wrapped . sort)
+
+instance MonoTraversable (VectorBag a) where
+  otraverse f (VectorBag v) = VectorBag <$> otraverse f v
+
+instance IsSequence (VectorBag a) where
+  fromList = VectorBag . fromList
+  break prd (VectorBag v) = bimap VectorBag VectorBag $ break prd v
+  span prd (VectorBag v) = bimap VectorBag VectorBag $ span prd v
+  dropWhile prd (VectorBag v) = VectorBag $ dropWhile prd v
+  takeWhile prd (VectorBag v) = VectorBag $ takeWhile prd v
+  splitAt idx (VectorBag v) = bimap VectorBag VectorBag $ splitAt idx v
+  unsafeSplitAt idx (VectorBag v) =
+    bimap VectorBag VectorBag $ unsafeSplitAt idx v
+  take n (VectorBag v) = VectorBag $ take n v
+  unsafeTake n (VectorBag v) = VectorBag $ unsafeTake n v
+  drop n (VectorBag v) = VectorBag $ drop n v
+  unsafeDrop n (VectorBag v) = VectorBag $ unsafeDrop n v
+  partition p (VectorBag v) = bimap VectorBag VectorBag $ partition p v
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Character.hs b/users/grfn/xanthous/src/Xanthous/Entities/Character.hs
new file mode 100644
index 0000000000..c8153086f1
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Character.hs
@@ -0,0 +1,241 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Character
+
+  ( -- * Character datatype
+    Character(..)
+  , characterName
+  , HasInventory(..)
+  , characterDamage
+  , characterHitpoints'
+  , characterHitpoints
+  , hitpointRecoveryRate
+  , speed
+  , body
+
+    -- *** Body
+  , Body(..)
+  , initialBody
+  , knuckles
+  , Knuckles(..)
+  , fistDamageChance
+  , damageKnuckles
+  , fistfightingDamage
+
+    -- * Character functions
+  , mkCharacter
+  , pickUpItem
+  , isDead
+  , isFullyHealed
+  , damage
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Brick
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Aeson (ToJSON, FromJSON)
+import           Data.Coerce (coerce)
+import           Test.QuickCheck
+import           Test.QuickCheck.Instances.Vector ()
+import           Test.QuickCheck.Arbitrary.Generic
+import           Test.QuickCheck.Gen (chooseUpTo)
+import           Test.QuickCheck.Checkers (EqProp)
+import           Control.Monad.State.Lazy (execState)
+import           Control.Monad.Trans.State.Lazy (execStateT)
+--------------------------------------------------------------------------------
+import           Xanthous.Game.State
+import           Xanthous.Entities.Item
+import           Xanthous.Entities.Common
+import           Xanthous.Data
+                 ( TicksPerTile, Hitpoints, Per, Ticks, (|*|), positioned )
+import qualified Xanthous.Entities.RawTypes as Raw
+import           Xanthous.Util (EqEqProp(EqEqProp), modifyKL)
+import           Xanthous.Monad (say_)
+--------------------------------------------------------------------------------
+
+-- | The status of the character's knuckles
+--
+-- This struct is used to track the damage and then eventual build-up of
+-- calluses when the character is fighting with their fists
+data Knuckles = Knuckles
+  { -- | How damaged are the knuckles currently, from 0 to 5?
+    --
+    -- At 0, no calluses will form
+    -- At 1 and up, the character will form calluses after a while
+    -- At 5, continuing to fistfight will deal the character even more damage
+    _knuckleDamage   :: !Word
+    -- | How built-up are the character's calluses, from 0 to 5?
+    --
+    -- Each level of calluses decreases the likelihood of being damaged when
+    -- fistfighting by 1%, up to 5 where the character will never be damaged
+    -- fistfighting
+  , _knuckleCalluses :: !Word
+
+    -- | Number of turns that have passed since the last time the knuckles were
+    -- damaged
+  , _ticksSinceDamaged :: Ticks
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving EqProp via EqEqProp Knuckles
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           Knuckles
+makeLenses ''Knuckles
+
+instance Semigroup Knuckles where
+  (Knuckles d₁ c₁ t₁) <> (Knuckles d₂ c₂ t₂) = Knuckles
+    (min (d₁ + d₂) 5)
+    (min (c₁ + c₂) 5)
+    (max t₁ t₂)
+
+instance Monoid Knuckles where
+  mempty = Knuckles 0 0 0
+
+instance Arbitrary Knuckles where
+  arbitrary = do
+    _knuckleDamage <- fromIntegral <$> chooseUpTo 5
+    _knuckleCalluses <- fromIntegral <$> chooseUpTo 5
+    _ticksSinceDamaged <- arbitrary
+    pure Knuckles{..}
+
+-- | Likelihood that the character fighting with their fists will damage
+-- themselves
+fistDamageChance :: Knuckles -> Float
+fistDamageChance knuckles
+  | calluses == 5 = 0
+  | otherwise = baseChance - (0.01 * fromIntegral calluses)
+  where
+    baseChance = 0.08
+    calluses = knuckles ^. knuckleCalluses
+
+-- | Damage the knuckles by a level (capping at the max knuckle damage)
+damageKnuckles :: Knuckles -> Knuckles
+damageKnuckles = execState $ do
+  knuckleDamage %= min 5 . succ
+  ticksSinceDamaged .= 0
+
+-- | Damage taken when fistfighting and 'fistDamageChance' has occurred
+fistfightingDamage :: Knuckles -> Hitpoints
+fistfightingDamage knuckles
+  | knuckles ^. knuckleDamage == 5 = 2
+  | otherwise = 1
+
+stepKnuckles :: Ticks -> Knuckles -> AppM Knuckles
+stepKnuckles ticks = execStateT . whenM (uses knuckleDamage (> 0)) $ do
+  ticksSinceDamaged += ticks
+  whenM (uses ticksSinceDamaged (>= 2000)) $ do
+    dam <- knuckleDamage <<.= 0
+    knuckleCalluses %= min 5 . (+ dam)
+    ticksSinceDamaged .= 0
+    lift $ say_ ["character", "body", "knuckles", "calluses"]
+
+
+-- | Status of the character's body
+data Body = Body
+  { _knuckles :: !Knuckles
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Body
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           Body
+makeLenses ''Body
+
+initialBody :: Body
+initialBody = Body { _knuckles = mempty }
+
+--------------------------------------------------------------------------------
+
+data Character = Character
+  { _inventory           :: !Inventory
+  , _characterName       :: !(Maybe Text)
+  , _characterHitpoints' :: !Double
+  , _speed               :: !TicksPerTile
+  , _body                :: !Body
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           Character
+makeFieldsNoPrefix ''Character
+
+characterHitpoints :: Character -> Hitpoints
+characterHitpoints = views characterHitpoints' floor
+
+scrollOffset :: Int
+scrollOffset = 5
+
+instance Draw Character where
+  draw _ = visibleRegion rloc rreg $ str "@"
+    where
+      rloc = Location (negate scrollOffset, negate scrollOffset)
+      rreg = (2 * scrollOffset, 2 * scrollOffset)
+  drawPriority = const maxBound -- Character should always be on top, for now
+
+instance Brain Character where
+  step ticks = execStateT $ do
+    positioned . characterHitpoints' %= \hp ->
+      if hp > fromIntegral initialHitpoints
+      then hp
+      else hp + hitpointRecoveryRate |*| ticks
+    modifyKL (positioned . body . knuckles) $ lift . stepKnuckles ticks
+
+instance Entity Character where
+  description _ = "yourself"
+  entityChar _ = "@"
+
+instance Arbitrary Character where
+  arbitrary = genericArbitrary
+
+initialHitpoints :: Hitpoints
+initialHitpoints = 10
+
+hitpointRecoveryRate :: Double `Per` Ticks
+hitpointRecoveryRate = 1.0 / (15 * coerce defaultSpeed)
+
+defaultSpeed :: TicksPerTile
+defaultSpeed = 100
+
+mkCharacter :: Character
+mkCharacter = Character
+  { _inventory           = mempty
+  , _characterName       = Nothing
+  , _characterHitpoints' = fromIntegral initialHitpoints
+  , _speed               = defaultSpeed
+  , _body                = initialBody
+  }
+
+defaultCharacterDamage :: Hitpoints
+defaultCharacterDamage = 1
+
+-- | Returns the damage that the character currently does with an attack
+-- TODO use double-handed/left-hand/right-hand here
+characterDamage :: Character -> Hitpoints
+characterDamage
+  = fromMaybe defaultCharacterDamage
+  . filter (/= 0)
+  . Just
+  . sumOf (inventory . wielded . wieldedItems . wieldableItem . Raw.damage)
+
+-- | Is the character fully healed up to or past their initial hitpoints?
+isFullyHealed :: Character -> Bool
+isFullyHealed = (>= initialHitpoints) . characterHitpoints
+
+-- | Is the character dead?
+isDead :: Character -> Bool
+isDead = (== 0) . characterHitpoints
+
+pickUpItem :: Item -> Character -> Character
+pickUpItem it = inventory . backpack %~ (it <|)
+
+damage :: Hitpoints -> Character -> Character
+damage (fromIntegral -> amount) = characterHitpoints' %~ \case
+  n | n <= amount -> 0
+    | otherwise  -> n - amount
+
+{-# ANN module ("Hlint: ignore Use newtype instead of data" :: String) #-}
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Common.hs b/users/grfn/xanthous/src/Xanthous/Entities/Common.hs
new file mode 100644
index 0000000000..368b03f25b
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Common.hs
@@ -0,0 +1,290 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+-- |
+-- Module      : Xanthous.Entities.Common
+-- Description : Common data type definitions and utilities for entities
+--
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Common
+  ( -- * Inventory
+    Inventory(..)
+  , HasInventory(..)
+  , backpack
+  , wielded
+  , items
+  , InventoryPosition(..)
+  , describeInventoryPosition
+  , inventoryPosition
+  , itemsWithPosition
+  , removeItemFromPosition
+
+    -- ** Wielded items
+  , Wielded(..)
+  , nothingWielded
+  , hands
+  , leftHand
+  , rightHand
+  , inLeftHand
+  , inRightHand
+  , doubleHanded
+  , Hand(..)
+  , itemsInHand
+  , inHand
+  , wieldInHand
+  , describeHand
+  , wieldedItems
+  , WieldedItem(..)
+  , wieldedItem
+  , wieldableItem
+  , asWieldedItem
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.Aeson (ToJSON, FromJSON)
+import           Data.Aeson.Generic.DerivingVia
+import           Test.QuickCheck
+import           Test.QuickCheck.Checkers (EqProp)
+--------------------------------------------------------------------------------
+import           Xanthous.Data (Positioned(..), positioned)
+import           Xanthous.Util.QuickCheck
+import           Xanthous.Game.State
+import           Xanthous.Entities.Item
+import           Xanthous.Entities.RawTypes (WieldableItem, wieldable)
+import           Xanthous.Util (removeFirst, EqEqProp(..))
+--------------------------------------------------------------------------------
+
+data WieldedItem = WieldedItem
+  { _wieldedItem :: Item
+  , _wieldableItem :: WieldableItem
+    -- ^ Invariant: item ^. itemType . wieldable ≡ Just wieldableItem
+  }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           WieldedItem
+makeFieldsNoPrefix ''WieldedItem
+
+asWieldedItem :: Prism' Item WieldedItem
+asWieldedItem = prism' hither yon
+ where
+   yon item = WieldedItem item <$> item ^. itemType . wieldable
+   hither (WieldedItem item _) = item
+
+instance Brain WieldedItem where
+  step ticks (Positioned p wi) =
+    over positioned (\i -> WieldedItem i $ wi ^. wieldableItem)
+    <$> step ticks (Positioned p $ wi ^. wieldedItem)
+
+instance Draw WieldedItem where
+  draw = draw . view wieldedItem
+
+instance Entity WieldedItem where
+  entityAttributes = entityAttributes . view wieldedItem
+  description = description . view wieldedItem
+  entityChar = entityChar . view wieldedItem
+
+instance Arbitrary WieldedItem where
+  arbitrary = genericArbitrary <&> \wi ->
+    wi & wieldedItem . itemType . wieldable ?~ wi ^. wieldableItem
+
+data Wielded
+  = DoubleHanded WieldedItem
+  | Hands { _leftHand :: !(Maybe WieldedItem)
+          , _rightHand :: !(Maybe WieldedItem)
+          }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Wielded
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ 'SumEnc 'ObjWithSingleField ]
+           Wielded
+
+
+nothingWielded :: Wielded
+nothingWielded = Hands Nothing Nothing
+
+hands :: Prism' Wielded (Maybe WieldedItem, Maybe WieldedItem)
+hands = prism' (uncurry Hands) $ \case
+  Hands l r -> Just (l, r)
+  _ -> Nothing
+
+leftHand :: Traversal' Wielded (Maybe WieldedItem)
+leftHand = hands . _1
+
+inLeftHand :: WieldedItem -> Wielded
+inLeftHand wi = Hands (Just wi) Nothing
+
+rightHand :: Traversal' Wielded (Maybe WieldedItem)
+rightHand = hands . _2
+
+inRightHand :: WieldedItem -> Wielded
+inRightHand wi = Hands Nothing (Just wi)
+
+doubleHanded :: Prism' Wielded WieldedItem
+doubleHanded = prism' DoubleHanded $ \case
+  DoubleHanded i -> Just i
+  _ -> Nothing
+
+wieldedItems :: Traversal' Wielded WieldedItem
+wieldedItems k (DoubleHanded wielded) = DoubleHanded <$> k wielded
+wieldedItems k (Hands l r) = Hands <$> _Just k l <*> _Just k r
+
+
+data Hand
+  = LeftHand
+  | RightHand
+  | BothHands
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Hand
+
+itemsInHand :: Hand -> Wielded -> [WieldedItem]
+itemsInHand LeftHand (DoubleHanded wi) = [wi]
+itemsInHand LeftHand (Hands lh _) = toList lh
+itemsInHand RightHand (DoubleHanded wi) = [wi]
+itemsInHand RightHand (Hands _ rh) = toList rh
+itemsInHand BothHands (DoubleHanded wi) = [wi]
+itemsInHand BothHands (Hands lh rh) = toList lh <> toList rh
+
+inHand :: Hand -> WieldedItem -> Wielded
+inHand LeftHand = inLeftHand
+inHand RightHand = inRightHand
+inHand BothHands = review doubleHanded
+
+wieldInHand :: Hand -> WieldedItem -> Wielded -> ([WieldedItem], Wielded)
+wieldInHand hand item w = (itemsInHand hand w, doWield)
+  where
+    doWield = case (hand, w) of
+      (LeftHand, Hands _ r) -> Hands (Just item) r
+      (LeftHand, DoubleHanded _) -> inLeftHand item
+      (RightHand, Hands l _) -> Hands l (Just item)
+      (RightHand, DoubleHanded _) -> inRightHand item
+      (BothHands, _) -> DoubleHanded item
+
+describeHand :: Hand -> Text
+describeHand LeftHand = "your left hand"
+describeHand RightHand = "your right hand"
+describeHand BothHands = "both hands"
+
+data Inventory = Inventory
+  { _backpack :: Vector Item
+  , _wielded :: Wielded
+  }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Inventory
+  deriving EqProp via EqEqProp Inventory
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           Inventory
+makeFieldsNoPrefix ''Inventory
+
+items :: Traversal' Inventory Item
+items k (Inventory bp w) = Inventory
+  <$> traversed k bp
+  <*> (wieldedItems . wieldedItem) k w
+
+type instance Element Inventory = Item
+
+instance MonoFunctor Inventory where
+  omap = over items
+
+instance MonoFoldable Inventory where
+  ofoldMap = foldMapOf items
+  ofoldr = foldrOf items
+  ofoldl' = foldlOf' items
+  otoList = toListOf items
+  oall = allOf items
+  oany = anyOf items
+  onull = nullOf items
+  ofoldr1Ex = foldr1Of items
+  ofoldl1Ex' = foldl1Of' items
+  headEx = headEx . toListOf items
+  lastEx = lastEx . toListOf items
+
+instance MonoTraversable Inventory where
+  otraverse = traverseOf items
+
+instance Semigroup Inventory where
+  inv₁ <> inv₂ =
+    let backpack' = inv₁ ^. backpack <> inv₂ ^. backpack
+        (wielded', backpack'') = case (inv₁ ^. wielded, inv₂ ^. wielded) of
+          (wielded₁, wielded₂@(DoubleHanded _)) ->
+            (wielded₂, backpack' <> fromList (wielded₁ ^.. wieldedItems . wieldedItem))
+          (wielded₁, wielded₂@(Hands (Just _) (Just _))) ->
+            (wielded₂, backpack' <> fromList (wielded₁ ^.. wieldedItems . wieldedItem))
+          (wielded₁, Hands Nothing Nothing) -> (wielded₁, backpack')
+          (Hands Nothing Nothing, wielded₂) -> (wielded₂, backpack')
+          (Hands (Just l₁) Nothing, Hands Nothing (Just r₂)) ->
+            (Hands (Just l₁) (Just r₂), backpack')
+          (wielded₁@(DoubleHanded _), wielded₂) ->
+            (wielded₁, backpack' <> fromList (wielded₂ ^.. wieldedItems . wieldedItem))
+          (Hands Nothing (Just r₁), Hands Nothing (Just r₂)) ->
+            (Hands Nothing (Just r₂), r₁ ^. wieldedItem <| backpack')
+          (Hands Nothing r₁, Hands (Just l₂) Nothing) ->
+            (Hands (Just l₂) r₁, backpack')
+          (Hands (Just l₁) Nothing, Hands (Just l₂) Nothing) ->
+            (Hands (Just l₂) Nothing, l₁ ^. wieldedItem <| backpack')
+          (Hands (Just l₁) (Just r₁), Hands Nothing (Just r₂)) ->
+            (Hands (Just l₁) (Just r₂), r₁ ^. wieldedItem <| backpack')
+          (Hands (Just l₁) (Just r₁), Hands (Just l₂) Nothing) ->
+            (Hands (Just l₂) (Just r₁), l₁ ^. wieldedItem <| backpack')
+    in Inventory backpack'' wielded'
+
+instance Monoid Inventory where
+  mempty = Inventory mempty $ Hands Nothing Nothing
+
+class HasInventory s a | s -> a where
+  inventory :: Lens' s a
+  {-# MINIMAL inventory #-}
+
+-- | Representation for where in the inventory an item might be
+data InventoryPosition
+  = Backpack
+  | InHand Hand
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary InventoryPosition
+
+-- | Return a human-readable description of the given 'InventoryPosition'
+describeInventoryPosition :: InventoryPosition -> Text
+describeInventoryPosition Backpack       = "In backpack"
+describeInventoryPosition (InHand hand)  = "Wielded, in " <> describeHand hand
+
+-- | Given a position in the inventory, return a traversal on the inventory over
+-- all the items in that position
+inventoryPosition :: InventoryPosition -> Traversal' Inventory Item
+inventoryPosition Backpack = backpack . traversed
+inventoryPosition (InHand LeftHand) = wielded . leftHand . _Just . wieldedItem
+inventoryPosition (InHand RightHand) = wielded . leftHand . _Just . wieldedItem
+inventoryPosition (InHand BothHands) = wielded . doubleHanded . wieldedItem
+
+-- | A fold over all the items in the inventory accompanied by their position in
+-- the inventory
+--
+-- Invariant: This will return items in the same order as 'items'
+itemsWithPosition :: Fold Inventory (InventoryPosition, Item)
+itemsWithPosition = folding $ (<>) <$> backpackItems <*> handItems
+  where
+    backpackItems = toListOf $ backpack . folded . to (Backpack ,)
+    handItems inv = case inv ^. wielded of
+       DoubleHanded i -> pure (InHand BothHands, i ^. wieldedItem)
+       Hands l r -> (l ^.. folded . wieldedItem . to (InHand LeftHand ,))
+                 <> (r ^.. folded . wieldedItem . to (InHand RightHand ,))
+
+-- | Remove the first item equal to 'Item' from the given position in the
+-- inventory
+removeItemFromPosition :: InventoryPosition -> Item -> Inventory -> Inventory
+removeItemFromPosition Backpack item inv
+  = inv & backpack %~ removeFirst (== item)
+removeItemFromPosition (InHand LeftHand) item inv
+  = inv & wielded . leftHand %~ filter ((/= item) . view wieldedItem)
+removeItemFromPosition (InHand RightHand) item inv
+  = inv & wielded . rightHand %~ filter ((/= item) . view wieldedItem)
+removeItemFromPosition (InHand BothHands) item inv
+  | has (wielded . doubleHanded . wieldedItem . filtered (== item)) inv
+  = inv & wielded .~ nothingWielded
+  | otherwise
+  = inv
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Creature.hs b/users/grfn/xanthous/src/Xanthous/Entities/Creature.hs
new file mode 100644
index 0000000000..3ea610795e
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Creature.hs
@@ -0,0 +1,88 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Creature
+  ( -- * Creature
+    Creature(..)
+    -- ** Lenses
+  , creatureType
+  , hitpoints
+  , hippocampus
+  , inventory
+
+    -- ** Creature functions
+  , damage
+  , isDead
+  , visionRadius
+
+    -- * Hippocampus
+  , Hippocampus(..)
+    -- ** Lenses
+  , destination
+    -- ** Destination
+  , Destination(..)
+  , destinationFromPos
+    -- *** Lenses
+  , destinationPosition
+  , destinationProgress
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Test.QuickCheck
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Aeson (ToJSON, FromJSON)
+--------------------------------------------------------------------------------
+import           Xanthous.AI.Gormlak
+import           Xanthous.Entities.RawTypes hiding
+                 (Creature, description, damage)
+import qualified Xanthous.Entities.RawTypes as Raw
+import           Xanthous.Game.State
+import           Xanthous.Data
+import           Xanthous.Data.Entities
+import           Xanthous.Entities.Creature.Hippocampus
+import           Xanthous.Util.QuickCheck (GenericArbitrary(..))
+import           Xanthous.Entities.Common (Inventory, HasInventory(..))
+--------------------------------------------------------------------------------
+
+data Creature = Creature
+  { _creatureType   :: !CreatureType
+  , _hitpoints      :: !Hitpoints
+  , _hippocampus    :: !Hippocampus
+  , _inventory      :: !Inventory
+  }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Draw via DrawRawCharPriority "_creatureType" 1000 Creature
+  deriving Arbitrary via GenericArbitrary Creature
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       Creature
+makeFieldsNoPrefix ''Creature
+
+instance HasVisionRadius Creature where
+  visionRadius = const 50 -- TODO
+
+instance Brain Creature where
+  step = brainVia GormlakBrain
+  entityCanMove = const True
+
+instance Entity Creature where
+  entityAttributes _ = defaultEntityAttributes
+    & blocksObject .~ True
+  description = view $ creatureType . Raw.description
+  entityChar = view $ creatureType . char
+  entityCollision = const $ Just Combat
+
+--------------------------------------------------------------------------------
+
+damage :: Hitpoints -> Creature -> Creature
+damage amount = hitpoints %~ \hp ->
+  if hp <= amount
+  then 0
+  else hp - amount
+
+isDead :: Creature -> Bool
+isDead = views hitpoints (== 0)
+
+{-# ANN module ("Hlint: ignore Use newtype instead of data" :: String) #-}
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Creature/Hippocampus.hs b/users/grfn/xanthous/src/Xanthous/Entities/Creature/Hippocampus.hs
new file mode 100644
index 0000000000..d13ea8055c
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Creature/Hippocampus.hs
@@ -0,0 +1,71 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Creature.Hippocampus
+  (-- * Hippocampus
+    Hippocampus(..)
+  , initialHippocampus
+    -- ** Lenses
+  , destination
+  , greetedCharacter
+    -- ** Destination
+  , Destination(..)
+  , destinationFromPos
+    -- *** Lenses
+  , destinationPosition
+  , destinationProgress
+  )
+where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Aeson (ToJSON, FromJSON)
+import           Test.QuickCheck
+import           Test.QuickCheck.Arbitrary.Generic
+--------------------------------------------------------------------------------
+import           Xanthous.Data
+--------------------------------------------------------------------------------
+
+
+data Destination = Destination
+  { _destinationPosition :: !Position
+    -- | The progress towards the destination, tracked as an offset from the
+    -- creature's original position.
+    --
+    -- When this value reaches >= 1, the creature has reached their destination
+  , _destinationProgress :: !Tiles
+  }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       Destination
+instance Arbitrary Destination where arbitrary = genericArbitrary
+makeLenses ''Destination
+
+destinationFromPos :: Position -> Destination
+destinationFromPos _destinationPosition =
+  let _destinationProgress = 0
+  in Destination{..}
+
+data Hippocampus = Hippocampus
+  { _destination      :: !(Maybe Destination)
+  , -- | Has this creature greeted the character in any way yet?
+    --
+    -- Some creature types ignore this field
+    _greetedCharacter :: !Bool
+  }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Hippocampus
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       Hippocampus
+makeLenses ''Hippocampus
+
+initialHippocampus :: Hippocampus
+initialHippocampus = Hippocampus
+  { _destination      = Nothing
+  , _greetedCharacter = False
+  }
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Draw/Util.hs b/users/grfn/xanthous/src/Xanthous/Entities/Draw/Util.hs
new file mode 100644
index 0000000000..aa6c5fa4fc
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Draw/Util.hs
@@ -0,0 +1,31 @@
+module Xanthous.Entities.Draw.Util where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Brick.Widgets.Border.Style
+import Brick.Types (Edges(..))
+--------------------------------------------------------------------------------
+
+borderFromEdges :: BorderStyle -> Edges Bool -> Char
+borderFromEdges bstyle edges = ($ bstyle) $ case edges of
+  Edges False False  False False -> const '☐'
+
+  Edges True  False  False False -> bsVertical
+  Edges False True   False False -> bsVertical
+  Edges False False  True  False -> bsHorizontal
+  Edges False False  False True  -> bsHorizontal
+
+  Edges True  True   False False -> bsVertical
+  Edges True  False  True  False -> bsCornerBR
+  Edges True  False  False True  -> bsCornerBL
+
+  Edges False True   True  False -> bsCornerTR
+  Edges False True   False True  -> bsCornerTL
+  Edges False False  True  True  -> bsHorizontal
+
+  Edges False True   True  True  -> bsIntersectT
+  Edges True  False  True  True  -> bsIntersectB
+  Edges True  True   False True  -> bsIntersectL
+  Edges True  True   True  False -> bsIntersectR
+
+  Edges True  True   True  True  -> bsIntersectFull
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Entities.hs b/users/grfn/xanthous/src/Xanthous/Entities/Entities.hs
new file mode 100644
index 0000000000..a0c037a1b4
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Entities.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE StandaloneDeriving #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Entities () where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Test.QuickCheck
+import qualified Test.QuickCheck.Gen as Gen
+import           Data.Aeson
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.Character
+import           Xanthous.Entities.Item
+import           Xanthous.Entities.Creature
+import           Xanthous.Entities.Environment
+import           Xanthous.Entities.Marker
+import           Xanthous.Game.State
+import           Xanthous.Util.QuickCheck
+import           Data.Aeson.Generic.DerivingVia
+--------------------------------------------------------------------------------
+
+instance Arbitrary SomeEntity where
+  arbitrary = Gen.oneof
+    [ SomeEntity <$> arbitrary @Character
+    , SomeEntity <$> arbitrary @Item
+    , SomeEntity <$> arbitrary @Creature
+    , SomeEntity <$> arbitrary @Wall
+    , SomeEntity <$> arbitrary @Door
+    , SomeEntity <$> arbitrary @GroundMessage
+    , SomeEntity <$> arbitrary @Staircase
+    , SomeEntity <$> arbitrary @Marker
+    ]
+
+instance FromJSON SomeEntity where
+  parseJSON = withObject "Entity" $ \obj -> do
+    (entityType :: Text) <- obj .: "type"
+    case entityType of
+      "Character" -> SomeEntity @Character <$> obj .: "data"
+      "Item" -> SomeEntity @Item <$> obj .: "data"
+      "Creature" -> SomeEntity @Creature <$> obj .: "data"
+      "Wall" -> SomeEntity @Wall <$> obj .: "data"
+      "Door" -> SomeEntity @Door <$> obj .: "data"
+      "GroundMessage" -> SomeEntity @GroundMessage <$> obj .: "data"
+      "Staircase" -> SomeEntity @Staircase <$> obj .: "data"
+      "Marker" -> SomeEntity @Marker <$> obj .: "data"
+      _ -> fail . unpack $ "Invalid entity type \"" <> entityType <> "\""
+
+deriving via WithOptions '[ FieldLabelModifier '[Drop 1] ] GameLevel
+  instance FromJSON GameLevel
+deriving via WithOptions '[ FieldLabelModifier '[Drop 1] ] GameState
+  instance FromJSON GameState
+
+instance Entity SomeEntity where
+  entityAttributes (SomeEntity ent) = entityAttributes ent
+  description (SomeEntity ent) = description ent
+  entityChar (SomeEntity ent) = entityChar ent
+  entityCollision (SomeEntity ent) = entityCollision ent
+
+instance Function SomeEntity where
+  function = functionJSON
+
+instance CoArbitrary SomeEntity where
+  coarbitrary = coarbitrary . encode
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Entities.hs-boot b/users/grfn/xanthous/src/Xanthous/Entities/Entities.hs-boot
new file mode 100644
index 0000000000..519a862c6a
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Entities.hs-boot
@@ -0,0 +1,14 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Xanthous.Entities.Entities where
+
+import Test.QuickCheck
+import Data.Aeson
+import Xanthous.Game.State (SomeEntity, GameState, Entity)
+
+instance Arbitrary SomeEntity
+instance Function SomeEntity
+instance CoArbitrary SomeEntity
+instance FromJSON SomeEntity
+instance Entity SomeEntity
+
+instance FromJSON GameState
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Environment.hs b/users/grfn/xanthous/src/Xanthous/Entities/Environment.hs
new file mode 100644
index 0000000000..b45a91eabe
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Environment.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Xanthous.Entities.Environment
+  (
+    -- * Walls
+    Wall(..)
+
+    -- * Doors
+  , Door(..)
+  , open
+  , closed
+  , locked
+  , unlockedDoor
+
+    -- * Messages
+  , GroundMessage(..)
+
+    -- * Stairs
+  , Staircase(..)
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Test.QuickCheck
+import Brick (str)
+import Brick.Widgets.Border.Style (unicode)
+import Brick.Types (Edges(..))
+import Data.Aeson
+import Data.Aeson.Generic.DerivingVia
+--------------------------------------------------------------------------------
+import Xanthous.Entities.Draw.Util
+import Xanthous.Data
+import Xanthous.Data.Entities
+import Xanthous.Game.State
+import Xanthous.Util.QuickCheck
+--------------------------------------------------------------------------------
+
+data Wall = Wall
+  deriving stock (Show, Eq, Ord, Generic, Enum)
+  deriving anyclass (NFData, CoArbitrary, Function)
+
+instance ToJSON Wall where
+  toJSON = const $ String "Wall"
+
+instance FromJSON Wall where
+  parseJSON = withText "Wall" $ \case
+    "Wall" -> pure Wall
+    _      -> fail "Invalid Wall: expected Wall"
+
+instance Brain Wall where step = brainVia Brainless
+
+instance Entity Wall where
+  entityAttributes _ = defaultEntityAttributes
+    & blocksVision .~ True
+    & blocksObject .~ True
+  description _ = "a wall"
+  entityChar _ = "┼"
+
+instance Arbitrary Wall where
+  arbitrary = pure Wall
+
+wallEdges :: (MonoFoldable mono, Element mono ~ SomeEntity)
+          => Neighbors mono -> Edges Bool
+wallEdges neighs = any (entityIs @Wall) <$> edges neighs
+
+instance Draw Wall where
+  drawWithNeighbors neighs _wall =
+    str . pure . borderFromEdges unicode $ wallEdges neighs
+
+data Door = Door
+  { _open   :: Bool
+  , _locked :: Bool
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function, ToJSON, FromJSON)
+  deriving Arbitrary via GenericArbitrary Door
+makeLenses ''Door
+
+instance Draw Door where
+  drawWithNeighbors neighs door
+    = str . pure . ($ door ^. open) $ case wallEdges neighs of
+        Edges True  False  False False -> vertDoor
+        Edges False True   False False -> vertDoor
+        Edges True  True   False False -> vertDoor
+        Edges False False  True  False -> horizDoor
+        Edges False False  False True  -> horizDoor
+        Edges False False  True  True  -> horizDoor
+        _                              -> allsidesDoor
+    where
+      horizDoor True = '␣'
+      horizDoor False = 'ᚔ'
+      vertDoor True = '['
+      vertDoor False = 'ǂ'
+      allsidesDoor True = '+'
+      allsidesDoor False = '▥'
+
+instance Brain Door where step = brainVia Brainless
+
+instance Entity Door where
+  entityAttributes door = defaultEntityAttributes
+    & blocksVision .~ not (door ^. open)
+  description door | door ^. open = "an open door"
+                   | otherwise    = "a closed door"
+  entityChar _ = "d"
+  entityCollision door | door ^. open = Nothing
+                       | otherwise = Just Stop
+
+closed :: Lens' Door Bool
+closed = open . involuted not
+
+-- | A closed, unlocked door
+unlockedDoor :: Door
+unlockedDoor = Door
+  { _open = False
+  , _locked = False
+  }
+
+--------------------------------------------------------------------------------
+
+newtype GroundMessage = GroundMessage Text
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary GroundMessage
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ 'TagSingleConstructors 'True
+                        , 'SumEnc 'ObjWithSingleField
+                        ]
+           GroundMessage
+  deriving Draw
+       via DrawStyledCharacter ('Just 'Yellow) 'Nothing "≈"
+           GroundMessage
+instance Brain GroundMessage where step = brainVia Brainless
+
+instance Entity GroundMessage where
+  description = const "a message on the ground. Press r. to read it."
+  entityChar = const "≈"
+  entityCollision = const Nothing
+
+--------------------------------------------------------------------------------
+
+data Staircase = UpStaircase | DownStaircase
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Staircase
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ 'TagSingleConstructors 'True
+                        , 'SumEnc 'ObjWithSingleField
+                        ]
+           Staircase
+instance Brain Staircase where step = brainVia Brainless
+
+instance Draw Staircase where
+  draw UpStaircase = str "<"
+  draw DownStaircase = str ">"
+
+instance Entity Staircase where
+  description UpStaircase = "a staircase leading upwards"
+  description DownStaircase = "a staircase leading downwards"
+  entityChar UpStaircase = "<"
+  entityChar DownStaircase = ">"
+  entityCollision = const Nothing
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Item.hs b/users/grfn/xanthous/src/Xanthous/Entities/Item.hs
new file mode 100644
index 0000000000..eadd625696
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Item.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Item
+  ( Item(..)
+  , itemType
+  , density
+  , volume
+  , newWithType
+  , isEdible
+  , weight
+  , fullDescription
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Test.QuickCheck (Arbitrary, CoArbitrary, Function)
+import           Data.Aeson (ToJSON, FromJSON)
+import           Data.Aeson.Generic.DerivingVia
+import           Control.Monad.Random (MonadRandom)
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.RawTypes (ItemType)
+import qualified Xanthous.Entities.RawTypes as Raw
+import           Xanthous.Game.State
+import           Xanthous.Data (Grams, Per, Cubic, Meters, (|*|))
+import           Xanthous.Util.QuickCheck (GenericArbitrary(GenericArbitrary))
+import           Xanthous.Random (choose, FiniteInterval(..))
+--------------------------------------------------------------------------------
+
+data Item = Item
+  { _itemType :: ItemType
+  , _density  :: Grams `Per` Cubic Meters
+  , _volume   :: Cubic Meters
+  }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Draw via DrawRawChar "_itemType" Item
+  deriving Arbitrary via GenericArbitrary Item
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       Item
+makeLenses ''Item
+
+-- deriving via (Brainless Item) instance Brain Item
+instance Brain Item where step = brainVia Brainless
+
+instance Entity Item where
+  description = view $ itemType . Raw.description
+  entityChar = view $ itemType . Raw.char
+  entityCollision = const Nothing
+
+newWithType :: MonadRandom m => ItemType -> m Item
+newWithType _itemType = do
+  _density <- choose . FiniteInterval $ _itemType ^. Raw.density
+  _volume  <- choose . FiniteInterval $ _itemType ^. Raw.volume
+  pure Item {..}
+
+isEdible :: Item -> Bool
+isEdible = Raw.isEdible . view itemType
+
+-- | The weight of this item, calculated by multiplying its volume by the
+-- density of its material
+weight :: Item -> Grams
+weight item = (item ^. density) |*| (item ^. volume)
+
+-- | Describe the item in full detail
+fullDescription :: Item -> Text
+fullDescription item = unlines
+  [ item ^. itemType . Raw.description
+  , ""
+  , item ^. itemType . Raw.longDescription
+  , ""
+  , "volume: " <> tshow (item ^. volume)
+  , "density: " <> tshow (item ^. density)
+  , "weight: " <> tshow (weight item)
+  ]
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Marker.hs b/users/grfn/xanthous/src/Xanthous/Entities/Marker.hs
new file mode 100644
index 0000000000..14d02872ed
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Marker.hs
@@ -0,0 +1,41 @@
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Marker ( Marker(..) ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.Aeson
+import           Test.QuickCheck
+import qualified Graphics.Vty.Attributes as Vty
+import qualified Graphics.Vty.Image as Vty
+import           Brick.Widgets.Core (raw)
+--------------------------------------------------------------------------------
+import           Xanthous.Game.State
+import           Xanthous.Data.Entities (EntityAttributes(..))
+--------------------------------------------------------------------------------
+
+-- | Mark on the map - for use in debugging / development only.
+newtype Marker = Marker Text
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (Semigroup, Monoid, ToJSON, FromJSON, Arbitrary) via Text
+
+instance Brain Marker where step = brainVia Brainless
+
+instance Entity Marker where
+  entityAttributes = const EntityAttributes
+    { _blocksVision = False
+    , _blocksObject = False
+    , _collision = Stop
+    }
+  description (Marker m) = "[M] " <> m
+  entityChar = const $ "X" & style .~ markerStyle
+  entityCollision = const Nothing
+
+instance Draw Marker where
+  draw = const . raw $ Vty.char markerStyle 'X'
+  drawPriority = const maxBound
+
+markerStyle :: Vty.Attr
+markerStyle = Vty.defAttr
+  `Vty.withForeColor` Vty.red
+  `Vty.withBackColor` Vty.black
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/RawTypes.hs b/users/grfn/xanthous/src/Xanthous/Entities/RawTypes.hs
new file mode 100644
index 0000000000..a7021d76cf
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/RawTypes.hs
@@ -0,0 +1,286 @@
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.RawTypes
+  (
+    EntityRaw(..)
+  , _Creature
+  , _Item
+
+    -- * Creatures
+  , CreatureType(..)
+  , hostile
+    -- ** Generation parameters
+  , CreatureGenerateParams(..)
+  , canGenerate
+    -- ** Language
+  , LanguageName(..)
+  , getLanguage
+    -- ** Attacks
+  , Attack(..)
+
+    -- * Items
+  , ItemType(..)
+    -- ** Item sub-types
+    -- *** Edible
+  , EdibleItem(..)
+  , isEdible
+    -- *** Wieldable
+  , WieldableItem(..)
+  , isWieldable
+
+    -- * Lens classes
+  , HasAttackMessage(..)
+  , HasAttacks(..)
+  , HasChance(..)
+  , HasChar(..)
+  , HasCreatureAttackMessage(..)
+  , HasDamage(..)
+  , HasDensity(..)
+  , HasDescription(..)
+  , HasEatMessage(..)
+  , HasEdible(..)
+  , HasEntityName(..)
+  , HasEquippedItem(..)
+  , HasFriendly(..)
+  , HasGenerateParams(..)
+  , HasHitpointsHealed(..)
+  , HasLanguage(..)
+  , HasLevelRange(..)
+  , HasLongDescription(..)
+  , HasMaxHitpoints(..)
+  , HasName(..)
+  , HasSayVerb(..)
+  , HasSpeed(..)
+  , HasVolume(..)
+  , HasWieldable(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Test.QuickCheck
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Aeson (ToJSON, FromJSON)
+import           Data.Interval (Interval, lowerBound', upperBound')
+import qualified Data.Interval as Interval
+--------------------------------------------------------------------------------
+import           Xanthous.Messages (Message(..))
+import           Xanthous.Data (TicksPerTile, Hitpoints, Per, Grams, Cubic, Meters)
+import           Xanthous.Data.EntityChar
+import           Xanthous.Util.QuickCheck
+import           Xanthous.Generators.Speech (Language, gormlak, english)
+import           Xanthous.Orphans ()
+import           Xanthous.Util (EqProp, EqEqProp(..))
+--------------------------------------------------------------------------------
+
+-- | Identifiers for languages that creatures can speak.
+--
+-- Non-verbal or non-sentient creatures have Nothing as their language
+--
+-- At some point, we will likely want to make languages be defined in data files
+-- somewhere, and reference them that way instead.
+data LanguageName = Gormlak | English
+  deriving stock (Show, Eq, Ord, Generic, Enum, Bounded)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary LanguageName
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ AllNullaryToStringTag 'True ]
+                       LanguageName
+
+-- | Resolve a 'LanguageName' into an actual 'Language'
+getLanguage :: LanguageName -> Language
+getLanguage Gormlak = gormlak
+getLanguage English = english
+
+-- | Natural attacks for creature types
+data Attack = Attack
+  { -- | the @{{creature}}@ @{{description}}@
+    _description :: !Message
+    -- | Damage dealt
+  , _damage      :: !Hitpoints
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Attack
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1]
+                        , OmitNothingFields 'True
+                        ]
+                       Attack
+makeFieldsNoPrefix ''Attack
+
+-- | Description for generating an item equipped to a creature
+data CreatureEquippedItem = CreatureEquippedItem
+  { -- | Name of the entity type to generate
+    _entityName :: !Text
+    -- | Chance of generating the item when generating the creature
+    --
+    -- A chance of 1.0 will always generate the item
+  , _chance :: !Double
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary CreatureEquippedItem
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1]
+                        , OmitNothingFields 'True
+                        ]
+                       CreatureEquippedItem
+makeFieldsNoPrefix ''CreatureEquippedItem
+
+
+data CreatureGenerateParams = CreatureGenerateParams
+  { -- | Range of dungeon levels at which to generate this creature
+    _levelRange :: !(Interval Word)
+    -- | Item equipped to the creature
+  , _equippedItem :: !(Maybe CreatureEquippedItem)
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary CreatureGenerateParams
+  deriving EqProp via EqEqProp CreatureGenerateParams
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       CreatureGenerateParams
+makeFieldsNoPrefix ''CreatureGenerateParams
+
+instance Ord CreatureGenerateParams where
+  compare
+    = (compare `on` lowerBound' . _levelRange)
+    <> (compare `on` upperBound' . _levelRange)
+    <> (compare `on` _equippedItem)
+
+-- | Can a creature with these generate params be generated on this level?
+canGenerate
+  :: Word -- ^ Level number
+  -> CreatureGenerateParams
+  -> Bool
+canGenerate levelNumber gps = Interval.member levelNumber $ gps ^. levelRange
+
+data CreatureType = CreatureType
+  { _name           :: !Text
+  , _description    :: !Text
+  , _char           :: !EntityChar
+  , _maxHitpoints   :: !Hitpoints
+  , _friendly       :: !Bool
+  , _speed          :: !TicksPerTile
+  , _language       :: !(Maybe LanguageName)
+  , -- | The verb, in present tense, for when the creature says something
+    _sayVerb        :: !(Maybe Text)
+  , -- | The creature's natural attacks
+    _attacks        :: !(NonNull (Vector Attack))
+    -- | Parameters for generating the creature in levels
+  , _generateParams :: !(Maybe CreatureGenerateParams)
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary CreatureType
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1]
+                        , OmitNothingFields 'True
+                        ]
+                       CreatureType
+makeFieldsNoPrefix ''CreatureType
+
+hostile :: Lens' CreatureType Bool
+hostile = friendly . involuted not
+
+--------------------------------------------------------------------------------
+
+data EdibleItem = EdibleItem
+  { _hitpointsHealed :: !Int
+  , _eatMessage      :: !(Maybe Message)
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary EdibleItem
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       EdibleItem
+makeFieldsNoPrefix ''EdibleItem
+
+data WieldableItem = WieldableItem
+  { _damage :: !Hitpoints
+    -- | Message to use when the character is using this item to attack a
+    --  creature.
+    --
+    -- Grammatically, this should be of the form "slash at the
+    -- {{creature.creatureType.name}} with your dagger"
+    --
+    -- = Parameters
+    --
+    -- [@creature@ (type: 'Creature')] The creature being attacked
+  , _attackMessage :: !(Maybe Message)
+    -- | Message to use when a creature is using this item to attack the
+    -- character.
+    --
+    -- Grammatically, should be of the form "The creature slashes you with its
+    -- dagger".
+    --
+    -- = Parameters
+    --
+    -- [@creature@ (type: 'Creature')] The creature doing the attacking
+    -- [@item@ (type: 'Item')] The item itself
+  , _creatureAttackMessage :: !(Maybe Message)
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary WieldableItem
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       WieldableItem
+makeFieldsNoPrefix ''WieldableItem
+
+--------------------------------------------------------------------------------
+
+data ItemType = ItemType
+  { _name            :: !Text
+  , _description     :: !Text
+  , _longDescription :: !Text
+  , _char            :: !EntityChar
+  , _density         :: !(Interval (Grams `Per` Cubic Meters))
+  , _volume          :: !(Interval (Cubic Meters))
+  , _edible          :: !(Maybe EdibleItem)
+  , _wieldable       :: !(Maybe WieldableItem)
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary ItemType
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+                       ItemType
+makeFieldsNoPrefix ''ItemType
+
+instance Ord ItemType where
+  compare x y
+    = compareOf name x y
+    <> compareOf description x y
+    <> compareOf longDescription x y
+    <> compareOf char x y
+    <> compareOf (density . to extractInterval) x y
+    <> compareOf (volume . to extractInterval) x y
+    <> compareOf edible x y
+    <> compareOf wieldable x y
+    where
+      compareOf l = comparing (view l)
+      extractInterval = lowerBound' &&& upperBound'
+
+-- | Can this item be eaten?
+isEdible :: ItemType -> Bool
+isEdible = has $ edible . _Just
+
+-- | Can this item be used as a weapon?
+isWieldable :: ItemType -> Bool
+isWieldable = has $ wieldable . _Just
+
+--------------------------------------------------------------------------------
+
+data EntityRaw
+  = Creature !CreatureType
+  | Item !ItemType
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+  deriving Arbitrary via GenericArbitrary EntityRaw
+  deriving (FromJSON)
+       via WithOptions '[ SumEnc ObjWithSingleField ]
+                       EntityRaw
+makePrisms ''EntityRaw
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws.hs b/users/grfn/xanthous/src/Xanthous/Entities/Raws.hs
new file mode 100644
index 0000000000..10f0d83193
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.Raws
+  ( raws
+  , raw
+  , RawType(..)
+  , rawsWithType
+  ) where
+--------------------------------------------------------------------------------
+import           Data.FileEmbed
+import qualified Data.Yaml as Yaml
+import           Xanthous.Prelude
+import           System.FilePath.Posix
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.RawTypes
+import           Xanthous.AI.Gormlak ()
+--------------------------------------------------------------------------------
+rawRaws :: [(FilePath, ByteString)]
+rawRaws = $(embedDir "src/Xanthous/Entities/Raws")
+
+raws :: HashMap Text EntityRaw
+raws
+  = mapFromList
+  . map (bimap
+         (pack . takeBaseName)
+         (either (error . Yaml.prettyPrintParseException) id
+          . Yaml.decodeEither'))
+  $ rawRaws
+
+raw :: Text -> Maybe EntityRaw
+raw n = raws ^. at n
+
+class RawType (a :: Type) where
+  _RawType :: Prism' EntityRaw a
+
+instance RawType CreatureType where
+  _RawType = prism' Creature $ \case
+    Creature c -> Just c
+    _ -> Nothing
+
+instance RawType ItemType where
+  _RawType = prism' Item $ \case
+    Item i -> Just i
+    _ -> Nothing
+
+rawsWithType :: forall a. RawType a => HashMap Text a
+rawsWithType = mapFromList . itoListOf (ifolded . _RawType) $ raws
+
+--------------------------------------------------------------------------------
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/broken-dagger.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/broken-dagger.yaml
new file mode 100644
index 0000000000..12c76fc14b
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/broken-dagger.yaml
@@ -0,0 +1,24 @@
+Item:
+  name: broken dagger
+  description: a short, broken dagger
+  longDescription: A short dagger with a twisted, chipped blade
+  char:
+    char: †
+    style:
+      foreground: black
+  wieldable:
+    damage: 3
+    attackMessage:
+      - slash at the {{creature.creatureType.name}} with your dagger
+      - stab the {{creature.creatureType.name}} with your dagger
+    creatureAttackMessage:
+      - The {{creature.creatureType.name}} slashes at you with its dagger.
+      - The {{creature.creatureType.name}} stabs you with its dagger.
+  # Just the steel, not the handle, for now
+  density: [7750 , 8050000]
+  # 15cm – 45cm
+  # ×
+  # 2cm – 3cm
+  # ×
+  # .5cm – 1cm
+  volume: [0.15, 1.35]
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/gormlak.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/gormlak.yaml
new file mode 100644
index 0000000000..ad3d9cb147
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/gormlak.yaml
@@ -0,0 +1,20 @@
+Creature:
+  name: gormlak
+  description: a gormlak
+  longDescription: |
+    A chittering imp-like creature with bright yellow horns and sharp claws. It
+    adores shiny objects and gathers in swarms.
+  char:
+    char: g
+    style:
+      foreground: red
+  maxHitpoints: 5
+  speed: 125
+  friendly: false
+  language: Gormlak
+  sayVerb: yells
+  attacks:
+  - description:
+      - claws you
+      - slashes you with its claws
+    damage: 1
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/husk.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/husk.yaml
new file mode 100644
index 0000000000..cdfcde616d
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/husk.yaml
@@ -0,0 +1,26 @@
+Creature:
+  name: husk
+  description: an empty husk of some humanoid creature
+  longDescription: |
+    An empty husk of a humanoid creature. All semblance of sentience has long
+    left its eyes; instead it shambles about aimlessly, always hungering for the
+    warmth of life.
+  char:
+    char: h
+    style:
+      foreground: black
+  maxHitpoints: 6
+  speed: 110
+  friendly: false
+  attacks:
+  - description:
+      - swings its arms at you
+      - elbows you
+    damage: 1
+  - description: kicks you
+    damage: 2
+  generateParams:
+    levelRange: [1, PosInf]
+    equippedItem:
+      entityName: broken-dagger
+      chance: 0.9
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/noodles.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/noodles.yaml
new file mode 100644
index 0000000000..c0501a18a8
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/noodles.yaml
@@ -0,0 +1,14 @@
+Item:
+  name: noodles
+  description: "a big bowl o' noodles"
+  longDescription: You know exactly what kind of noodles
+  char:
+    char: 'n'
+    style:
+      foreground: yellow
+  edible:
+    hitpointsHealed: 2
+    eatMessage:
+      - You slurp up the noodles. Yumm!
+  density: 500000
+  volume: 0.001
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/ooze.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/ooze.yaml
new file mode 100644
index 0000000000..fe427c94ab
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/ooze.yaml
@@ -0,0 +1,15 @@
+Creature:
+  name: ooze
+  description: an ooze
+  longDescription: |
+    A jiggling, amorphous, bright green caustic blob
+  char:
+    char: o
+    style:
+      foreground: green
+  maxHitpoints: 3
+  speed: 100
+  friendly: false
+  attacks:
+  - description: slams into you
+    damage: 1
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/rock.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/rock.yaml
new file mode 100644
index 0000000000..3f4e133fe2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/rock.yaml
@@ -0,0 +1,10 @@
+Item:
+  name: rock
+  description: a rock
+  longDescription: a medium-sized rock made out of some unknown stone
+  char: .
+  wieldable:
+    damage: 1
+    attackMessage: hit the {{creature.creatureType.name}} in the head with your rock
+  density: [ 1500000, 2500000 ]
+  volume: [ 0.000125, 0.001 ]
diff --git a/users/grfn/xanthous/src/Xanthous/Entities/Raws/stick.yaml b/users/grfn/xanthous/src/Xanthous/Entities/Raws/stick.yaml
new file mode 100644
index 0000000000..7f9e1faffe
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Entities/Raws/stick.yaml
@@ -0,0 +1,22 @@
+Item:
+  name: stick
+  description: a wooden stick
+  longDescription: A sturdy branch broken off from some sort of tree
+  char:
+    char: ∤
+    style:
+      foreground: yellow
+  wieldable:
+    damage: 2
+    attackMessage:
+      - bonk the {{creature.creatureType.name}} over the head with your stick
+      - bash the {{creature.creatureType.name}} on the noggin with your stick
+      - whack the {{creature.creatureType.name}} with your stick
+    creatureAttackMessage:
+      - The {{creature.creatureType.name}} bonks you over the head with its stick.
+      - The {{creature.creatureType.name}} bashes you on the noggin with its stick.
+      - The {{creature.creatureType.name}} whacks you with its stick.
+  # https://www.sciencedirect.com/topics/agricultural-and-biological-sciences/wood-density
+  # it's a hard stick. so it's dense wood.
+  density: 890000 # g/m³
+  volume: [ 0.003, 0.006 ] # ≈3.5 cm radius × ≈1m length
diff --git a/users/grfn/xanthous/src/Xanthous/Game.hs b/users/grfn/xanthous/src/Xanthous/Game.hs
new file mode 100644
index 0000000000..89c23f0de8
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game.hs
@@ -0,0 +1,73 @@
+module Xanthous.Game
+  ( GameState(..)
+  , levels
+  , entities
+  , revealedPositions
+  , messageHistory
+  , randomGen
+  , promptState
+  , GamePromptState(..)
+
+  , getInitialState
+  , initialStateFromSeed
+
+  , positionedCharacter
+  , character
+  , characterPosition
+  , updateCharacterVision
+  , characterVisiblePositions
+  , entitiesAtCharacter
+  , revealedEntitiesAtPosition
+
+    -- * Messages
+  , MessageHistory(..)
+  , HasMessages(..)
+  , HasTurn(..)
+  , HasDisplayedTurn(..)
+  , pushMessage
+  , previousMessage
+  , nextTurn
+
+    -- * Collisions
+  , Collision(..)
+  , collisionAt
+
+    -- * App monad
+  , AppT(..)
+
+    -- * Saving the game
+  , saveGame
+  , loadGame
+  , saved
+
+    -- * Debug State
+  , DebugState(..)
+  , debugState
+  , allRevealed
+  ) where
+--------------------------------------------------------------------------------
+import qualified Codec.Compression.Zlib as Zlib
+import           Codec.Compression.Zlib.Internal (DecompressError)
+import qualified Data.Aeson as JSON
+import           System.IO.Unsafe
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Xanthous.Game.State
+import           Xanthous.Game.Lenses
+import           Xanthous.Game.Arbitrary ()
+import           Xanthous.Entities.Entities ()
+--------------------------------------------------------------------------------
+
+saveGame :: GameState -> LByteString
+saveGame = Zlib.compress . JSON.encode
+
+loadGame :: LByteString -> Maybe GameState
+loadGame = JSON.decode <=< decompressZlibMay
+  where
+    decompressZlibMay bs
+      = unsafeDupablePerformIO
+      $ (let r = Zlib.decompress bs in r `seq` pure (Just r))
+      `catch` \(_ :: DecompressError) -> pure Nothing
+
+saved :: Prism' LByteString GameState
+saved = prism' saveGame loadGame
diff --git a/users/grfn/xanthous/src/Xanthous/Game/Arbitrary.hs b/users/grfn/xanthous/src/Xanthous/Game/Arbitrary.hs
new file mode 100644
index 0000000000..679bfe5459
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/Arbitrary.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE UndecidableInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Xanthous.Game.Arbitrary where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (foldMap)
+--------------------------------------------------------------------------------
+import           Test.QuickCheck
+import           System.Random
+import           Data.Foldable (foldMap)
+--------------------------------------------------------------------------------
+import           Xanthous.Data.Levels
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Entities.Entities ()
+import           Xanthous.Entities.Character
+import           Xanthous.Game.State
+import           Xanthous.Orphans ()
+import           Xanthous.Util.QuickCheck (GenericArbitrary(..))
+--------------------------------------------------------------------------------
+
+deriving via GenericArbitrary GameLevel instance Arbitrary GameLevel
+
+instance Arbitrary GameState where
+  arbitrary = do
+    chr <- arbitrary @Character
+    _upStaircasePosition <- arbitrary
+    _messageHistory <- arbitrary
+    levs <- arbitrary @(Levels GameLevel)
+    _levelRevealedPositions <-
+      fmap setFromList
+      . sublistOf
+      . foldMap (EntityMap.positions . _levelEntities)
+      $ levs
+    let (_characterEntityID, _levelEntities) =
+          EntityMap.insertAtReturningID _upStaircasePosition (SomeEntity chr)
+          $ levs ^. current . levelEntities
+        _levels = levs & current .~ GameLevel {..}
+    _randomGen <- mkStdGen <$> arbitrary
+    let _promptState = NoPrompt -- TODO
+    _activePanel <- arbitrary
+    _debugState <- arbitrary
+    let _autocommand = NoAutocommand
+    _memo <- arbitrary
+    _savefile <- arbitrary
+    pure $ GameState {..}
+
+
+instance CoArbitrary GameLevel
+instance Function GameLevel
+instance CoArbitrary GameState
+instance Function GameState
diff --git a/users/grfn/xanthous/src/Xanthous/Game/Draw.hs b/users/grfn/xanthous/src/Xanthous/Game/Draw.hs
new file mode 100644
index 0000000000..291dfd8b5e
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/Draw.hs
@@ -0,0 +1,224 @@
+--------------------------------------------------------------------------------
+module Xanthous.Game.Draw
+  ( drawGame
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Brick hiding (loc, on)
+import           Brick.Widgets.Border
+import           Brick.Widgets.Border.Style
+import           Brick.Widgets.Edit
+import           Control.Monad.State.Lazy (evalState)
+import           Control.Monad.State.Class ( get, MonadState, gets )
+--------------------------------------------------------------------------------
+import           Xanthous.Data
+import           Xanthous.Data.App (ResourceName, Panel(..))
+import qualified Xanthous.Data.App as Resource
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Game.State
+import           Xanthous.Entities.Common (Wielded(..), wielded, backpack)
+import           Xanthous.Entities.Character
+import           Xanthous.Entities.Item (Item)
+import           Xanthous.Game
+                 ( characterPosition
+                 , character
+                 , revealedEntitiesAtPosition
+                 )
+import           Xanthous.Game.Prompt
+import           Xanthous.Orphans ()
+import Brick.Widgets.Center (hCenter)
+import Xanthous.Command (Keybinding (..), keybindings, Command, commandIsHidden)
+import Graphics.Vty.Input.Events (Modifier(..))
+import Graphics.Vty.Input (Key(..))
+import Brick.Widgets.Table
+--------------------------------------------------------------------------------
+
+cursorPosition :: GameState -> Widget ResourceName -> Widget ResourceName
+cursorPosition game
+  | WaitingPrompt _ (Prompt _ _ (preview promptStatePosition -> Just pos) _ _)
+    <- game ^. promptState
+  = showCursor Resource.Prompt (pos ^. loc)
+  | otherwise
+  = showCursor Resource.Character (game ^. characterPosition . loc)
+
+drawMessages :: MessageHistory -> Widget ResourceName
+drawMessages = txtWrap . (<> " ") . unwords . reverse . oextract
+
+drawPromptState :: GamePromptState m -> Widget ResourceName
+drawPromptState NoPrompt = emptyWidget
+drawPromptState (WaitingPrompt msg (Prompt _ pt ps pri _)) =
+  case (pt, ps, pri) of
+    (SStringPrompt, StringPromptState edit, mDef) ->
+      txt msg
+      <+> txt (maybe "" (\def -> "(default: " <> def <> ") ") mDef)
+      <+> renderEditor (txt . fold) True edit
+    (SDirectionPrompt, DirectionPromptState, _) -> txtWrap msg
+    (SMenu, _, menuItems) ->
+      txtWrap msg
+      <=> foldl' (<=>) emptyWidget (map drawMenuItem $ itoList menuItems)
+    _ -> txtWrap msg
+  where
+    drawMenuItem (chr, MenuOption m _) =
+      str ("[" <> pure chr <> "] ") <+> txtWrap m
+
+drawEntities
+  :: forall m. MonadState GameState m
+  => m (Widget ResourceName)
+drawEntities = do
+  allEnts <- use entities
+  let entityPositions = EntityMap.positions allEnts
+      maxY = fromMaybe 0 $ maximumOf (folded . y) entityPositions
+      maxX = fromMaybe 0 $ maximumOf (folded . x) entityPositions
+      rows = traverse mkRow [0..maxY]
+      mkRow rowY = hBox <$> traverse (renderEntityAt . flip Position rowY) [0..maxX]
+      renderEntityAt pos
+        = renderTopEntity pos <$> revealedEntitiesAtPosition pos
+      renderTopEntity pos ents
+        = let neighbors = EntityMap.neighbors pos allEnts
+          in maybe (str " ") (drawWithNeighbors neighbors)
+             $ maximumBy (compare `on` drawPriority)
+             <$> fromNullable ents
+  vBox <$> rows
+
+drawMap :: MonadState GameState m => m (Widget ResourceName)
+drawMap = do
+  cursorPos <- gets cursorPosition
+  viewport Resource.MapViewport Both . cursorPos <$> drawEntities
+
+bullet :: Char
+bullet = '•'
+
+drawInventoryPanel :: GameState -> Widget ResourceName
+drawInventoryPanel game
+  =   drawWielded  (game ^. character . inventory . wielded)
+  <=> drawBackpack (game ^. character . inventory . backpack)
+  where
+    drawWielded (Hands Nothing Nothing) = emptyWidget
+    drawWielded (DoubleHanded i) =
+      txtWrap $ "You are holding " <> description i <> " in both hands"
+    drawWielded (Hands l r) = drawHand "left" l <=> drawHand "right" r
+    drawHand side = maybe emptyWidget $ \i ->
+      txtWrap ( "You are holding "
+              <> description i
+              <> " in your " <> side <> " hand"
+              )
+      <=> txt " "
+
+    drawBackpack :: Vector Item -> Widget ResourceName
+    drawBackpack Empty = txtWrap "Your backpack is empty right now."
+    drawBackpack backpackItems
+      = txtWrap ( "You are currently carrying the following items in your "
+                <> "backpack:")
+        <=> txt " "
+        <=> foldl' (<=>) emptyWidget
+            (map
+              (txtWrap . ((bullet <| " ") <>) . description)
+              backpackItems)
+
+drawHelpPanel :: Widget ResourceName
+drawHelpPanel
+  = txtWrap "To move in a direction or attack, use vi keys (hjklyubn):"
+  <=> txt " "
+  <=> hCenter keyStar
+  <=> txt " "
+  <=> cmds
+  where
+    keyStar
+      =   txt "y k u"
+      <=> txt " \\|/"
+      <=> txt "h-.-l"
+      <=> txt " /|\\"
+      <=> txt "b j n"
+
+    cmds
+      = renderTable
+      . alignRight 0
+      . setDefaultRowAlignment AlignTop
+      . surroundingBorder False
+      . rowBorders False
+      . columnBorders False
+      . table $ help <&> \(key, cmd) -> [ txt $ key <> " : "
+                                       , hLimitPercent 100 $ txtWrap cmd]
+
+    help =
+      extraHelp <>
+      keybindings
+        ^.. ifolded
+          . filtered (not . commandIsHidden)
+          . withIndex
+          . to (bimap displayKeybinding displayCommand)
+    extraHelp
+      = [("Shift-Dir", "Auto-move")]
+
+    displayCommand = tshow @Command
+    displayKeybinding (Keybinding k mods) = foldMap showMod mods <> showKey k
+
+    showMod MCtrl  = "Ctrl-"
+    showMod MShift = "Shift-"
+    showMod MAlt   = "Alt-"
+    showMod MMeta  = "Meta-"
+
+    showKey (KChar c) = pack [c]
+    showKey KEsc = "<Esc>"
+    showKey KBS = "<Backspace>"
+    showKey KEnter = "<Enter>"
+    showKey KLeft = "<Left>"
+    showKey KRight = "<Right>"
+    showKey KUp = "<Up>"
+    showKey KDown = "<Down>"
+    showKey KUpLeft = "<UpLeft>"
+    showKey KUpRight = "<UpRight>"
+    showKey KDownLeft = "<DownLeft>"
+    showKey KDownRight = "<DownRight>"
+    showKey KCenter = "<Center>"
+    showKey (KFun n) = "<F" <> tshow n <> ">"
+    showKey KBackTab = "<BackTab>"
+    showKey KPrtScr = "<PrtScr>"
+    showKey KPause = "<Pause>"
+    showKey KIns = "<Ins>"
+    showKey KHome = "<Home>"
+    showKey KPageUp = "<PageUp>"
+    showKey KDel = "<Del>"
+    showKey KEnd = "<End>"
+    showKey KPageDown = "<PageDown>"
+    showKey KBegin = "<Begin>"
+    showKey KMenu = "<Menu>"
+
+drawPanel :: GameState -> Panel -> Widget ResourceName
+drawPanel game panel
+  = border
+  . hLimit 35
+  . viewport (Resource.Panel panel) Vertical
+  $ case panel of
+      HelpPanel -> drawHelpPanel
+      InventoryPanel -> drawInventoryPanel game
+      ItemDescriptionPanel desc -> txtWrap desc
+
+drawCharacterInfo :: Character -> Widget ResourceName
+drawCharacterInfo ch = txt " " <+> charName <+> charHitpoints
+  where
+    charName | Just n <- ch ^. characterName
+             = txt $ n <> " "
+             | otherwise
+             = emptyWidget
+    charHitpoints
+        = txt "Hitpoints: "
+      <+> txt (tshow $ let Hitpoints hp = characterHitpoints ch in hp)
+
+drawGame :: GameState -> [Widget ResourceName]
+drawGame = evalState $ do
+  game <- get
+  drawnMap <- drawMap
+  pure
+    . pure
+    . withBorderStyle unicode
+    $ case game ^. promptState of
+        NoPrompt -> drawMessages (game ^. messageHistory)
+        _ -> emptyWidget
+    <=> drawPromptState (game ^. promptState)
+    <=>
+    (maybe emptyWidget (drawPanel game) (game ^. activePanel)
+    <+> border drawnMap
+    )
+    <=> drawCharacterInfo (game ^. character)
diff --git a/users/grfn/xanthous/src/Xanthous/Game/Env.hs b/users/grfn/xanthous/src/Xanthous/Game/Env.hs
new file mode 100644
index 0000000000..5d7b275c8a
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/Env.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Game.Env
+  ( Config(..)
+  , defaultConfig
+  , disableSaving
+  , GameEnv(..)
+  , eventChan
+  , config
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Brick.BChan (BChan)
+import Xanthous.Data.App (AppEvent)
+--------------------------------------------------------------------------------
+
+data Config = Config
+  { _disableSaving :: Bool
+  }
+  deriving stock (Generic, Show, Eq)
+makeLenses ''Config
+{-# ANN Config ("HLint: ignore Use newtype instead of data" :: String) #-}
+
+defaultConfig :: Config
+defaultConfig = Config
+  { _disableSaving = False
+  }
+
+--------------------------------------------------------------------------------
+
+data GameEnv = GameEnv
+  { _eventChan :: BChan AppEvent
+  , _config :: Config
+  }
+  deriving stock (Generic)
+makeLenses ''GameEnv
diff --git a/users/grfn/xanthous/src/Xanthous/Game/Lenses.hs b/users/grfn/xanthous/src/Xanthous/Game/Lenses.hs
new file mode 100644
index 0000000000..c692a3b479
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/Lenses.hs
@@ -0,0 +1,178 @@
+{-# LANGUAGE RecordWildCards       #-}
+{-# LANGUAGE QuantifiedConstraints #-}
+{-# LANGUAGE AllowAmbiguousTypes   #-}
+--------------------------------------------------------------------------------
+module Xanthous.Game.Lenses
+  ( clearMemo
+  , positionedCharacter
+  , character
+  , characterPosition
+  , updateCharacterVision
+  , characterVisiblePositions
+  , characterVisibleEntities
+  , positionIsCharacterVisible
+  , getInitialState
+  , initialStateFromSeed
+  , entitiesAtCharacter
+  , revealedEntitiesAtPosition
+  , hearingRadius
+
+    -- * Collisions
+  , Collision(..)
+  , entitiesCollision
+  , collisionAt
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           System.Random
+import           Control.Monad.State
+import           Control.Monad.Random (getRandom)
+--------------------------------------------------------------------------------
+import           Xanthous.Game.State
+import qualified Xanthous.Game.Memo as Memo
+import           Xanthous.Data
+import           Xanthous.Data.Levels
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Data.EntityMap.Graphics
+                 (visiblePositions, visibleEntities)
+import           Xanthous.Data.VectorBag
+import           Xanthous.Entities.Character (Character, mkCharacter)
+import           {-# SOURCE #-} Xanthous.Entities.Entities ()
+import           Xanthous.Game.Memo (emptyMemoState, MemoState)
+import           Xanthous.Data.Memo (fillWithM, Memoized)
+--------------------------------------------------------------------------------
+
+getInitialState :: IO GameState
+getInitialState = initialStateFromSeed <$> getRandom
+
+initialStateFromSeed :: Int -> GameState
+initialStateFromSeed seed =
+  let _randomGen = mkStdGen seed
+      chr = mkCharacter
+      _upStaircasePosition = Position 0 0
+      (_characterEntityID, _levelEntities)
+        = EntityMap.insertAtReturningID
+          _upStaircasePosition
+          (SomeEntity chr)
+          mempty
+      _levelRevealedPositions = mempty
+      level = GameLevel {..}
+      _levels = oneLevel level
+      _messageHistory = mempty
+      _promptState = NoPrompt
+      _activePanel = Nothing
+      _debugState = DebugState
+        { _allRevealed = False
+        }
+      _savefile = Nothing
+      _autocommand = NoAutocommand
+      _memo = emptyMemoState
+  in GameState {..}
+
+clearMemo :: MonadState GameState m => Lens' MemoState (Memoized k v) -> m ()
+clearMemo l = memo %= Memo.clear l
+
+positionedCharacter :: Lens' GameState (Positioned Character)
+positionedCharacter = lens getPositionedCharacter setPositionedCharacter
+  where
+    setPositionedCharacter :: GameState -> Positioned Character -> GameState
+    setPositionedCharacter game chr
+      = game
+      &  entities . at (game ^. characterEntityID)
+      ?~ fmap SomeEntity chr
+
+    getPositionedCharacter :: GameState -> Positioned Character
+    getPositionedCharacter game
+      = over positioned
+        ( fromMaybe (error "Invariant error: Character was not a character!")
+        . downcastEntity
+        )
+      . fromMaybe (error "Invariant error: Character not found!")
+      $ EntityMap.lookupWithPosition
+        (game ^. characterEntityID)
+        (game ^. entities)
+
+
+character :: Lens' GameState Character
+character = positionedCharacter . positioned
+
+characterPosition :: Lens' GameState Position
+characterPosition = positionedCharacter . position
+
+-- TODO make this dynamic
+visionRadius :: Word
+visionRadius = 12
+
+-- TODO make this dynamic
+hearingRadius :: Word
+hearingRadius = 12
+
+-- | Update the revealed entities at the character's position based on their
+-- vision
+updateCharacterVision :: GameState -> GameState
+updateCharacterVision = execState $ do
+  positions <- characterVisiblePositions
+  revealedPositions <>= positions
+
+characterVisiblePositions :: MonadState GameState m => m (Set Position)
+characterVisiblePositions = do
+  charPos <- use characterPosition
+  fillWithM
+    (memo . Memo.characterVisiblePositions)
+    charPos
+    (uses entities $ visiblePositions charPos visionRadius)
+
+characterVisibleEntities :: GameState -> EntityMap.EntityMap SomeEntity
+characterVisibleEntities game =
+  let charPos = game ^. characterPosition
+  in visibleEntities charPos visionRadius $ game ^. entities
+
+positionIsCharacterVisible :: MonadState GameState m => Position -> m Bool
+positionIsCharacterVisible p = (p `elem`) <$> characterVisiblePositions
+-- ^ TODO optimize
+
+entitiesCollision
+  :: ( Functor f
+    , forall xx. MonoFoldable (f xx)
+    , Element (f SomeEntity) ~ SomeEntity
+    , Element (f (Maybe Collision)) ~ Maybe Collision
+    , Show (f (Maybe Collision))
+    , Show (f SomeEntity)
+    )
+  => f SomeEntity
+  -> Maybe Collision
+entitiesCollision = join . maximumMay . fmap entityCollision
+
+collisionAt :: MonadState GameState m => Position -> m (Maybe Collision)
+collisionAt p = uses (entities . EntityMap.atPosition p) entitiesCollision
+
+entitiesAtCharacter :: Lens' GameState (VectorBag SomeEntity)
+entitiesAtCharacter = lens getter setter
+  where
+    getter gs = gs ^. entities . EntityMap.atPosition (gs ^. characterPosition)
+    setter gs ents = gs
+      & entities . EntityMap.atPosition (gs ^. characterPosition) .~ ents
+
+-- | Returns all entities at the given position that are revealed to the
+-- character.
+--
+-- Concretely, this is either entities that are *currently* visible to the
+-- character, or entities, that are immobile and that the character has seen
+-- before
+revealedEntitiesAtPosition
+  :: MonadState GameState m
+  => Position
+  -> m (VectorBag SomeEntity)
+revealedEntitiesAtPosition p = do
+  allRev <- use $ debugState . allRevealed
+  cvps <- characterVisiblePositions
+  entitiesAtPosition <- use $ entities . EntityMap.atPosition p
+  revealed <- use revealedPositions
+  let immobileEntitiesAtPosition = filter (not . entityCanMove) entitiesAtPosition
+  pure $ if | allRev || p `member` cvps
+              -> entitiesAtPosition
+            | p `member` revealed
+              -> immobileEntitiesAtPosition
+            | otherwise
+              -> mempty
diff --git a/users/grfn/xanthous/src/Xanthous/Game/Memo.hs b/users/grfn/xanthous/src/Xanthous/Game/Memo.hs
new file mode 100644
index 0000000000..154063b5dd
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/Memo.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+-- | Memoized versions of calculations
+--------------------------------------------------------------------------------
+module Xanthous.Game.Memo
+  ( MemoState
+  , emptyMemoState
+  , clear
+    -- ** Memo lenses
+  , characterVisiblePositions
+
+    -- * Memoized values
+  , Memoized(UnMemoized)
+  , memoizeWith
+  , getMemoized
+  , runMemoized
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Data.Aeson (ToJSON, FromJSON)
+import Data.Aeson.Generic.DerivingVia
+import Test.QuickCheck (CoArbitrary, Function, Arbitrary)
+--------------------------------------------------------------------------------
+import Xanthous.Data (Position)
+import Xanthous.Data.Memo
+import Xanthous.Util.QuickCheck (GenericArbitrary(GenericArbitrary))
+--------------------------------------------------------------------------------
+
+-- | Memoized calculations on the game state
+data MemoState = MemoState
+  { -- | Memoized version of 'Xanthous.Game.Lenses.characterVisiblePositions',
+    -- memoized with the position of the character
+    _characterVisiblePositions :: Memoized Position (Set Position)
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary MemoState
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           MemoState
+makeLenses ''MemoState
+
+emptyMemoState :: MemoState
+emptyMemoState = MemoState { _characterVisiblePositions = UnMemoized }
+{-# INLINE emptyMemoState #-}
+
+clear :: ASetter' MemoState (Memoized key val) -> MemoState -> MemoState
+clear = flip set UnMemoized
+{-# INLINE clear #-}
+
+{-# ANN module ("Hlint: ignore Use newtype instead of data" :: String) #-}
diff --git a/users/grfn/xanthous/src/Xanthous/Game/Prompt.hs b/users/grfn/xanthous/src/Xanthous/Game/Prompt.hs
new file mode 100644
index 0000000000..2d6c0a280f
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/Prompt.hs
@@ -0,0 +1,359 @@
+{-# LANGUAGE DeriveFunctor        #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE StandaloneDeriving   #-}
+{-# LANGUAGE GADTs                #-}
+--------------------------------------------------------------------------------
+module Xanthous.Game.Prompt
+  ( PromptType(..)
+  , SPromptType(..)
+  , SingPromptType(..)
+  , PromptCancellable(..)
+  , PromptResult(..)
+  , PromptState(..)
+  , promptStatePosition
+  , MenuOption(..)
+  , mkMenuItems
+  , PromptInput
+  , Prompt(..)
+  , mkPrompt
+  , mkStringPrompt
+  , mkStringPromptWithDefault
+  , mkMenu
+  , mkPointOnMapPrompt
+  , mkFirePrompt
+  , isCancellable
+  , submitPrompt
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Brick.Widgets.Edit (Editor, editorText, getEditContents)
+import           Test.QuickCheck
+import           Test.QuickCheck.Arbitrary.Generic
+--------------------------------------------------------------------------------
+import           Xanthous.Util (smallestNotIn, AlphaChar (..))
+import           Xanthous.Data (Direction, Position, Tiles)
+import           Xanthous.Data.App (ResourceName)
+import qualified Xanthous.Data.App as Resource
+--------------------------------------------------------------------------------
+
+data PromptType where
+  StringPrompt    :: PromptType
+  Confirm         :: PromptType
+  Menu            :: Type -> PromptType
+  DirectionPrompt :: PromptType
+  PointOnMap      :: PromptType
+  -- | Throw an item or fire a projectile weapon. Prompt is to select the
+  -- direction
+  Fire            :: PromptType
+  Continue        :: PromptType
+  deriving stock (Generic)
+
+instance Show PromptType where
+  show StringPrompt = "StringPrompt"
+  show Confirm = "Confirm"
+  show (Menu _) = "Menu"
+  show DirectionPrompt = "DirectionPrompt"
+  show PointOnMap = "PointOnMap"
+  show Continue = "Continue"
+  show Fire = "Fire"
+
+data SPromptType :: PromptType -> Type where
+  SStringPrompt    :: SPromptType 'StringPrompt
+  SConfirm         :: SPromptType 'Confirm
+  SMenu            :: SPromptType ('Menu a)
+  SDirectionPrompt :: SPromptType 'DirectionPrompt
+  SPointOnMap      :: SPromptType 'PointOnMap
+  SContinue        :: SPromptType 'Continue
+  SFire            :: SPromptType 'Fire
+
+instance NFData (SPromptType pt) where
+  rnf SStringPrompt = ()
+  rnf SConfirm = ()
+  rnf SMenu = ()
+  rnf SDirectionPrompt = ()
+  rnf SPointOnMap = ()
+  rnf SContinue = ()
+  rnf SFire = ()
+
+class SingPromptType pt where singPromptType :: SPromptType pt
+instance SingPromptType 'StringPrompt where singPromptType = SStringPrompt
+instance SingPromptType 'Confirm where singPromptType = SConfirm
+instance SingPromptType 'DirectionPrompt where singPromptType = SDirectionPrompt
+instance SingPromptType 'PointOnMap where singPromptType = SPointOnMap
+instance SingPromptType 'Continue where singPromptType = SContinue
+instance SingPromptType 'Fire where singPromptType = SFire
+
+instance Show (SPromptType pt) where
+  show SStringPrompt    = "SStringPrompt"
+  show SConfirm         = "SConfirm"
+  show SMenu            = "SMenu"
+  show SDirectionPrompt = "SDirectionPrompt"
+  show SPointOnMap      = "SPointOnMap"
+  show SContinue        = "SContinue"
+  show SFire            = "SFire"
+
+data PromptCancellable
+  = Cancellable
+  | Uncancellable
+  deriving stock (Show, Eq, Ord, Enum, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+
+instance Arbitrary PromptCancellable where
+  arbitrary = genericArbitrary
+
+data PromptResult (pt :: PromptType) where
+  StringResult     :: Text      -> PromptResult 'StringPrompt
+  ConfirmResult    :: Bool      -> PromptResult 'Confirm
+  MenuResult       :: forall a. a    -> PromptResult ('Menu a)
+  DirectionResult  :: Direction -> PromptResult 'DirectionPrompt
+  PointOnMapResult :: Position  -> PromptResult 'PointOnMap
+  FireResult       :: Position  -> PromptResult 'Fire
+  ContinueResult   ::             PromptResult 'Continue
+
+instance Arbitrary (PromptResult 'StringPrompt) where
+  arbitrary = StringResult <$> arbitrary
+
+instance Arbitrary (PromptResult 'Confirm) where
+  arbitrary = ConfirmResult <$> arbitrary
+
+instance Arbitrary a => Arbitrary (PromptResult ('Menu a)) where
+  arbitrary = MenuResult <$> arbitrary
+
+instance Arbitrary (PromptResult 'DirectionPrompt) where
+  arbitrary = DirectionResult <$> arbitrary
+
+instance Arbitrary (PromptResult 'PointOnMap) where
+  arbitrary = PointOnMapResult <$> arbitrary
+
+instance Arbitrary (PromptResult 'Continue) where
+  arbitrary = pure ContinueResult
+
+instance Arbitrary (PromptResult 'Fire) where
+  arbitrary = FireResult <$> arbitrary
+
+--------------------------------------------------------------------------------
+
+data PromptState pt where
+  StringPromptState
+    :: Editor Text ResourceName     -> PromptState 'StringPrompt
+  DirectionPromptState  ::            PromptState 'DirectionPrompt
+  ContinuePromptState   ::            PromptState 'Continue
+  ConfirmPromptState    ::            PromptState 'Confirm
+  MenuPromptState       :: forall a.       PromptState ('Menu a)
+  PointOnMapPromptState :: Position -> PromptState 'PointOnMap
+  FirePromptState       :: Position -> PromptState 'Fire
+
+instance NFData (PromptState pt) where
+  rnf sps@(StringPromptState ed) = sps `deepseq` ed `deepseq` ()
+  rnf DirectionPromptState = ()
+  rnf ContinuePromptState = ()
+  rnf ConfirmPromptState = ()
+  rnf MenuPromptState = ()
+  rnf pomps@(PointOnMapPromptState pos) = pomps `deepseq` pos `deepseq` ()
+  rnf fps@(FirePromptState pos) = fps `deepseq` pos `deepseq` ()
+
+instance Arbitrary (PromptState 'StringPrompt) where
+  arbitrary = StringPromptState <$> arbitrary
+
+instance Arbitrary (PromptState 'DirectionPrompt) where
+  arbitrary = pure DirectionPromptState
+
+instance Arbitrary (PromptState 'Continue) where
+  arbitrary = pure ContinuePromptState
+
+instance Arbitrary (PromptState ('Menu a)) where
+  arbitrary = pure MenuPromptState
+
+instance Arbitrary (PromptState 'Fire) where
+  arbitrary = FirePromptState <$> arbitrary
+
+instance CoArbitrary (PromptState 'StringPrompt) where
+  coarbitrary (StringPromptState ed) = coarbitrary ed
+
+instance CoArbitrary (PromptState 'DirectionPrompt) where
+  coarbitrary DirectionPromptState = coarbitrary ()
+
+instance CoArbitrary (PromptState 'Continue) where
+  coarbitrary ContinuePromptState = coarbitrary ()
+
+instance CoArbitrary (PromptState ('Menu a)) where
+  coarbitrary MenuPromptState = coarbitrary ()
+
+instance CoArbitrary (PromptState 'Fire) where
+  coarbitrary (FirePromptState pos) = coarbitrary pos
+
+deriving stock instance Show (PromptState pt)
+
+-- | Traversal over the position for the prompt types with positions in their
+-- prompt state (currently 'Fire' and 'PointOnMap')
+promptStatePosition :: forall pt. Traversal' (PromptState pt) Position
+promptStatePosition _ ps@(StringPromptState _) = pure ps
+promptStatePosition _ DirectionPromptState = pure DirectionPromptState
+promptStatePosition _ ContinuePromptState = pure ContinuePromptState
+promptStatePosition _ ConfirmPromptState = pure ConfirmPromptState
+promptStatePosition _ MenuPromptState = pure MenuPromptState
+promptStatePosition f (PointOnMapPromptState p) = PointOnMapPromptState <$> f p
+promptStatePosition f (FirePromptState p) = FirePromptState <$> f p
+
+data MenuOption a = MenuOption Text a
+  deriving stock (Eq, Generic, Functor)
+  deriving anyclass (NFData, CoArbitrary, Function)
+
+instance Comonad MenuOption where
+  extract (MenuOption _ x) = x
+  extend cok mo@(MenuOption text _) = MenuOption text (cok mo)
+
+mkMenuItems :: (MonoFoldable f, Element f ~ (Char, MenuOption a))
+            => f
+            -> Map Char (MenuOption a)
+mkMenuItems = flip foldl' mempty $ \items (chr, option) ->
+  let chr' = if has (ix chr) items
+             then getAlphaChar . smallestNotIn . map AlphaChar $ keys items
+             else chr
+  in items & at chr' ?~ option
+
+instance Show (MenuOption a) where
+  show (MenuOption m _) = show m
+
+type family PromptInput (pt :: PromptType) :: Type where
+  PromptInput ('Menu a)     = Map Char (MenuOption a)
+  PromptInput 'PointOnMap   = Position -- Character pos
+  PromptInput 'Fire         = (Position, Tiles) -- Nearest enemy, range
+  PromptInput 'StringPrompt = Maybe Text -- Default value
+  PromptInput _ = ()
+
+data Prompt (m :: Type -> Type) where
+  Prompt
+    :: forall (pt :: PromptType)
+        (m :: Type -> Type).
+      PromptCancellable
+    -> SPromptType pt
+    -> PromptState pt
+    -> PromptInput pt
+    -> (PromptResult pt -> m ())
+    -> Prompt m
+
+instance Show (Prompt m) where
+  show (Prompt c pt ps pri _)
+    = "(Prompt "
+    <> show c <> " "
+    <> show pt <> " "
+    <> show ps <> " "
+    <> showPri
+    <> " <function>)"
+    where showPri = case pt of
+            SMenu -> show pri
+            _ -> "()"
+
+instance NFData (Prompt m) where
+  rnf (Prompt c SMenu ps pri cb)
+            = c
+    `deepseq` ps
+    `deepseq` pri
+    `seq` cb
+    `seq` ()
+  rnf (Prompt c spt ps pri cb)
+            = c
+    `deepseq` spt
+    `deepseq` ps
+    `deepseq` pri
+    `seq` cb
+    `seq` ()
+
+instance CoArbitrary (m ()) => CoArbitrary (Prompt m) where
+  coarbitrary (Prompt c SStringPrompt ps pri cb) =
+    variant @Int 1 . coarbitrary (c, ps, pri, cb)
+  coarbitrary (Prompt c SConfirm _ pri cb) = -- TODO fill in prompt state
+    variant @Int 2 . coarbitrary (c, pri, cb)
+  coarbitrary (Prompt c SMenu _ps _pri _cb) =
+    variant @Int 3 . coarbitrary c {-, ps, pri, cb -}
+  coarbitrary (Prompt c SDirectionPrompt ps pri cb) =
+    variant @Int 4 . coarbitrary (c, ps, pri, cb)
+  coarbitrary (Prompt c SPointOnMap _ pri cb) = -- TODO fill in prompt state
+    variant @Int 5 . coarbitrary (c, pri, cb)
+  coarbitrary (Prompt c SContinue ps pri cb) =
+    variant @Int 6 . coarbitrary (c, ps, pri, cb)
+  coarbitrary (Prompt c SFire ps pri cb) =
+    variant @Int 7 . coarbitrary (c, ps, pri, cb)
+
+-- instance Function (Prompt m) where
+--   function = functionMap toTuple _fromTuple
+--     where
+--       toTuple (Prompt c pt ps pri cb) = (c, pt, ps, pri, cb)
+
+
+mkPrompt
+  :: (PromptInput pt ~ ())
+  => PromptCancellable       -- ^ Is the prompt cancellable or not?
+  -> SPromptType pt          -- ^ The type of the prompt
+  -> (PromptResult pt -> m ()) -- ^ Function to call when the prompt is complete
+  -> Prompt m
+mkPrompt c pt@SDirectionPrompt cb = Prompt c pt DirectionPromptState () cb
+mkPrompt c pt@SContinue cb = Prompt c pt ContinuePromptState () cb
+mkPrompt c pt@SConfirm cb = Prompt c pt ConfirmPromptState () cb
+
+mkStringPrompt
+  :: PromptCancellable                  -- ^ Is the prompt cancellable or not?
+  -> (PromptResult 'StringPrompt -> m ()) -- ^ Function to call when the prompt is complete
+  -> Prompt m
+mkStringPrompt c =
+  let ps = StringPromptState $ editorText Resource.Prompt (Just 1) ""
+  in Prompt c SStringPrompt ps Nothing
+
+mkStringPromptWithDefault
+  :: PromptCancellable                  -- ^ Is the prompt cancellable or not?
+  -> Text                               -- ^ Default value for the prompt
+  -> (PromptResult 'StringPrompt -> m ()) -- ^ Function to call when the prompt is complete
+  -> Prompt m
+mkStringPromptWithDefault c def =
+  let ps = StringPromptState $ editorText Resource.Prompt (Just 1) ""
+  in Prompt c SStringPrompt ps (Just def)
+
+mkMenu
+  :: forall a m.
+    PromptCancellable
+  -> Map Char (MenuOption a) -- ^ Menu items
+  -> (PromptResult ('Menu a) -> m ())
+  -> Prompt m
+mkMenu c = Prompt c SMenu MenuPromptState
+
+mkPointOnMapPrompt
+  :: PromptCancellable
+  -> Position
+  -> (PromptResult 'PointOnMap -> m ())
+  -> Prompt m
+mkPointOnMapPrompt c pos = Prompt c SPointOnMap (PointOnMapPromptState pos) pos
+
+mkFirePrompt
+  :: PromptCancellable
+  -> Position -- ^ Initial position
+  -> Tiles    -- ^ Range
+  -> (PromptResult 'Fire -> m ())
+  -> Prompt m
+mkFirePrompt c pos range = Prompt c SFire (FirePromptState pos) (pos, range)
+
+isCancellable :: Prompt m -> Bool
+isCancellable (Prompt Cancellable _ _ _ _)   = True
+isCancellable (Prompt Uncancellable _ _ _ _) = False
+
+submitPrompt :: Applicative m => Prompt m -> m ()
+submitPrompt (Prompt _ pt ps pri cb) =
+  case (pt, ps, pri) of
+    (SStringPrompt, StringPromptState edit, mDef) ->
+      let inputVal = mconcat . getEditContents $ edit
+          val | null inputVal, Just def <- mDef = def
+              | otherwise = inputVal
+      in cb $ StringResult val
+    (SDirectionPrompt, DirectionPromptState, _) ->
+      pure () -- Don't use submit with a direction prompt
+    (SContinue, ContinuePromptState, _) ->
+      cb ContinueResult
+    (SMenu, MenuPromptState, _) ->
+      pure () -- Don't use submit with a menu prompt
+    (SPointOnMap, PointOnMapPromptState pos, _) ->
+      cb $ PointOnMapResult pos
+    (SConfirm, ConfirmPromptState, _) ->
+      cb $ ConfirmResult True
+    (SFire, FirePromptState pos, _) ->
+      cb $ FireResult pos
diff --git a/users/grfn/xanthous/src/Xanthous/Game/State.hs b/users/grfn/xanthous/src/Xanthous/Game/State.hs
new file mode 100644
index 0000000000..13b1ba1588
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Game/State.hs
@@ -0,0 +1,572 @@
+{-# LANGUAGE StandaloneDeriving   #-}
+{-# LANGUAGE RecordWildCards      #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE TemplateHaskell      #-}
+{-# LANGUAGE GADTs                #-}
+{-# LANGUAGE AllowAmbiguousTypes  #-}
+--------------------------------------------------------------------------------
+module Xanthous.Game.State
+  ( GameState(..)
+  , entities
+  , levels
+  , revealedPositions
+  , messageHistory
+  , randomGen
+  , activePanel
+  , promptState
+  , characterEntityID
+  , autocommand
+  , savefile
+  , memo
+  , GamePromptState(..)
+
+    -- * Game Level
+  , GameLevel(..)
+  , levelEntities
+  , upStaircasePosition
+  , levelRevealedPositions
+
+    -- * Messages
+  , MessageHistory(..)
+  , HasMessages(..)
+  , HasTurn(..)
+  , HasDisplayedTurn(..)
+  , pushMessage
+  , previousMessage
+  , nextTurn
+
+    -- * Autocommands
+  , Autocommand(..)
+  , AutocommandState(..)
+  , _NoAutocommand
+  , _ActiveAutocommand
+
+    -- * App monad
+  , AppT(..)
+  , AppM
+  , runAppT
+
+    -- * Entities
+  , Draw(..)
+  , Brain(..)
+  , Brainless(..)
+  , brainVia
+  , Collision(..)
+  , Entity(..)
+  , SomeEntity(..)
+  , downcastEntity
+  , _SomeEntity
+  , entityIs
+  , entityTypeName
+
+    -- ** Vias
+  , Color(..)
+  , DrawNothing(..)
+  , DrawRawChar(..)
+  , DrawRawCharPriority(..)
+  , DrawCharacter(..)
+  , DrawStyledCharacter(..)
+  , DeriveEntity(..)
+    -- ** Field classes
+  , HasChar(..)
+  , HasStyle(..)
+
+    -- * Debug State
+  , DebugState(..)
+  , debugState
+  , allRevealed
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.List.NonEmpty ( NonEmpty((:|)))
+import qualified Data.List.NonEmpty as NonEmpty
+import           Data.Typeable
+import           Data.Coerce
+import           System.Random
+import           Test.QuickCheck
+import           Test.QuickCheck.Arbitrary.Generic
+import           Control.Monad.Random.Class
+import           Control.Monad.State
+import           Control.Monad.Trans.Control (MonadTransControl(..))
+import           Control.Monad.Trans.Compose
+import           Control.Monad.Morph (MFunctor(..))
+import           Brick (EventM, Widget, raw, str, emptyWidget)
+import           Data.Aeson (ToJSON(..), FromJSON(..), Value(Null))
+import qualified Data.Aeson as JSON
+import           Data.Aeson.Generic.DerivingVia
+import           Data.Generics.Product.Fields
+import qualified Graphics.Vty.Attributes as Vty
+import qualified Graphics.Vty.Image as Vty
+--------------------------------------------------------------------------------
+import           Xanthous.Util (KnownBool(..))
+import           Xanthous.Data
+import           Xanthous.Data.App
+import           Xanthous.Data.Levels
+import           Xanthous.Data.EntityMap (EntityMap, EntityID)
+import           Xanthous.Data.EntityChar
+import           Xanthous.Data.VectorBag
+import           Xanthous.Data.Entities
+import           Xanthous.Orphans ()
+import           Xanthous.Game.Prompt
+import           Xanthous.Game.Env
+import           Xanthous.Game.Memo (MemoState)
+--------------------------------------------------------------------------------
+
+data MessageHistory
+  = MessageHistory
+  { _messages      :: Map Word (NonEmpty Text)
+  , _turn          :: Word
+  , _displayedTurn :: Maybe Word
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary MessageHistory
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           MessageHistory
+makeFieldsNoPrefix ''MessageHistory
+
+instance Semigroup MessageHistory where
+  (MessageHistory msgs₁ turn₁ dt₁) <> (MessageHistory msgs₂ turn₂ dt₂) =
+    MessageHistory (msgs₁ <> msgs₂) (max turn₁ turn₂) $ case (dt₁, dt₂) of
+      (_, Nothing)      -> Nothing
+      (Just t, _)       -> Just t
+      (Nothing, Just t) -> Just t
+
+instance Monoid MessageHistory where
+  mempty = MessageHistory mempty 0 Nothing
+
+type instance Element MessageHistory = [Text]
+instance MonoFunctor MessageHistory where
+  omap f mh@(MessageHistory _ t _) =
+    mh & messages . at t %~ (NonEmpty.nonEmpty . f . toList =<<)
+
+instance MonoComonad MessageHistory where
+  oextract (MessageHistory ms t dt) = maybe [] toList $ ms ^. at (fromMaybe t dt)
+  oextend cok mh@(MessageHistory _ t dt) =
+    mh & messages . at (fromMaybe t dt) .~ NonEmpty.nonEmpty (cok mh)
+
+pushMessage :: Text -> MessageHistory -> MessageHistory
+pushMessage msg mh@(MessageHistory _ turn' _) =
+  mh
+  & messages . at turn' %~ \case
+    Nothing -> Just $ msg :| mempty
+    Just msgs -> Just $ msg <| msgs
+  & displayedTurn .~ Nothing
+
+nextTurn :: MessageHistory -> MessageHistory
+nextTurn = (turn +~ 1) . (displayedTurn .~ Nothing)
+
+previousMessage :: MessageHistory -> MessageHistory
+previousMessage mh = mh & displayedTurn .~ maximumOf
+  (messages . ifolded . asIndex . filtered (< mh ^. turn))
+  mh
+
+
+--------------------------------------------------------------------------------
+
+data GamePromptState m where
+  NoPrompt :: GamePromptState m
+  WaitingPrompt :: Text -> Prompt m -> GamePromptState m
+  deriving stock (Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Non-injective! We never try to serialize waiting prompts, since:
+--
+--  * they contain callback functions
+--  * we can't save the game when in a prompt anyway
+instance ToJSON (GamePromptState m) where
+  toJSON _ = Null
+
+-- | Always expects Null
+instance FromJSON (GamePromptState m) where
+  parseJSON Null = pure NoPrompt
+  parseJSON _ = fail "Invalid GamePromptState; expected null"
+
+instance CoArbitrary (GamePromptState m) where
+  coarbitrary NoPrompt = variant @Int 1
+  coarbitrary (WaitingPrompt txt _) = variant @Int 2 . coarbitrary txt
+
+instance Function (GamePromptState m) where
+  function = functionMap onlyNoPrompt (const NoPrompt)
+    where
+      onlyNoPrompt NoPrompt = ()
+      onlyNoPrompt (WaitingPrompt _ _) =
+        error "Can't handle prompts in Function!"
+
+--------------------------------------------------------------------------------
+
+newtype AppT m a
+  = AppT { unAppT :: ReaderT GameEnv (StateT GameState m) a }
+  deriving ( Functor
+           , Applicative
+           , Monad
+           , MonadState GameState
+           , MonadReader GameEnv
+           , MonadIO
+           )
+       via (ReaderT GameEnv (StateT GameState m))
+  deriving ( MonadTrans
+           , MFunctor
+           )
+       via (ReaderT GameEnv `ComposeT` StateT GameState)
+
+type AppM = AppT (EventM ResourceName)
+
+--------------------------------------------------------------------------------
+
+class Draw a where
+  drawWithNeighbors :: Neighbors (VectorBag SomeEntity) -> a -> Widget n
+  drawWithNeighbors = const draw
+
+  draw :: a -> Widget n
+  draw = drawWithNeighbors $ pure mempty
+
+  -- | higher priority gets drawn on top
+  drawPriority :: a -> Word
+  drawPriority = const minBound
+
+instance Draw a => Draw (Positioned a) where
+  drawWithNeighbors ns (Positioned _ a) = drawWithNeighbors ns a
+  draw (Positioned _ a) = draw a
+
+newtype DrawCharacter (char :: Symbol) (a :: Type) where
+  DrawCharacter :: a -> DrawCharacter char a
+
+instance KnownSymbol char => Draw (DrawCharacter char a) where
+  draw _ = str $ symbolVal @char Proxy
+
+data Color = Black | Red | Green | Yellow | Blue | Magenta | Cyan | White
+
+class KnownColor (color :: Color) where
+  colorVal :: forall proxy. proxy color -> Vty.Color
+
+instance KnownColor 'Black where colorVal _ = Vty.black
+instance KnownColor 'Red where colorVal _ = Vty.red
+instance KnownColor 'Green where colorVal _ = Vty.green
+instance KnownColor 'Yellow where colorVal _ = Vty.yellow
+instance KnownColor 'Blue where colorVal _ = Vty.blue
+instance KnownColor 'Magenta where colorVal _ = Vty.magenta
+instance KnownColor 'Cyan where colorVal _ = Vty.cyan
+instance KnownColor 'White where colorVal _ = Vty.white
+
+class KnownMaybeColor (maybeColor :: Maybe Color) where
+  maybeColorVal :: forall proxy. proxy maybeColor -> Maybe Vty.Color
+
+instance KnownMaybeColor 'Nothing where maybeColorVal _ = Nothing
+instance KnownColor color => KnownMaybeColor ('Just color) where
+  maybeColorVal _ = Just $ colorVal @color Proxy
+
+newtype DrawStyledCharacter (fg :: Maybe Color) (bg :: Maybe Color) (char :: Symbol) (a :: Type) where
+  DrawStyledCharacter :: a -> DrawStyledCharacter fg bg char a
+
+instance
+  ( KnownMaybeColor fg
+  , KnownMaybeColor bg
+  , KnownSymbol char
+  )
+  => Draw (DrawStyledCharacter fg bg char a) where
+  draw _ = raw $ Vty.string attr $ symbolVal @char Proxy
+    where attr = Vty.Attr
+            { Vty.attrStyle = Vty.Default
+            , Vty.attrForeColor = maybe Vty.Default Vty.SetTo
+                                  $ maybeColorVal @fg Proxy
+            , Vty.attrBackColor = maybe Vty.Default Vty.SetTo
+                                  $ maybeColorVal @bg Proxy
+            , Vty.attrURL = Vty.Default
+            }
+
+instance Draw EntityChar where
+  draw EntityChar{..} = raw $ Vty.string _style [_char]
+
+--------------------------------------------------------------------------------
+
+newtype DrawNothing (a :: Type) = DrawNothing a
+
+instance Draw (DrawNothing a) where
+  draw = const emptyWidget
+  drawPriority = const 0
+
+newtype DrawRawChar (rawField :: Symbol) (a :: Type) = DrawRawChar a
+
+instance
+  forall rawField a raw.
+  ( HasField rawField a a raw raw
+  , HasChar raw EntityChar
+  ) => Draw (DrawRawChar rawField a) where
+  draw (DrawRawChar e) = draw $ e ^. field @rawField . char
+
+newtype DrawRawCharPriority
+  (rawField :: Symbol)
+  (priority :: Nat)
+  (a :: Type)
+  = DrawRawCharPriority a
+
+instance
+  forall rawField priority a raw.
+  ( HasField rawField a a raw raw
+  , KnownNat priority
+  , HasChar raw EntityChar
+  ) => Draw (DrawRawCharPriority rawField priority a) where
+  draw (DrawRawCharPriority e) = draw $ e ^. field @rawField . char
+  drawPriority = const . fromIntegral $ natVal @priority Proxy
+
+
+--------------------------------------------------------------------------------
+
+class Brain a where
+  step :: Ticks -> Positioned a -> AppM (Positioned a)
+  -- | Does this entity ever move on its own?
+  entityCanMove :: a -> Bool
+  entityCanMove = const False
+
+newtype Brainless a = Brainless a
+
+instance Brain (Brainless a) where
+  step = const pure
+
+-- | Workaround for the inability to use DerivingVia on Brain due to the lack of
+-- higher-order roles (specifically AppT not having its last type argument have
+-- role representational bc of StateT)
+brainVia
+  :: forall brain entity. (Coercible entity brain, Brain brain)
+  => (entity -> brain) -- ^ constructor, ignored
+  -> (Ticks -> Positioned entity -> AppM (Positioned entity))
+brainVia _ ticks = fmap coerce . step ticks . coerce @_ @(Positioned brain)
+
+--------------------------------------------------------------------------------
+
+class ( Show a, Eq a, Ord a, NFData a
+      , ToJSON a, FromJSON a
+      , Draw a, Brain a
+      ) => Entity a where
+  entityAttributes :: a -> EntityAttributes
+  entityAttributes = const defaultEntityAttributes
+  description :: a -> Text
+  entityChar :: a -> EntityChar
+  entityCollision :: a -> Maybe Collision
+  entityCollision = const $ Just Stop
+
+data SomeEntity where
+  SomeEntity :: forall a. (Entity a, Typeable a) => a -> SomeEntity
+
+instance Show SomeEntity where
+  show (SomeEntity e) = "SomeEntity (" <> show e <> ")"
+
+instance Eq SomeEntity where
+  (SomeEntity (a :: ea)) == (SomeEntity (b :: eb)) = case eqT @ea @eb of
+    Just Refl -> a == b
+    _ -> False
+
+instance Ord SomeEntity where
+  compare (SomeEntity (a :: ea)) (SomeEntity (b :: eb)) = case eqT @ea @eb of
+    Just Refl -> compare a b
+    _ -> compare (typeRep $ Proxy @ea) (typeRep $ Proxy @eb)
+
+
+instance NFData SomeEntity where
+  rnf (SomeEntity ent) = ent `deepseq` ()
+
+instance ToJSON SomeEntity where
+  toJSON (SomeEntity ent) = entityToJSON ent
+    where
+      entityToJSON :: forall entity. (Entity entity, Typeable entity)
+                   => entity -> JSON.Value
+      entityToJSON entity = JSON.object
+        [ "type" JSON..= tshow (typeRep @_ @entity Proxy)
+        , "data" JSON..= toJSON entity
+        ]
+
+instance Draw SomeEntity where
+  drawWithNeighbors ns (SomeEntity ent) = drawWithNeighbors ns ent
+  drawPriority (SomeEntity ent) = drawPriority ent
+
+instance Brain SomeEntity where
+  step ticks (Positioned p (SomeEntity ent)) =
+    fmap SomeEntity <$> step ticks (Positioned p ent)
+  entityCanMove (SomeEntity ent) = entityCanMove ent
+
+downcastEntity :: forall (a :: Type). (Typeable a) => SomeEntity -> Maybe a
+downcastEntity (SomeEntity e) = cast e
+
+entityIs :: forall (a :: Type). (Typeable a) => SomeEntity -> Bool
+entityIs = isJust . downcastEntity @a
+
+_SomeEntity :: forall a. (Entity a, Typeable a) => Prism' SomeEntity a
+_SomeEntity = prism' SomeEntity downcastEntity
+
+-- | Get the name of the type of 'SomeEntity' as a string
+entityTypeName :: SomeEntity -> Text
+entityTypeName (SomeEntity e) = pack . tyConName . typeRepTyCon $ typeOf e
+
+newtype DeriveEntity
+  (blocksVision :: Bool)
+  (description :: Symbol)
+  (entityChar :: Symbol)
+  (entity :: Type)
+  = DeriveEntity entity
+  deriving newtype (Show, Eq, Ord, NFData, ToJSON, FromJSON, Draw)
+
+instance Brain entity => Brain (DeriveEntity b d c entity) where
+  step = brainVia $ \(DeriveEntity e) -> e
+
+instance
+  ( KnownBool blocksVision
+  , KnownSymbol description
+  , KnownSymbol entityChar
+  , Show entity, Eq entity, Ord entity, NFData entity
+  , ToJSON entity, FromJSON entity
+  , Draw entity, Brain entity
+  )
+  => Entity (DeriveEntity blocksVision description entityChar entity) where
+  entityAttributes _ = defaultEntityAttributes
+    & blocksVision .~ boolVal @blocksVision
+  description _ = pack . symbolVal $ Proxy @description
+  entityChar _ = fromString . symbolVal $ Proxy @entityChar
+
+--------------------------------------------------------------------------------
+
+data GameLevel = GameLevel
+  { _levelEntities :: !(EntityMap SomeEntity)
+  , _upStaircasePosition :: !Position
+  , _levelRevealedPositions :: !(Set Position)
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+  deriving (ToJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           GameLevel
+
+--------------------------------------------------------------------------------
+
+data Autocommand
+  = AutoMove Direction
+  | AutoRest
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (NFData, Hashable, ToJSON, FromJSON, CoArbitrary, Function)
+  deriving Arbitrary via GenericArbitrary Autocommand
+{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
+
+data AutocommandState
+  = NoAutocommand
+  | ActiveAutocommand Autocommand (Async ())
+  deriving stock (Eq, Ord, Generic)
+  deriving anyclass (Hashable)
+
+instance Show AutocommandState where
+  show NoAutocommand = "NoAutocommand"
+  show (ActiveAutocommand ac _) =
+    "(ActiveAutocommand " <> show ac <> " <Async>)"
+
+instance ToJSON AutocommandState where
+  toJSON = const Null
+
+instance FromJSON AutocommandState where
+  parseJSON Null = pure NoAutocommand
+  parseJSON _ = fail "Invalid AutocommandState; expected null"
+
+instance NFData AutocommandState where
+  rnf NoAutocommand = ()
+  rnf (ActiveAutocommand ac t) = ac `deepseq` t `seq` ()
+
+instance CoArbitrary AutocommandState where
+  coarbitrary NoAutocommand = variant @Int 1
+  coarbitrary (ActiveAutocommand ac t)
+    = variant @Int 2
+    . coarbitrary ac
+    . coarbitrary (hash t)
+
+instance Function AutocommandState where
+  function = functionMap onlyNoAC (const NoAutocommand)
+    where
+      onlyNoAC NoAutocommand = ()
+      onlyNoAC _ = error "Can't handle autocommands in Function"
+
+--------------------------------------------------------------------------------
+
+
+data DebugState = DebugState
+  { _allRevealed :: !Bool
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           DebugState
+{-# ANN DebugState ("HLint: ignore Use newtype instead of data" :: String) #-}
+
+instance Arbitrary DebugState where
+  arbitrary = genericArbitrary
+
+data GameState = GameState
+  { _levels            :: !(Levels GameLevel)
+  , _characterEntityID :: !EntityID
+  , _messageHistory    :: !MessageHistory
+  , _randomGen         :: !StdGen
+
+    -- | The active panel displayed in the UI, if any
+  , _activePanel       :: !(Maybe Panel)
+
+  , _promptState       :: !(GamePromptState AppM)
+  , _debugState        :: !DebugState
+  , _autocommand       :: !AutocommandState
+
+  -- | The path to the savefile that was loaded for this game, if any
+  , _savefile          :: !(Maybe FilePath)
+
+  , _memo              :: MemoState
+  }
+  deriving stock (Show, Generic)
+  deriving anyclass (NFData)
+  deriving (ToJSON)
+       via WithOptions '[ FieldLabelModifier '[Drop 1] ]
+           GameState
+
+makeLenses ''GameLevel
+makeLenses ''GameState
+
+entities :: Lens' GameState (EntityMap SomeEntity)
+entities = levels . current . levelEntities
+
+revealedPositions :: Lens' GameState (Set Position)
+revealedPositions = levels . current . levelRevealedPositions
+
+instance Eq GameState where
+  (==) = (==) `on` \gs ->
+    ( gs ^. entities
+    , gs ^. revealedPositions
+    , gs ^. characterEntityID
+    , gs ^. messageHistory
+    , gs ^. activePanel
+    , gs ^. debugState
+    )
+
+--------------------------------------------------------------------------------
+
+runAppT :: Monad m => AppT m a -> GameEnv -> GameState -> m (a, GameState)
+runAppT appt env initialState
+  = flip runStateT initialState
+  . flip runReaderT env
+  . unAppT
+  $ appt
+
+instance (Monad m) => MonadRandom (AppT m) where
+  getRandomR rng = randomGen %%= randomR rng
+  getRandom = randomGen %%= random
+  getRandomRs rng = uses randomGen $ randomRs rng
+  getRandoms = uses randomGen randoms
+
+instance MonadTransControl AppT where
+  type StT AppT a = (a, GameState)
+  liftWith f
+    = AppT
+    . ReaderT $ \e
+    -> StateT $ \s
+    -> (,s) <$> f (\action -> runAppT action e s)
+  restoreT = AppT . ReaderT . const . StateT . const
+
+--------------------------------------------------------------------------------
+
+makeLenses ''DebugState
+makePrisms ''AutocommandState
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Level.hs b/users/grfn/xanthous/src/Xanthous/Generators/Level.hs
new file mode 100644
index 0000000000..fc57402e7d
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Level.hs
@@ -0,0 +1,172 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE GADTs           #-}
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level
+  ( generate
+  , Generator(..)
+  , SGenerator(..)
+  , GeneratorInput(..)
+  , generateFromInput
+  , parseGeneratorInput
+  , showCells
+  , Level(..)
+  , levelWalls
+  , levelItems
+  , levelCreatures
+  , levelDoors
+  , levelCharacterPosition
+  , levelTutorialMessage
+  , levelExtra
+  , generateLevel
+  , levelToEntityMap
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Data.Array.Unboxed
+import qualified Options.Applicative as Opt
+import           Control.Monad.Random
+--------------------------------------------------------------------------------
+import qualified Xanthous.Generators.Level.CaveAutomata as CaveAutomata
+import qualified Xanthous.Generators.Level.Dungeon as Dungeon
+import           Xanthous.Generators.Level.Util
+import           Xanthous.Generators.Level.LevelContents
+import           Xanthous.Generators.Level.Village as Village
+import           Xanthous.Data (Dimensions, Position'(Position), Position)
+import           Xanthous.Data.EntityMap (EntityMap, _EntityMap)
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Entities.Environment
+import           Xanthous.Entities.Item (Item)
+import           Xanthous.Entities.Creature (Creature)
+import           Xanthous.Game.State (SomeEntity(..))
+import           Linear.V2
+--------------------------------------------------------------------------------
+
+data Generator
+  = CaveAutomata
+  | Dungeon
+  deriving stock (Show, Eq)
+
+data SGenerator (gen :: Generator) where
+  SCaveAutomata :: SGenerator 'CaveAutomata
+  SDungeon :: SGenerator 'Dungeon
+
+type family Params (gen :: Generator) :: Type where
+  Params 'CaveAutomata = CaveAutomata.Params
+  Params 'Dungeon = Dungeon.Params
+
+generate
+  :: RandomGen g
+  => SGenerator gen
+  -> Params gen
+  -> Dimensions
+  -> g
+  -> Cells
+generate SCaveAutomata = CaveAutomata.generate
+generate SDungeon = Dungeon.generate
+
+data GeneratorInput where
+  GeneratorInput :: forall gen. SGenerator gen -> Params gen -> GeneratorInput
+
+generateFromInput :: RandomGen g => GeneratorInput -> Dimensions -> g -> Cells
+generateFromInput (GeneratorInput sg ps) = generate sg ps
+
+parseGeneratorInput :: Opt.Parser GeneratorInput
+parseGeneratorInput = Opt.subparser
+  $ generatorCommand SCaveAutomata
+      "cave"
+      "Cellular-automata based cave generator"
+      CaveAutomata.parseParams
+  <> generatorCommand SDungeon
+      "dungeon"
+      "Classic dungeon map generator"
+      Dungeon.parseParams
+  where
+    generatorCommand sgen name desc parseParams =
+      Opt.command name
+        (Opt.info
+          (GeneratorInput sgen <$> parseParams)
+          (Opt.progDesc desc)
+        )
+
+
+showCells :: Cells -> Text
+showCells arr =
+  let (V2 minX minY, V2 maxX maxY) = bounds arr
+      showCellVal True = "x"
+      showCellVal False = " "
+      showCell = showCellVal . (arr !)
+      row r = foldMap (showCell . (`V2` r)) [minX..maxX]
+      rows = row <$> [minY..maxY]
+  in intercalate "\n" rows
+
+cellsToWalls :: Cells -> EntityMap Wall
+cellsToWalls cells = foldl' maybeInsertWall mempty . assocs $ cells
+  where
+    maybeInsertWall em (pos@(V2 x y), True)
+      | not (surroundedOnAllSides pos) =
+        let x' = fromIntegral x
+            y' = fromIntegral y
+        in EntityMap.insertAt (Position x' y') Wall em
+    maybeInsertWall em _ = em
+    surroundedOnAllSides pos = numAliveNeighbors cells pos == 8
+
+--------------------------------------------------------------------------------
+
+data Level = Level
+  { _levelWalls             :: !(EntityMap Wall)
+  , _levelDoors             :: !(EntityMap Door)
+  , _levelItems             :: !(EntityMap Item)
+  , _levelCreatures         :: !(EntityMap Creature)
+  , _levelTutorialMessage   :: !(EntityMap GroundMessage)
+  , _levelStaircases        :: !(EntityMap Staircase)
+  , _levelExtra             :: !(EntityMap SomeEntity) -- ^ TODO this is a bit of a hack...
+  , _levelCharacterPosition :: !Position
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+makeLenses ''Level
+
+generateLevel
+  :: MonadRandom m
+  => SGenerator gen
+  -> Params gen
+  -> Dimensions
+  -> Word -- ^ Level number, starting at 0
+  -> m Level
+generateLevel gen ps dims num = do
+  rand <- mkStdGen <$> getRandom
+  let cells = generate gen ps dims rand
+      _levelWalls = cellsToWalls cells
+  village <- generateVillage cells gen
+  let _levelExtra = village
+  _levelItems <- randomItems cells
+  _levelCreatures <- randomCreatures num cells
+  _levelDoors <- randomDoors cells
+  _levelCharacterPosition <- chooseCharacterPosition cells
+  let upStaircase = _EntityMap # [(_levelCharacterPosition, UpStaircase)]
+  downStaircase <- placeDownStaircase cells
+  let _levelStaircases = upStaircase <> downStaircase
+  _levelTutorialMessage <-
+    if num == 0
+    then tutorialMessage cells _levelCharacterPosition
+    else pure mempty
+  pure Level {..}
+
+levelToEntityMap :: Level -> EntityMap SomeEntity
+levelToEntityMap level
+  = (SomeEntity <$> level ^. levelWalls)
+  <> (SomeEntity <$> level ^. levelDoors)
+  <> (SomeEntity <$> level ^. levelItems)
+  <> (SomeEntity <$> level ^. levelCreatures)
+  <> (SomeEntity <$> level ^. levelTutorialMessage)
+  <> (SomeEntity <$> level ^. levelStaircases)
+  <> (level ^. levelExtra)
+
+generateVillage
+  :: MonadRandom m
+  => Cells -- ^ Wall positions
+  -> SGenerator gen
+  -> m (EntityMap SomeEntity)
+generateVillage wallPositions SCaveAutomata = Village.fromCave wallPositions
+generateVillage _ _ = pure mempty
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Level/CaveAutomata.hs b/users/grfn/xanthous/src/Xanthous/Generators/Level/CaveAutomata.hs
new file mode 100644
index 0000000000..03d534ca39
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Level/CaveAutomata.hs
@@ -0,0 +1,112 @@
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level.CaveAutomata
+  ( Params(..)
+  , defaultParams
+  , parseParams
+  , generate
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Control.Monad.Random (RandomGen, runRandT)
+import           Data.Array.ST
+import           Data.Array.Unboxed
+import qualified Options.Applicative as Opt
+--------------------------------------------------------------------------------
+import           Xanthous.Util (between)
+import           Xanthous.Util.Optparse
+import           Xanthous.Data (Dimensions, width, height)
+import           Xanthous.Generators.Level.Util
+import           Linear.V2
+--------------------------------------------------------------------------------
+
+data Params = Params
+  { _aliveStartChance :: Double
+  , _birthLimit :: Word
+  , _deathLimit :: Word
+  , _steps :: Word
+  }
+  deriving stock (Show, Eq, Generic)
+makeLenses ''Params
+
+defaultParams :: Params
+defaultParams = Params
+  { _aliveStartChance = 0.6
+  , _birthLimit = 3
+  , _deathLimit = 4
+  , _steps = 4
+  }
+
+parseParams :: Opt.Parser Params
+parseParams = Params
+  <$> Opt.option parseChance
+      ( Opt.long "alive-start-chance"
+      <> Opt.value (defaultParams ^. aliveStartChance)
+      <> Opt.showDefault
+      <> Opt.help ( "Chance for each cell to start alive at the beginning of "
+                 <> "the cellular automata"
+                 )
+      <> Opt.metavar "CHANCE"
+      )
+  <*> Opt.option parseNeighbors
+      ( Opt.long "birth-limit"
+      <> Opt.value (defaultParams ^. birthLimit)
+      <> Opt.showDefault
+      <> Opt.help "Minimum neighbor count required for birth of a cell"
+      <> Opt.metavar "NEIGHBORS"
+      )
+  <*> Opt.option parseNeighbors
+      ( Opt.long "death-limit"
+      <> Opt.value (defaultParams ^. deathLimit)
+      <> Opt.showDefault
+      <> Opt.help "Maximum neighbor count required for death of a cell"
+      <> Opt.metavar "NEIGHBORS"
+      )
+  <*> Opt.option Opt.auto
+      ( Opt.long "steps"
+      <> Opt.value (defaultParams ^. steps)
+      <> Opt.showDefault
+      <> Opt.help "Number of generations to run the automata for"
+      <> Opt.metavar "STEPS"
+      )
+  <**> Opt.helper
+  where
+    parseChance = readWithGuard
+      (between 0 1)
+      $ \res -> "Chance must be in the range [0,1], got: " <> show res
+
+    parseNeighbors = readWithGuard
+      (between 0 8)
+      $ \res -> "Neighbors must be in the range [0,8], got: " <> show res
+
+generate :: RandomGen g => Params -> Dimensions -> g -> Cells
+generate params dims gen
+  = runSTUArray
+  $ fmap fst
+  $ flip runRandT gen
+  $ generate' params dims
+
+generate' :: RandomGen g => Params -> Dimensions -> CellM g s (MCells s)
+generate' params dims = do
+  cells <- randInitialize dims $ params ^. aliveStartChance
+  let steps' = params ^. steps
+  when (steps' > 0)
+   $ for_ [0 .. pred steps'] . const $ stepAutomata cells dims params
+  -- Remove all but the largest contiguous region of unfilled space
+  (_: smallerRegions) <- lift $ regions @UArray . amap not <$> freeze cells
+  lift $ fillAllM (fold smallerRegions) cells
+  lift $ fillOuterEdgesM cells
+  pure cells
+
+stepAutomata :: forall s g. MCells s -> Dimensions -> Params -> CellM g s ()
+stepAutomata cells dims params = do
+  origCells <- lift $ cloneMArray @_ @(STUArray s) cells
+  for_ (range (0, V2 (dims ^. width) (dims ^. height))) $ \pos -> do
+    neighs <- lift $ numAliveNeighborsM origCells pos
+    origValue <- lift $ readArray origCells pos
+    lift . writeArray cells pos
+      $ if origValue
+        then neighs >= params ^. deathLimit
+        else neighs > params ^. birthLimit
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Level/Dungeon.hs b/users/grfn/xanthous/src/Xanthous/Generators/Level/Dungeon.hs
new file mode 100644
index 0000000000..0be7c0435c
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Level/Dungeon.hs
@@ -0,0 +1,190 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level.Dungeon
+  ( Params(..)
+  , defaultParams
+  , parseParams
+  , generate
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding ((:>))
+--------------------------------------------------------------------------------
+import           Control.Monad.Random
+import           Data.Array.ST
+import           Data.Array.IArray (amap)
+import           Data.Stream.Infinite (Stream(..))
+import qualified Data.Stream.Infinite as Stream
+import qualified Data.Graph.Inductive.Graph as Graph
+import           Data.Graph.Inductive.PatriciaTree
+import qualified Data.List.NonEmpty as NE
+import           Data.Maybe (fromJust)
+import           Linear.V2
+import           Linear.Metric
+import qualified Options.Applicative as Opt
+--------------------------------------------------------------------------------
+import           Xanthous.Random
+import           Xanthous.Data hiding (x, y, _x, _y, edges, distance)
+import           Xanthous.Generators.Level.Util
+import           Xanthous.Util.Graphics (delaunay, straightLine)
+import           Xanthous.Util.Graph (mstSubGraph)
+--------------------------------------------------------------------------------
+
+data Params = Params
+  { _numRoomsRange :: (Word, Word)
+  , _roomDimensionRange :: (Word, Word)
+  , _connectednessRatioRange :: (Double, Double)
+  }
+  deriving stock (Show, Eq, Ord, Generic)
+makeLenses ''Params
+
+defaultParams :: Params
+defaultParams = Params
+  { _numRoomsRange = (6, 8)
+  , _roomDimensionRange = (3, 12)
+  , _connectednessRatioRange = (0.1, 0.15)
+  }
+
+parseParams :: Opt.Parser Params
+parseParams = Params
+  <$> parseRange
+        "num-rooms"
+        "number of rooms to generate in the dungeon"
+        "ROOMS"
+        (defaultParams ^. numRoomsRange)
+  <*> parseRange
+        "room-size"
+        "size in tiles of one of the sides of a room"
+        "TILES"
+        (defaultParams ^. roomDimensionRange)
+  <*> parseRange
+        "connectedness-ratio"
+        ( "ratio of edges from the delaunay triangulation to re-add to the "
+        <> "minimum-spanning-tree")
+        "RATIO"
+        (defaultParams ^. connectednessRatioRange)
+  <**> Opt.helper
+  where
+    parseRange name desc metavar (defMin, defMax) =
+      (,)
+      <$> Opt.option Opt.auto
+          ( Opt.long ("min-" <> name)
+          <> Opt.value defMin
+          <> Opt.showDefault
+          <> Opt.help ("Minimum " <> desc)
+          <> Opt.metavar metavar
+          )
+      <*> Opt.option Opt.auto
+          ( Opt.long ("max-" <> name)
+          <> Opt.value defMax
+          <> Opt.showDefault
+          <> Opt.help ("Maximum " <> desc)
+          <> Opt.metavar metavar
+          )
+
+generate :: RandomGen g => Params -> Dimensions -> g -> Cells
+generate params dims gen
+  = amap not
+  $ runSTUArray
+  $ fmap fst
+  $ flip runRandT gen
+  $ generate' params dims
+
+--------------------------------------------------------------------------------
+
+generate' :: RandomGen g => Params -> Dimensions -> CellM g s (MCells s)
+generate' params dims = do
+  cells <- initializeEmpty dims
+  rooms <- genRooms params dims
+  for_ rooms $ fillRoom cells
+
+  let fullRoomGraph = delaunayRoomGraph rooms
+      mst = mstSubGraph fullRoomGraph
+      mstEdges = Graph.edges mst
+      nonMSTEdges = filter (\(n₁, n₂, _) -> (n₁, n₂) `notElem` mstEdges)
+                    $ Graph.labEdges fullRoomGraph
+
+  reintroEdgeCount <- floor . (* fromIntegral (length nonMSTEdges))
+                     <$> getRandomR (params ^. connectednessRatioRange)
+  let reintroEdges = take reintroEdgeCount nonMSTEdges
+      corridorGraph = Graph.insEdges reintroEdges mst
+
+  corridors <- traverse
+              ( uncurry corridorBetween
+              . over both (fromJust . Graph.lab corridorGraph)
+              ) $ Graph.edges corridorGraph
+
+  for_ (join corridors) $ \pt -> lift $ writeArray cells pt True
+
+  pure cells
+
+type Room = Box Word
+
+genRooms :: MonadRandom m => Params -> Dimensions -> m [Room]
+genRooms params dims = do
+  numRooms <- fromIntegral <$> getRandomR (params ^. numRoomsRange)
+  subRand . fmap (Stream.take numRooms . removeIntersecting []) . infinitely $ do
+    roomWidth <- getRandomR $ params ^. roomDimensionRange
+    roomHeight <- getRandomR $ params ^. roomDimensionRange
+    xPos <- getRandomR (0, dims ^. width - roomWidth)
+    yPos <- getRandomR (0, dims ^. height - roomHeight)
+    pure Box
+      { _topLeftCorner = V2 xPos yPos
+      , _dimensions = V2 roomWidth roomHeight
+      }
+  where
+    removeIntersecting seen (room :> rooms)
+      | any (boxIntersects room) seen
+      = removeIntersecting seen rooms
+      | otherwise
+      = room :> removeIntersecting (room : seen) rooms
+    streamRepeat x = x :> streamRepeat x
+    infinitely = sequence . streamRepeat
+
+delaunayRoomGraph :: [Room] -> Gr Room Double
+delaunayRoomGraph rooms =
+  Graph.insEdges edges . Graph.insNodes nodes $ Graph.empty
+  where
+    edges = map (\((n₁, room₁), (n₂, room₂)) -> (n₁, n₂, roomDist room₁ room₂))
+          . over (mapped . both) snd
+          . delaunay @Double
+          . NE.fromList
+          . map (\p@(_, room) -> (boxCenter $ fromIntegral <$> room, p))
+          $ nodes
+    nodes = zip [0..] rooms
+    roomDist = distance `on` (boxCenter . fmap fromIntegral)
+
+fillRoom :: MCells s -> Room -> CellM g s ()
+fillRoom cells room =
+  let V2 posx posy = room ^. topLeftCorner
+      V2 dimx dimy = room ^. dimensions
+  in for_ [posx .. posx + dimx] $ \x ->
+       for_ [posy .. posy + dimy] $ \y ->
+         lift $ writeArray cells (V2 x y) True
+
+corridorBetween :: MonadRandom m => Room -> Room -> m [V2 Word]
+corridorBetween originRoom destinationRoom
+  = straightLine <$> origin <*> destination
+  where
+    origin = choose . NE.fromList =<< originEdge
+    destination = choose . NE.fromList =<< destinationEdge
+    originEdge = pickEdge originRoom originCorner
+    destinationEdge = pickEdge destinationRoom destinationCorner
+    pickEdge room corner = choose . over both (boxEdge room) $ cornerEdges corner
+    originCorner =
+      case ( compare (originRoom ^. topLeftCorner . _x)
+                     (destinationRoom ^. topLeftCorner . _x)
+           , compare (originRoom ^. topLeftCorner . _y)
+                     (destinationRoom ^. topLeftCorner . _y)
+           ) of
+        (LT, LT) -> BottomRight
+        (LT, GT) -> TopRight
+        (GT, LT) -> BottomLeft
+        (GT, GT) -> TopLeft
+
+        (EQ, LT) -> BottomLeft
+        (EQ, GT) -> TopRight
+        (GT, EQ) -> TopLeft
+        (LT, EQ) -> BottomRight
+        (EQ, EQ) -> TopLeft -- should never happen
+
+    destinationCorner = opposite originCorner
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Level/LevelContents.hs b/users/grfn/xanthous/src/Xanthous/Generators/Level/LevelContents.hs
new file mode 100644
index 0000000000..4f8a2f42ee
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Level/LevelContents.hs
@@ -0,0 +1,182 @@
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level.LevelContents
+  ( chooseCharacterPosition
+  , randomItems
+  , randomCreatures
+  , randomDoors
+  , placeDownStaircase
+  , tutorialMessage
+  , entityFromRaw
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (any, toList)
+--------------------------------------------------------------------------------
+import           Control.Monad.Random
+import           Data.Array.IArray (amap, bounds, rangeSize, (!))
+import qualified Data.Array.IArray as Arr
+import           Data.Foldable (any, toList)
+import           Linear.V2
+--------------------------------------------------------------------------------
+import           Xanthous.Generators.Level.Util
+import           Xanthous.Random hiding (chance)
+import qualified Xanthous.Random as Random
+import           Xanthous.Data
+                 ( positionFromV2,  Position, _Position
+                 , rotations, arrayNeighbors, Neighbors(..)
+                 , neighborPositions
+                 )
+import           Xanthous.Data.EntityMap (EntityMap, _EntityMap)
+import           Xanthous.Entities.Raws (rawsWithType, RawType, raw)
+import qualified Xanthous.Entities.Item as Item
+import           Xanthous.Entities.Item (Item)
+import qualified Xanthous.Entities.Creature as Creature
+import           Xanthous.Entities.Creature (Creature)
+import           Xanthous.Entities.Environment
+                 (GroundMessage(..), Door(..), unlockedDoor, Staircase(..))
+import           Xanthous.Messages (message_)
+import           Xanthous.Util.Graphics (circle)
+import           Xanthous.Entities.RawTypes
+import           Xanthous.Entities.Creature.Hippocampus (initialHippocampus)
+import           Xanthous.Entities.Common (inRightHand, asWieldedItem, wielded)
+import           Xanthous.Game.State (SomeEntity(SomeEntity))
+--------------------------------------------------------------------------------
+
+chooseCharacterPosition :: MonadRandom m => Cells -> m Position
+chooseCharacterPosition = randomPosition
+
+randomItems :: MonadRandom m => Cells -> m (EntityMap Item)
+randomItems = randomEntities (fmap Identity . Item.newWithType) (0.0004, 0.001)
+
+placeDownStaircase :: MonadRandom m => Cells -> m (EntityMap Staircase)
+placeDownStaircase cells = do
+  pos <- randomPosition cells
+  pure $ _EntityMap # [(pos, DownStaircase)]
+
+randomDoors :: MonadRandom m => Cells -> m (EntityMap Door)
+randomDoors cells = do
+  doorRatio <- getRandomR subsetRange
+  let numDoors = floor $ doorRatio * fromIntegral (length candidateCells)
+      doorPositions =
+        removeAdjacent . fmap positionFromV2 . take numDoors $ candidateCells
+      doors = zip doorPositions $ repeat unlockedDoor
+  pure $ _EntityMap # doors
+  where
+    removeAdjacent =
+      foldr (\pos acc ->
+               if pos `elem` (acc >>= toList . neighborPositions)
+               then acc
+               else pos : acc
+            ) []
+    candidateCells = filter doorable $ Arr.indices cells
+    subsetRange = (0.8 :: Double, 1.0)
+    doorable pos =
+      not (fromMaybe True $ cells ^? ix pos)
+      && any (teeish . fmap (fromMaybe True))
+        (rotations $ arrayNeighbors cells pos)
+    -- only generate doors at the *ends* of hallways, eg (where O is walkable,
+    -- X is a wall, and D is a door):
+    --
+    -- O O O
+    -- X D X
+    --   O
+    teeish (fmap not -> (Neighbors tl t tr l r _ b _ )) =
+      and [tl, t, tr, b] && (and . fmap not) [l, r]
+
+randomCreatures
+  :: MonadRandom m
+  => Word -- ^ Level number, starting at 0
+  -> Cells
+  -> m (EntityMap Creature)
+randomCreatures levelNumber
+  = randomEntities maybeNewCreature (0.0007, 0.002)
+  where
+    maybeNewCreature cType
+      | maybe True (canGenerate levelNumber) $ cType ^. generateParams
+      = Just <$> newCreatureWithType cType
+      | otherwise
+      = pure Nothing
+
+newCreatureWithType :: MonadRandom m => CreatureType -> m Creature
+newCreatureWithType _creatureType = do
+  let _hitpoints = _creatureType ^. maxHitpoints
+      _hippocampus = initialHippocampus
+
+  equipped <- fmap join
+            . traverse genEquipped
+            $ _creatureType
+            ^.. generateParams . _Just . equippedItem . _Just
+  let _inventory = maybe id (\ei -> wielded .~ inRightHand ei) (headMay equipped) mempty
+  pure Creature.Creature {..}
+  where
+    genEquipped cei = do
+      doGen <- Random.chance $ cei ^. chance
+      let entName = cei ^. entityName
+          itemType =
+            fromMaybe (error $ "raw \"" <> unpack entName <> "\" not of type Item")
+            . preview _Item
+            . fromMaybe (error $ "Could not find raw: " <> unpack entName)
+            $ raw entName
+      item <- Item.newWithType itemType
+      if doGen
+        then pure [fromMaybe (error $ "raw \"" <> unpack entName <> "\" not wieldable")
+                  $ preview asWieldedItem item]
+        else pure []
+
+
+tutorialMessage :: MonadRandom m
+  => Cells
+  -> Position -- ^ CharacterPosition
+  -> m (EntityMap GroundMessage)
+tutorialMessage cells characterPosition = do
+  let distance = 2
+  pos <- fmap (fromMaybe (error "No valid positions for tutorial message?"))
+        . choose . ChooseElement
+        $ accessiblePositionsWithin distance cells characterPosition
+  msg <- message_ ["tutorial", "message1"]
+  pure $ _EntityMap # [(pos, GroundMessage msg)]
+  where
+    accessiblePositionsWithin :: Int -> Cells -> Position -> [Position]
+    accessiblePositionsWithin dist valid pos =
+      review _Position
+      <$> filter
+            (\pt -> not $ valid ! (fromIntegral <$> pt))
+            (circle (pos ^. _Position) dist)
+
+randomEntities
+  :: forall entity raw m t. (MonadRandom m, RawType raw, Functor t, Foldable t)
+  => (raw -> m (t entity))
+  -> (Float, Float)
+  -> Cells
+  -> m (EntityMap entity)
+randomEntities newWithType sizeRange cells =
+  case fromNullable $ rawsWithType @raw of
+    Nothing -> pure mempty
+    Just raws -> do
+      let len = rangeSize $ bounds cells
+      (numEntities :: Int) <-
+        floor . (* fromIntegral len) <$> getRandomR sizeRange
+      entities <- for [0..numEntities] $ const $ do
+        pos <- randomPosition cells
+        r <- choose raws
+        entities <- newWithType r
+        pure $ (pos, ) <$> entities
+      pure $ _EntityMap # (entities >>= toList)
+
+randomPosition :: MonadRandom m => Cells -> m Position
+randomPosition = fmap positionFromV2 . choose . impureNonNull . cellCandidates
+
+-- cellCandidates :: Cells -> Cells
+cellCandidates :: Cells -> Set (V2 Word)
+cellCandidates
+  -- find the largest contiguous region of cells in the cave.
+  = maximumBy (compare `on` length)
+  . fromMaybe (error "No regions generated! this should never happen.")
+  . fromNullable
+  . regions
+  -- cells ends up with true = wall, we want true = can put an item here
+  . amap not
+
+entityFromRaw :: MonadRandom m => EntityRaw -> m SomeEntity
+entityFromRaw (Creature ct) = SomeEntity <$> newCreatureWithType ct
+entityFromRaw (Item it) = SomeEntity <$> Item.newWithType it
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Level/Util.hs b/users/grfn/xanthous/src/Xanthous/Generators/Level/Util.hs
new file mode 100644
index 0000000000..0008eb965c
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Level/Util.hs
@@ -0,0 +1,236 @@
+{-# LANGUAGE QuantifiedConstraints #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level.Util
+  ( MCells
+  , Cells
+  , CellM
+  , randInitialize
+  , initializeEmpty
+  , numAliveNeighborsM
+  , numAliveNeighbors
+  , fillOuterEdgesM
+  , cloneMArray
+  , floodFill
+  , regions
+  , fillAll
+  , fillAllM
+  , fromPoints
+  , fromPointsM
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (Foldable, toList, for_)
+--------------------------------------------------------------------------------
+import           Data.Array.ST
+import           Data.Array.Unboxed
+import           Control.Monad.ST
+import           Control.Monad.Random
+import           Data.Monoid
+import           Data.Foldable (Foldable, toList, for_)
+import qualified Data.Set as Set
+import           Data.Semigroup.Foldable
+import           Linear.V2
+--------------------------------------------------------------------------------
+import           Xanthous.Util (foldlMapM', maximum1, minimum1)
+import           Xanthous.Data (Dimensions, width, height)
+--------------------------------------------------------------------------------
+
+type MCells s = STUArray s (V2 Word) Bool
+type Cells = UArray (V2 Word) Bool
+type CellM g s a = RandT g (ST s) a
+
+randInitialize :: RandomGen g => Dimensions -> Double -> CellM g s (MCells s)
+randInitialize dims aliveChance = do
+  res <- initializeEmpty dims
+  for_ [0..dims ^. width] $ \i ->
+    for_ [0..dims ^. height] $ \j -> do
+      val <- (>= aliveChance) <$> getRandomR (0, 1)
+      lift $ writeArray res (V2 i j) val
+  pure res
+
+initializeEmpty :: RandomGen g => Dimensions -> CellM g s (MCells s)
+initializeEmpty dims =
+  lift $ newArray (0, V2 (dims ^. width) (dims ^. height)) False
+
+-- | Returns the number of neighbors of the given point in the given array that
+-- are True.
+--
+-- Behavior if point is out-of-bounds for the array is undefined, but will not
+-- error
+numAliveNeighborsM
+  :: forall a i m
+  . (MArray a Bool m, Ix i, Integral i)
+  => a (V2 i) Bool
+  -> V2 i
+  -> m Word
+numAliveNeighborsM cells pt@(V2 x y) = do
+  cellBounds <- getBounds cells
+  getSum <$> foldlMapM'
+    (fmap (Sum . fromIntegral . fromEnum) . boundedGet cellBounds)
+    neighborPositions
+
+  where
+    boundedGet :: (V2 i, V2 i) -> (Int, Int) -> m Bool
+    boundedGet bnds _
+      | not (inRange bnds pt)
+      = pure True
+    boundedGet (V2 minX minY, V2 maxX maxY) (i, j)
+      | (x <= minX && i < 0)
+      || (y <= minY && j < 0)
+      || (x >= maxX && i > 0)
+      || (y >= maxY && j > 0)
+      = pure True
+      | otherwise =
+        let nx = fromIntegral $ fromIntegral x + i
+            ny = fromIntegral $ fromIntegral y + j
+        in readArray cells $ V2 nx ny
+
+-- | Returns the number of neighbors of the given point in the given array that
+-- are True.
+--
+-- Behavior if point is out-of-bounds for the array is undefined, but will not
+-- error
+numAliveNeighbors
+  :: forall a i
+  . (IArray a Bool, Ix i, Integral i)
+  => a (V2 i) Bool
+  -> V2 i
+  -> Word
+numAliveNeighbors cells pt@(V2 x y) =
+  let cellBounds = bounds cells
+  in getSum $ foldMap
+      (Sum . fromIntegral . fromEnum . boundedGet cellBounds)
+      neighborPositions
+
+  where
+    boundedGet :: (V2 i, V2 i) -> (Int, Int) -> Bool
+    boundedGet bnds _
+      | not (inRange bnds pt)
+      = True
+    boundedGet (V2 minX minY, V2 maxX maxY) (i, j)
+      | (x <= minX && i < 0)
+      || (y <= minY && j < 0)
+      || (x >= maxX && i > 0)
+      || (y >= maxY && j > 0)
+      = True
+      | otherwise =
+        let nx = fromIntegral $ fromIntegral x + i
+            ny = fromIntegral $ fromIntegral y + j
+        in cells ! V2 nx ny
+
+neighborPositions :: [(Int, Int)]
+neighborPositions = [(i, j) | i <- [-1..1], j <- [-1..1], (i, j) /= (0, 0)]
+
+fillOuterEdgesM :: (MArray a Bool m, Ix i) => a (V2 i) Bool -> m ()
+fillOuterEdgesM arr = do
+  (V2 minX minY, V2 maxX maxY) <- getBounds arr
+  for_ (range (minX, maxX)) $ \x -> do
+    writeArray arr (V2 x minY) True
+    writeArray arr (V2 x maxY) True
+  for_ (range (minY, maxY)) $ \y -> do
+    writeArray arr (V2 minX y) True
+    writeArray arr (V2 maxX y) True
+
+cloneMArray
+  :: forall a a' i e m.
+  ( Ix i
+  , MArray a e m
+  , MArray a' e m
+  , IArray UArray e
+  )
+  => a i e
+  -> m (a' i e)
+cloneMArray = thaw @_ @UArray <=< freeze
+
+--------------------------------------------------------------------------------
+
+-- | Flood fill a cell array starting at a point, returning a list of all the
+-- (true) cell locations reachable from that point
+floodFill :: forall a i.
+            ( IArray a Bool
+            , Ix i
+            , Enum i
+            , Bounded i
+            , Eq i
+            )
+          => a (V2 i) Bool -- ^ array
+          -> (V2 i)        -- ^ position
+          -> Set (V2 i)
+floodFill = go mempty
+  where
+    go :: Set (V2 i) -> a (V2 i) Bool -> (V2 i) -> Set (V2 i)
+    go res arr@(bounds -> arrBounds) idx@(V2 x y)
+      | not (inRange arrBounds idx) =  res
+      | not (arr ! idx) =  res
+      | otherwise =
+        let neighbors
+              = filter (inRange arrBounds)
+              . filter (/= idx)
+              . filter (`notMember` res)
+              $ V2
+              <$> [(if x == minBound then x else pred x)
+                   ..
+                   (if x == maxBound then x else succ x)]
+              <*> [(if y == minBound then y else pred y)
+                   ..
+                   (if y == maxBound then y else succ y)]
+        in foldl' (\r idx' ->
+                     if arr ! idx'
+                     then r <> (let r' = r & contains idx' .~ True
+                               in r' `seq` go r' arr idx')
+                     else r)
+           (res & contains idx .~ True) neighbors
+{-# SPECIALIZE floodFill :: UArray (V2 Word) Bool -> (V2 Word) -> Set (V2 Word) #-}
+
+-- | Gives a list of all the disconnected regions in a cell array, represented
+-- each as lists of points
+regions :: forall a i.
+          ( IArray a Bool
+          , Ix i
+          , Enum i
+          , Bounded i
+          , Eq i
+          )
+        => a (V2 i) Bool
+        -> [Set (V2 i)]
+regions arr
+  | Just firstPoint <- findFirstPoint arr =
+      let region = floodFill arr firstPoint
+          arr' = fillAll region arr
+      in region : regions arr'
+  | otherwise = []
+  where
+    findFirstPoint :: a (V2 i) Bool -> Maybe (V2 i)
+    findFirstPoint = fmap fst . headMay . filter snd . assocs
+{-# SPECIALIZE regions :: UArray (V2 Word) Bool -> [Set (V2 Word)] #-}
+
+fillAll :: (IArray a Bool, Ix i, Foldable f) => f i -> a i Bool -> a i Bool
+fillAll ixes a = accum (const fst) a $ (, (False, ())) <$> toList ixes
+
+fillAllM :: (MArray a Bool m, Ix i, Foldable f) => f i -> a i Bool -> m ()
+fillAllM ixes a = for_ ixes $ \i -> writeArray a i False
+
+fromPoints
+  :: forall a f i.
+    ( IArray a Bool
+    , Ix i
+    , Functor f
+    , Foldable1 f
+    )
+  => f (i, i)
+  -> a (i, i) Bool
+fromPoints points =
+  let pts = Set.fromList $ toList points
+      dims = ( (minimum1 $ fst <$> points, minimum1 $ snd <$> points)
+             , (maximum1 $ fst <$> points, maximum1 $ snd <$> points)
+             )
+  in array dims $ range dims <&> \i -> (i, i `member` pts)
+
+fromPointsM
+  :: (MArray a Bool m, Ix i, Element f ~ i, MonoFoldable f)
+  => NonNull f
+  -> m (a i Bool)
+fromPointsM points = do
+  arr <- newArray (minimum points, maximum points) False
+  fillAllM (otoList points) arr
+  pure arr
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Level/Village.hs b/users/grfn/xanthous/src/Xanthous/Generators/Level/Village.hs
new file mode 100644
index 0000000000..ab7de95e68
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Level/Village.hs
@@ -0,0 +1,126 @@
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level.Village
+  ( fromCave
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (any, failing, toList)
+--------------------------------------------------------------------------------
+import           Control.Monad.Random (MonadRandom)
+import           Control.Monad.State (execStateT, MonadState, modify)
+import           Control.Monad.Trans.Maybe
+import           Control.Parallel.Strategies
+import           Data.Array.IArray
+import           Data.Foldable (any, toList)
+--------------------------------------------------------------------------------
+import           Xanthous.Data
+import           Xanthous.Data.EntityMap (EntityMap)
+import qualified Xanthous.Data.EntityMap as EntityMap
+import           Xanthous.Entities.Environment
+import           Xanthous.Generators.Level.Util
+import           Xanthous.Game.State (SomeEntity(..))
+import           Xanthous.Random
+--------------------------------------------------------------------------------
+
+fromCave :: MonadRandom m
+         => Cells -- ^ The positions of all the walls
+         -> m (EntityMap SomeEntity)
+fromCave wallPositions = execStateT (fromCave' wallPositions) mempty
+
+fromCave' :: forall m. (MonadRandom m, MonadState (EntityMap SomeEntity) m)
+          => Cells
+          -> m ()
+fromCave' wallPositions = failing (pure ()) $ do
+  Just villageRegion <-
+    choose
+    . (`using` parTraversable rdeepseq)
+    . weightedBy (\reg -> let circSize = length $ circumference reg
+                         in if circSize == 50
+                            then (1.0 :: Double)
+                            else 1.0 / (fromIntegral . abs $ circSize - 50))
+    $ regions closedHallways
+
+  let circ = setFromList . circumference $ villageRegion
+
+  centerPoints <- chooseSubset (0.1 :: Double) $ toList circ
+
+  roomTiles <- foldM
+              (flip $ const $ stepOut circ)
+              (map pure centerPoints)
+              [0 :: Int ..2]
+
+  let roomWalls = circumference . setFromList @(Set _) <$> roomTiles
+      allWalls = join roomWalls
+
+  doorPositions <- fmap join . for roomWalls $ \room ->
+    let candidates = filter (`notMember` circ) room
+    in fmap toList . choose $ ChooseElement candidates
+
+  let entryways =
+        filter (\pt ->
+                  let ncs = neighborCells pt
+                  in any ((&&) <$> (not . (wallPositions !))
+                              <*> (`notMember` villageRegion)) ncs
+                   && any ((&&) <$> (`member` villageRegion)
+                              <*> (`notElem` allWalls)) ncs)
+                  $ toList villageRegion
+
+  Just entryway <- choose $ ChooseElement entryways
+
+  for_ (filter ((&&) <$> (`notElem` doorPositions) <*> (/= entryway)) allWalls)
+    $ insertEntity Wall
+  for_ (filter (/= entryway) doorPositions) $ insertEntity unlockedDoor
+  insertEntity unlockedDoor entryway
+
+
+  where
+    insertEntity e pt = modify $ EntityMap.insertAt (ptToPos pt) $ SomeEntity e
+    ptToPos pt = _Position # (fromIntegral <$> pt)
+
+    stepOut :: Set (V2 Word) -> [[V2 Word]] -> MaybeT m [[V2 Word]]
+    stepOut circ rooms = for rooms $ \room ->
+      let nextLevels = hashNub $ toList . neighborCells =<< room
+      in pure
+         . (<> room)
+         $ filter ((&&) <$> (`notMember` circ) <*> (`notElem` join rooms))
+         nextLevels
+
+    circumference pts =
+      filter (any (`notMember` pts) . neighborCells) $ toList pts
+    closedHallways = closeHallways livePositions
+    livePositions = amap not wallPositions
+
+--------------------------------------------------------------------------------
+
+closeHallways :: Cells -> Cells
+closeHallways livePositions =
+  livePositions // mapMaybe closeHallway (assocs livePositions)
+  where
+    closeHallway (_, False) = Nothing
+    closeHallway (pos, _)
+      | isHallway pos = Just (pos, False)
+      | otherwise     = Nothing
+    isHallway pos = any ((&&) <$> not . view left <*> not . view right)
+      . rotations
+      . fmap (fromMaybe False)
+      $ arrayNeighbors livePositions pos
+
+failing :: Monad m => m a -> MaybeT m a -> m a
+failing result = (maybe result pure =<<) . runMaybeT
+
+{-
+
+import Xanthous.Generators.Village
+import Xanthous.Generators
+import Xanthous.Data
+import System.Random
+import qualified Data.Text
+import qualified Xanthous.Generators.CaveAutomata as CA
+let gi = GeneratorInput SCaveAutomata CA.defaultParams
+wallPositions <- generateFromInput gi (Dimensions 80 50) <$> getStdGen
+putStrLn . Data.Text.unpack $ showCells wallPositions
+
+import Data.Array.IArray
+let closedHallways = closeHallways . amap not $ wallPositions
+putStrLn . Data.Text.unpack . showCells $ amap not closedHallways
+
+-}
diff --git a/users/grfn/xanthous/src/Xanthous/Generators/Speech.hs b/users/grfn/xanthous/src/Xanthous/Generators/Speech.hs
new file mode 100644
index 0000000000..8abc00b6a2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Generators/Speech.hs
@@ -0,0 +1,181 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedLists #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Speech
+  ( -- * Language definition
+    Language(..)
+    -- ** Lenses
+  , phonotactics
+  , syllablesPerWord
+
+    -- ** Phonotactics
+  , Phonotactics(..)
+    -- *** Lenses
+  , onsets
+  , nuclei
+  , codas
+  , numOnsets
+  , numNuclei
+  , numCodas
+
+    -- * Language generation
+  , syllable
+  , word
+
+    -- * Languages
+  , english
+  , gormlak
+
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (replicateM)
+import           Data.Interval (Interval, (<=..<=))
+import qualified Data.Interval as Interval
+import           Control.Monad.Random.Class (MonadRandom)
+import           Xanthous.Random (chooseRange, choose, ChooseElement (..), Weighted (Weighted))
+import           Control.Monad (replicateM)
+import           Test.QuickCheck (Arbitrary, CoArbitrary, Function)
+import           Test.QuickCheck.Instances.Text ()
+import           Data.List.NonEmpty (NonEmpty)
+--------------------------------------------------------------------------------
+
+newtype Phoneme = Phoneme Text
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData, CoArbitrary, Function)
+  deriving newtype (IsString, Semigroup, Monoid, Arbitrary)
+
+-- | The phonotactics of a language
+--
+-- The phonotactics of a language represent the restriction on the phonemes in
+-- the syllables of a language.
+--
+-- Syllables in a language consist of an onset, a nucleus, and a coda (the
+-- nucleus and the coda together representing the "rhyme" of the syllable).
+data Phonotactics = Phonotactics
+  { _onsets    :: [Phoneme] -- ^ The permissible onsets, or consonant clusters
+                           --   at the beginning of a syllable
+  , _nuclei    :: [Phoneme] -- ^ The permissible nuclei, or vowel clusters in
+                           --   the middle of a syllable
+  , _codas     :: [Phoneme] -- ^ The permissible codas, or consonant clusters at
+                           --   the end of a syllable
+  , _numOnsets :: Interval Word -- ^ The range of number of allowable onsets
+  , _numNuclei :: Interval Word -- ^ The range of number of allowable nuclei
+  , _numCodas  :: Interval Word -- ^ The range of number of allowable codas
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+makeLenses ''Phonotactics
+
+-- | Randomly generate a syllable with the given 'Phonotactics'
+syllable :: MonadRandom m => Phonotactics -> m Text
+syllable phonotactics = do
+  let genPart num choices = do
+        n <- fromIntegral . fromMaybe 0 <$> chooseRange (phonotactics ^. num)
+        fmap (fromMaybe mempty . mconcat)
+          . replicateM n
+          . choose . ChooseElement
+          $ phonotactics ^. choices
+
+  (Phoneme onset) <- genPart numOnsets onsets
+  (Phoneme nucleus) <- genPart numNuclei nuclei
+  (Phoneme coda) <- genPart numCodas codas
+
+  pure $ onset <> nucleus <> coda
+
+-- | A definition for a language
+--
+-- Currently this provides enough information to generate multi-syllabic words,
+-- but in the future will likely also include grammar-related things.
+data Language = Language
+  { _phonotactics :: Phonotactics
+  , _syllablesPerWord :: Weighted Int NonEmpty Int
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+makeLenses ''Language
+
+word :: MonadRandom m => Language -> m Text
+word lang = do
+  numSyllables <- choose $ lang ^. syllablesPerWord
+  mconcat <$> replicateM numSyllables (syllable $ lang ^. phonotactics)
+
+--------------------------------------------------------------------------------
+
+-- <https://en.wikipedia.org/wiki/English_phonology#Phonotactics>
+englishPhonotactics :: Phonotactics
+englishPhonotactics = Phonotactics
+  { _onsets = [ "pl" , "bl" , "kl" , "gl" , "pr" , "br" , "tr" , "dr" , "kr"
+              , "gr" , "tw" , "dw" , "gw" , "kw" , "pw"
+
+              , "fl" , "sl" , {- "thl", -} "shl" {- , "vl" -}
+              , "p", "b", "t", "d", "k", "ɡ", "m", "n", "f", "v", "th", "s"
+              , "z", "h", "l", "w"
+
+              , "sp", "st", "sk"
+
+              , "sm", "sn"
+
+              , "sf", "sth"
+
+              , "spl", "skl", "spr", "str", "skr", "skw", "sm", "sp", "st", "sk"
+              ]
+  , _nuclei = [ "a", "e", "i", "o", "u", "ur", "ar", "or", "ear", "are", "ure"
+              , "oa", "ee", "oo", "ei", "ie", "oi", "ou"
+              ]
+  , _codas = [ "m", "n", "ng", "p", "t", "tsh", "k", "f", "sh", "s", "th", "x"
+             , "v", "z", "zh", "l", "r", "w"
+
+             , "lk", "lb", "lt", "ld", "ltsh", "ldsh", "lk"
+             , "rp", "rb", "rt", "rd", "rtsh", "rdsh", "rk", "rɡ"
+             , "lf", "lv", "lth", "ls", "lz", "lsh", "lth"
+             , "rf", "rv", "rth", "rs", "rz", "rth"
+             , "lm", "ln"
+             , "rm", "rn", "rl"
+             , "mp", "nt", "nd", "nth", "nsh", "nk"
+             , "mf", "ms", "mth", "nf", "nth", "ns", "nz", "nth"
+             , "ft", "sp", "st", "sk"
+             , "fth"
+             , "pt", "kt"
+             , "pth", "ps", "th", "ts", "dth", "dz", "ks"
+             , "lpt", "lps", "lfth", "lts", "lst", "lkt", "lks"
+             , "rmth", "rpt", "rps", "rts", "rst", "rkt"
+             , "mpt", "mps", "ndth", "nkt", "nks", "nkth"
+             , "ksth", "kst"
+             ]
+  , _numOnsets = 0 <=..<= 1
+  , _numNuclei = Interval.singleton 1
+  , _numCodas  = 0 <=..<= 1
+  }
+
+english :: Language
+english = Language
+  { _phonotactics = englishPhonotactics
+  , _syllablesPerWord = Weighted [(20, 1),
+                                  (7,  2),
+                                  (2,  3),
+                                  (1,  4)]
+  }
+
+gormlakPhonotactics :: Phonotactics
+gormlakPhonotactics = Phonotactics
+ { _onsets = [ "h", "l", "g", "b", "m", "n", "ng"
+             , "gl", "bl", "fl"
+             ]
+ , _numOnsets = Interval.singleton 1
+ , _nuclei = [ "a", "o", "aa", "u" ]
+ , _numNuclei = Interval.singleton 1
+ , _codas = [ "r", "l", "g", "m", "n"
+            , "rl", "gl", "ml", "rm"
+            , "n", "k"
+            ]
+ , _numCodas = Interval.singleton 1
+ }
+
+gormlak :: Language
+gormlak = Language
+  { _phonotactics = gormlakPhonotactics
+  , _syllablesPerWord = Weighted [ (5, 2)
+                                 , (5, 1)
+                                 , (1, 3)
+                                 ]
+  }
diff --git a/users/grfn/xanthous/src/Xanthous/Messages.hs b/users/grfn/xanthous/src/Xanthous/Messages.hs
new file mode 100644
index 0000000000..c273d65082
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Messages.hs
@@ -0,0 +1,114 @@
+{-# LANGUAGE TemplateHaskell #-}
+--------------------------------------------------------------------------------
+module Xanthous.Messages
+  ( Message(..)
+  , resolve
+  , MessageMap(..)
+  , lookupMessage
+
+    -- * Game messages
+  , messages
+  , render
+  , render_
+  , lookup
+  , message
+  , message_
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude hiding (lookup)
+--------------------------------------------------------------------------------
+import           Control.Monad.Random.Class (MonadRandom)
+import           Data.Aeson (FromJSON, ToJSON, toJSON, object)
+import qualified Data.Aeson as JSON
+import           Data.Aeson.Generic.DerivingVia
+import           Data.FileEmbed
+import           Data.List.NonEmpty
+import           Test.QuickCheck hiding (choose)
+import           Test.QuickCheck.Instances.UnorderedContainers ()
+import           Text.Mustache
+import qualified Data.Yaml as Yaml
+--------------------------------------------------------------------------------
+import           Xanthous.Random
+import           Xanthous.Orphans ()
+--------------------------------------------------------------------------------
+
+data Message = Single Template | Choice (NonEmpty Template)
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (CoArbitrary, Function, NFData)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ SumEnc UntaggedVal ]
+           Message
+
+instance Arbitrary Message where
+  arbitrary =
+    frequency [ (10, Single <$> arbitrary)
+              , (1, Choice <$> arbitrary)
+              ]
+  shrink = genericShrink
+
+resolve :: MonadRandom m => Message -> m Template
+resolve (Single t) = pure t
+resolve (Choice ts) = choose ts
+
+data MessageMap = Direct Message | Nested (HashMap Text MessageMap)
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (CoArbitrary, Function, NFData)
+  deriving (ToJSON, FromJSON)
+       via WithOptions '[ SumEnc UntaggedVal ]
+           MessageMap
+
+instance Arbitrary MessageMap where
+  arbitrary = frequency [ (10, Direct <$> arbitrary)
+                        , (1, Nested <$> arbitrary)
+                        ]
+
+lookupMessage :: [Text] -> MessageMap -> Maybe Message
+lookupMessage [] (Direct msg) = Just msg
+lookupMessage (k:ks) (Nested m) = lookupMessage ks =<< m ^. at k
+lookupMessage _ _ = Nothing
+
+type instance Index MessageMap = [Text]
+type instance IxValue MessageMap = Message
+instance Ixed MessageMap where
+  ix [] f (Direct msg) = Direct <$> f msg
+  ix (k:ks) f (Nested m) = case m ^. at k of
+    Just m' -> ix ks f m' <&> \m'' ->
+      Nested $ m & at k ?~ m''
+    Nothing -> pure $ Nested m
+  ix _ _ m = pure m
+
+--------------------------------------------------------------------------------
+
+rawMessages :: ByteString
+rawMessages = $(embedFile "src/Xanthous/messages.yaml")
+
+messages :: MessageMap
+messages
+  = either (error . Yaml.prettyPrintParseException) id
+  $ Yaml.decodeEither' rawMessages
+
+render :: (MonadRandom m, ToJSON params) => Message -> params -> m Text
+render msg params = do
+  tpl <- resolve msg
+  pure . toStrict . renderMustache tpl $ toJSON params
+
+-- | Render a message with an empty set of params
+render_ :: (MonadRandom m) => Message -> m Text
+render_ msg = render msg $ object []
+
+lookup :: [Text] -> Message
+lookup path = fromMaybe notFound $ messages ^? ix path
+  where notFound
+          = Single
+          $ compileMustacheText "template" "Message not found"
+          ^?! _Right
+
+message :: (MonadRandom m, ToJSON params) => [Text] -> params -> m Text
+message path params = maybe notFound (`render` params) $ messages ^? ix path
+  where
+    notFound = pure "Message not found"
+
+message_ :: (MonadRandom m) => [Text] -> m Text
+message_ path = maybe notFound (`render` JSON.object []) $ messages ^? ix path
+  where
+    notFound = pure "Message not found"
diff --git a/users/grfn/xanthous/src/Xanthous/Messages/Template.hs b/users/grfn/xanthous/src/Xanthous/Messages/Template.hs
new file mode 100644
index 0000000000..5176880355
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Messages/Template.hs
@@ -0,0 +1,275 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+--------------------------------------------------------------------------------
+module Xanthous.Messages.Template
+  ( -- * Template AST
+    Template(..)
+  , Substitution(..)
+  , Filter(..)
+
+    -- ** Template AST transformations
+  , reduceTemplate
+
+    -- * Template parser
+  , template
+  , runParser
+  , errorBundlePretty
+
+    -- * Template pretty-printer
+  , ppTemplate
+
+    -- * Rendering templates
+  , TemplateVar(..)
+  , nested
+  , TemplateVars(..)
+  , vars
+  , RenderError
+  , render
+  )
+where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding
+                 (many, concat, try, elements, some, parts)
+--------------------------------------------------------------------------------
+import           Test.QuickCheck hiding (label)
+import           Test.QuickCheck.Instances.Text ()
+import           Test.QuickCheck.Instances.Semigroup ()
+import           Test.QuickCheck.Checkers (EqProp)
+import           Control.Monad.Combinators.NonEmpty
+import           Data.List.NonEmpty (NonEmpty(..))
+import           Data.Data
+import           Text.Megaparsec hiding (sepBy1, some)
+import           Text.Megaparsec.Char
+import qualified Text.Megaparsec.Char.Lexer as L
+import           Data.Function (fix)
+--------------------------------------------------------------------------------
+import Xanthous.Util (EqEqProp(..))
+--------------------------------------------------------------------------------
+
+genIdentifier :: Gen Text
+genIdentifier = pack <$> listOf1 (elements identifierChars)
+
+identifierChars :: String
+identifierChars = ['a'..'z'] <> ['A'..'Z'] <> ['-', '_']
+
+newtype Filter = FilterName Text
+  deriving stock (Show, Eq, Ord, Generic, Data)
+  deriving anyclass (NFData)
+  deriving (IsString) via Text
+
+instance Arbitrary Filter where
+  arbitrary = FilterName <$> genIdentifier
+  shrink (FilterName fn) = fmap FilterName . filter (not . null) $ shrink fn
+
+data Substitution
+  = SubstPath (NonEmpty Text)
+  | SubstFilter Substitution Filter
+  deriving stock (Show, Eq, Ord, Generic, Data)
+  deriving anyclass (NFData)
+
+instance Arbitrary Substitution where
+  arbitrary = sized . fix $ \gen n ->
+    let leaves =
+          [ SubstPath <$> ((:|) <$> genIdentifier <*> listOf genIdentifier)]
+        subtree = gen $ n `div` 2
+    in if n == 0
+       then oneof leaves
+       else oneof $ leaves <> [ SubstFilter <$> subtree <*> arbitrary ]
+  shrink (SubstPath pth) =
+    fmap SubstPath
+    . filter (not . any ((||) <$> null <*> any (`notElem` identifierChars)))
+    $ shrink pth
+  shrink (SubstFilter s f)
+    = shrink s
+    <> (uncurry SubstFilter <$> shrink (s, f))
+
+data Template
+  = Literal Text
+  | Subst Substitution
+  | Concat Template Template
+  deriving stock (Show, Generic, Data)
+  deriving anyclass (NFData)
+  deriving EqProp via EqEqProp Template
+
+instance Plated Template where
+  plate _ tpl@(Literal _) = pure tpl
+  plate _ tpl@(Subst _) = pure tpl
+  plate f (Concat tpl₁ tpl₂) = Concat <$> f tpl₁ <*> f tpl₂
+
+reduceTemplate :: Template -> Template
+reduceTemplate = transform $ \case
+  (Concat (Literal t₁) (Literal t₂)) -> Literal (t₁ <> t₂)
+  (Concat (Literal "") t) -> t
+  (Concat t (Literal "")) -> t
+  (Concat t₁ (Concat t₂ t₃)) -> Concat (Concat t₁ t₂) t₃
+  (Concat (Concat t₁ (Literal t₂)) (Literal t₃)) -> (Concat t₁ (Literal $ t₂ <> t₃))
+  t -> t
+
+instance Eq Template where
+  tpl₁ == tpl₂ = case (reduceTemplate tpl₁, reduceTemplate tpl₂) of
+    (Literal t₁, Literal t₂) -> t₁ == t₂
+    (Subst s₁, Subst s₂) -> s₁ == s₂
+    (Concat ta₁ ta₂, Concat tb₁ tb₂) -> ta₁ == tb₁ && ta₂ == tb₂
+    _ -> False
+
+instance Arbitrary Template where
+  arbitrary = sized . fix $ \gen n ->
+    let leaves = [ Literal . pack . filter (`notElem` ['\\', '{']) <$> arbitrary
+                 , Subst <$> arbitrary
+                 ]
+        subtree = gen $ n `div` 2
+        genConcat = Concat <$> subtree <*> subtree
+    in if n == 0
+       then oneof leaves
+       else oneof $ genConcat : leaves
+  shrink (Literal t) = Literal <$> shrink t
+  shrink (Subst s) = Subst <$> shrink s
+  shrink (Concat t₁ t₂)
+    = shrink t₁
+    <> shrink t₂
+    <> (Concat <$> shrink t₁ <*> shrink t₂)
+
+instance Semigroup Template where
+  (<>) = Concat
+
+instance Monoid Template where
+  mempty = Literal ""
+
+--------------------------------------------------------------------------------
+
+type Parser = Parsec Void Text
+
+sc :: Parser ()
+sc = L.space space1 empty empty
+
+lexeme :: Parser a -> Parser a
+lexeme = L.lexeme sc
+
+symbol :: Text -> Parser Text
+symbol = L.symbol sc
+
+identifier :: Parser Text
+identifier = lexeme . label "identifier" $ do
+  firstChar <- letterChar <|> oneOf ['-', '_']
+  restChars <- many $ alphaNumChar <|> oneOf ['-', '_']
+  pure $ firstChar <| pack restChars
+
+filterName :: Parser Filter
+filterName = FilterName <$> identifier
+
+substitutionPath :: Parser Substitution
+substitutionPath = SubstPath <$> sepBy1 identifier (char '.')
+
+substitutionFilter :: Parser Substitution
+substitutionFilter = do
+  path <- substitutionPath
+  fs <- some $ symbol "|" *> filterName
+  pure $ foldl' SubstFilter path fs
+  -- pure $ SubstFilter path f
+
+substitutionContents :: Parser Substitution
+substitutionContents
+  =   try substitutionFilter
+  <|> substitutionPath
+
+substitution :: Parser Substitution
+substitution = between (string "{{") (string "}}") substitutionContents
+
+literal :: Parser Template
+literal = Literal <$>
+  (   (string "\\{" $> "{")
+  <|> takeWhile1P Nothing (`notElem` ['\\', '{'])
+  )
+
+subst :: Parser Template
+subst = Subst <$> substitution
+
+template' :: Parser Template
+template' = do
+  parts <- many $ literal <|> subst
+  pure $ foldr Concat (Literal "") parts
+
+
+template :: Parser Template
+template = reduceTemplate <$> template' <* eof
+
+--------------------------------------------------------------------------------
+
+ppSubstitution :: Substitution -> Text
+ppSubstitution (SubstPath substParts) = intercalate "." substParts
+ppSubstitution (SubstFilter s (FilterName f)) = ppSubstitution s <> " | " <> f
+
+ppTemplate :: Template -> Text
+ppTemplate (Literal txt) = txt
+ppTemplate (Subst s) = "{{" <> ppSubstitution s <> "}}"
+ppTemplate (Concat tpl₁ tpl₂) = ppTemplate tpl₁ <> ppTemplate tpl₂
+
+--------------------------------------------------------------------------------
+
+data TemplateVar
+  = Val Text
+  | Nested (Map Text TemplateVar)
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+
+nested :: [(Text, TemplateVar)] -> TemplateVar
+nested = Nested . mapFromList
+
+instance Arbitrary TemplateVar where
+  arbitrary = sized . fix $ \gen n ->
+    let nst = fmap mapFromList . listOf $ (,) <$> arbitrary <*> gen (n `div` 2)
+    in if n == 0
+       then Val <$> arbitrary
+       else oneof [ Val <$> arbitrary
+                  , Nested <$> nst]
+
+newtype TemplateVars = Vars { getTemplateVars :: Map Text TemplateVar }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+  deriving (Arbitrary) via (Map Text TemplateVar)
+
+type instance Index TemplateVars = Text
+type instance IxValue TemplateVars = TemplateVar
+instance Ixed TemplateVars where
+  ix k f (Vars vs) = Vars <$> ix k f vs
+instance At TemplateVars where
+  at k f (Vars vs) = Vars <$> at k f vs
+
+vars :: [(Text, TemplateVar)] -> TemplateVars
+vars = Vars . mapFromList
+
+lookupVar :: TemplateVars -> NonEmpty Text -> Maybe TemplateVar
+lookupVar vs (p :| []) = vs ^. at p
+lookupVar vs (p :| (p₁ : ps)) = vs ^. at p >>= \case
+  (Val _) -> Nothing
+  (Nested vs') -> lookupVar (Vars vs') $ p₁ :| ps
+
+data RenderError
+  = NoSuchVariable (NonEmpty Text)
+  | NestedFurther (NonEmpty Text)
+  | NoSuchFilter Filter
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (NFData)
+
+renderSubst
+  :: Map Filter (Text -> Text) -- ^ Filters
+  -> TemplateVars
+  -> Substitution
+  -> Either RenderError Text
+renderSubst _ vs (SubstPath pth) =
+  case lookupVar vs pth of
+    Just (Val v) -> Right v
+    Just (Nested _) -> Left $ NestedFurther pth
+    Nothing -> Left $ NoSuchVariable pth
+renderSubst fs vs (SubstFilter s fn) =
+  case fs ^. at fn of
+    Just filterFn -> filterFn <$> renderSubst fs vs s
+    Nothing -> Left $ NoSuchFilter fn
+
+render
+  :: Map Filter (Text -> Text) -- ^ Filters
+  -> TemplateVars             -- ^ Template variables
+  -> Template                 -- ^ Template
+  -> Either RenderError Text
+render _ _ (Literal s) = pure s
+render fs vs (Concat t₁ t₂) = (<>) <$> render fs vs t₁ <*> render fs vs t₂
+render fs vs (Subst s) = renderSubst fs vs s
diff --git a/users/grfn/xanthous/src/Xanthous/Monad.hs b/users/grfn/xanthous/src/Xanthous/Monad.hs
new file mode 100644
index 0000000000..db602de56f
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Monad.hs
@@ -0,0 +1,76 @@
+--------------------------------------------------------------------------------
+module Xanthous.Monad
+  ( AppT(..)
+  , AppM
+  , runAppT
+  , continue
+  , halt
+
+    -- * Messages
+  , say
+  , say_
+  , message
+  , message_
+  , writeMessage
+
+    -- * Autocommands
+  , cancelAutocommand
+
+    -- * Events
+  , sendEvent
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+import           Control.Monad.Random
+import           Control.Monad.State
+import qualified Brick
+import           Brick (EventM, Next)
+import           Brick.BChan (writeBChan)
+import           Data.Aeson (ToJSON, object)
+--------------------------------------------------------------------------------
+import           Xanthous.Data.App (AppEvent)
+import           Xanthous.Game.State
+import           Xanthous.Game.Env
+import           Xanthous.Messages (Message)
+import qualified Xanthous.Messages as Messages
+--------------------------------------------------------------------------------
+
+halt :: AppT (EventM n) (Next GameState)
+halt = lift . Brick.halt =<< get
+
+continue :: AppT (EventM n) (Next GameState)
+continue = lift . Brick.continue =<< get
+
+--------------------------------------------------------------------------------
+
+say :: (MonadRandom m, ToJSON params, MonadState GameState m)
+    => [Text] -> params -> m ()
+say msgPath = writeMessage <=< Messages.message msgPath
+
+say_ :: (MonadRandom m, MonadState GameState m) => [Text] -> m ()
+say_ msgPath = say msgPath $ object []
+
+message :: (MonadRandom m, ToJSON params, MonadState GameState m)
+        => Message -> params -> m ()
+message msg = writeMessage <=< Messages.render msg
+
+message_ :: (MonadRandom m, MonadState GameState m)
+         => Message ->  m ()
+message_ msg = message msg $ object []
+
+writeMessage :: MonadState GameState m => Text -> m ()
+writeMessage m = messageHistory %= pushMessage m
+
+-- | Cancel the currently active autocommand, if any
+cancelAutocommand :: (MonadState GameState m, MonadIO m) => m ()
+cancelAutocommand = do
+  traverse_ (liftIO . cancel . snd) =<< preuse (autocommand . _ActiveAutocommand)
+  autocommand .= NoAutocommand
+
+--------------------------------------------------------------------------------
+
+-- | Send an event to the app in an environment where the game env is available
+sendEvent :: (MonadReader GameEnv m, MonadIO m) => AppEvent -> m ()
+sendEvent evt = do
+  ec <- view eventChan
+  liftIO $ writeBChan ec evt
diff --git a/users/grfn/xanthous/src/Xanthous/Orphans.hs b/users/grfn/xanthous/src/Xanthous/Orphans.hs
new file mode 100644
index 0000000000..385873e7b4
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Orphans.hs
@@ -0,0 +1,490 @@
+{-# LANGUAGE RecordWildCards       #-}
+{-# LANGUAGE StandaloneDeriving    #-}
+{-# LANGUAGE UndecidableInstances  #-}
+{-# LANGUAGE PackageImports        #-}
+{-# OPTIONS_GHC -Wno-orphans       #-}
+{-# OPTIONS_GHC -Wno-type-defaults #-}
+--------------------------------------------------------------------------------
+module Xanthous.Orphans
+  ( ppTemplate
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (elements, (.=))
+--------------------------------------------------------------------------------
+import           Data.Aeson hiding (Key)
+import qualified Data.Aeson.KeyMap as KM
+import           Data.Aeson.Types (typeMismatch)
+import           Data.List.NonEmpty (NonEmpty(..))
+import           Graphics.Vty.Attributes
+import           Brick.Widgets.Edit
+import           Data.Text.Zipper.Generic (GenericTextZipper)
+import           Brick.Widgets.Core (getName)
+import           System.Random.Internal (StdGen (..))
+import           System.Random.SplitMix (SMGen ())
+import           Test.QuickCheck
+import           "quickcheck-instances" Test.QuickCheck.Instances ()
+import           Text.Megaparsec (errorBundlePretty)
+import           Text.Megaparsec.Pos
+import           Text.Mustache
+import           Text.Mustache.Type ( showKey )
+import           Control.Monad.State
+import           Linear
+import qualified Data.Interval as Interval
+import           Data.Interval ( Interval, Extended (..), Boundary (..)
+                               , lowerBound', upperBound', (<=..<), (<=..<=)
+                               , interval)
+import           Test.QuickCheck.Checkers (EqProp ((=-=)))
+--------------------------------------------------------------------------------
+import           Xanthous.Util.JSON
+import           Xanthous.Util.QuickCheck
+import           Xanthous.Util (EqEqProp(EqEqProp))
+import qualified Graphics.Vty.Input.Events
+--------------------------------------------------------------------------------
+
+instance forall s a.
+  ( Cons s s a a
+  , IsSequence s
+  , Element s ~ a
+  ) => Cons (NonNull s) (NonNull s) a a where
+  _Cons = prism hither yon
+    where
+      hither :: (a, NonNull s) -> NonNull s
+      hither (a, ns) =
+        let s = toNullable ns
+        in impureNonNull $ a <| s
+
+      yon :: NonNull s -> Either (NonNull s) (a, NonNull s)
+      yon ns = case nuncons ns of
+        (_, Nothing) -> Left ns
+        (x, Just xs) -> Right (x, xs)
+
+instance forall a. Cons (NonEmpty a) (NonEmpty a) a a where
+  _Cons = prism hither yon
+    where
+      hither :: (a, NonEmpty a) -> NonEmpty a
+      hither (a, x :| xs) = a :| (x : xs)
+
+      yon :: NonEmpty a -> Either (NonEmpty a) (a, NonEmpty a)
+      yon ns@(x :| xs) = case xs of
+        (y : ys) -> Right (x, y :| ys)
+        [] -> Left ns
+
+
+instance Arbitrary PName where
+  arbitrary = PName . pack <$> listOf1 (elements ['a'..'z'])
+
+instance Arbitrary Key where
+  arbitrary = Key <$> listOf1 arbSafeText
+    where arbSafeText = pack <$> listOf1 (elements ['a'..'z'])
+  shrink (Key []) = error "unreachable"
+  shrink k@(Key [_]) = pure k
+  shrink (Key (p:ps)) = Key . (p :) <$> shrink ps
+
+instance Arbitrary Pos where
+  arbitrary = mkPos . succ . abs <$> arbitrary
+  shrink (unPos -> 1) = []
+  shrink (unPos -> x) = mkPos <$> [x..1]
+
+instance Arbitrary Node where
+  arbitrary = scale (`div` 10) $ sized node
+    where
+      node n | n > 0 = oneof $ leaves ++ branches (n `div` 4)
+      node _ = oneof leaves
+      branches n =
+        [ Section <$> arbitrary <*> subnodes n
+        , InvertedSection <$> arbitrary <*> subnodes n
+        ]
+      subnodes = fmap concatTextBlocks . listOf . node
+      leaves =
+        [ TextBlock . pack <$> listOf1 (elements ['a'..'z'])
+        , EscapedVar <$> arbitrary
+        , UnescapedVar <$> arbitrary
+        -- TODO fix pretty-printing of mustache partials
+        -- , Partial <$> arbitrary <*> arbitrary
+        ]
+  shrink = genericShrink
+
+concatTextBlocks :: [Node] -> [Node]
+concatTextBlocks [] = []
+concatTextBlocks [x] = [x]
+concatTextBlocks (TextBlock txt₁ : TextBlock txt₂ : xs)
+  = concatTextBlocks $ TextBlock (txt₁ <> txt₂) : concatTextBlocks xs
+concatTextBlocks (x : xs) = x : concatTextBlocks xs
+
+instance Arbitrary Template where
+  arbitrary = scale (`div` 8) $ do
+    template <- concatTextBlocks <$> arbitrary
+    -- templateName <- arbitrary
+    -- rest <- arbitrary
+    let templateName = "template"
+        rest = mempty
+    pure $ Template
+      { templateActual = templateName
+      , templateCache = rest & at templateName ?~ template
+      }
+  shrink (Template actual cache) =
+    let Just tpl = cache ^. at actual
+    in do
+      cache' <- shrink cache
+      tpl' <- shrink tpl
+      actual' <- shrink actual
+      pure $ Template
+        { templateActual = actual'
+        , templateCache = cache' & at actual' ?~ tpl'
+        }
+
+instance CoArbitrary Template where
+  coarbitrary = coarbitrary . ppTemplate
+
+instance Function Template where
+  function = functionMap ppTemplate parseTemplatePartial
+    where
+      parseTemplatePartial txt
+        = compileMustacheText "template" txt ^?! _Right
+
+ppNode :: Map PName [Node] -> Node -> Text
+ppNode _ (TextBlock txt) = txt
+ppNode _ (EscapedVar k) = "{{" <> showKey k <> "}}"
+ppNode ctx (Section k body) =
+  let sk = showKey k
+  in "{{#" <> sk <> "}}" <> foldMap (ppNode ctx) body <> "{{/" <> sk <> "}}"
+ppNode _ (UnescapedVar k) = "{{{" <> showKey k <> "}}}"
+ppNode ctx (InvertedSection k body) =
+  let sk = showKey k
+  in "{{^" <> sk <> "}}" <> foldMap (ppNode ctx) body <> "{{/" <> sk <> "}}"
+ppNode _ (Partial n _) = "{{> " <> unPName n <> "}}"
+
+ppTemplate :: Template -> Text
+ppTemplate (Template actual cache) =
+  case cache ^. at actual of
+    Nothing -> error "Template not found?"
+    Just nodes -> foldMap (ppNode cache) nodes
+
+instance ToJSON Template where
+  toJSON = String . ppTemplate
+
+instance FromJSON Template where
+  parseJSON
+    = withText "Template"
+    $ either (fail . errorBundlePretty) pure
+    . compileMustacheText "template"
+
+deriving anyclass instance NFData Node
+deriving anyclass instance NFData Template
+
+instance FromJSON Color where
+  parseJSON (String "black")         = pure black
+  parseJSON (String "red")           = pure red
+  parseJSON (String "green")         = pure green
+  parseJSON (String "yellow")        = pure yellow
+  parseJSON (String "blue")          = pure blue
+  parseJSON (String "magenta")       = pure magenta
+  parseJSON (String "cyan")          = pure cyan
+  parseJSON (String "white")         = pure white
+  parseJSON (String "brightBlack")   = pure brightBlack
+  parseJSON (String "brightRed")     = pure brightRed
+  parseJSON (String "brightGreen")   = pure brightGreen
+  parseJSON (String "brightYellow")  = pure brightYellow
+  parseJSON (String "brightBlue")    = pure brightBlue
+  parseJSON (String "brightMagenta") = pure brightMagenta
+  parseJSON (String "brightCyan")    = pure brightCyan
+  parseJSON (String "brightWhite")   = pure brightWhite
+  parseJSON n@(Number _)             = Color240 <$> parseJSON n
+  parseJSON x                        = typeMismatch "Color" x
+
+instance ToJSON Color where
+  toJSON color
+    | color == black         = "black"
+    | color == red           = "red"
+    | color == green         = "green"
+    | color == yellow        = "yellow"
+    | color == blue          = "blue"
+    | color == magenta       = "magenta"
+    | color == cyan          = "cyan"
+    | color == white         = "white"
+    | color == brightBlack   = "brightBlack"
+    | color == brightRed     = "brightRed"
+    | color == brightGreen   = "brightGreen"
+    | color == brightYellow  = "brightYellow"
+    | color == brightBlue    = "brightBlue"
+    | color == brightMagenta = "brightMagenta"
+    | color == brightCyan    = "brightCyan"
+    | color == brightWhite   = "brightWhite"
+    | Color240 num <- color  = toJSON num
+    | otherwise             = error $ "unimplemented: " <> show color
+
+instance (Eq a, Show a, Read a, FromJSON a) => FromJSON (MaybeDefault a) where
+  parseJSON Null                   = pure Default
+  parseJSON (String "keepCurrent") = pure KeepCurrent
+  parseJSON x                      = SetTo <$> parseJSON x
+
+instance ToJSON a => ToJSON (MaybeDefault a) where
+  toJSON Default     = Null
+  toJSON KeepCurrent = String "keepCurrent"
+  toJSON (SetTo x)   = toJSON x
+
+--------------------------------------------------------------------------------
+
+instance Arbitrary Color where
+  arbitrary = oneof [ Color240 <$> choose (0, 239)
+                    , ISOColor <$> choose (0, 15)
+                    ]
+
+deriving anyclass instance CoArbitrary Color
+deriving anyclass instance Function Color
+
+instance (Eq a, Show a, Read a, Arbitrary a) => Arbitrary (MaybeDefault a) where
+  arbitrary = oneof [ pure Default
+                    , pure KeepCurrent
+                    , SetTo <$> arbitrary
+                    ]
+
+instance CoArbitrary a => CoArbitrary (MaybeDefault a) where
+  coarbitrary Default = variant @Int 1
+  coarbitrary KeepCurrent = variant @Int 2
+  coarbitrary (SetTo x) = variant @Int 3 . coarbitrary x
+
+instance (Eq a, Show a, Read a, Function a) => Function (MaybeDefault a) where
+  function = functionShow
+
+deriving via (EqEqProp Attr) instance EqProp Attr
+
+instance Arbitrary Attr where
+  arbitrary = do
+    attrStyle <- arbitrary
+    attrForeColor <- arbitrary
+    attrBackColor <- arbitrary
+    attrURL <- arbitrary
+    pure Attr {..}
+
+deriving anyclass instance CoArbitrary Attr
+deriving anyclass instance Function Attr
+
+instance ToJSON Attr where
+  toJSON Attr{..} = object
+    [ "style" .= maybeDefaultToJSONWith styleToJSON attrStyle
+    , "foreground" .= attrForeColor
+    , "background" .= attrBackColor
+    , "url" .= attrURL
+    ]
+    where
+      maybeDefaultToJSONWith _ Default = Null
+      maybeDefaultToJSONWith _ KeepCurrent = String "keepCurrent"
+      maybeDefaultToJSONWith tj (SetTo x) = tj x
+      styleToJSON style
+        | style == standout     = "standout"
+        | style == underline    = "underline"
+        | style == reverseVideo = "reverseVideo"
+        | style == blink        = "blink"
+        | style == dim          = "dim"
+        | style == bold         = "bold"
+        | style == italic       = "italic"
+        | otherwise            = toJSON style
+
+instance FromJSON Attr where
+  parseJSON = withObject "Attr" $ \obj -> do
+    attrStyle <- parseStyle =<< obj .:? "style" .!= Default
+    attrForeColor <- obj .:? "foreground" .!= Default
+    attrBackColor <- obj .:? "background" .!= Default
+    attrURL <- obj .:? "url" .!= Default
+    pure Attr{..}
+
+    where
+      parseStyle (SetTo (String "standout"))     = pure (SetTo standout)
+      parseStyle (SetTo (String "underline"))    = pure (SetTo underline)
+      parseStyle (SetTo (String "reverseVideo")) = pure (SetTo reverseVideo)
+      parseStyle (SetTo (String "blink"))        = pure (SetTo blink)
+      parseStyle (SetTo (String "dim"))          = pure (SetTo dim)
+      parseStyle (SetTo (String "bold"))         = pure (SetTo bold)
+      parseStyle (SetTo (String "italic"))       = pure (SetTo italic)
+      parseStyle (SetTo n@(Number _))            = SetTo <$> parseJSON n
+      parseStyle (SetTo v)                       = typeMismatch "Style" v
+      parseStyle Default                         = pure Default
+      parseStyle KeepCurrent                     = pure KeepCurrent
+
+deriving stock instance Ord Color
+deriving stock instance Ord a => Ord (MaybeDefault a)
+deriving stock instance Ord Attr
+
+deriving anyclass instance Hashable Graphics.Vty.Input.Events.Key
+deriving anyclass instance NFData Graphics.Vty.Input.Events.Key
+deriving anyclass instance Hashable Graphics.Vty.Input.Events.Modifier
+deriving anyclass instance NFData Graphics.Vty.Input.Events.Modifier
+
+--------------------------------------------------------------------------------
+
+instance (SemiSequence a, Arbitrary (Element a), Arbitrary a)
+         => Arbitrary (NonNull a) where
+  arbitrary = ncons <$> arbitrary <*> arbitrary
+
+instance ToJSON a => ToJSON (NonNull a) where
+  toJSON = toJSON . toNullable
+
+instance (FromJSON a, MonoFoldable a) => FromJSON (NonNull a) where
+  parseJSON = maybe (fail "Found empty list") pure . fromNullable <=< parseJSON
+
+instance NFData a => NFData (NonNull a) where
+  rnf xs = xs `seq` toNullable xs `deepseq` ()
+
+--------------------------------------------------------------------------------
+
+instance forall t name. (NFData t, Monoid t, NFData name)
+                 => NFData (Editor t name) where
+  rnf ed = getName @_ @name ed `deepseq` getEditContents ed `deepseq` ()
+
+deriving via (ReadShowJSON SMGen) instance ToJSON SMGen
+deriving via (ReadShowJSON SMGen) instance FromJSON SMGen
+
+instance ToJSON StdGen where
+  toJSON = toJSON . unStdGen
+  toEncoding = toEncoding . unStdGen
+
+instance FromJSON StdGen where
+  parseJSON = fmap StdGen . parseJSON
+
+--------------------------------------------------------------------------------
+
+instance CoArbitrary a => CoArbitrary (NonNull a) where
+  coarbitrary = coarbitrary . toNullable
+
+instance (MonoFoldable a, Function a) => Function (NonNull a) where
+  function = functionMap toNullable $ fromMaybe (error "null") . fromNullable
+
+instance (Arbitrary t, Arbitrary n, GenericTextZipper t)
+       => Arbitrary (Editor t n) where
+  arbitrary = editor <$> arbitrary <*> arbitrary <*> arbitrary
+
+instance forall t n. (CoArbitrary t, CoArbitrary n, Monoid t)
+              => CoArbitrary (Editor t n) where
+  coarbitrary ed = coarbitrary (getName @_ @n ed, getEditContents ed)
+
+instance CoArbitrary StdGen where
+  coarbitrary = coarbitrary . show
+
+instance Function StdGen where
+  function = functionMap unStdGen StdGen
+
+instance Function SMGen where
+  function = functionShow
+
+--------------------------------------------------------------------------------
+
+deriving newtype instance (Arbitrary s, CoArbitrary (m (a, s)))
+            => CoArbitrary (StateT s m a)
+
+--------------------------------------------------------------------------------
+
+deriving via (GenericArbitrary (V2 a)) instance Arbitrary a => Arbitrary (V2 a)
+instance CoArbitrary a => CoArbitrary (V2 a)
+instance Function a => Function (V2 a)
+
+--------------------------------------------------------------------------------
+
+instance CoArbitrary Boundary
+instance Function Boundary
+
+instance Arbitrary a => Arbitrary (Extended a) where
+  arbitrary = oneof [ pure NegInf
+                    , pure PosInf
+                    , Finite <$> arbitrary
+                    ]
+
+instance CoArbitrary a => CoArbitrary (Extended a) where
+  coarbitrary NegInf = variant 1
+  coarbitrary PosInf = variant 2
+  coarbitrary (Finite x) = variant 3 . coarbitrary x
+
+instance (Function a) => Function (Extended a) where
+  function = functionMap g h
+    where
+     g NegInf = Left True
+     g (Finite a) = Right a
+     g PosInf = Left False
+     h (Left False) = PosInf
+     h (Left True) = NegInf
+     h (Right a) = Finite a
+
+instance ToJSON a => ToJSON (Extended a) where
+  toJSON NegInf = String "NegInf"
+  toJSON PosInf = String "PosInf"
+  toJSON (Finite x) = toJSON x
+
+instance FromJSON a => FromJSON (Extended a) where
+  parseJSON (String "NegInf") = pure NegInf
+  parseJSON (String "PosInf") = pure PosInf
+  parseJSON val               = Finite <$> parseJSON val
+
+instance (EqProp a, Show a) => EqProp (Extended a) where
+  NegInf =-= NegInf = property True
+  PosInf =-= PosInf = property True
+  (Finite x) =-= (Finite y) = x =-= y
+  x =-= y = counterexample (show x <> " /= " <> show y) False
+
+instance Arbitrary Interval.Boundary where
+  arbitrary = elements [ Interval.Open , Interval.Closed ]
+
+instance (Ord r, Arbitrary r) => Arbitrary (Interval r) where
+  arbitrary = do
+    lower <- arbitrary
+    upper <- arbitrary
+    pure $ (if upper < lower then flip else id)
+      Interval.interval
+      lower
+      upper
+
+instance CoArbitrary a => CoArbitrary (Interval a) where
+  coarbitrary int = coarbitrary (lowerBound' int) . coarbitrary (upperBound' int)
+
+instance (Function a, Ord a) => Function (Interval a) where
+  function = functionMap g h
+    where
+      g = lowerBound' &&& upperBound'
+      h = uncurry interval
+
+deriving via (EqEqProp (Interval a)) instance Eq a => (EqProp (Interval a))
+
+instance ToJSON a => ToJSON (Interval a) where
+  toJSON x = Array . fromList $
+    [ object [ lowerKey .= lowerVal ]
+    , object [ upperKey .= upperVal ]
+    ]
+    where
+      (lowerVal, lowerBoundary) = lowerBound' x
+      (upperVal, upperBoundary) = upperBound' x
+      upperKey = boundaryToKey upperBoundary
+      lowerKey = boundaryToKey lowerBoundary
+      boundaryToKey Open = "Excluded"
+      boundaryToKey Closed = "Included"
+
+instance forall a. (FromJSON a, Ord a) => FromJSON (Interval a) where
+  parseJSON x =
+    boundPairWithBoundary x
+      <|> boundPairWithoutBoundary x
+      <|> singleVal x
+    where
+      boundPairWithBoundary = withArray "Bound pair" $ \arr -> do
+        checkLength arr
+        lower <- parseBound $ arr ^?! ix 0
+        upper <- parseBound $ arr ^?! ix 1
+        pure $ interval lower upper
+      parseBound = withObject "Bound" $ \obj -> do
+        when (KM.size obj /= 1) $ fail "Expected an object with a single key"
+        let [(k, v)] = obj ^@.. ifolded
+        boundary <- case k of
+          "Excluded" -> pure Open
+          "Open"     -> pure Open
+          "Included" -> pure Closed
+          "Closed"   -> pure Closed
+          _          -> fail "Invalid boundary specification"
+        val <- parseJSON v
+        pure (val, boundary)
+      boundPairWithoutBoundary = withArray "Bound pair" $ \arr -> do
+        checkLength arr
+        lower <- parseJSON $ arr ^?! ix 0
+        upper <- parseJSON $ arr ^?! ix 1
+        pure $ lower <=..< upper
+      singleVal v = do
+        val <- parseJSON v
+        pure $ val <=..<= val
+      checkLength arr =
+        when (length arr /= 2) $ fail "Expected array of length 2"
diff --git a/users/grfn/xanthous/src/Xanthous/Physics.hs b/users/grfn/xanthous/src/Xanthous/Physics.hs
new file mode 100644
index 0000000000..37530cbbc2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Physics.hs
@@ -0,0 +1,71 @@
+--------------------------------------------------------------------------------
+module Xanthous.Physics
+  ( throwDistance
+  , bluntThrowDamage
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+import Xanthous.Data
+       ( Meters
+       , (:**:)(..)
+       , Square
+       , Grams
+       , (|*|)
+       , (|/|)
+       , Hitpoints
+       , Per (..)
+       , squared
+       , Uno(..), (|+|)
+       )
+--------------------------------------------------------------------------------
+
+-- university shotputter can put a 16 lb shot about 14 meters
+-- ≈ 7.25 kg 14 meters
+-- 14m = x / (7.25kg × y + z)²
+-- 14m = x / (7250g × y + z)²
+--
+-- we don't want to scale down too much:
+--
+-- 10 kg 10 meters
+-- = 10000 g 10 meters
+--
+-- 15 kg w meters
+-- = 15000 g w meters
+--
+-- 14m = x / (7250g × y + z)²
+-- 10m = x / (10000g × y + z)²
+-- wm = x / (15000g × y + z)²
+--
+-- w≈0.527301 ∧ y≈0.000212178 sqrt(x) ∧ z≈1.80555 sqrt(x) ∧ 22824.1 sqrt(x)!=0
+--
+-- x = 101500
+-- y = 0.0675979
+-- z = 575.231
+--
+
+-- TODO make this dynamic
+strength :: Meters :**: Square Grams
+strength = Times 10150000
+
+yCoeff :: Uno Double
+yCoeff = Uno 0.0675979
+
+zCoeff :: Uno Double
+zCoeff = Uno 575.231
+
+-- | Calculate the maximum distance an object with the given weight can be
+-- thrown
+throwDistance
+  :: Grams  -- ^ Weight of the object
+  -> Meters -- ^ Max distance thrown
+throwDistance weight = strength |/| squared (weight |*| yCoeff |+| zCoeff)
+
+-- | Returns the damage dealt by a blunt object with the given weight when
+-- thrown
+bluntThrowDamage
+  :: Grams
+  -> Hitpoints
+bluntThrowDamage weight = throwDamageRatio |*| weight
+  where
+    throwDamageRatio :: Hitpoints `Per` Grams
+    throwDamageRatio = Rate $ 1 / 5000
diff --git a/users/grfn/xanthous/src/Xanthous/Prelude.hs b/users/grfn/xanthous/src/Xanthous/Prelude.hs
new file mode 100644
index 0000000000..2cb4299303
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Prelude.hs
@@ -0,0 +1,48 @@
+--------------------------------------------------------------------------------
+module Xanthous.Prelude
+  ( module ClassyPrelude
+  , Type
+  , Constraint
+  , module GHC.TypeLits
+  , module Control.Lens
+  , module Data.Void
+  , module Control.Comonad
+  , module Witherable
+  , fail
+
+  , (&!)
+
+    -- * Classy-Prelude addons
+  , ninsertSet
+  , ndeleteSet
+  , toVector
+  ) where
+--------------------------------------------------------------------------------
+import ClassyPrelude hiding
+  ( return, (<|), unsnoc, uncons, cons, snoc, index, (<.>), Index, say
+  , catMaybes, filter, mapMaybe, hashNub, ordNub
+  , Memoized, runMemoized
+  )
+import Data.Kind
+import GHC.TypeLits hiding (Text)
+import Control.Lens hiding (levels, Level)
+import Data.Void
+import Control.Comonad
+import Witherable
+import Control.Monad.Fail (fail)
+--------------------------------------------------------------------------------
+
+ninsertSet
+  :: (IsSet set, MonoPointed set)
+  => Element set -> NonNull set -> NonNull set
+ninsertSet x xs = impureNonNull $ opoint x `union` toNullable xs
+
+ndeleteSet :: IsSet b => Element b -> NonNull b -> b
+ndeleteSet x = deleteSet x . toNullable
+
+toVector :: (MonoFoldable (f a), Element (f a) ~ a) => f a -> Vector a
+toVector = fromList . toList
+
+infixl 1 &!
+(&!) :: a -> (a -> b) -> b
+(&!) = flip ($!)
diff --git a/users/grfn/xanthous/src/Xanthous/Random.hs b/users/grfn/xanthous/src/Xanthous/Random.hs
new file mode 100644
index 0000000000..329b321b8b
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Random.hs
@@ -0,0 +1,186 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+--------------------------------------------------------------------------------
+module Xanthous.Random
+  ( Choose(..)
+  , ChooseElement(..)
+  , Weighted(..)
+  , evenlyWeighted
+  , weightedBy
+  , subRand
+  , chance
+  , chooseSubset
+  , chooseRange
+  , FiniteInterval(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.List.NonEmpty (NonEmpty(..))
+import           Control.Monad.Random.Class (MonadRandom(getRandomR, getRandom))
+import           Control.Monad.Random (Rand, evalRand, mkStdGen, StdGen)
+import           Data.Functor.Compose
+import           Data.Random.Shuffle.Weighted
+import           Data.Random.Distribution
+import           Data.Random.Distribution.Uniform
+import           Data.Random.Distribution.Uniform.Exclusive
+import           Data.Random.Sample
+import qualified Data.Random.Source as DRS
+import           Data.Interval ( Interval, lowerBound', Extended (Finite)
+                               , upperBound', Boundary (Closed), lowerBound, upperBound
+                               )
+--------------------------------------------------------------------------------
+
+instance {-# INCOHERENT #-} (Monad m, MonadRandom m) => DRS.MonadRandom m where
+  getRandomWord8 = getRandom
+  getRandomWord16 = getRandom
+  getRandomWord32 = getRandom
+  getRandomWord64 = getRandom
+  getRandomDouble = getRandom
+  getRandomNByteInteger n = getRandomR (0, 256 ^ n)
+
+class Choose a where
+  type RandomResult a
+  choose :: MonadRandom m => a -> m (RandomResult a)
+
+newtype ChooseElement a = ChooseElement a
+
+instance MonoFoldable a => Choose (ChooseElement a) where
+  type RandomResult (ChooseElement a) = Maybe (Element a)
+  choose (ChooseElement xs) = do
+    chosenIdx <- getRandomR (0, olength xs - 1)
+    let pick _ (Just x) = Just x
+        pick (x, i) Nothing
+          | i == chosenIdx = Just x
+          | otherwise = Nothing
+    pure $ ofoldr pick Nothing $ zip (toList xs) [0..]
+
+instance MonoFoldable a => Choose (NonNull a) where
+  type RandomResult (NonNull a) = Element a
+  choose
+    = fmap (fromMaybe (error "unreachable")) -- why not lol
+    . choose
+    . ChooseElement
+    . toNullable
+
+instance Choose (NonEmpty a) where
+  type RandomResult (NonEmpty a) = a
+  choose = choose . fromNonEmpty @[_]
+
+instance Choose (a, a) where
+  type RandomResult (a, a) = a
+  choose (x, y) = choose (x :| [y])
+
+newtype Weighted w t a = Weighted (t (w, a))
+  deriving (Functor, Foldable) via (t `Compose` (,) w)
+
+deriving newtype instance Eq (t (w, a)) => Eq (Weighted w t a)
+deriving newtype instance Show (t (w, a)) => Show (Weighted w t a)
+deriving newtype instance NFData (t (w, a)) => NFData (Weighted w t a)
+
+instance Traversable t => Traversable (Weighted w t) where
+  traverse f (Weighted twa) = Weighted <$> (traverse . traverse) f twa
+
+evenlyWeighted :: [a] -> Weighted Int [] a
+evenlyWeighted = Weighted . itoList
+
+-- | Weight the elements of some functor by a function. Larger values of 'w' per
+-- its 'Ord' instance will be more likely to be generated
+weightedBy :: Functor t => (a -> w) -> t a -> Weighted w t a
+weightedBy weighting xs = Weighted $ (weighting &&& id) <$> xs
+
+instance (Num w, Ord w, Distribution Uniform w, Excludable w)
+       => Choose (Weighted w [] a) where
+  type RandomResult (Weighted w [] a) = Maybe a
+  choose (Weighted ws) = sample $ headMay <$> weightedSample 1 ws
+
+instance (Num w, Ord w, Distribution Uniform w, Excludable w)
+       => Choose (Weighted w NonEmpty a) where
+  type RandomResult (Weighted w NonEmpty a) = a
+  choose (Weighted ws) =
+    sample
+    $ fromMaybe (error "unreachable") . headMay
+    <$> weightedSample 1 (toList ws)
+
+subRand :: MonadRandom m => Rand StdGen a -> m a
+subRand sub = evalRand sub . mkStdGen <$> getRandom
+
+-- | Has a @n@ chance of returning 'True'
+--
+-- eg, chance 0.5 will return 'True' half the time
+chance
+  :: (Num w, Ord w, Distribution Uniform w, Excludable w, MonadRandom m)
+  => w
+  -> m Bool
+chance n = choose $ weightedBy (bool 1 (n * 2)) bools
+
+-- | Choose a random subset of *about* @w@ of the elements of the given
+-- 'Witherable' structure
+chooseSubset :: ( Num w, Ord w, Distribution Uniform w, Excludable w
+               , Witherable t
+               , MonadRandom m
+               ) => w -> t a -> m (t a)
+chooseSubset = filterA . const . chance
+
+-- | Choose a random @n@ in the given interval
+chooseRange
+  :: ( MonadRandom m
+    , Distribution Uniform n
+    , Enum n
+    , Bounded n
+    , Ord n
+    )
+  => Interval n
+  -> m (Maybe n)
+chooseRange int = traverse sample distribution
+  where
+    (lower, lowerBoundary) = lowerBound' int
+    lowerR = case lower of
+      Finite x -> if lowerBoundary == Closed
+                 then x
+                 else succ x
+      _ -> minBound
+    (upper, upperBoundary) = upperBound' int
+    upperR = case upper of
+      Finite x -> if upperBoundary == Closed
+                 then x
+                 else pred x
+      _ -> maxBound
+    distribution
+      | lowerR <= upperR = Just $ Uniform lowerR upperR
+      | otherwise = Nothing
+
+instance ( Distribution Uniform n
+         , Enum n
+         , Bounded n
+         , Ord n
+         )
+         => Choose (Interval n) where
+  type RandomResult (Interval n) = n
+  choose = fmap (fromMaybe $ error "Invalid interval") . chooseRange
+
+newtype FiniteInterval a
+  = FiniteInterval { unwrapFiniteInterval :: (Interval a) }
+
+instance ( Distribution Uniform n
+         , Ord n
+         )
+         => Choose (FiniteInterval n) where
+  type RandomResult (FiniteInterval n) = n
+  -- TODO broken with open/closed right now
+  choose
+    = sample
+    . uncurry Uniform
+    . over both getFinite
+    . (lowerBound &&& upperBound)
+    . unwrapFiniteInterval
+    where
+      getFinite (Finite x) = x
+      getFinite _ = error "Infinite value"
+
+--------------------------------------------------------------------------------
+
+bools :: NonEmpty Bool
+bools = True :| [False]
diff --git a/users/grfn/xanthous/src/Xanthous/Util.hs b/users/grfn/xanthous/src/Xanthous/Util.hs
new file mode 100644
index 0000000000..f918340f05
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util.hs
@@ -0,0 +1,351 @@
+{-# LANGUAGE BangPatterns          #-}
+{-# LANGUAGE AllowAmbiguousTypes   #-}
+{-# LANGUAGE QuantifiedConstraints #-}
+--------------------------------------------------------------------------------
+module Xanthous.Util
+  ( EqEqProp(..)
+  , EqProp(..)
+  , foldlMapM
+  , foldlMapM'
+  , between
+
+  , appendVia
+
+    -- * Foldable
+    -- ** Uniqueness
+    -- *** Predicates on uniqueness
+  , isUniqueOf
+  , isUnique
+    -- *** Removing all duplicate elements in n * log n time
+  , uniqueOf
+  , unique
+    -- *** Removing sequentially duplicate elements in linear time
+  , uniqOf
+  , uniq
+    -- ** Bag sequence algorithms
+  , takeWhileInclusive
+  , smallestNotIn
+  , removeVectorIndex
+  , removeFirst
+  , maximum1
+  , minimum1
+
+    -- * Combinators
+  , times, times_, endoTimes
+
+    -- * State utilities
+  , modifyK, modifyKL, useListOf
+
+    -- * Type-level programming utils
+  , KnownBool(..)
+
+    -- *
+  , AlphaChar(..)
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (foldr)
+--------------------------------------------------------------------------------
+import           Test.QuickCheck.Checkers
+import           Data.Foldable (foldr)
+import           Data.Monoid
+import           Data.Proxy
+import qualified Data.Vector as V
+import           Data.Semigroup (Max(..), Min(..))
+import           Data.Semigroup.Foldable
+import           Control.Monad.State.Class
+import           Control.Monad.State (evalState)
+--------------------------------------------------------------------------------
+
+newtype EqEqProp a = EqEqProp a
+  deriving newtype Eq
+
+instance Eq a => EqProp (EqEqProp a) where
+  (=-=) = eq
+
+foldlMapM :: forall g b a m. (Foldable g, Monoid b, Applicative m) => (a -> m b) -> g a -> m b
+foldlMapM f = foldr f' (pure mempty)
+  where
+    f' :: a -> m b -> m b
+    f' x = liftA2 mappend (f x)
+
+-- Strict in the monoidal accumulator. For monads strict
+-- in the left argument of bind, this will run in constant
+-- space.
+foldlMapM' :: forall g b a m. (Foldable g, Monoid b, Monad m) => (a -> m b) -> g a -> m b
+foldlMapM' f xs = foldr f' pure xs mempty
+  where
+  f' :: a -> (b -> m b) -> b -> m b
+  f' x k bl = do
+    br <- f x
+    let !b = mappend bl br
+    k b
+
+-- | Returns whether the third argument is in the range given by the first two
+-- arguments, inclusive
+--
+-- >>> between (0 :: Int) 2 2
+-- True
+--
+-- >>> between (0 :: Int) 2 3
+-- False
+between
+  :: Ord a
+  => a -- ^ lower bound
+  -> a -- ^ upper bound
+  -> a -- ^ scrutinee
+  -> Bool
+between lower upper x = x >= lower && x <= upper
+
+-- |
+-- >>> appendVia Sum 1 2
+-- 3
+appendVia :: (Rewrapping s t, Semigroup s) => (Unwrapped s -> s) -> Unwrapped s -> Unwrapped s -> Unwrapped s
+appendVia wrap x y = op wrap $ wrap x <> wrap y
+
+--------------------------------------------------------------------------------
+
+-- | Returns True if the targets of the given 'Fold' are unique per the 'Ord' instance for @a@
+--
+-- >>> isUniqueOf (folded . _1) ([(1, 2), (2, 2), (3, 2)] :: [(Int, Int)])
+-- True
+--
+-- >>> isUniqueOf (folded . _2) ([(1, 2), (2, 2), (3, 2)] :: [(Int, Int)])
+-- False
+--
+-- @
+-- 'isUniqueOf' :: Ord a => 'Getter' s a     -> s -> 'Bool'
+-- 'isUniqueOf' :: Ord a => 'Fold' s a       -> s -> 'Bool'
+-- 'isUniqueOf' :: Ord a => 'Lens'' s a      -> s -> 'Bool'
+-- 'isUniqueOf' :: Ord a => 'Iso'' s a       -> s -> 'Bool'
+-- 'isUniqueOf' :: Ord a => 'Traversal'' s a -> s -> 'Bool'
+-- 'isUniqueOf' :: Ord a => 'Prism'' s a     -> s -> 'Bool'
+-- @
+isUniqueOf :: Ord a => Getting (Endo (Set a, Bool)) s a -> s -> Bool
+isUniqueOf aFold = orOf _2 . foldrOf aFold rejectUnique (mempty, True)
+ where
+  rejectUnique x (seen, acc)
+    | seen ^. contains x = (seen, False)
+    | otherwise          = (seen & contains x .~ True, acc)
+
+-- | Returns true if the given 'Foldable' container contains only unique
+-- elements, as determined by the 'Ord' instance for @a@
+--
+-- >>> isUnique ([3, 1, 2] :: [Int])
+-- True
+--
+-- >>> isUnique ([1, 1, 2, 2, 3, 1] :: [Int])
+-- False
+isUnique :: (Foldable f, Ord a) => f a -> Bool
+isUnique = isUniqueOf folded
+
+
+-- | O(n * log n). Returns a monoidal, 'Cons'able container (a list, a Set,
+-- etc.) consisting of the unique (per the 'Ord' instance for @a@) targets of
+-- the given 'Fold'
+--
+-- >>> uniqueOf (folded . _2) ([(1, 2), (2, 2), (3, 2), (4, 3)] :: [(Int, Int)]) :: [Int]
+-- [2,3]
+--
+-- @
+-- 'uniqueOf' :: Ord a => 'Getter' s a     -> s -> [a]
+-- 'uniqueOf' :: Ord a => 'Fold' s a       -> s -> [a]
+-- 'uniqueOf' :: Ord a => 'Lens'' s a      -> s -> [a]
+-- 'uniqueOf' :: Ord a => 'Iso'' s a       -> s -> [a]
+-- 'uniqueOf' :: Ord a => 'Traversal'' s a -> s -> [a]
+-- 'uniqueOf' :: Ord a => 'Prism'' s a     -> s -> [a]
+-- @
+uniqueOf
+  :: (Monoid c, Ord w, Cons c c w w) => Getting (Endo (Set w, c)) a w -> a -> c
+uniqueOf aFold = snd . foldrOf aFold rejectUnique (mempty, mempty)
+ where
+  rejectUnique x (seen, acc)
+    | seen ^. contains x = (seen, acc)
+    | otherwise          = (seen & contains x .~ True, cons x acc)
+
+-- | Returns a monoidal, 'Cons'able container (a list, a Set, etc.) consisting
+-- of the unique (per the 'Ord' instance for @a@) contents of the given
+-- 'Foldable' container
+--
+-- >>> unique [1, 1, 2, 2, 3, 1] :: [Int]
+-- [2,3,1]
+
+-- >>> unique [1, 1, 2, 2, 3, 1] :: Set Int
+-- fromList [3,2,1]
+unique :: (Foldable f, Cons c c a a, Ord a, Monoid c) => f a -> c
+unique = uniqueOf folded
+
+--------------------------------------------------------------------------------
+
+-- | O(n). Returns a monoidal, 'Cons'able container (a list, a Vector, etc.)
+-- consisting of the targets of the given 'Fold' with sequential duplicate
+-- elements removed
+--
+-- This function (sorry for the confusing name) differs from 'uniqueOf' in that
+-- it only compares /sequentially/ duplicate elements (and thus operates in
+-- linear time).
+-- cf 'Data.Vector.uniq' and POSIX @uniq@ for the name
+--
+-- >>> uniqOf (folded . _2) ([(1, 2), (2, 2), (3, 1), (4, 2)] :: [(Int, Int)]) :: [Int]
+-- [2,1,2]
+--
+-- @
+-- 'uniqOf' :: Eq a => 'Getter' s a     -> s -> [a]
+-- 'uniqOf' :: Eq a => 'Fold' s a       -> s -> [a]
+-- 'uniqOf' :: Eq a => 'Lens'' s a      -> s -> [a]
+-- 'uniqOf' :: Eq a => 'Iso'' s a       -> s -> [a]
+-- 'uniqOf' :: Eq a => 'Traversal'' s a -> s -> [a]
+-- 'uniqOf' :: Eq a => 'Prism'' s a     -> s -> [a]
+-- @
+uniqOf :: (Monoid c, Cons c c w w, Eq w) => Getting (Endo (Maybe w, c)) a w -> a -> c
+uniqOf aFold = snd . foldrOf aFold rejectSeen (Nothing, mempty)
+  where
+    rejectSeen x (Nothing, acc) = (Just x, x <| acc)
+    rejectSeen x tup@(Just a, acc)
+      | x == a     = tup
+      | otherwise = (Just x, x <| acc)
+
+-- | O(n). Returns a monoidal, 'Cons'able container (a list, a Vector, etc.)
+-- consisting of the targets of the given 'Foldable' container with sequential
+-- duplicate elements removed
+--
+-- This function (sorry for the confusing name) differs from 'unique' in that
+-- it only compares /sequentially/ unique elements (and thus operates in linear
+-- time).
+-- cf 'Data.Vector.uniq' and POSIX @uniq@ for the name
+--
+-- >>> uniq [1, 1, 1, 2, 2, 2, 3, 3, 1] :: [Int]
+-- [1,2,3,1]
+--
+-- >>> uniq [1, 1, 1, 2, 2, 2, 3, 3, 1] :: Vector Int
+-- [1,2,3,1]
+--
+uniq :: (Foldable f, Eq a, Cons c c a a, Monoid c) => f a -> c
+uniq = uniqOf folded
+
+-- | Like 'takeWhile', but inclusive
+takeWhileInclusive :: (a -> Bool) -> [a] -> [a]
+takeWhileInclusive _ [] = []
+takeWhileInclusive p (x:xs) = x : if p x then takeWhileInclusive p xs else []
+
+-- | Returns the smallest value not in a list
+smallestNotIn :: (Ord a, Bounded a, Enum a) => [a] -> a
+smallestNotIn xs = case uniq $ sort xs of
+  [] -> minBound
+  xs'@(x : _)
+    | x > minBound -> minBound
+    | otherwise
+    -> snd . headEx . filter (uncurry (/=)) $ zip (xs' ++ [minBound]) [minBound..]
+
+-- | Remove the element at the given index, if any, from the given vector
+removeVectorIndex :: Int -> Vector a -> Vector a
+removeVectorIndex idx vect =
+  let (before, after) = V.splitAt idx vect
+  in before <> fromMaybe Empty (tailMay after)
+
+-- | Remove the first element in a sequence that matches a given predicate
+removeFirst :: IsSequence seq => (Element seq -> Bool) -> seq -> seq
+removeFirst p
+  = flip evalState False
+  . filterM (\x -> do
+                found <- get
+                let matches = p x
+                when matches $ put True
+                pure $ found || not matches)
+
+maximum1 :: (Ord a, Foldable1 f) => f a -> a
+maximum1 = getMax . foldMap1 Max
+
+minimum1 :: (Ord a, Foldable1 f) => f a -> a
+minimum1 = getMin . foldMap1 Min
+
+times :: (Applicative f, Num n, Enum n) => n -> (n -> f b) -> f [b]
+times n f = traverse f [1..n]
+
+times_ :: (Applicative f, Num n, Enum n) => n -> f a -> f [a]
+times_ n fa = times n (const fa)
+
+-- | Multiply an endomorphism by an integral
+--
+-- >>> endoTimes (4 :: Int) succ (5 :: Int)
+-- 9
+endoTimes :: Integral n => n -> (a -> a) -> a -> a
+endoTimes n f = appEndo $ stimes n (Endo f)
+
+--------------------------------------------------------------------------------
+
+-- | This class gives a boolean associated with a type-level bool, a'la
+-- 'KnownSymbol', 'KnownNat' etc.
+class KnownBool (bool :: Bool) where
+  boolVal' :: forall proxy. proxy bool -> Bool
+  boolVal' _ = boolVal @bool
+
+  boolVal :: Bool
+  boolVal = boolVal' $ Proxy @bool
+
+instance KnownBool 'True where boolVal = True
+instance KnownBool 'False where boolVal = False
+
+--------------------------------------------------------------------------------
+
+-- | Modify some monadic state via the application of a kleisli endomorphism on
+-- the state itself
+--
+-- Note that any changes made to the state during execution of @k@ will be
+-- overwritten
+--
+-- @@
+-- modifyK pure === pure ()
+-- @@
+modifyK :: MonadState s m => (s -> m s) -> m ()
+modifyK k = get >>= k >>= put
+
+-- | Modify some monadic state via the application of a kleisli endomorphism on
+-- the target of a lens
+--
+-- Note that any changes made to the state during execution of @k@ will be
+-- overwritten
+--
+-- @@
+-- modifyKL id pure === pure ()
+-- @@
+modifyKL :: MonadState s m => LensLike m s s a b -> (a -> m b) -> m ()
+modifyKL l k = get >>= traverseOf l k >>= put
+
+-- | Use a list of all the targets of a 'Fold' in the current state
+--
+-- @@
+-- evalState (useListOf folded) === toList
+-- @@
+useListOf :: MonadState s m => Getting (Endo [a]) s a -> m [a]
+useListOf = gets . toListOf
+
+--------------------------------------------------------------------------------
+
+-- | A newtype wrapper around 'Char' whose 'Enum' and 'Bounded' instances only
+-- include the characters @[a-zA-Z]@
+--
+-- >>> succ (AlphaChar 'z')
+-- 'A'
+newtype AlphaChar = AlphaChar { getAlphaChar :: Char }
+  deriving stock Show
+  deriving (Eq, Ord) via Char
+
+instance Enum AlphaChar where
+  toEnum n
+    | between 0 25 n
+    = AlphaChar . toEnum $ n + fromEnum 'a'
+    | between 26 51 n
+    = AlphaChar . toEnum $ n - 26 + fromEnum 'A'
+    | otherwise
+    = error $ "Tag " <> show n <> " out of range [0, 51] for enum AlphaChar"
+  fromEnum (AlphaChar chr)
+    | between 'a' 'z' chr
+    = fromEnum chr - fromEnum 'a'
+    | between 'A' 'Z' chr
+    = fromEnum chr - fromEnum 'A'
+    | otherwise
+    = error $ "Invalid value for alpha char: " <> show chr
+
+instance Bounded AlphaChar where
+  minBound = AlphaChar 'a'
+  maxBound = AlphaChar 'Z'
diff --git a/users/grfn/xanthous/src/Xanthous/Util/Comonad.hs b/users/grfn/xanthous/src/Xanthous/Util/Comonad.hs
new file mode 100644
index 0000000000..9e158cc8e2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/Comonad.hs
@@ -0,0 +1,24 @@
+--------------------------------------------------------------------------------
+module Xanthous.Util.Comonad
+  ( -- * Store comonad utils
+    replace
+  , current
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Control.Comonad.Store.Class
+--------------------------------------------------------------------------------
+
+-- | Replace the current position of a store comonad with a new value by
+-- comparing positions
+replace :: (Eq i, ComonadStore i w) => w a -> a -> w a
+replace w x = w =>> \w' -> if pos w' == pos w then x else extract w'
+{-# INLINE replace #-}
+
+-- | Lens into the current position of a store comonad.
+--
+--     current = lens extract replace
+current :: (Eq i, ComonadStore i w) => Lens' (w a) a
+current = lens extract replace
+{-# INLINE current #-}
diff --git a/users/grfn/xanthous/src/Xanthous/Util/Graph.hs b/users/grfn/xanthous/src/Xanthous/Util/Graph.hs
new file mode 100644
index 0000000000..8e5c04f4bf
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/Graph.hs
@@ -0,0 +1,33 @@
+--------------------------------------------------------------------------------
+module Xanthous.Util.Graph where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+import           Data.Graph.Inductive.Query.MST (msTree)
+import qualified Data.Graph.Inductive.Graph as Graph
+import           Data.Graph.Inductive.Graph
+import           Data.Graph.Inductive.Basic (undir)
+import           Data.Set (isSubsetOf)
+--------------------------------------------------------------------------------
+
+mstSubGraph
+  :: forall gr node edge. (DynGraph gr, Real edge, Show edge)
+  => gr node edge -> gr node edge
+mstSubGraph graph = insEdges mstEdges . insNodes (labNodes graph) $ Graph.empty
+  where
+    mstEdges = ordNub $ do
+      LP path <- msTree $ undir graph
+      case path of
+        [] -> []
+        [_] -> []
+        ((n₂, edgeWeight) : (n₁, _) : _) ->
+          pure (n₁, n₂, edgeWeight)
+
+isSubGraphOf
+  :: (Graph gr1, Graph gr2, Ord node, Ord edge)
+  => gr1 node edge
+  -> gr2 node edge
+  -> Bool
+isSubGraphOf graph₁ graph₂
+  = setFromList (labNodes graph₁) `isSubsetOf` setFromList (labNodes graph₂)
+  && setFromList (labEdges graph₁) `isSubsetOf` setFromList (labEdges graph₂)
diff --git a/users/grfn/xanthous/src/Xanthous/Util/Graphics.hs b/users/grfn/xanthous/src/Xanthous/Util/Graphics.hs
new file mode 100644
index 0000000000..0cb009f45a
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/Graphics.hs
@@ -0,0 +1,177 @@
+{-# LANGUAGE TemplateHaskell #-}
+-- | Graphics algorithms and utils for rendering things in 2D space
+--------------------------------------------------------------------------------
+module Xanthous.Util.Graphics
+  ( circle
+  , filledCircle
+  , line
+  , straightLine
+  , delaunay
+
+    -- * Debugging and testing tools
+  , renderBooleanGraphics
+  , showBooleanGraphics
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude
+--------------------------------------------------------------------------------
+-- https://github.com/noinia/hgeometry/issues/28
+-- import qualified Algorithms.Geometry.DelaunayTriangulation.DivideAndConquer
+--               as Geometry
+import qualified Algorithms.Geometry.DelaunayTriangulation.Naive
+              as Geometry
+import qualified Algorithms.Geometry.DelaunayTriangulation.Types as Geometry
+import           Control.Monad.State (execState, State)
+import qualified Data.Geometry.Point as Geometry
+import           Data.Ext ((:+)(..))
+import           Data.List (unfoldr)
+import           Data.List.NonEmpty (NonEmpty((:|)))
+import qualified Data.List.NonEmpty as NE
+import           Data.Ix (Ix)
+import           Linear.V2
+--------------------------------------------------------------------------------
+
+
+-- | Generate a circle centered at the given point and with the given radius
+-- using the <midpoint circle algorithm
+-- https://en.wikipedia.org/wiki/Midpoint_circle_algorithm>.
+--
+-- Code taken from <https://rosettacode.org/wiki/Bitmap/Midpoint_circle_algorithm#Haskell>
+circle :: (Num i, Ord i)
+       => V2 i -- ^ center
+       -> i    -- ^ radius
+       -> [V2 i]
+circle (V2 x₀ y₀) radius
+  -- Four initial points, plus the generated points
+  = V2 x₀ (y₀ + radius)
+  : V2 x₀ (y₀ - radius)
+  : V2 (x₀ + radius) y₀
+  : V2 (x₀ - radius) y₀
+  : points
+    where
+      -- Creates the (x, y) octet offsets, then maps them to absolute points in all octets.
+      points = concatMap generatePoints $ unfoldr step initialValues
+
+      generatePoints (V2 x y)
+        = [ V2 (x₀ `xop` x') (y₀ `yop` y')
+          | (x', y') <- [(x, y), (y, x)]
+          , xop <- [(+), (-)]
+          , yop <- [(+), (-)]
+          ]
+
+      initialValues = (1 - radius, 1, (-2) * radius, 0, radius)
+
+      step (f, ddf_x, ddf_y, x, y)
+        | x >= y = Nothing
+        | otherwise = Just (V2 x' y', (f', ddf_x', ddf_y', x', y'))
+        where
+          (f', ddf_y', y') | f >= 0 = (f + ddf_y' + ddf_x', ddf_y + 2, y - 1)
+                           | otherwise = (f + ddf_x, ddf_y, y)
+          ddf_x' = ddf_x + 2
+          x' = x + 1
+
+
+data FillState i
+  = FillState
+  { _inCircle :: Bool
+  , _result :: NonEmpty (V2 i)
+  }
+makeLenses ''FillState
+
+runFillState :: NonEmpty (V2 i) -> State (FillState i) a -> [V2 i]
+runFillState circumference s
+  = toList
+  . view result
+  . execState s
+  $ FillState False circumference
+
+-- | Generate a *filled* circle centered at the given point and with the given
+-- radius by filling a circle generated with 'circle'
+filledCircle :: (Num i, Integral i, Ix i)
+             => V2 i -- ^ center
+             -> i    -- ^ radius
+             -> [V2 i]
+filledCircle center radius =
+  case NE.nonEmpty (circle center radius) of
+    Nothing -> []
+    Just circumference -> runFillState circumference $
+      -- the first and last lines of all circles are solid, so the whole "in the
+      -- circle, out of the circle" thing doesn't work... but that's fine since
+      -- we don't need to fill them. So just skip them
+      for_ [succ minX..pred maxX] $ \x ->
+        for_ [minY..maxY] $ \y -> do
+          let pt = V2 x y
+              next = V2 x $ succ y
+          whenM (use inCircle) $ result %= NE.cons pt
+
+          when (pt `elem` circumference && next `notElem` circumference)
+            $ inCircle %= not
+
+      where
+        (V2 minX minY, V2 maxX maxY) = minmaxes circumference
+
+-- | Draw a line between two points using Bresenham's line drawing algorithm
+--
+-- Code taken from <https://wiki.haskell.org/Bresenham%27s_line_drawing_algorithm>
+line :: (Num i, Ord i) => V2 i -> V2 i -> [V2 i]
+line pa@(V2 xa ya) pb@(V2 xb yb)
+  = (if maySwitch pa < maySwitch pb then id else reverse) points
+  where
+    points               = map maySwitch . unfoldr go $ (x₁, y₁, 0)
+    steep                = abs (yb - ya) > abs (xb - xa)
+    maySwitch            = if steep then view _yx else id
+    [V2 x₁ y₁, V2 x₂ y₂] = sort [maySwitch pa, maySwitch pb]
+    δx                   = x₂ - x₁
+    δy                   = abs (y₂ - y₁)
+    ystep                = if y₁ < y₂ then 1 else -1
+    go (xTemp, yTemp, err)
+      | xTemp > x₂ = Nothing
+      | otherwise  = Just (V2 xTemp yTemp, (xTemp + 1, newY, newError))
+      where
+        tempError        = err + δy
+        (newY, newError) = if (2 * tempError) >= δx
+                           then (yTemp + ystep, tempError - δx)
+                           else (yTemp, tempError)
+{-# SPECIALIZE line :: V2 Int -> V2 Int -> [V2 Int] #-}
+{-# SPECIALIZE line :: V2 Word -> V2 Word -> [V2 Word] #-}
+
+straightLine :: (Num i, Ord i) => V2 i -> V2 i -> [V2 i]
+straightLine pa@(V2 xa _) pb@(V2 _ yb) = line pa midpoint ++ line midpoint pb
+  where midpoint = V2 xa yb
+
+delaunay
+  :: (Ord n, Fractional n)
+  => NonEmpty (V2 n, p)
+  -> [((V2 n, p), (V2 n, p))]
+delaunay
+  = map (over both fromPoint)
+  . Geometry.edgesAsPoints
+  . Geometry.delaunayTriangulation
+  . map toPoint
+  where
+    toPoint (V2 px py, pid) = Geometry.Point2 px py :+ pid
+    fromPoint (Geometry.Point2 px py :+ pid) = (V2 px py, pid)
+
+--------------------------------------------------------------------------------
+
+renderBooleanGraphics :: forall i. (Num i, Ord i, Enum i) => [V2 i] -> String
+renderBooleanGraphics [] = ""
+renderBooleanGraphics (pt : pts') = intercalate "\n" rows
+  where
+    rows = row <$> [minX..maxX]
+    row x = [minY..maxY] <&> \y -> if V2 x y `member` ptSet then 'X' else ' '
+    (V2 minX minY, V2 maxX maxY) = minmaxes pts
+    pts = pt :| pts'
+    ptSet :: Set (V2 i)
+    ptSet = setFromList $ toList pts
+
+showBooleanGraphics :: forall i. (Num i, Ord i, Enum i) => [V2 i] -> IO ()
+showBooleanGraphics = putStrLn . pack . renderBooleanGraphics
+
+minmaxes :: forall i. (Ord i) => NonEmpty (V2 i) -> (V2 i, V2 i)
+minmaxes xs =
+  ( V2 (minimum1Of (traverse1 . _x) xs)
+       (minimum1Of (traverse1 . _y) xs)
+  , V2 (maximum1Of (traverse1 . _x) xs)
+       (maximum1Of (traverse1 . _y) xs)
+  )
diff --git a/users/grfn/xanthous/src/Xanthous/Util/Inflection.hs b/users/grfn/xanthous/src/Xanthous/Util/Inflection.hs
new file mode 100644
index 0000000000..724f2339dd
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/Inflection.hs
@@ -0,0 +1,14 @@
+
+module Xanthous.Util.Inflection
+  ( toSentence
+  ) where
+
+import Xanthous.Prelude
+
+toSentence :: (MonoFoldable mono, Element mono ~ Text) => mono -> Text
+toSentence xs = case reverse . toList $ xs of
+  [] -> ""
+  [x] -> x
+  [b, a] -> a <> " and " <> b
+  (final : butlast) ->
+    intercalate ", " (reverse butlast) <> ", and " <> final
diff --git a/users/grfn/xanthous/src/Xanthous/Util/JSON.hs b/users/grfn/xanthous/src/Xanthous/Util/JSON.hs
new file mode 100644
index 0000000000..91d1328e4a
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/JSON.hs
@@ -0,0 +1,19 @@
+--------------------------------------------------------------------------------
+module Xanthous.Util.JSON
+  ( ReadShowJSON(..)
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import Data.Aeson
+--------------------------------------------------------------------------------
+
+newtype ReadShowJSON a = ReadShowJSON a
+  deriving newtype (Read, Show)
+
+instance Show a => ToJSON (ReadShowJSON a) where
+  toJSON = toJSON . show
+
+instance Read a => FromJSON (ReadShowJSON a) where
+  parseJSON = withText "readable"
+    $ maybe (fail "Could not read") pure . readMay
diff --git a/users/grfn/xanthous/src/Xanthous/Util/Optparse.hs b/users/grfn/xanthous/src/Xanthous/Util/Optparse.hs
new file mode 100644
index 0000000000..dfa6537235
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/Optparse.hs
@@ -0,0 +1,21 @@
+--------------------------------------------------------------------------------
+module Xanthous.Util.Optparse
+  ( readWithGuard
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+--------------------------------------------------------------------------------
+import qualified Options.Applicative as Opt
+--------------------------------------------------------------------------------
+
+readWithGuard
+  :: Read b
+  => (b -> Bool)
+  -> (b -> String)
+  -> Opt.ReadM b
+readWithGuard predicate errmsg = do
+  res <- Opt.auto
+  unless (predicate res)
+    $ Opt.readerError
+    $ errmsg res
+  pure res
diff --git a/users/grfn/xanthous/src/Xanthous/Util/QuickCheck.hs b/users/grfn/xanthous/src/Xanthous/Util/QuickCheck.hs
new file mode 100644
index 0000000000..aa881b3227
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Util/QuickCheck.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE UndecidableInstances #-}
+module Xanthous.Util.QuickCheck
+  ( functionShow
+  , FunctionShow(..)
+  , functionJSON
+  , FunctionJSON(..)
+  , genericArbitrary
+  , GenericArbitrary(..)
+  ) where
+--------------------------------------------------------------------------------
+import Xanthous.Prelude
+import Test.QuickCheck
+import Test.QuickCheck.Function
+import Test.QuickCheck.Instances.ByteString ()
+import Test.QuickCheck.Arbitrary.Generic
+import Data.Aeson
+--------------------------------------------------------------------------------
+
+newtype FunctionShow a = FunctionShow a
+  deriving newtype (Show, Read)
+
+instance (Show a, Read a) => Function (FunctionShow a) where
+  function = functionShow
+
+functionJSON :: (ToJSON a, FromJSON a) => (a -> c) -> a :-> c
+functionJSON = functionMap encode (headEx . decode)
+
+newtype FunctionJSON a = FunctionJSON a
+  deriving newtype (ToJSON, FromJSON)
+
+instance (ToJSON a, FromJSON a) => Function (FunctionJSON a) where
+  function = functionJSON
diff --git a/users/grfn/xanthous/src/Xanthous/keybindings.yaml b/users/grfn/xanthous/src/Xanthous/keybindings.yaml
new file mode 100644
index 0000000000..cffb27cb03
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/keybindings.yaml
@@ -0,0 +1,22 @@
+q: Quit
+?: Help
+.: Wait
+C-p: PreviousMessage
+',': PickUp
+d: Drop
+o: Open
+c: Close
+;: Look
+e: Eat
+S: Save
+r: Read
+i: ShowInventory
+I: DescribeInventory
+w: Wield
+f: Fire
+'<': GoUp
+'>': GoDown
+R: Rest
+
+# Debug commands
+M-r: ToggleRevealAll
diff --git a/users/grfn/xanthous/src/Xanthous/messages.yaml b/users/grfn/xanthous/src/Xanthous/messages.yaml
new file mode 100644
index 0000000000..bc08ec1ad2
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/messages.yaml
@@ -0,0 +1,161 @@
+welcome: Welcome to Xanthous, {{characterName}}! It's dangerous out there, why not stay inside? Press ? for help.
+dead:
+  - You have died...
+  - You die...
+  - You perish...
+  - You have perished...
+
+generic:
+  continue: Press enter to continue...
+
+save:
+  disabled: "Sorry, saving is currently disabled"
+  location: "Enter filename to save to: "
+  overwrite: "A file named {{filename}} already exists. Would you like to overwrite it? "
+
+quit:
+  confirm: Really quit without saving?
+
+entities:
+  description: You see here {{entityDescriptions}}
+  say:
+    creature:
+      visible: The {{creature.creatureType.name}} {{creature.creatureType.sayVerb}} "{{message}}"
+      invisible: You hear something yell "{{message}}" in the distance
+
+pickUp:
+  menu: What would you like to pick up?
+  pickUp: You pick up the {{item.itemType.name}}.
+  nothingToPickUp: "There's nothing here to pick up"
+
+cant:
+  goUp:
+    - You can't go up here
+    - There's nothing here that would let you go up
+  goDown:
+    - You can't go down here
+    - There's nothing here that would let you go down
+
+open:
+  prompt: Direction to open (hjklybnu.)?
+  success: "You open the door."
+  locked: "That door is locked"
+  nothingToOpen: "There's nothing to open there."
+  alreadyOpen: "That door is already open."
+
+close:
+  prompt: Direction to close (hjklybnu.)?
+  success:
+    - You close the door.
+    - You shut the door.
+  nothingToClose: "There's nothing to close there."
+  alreadyClosed: "That door is already closed."
+  blocked: "The {{entityDescriptions}} {{blockOrBlocks}} the door!"
+
+look:
+  prompt: Select a position on the map to describe (use Enter to confirm)
+  nothing: There's nothing there
+
+character:
+  namePrompt: "What's your name? "
+  body:
+    knuckles:
+      calluses:
+      - You've started developing calluses on your knuckles from all the punching you've been doing.
+      - You've been fighting with your fists so much they're starting to develop calluses.
+
+combat:
+  nothingToAttack: There's nothing to attack there.
+  menu: Which creature would you like to attack?
+  fistSelfDamage:
+    - You hit so hard with your fists you hurt yourself!
+    - The punch leaves your knuckles bloody!
+  fistExtraSelfDamage:
+    - You hurt your already-bloody fists with the strike!
+    - Ouch! Your fists were already bleeding!
+  hit:
+    fists:
+      - You punch the {{creature.creatureType.name}} with your bare fists! It hurts. A lot.
+      - You strike the {{creature.creatureType.name}} with your bare fists! It leaves a bit of a bruise on your knuckles.
+    generic:
+      - You hit the {{creature.creatureType.name}}.
+      - You attack the {{creature.creatureType.name}}.
+  creatureAttack:
+    natural: The {{creature.creatureType.name}} {{attackDescription}}.
+    genericWeapon: The {{creature.creatureType.name}} attacks you with its {{item.itemType.name}}.
+  killed:
+    - You kill the {{creature.creatureType.name}}!
+    - You've killed the {{creature.creatureType.name}}!
+
+debug:
+  toggleRevealAll: revealAll now set to {{revealAll}}
+
+eat:
+  noFood:
+    - You have nothing edible.
+    - You don't have any food.
+    - You don't have anything to eat.
+    - You search your pockets for something edible, and come up short.
+  menuPrompt: What would you like to eat?
+  eat: You eat the {{item.itemType.name}}.
+
+read:
+  prompt: Direction to read (hjklybnu.)?
+  nothing: "There's nothing there to read"
+  result: "\"{{message}}\""
+
+inventory:
+  describe:
+    select: Select an item in your inventory to describe
+    nothing: You aren't carrying anything
+
+wield:
+  nothing:
+    - You aren't carrying anything you can wield
+    - You can't wield anything in your backpack
+    - You can't wield anything currently in your backpack
+  menu: What would you like to wield?
+  hand: Wield in which hand?
+  wielded: You wield the {{item.wieldedItem.itemType.name}} in {{hand}}
+
+fire:
+  nothing:
+    - You don't currently have anything you can throw
+    - You don't have anything to throw
+  zeroRange:
+    - That item is too heavy to throw!
+    - That's too heavy to throw
+    - You're not strong enough to throw that any meaningful distance
+  menu: What would you like to throw?
+  target: Choose a target
+  atRange:
+    - It's too heavy for you to throw any further than this
+  fired:
+    noTarget:
+      - You throw the {{item.itemType.name}} at the ground
+    noDamage:
+      - You throw the {{item.itemType.name}} at the {{creature.creatureType.name}}. It doesn't seem to care.
+      - You throw the {{item.itemType.name}} at the {{creature.creatureType.name}}. It doesn't seem to do anything.
+      - You throw the {{item.itemType.name}} at the {{creature.creatureType.name}}. It doesn't seem to hurt it.
+    someDamage:
+      - You throw the {{item.itemType.name}} at the {{creature.creatureType.name}}. It hits it on the head!.
+
+drop:
+  nothing: You aren't carrying anything
+  menu: What would you like to drop?
+  # TODO: use actual hands
+  dropped:
+    - You drop the {{item.itemType.name}}.
+    - You drop the {{item.itemType.name}} on the ground.
+    - You put the {{item.itemType.name}} on the ground.
+    - You take the {{item.itemType.name}} out of your backpack and put it on the ground.
+    - You take the {{item.itemType.name}} out of your backpack and drop it on the ground.
+
+autocommands:
+  enemyInSight: There's a {{firstEntity.creatureType.name}} nearby!
+  resting: Resting...
+  doneResting: Done resting
+###
+
+tutorial:
+  message1: The caves are dark and full of nightmarish creatures - and you are likely to perish without food. Seek out sustenance! You can pick items up with ,.
diff --git a/users/grfn/xanthous/test/Spec.hs b/users/grfn/xanthous/test/Spec.hs
new file mode 100644
index 0000000000..51758d6a25
--- /dev/null
+++ b/users/grfn/xanthous/test/Spec.hs
@@ -0,0 +1,61 @@
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import qualified Xanthous.CommandSpec
+import qualified Xanthous.Data.EntitiesSpec
+import qualified Xanthous.Data.EntityCharSpec
+import qualified Xanthous.Data.EntityMap.GraphicsSpec
+import qualified Xanthous.Data.EntityMapSpec
+import qualified Xanthous.Data.LevelsSpec
+import qualified Xanthous.Data.MemoSpec
+import qualified Xanthous.Data.NestedMapSpec
+import qualified Xanthous.DataSpec
+import qualified Xanthous.Entities.CommonSpec
+import qualified Xanthous.Entities.RawsSpec
+import qualified Xanthous.Entities.RawTypesSpec
+import qualified Xanthous.Entities.CharacterSpec
+import qualified Xanthous.GameSpec
+import qualified Xanthous.Game.StateSpec
+import qualified Xanthous.Game.PromptSpec
+import qualified Xanthous.Generators.Level.UtilSpec
+import qualified Xanthous.MessageSpec
+import qualified Xanthous.Messages.TemplateSpec
+import qualified Xanthous.OrphansSpec
+import qualified Xanthous.RandomSpec
+import qualified Xanthous.Util.GraphSpec
+import qualified Xanthous.Util.GraphicsSpec
+import qualified Xanthous.Util.InflectionSpec
+import qualified Xanthous.UtilSpec
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMainWithRerun test
+
+test :: TestTree
+test = testGroup "Xanthous"
+  [ Xanthous.CommandSpec.test
+  , Xanthous.Data.EntitiesSpec.test
+  , Xanthous.Data.EntityMap.GraphicsSpec.test
+  , Xanthous.Data.EntityMapSpec.test
+  , Xanthous.Data.LevelsSpec.test
+  , Xanthous.Data.MemoSpec.test
+  , Xanthous.Data.NestedMapSpec.test
+  , Xanthous.DataSpec.test
+  , Xanthous.Entities.CommonSpec.test
+  , Xanthous.Entities.RawsSpec.test
+  , Xanthous.Entities.CharacterSpec.test
+  , Xanthous.Entities.RawTypesSpec.test
+  , Xanthous.GameSpec.test
+  , Xanthous.Game.StateSpec.test
+  , Xanthous.Game.PromptSpec.test
+  , Xanthous.Generators.Level.UtilSpec.test
+  , Xanthous.MessageSpec.test
+  , Xanthous.Messages.TemplateSpec.test
+  , Xanthous.OrphansSpec.test
+  , Xanthous.RandomSpec.test
+  , Xanthous.Util.GraphSpec.test
+  , Xanthous.Util.GraphicsSpec.test
+  , Xanthous.Util.InflectionSpec.test
+  , Xanthous.UtilSpec.test
+  , Xanthous.Data.EntityCharSpec.test
+  ]
diff --git a/users/grfn/xanthous/test/Test/Prelude.hs b/users/grfn/xanthous/test/Test/Prelude.hs
new file mode 100644
index 0000000000..75c1ebf5e7
--- /dev/null
+++ b/users/grfn/xanthous/test/Test/Prelude.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+--------------------------------------------------------------------------------
+module Test.Prelude
+  ( module Xanthous.Prelude
+  , module Test.Tasty
+  , module Test.Tasty.HUnit
+  , module Test.Tasty.QuickCheck
+  , module Test.Tasty.Ingredients.Rerun
+  , module Test.QuickCheck.Classes
+  , testBatch
+  , jsonRoundTrip
+  ) where
+--------------------------------------------------------------------------------
+import           Xanthous.Prelude hiding (assert, elements)
+--------------------------------------------------------------------------------
+import           Test.Tasty
+import           Test.Tasty.QuickCheck
+import           Test.Tasty.HUnit
+import           Test.Tasty.Ingredients.Rerun
+import           Test.QuickCheck.Classes
+import           Test.QuickCheck.Checkers (TestBatch, EqProp ((=-=)))
+import           Test.QuickCheck.Instances.ByteString ()
+--------------------------------------------------------------------------------
+import qualified Data.Aeson as JSON
+import           Data.Aeson (ToJSON, FromJSON)
+--------------------------------------------------------------------------------
+
+testBatch :: TestBatch -> TestTree
+testBatch (name, tests) = testGroup name $ uncurry testProperty <$> tests
+
+jsonRoundTrip
+  :: forall a. (ToJSON a, FromJSON a, EqProp a, Arbitrary a, Show a) => TestTree
+jsonRoundTrip = testProperty "JSON round trip" $ \(x :: a) ->
+  JSON.decode (JSON.encode x) =-= Just x
diff --git a/users/grfn/xanthous/test/Xanthous/CommandSpec.hs b/users/grfn/xanthous/test/Xanthous/CommandSpec.hs
new file mode 100644
index 0000000000..13f69a808d
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/CommandSpec.hs
@@ -0,0 +1,40 @@
+--------------------------------------------------------------------------------
+module Xanthous.CommandSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Xanthous.Command
+--------------------------------------------------------------------------------
+import           Data.Aeson (fromJSON, Value(String))
+import qualified Data.Aeson as A
+import           Graphics.Vty.Input (Key(..), Modifier(..))
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.CommandSpec"
+  [ testGroup "keybindings"
+    [ testCase "all are valid" $ keybindings `deepseq` pure ()
+    , testProperty "all non-move commands are bound" $ \cmd ->
+        let isn'tMove = case cmd of
+                          Move _ -> False
+                          StartAutoMove _ -> False
+                          _ -> True
+        in isn'tMove ==> member cmd commands
+    ]
+  , testGroup "instance FromJSON Keybinding" $
+    [ ("q", Keybinding (KChar 'q') [])
+    , ("<up>", Keybinding KUp [])
+    , ("<left>", Keybinding KLeft [])
+    , ("<right>", Keybinding KRight [])
+    , ("<down>", Keybinding KDown [])
+    , ("S-q", Keybinding (KChar 'q') [MShift])
+    , ("C-S-q", Keybinding (KChar 'q') [MCtrl, MShift])
+    , ("m-<UP>", Keybinding KUp [MMeta])
+    , ("S", Keybinding (KChar 'S') [])
+    ] <&> \(s, kb) ->
+      testCase (fromString $ unpack s <> " -> " <> show kb)
+       $ fromJSON (String s) @?= A.Success kb
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Data/EntitiesSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/EntitiesSpec.hs
new file mode 100644
index 0000000000..e403503743
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/EntitiesSpec.hs
@@ -0,0 +1,28 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntitiesSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import qualified Data.Aeson as JSON
+--------------------------------------------------------------------------------
+import           Xanthous.Data.Entities
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data.Entities"
+  [ testGroup "Collision"
+    [ testProperty "JSON round-trip" $ \(c :: Collision) ->
+        JSON.decode (JSON.encode c) === Just c
+    , testGroup "JSON encoding examples"
+      [ testCase "Stop" $ JSON.encode Stop @?= "\"Stop\""
+      , testCase "Combat" $ JSON.encode Combat @?= "\"Combat\""
+      ]
+    ]
+  , testGroup "EntityAttributes"
+    [ testProperty "JSON round-trip" $ \(ea :: EntityAttributes) ->
+        JSON.decode (JSON.encode ea) === Just ea
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Data/EntityCharSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/EntityCharSpec.hs
new file mode 100644
index 0000000000..9e8024c9d2
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/EntityCharSpec.hs
@@ -0,0 +1,18 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntityCharSpec where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import qualified Data.Aeson as JSON
+--------------------------------------------------------------------------------
+import           Xanthous.Data.EntityChar
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data.EntityChar"
+  [ testProperty "JSON round-trip" $ \(ec :: EntityChar) ->
+      JSON.decode (JSON.encode ec) === Just ec
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Data/EntityMap/GraphicsSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/EntityMap/GraphicsSpec.hs
new file mode 100644
index 0000000000..fd37548ce8
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/EntityMap/GraphicsSpec.hs
@@ -0,0 +1,57 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntityMap.GraphicsSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+import Data.Aeson
+--------------------------------------------------------------------------------
+import Xanthous.Game.State
+import Xanthous.Data
+import Xanthous.Data.EntityMap
+import Xanthous.Data.EntityMap.Graphics
+import Xanthous.Entities.Environment (Wall(..))
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data.EntityMap.Graphics"
+  [ testGroup "visiblePositions"
+    [ testProperty "one step in each cardinal direction is always visible"
+      $ \pos (Cardinal dir) (Positive r) (wallPositions :: Set Position)->
+          pos `notMember` wallPositions ==>
+          let em = review _EntityMap . map (, Wall) . toList $ wallPositions
+              em' = em & atPosition (move dir pos) %~ (Wall <|)
+              poss = visiblePositions pos r em'
+          in counterexample ("visiblePositions: " <> show poss)
+             $ move dir pos `member` poss
+    , testGroup "bugs"
+      [ testCase "non-contiguous bug 1"
+        $ let charPos = Position 20 20
+              gormlakPos = Position 17 19
+              em = insertAt gormlakPos TestEntity
+                   . insertAt charPos TestEntity
+                   $ mempty
+              visPositions = visiblePositions charPos 12 em
+          in (gormlakPos `member` visPositions) @?
+             ( "not ("
+             <> show gormlakPos <> " `member` "
+             <> show visPositions
+             <> ")"
+             )
+      ]
+    ]
+  ]
+
+--------------------------------------------------------------------------------
+
+data TestEntity = TestEntity
+  deriving stock (Show, Eq, Ord, Generic)
+  deriving anyclass (ToJSON, FromJSON, NFData)
+
+instance Brain TestEntity where
+  step _ = pure
+instance Draw TestEntity
+instance Entity TestEntity where
+  description _ = ""
+  entityChar _ = "e"
diff --git a/users/grfn/xanthous/test/Xanthous/Data/EntityMapSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/EntityMapSpec.hs
new file mode 100644
index 0000000000..7c5cad0196
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/EntityMapSpec.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE ApplicativeDo #-}
+--------------------------------------------------------------------------------
+module Xanthous.Data.EntityMapSpec where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import qualified Data.Aeson as JSON
+--------------------------------------------------------------------------------
+import           Xanthous.Data.EntityMap
+import           Xanthous.Data (Positioned(..))
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = localOption (QuickCheckTests 20)
+  $ testGroup "Xanthous.Data.EntityMap"
+  [ testBatch $ monoid @(EntityMap Int) mempty
+  , testGroup "Deduplicate"
+    [ testGroup "Semigroup laws"
+      [ testProperty "associative" $ \(a :: Deduplicate (EntityMap Int)) b c ->
+          a <> (b <> c) === (a <> b) <> c
+      ]
+    ]
+  , testGroup "Eq laws"
+    [ testProperty "reflexivity" $ \(em :: EntityMap Int) ->
+        em == em
+    , testProperty "symmetric" $ \(em₁ :: EntityMap Int) em₂ ->
+        (em₁ == em₂) == (em₂ == em₁)
+    , testProperty "transitive" $ \(em₁ :: EntityMap Int) em₂ em₃ ->
+        if (em₁ == em₂ && em₂ == em₃)
+        then (em₁ == em₃)
+        else True
+    ]
+  , testGroup "JSON encoding/decoding"
+    [ testProperty "round-trips" $ \(em :: EntityMap Int) ->
+        let em' = JSON.decode (JSON.encode em)
+        in counterexample (show (em' ^? _Just . lastID, em ^. lastID
+                                , em' ^? _Just . byID == em ^. byID . re _Just
+                                , em' ^? _Just . byPosition == em ^. byPosition . re _Just
+                                , em' ^? _Just . _EntityMap == em ^. _EntityMap . re _Just
+                                ))
+           $ em' === Just em
+    , testProperty "Preserves IDs" $ \(em :: EntityMap Int) ->
+        let Just em' = JSON.decode $ JSON.encode em
+        in toEIDsAndPositioned em' === toEIDsAndPositioned em
+    ]
+
+  , localOption (QuickCheckTests 50)
+  $ testGroup "atPosition"
+    [ testProperty "setget" $ \pos (em :: EntityMap Int) es ->
+        view (atPosition pos) (set (atPosition pos) es em) === es
+    , testProperty "getset" $ \pos (em :: EntityMap Int) ->
+        set (atPosition pos) (view (atPosition pos) em) em === em
+    , testProperty "setset" $ \pos (em :: EntityMap Int) es ->
+        (set (atPosition pos) es . set (atPosition pos) es) em
+        ===
+        set (atPosition pos) es em
+      -- testProperty "lens laws" $ \pos -> isLens $ atPosition @Int pos
+    , testProperty "preserves IDs" $ \(em :: EntityMap Int) e1 e2 p ->
+        let (eid, em') = insertAtReturningID p e1 em
+            em'' = em' & atPosition p %~ (e2 <|)
+        in
+          counterexample ("em': " <> show em')
+          . counterexample ("em'': " <> show em'')
+          $ em'' ^. at eid === Just (Positioned p e1)
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Data/LevelsSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/LevelsSpec.hs
new file mode 100644
index 0000000000..a752833162
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/LevelsSpec.hs
@@ -0,0 +1,66 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.LevelsSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+--------------------------------------------------------------------------------
+import qualified Data.Aeson as JSON
+--------------------------------------------------------------------------------
+import Xanthous.Util (between)
+import Xanthous.Data.Levels
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data.Levels"
+  [ testGroup "current"
+    [ testProperty "view is extract" $ \(levels :: Levels Int) ->
+        levels ^. current === extract levels
+    , testProperty "set replaces current" $ \(levels :: Levels Int) new ->
+        extract (set current new levels) === new
+    , testProperty "set extract is id" $ \(levels :: Levels Int) ->
+        set current (extract levels) levels === levels
+    , testProperty "set y ∘ set x ≡ set y" $ \(levels :: Levels Int) x y ->
+        set current y (set current x levels) === set current y levels
+    ]
+  , localOption (QuickCheckTests 20)
+  $ testBatch $ semigroup @(Levels Int) (error "unused", 1 :: Int)
+  , testGroup "next/prev"
+    [ testGroup "nextLevel"
+      [ testProperty "seeks forwards" $ \(levels :: Levels Int) genned ->
+          (pos . runIdentity . nextLevel (Identity genned) $ levels)
+          === pos levels + 1
+      , testProperty "maintains the invariant" $ \(levels :: Levels Int) genned ->
+          let levels' = runIdentity . nextLevel (Identity genned) $ levels
+          in between 0 (toEnum $ length levels') $ pos levels'
+      , testProperty "extract is total" $ \(levels :: Levels Int) genned ->
+          let levels' = runIdentity . nextLevel (Identity genned) $ levels
+          in total $ extract levels'
+      , testProperty "uses the generated level as the next level"
+        $ \(levels :: Levels Int) genned ->
+          let levels' = seek (toEnum $ length levels - 1) levels
+              levels'' = runIdentity . nextLevel (Identity genned) $ levels'
+          in counterexample (show levels'')
+             $ extract levels'' === genned
+      ]
+    , testGroup "prevLevel"
+      [ testProperty "seeks backwards" $ \(levels :: Levels Int) ->
+          case prevLevel levels of
+            Nothing -> property Discard
+            Just levels' -> pos levels' === pos levels - 1
+      , testProperty "maintains the invariant" $ \(levels :: Levels Int) ->
+          case prevLevel levels of
+            Nothing -> property Discard
+            Just levels' -> property $ between 0 (toEnum $ length levels') $ pos levels'
+      , testProperty "extract is total" $ \(levels :: Levels Int) ->
+          case prevLevel levels of
+            Nothing -> property Discard
+            Just levels' -> total $ extract levels'
+      ]
+    ]
+  , testGroup "JSON"
+    [ testProperty "toJSON/parseJSON round-trip" $ \(levels :: Levels Int) ->
+        JSON.decode (JSON.encode levels) === Just levels
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Data/MemoSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/MemoSpec.hs
new file mode 100644
index 0000000000..ad81f1984d
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/MemoSpec.hs
@@ -0,0 +1,19 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.MemoSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+import Test.QuickCheck.Instances.Text ()
+--------------------------------------------------------------------------------
+import Xanthous.Data.Memo
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data.MemoSpec"
+  [ testGroup "getMemoized"
+    [ testProperty "when key matches" $ \k v ->
+        getMemoized @Int @Int k (memoizeWith k v) === Just v
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Data/NestedMapSpec.hs b/users/grfn/xanthous/test/Xanthous/Data/NestedMapSpec.hs
new file mode 100644
index 0000000000..acf7a67268
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Data/NestedMapSpec.hs
@@ -0,0 +1,20 @@
+--------------------------------------------------------------------------------
+module Xanthous.Data.NestedMapSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Test.QuickCheck.Instances.Semigroup ()
+--------------------------------------------------------------------------------
+import qualified Xanthous.Data.NestedMap as NM
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data.NestedMap"
+  [ testProperty "insert/lookup" $ \nm ks v ->
+      let nm' = NM.insert ks v nm
+      in counterexample ("inserted: " <> show nm')
+         $ NM.lookup @Map @Int @Int ks nm' === Just (NM.Val v)
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/DataSpec.hs b/users/grfn/xanthous/test/Xanthous/DataSpec.hs
new file mode 100644
index 0000000000..9e67505ba9
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/DataSpec.hs
@@ -0,0 +1,109 @@
+--------------------------------------------------------------------------------
+module Xanthous.DataSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude hiding (Right, Left, Down, toList, all)
+import Data.Group
+import Data.Foldable (toList, all)
+--------------------------------------------------------------------------------
+import Xanthous.Data
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Data"
+  [ testGroup "Position"
+    [ testBatch $ monoid @Position mempty
+    , testProperty "group laws" $ \(pos :: Position) ->
+        pos <> invert pos == mempty && invert pos <> pos == mempty
+    , testGroup "stepTowards laws"
+      [ testProperty "takes only one step" $ \src tgt ->
+          src /= tgt ==>
+            isUnit (src `diffPositions` (src `stepTowards` tgt))
+      -- , testProperty "moves in the right direction" $ \src tgt ->
+      --     stepTowards src tgt == move (directionOf src tgt) src
+      ]
+    , testProperty "directionOf laws" $ \pos dir ->
+        directionOf pos (move dir pos) == dir
+    , testProperty "diffPositions is add inverse" $ \(pos₁ :: Position) pos₂ ->
+        diffPositions pos₁ pos₂ == addPositions pos₁ (invert pos₂)
+    , testGroup "isUnit"
+      [ testProperty "double direction is never unit" $ \dir ->
+          not . isUnit $ move dir (asPosition dir)
+      , testCase "examples" $ do
+          isUnit (Position @Int 1 1) @? "not . isUnit $ Position 1 1"
+          isUnit (Position @Int 0 (-1)) @? "not . isUnit $ Position 0 (-1)"
+          (not . isUnit) (Position @Int 1 13) @? "isUnit $ Position 1 13"
+      ]
+    ]
+
+  , testGroup "Direction"
+    [ testProperty "opposite is involutive" $ \(dir :: Direction) ->
+        opposite (opposite dir) == dir
+    , testProperty "opposite provides inverse" $ \dir ->
+        invert (asPosition dir) === asPosition (opposite dir)
+    , testProperty "asPosition isUnit" $ \dir ->
+        dir /= Here ==> isUnit (asPosition dir)
+    , testGroup "Move"
+      [ testCase "Up"        $ move Up mempty        @?= Position @Int 0 (-1)
+      , testCase "Down"      $ move Down mempty      @?= Position @Int 0 1
+      , testCase "Left"      $ move Left mempty      @?= Position @Int (-1) 0
+      , testCase "Right"     $ move Right mempty     @?= Position @Int 1 0
+      , testCase "UpLeft"    $ move UpLeft mempty    @?= Position @Int (-1) (-1)
+      , testCase "UpRight"   $ move UpRight mempty   @?= Position @Int 1 (-1)
+      , testCase "DownLeft"  $ move DownLeft mempty  @?= Position @Int (-1) 1
+      , testCase "DownRight" $ move DownRight mempty @?= Position @Int 1 1
+      ]
+    ]
+
+  , testGroup "Corner"
+    [ testGroup "instance Opposite"
+      [ testProperty "involutive" $ \(corner :: Corner) ->
+          opposite (opposite corner) === corner
+      ]
+    ]
+
+  , testGroup "Edge"
+    [ testGroup "instance Opposite"
+      [ testProperty "involutive" $ \(edge :: Edge) ->
+          opposite (opposite edge) === edge
+      ]
+    ]
+
+  , testGroup "Box"
+    [ testGroup "boxIntersects"
+      [ testProperty "True" $ \dims ->
+          boxIntersects (Box @Word (V2 1 1) (V2 2 2))
+                        (Box (V2 2 2) dims)
+      , testProperty "False" $ \dims ->
+          not $ boxIntersects (Box @Word (V2 1 1) (V2 2 2))
+                            (Box (V2 4 2) dims)
+      ]
+    ]
+
+  , testGroup "Neighbors"
+    [ testGroup "rotations"
+      [ testProperty "always has the same members"
+        $ \(neighs :: Neighbors Int) ->
+          all (\ns -> sort (toList ns) == sort (toList neighs))
+          $ rotations neighs
+      , testProperty "all rotations have the same rotations"
+        $ \(neighs :: Neighbors Int) ->
+          let rots = rotations neighs
+          in all (\ns -> sort (toList $ rotations ns) == sort (toList rots))
+             rots
+      ]
+    ]
+
+  , testGroup "units"
+    [ testGroup "unit suffixes"
+      [ testCase "density"
+        $ tshow (10000 :: Grams `Per` Cubic Meters) @?= "10000.0 g/m³"
+      , testCase "volume"
+        $ tshow (5 :: Cubic Meters) @?= "5.0 m³"
+      , testCase "area"
+        $ tshow (5 :: Square Meters) @?= "5.0 m²"
+      ]
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Entities/CharacterSpec.hs b/users/grfn/xanthous/test/Xanthous/Entities/CharacterSpec.hs
new file mode 100644
index 0000000000..734cce1efb
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Entities/CharacterSpec.hs
@@ -0,0 +1,24 @@
+{-# OPTIONS_GHC -Wno-type-defaults #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.CharacterSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.Character
+import           Xanthous.Util (endoTimes)
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Entities.CharacterSpec"
+  [ testGroup "Knuckles"
+    [ testBatch $ monoid @Knuckles mempty
+    , testGroup "damageKnuckles"
+      [ testCase "caps at 5" $
+          let knuckles' = endoTimes 6 damageKnuckles mempty
+          in _knuckleDamage knuckles' @?= 5
+      ]
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Entities/CommonSpec.hs b/users/grfn/xanthous/test/Xanthous/Entities/CommonSpec.hs
new file mode 100644
index 0000000000..a6f8401cf7
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Entities/CommonSpec.hs
@@ -0,0 +1,65 @@
+--------------------------------------------------------------------------------
+module Xanthous.Entities.CommonSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+import           Data.Vector.Lens (toVectorOf)
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.Common
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+newtype OneHand = OneHand Hand
+  deriving stock Show
+
+instance Arbitrary OneHand where
+  arbitrary = OneHand <$> elements [LeftHand, RightHand]
+
+otherHand :: Hand -> Hand
+otherHand LeftHand = RightHand
+otherHand RightHand = LeftHand
+otherHand BothHands = error "OtherHand BothHands"
+
+test :: TestTree
+test = testGroup "Xanthous.Entities.CommonSpec"
+  [ testGroup "Inventory"
+    [ testProperty "items === itemsWithPosition . _2" $ \inv ->
+        inv ^.. items === inv ^.. itemsWithPosition . _2
+    , testGroup "removeItemFromPosition" $
+      let rewield w inv =
+            let (old, inv') = inv & wielded <<.~ w
+            in inv' & backpack <>~ toVectorOf (wieldedItems . wieldedItem) old
+      in [ (Backpack, \item -> backpack %~ (item ^. wieldedItem <|))
+         , (InHand LeftHand, rewield . inLeftHand)
+         , (InHand RightHand, rewield . inRightHand)
+         , (InHand BothHands, rewield . review doubleHanded)
+         ] <&> \(pos, addItem) ->
+           testProperty (show pos) $ \inv item ->
+             let inv' = addItem item inv
+                 inv'' = removeItemFromPosition pos (item ^. wieldedItem) inv'
+             in inv'' ^.. items === inv ^.. items
+    ]
+  , testGroup "Wielded items"
+    [ testGroup "wieldInHand"
+      [ testProperty "puts the item in the hand" $ \w hand item ->
+          let (_, w') = wieldInHand hand item w
+          in itemsInHand hand w' === [item]
+      , testProperty "returns items in both hands when wielding double-handed"
+        $ \lh rh newItem ->
+          let w = Hands (Just lh) (Just rh)
+              (prevItems, _) = wieldInHand BothHands newItem w
+          in prevItems === [lh, rh]
+      , testProperty "wielding in one hand leaves the item in the other hand"
+        $ \(OneHand h) existingItem newItem ->
+          let (_, w) = wieldInHand h existingItem nothingWielded
+              (prevItems, w') = wieldInHand (otherHand h) newItem w
+          in   prevItems === []
+          .&&. sort (w' ^.. wieldedItems) === sort [existingItem, newItem]
+      , testProperty "always leaves the same items overall" $ \w hand item ->
+          let (prevItems, w') = wieldInHand hand item w
+          in  sort (prevItems <> (w' ^.. wieldedItems))
+          === sort (item : w ^.. wieldedItems)
+      ]
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Entities/RawTypesSpec.hs b/users/grfn/xanthous/test/Xanthous/Entities/RawTypesSpec.hs
new file mode 100644
index 0000000000..e23f7faba3
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Entities/RawTypesSpec.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Xanthous.Entities.RawTypesSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Data.Interval (Extended(..), (<=..<=))
+--------------------------------------------------------------------------------
+import           Xanthous.Entities.RawTypes
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Entities.RawTypesSpec"
+  [ testGroup "CreatureGenerateParams"
+    [ testGroup "Ord laws"
+      [ testProperty "comparability" $ \(a :: CreatureGenerateParams) b ->
+          a <= b || b <= a
+      , testProperty "transitivity" $ \(a :: CreatureGenerateParams) b c ->
+          a <= b && b <= c ==> a <= c
+      , testProperty "reflexivity" $ \(a :: CreatureGenerateParams) ->
+          a <= a
+      , testProperty "antisymmetry" $ \(a :: CreatureGenerateParams) b ->
+          (a <= b && b <= a) == (a == b)
+      ]
+    , testGroup "canGenerate" $
+      let makeParams minB maxB =
+            let _levelRange = maybe NegInf Finite minB <=..<= maybe PosInf Finite maxB
+                _equippedItem = Nothing
+            in CreatureGenerateParams {..}
+      in
+        [ testProperty "no bounds" $ \level ->
+            let gps = makeParams Nothing Nothing
+            in canGenerate level gps
+        , testProperty "min bound" $ \level minB ->
+            let gps = makeParams (Just minB) Nothing
+            in canGenerate level gps === (level >= minB)
+        , testProperty "max bound" $ \level maxB ->
+            let gps = makeParams Nothing (Just maxB)
+            in canGenerate level gps === (level <= maxB)
+        ]
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Entities/RawsSpec.hs b/users/grfn/xanthous/test/Xanthous/Entities/RawsSpec.hs
new file mode 100644
index 0000000000..b6c80be51b
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Entities/RawsSpec.hs
@@ -0,0 +1,30 @@
+-- |
+
+module Xanthous.Entities.RawsSpec (main, test) where
+
+import Test.Prelude
+import Xanthous.Entities.Raws
+import Xanthous.Entities.RawTypes
+       (_Creature, entityName, generateParams, HasEquippedItem (equippedItem))
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Entities.Raws"
+  [ testGroup "raws"
+    [ testCase "are all valid" $ raws `deepseq` pure ()
+    , testCase "all CreatureEquippedItems reference existent entity names" $
+      let notFound
+            = raws
+              ^.. folded
+              . _Creature
+              . generateParams
+              . _Just
+              . equippedItem
+              . _Just
+              . entityName
+              . filtered (isNothing . raw)
+      in null notFound @? ("Some entities weren't found: " <> show notFound)
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Game/PromptSpec.hs b/users/grfn/xanthous/test/Xanthous/Game/PromptSpec.hs
new file mode 100644
index 0000000000..d7a3df4aca
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Game/PromptSpec.hs
@@ -0,0 +1,19 @@
+--------------------------------------------------------------------------------
+module Xanthous.Game.PromptSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Xanthous.Game.Prompt
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Game.PromptSpec"
+  [ testGroup "mkMenuItems"
+    [ testCase "with duplicate items"
+      $ mkMenuItems @[_] [('a', MenuOption @Int "a" 1), ('a', MenuOption "a" 2)]
+        @?= mapFromList [('a', MenuOption "a" 1), ('b', MenuOption "a" 2)]
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Game/StateSpec.hs b/users/grfn/xanthous/test/Xanthous/Game/StateSpec.hs
new file mode 100644
index 0000000000..34584f73b2
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Game/StateSpec.hs
@@ -0,0 +1,30 @@
+--------------------------------------------------------------------------------
+module Xanthous.Game.StateSpec (main, test) where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Xanthous.Game.State
+import           Xanthous.Entities.Raws (raws)
+import           Xanthous.Generators.Level.LevelContents (entityFromRaw)
+import           Control.Monad.Random (evalRandT)
+import           System.Random (getStdGen)
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Game.StateSpec"
+  [ testGroup "entityTypeName"
+    [ testCase "for a creature" $ do
+        let gormlakRaw = raws ^?! ix "gormlak"
+        creature <- runRand $ entityFromRaw gormlakRaw
+        entityTypeName creature @?= "Creature"
+    , testCase "for an item" $ do
+        let stickRaw = raws ^?! ix "stick"
+        item <- runRand $ entityFromRaw stickRaw
+        entityTypeName item @?= "Item"
+    ]
+  ]
+  where
+    runRand x = evalRandT x =<< getStdGen
diff --git a/users/grfn/xanthous/test/Xanthous/GameSpec.hs b/users/grfn/xanthous/test/Xanthous/GameSpec.hs
new file mode 100644
index 0000000000..2fa8527d0e
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/GameSpec.hs
@@ -0,0 +1,55 @@
+module Xanthous.GameSpec where
+
+import Test.Prelude hiding (Down)
+import Xanthous.Game
+import Xanthous.Game.State
+import Control.Lens.Properties
+import Xanthous.Data (move, Direction(Down))
+import Xanthous.Data.EntityMap (atPosition)
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test
+  = localOption (QuickCheckTests 10)
+  . localOption (QuickCheckMaxSize 10)
+  $ testGroup "Xanthous.Game"
+  [ testGroup "positionedCharacter"
+    [ testProperty "lens laws" $ isLens positionedCharacter
+    , testCase "updates the position of the character" $ do
+      initialGame <- getInitialState
+      let initialPos = initialGame ^. characterPosition
+          updatedGame = initialGame & characterPosition %~ move Down
+          updatedPos = updatedGame ^. characterPosition
+      updatedPos @?= move Down initialPos
+      updatedGame ^. entities . atPosition initialPos @?= fromList []
+      updatedGame ^. entities . atPosition updatedPos
+        @?= fromList [SomeEntity $ initialGame ^. character]
+    ]
+  , testGroup "characterPosition"
+    [ testProperty "lens laws" $ isLens characterPosition
+    ]
+  , testGroup "character"
+    [ testProperty "lens laws" $ isLens character
+    ]
+  , testGroup "MessageHistory"
+    [ testGroup "MonoComonad laws"
+      [ testProperty "oextend oextract ≡ id"
+        $ \(mh :: MessageHistory) -> oextend oextract mh === mh
+      , testProperty "oextract ∘ oextend f ≡ f"
+        $ \(mh :: MessageHistory) f -> (oextract . oextend f) mh === f mh
+      , testProperty "oextend f ∘ oextend g ≡ oextend (f . oextend g)"
+        $ \(mh :: MessageHistory) f g ->
+          (oextend f . oextend g) mh === oextend (f . oextend g) mh
+      ]
+    ]
+  , testGroup "Saving the game"
+    [ testProperty "forms a prism" $ isPrism saved
+    , testProperty "round-trips" $ \gs ->
+        loadGame (saveGame gs) === Just gs
+    , testProperty "preserves the character ID" $ \gs ->
+        let Just gs' = loadGame $ saveGame gs
+        in gs' ^. character === gs ^. character
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Generators/Level/UtilSpec.hs b/users/grfn/xanthous/test/Xanthous/Generators/Level/UtilSpec.hs
new file mode 100644
index 0000000000..b53c657f75
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Generators/Level/UtilSpec.hs
@@ -0,0 +1,127 @@
+{-# LANGUAGE PackageImports #-}
+--------------------------------------------------------------------------------
+module Xanthous.Generators.Level.UtilSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+import System.Random (mkStdGen)
+import Control.Monad.Random (runRandT)
+import Data.Array.ST (STUArray, runSTUArray, thaw)
+import Data.Array.IArray (bounds, array)
+import Data.Array.MArray (newArray, readArray, writeArray)
+import Data.Array (Array, range, listArray, Ix)
+import Control.Monad.ST (ST, runST)
+import "checkers" Test.QuickCheck.Instances.Array ()
+import Linear.V2
+--------------------------------------------------------------------------------
+import Xanthous.Util
+import Xanthous.Data (width, height)
+--------------------------------------------------------------------------------
+import Xanthous.Generators.Level.Util
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+--------------------------------------------------------------------------------
+
+newtype GenArray a b = GenArray (Array a b)
+  deriving stock (Show, Eq)
+
+instance (Ix a, Arbitrary a, CoArbitrary a, Arbitrary b)
+       => Arbitrary (GenArray a b) where
+  arbitrary = GenArray <$> do
+    (mkElem :: a -> b) <- arbitrary
+    minDims <- arbitrary
+    maxDims <- arbitrary
+    let bnds = (minDims, maxDims)
+    pure $ listArray bnds $ mkElem <$> range bnds
+
+test :: TestTree
+test = testGroup "Xanthous.Generators.Util"
+  [ testGroup "randInitialize"
+    [ testProperty "returns an array of the correct dimensions"
+      $ \dims seed aliveChance ->
+        let gen = mkStdGen seed
+            res = runSTUArray
+                $ fmap fst
+                $ flip runRandT gen
+                $ randInitialize dims aliveChance
+        in bounds res === (0, V2 (dims ^. width) (dims ^. height))
+    ]
+  , testGroup "numAliveNeighborsM"
+    [ testProperty "maxes out at 8"
+      $ \(GenArray (arr :: Array (V2 Word) Bool)) loc ->
+        let
+          act :: forall s. ST s Word
+          act = do
+            mArr <- thaw @_ @_ @_ @(STUArray s) arr
+            numAliveNeighborsM mArr loc
+          res = runST act
+        in counterexample (show res) $ between 0 8 res
+    , testCase "on the outer x edge" $
+      let act :: forall s. ST s Word
+          act = do
+            cells <- thaw @_ @_ @_ @(STUArray s) $ array @Array @Bool @(V2 Word)
+              (V2 0 0, V2 2 2)
+              [ (V2 0 0, True),  (V2 1 0, True),  (V2 2 0, True)
+              , (V2 0 1, False), (V2 1 1, False), (V2 2 1, True)
+              , (V2 0 2, True),  (V2 1 2, True),  (V2 2 2, True)
+              ]
+            numAliveNeighborsM cells (V2 0 1)
+          res = runST act
+      in res @?= 7
+    , testCase "on the outer y edge" $
+      let act :: forall s. ST s Word
+          act = do
+            cells <- thaw @_ @_ @_ @(STUArray s) $ array @Array @Bool @(V2 Word)
+              (V2 0 0, V2 2 2)
+              [ (V2 0 0, True),  (V2 1 0, True),  (V2 2 0, True)
+              , (V2 0 1, False), (V2 1 1, False), (V2 2 1, True)
+              , (V2 0 2, True),  (V2 1 2, True),  (V2 2 2, True)
+              ]
+            numAliveNeighborsM cells (V2 1 0)
+          res = runST act
+      in res @?= 6
+    ]
+  , testGroup "numAliveNeighbors"
+    [ testProperty "is equivalient to runST . numAliveNeighborsM . thaw" $
+      \(GenArray (arr :: Array (V2 Word) Bool)) loc ->
+        let
+          act :: forall s. ST s Word
+          act = do
+            mArr <- thaw @_ @_ @_ @(STUArray s) arr
+            numAliveNeighborsM mArr loc
+          res = runST act
+        in numAliveNeighbors arr loc === res
+    , testCase "on the outer x edge" $
+      let cells =
+            array @Array @Bool @(V2 Word)
+            (V2 0 0, V2 2 2)
+            [ (V2 0 0, True),  (V2 1 0, True),  (V2 2 0, True)
+            , (V2 0 1, False), (V2 1 1, False), (V2 2 1, True)
+            , (V2 0 2, True),  (V2 1 2, True),  (V2 2 2, True)
+            ]
+      in numAliveNeighbors cells (V2 0 1) @?= 7
+    , testCase "on the outer y edge" $
+      let cells =
+            array @Array @Bool @(V2 Word)
+            (V2 0 0, V2 2 2)
+            [ (V2 0 0, True),  (V2 1 0, True),  (V2 2 0, True)
+            , (V2 0 1, False), (V2 1 1, False), (V2 2 1, True)
+            , (V2 0 2, True),  (V2 1 2, True),  (V2 2 2, True)
+            ]
+      in numAliveNeighbors cells (V2 1 0) @?= 6
+    ]
+  , testGroup "cloneMArray"
+      [ testCase "clones the array" $ runST $
+          let
+            go :: forall s. ST s Assertion
+            go = do
+              arr <- newArray @(STUArray s) (0 :: Int, 5) (1 :: Int)
+              arr' <- cloneMArray @_ @(STUArray s) arr
+              writeArray arr' 0 1234
+              x <- readArray arr 0
+              pure $ x @?= 1
+          in go
+      ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/MessageSpec.hs b/users/grfn/xanthous/test/Xanthous/MessageSpec.hs
new file mode 100644
index 0000000000..2068e338ba
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/MessageSpec.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE OverloadedLists #-}
+module Xanthous.MessageSpec ( main, test ) where
+
+import Test.Prelude
+import Xanthous.Messages
+import Data.Aeson
+import Text.Mustache
+import Control.Lens.Properties
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Messages"
+  [ testGroup "Message"
+    [ testGroup "JSON decoding"
+      [ testCase "Single"
+        $ decode "\"Test Single Template\""
+        @?= Just (Single
+                  $ compileMustacheText "template" "Test Single Template"
+                  ^?! _Right)
+      , testCase "Choice"
+        $ decode "[\"Choice 1\", \"Choice 2\"]"
+        @?= Just
+            (Choice
+            [ compileMustacheText "template" "Choice 1" ^?! _Right
+            , compileMustacheText "template" "Choice 2" ^?! _Right
+            ])
+      ]
+    ]
+  , localOption (QuickCheckTests 50)
+  . localOption (QuickCheckMaxSize 10)
+  $ testGroup "MessageMap"
+    [ testGroup "instance Ixed"
+        [ testProperty "traversal laws" $ \k ->
+            isTraversal $ ix @MessageMap k
+        , testCase "preview when exists" $
+          let
+            Right tpl = compileMustacheText "foo" "bar"
+            msg = Single tpl
+            mm = Nested [("foo", Direct msg)]
+          in mm ^? ix ["foo"] @?= Just msg
+        ]
+    , testGroup "lookupMessage"
+      [ testProperty "is equivalent to preview ix" $ \msgMap path ->
+          lookupMessage path msgMap === msgMap ^? ix path
+      ]
+    ]
+
+  , testGroup "Messages"
+    [ testCase "are all valid" $ messages `deepseq` pure ()
+    ]
+
+  , testGroup "Template"
+    [ testGroup "eq"
+      [ testProperty "reflexive" $ \(tpl :: Template) -> tpl == tpl
+      ]
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Messages/TemplateSpec.hs b/users/grfn/xanthous/test/Xanthous/Messages/TemplateSpec.hs
new file mode 100644
index 0000000000..2a3873c3b0
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Messages/TemplateSpec.hs
@@ -0,0 +1,80 @@
+--------------------------------------------------------------------------------
+module Xanthous.Messages.TemplateSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+import Test.QuickCheck.Instances.Text ()
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Function (fix)
+--------------------------------------------------------------------------------
+import Xanthous.Messages.Template
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Messages.Template"
+  [ testGroup "parsing"
+    [ testProperty "literals" $ forAll genLiteral $ \s ->
+        testParse template s === Right (Literal s)
+    , parseCase "escaped curlies"
+      "foo\\{"
+      $ Literal "foo{"
+    , parseCase "simple substitution"
+      "foo {{bar}}"
+      $ Literal "foo " `Concat` Subst (SubstPath $ "bar" :| [])
+    , parseCase "substitution with filters"
+      "foo {{bar | baz}}"
+      $ Literal "foo "
+      `Concat` Subst (SubstFilter (SubstPath $ "bar" :| [])
+                                  (FilterName "baz"))
+    , parseCase "substitution with multiple filters"
+      "foo {{bar | baz | qux}}"
+      $ Literal "foo "
+      `Concat` Subst (SubstFilter (SubstFilter (SubstPath $ "bar" :| [])
+                                                (FilterName "baz"))
+                                  (FilterName "qux"))
+    , parseCase "two substitutions and a literal"
+      "{{a}}{{b}}c"
+      $ Subst (SubstPath $ "a" :| [])
+      `Concat` Subst (SubstPath $ "b" :| [])
+      `Concat` Literal "c"
+    , localOption (QuickCheckTests 10)
+    $ testProperty "round-trips with ppTemplate" $ \tpl ->
+        testParse template (ppTemplate tpl) === Right tpl
+    ]
+  , testBatch $ monoid @Template mempty
+  , testGroup "rendering"
+    [ testProperty "rendering literals renders literally"
+      $ forAll genLiteral $ \s fs vs ->
+        render fs vs (Literal s) === Right s
+    , testProperty "rendering substitutions renders substitutions"
+      $ forAll genPath $ \ident val fs ->
+        let tpl = Subst (SubstPath ident)
+            tvs = varsWith ident val
+        in render fs tvs tpl === Right val
+    , testProperty "filters filter" $ forAll genPath
+      $ \ident filterName filterFn val ->
+        let tpl = Subst (SubstFilter (SubstPath ident) filterName)
+            fs = mapFromList [(filterName, filterFn)]
+            vs = varsWith ident val
+        in render fs vs tpl === Right (filterFn val)
+    ]
+  ]
+  where
+    genLiteral = pack . filter (`notElem` ['\\', '{']) <$> arbitrary
+    parseCase name input expected =
+      testCase name $ testParse template input @?= Right expected
+    testParse p = over _Left errorBundlePretty . runParser p "<test>"
+    genIdentifier = pack @Text <$> listOf1 (elements identifierChars)
+    identifierChars = ['a'..'z'] <> ['A'..'Z'] <> ['-', '_']
+
+    varsWith (p :| []) val = vars [(p, Val val)]
+    varsWith (phead :| ps) val = vars . pure . (phead ,) . flip fix ps $
+      \next pth -> case pth of
+          [] -> Val val
+          p : ps' -> nested [(p, next ps')]
+
+    genPath = (:|) <$> genIdentifier <*> listOf genIdentifier
+
+--
diff --git a/users/grfn/xanthous/test/Xanthous/OrphansSpec.hs b/users/grfn/xanthous/test/Xanthous/OrphansSpec.hs
new file mode 100644
index 0000000000..0d800e8a91
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/OrphansSpec.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE OverloadedLists #-}
+--------------------------------------------------------------------------------
+module Xanthous.OrphansSpec where
+--------------------------------------------------------------------------------
+import           Test.Prelude
+--------------------------------------------------------------------------------
+import           Text.Mustache
+import           Text.Megaparsec (errorBundlePretty)
+import           Graphics.Vty.Attributes
+import qualified Data.Aeson as JSON
+import           Data.Interval (Interval, (<=..<=), (<=..<), (<..<=))
+import           Data.Aeson ( ToJSON(toJSON), object, Value(Array) )
+import           Data.Aeson.Types (fromJSON)
+import           Data.IntegerInterval (Extended(Finite))
+--------------------------------------------------------------------------------
+import           Xanthous.Orphans
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Orphans"
+  [ localOption (QuickCheckTests 50)
+  . localOption (QuickCheckMaxSize 10)
+  $ testGroup "Template"
+    [ testProperty "ppTemplate / compileMustacheText " \tpl ->
+        let src = ppTemplate tpl
+            res :: Either String Template
+            res = over _Left errorBundlePretty
+                $ compileMustacheText (templateActual tpl) src
+            expected = templateCache tpl ^?! at (templateActual tpl)
+        in
+          counterexample (unpack src)
+          $ Right expected === do
+            (Template actual cache) <- res
+            maybe (Left "Template not found") Right $ cache ^? at actual
+    , testProperty "JSON round trip" $ \(tpl :: Template) ->
+        counterexample (unpack $ ppTemplate tpl)
+        $ JSON.decode (JSON.encode tpl) === Just tpl
+    ]
+  , testGroup "Attr"
+    [ jsonRoundTrip @Attr ]
+  , testGroup "Extended"
+    [ jsonRoundTrip @(Extended Int) ]
+  , testGroup "Interval"
+    [ testGroup "JSON"
+      [ jsonRoundTrip @(Interval Int)
+      , testCase "parses a single value as a length-1 interval" $
+          getSuccess (fromJSON $ toJSON (1 :: Int))
+          @?= Just (Finite (1 :: Int) <=..<= Finite 1)
+      , testCase "parses a pair of values as a single-ended interval" $
+          getSuccess (fromJSON $ toJSON ([1, 2] :: [Int]))
+          @?= Just (Finite (1 :: Int) <=..< Finite (2 :: Int))
+      , testCase "parses the full included/excluded syntax" $
+          getSuccess (fromJSON $ Array [ object [ "Excluded" JSON..= (1 :: Int) ]
+                                       , object [ "Included" JSON..= (4 :: Int) ]
+                                       ])
+          @?= Just (Finite (1 :: Int) <..<= Finite (4 :: Int))
+      , testCase "parses open/closed as aliases" $
+          getSuccess (fromJSON $ Array [ object [ "Open" JSON..= (1 :: Int) ]
+                                       , object [ "Closed" JSON..= (4 :: Int) ]
+                                       ])
+          @?= Just (Finite (1 :: Int) <..<= Finite (4 :: Int))
+      ]
+    ]
+  ]
+  where
+    getSuccess :: JSON.Result a -> Maybe a
+    getSuccess (JSON.Error _) = Nothing
+    getSuccess (JSON.Success r) = Just r
diff --git a/users/grfn/xanthous/test/Xanthous/RandomSpec.hs b/users/grfn/xanthous/test/Xanthous/RandomSpec.hs
new file mode 100644
index 0000000000..c88bd95629
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/RandomSpec.hs
@@ -0,0 +1,45 @@
+--------------------------------------------------------------------------------
+module Xanthous.RandomSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+--------------------------------------------------------------------------------
+import Control.Monad.Random
+--------------------------------------------------------------------------------
+import           Xanthous.Random
+import           Xanthous.Orphans ()
+import qualified Data.Interval as Interval
+import           Data.Interval (Interval, Extended (Finite), (<=..<=))
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Random"
+  [ testGroup "chooseSubset"
+    [ testProperty "chooses a subset"
+      $ \(l :: [Int]) (Positive (r :: Double)) -> randomTest $ do
+        ss <- chooseSubset r l
+        pure $ all (`elem` l) ss
+    ]
+  , testGroup "chooseRange"
+    [ testProperty "chooses in the range"
+      $ \(rng :: Interval Int) ->
+        not (Interval.null rng)
+        ==> randomTest ( do
+                chooseRange rng >>= \case
+                  Just r -> pure
+                           . counterexample (show r)
+                           $ r `Interval.member` rng
+                  Nothing -> pure $ property Discard
+            )
+    , testProperty "nonEmpty range is never empty"
+      $ \ (lower :: Int) (NonZero diff) -> randomTest $ do
+        let upper = lower + diff
+        r <- chooseRange (Finite lower <=..<= Finite upper)
+        pure $ isJust r
+
+    ]
+  ]
+  where
+    randomTest prop = evalRandT prop . mkStdGen =<< arbitrary
diff --git a/users/grfn/xanthous/test/Xanthous/Util/GraphSpec.hs b/users/grfn/xanthous/test/Xanthous/Util/GraphSpec.hs
new file mode 100644
index 0000000000..35ff090b28
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Util/GraphSpec.hs
@@ -0,0 +1,39 @@
+module Xanthous.Util.GraphSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude
+--------------------------------------------------------------------------------
+import Xanthous.Util.Graph
+import Data.Graph.Inductive.Basic
+import Data.Graph.Inductive.Graph (labNodes, size, order)
+import Data.Graph.Inductive.PatriciaTree
+import Data.Graph.Inductive.Arbitrary
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Util.Graph"
+  [ testGroup "mstSubGraph"
+    [ testProperty "always produces a subgraph"
+        $ \(CG _ (graph :: Gr Int Int)) ->
+          let msg = mstSubGraph $ undir graph
+          in counterexample (show msg)
+            $ msg `isSubGraphOf` undir graph
+    , testProperty "returns a graph with the same nodes"
+        $ \(CG _ (graph :: Gr Int Int)) ->
+          let msg = mstSubGraph graph
+          in counterexample (show msg)
+            $ labNodes msg === labNodes graph
+    , testProperty "has nodes - 1 edges"
+        $ \(CG _ (graph :: Gr Int Int)) ->
+          order graph > 1 ==>
+          let msg = mstSubGraph graph
+          in counterexample (show msg)
+            $ size msg === order graph - 1
+    , testProperty "always produces a simple graph"
+        $ \(CG _ (graph :: Gr Int Int)) ->
+          let msg = mstSubGraph graph
+          in counterexample (show msg) $ isSimple msg
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/Util/GraphicsSpec.hs b/users/grfn/xanthous/test/Xanthous/Util/GraphicsSpec.hs
new file mode 100644
index 0000000000..61e5892803
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Util/GraphicsSpec.hs
@@ -0,0 +1,72 @@
+module Xanthous.Util.GraphicsSpec (main, test) where
+--------------------------------------------------------------------------------
+import Test.Prelude hiding (head)
+--------------------------------------------------------------------------------
+import Data.List (nub, head)
+import Data.Set (isSubsetOf)
+import Linear.V2
+--------------------------------------------------------------------------------
+import Xanthous.Util.Graphics
+import Xanthous.Util
+import Xanthous.Orphans ()
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Util.Graphics"
+  [ testGroup "circle"
+    [ testCase "radius 1, origin 2,2"
+      {-
+        |   | 0 | 1 | 2 | 3 |
+        |---+---+---+---+---|
+        | 0 |   |   |   |   |
+        | 1 |   |   | x |   |
+        | 2 |   | x |   | x |
+        | 3 |   |   | x |   |
+      -}
+      $ (sort . unique @[] @[_]) (circle @Int (V2 2 2) 1)
+      @?= [ V2 1 2
+          , V2 2 1, V2 2 3
+          , V2 3 2
+          ]
+    , testCase "radius 12, origin 0"
+      $   (sort . nub) (circle @Int 0 12)
+      @?= (sort . nub)
+          [ V2 (-12) (-4), V2 (-12) (-3), V2 (-12) (-2), V2 (-12) (-1)
+          , V2 (-12) 0, V2 (-12) 1, V2 (-12) 2, V2 (-12) 3, V2 (-12) 4
+          , V2 (-11) (-6), V2 (-11) (-5), V2 (-11) 5, V2 (-11) 6, V2 (-10) (-7)
+          , V2 (-10) 7, V2 (-9) (-9), V2 (-9) (-8), V2 (-9) 8, V2 (-9) 9
+          , V2 (-8) (-9), V2 (-8) 9, V2 (-7) (-10), V2 (-7) 10, V2 (-6) (-11)
+          , V2 (-6) 11, V2 (-5) (-11), V2 (-5) 11, V2 (-4) (-12), V2 (-4) 12
+          , V2 (-3) (-12), V2 (-3) 12, V2 (-2) (-12), V2 (-2) 12, V2 (-1) (-12)
+          , V2 (-1) 12, V2 0 (-12), V2 0 12, V2 1 (-12), V2 1 12, V2 2 (-12)
+          , V2 2 12, V2 3 (-12), V2 3 12, V2 4 (-12), V2 4 12, V2 5 (-11)
+          , V2 5 11, V2 6 (-11), V2 6 11, V2 7 (-10), V2 7 10, V2 8 (-9), V2 8 9
+          , V2 9 (-9), V2 9 (-8), V2 9 8, V2 9 9, V2 10 (-7), V2 10 7
+          , V2 11 (-6), V2 11 (-5), V2 11 5, V2 11 6, V2 12 (-4), V2 12 (-3)
+          , V2 12 (-2), V2 12 (-1), V2 12 0, V2 12 1, V2 12 2, V2 12 3, V2 12 4
+          ]
+    ]
+  , testGroup "filledCircle"
+    [ testProperty "is a superset of circle" $ \center radius ->
+        let circ = circle @Int center radius
+            filledCirc = filledCircle center radius
+        in counterexample ( "circle: " <> show circ
+                           <> "\nfilledCircle: " <> show filledCirc)
+          $ setFromList circ `isSubsetOf` setFromList filledCirc
+    -- TODO later
+    -- , testProperty "is always contiguous" $ \center radius ->
+    --     let filledCirc = filledCircle center radius
+    --     in counterexample (renderBooleanGraphics filledCirc) $
+    ]
+  , testGroup "line"
+    [ testProperty "starts and ends at the start and end points" $ \start end ->
+        let ℓ = line @Int start end
+        in counterexample ("line: " <> show ℓ)
+        $ length ℓ > 2 ==> (head ℓ === start) .&&. (head (reverse ℓ) === end)
+    ]
+  ]
+
+--------------------------------------------------------------------------------
diff --git a/users/grfn/xanthous/test/Xanthous/Util/InflectionSpec.hs b/users/grfn/xanthous/test/Xanthous/Util/InflectionSpec.hs
new file mode 100644
index 0000000000..fad8410431
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/Util/InflectionSpec.hs
@@ -0,0 +1,18 @@
+module Xanthous.Util.InflectionSpec (main, test) where
+
+import Test.Prelude
+import Xanthous.Util.Inflection
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Util.Inflection"
+  [ testGroup "toSentence"
+    [ testCase "empty"  $ toSentence [] @?= ""
+    , testCase "single" $ toSentence ["x"] @?= "x"
+    , testCase "two"    $ toSentence ["x", "y"] @?= "x and y"
+    , testCase "three"  $ toSentence ["x", "y", "z"] @?= "x, y, and z"
+    , testCase "four"   $ toSentence ["x", "y", "z", "w"] @?= "x, y, z, and w"
+    ]
+  ]
diff --git a/users/grfn/xanthous/test/Xanthous/UtilSpec.hs b/users/grfn/xanthous/test/Xanthous/UtilSpec.hs
new file mode 100644
index 0000000000..684a03b2c7
--- /dev/null
+++ b/users/grfn/xanthous/test/Xanthous/UtilSpec.hs
@@ -0,0 +1,46 @@
+module Xanthous.UtilSpec (main, test) where
+
+import Test.Prelude
+import Xanthous.Util
+import Control.Monad.State.Lazy (execState)
+
+main :: IO ()
+main = defaultMain test
+
+test :: TestTree
+test = testGroup "Xanthous.Util"
+  [ testGroup "smallestNotIn"
+    [ testCase "examples" $ do
+        smallestNotIn [7 :: Word, 3, 7] @?= 0
+        smallestNotIn [7 :: Word, 0, 1, 3, 7] @?= 2
+    , testProperty "returns an element not in the list" $ \(xs :: [Word]) ->
+        smallestNotIn xs `notElem` xs
+    , testProperty "pred return is in the list" $ \(xs :: [Word]) ->
+        let res = smallestNotIn xs
+        in res /= 0 ==> pred res `elem` xs
+    , testProperty "ignores order" $ \(xs :: [Word]) ->
+        forAll (shuffle xs) $ \shuffledXs ->
+          smallestNotIn xs === smallestNotIn shuffledXs
+    ]
+  , testGroup "takeWhileInclusive"
+    [ testProperty "takeWhileInclusive (const True) ≡ id"
+      $ \(xs :: [Int]) -> takeWhileInclusive (const True) xs === xs
+    ]
+  , testGroup "endoTimes"
+    [ testCase "endoTimes 4 succ 5"
+      $ endoTimes (4 :: Int) succ (5 :: Int) @?= 9
+    ]
+  , testGroup "modifyKL"
+    [ testCase "_1 += 1"
+      $ execState (modifyKL _1 $ pure . succ) (1 :: Int, 2 :: Int) @?= (2, 2)
+    ]
+  , testGroup "removeFirst"
+    [ testCase "example" $
+      removeFirst @[Int] (> 5) [1..10] @?= [1, 2, 3, 4, 5, 7, 8, 9, 10]
+    , testProperty "the result is the right length" $ \(xs :: [Int]) p ->
+        length (removeFirst p xs) `elem` [length xs, length xs - 1]
+    ]
+  , testGroup "AlphaChar"
+    [ testCase "succ 'z'" $ succ (AlphaChar 'z') @?= AlphaChar 'A'
+    ]
+  ]
diff --git a/users/grfn/xanthous/xanthous.cabal b/users/grfn/xanthous/xanthous.cabal
new file mode 100644
index 0000000000..1555f728ac
--- /dev/null
+++ b/users/grfn/xanthous/xanthous.cabal
@@ -0,0 +1,529 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.6.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 107b223a62633bc51425e8f9d5ab489a7a47464953a81ca693efb496c41f1aa3
+
+name:           xanthous
+version:        0.1.0.0
+synopsis:       A WIP TUI RPG
+description:    Please see the README on GitHub at <https://github.com/glittershark/xanthous>
+category:       Game
+homepage:       https://github.com/glittershark/xanthous#readme
+bug-reports:    https://github.com/glittershark/xanthous/issues
+author:         Griffin Smith
+maintainer:     root@gws.fyi
+copyright:      2019 Griffin Smith
+license:        GPL-3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.org
+
+source-repository head
+  type: git
+  location: https://github.com/glittershark/xanthous
+
+library
+  exposed-modules:
+      Data.Aeson.Generic.DerivingVia
+      Xanthous.AI.Gormlak
+      Xanthous.App
+      Xanthous.App.Autocommands
+      Xanthous.App.Common
+      Xanthous.App.Prompt
+      Xanthous.App.Time
+      Xanthous.Command
+      Xanthous.Data
+      Xanthous.Data.App
+      Xanthous.Data.Entities
+      Xanthous.Data.EntityChar
+      Xanthous.Data.EntityMap
+      Xanthous.Data.EntityMap.Graphics
+      Xanthous.Data.Levels
+      Xanthous.Data.Memo
+      Xanthous.Data.NestedMap
+      Xanthous.Data.VectorBag
+      Xanthous.Entities.Character
+      Xanthous.Entities.Common
+      Xanthous.Entities.Creature
+      Xanthous.Entities.Creature.Hippocampus
+      Xanthous.Entities.Draw.Util
+      Xanthous.Entities.Entities
+      Xanthous.Entities.Environment
+      Xanthous.Entities.Item
+      Xanthous.Entities.Marker
+      Xanthous.Entities.Raws
+      Xanthous.Entities.RawTypes
+      Xanthous.Game
+      Xanthous.Game.Arbitrary
+      Xanthous.Game.Draw
+      Xanthous.Game.Env
+      Xanthous.Game.Lenses
+      Xanthous.Game.Memo
+      Xanthous.Game.Prompt
+      Xanthous.Game.State
+      Xanthous.Generators.Level
+      Xanthous.Generators.Level.CaveAutomata
+      Xanthous.Generators.Level.Dungeon
+      Xanthous.Generators.Level.LevelContents
+      Xanthous.Generators.Level.Util
+      Xanthous.Generators.Level.Village
+      Xanthous.Generators.Speech
+      Xanthous.Messages
+      Xanthous.Messages.Template
+      Xanthous.Monad
+      Xanthous.Orphans
+      Xanthous.Physics
+      Xanthous.Prelude
+      Xanthous.Random
+      Xanthous.Util
+      Xanthous.Util.Comonad
+      Xanthous.Util.Graph
+      Xanthous.Util.Graphics
+      Xanthous.Util.Inflection
+      Xanthous.Util.JSON
+      Xanthous.Util.Optparse
+      Xanthous.Util.QuickCheck
+  other-modules:
+      Paths_xanthous
+  hs-source-dirs:
+      src
+  default-extensions:
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DeriveAnyClass
+      DeriveGeneric
+      DerivingStrategies
+      DerivingVia
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTSyntax
+      GeneralizedNewtypeDeriving
+      KindSignatures
+      StandaloneKindSignatures
+      LambdaCase
+      MultiWayIf
+      NoImplicitPrelude
+      NoStarIsType
+      OverloadedStrings
+      PolyKinds
+      RankNTypes
+      ScopedTypeVariables
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeOperators
+      ViewPatterns
+  ghc-options: -Wall
+  build-depends:
+      JuicyPixels
+    , MonadRandom
+    , QuickCheck
+    , Rasterific
+    , aeson
+    , array
+    , async
+    , base
+    , bifunctors
+    , brick
+    , checkers
+    , classy-prelude
+    , comonad
+    , comonad-extras
+    , constraints
+    , containers
+    , criterion
+    , data-default
+    , data-interval
+    , deepseq
+    , directory
+    , fgl
+    , fgl-arbitrary
+    , file-embed
+    , filepath
+    , generic-arbitrary
+    , generic-lens
+    , groups
+    , hgeometry
+    , hgeometry-combinatorial
+    , lens
+    , lifted-async
+    , linear
+    , megaparsec
+    , mmorph
+    , monad-control
+    , mtl
+    , optparse-applicative
+    , parallel
+    , parser-combinators
+    , pointed
+    , quickcheck-instances
+    , quickcheck-text
+    , random
+    , random-extras
+    , random-fu
+    , random-source
+    , raw-strings-qq
+    , reflection
+    , semigroupoids
+    , semigroups
+    , splitmix
+    , stache
+    , streams
+    , text
+    , text-zipper
+    , tomland
+    , transformers
+    , vector
+    , vty
+    , witherable
+    , yaml
+    , zlib
+  default-language: Haskell2010
+
+executable xanthous
+  main-is: Main.hs
+  other-modules:
+      Paths_xanthous
+  hs-source-dirs:
+      app
+  default-extensions:
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DeriveAnyClass
+      DeriveGeneric
+      DerivingStrategies
+      DerivingVia
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTSyntax
+      GeneralizedNewtypeDeriving
+      KindSignatures
+      StandaloneKindSignatures
+      LambdaCase
+      MultiWayIf
+      NoImplicitPrelude
+      NoStarIsType
+      OverloadedStrings
+      PolyKinds
+      RankNTypes
+      ScopedTypeVariables
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeOperators
+      ViewPatterns
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -O2
+  build-depends:
+      JuicyPixels
+    , MonadRandom
+    , QuickCheck
+    , Rasterific
+    , aeson
+    , array
+    , async
+    , base
+    , bifunctors
+    , brick
+    , checkers
+    , classy-prelude
+    , comonad
+    , comonad-extras
+    , constraints
+    , containers
+    , criterion
+    , data-default
+    , data-interval
+    , deepseq
+    , directory
+    , fgl
+    , fgl-arbitrary
+    , file-embed
+    , filepath
+    , generic-arbitrary
+    , generic-lens
+    , groups
+    , hgeometry
+    , hgeometry-combinatorial
+    , lens
+    , lifted-async
+    , linear
+    , megaparsec
+    , mmorph
+    , monad-control
+    , mtl
+    , optparse-applicative
+    , parallel
+    , parser-combinators
+    , pointed
+    , quickcheck-instances
+    , quickcheck-text
+    , random
+    , random-extras
+    , random-fu
+    , random-source
+    , raw-strings-qq
+    , reflection
+    , semigroupoids
+    , semigroups
+    , splitmix
+    , stache
+    , streams
+    , text
+    , text-zipper
+    , tomland
+    , transformers
+    , vector
+    , vty
+    , witherable
+    , xanthous
+    , yaml
+    , zlib
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Test.Prelude
+      Xanthous.CommandSpec
+      Xanthous.Data.EntitiesSpec
+      Xanthous.Data.EntityCharSpec
+      Xanthous.Data.EntityMap.GraphicsSpec
+      Xanthous.Data.EntityMapSpec
+      Xanthous.Data.LevelsSpec
+      Xanthous.Data.MemoSpec
+      Xanthous.Data.NestedMapSpec
+      Xanthous.DataSpec
+      Xanthous.Entities.CharacterSpec
+      Xanthous.Entities.CommonSpec
+      Xanthous.Entities.RawsSpec
+      Xanthous.Entities.RawTypesSpec
+      Xanthous.Game.PromptSpec
+      Xanthous.Game.StateSpec
+      Xanthous.GameSpec
+      Xanthous.Generators.Level.UtilSpec
+      Xanthous.Messages.TemplateSpec
+      Xanthous.MessageSpec
+      Xanthous.OrphansSpec
+      Xanthous.RandomSpec
+      Xanthous.Util.GraphicsSpec
+      Xanthous.Util.GraphSpec
+      Xanthous.Util.InflectionSpec
+      Xanthous.UtilSpec
+      Paths_xanthous
+  hs-source-dirs:
+      test
+  default-extensions:
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DeriveAnyClass
+      DeriveGeneric
+      DerivingStrategies
+      DerivingVia
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTSyntax
+      GeneralizedNewtypeDeriving
+      KindSignatures
+      StandaloneKindSignatures
+      LambdaCase
+      MultiWayIf
+      NoImplicitPrelude
+      NoStarIsType
+      OverloadedStrings
+      PolyKinds
+      RankNTypes
+      ScopedTypeVariables
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeOperators
+      ViewPatterns
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -O0
+  build-depends:
+      JuicyPixels
+    , MonadRandom
+    , QuickCheck
+    , Rasterific
+    , aeson
+    , array
+    , async
+    , base
+    , bifunctors
+    , brick
+    , checkers
+    , classy-prelude
+    , comonad
+    , comonad-extras
+    , constraints
+    , containers
+    , criterion
+    , data-default
+    , data-interval
+    , deepseq
+    , directory
+    , fgl
+    , fgl-arbitrary
+    , file-embed
+    , filepath
+    , generic-arbitrary
+    , generic-lens
+    , groups
+    , hgeometry
+    , hgeometry-combinatorial
+    , lens
+    , lens-properties
+    , lifted-async
+    , linear
+    , megaparsec
+    , mmorph
+    , monad-control
+    , mtl
+    , optparse-applicative
+    , parallel
+    , parser-combinators
+    , pointed
+    , quickcheck-instances
+    , quickcheck-text
+    , random
+    , random-extras
+    , random-fu
+    , random-source
+    , raw-strings-qq
+    , reflection
+    , semigroupoids
+    , semigroups
+    , splitmix
+    , stache
+    , streams
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , tasty-rerun
+    , text
+    , text-zipper
+    , tomland
+    , transformers
+    , vector
+    , vty
+    , witherable
+    , xanthous
+    , yaml
+    , zlib
+  default-language: Haskell2010
+
+benchmark benchmark
+  type: exitcode-stdio-1.0
+  main-is: Bench.hs
+  other-modules:
+      Bench.Prelude
+      Xanthous.Generators.UtilBench
+      Xanthous.RandomBench
+      Paths_xanthous
+  hs-source-dirs:
+      bench
+  default-extensions:
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DeriveAnyClass
+      DeriveGeneric
+      DerivingStrategies
+      DerivingVia
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTSyntax
+      GeneralizedNewtypeDeriving
+      KindSignatures
+      StandaloneKindSignatures
+      LambdaCase
+      MultiWayIf
+      NoImplicitPrelude
+      NoStarIsType
+      OverloadedStrings
+      PolyKinds
+      RankNTypes
+      ScopedTypeVariables
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeOperators
+      ViewPatterns
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      JuicyPixels
+    , MonadRandom
+    , QuickCheck
+    , Rasterific
+    , aeson
+    , array
+    , async
+    , base
+    , bifunctors
+    , brick
+    , checkers
+    , classy-prelude
+    , comonad
+    , comonad-extras
+    , constraints
+    , containers
+    , criterion
+    , data-default
+    , data-interval
+    , deepseq
+    , directory
+    , fgl
+    , fgl-arbitrary
+    , file-embed
+    , filepath
+    , generic-arbitrary
+    , generic-lens
+    , groups
+    , hgeometry
+    , hgeometry-combinatorial
+    , lens
+    , lifted-async
+    , linear
+    , megaparsec
+    , mmorph
+    , monad-control
+    , mtl
+    , optparse-applicative
+    , parallel
+    , parser-combinators
+    , pointed
+    , quickcheck-instances
+    , quickcheck-text
+    , random
+    , random-extras
+    , random-fu
+    , random-source
+    , raw-strings-qq
+    , reflection
+    , semigroupoids
+    , semigroups
+    , splitmix
+    , stache
+    , streams
+    , text
+    , text-zipper
+    , tomland
+    , transformers
+    , vector
+    , vty
+    , witherable
+    , xanthous
+    , yaml
+    , zlib
+  default-language: Haskell2010
diff --git a/users/isomer/OWNERS b/users/isomer/OWNERS
new file mode 100644
index 0000000000..6997cd391d
--- /dev/null
+++ b/users/isomer/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - isomer
diff --git a/users/isomer/keys.nix b/users/isomer/keys.nix
new file mode 100644
index 0000000000..8c29e27895
--- /dev/null
+++ b/users/isomer/keys.nix
@@ -0,0 +1,7 @@
+# SSH public keys
+{ ... }:
+
+rec {
+  perry = "cert-authority,principals=perry ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCXWKN+FXlQAQ36R4+FHJ9f15Tz/48xLK1f85Yf9eBrvJJVMn6ge3Cy8AJ2nymBtVvCC86q616yl4Mn+CrKBH/vHr4jY9nxJ7HHgKI8ERr+7KpLIAiiaeIBljWwCy918lK3MijRCuj0P0d3v8CEFJjyCsiyglDVcNhsW87VqqZE6lUg4Alw1CGAmNjamxdoIZxjZAM9vJtZrlYnUiu+X7vTl5ttTaZkLCCfu+/bJAKFBWPG5BPaNjjfGVuTKqEc4plkI3JeZBu3Or3LzlYxcvp71i+eKGJ8F/nMBlo25iQsQpi8ZS7JYAhj3mYVrstw7j+nkgbordvDOK5NbDMi6GzX";
+  all = [ perry ];
+}
diff --git a/users/lukegb/OWNERS b/users/lukegb/OWNERS
new file mode 100644
index 0000000000..676fbf1856
--- /dev/null
+++ b/users/lukegb/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - lukegb
diff --git a/users/lukegb/hgext/gerrithook.py b/users/lukegb/hgext/gerrithook.py
new file mode 100644
index 0000000000..ef02126ba0
--- /dev/null
+++ b/users/lukegb/hgext/gerrithook.py
@@ -0,0 +1,63 @@
+"""Bizarre hacks to make Gerrit better."""
+
+import collections
+import re
+import random
+import mercurial
+
+_ = mercurial.i18n._
+
+cmdtable = {}
+command = mercurial.registrar.command(cmdtable)
+
+testedwith = '5.3.1'
+
+_changeid_regex = re.compile(b'^Change-Id: (I.*)$', re.M)
+
+def random_hash():
+    """Returns a random SHA1-like hex string."""
+    return b"%040x" % random.getrandbits(160)
+
+def reposetup(ui, repo):
+
+    class GerritRepo(repo.__class__):
+        def commitctx(self, ctx, *args, **kwargs):
+            match = _changeid_regex.search(ctx._text)
+            if not match:
+                ctx._text = ctx._text.rstrip(b'\n')
+                ctx._text += b'\n\nChange-Id: I' + random_hash()
+            return super().commitctx(ctx, *args, **kwargs)
+
+    repo.__class__ = GerritRepo
+
+
+@command(b'gerrit-obsolete', [], _(b'[options]'))
+def gerritobsolete(ui, repo, **opts):
+    """Mark draft commits as obsolete by public commits based on Gerrit Change-Id tag."""
+    if repo.obsstore.readonly:
+        ui.error(b'obsstore is readonly')
+        return
+    changesets = collections.defaultdict(set)
+    drafts = set()
+    for draft in repo.set('draft() - obsolete()'):
+        match = _changeid_regex.search(draft.description())
+        if not match:
+            continue
+        changesets[match.groups()[0]].add(draft)
+        drafts.add(draft)
+    if not drafts:
+        return
+    publicparent = next(repo.set(
+        b'ancestor((public() and bookmark("canon")), %s)' % (
+            b', '.join(x.hex() for x in drafts))))
+    megare = b're:(?ms)^Change-Id: (%s)$' % (b'|'.join(changesets.keys()),)
+    markers = []
+    for public in repo.set('(%s..(public() and canon)) and desc(%s)', publicparent, megare):
+        match = _changeid_regex.search(public.description())
+        if not match:
+            continue
+        drafts = changesets[match.groups()[0]]
+        if not drafts:
+            continue
+        markers.append((tuple(drafts), (public,)))
+    mercurial.obsolete.createmarkers(repo, markers, operation=b'gerrit-obsolete')
diff --git a/users/lukegb/keys.nix b/users/lukegb/keys.nix
new file mode 100644
index 0000000000..e54009122f
--- /dev/null
+++ b/users/lukegb/keys.nix
@@ -0,0 +1,10 @@
+# My SSH public keys
+{ ... }:
+
+rec {
+  termius = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINytpHct7PLdLNp6MoaOPP7ccBPUQKymVNMqix//Wt1f";
+  porcorosso-wsl = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMhQ3yjf59eQjOfVXzXz5u8BS5c6hdL1yY8GqccaIjx3";
+  porcorosso-nixos = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILid+1rq3k3k7Kbaw8X63vrPrQdanH55TucQwp3ZWfo+";
+  clouvider-lon01-nix = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINQU7Y+Ha5m0ebwUjA55xXT/xbWZAWx1fVNFufle+vQj";
+  all = [ termius porcorosso-wsl porcorosso-nixos clouvider-lon01-nix ];
+}
diff --git a/users/qyliss/OWNERS b/users/qyliss/OWNERS
new file mode 100644
index 0000000000..d54ea3622d
--- /dev/null
+++ b/users/qyliss/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - qyliss
diff --git a/users/qyliss/keys.nix b/users/qyliss/keys.nix
new file mode 100644
index 0000000000..d0837a7c67
--- /dev/null
+++ b/users/qyliss/keys.nix
@@ -0,0 +1,8 @@
+# Public key from https://github.com/alyssais.keys
+{ ... }:
+
+{
+  all = [
+    "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDO11Pr7jKaRZ2It1yB312SKFN8mCV7aVYdry16LNwtnA6EDgFxyshG4Zmhl9asxQ9wa1lT3tdKB6ArA+VKxXMZB0zm15jYSLKpHQxMT7T3SqtTluJQpJD9zRtWeHbW/e1mtgn3tPYTHERB4HVGKIeGk97eOR2YOdXPHOIWhOXpogDtUlyt1bmWl0gyRHbWhViLeReHYhsu0KbZlo+ntN9aN7lPVkDfa7gUARv6IeGE5hAYHPRWmQ3VJCDaQnzsTtesLPFiNmV6Pq7qtWbHVNOG9XQLXJhD/305+yDZ2y/+KuBEQCroiWF8fPY/8gutfkZ0ZLjdGbXl38j5v+yRjreh+wjcN5MYWCWM18hMdutpoMd9D7PXaZz90V2vS+mRC81t3zXKrAy3Ke+LQBmlWSWxmKWdDoOTGOHjyPuCC/q+In7Q8hetB9/b9WUXTwEaaE3lUsa7y5JHAekNmdSoN3WD10nGYVUMvRRPGAlyqZTQdvxhn+6Pyu2piwIv/TMmC1CwiHr+fLbHxXQF745sOBQNmrdfiOzqDsKleybNB6i0AdDm5UZcYRcMLuxmryxN8O8qNUdMjMGoCeFcGwAIieqM+0xkPiByKr8ky2yV2lwOaZ4jrp/3j5GsGoQlvNKIPdCA/GQFad6vuqvhlbWcbdfiNpawrppLcJBsGB2NVjGbNQ=="
+  ];
+}
diff --git a/users/riking/OWNERS b/users/riking/OWNERS
new file mode 100644
index 0000000000..a39f4cd9f0
--- /dev/null
+++ b/users/riking/OWNERS
@@ -0,0 +1,3 @@
+inherit: false
+owners:
+ - riking
diff --git a/users/riking/adventofcode-2020/.gitignore b/users/riking/adventofcode-2020/.gitignore
new file mode 100644
index 0000000000..076ff41215
--- /dev/null
+++ b/users/riking/adventofcode-2020/.gitignore
@@ -0,0 +1,2 @@
+*/target
+*/input.txt
diff --git a/users/riking/adventofcode-2020/day01/Cargo.lock b/users/riking/adventofcode-2020/day01/Cargo.lock
new file mode 100644
index 0000000000..a1a18948a7
--- /dev/null
+++ b/users/riking/adventofcode-2020/day01/Cargo.lock
@@ -0,0 +1,14 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "anyhow"
+version = "1.0.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7"
+
+[[package]]
+name = "day01"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+]
diff --git a/users/riking/adventofcode-2020/day01/Cargo.toml b/users/riking/adventofcode-2020/day01/Cargo.toml
new file mode 100644
index 0000000000..d90ab548bb
--- /dev/null
+++ b/users/riking/adventofcode-2020/day01/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "day01"
+version = "0.1.0"
+authors = ["Kane York <kanepyork@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.34"
diff --git a/users/riking/adventofcode-2020/day01/default.nix b/users/riking/adventofcode-2020/day01/default.nix
new file mode 100644
index 0000000000..946069e3a6
--- /dev/null
+++ b/users/riking/adventofcode-2020/day01/default.nix
@@ -0,0 +1,10 @@
+{ depot, ... }:
+
+with depot.third_party;
+
+naersk.buildPackage {
+  src = ./.;
+
+  buildInputs = [ ];
+  doCheck = true;
+}
diff --git a/users/riking/adventofcode-2020/day01/src/main.rs b/users/riking/adventofcode-2020/day01/src/main.rs
new file mode 100644
index 0000000000..e8bc2a05e4
--- /dev/null
+++ b/users/riking/adventofcode-2020/day01/src/main.rs
@@ -0,0 +1,85 @@
+use anyhow::anyhow;
+use std::fs::File;
+use std::io::prelude::*;
+use std::io::BufReader;
+
+const PART_2: bool = true;
+
+fn day01(is_part2: bool, numbers: &Vec<i64>) -> Result<String, anyhow::Error> {
+    // println!("{:?}", numbers);
+
+    for n1 in numbers.iter() {
+        for n2 in numbers.iter() {
+            if is_part2 {
+                for n3 in numbers.iter() {
+                    if n1 + n2 + n3 == 2020 {
+                        return Ok((n1 * n2 * n3).to_string());
+                    }
+                }
+            } else {
+                if n1 + n2 == 2020 {
+                    return Ok((n1 * n2).to_string());
+                }
+            }
+        }
+    }
+
+    Err(anyhow!("no solution found"))
+}
+
+fn parse(filename: &str) -> Result<Vec<i64>, anyhow::Error> {
+    let f = File::open(filename)?;
+    let mut reader = BufReader::new(f);
+
+    let mut values = Vec::<i64>::new();
+
+    let mut line = String::new();
+    loop {
+        line.clear();
+        reader.read_line(&mut line)?;
+        let trimmed_line = line.trim();
+        if trimmed_line.is_empty() {
+            break;
+        }
+
+        values.push(trimmed_line.parse()?);
+    }
+    Ok(values)
+}
+
+fn main() -> anyhow::Result<()> {
+    let args: Vec<String> = std::env::args().collect();
+
+    // println!("{:?}", args);
+    if args.len() != 2 {
+        return Err(anyhow!("usage: day01 input_file"));
+    }
+    let filename = args.into_iter().skip(1).next().expect("args len == 1");
+
+    let numbers = parse(&filename)?;
+
+    println!("{}", day01(PART_2, &numbers)?);
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::day01;
+
+    #[test]
+    fn test_part1() {
+        let vec = vec![1721, 979, 366, 299, 675, 1456];
+        let result = day01(false, &vec).unwrap();
+
+        assert_eq!(result, 514579.to_string());
+    }
+
+    #[test]
+    fn test_part2() {
+        let vec = vec![1721, 979, 366, 299, 675, 1456];
+        let result = day01(true, &vec).unwrap();
+
+        assert_eq!(result, 241861950.to_string());
+    }
+}
diff --git a/users/riking/dotfiles/.mybashrc b/users/riking/dotfiles/.mybashrc
new file mode 100644
index 0000000000..c5ebc34a1f
--- /dev/null
+++ b/users/riking/dotfiles/.mybashrc
@@ -0,0 +1,53 @@
+
+# BEGIN: __USER_FUNCTIONS__
+function gh-clone() {
+	if [[ "x$2" == "x" ]]; then
+		IFS='/' read -ra PARTS <<< "$1"
+		user="${PARTS[0]}"
+		repo="${PARTS[1]}"
+	else
+		user="$1"
+		repo="$2"
+	fi
+	if [[ -d ~/go/src/github.com/"$user"/"$repo" ]]; then
+		cd ~/go/src/github.com/"${user}"/"${repo}"
+		return 0
+	fi
+	mkdir -p ~/go/src/github.com/"${user}"
+	cd ~/go/src/github.com/"${user}"
+	git clone git@github.com:"${user}"/"${repo}".git
+	cd ~/go/src/github.com/"${user}"/"${repo}"
+}
+
+function download() {
+	cd "${HOME}/Downloads"
+	wget "$@"
+}
+
+# todo: only one password pls
+function prodaccess() {
+	(ssh-add -L | grep -q 'ZgEu6S3SLatYN') || ssh-add "$HOME"/.ssh/id_ed25519
+	(ssh-add -L | grep -q 'Gfh2S3kUwZ8A6') || ssh-add "$HOME"/.ssh/id_rsa.discourse
+	echo "signing test" | gpg --clearsign > /dev/null
+}
+
+function reset-audio() {
+	pulseaudio -k && sudo alsa force-reload
+}
+
+function tvl-push() {
+	git push origin HEAD:refs/for/canon
+}
+
+# END: __USER_FUNCTIONS__
+
+# BEGIN: __USER_ENV__
+GOPATH=$HOME/go
+CDPATH=$HOME/go/src
+export GPG_TTY="$(tty)"
+
+export PATH="/usr/local/go/bin:$HOME/go/bin:$HOME/.rbenv/bin:$PATH"
+
+eval "$(rbenv init -)"
+# END: __USER_ENV__
+
diff --git a/users/riking/dotfiles/fish/conf.d/nix-env.fish b/users/riking/dotfiles/fish/conf.d/nix-env.fish
new file mode 100644
index 0000000000..6f79f97528
--- /dev/null
+++ b/users/riking/dotfiles/fish/conf.d/nix-env.fish
@@ -0,0 +1,141 @@
+# SPDX-License-Identifier: Unlicense
+# https://raw.githubusercontent.com/lilyball/nix-env.fish/master/conf.d/nix-env.fish
+
+# Setup Nix
+
+# We need to distinguish between single-user and multi-user installs.
+# This is difficult because there's no official way to do this.
+# We could look for the presence of /nix/var/nix/daemon-socket/socket but this will fail if the
+# daemon hasn't started yet. /nix/var/nix/daemon-socket will exist if the daemon has ever run, but
+# I don't think there's any protection against accidentally running `nix-daemon` as a user.
+# We also can't just look for /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh because
+# older single-user installs used the default profile instead of a per-user profile.
+# We can still check for it first, because all multi-user installs should have it, and so if it's
+# not present that's a pretty big indicator that this is a single-user install. If it does exist,
+# we still need to verify the install type. To that end we'll look for a root owner and sticky bit
+# on /nix/store. Multi-user installs set both, single-user installs don't. It's certainly possible
+# someone could do a single-user install as root and then manually set the sticky bit but that
+# would be extremely unusual.
+
+set -l nix_profile_path /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+set -l single_user_profile_path ~/.nix-profile/etc/profile.d/nix.sh
+if test -e $nix_profile_path
+  # The path exists. Double-check that this is a multi-user install.
+  # We can't just check for ~/.nix-profile/… because this may be a single-user install running as
+  # the wrong user.
+
+  # stat is not portable. Splitting the output of ls -nd is reliable on most platforms.
+  set -l owner (string split -n ' ' (ls -nd /nix/store 2>/dev/null))[3]
+  if not test -k /nix/store -a $owner -eq 0
+    # /nix/store is either not owned by root or not sticky. Assume single-user.
+    set nix_profile_path $single_user_profile_path
+  end
+else
+  # The path doesn't exist. Assume single-user
+  set nix_profile_path $single_user_profile_path
+end
+
+if test -e $nix_profile_path
+  # Source the nix setup script
+  # We're going to run the regular Nix profile under bash and then print out a few variables
+  for line in (env -u BASH_ENV bash -c '. "$0"; for name in PATH "${!NIX_@}"; do printf "%s=%s\0" "$name" "${!name}"; done' $nix_profile_path | string split0)
+    set -xg (string split -m 1 = $line)
+  end
+
+  # Insert Nix's fish share directories into fish's special variables.
+  # nixpkgs-installed fish tries to set these up already if NIX_PROFILES is defined, which won't
+  # be the case when sourcing $__fish_data_dir/share/config.fish normally, but might be for a
+  # recursive invocation. To guard against that, we'll only insert paths that don't already exit.
+  # Furthermore, for the vendor_conf.d sourcing, we'll use the pre-existing presence of a path in
+  # $fish_function_path to determine whether we want to source the relevant vendor_conf.d folder.
+
+  # To start, let's locally define NIX_PROFILES if it doesn't already exist.
+  set -al NIX_PROFILES
+  if test (count $NIX_PROFILES) -eq 0
+    set -a NIX_PROFILES $HOME/.nix-profile
+  end
+  # Replicate the logic from nixpkgs version of $__fish_data_dir/__fish_build_paths.fish.
+  set -l __nix_profile_paths (string split ' ' -- $NIX_PROFILES)[-1..1]
+  set -l __extra_completionsdir \
+    $__nix_profile_paths/etc/fish/completions \
+    $__nix_profile_paths/share/fish/vendor_completions.d
+  set -l __extra_functionsdir \
+    $__nix_profile_paths/etc/fish/functions \
+    $__nix_profile_paths/share/fish/vendor_functions.d
+  set -l __extra_confdir \
+    $__nix_profile_paths/etc/fish/conf.d \
+    $__nix_profile_paths/share/fish/vendor_conf.d \
+
+  ### Configure fish_function_path ###
+  # Remove any of our extra paths that may already exist.
+  # Record the equivalent __extra_confdir path for any function path that exists.
+  set -l existing_conf_paths
+  for path in $__extra_functionsdir
+    if set -l idx (contains --index -- $path $fish_function_path)
+      set -e fish_function_path[$idx]
+      set -a existing_conf_paths $__extra_confdir[(contains --index -- $path $__extra_functionsdir)]
+    end
+  end
+  # Insert the paths before $__fish_data_dir.
+  if set -l idx (contains --index -- $__fish_data_dir/functions $fish_function_path)
+    # Fish has no way to simply insert into the middle of an array.
+    set -l new_path $fish_function_path[1..$idx]
+    set -e new_path[$idx]
+    set -a new_path $__extra_functionsdir
+    set fish_function_path $new_path $fish_function_path[$idx..-1]
+  else
+    set -a fish_function_path $__extra_functionsdir
+  end
+
+  ### Configure fish_complete_path ###
+  # Remove any of our extra paths that may already exist.
+  for path in $__extra_completionsdir
+    if set -l idx (contains --index -- $path $fish_complete_path)
+      set -e fish_complete_path[$idx]
+    end
+  end
+  # Insert the paths before $__fish_data_dir.
+  if set -l idx (contains --index -- $__fish_data_dir/completions $fish_complete_path)
+    set -l new_path $fish_complete_path[1..$idx]
+    set -e new_path[$idx]
+    set -a new_path $__extra_completionsdir
+    set fish_complete_path $new_path $fish_complete_path[$idx..-1]
+  else
+    set -a fish_complete_path $__extra_completionsdir
+  end
+
+  ### Source conf directories ###
+  # The built-in directories were already sourced during shell initialization.
+  # Any __extra_confdir that came from $__fish_data_dir/__fish_build_paths.fish was also sourced.
+  # As explained above, we're using the presence of pre-existing paths in $fish_function_path as a
+  # signal that the corresponding conf dir has also already been sourced.
+  # In order to simulate this, we'll run through the same algorithm as found in
+  # $__fish_data_dir/config.fish except we'll avoid sourcing the file if it comes from an
+  # already-sourced location.
+  # Caveats:
+  # * Files will be sourced in a different order than we'd ideally do (because we're coming in
+  #   after the fact to source them).
+  # * If there are existing extra conf paths, files in them may have been sourced that should have
+  #   been suppressed by paths we're inserting in front.
+  # * Similarly any files in $__fish_data_dir/vendor_conf.d that should have been suppressed won't
+  #   have been.
+  set -l sourcelist
+  for file in $__fish_config_dir/conf.d/*.fish $__fish_sysconf_dir/conf.d/*.fish
+    # We know these paths were sourced already. Just record them.
+    set -l basename (string replace -r '^.*/' '' -- $file)
+    contains -- $basename $sourcelist
+    or set -a sourcelist $basename
+  end
+  for root in $__extra_confdir
+    for file in $root/*.fish
+      set -l basename (string replace -r '^.*/' '' -- $file)
+      contains -- $basename $sourcelist
+      and continue
+      set -a sourcelist $basename
+      contains -- $root $existing_conf_paths
+      and continue # this is a pre-existing path, it will have been sourced already
+      [ -f $file -a -r $file ]
+      and source $file
+    end
+  end
+end
diff --git a/users/riking/dotfiles/fish/config.fish b/users/riking/dotfiles/fish/config.fish
new file mode 100644
index 0000000000..c2454762bd
--- /dev/null
+++ b/users/riking/dotfiles/fish/config.fish
@@ -0,0 +1,8 @@
+set -gx GOPATH "$HOME/go"
+set -gx GPG_TTY (tty)
+set -gx DEPOT_ROOT "$GOPATH/src/code.tvl.fyi"
+
+set -gx PATH '/usr/local/go/bin' "$HOME/.cargo/bin" "$HOME/.rbenv/bin" $PATH
+status --is-interactive; and rbenv init - | source
+source ~/.opsrc.fish # work
+set -gx PATH "$HOME/go/bin" $PATH
diff --git a/users/riking/dotfiles/fish/fish_variables b/users/riking/dotfiles/fish/fish_variables
new file mode 100644
index 0000000000..fa8bff919f
--- /dev/null
+++ b/users/riking/dotfiles/fish/fish_variables
@@ -0,0 +1,32 @@
+# This file contains fish universal variable definitions.
+# VERSION: 3.0
+SETUVAR __fish_initialized:3100
+SETUVAR fish_color_autosuggestion:555\x1ebrblack
+SETUVAR fish_color_cancel:\x2dr
+SETUVAR fish_color_command:005fd7
+SETUVAR fish_color_comment:990000
+SETUVAR fish_color_cwd:green
+SETUVAR fish_color_cwd_root:red
+SETUVAR fish_color_end:009900
+SETUVAR fish_color_error:ff0000
+SETUVAR fish_color_escape:00a6b2
+SETUVAR fish_color_history_current:\x2d\x2dbold
+SETUVAR fish_color_host:normal
+SETUVAR fish_color_host_remote:yellow
+SETUVAR fish_color_match:\x2d\x2dbackground\x3dbrblue
+SETUVAR fish_color_normal:normal
+SETUVAR fish_color_operator:00a6b2
+SETUVAR fish_color_param:00afff
+SETUVAR fish_color_quote:999900
+SETUVAR fish_color_redirection:00afff
+SETUVAR fish_color_search_match:bryellow\x1e\x2d\x2dbackground\x3dbrblack
+SETUVAR fish_color_selection:white\x1e\x2d\x2dbold\x1e\x2d\x2dbackground\x3dbrblack
+SETUVAR fish_color_status:red
+SETUVAR fish_color_user:brgreen
+SETUVAR fish_color_valid_path:\x2d\x2dunderline
+SETUVAR fish_greeting:Welcome\x20to\x20fish\x2c\x20the\x20friendly\x20interactive\x20shell\x0aType\x20\x60help\x60\x20for\x20instructions\x20on\x20how\x20to\x20use\x20fish
+SETUVAR fish_key_bindings:fish_default_key_bindings
+SETUVAR fish_pager_color_completion:\x1d
+SETUVAR fish_pager_color_description:B3A06D\x1eyellow
+SETUVAR fish_pager_color_prefix:white\x1e\x2d\x2dbold\x1e\x2d\x2dunderline
+SETUVAR fish_pager_color_progress:brwhite\x1e\x2d\x2dbackground\x3dcyan
diff --git a/users/riking/dotfiles/fish/functions/ddate.fish b/users/riking/dotfiles/fish/functions/ddate.fish
new file mode 100644
index 0000000000..8152d31680
--- /dev/null
+++ b/users/riking/dotfiles/fish/functions/ddate.fish
@@ -0,0 +1,3 @@
+function ddate --description 'current date in Discourse format'
+    TZ=UTC date '+[date=%Y-%m-%d time=%H:%M:%S timezone=\"%Z\"]'
+end
diff --git a/users/riking/dotfiles/fish/functions/gh-clone.fish b/users/riking/dotfiles/fish/functions/gh-clone.fish
new file mode 100644
index 0000000000..109ec353f6
--- /dev/null
+++ b/users/riking/dotfiles/fish/functions/gh-clone.fish
@@ -0,0 +1,18 @@
+function gh-clone --description 'Clone and CD to a github repository'
+    if test (count $argv) -eq 1
+        set user (string split "/" -- $argv[1])[1]
+        set repo (string split "/" -- $argv[1])[2]
+    else
+        set user $argv[1]
+        set repo $argv[2]
+    end
+
+    if test -d "$HOME/go/src/github.com/$user/$repo"
+        cd "$HOME/go/src/github.com/$user/$repo"
+        return 0
+    end
+    mkdir -p "$HOME/go/src/github.com/$user"
+    cd "$HOME/go/src/github.com/$user"
+    git clone "git@github.com:$user/$repo.git"
+    cd "$HOME/go/src/github.com/$user/$repo"
+end
diff --git a/users/riking/dotfiles/fish/functions/prodaccess.fish b/users/riking/dotfiles/fish/functions/prodaccess.fish
new file mode 100644
index 0000000000..876c14c5e3
--- /dev/null
+++ b/users/riking/dotfiles/fish/functions/prodaccess.fish
@@ -0,0 +1,6 @@
+function prodaccess
+    ssh-add "$HOME/.ssh/id_ecdsa_sk"
+    begin; ssh-add -L | grep -q 'ZgEu6S3SLatYN'; end || ssh-add "$HOME"/.ssh/id_ed25519
+    begin; ssh-add -L | grep -q 'Gfh2S3kUwZ8A6'; end || ssh-add "$HOME"/.ssh/id_rsa.discourse
+    echo "signing test" | gpg --clearsign > /dev/null
+end
diff --git a/users/riking/dotfiles/fish/functions/reset-audio.fish b/users/riking/dotfiles/fish/functions/reset-audio.fish
new file mode 100644
index 0000000000..eb48578a52
--- /dev/null
+++ b/users/riking/dotfiles/fish/functions/reset-audio.fish
@@ -0,0 +1,4 @@
+function reset-audio --description "Resets pulse and alsa"
+    pulseaudio -k
+    sudo alsa force-reload
+end
diff --git a/users/riking/dotfiles/fish/functions/tvl-push.fish b/users/riking/dotfiles/fish/functions/tvl-push.fish
new file mode 100644
index 0000000000..f04ac830c0
--- /dev/null
+++ b/users/riking/dotfiles/fish/functions/tvl-push.fish
@@ -0,0 +1,3 @@
+function tvl-push
+    git push origin HEAD:refs/for/canon
+end
diff --git a/users/riking/dotfiles/regolith/Xresources b/users/riking/dotfiles/regolith/Xresources
new file mode 100644
index 0000000000..f47b93511a
--- /dev/null
+++ b/users/riking/dotfiles/regolith/Xresources
@@ -0,0 +1,5 @@
+#include "/etc/regolith/styles/ubuntu/root"
+
+i3-wm.program.lock: xset s activate
+i3-wm.program.1: /bin/sh $HOME/.config/regolith/initrc
+
diff --git a/users/riking/dotfiles/regolith/flags/first-time-setup-r1-4-1 b/users/riking/dotfiles/regolith/flags/first-time-setup-r1-4-1
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/riking/dotfiles/regolith/flags/first-time-setup-r1-4-1
diff --git a/users/riking/dotfiles/regolith/flags/show-shortcuts b/users/riking/dotfiles/regolith/flags/show-shortcuts
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/riking/dotfiles/regolith/flags/show-shortcuts
diff --git a/users/riking/dotfiles/regolith/flags/term-profile b/users/riking/dotfiles/regolith/flags/term-profile
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/riking/dotfiles/regolith/flags/term-profile
diff --git a/users/riking/dotfiles/regolith/flags/ui-fingerprint b/users/riking/dotfiles/regolith/flags/ui-fingerprint
new file mode 100644
index 0000000000..b35aedd2dc
--- /dev/null
+++ b/users/riking/dotfiles/regolith/flags/ui-fingerprint
@@ -0,0 +1 @@
+ec33ee15ff705ac4b167ba6b7f6df3c2
diff --git a/users/riking/dotfiles/regolith/initrc b/users/riking/dotfiles/regolith/initrc
new file mode 100755
index 0000000000..9b14613cd4
--- /dev/null
+++ b/users/riking/dotfiles/regolith/initrc
@@ -0,0 +1,3 @@
+
+xset s 900 5
+( xss-lock -n /usr/lib/xsecurelock/dimmer -l -- sh -c "XSECURELOCK_PASSWORD_PROMPT=time_hex XSECURELOCK_SHOW_DATETIME=1 XSECURELOCK_SAVER=saver_mpv XSECURELOCK_IMAGE_DURATION_SECONDS=10 XSECURELOCK_LIST_VIDEOS_COMMAND='find ~/Videos/Screensaver -type f' xsecurelock" )&
diff --git a/users/riking/dotfiles/tmux.conf b/users/riking/dotfiles/tmux.conf
new file mode 100644
index 0000000000..1f253cb27f
--- /dev/null
+++ b/users/riking/dotfiles/tmux.conf
@@ -0,0 +1,6 @@
+
+set -g mouse on
+set-option -g prefix C-a
+bind-key C-a send-prefix
+bind | split-window -h
+bind - split-window -v
diff --git a/users/riking/keys.nix b/users/riking/keys.nix
new file mode 100644
index 0000000000..5028709824
--- /dev/null
+++ b/users/riking/keys.nix
@@ -0,0 +1,20 @@
+# SSH public keys
+{ ... }:
+
+rec {
+  sk-ecljg09 = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBBwJ7dJJUkvIK+bDsVsCsCZSlbs90aOLsHN7XesC8/AmLA5rIRLO8I5ADoOjsWAXl/WAgxqOMmB4LxZjoXWa1a0AAAAEc3NoOg== riking@sk-ECLJG09";
+  sk-portable1 = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBCfA8/0nKk4jXclWHjRZIuicPeyIo9oDwahpnWjEATr7YaFDAo632KTSgqlW0lpx8lX9alLsJRhFV2XaSurYw/EAAAAEc3NoOg== riking@sk-portable1";
+  sk-portable2 = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBEX3DXreQR93SR68QZHTdaVd5RjlRM8C0jcZ4kI4OZwqk7xuk68w3g22q2OM7O+chj+n1N3u0hLxi82QfRnwyasAAAAEc3NoOg== riking@sk-portable2";
+  sk-desktop = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBB+JvN8nAxD+yo49Ohf/UDq7Z049yvkURJIA1XNbvKaAkvfWnCN5m9vTC1FyGxTyCwy4QpD1pFP5fIn0X/kvvfgAAAAEc3NoOg== riking@sk-kane-DAN-A4";
+
+  u2f = [ sk-ecljg09 sk-portable1 sk-portable2 sk-desktop ];
+
+  ed1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAjWIfFH2bAWMZG+HudV1MVHWUl83M/ZgEu6S3SLatYN riking@kane-DAN-A4";
+  ed2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICBblB4C9IgAijv+qN6Zs8TM2Sz7phQvVmRrcDn4VYNo riking@ECLJG09";
+
+  passworded = [ ed1 ed2 ];
+
+  unprotected = [ ];
+
+  all = u2f ++ passworded ++ unprotected;
+}
diff --git a/users/sterni/OWNERS b/users/sterni/OWNERS
new file mode 100644
index 0000000000..cace4d0f37
--- /dev/null
+++ b/users/sterni/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - sterni
diff --git a/users/sterni/clhs-lookup/README.md b/users/sterni/clhs-lookup/README.md
new file mode 100644
index 0000000000..1f42ff43a2
--- /dev/null
+++ b/users/sterni/clhs-lookup/README.md
@@ -0,0 +1,13 @@
+# clhs-lookup
+
+Simple cli to lookup symbols' documentation in a local copy of the
+Common Lisp HyperSpec.
+
+## usage
+
+```
+clhs-lookup [--print] symbol [symbol [...]]
+
+  --print  Print documentation paths to stdout instead of
+           opening them with $BROWSER (defaults to xdg-open).
+```
diff --git a/users/sterni/clhs-lookup/clhs-lookup.lisp b/users/sterni/clhs-lookup/clhs-lookup.lisp
new file mode 100644
index 0000000000..0e61dd901f
--- /dev/null
+++ b/users/sterni/clhs-lookup/clhs-lookup.lisp
@@ -0,0 +1,46 @@
+(in-package :clhs-lookup)
+(declaim (optimize (safety 3)))
+
+(defun find-symbols-paths (syms clhs)
+  "Find pathnames to HyperSpec files describing the listed
+  symbol names (as strings). Paths are returned in the order
+  of the symbols given with missing entries removed."
+  (check-type syms list)
+  (check-type clhs pathname)
+  (let* ((data-dir (merge-pathnames "HyperSpec/Data/" clhs))
+         (data (merge-pathnames "Map_Sym.txt" data-dir))
+         (found (make-hash-table :test #'equal))
+         (syms (mapcar #'string-upcase syms)))
+  (with-open-file (s data :direction :input)
+    (loop
+      with missing    = syms
+      for symbol-line = (read-line s nil :eof)
+      for path-line   = (read-line s nil :eof)
+      until (or (eq symbol-line :eof)
+                (eq path-line   :eof)
+                (null missing))
+      for pos = (position symbol-line missing :test #'equal)
+      when pos
+      do (progn
+           (delete symbol-line missing)
+           (setf (gethash symbol-line found) path-line)))
+    ; TODO(sterni): get rid of Data/../ in path
+    (mapcar
+      (lambda (x) (merge-pathnames x data-dir))
+      (remove nil
+        (mapcar (lambda (x) (gethash x found)) syms))))))
+
+(defun main ()
+  (let* ((browser (or (uiop:getenvp "BROWSER") "xdg-open"))
+         (args    (uiop:command-line-arguments))
+         (prin    (member "--print" args :test #'equal))
+         (syms    (remove-if (lambda (x) (eq (char x 0) #\-)) args))
+         (paths (find-symbols-paths syms *clhs-path*)))
+      (if (null paths)
+        (uiop:quit 1)
+        (dolist (p paths)
+          (if prin
+            (format t "~A~%" p)
+            (uiop:launch-program
+              (format nil "~A ~A" browser p)
+              :force-shell t))))))
diff --git a/users/sterni/clhs-lookup/default.nix b/users/sterni/clhs-lookup/default.nix
new file mode 100644
index 0000000000..1cde38e8ce
--- /dev/null
+++ b/users/sterni/clhs-lookup/default.nix
@@ -0,0 +1,39 @@
+{ pkgs, depot, ... }:
+
+let
+  inherit (pkgs) fetchzip writeText;
+  inherit (depot.nix) buildLisp;
+  inherit (builtins) replaceStrings;
+
+  clhsVersion = "7-0";
+
+  clhs = fetchzip {
+    name = "HyperSpec-${replaceStrings [ "-" ] [ "." ] clhsVersion}";
+    url = "ftp://ftp.lispworks.com/pub/software_tools/reference/HyperSpec-${clhsVersion}.tar.gz";
+    sha256 = "1zsi35245m5sfb862ibzy0pzlph48wvlggnqanymhgqkpa1v20ak";
+    stripRoot = false;
+  };
+
+  clhs-path = writeText "clhs-path.lisp" ''
+    (in-package :clhs-lookup.clhs-path)
+    (defparameter *clhs-path* (pathname "${clhs}/"))
+  '';
+
+  clhs-lookup = buildLisp.program {
+    name = "clhs-lookup";
+
+    deps = [
+      {
+        default = buildLisp.bundled "asdf";
+        sbcl = buildLisp.bundled "uiop";
+      }
+    ];
+
+    srcs = [
+      ./packages.lisp
+      clhs-path
+      ./clhs-lookup.lisp
+    ];
+  };
+in
+clhs-lookup
diff --git a/users/sterni/clhs-lookup/packages.lisp b/users/sterni/clhs-lookup/packages.lisp
new file mode 100644
index 0000000000..d059b96ce9
--- /dev/null
+++ b/users/sterni/clhs-lookup/packages.lisp
@@ -0,0 +1,10 @@
+(defpackage :clhs-lookup.clhs-path
+  (:use :cl)
+  (:export :*clhs-path*))
+
+(defpackage clhs-lookup
+  (:use :cl :uiop)
+  (:import-from :clhs-lookup.clhs-path :*clhs-path*)
+  (:export :main
+           :find-symbols-paths))
+
diff --git a/users/sterni/dot-time-man-pages/OWNERS b/users/sterni/dot-time-man-pages/OWNERS
new file mode 100644
index 0000000000..980c17b424
--- /dev/null
+++ b/users/sterni/dot-time-man-pages/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - edef
diff --git a/users/sterni/dot-time-man-pages/default.nix b/users/sterni/dot-time-man-pages/default.nix
new file mode 100644
index 0000000000..c449cde613
--- /dev/null
+++ b/users/sterni/dot-time-man-pages/default.nix
@@ -0,0 +1,70 @@
+{ depot, lib, ... }:
+
+let
+  # TODO(sterni): find a better place for this: is dot time //fun?
+
+  # get the email address of a depot user from //ops/users
+  findEmail = user:
+    let
+      res = builtins.filter ({ username, ... }: username == user) depot.ops.users;
+      len = builtins.length res;
+    in
+    if len == 1
+    then (builtins.head res).email
+    else builtins.throw "findEmail: got ${toString len} results instead of 1";
+
+  # dot-time(7) man page, ported from dotti.me
+  dot-time = rec {
+    name = "dot-time";
+    section = 7;
+    content = ''
+      .Dd $Mdocdate$
+      .Dt ${lib.toUpper name} ${toString section}
+      .Os
+      .Sh NAME
+      .Nm ${name}
+      .Nd a universal convention for conveying time
+      .Sh DESCRIPTION
+      For those of us who travel often or coordinate across many timezones,
+      working with local time is frequently impractical.
+      ISO8601, in all its wisdom, allows for time zone designators,
+      but still represents the hours and minutes as local time,
+      thus making it inconvenient for quickly comparing timestamps from
+      different locations.
+      .Pp
+      Dot time instead uses UTC for all date, hour, and minute indications,
+      and while it allows for time zone designators, they are optional
+      information that can be dropped without changing the indicated time.
+      It uses an alternate hour separator to make it easy to distinguish from
+      regular ISO8601.
+      When a time zone designator is provided, one can easily obtain
+      the matching local time by adding the UTC offset to the UTC time.
+      .Sh EXAMPLES
+      These timestamps all represent the same point in time.
+      .TS
+      allbox tab(|);
+      lb | lb | lb
+      l  | l  | l.
+      dot time|ISO8601|RFC3339
+      2019-06-19T22·13-04|2019-06-19T18:13-04|2019-06-19T18:13:00-04:00
+      2019-06-19T22·13+00|2019-06-19T22:13+00|2019-06-19T22:13:00Z
+      2019-06-19T22·13+02|2019-06-20T00:13+02|2019-06-20T00:13:00+02:00
+      .TE
+      .Sh SEE ALSO
+      .Lk https://dotti.me dotti.me
+      .Sh AUTHORS
+      .An -nosplit
+      .Sy dot time
+      has been proposed and documented by
+      .An edef Aq Mt ${findEmail "edef"}
+      and ported to
+      .Xr mdoc 7
+      by
+      .An sterni Aq Mt ${findEmail "sterni"} .
+    '';
+  };
+
+in
+depot.nix.buildManPages "dot-time" { } [
+  dot-time
+]
diff --git a/users/sterni/emacs/default.nix b/users/sterni/emacs/default.nix
new file mode 100644
index 0000000000..69d1d4ef41
--- /dev/null
+++ b/users/sterni/emacs/default.nix
@@ -0,0 +1,94 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs.emacsNativeComp.pkgs) withPackages;
+
+  emacs = withPackages (epkgs: [
+    epkgs.bqn-mode
+    #epkgs.elpaPackages.ada-mode
+    epkgs.elpaPackages.rainbow-mode
+    epkgs.elpaPackages.undo-tree
+    epkgs.elpaPackages.which-key
+    epkgs.melpaPackages.adoc-mode
+    epkgs.melpaPackages.cmake-mode
+    epkgs.melpaPackages.direnv
+    epkgs.melpaPackages.dockerfile-mode
+    epkgs.melpaPackages.editorconfig
+    epkgs.melpaPackages.elfeed
+    epkgs.melpaPackages.evil
+    epkgs.melpaPackages.evil-collection
+    epkgs.melpaPackages.haskell-mode
+    epkgs.melpaPackages.hl-todo
+    epkgs.melpaPackages.jq-mode
+    epkgs.melpaPackages.languagetool
+    epkgs.melpaPackages.lsp-haskell
+    epkgs.melpaPackages.lsp-mode
+    epkgs.melpaPackages.lsp-ui
+    epkgs.melpaPackages.magit
+    epkgs.melpaPackages.markdown-mode
+    epkgs.melpaPackages.meson-mode
+    epkgs.melpaPackages.nix-mode
+    epkgs.melpaPackages.org-clock-csv
+    epkgs.melpaPackages.paredit
+    epkgs.melpaPackages.rainbow-delimiters
+    epkgs.melpaPackages.sly
+    epkgs.melpaPackages.use-package
+    epkgs.melpaPackages.yaml-mode
+    epkgs.rust-mode
+    epkgs.tvlPackages.tvl
+    epkgs.urweb-mode
+
+    # TODO(sterni): until org-tracker is part of depot
+    epkgs.ivy
+    epkgs.dash
+    epkgs.s
+    epkgs.jiralib2
+  ]);
+
+  configDirectory = pkgs.symlinkJoin {
+    name = "emacs.d";
+    paths = [
+      ./.
+      (pkgs.writeTextFile {
+        name = "injected-emacs.d";
+        destination = "/nix-inject.el";
+        text = ''
+          ;; bqn-mode
+          (setq bqn-interpreter-path "${pkgs.cbqn}/bin/BQN")
+
+          ;; languagetool
+          (setq languagetool-java-bin "${pkgs.jre}/bin/java"
+                languagetool-console-command "${pkgs.languagetool}/share/languagetool-commandline.jar"
+                languagetool-server-command "${pkgs.languagetool}/share/languagetool-server.jar")
+
+          ;; use bash instead of fish from SHELL for some things, as it plays
+          ;; nicer with TERM=dumb, as I don't need/want vterm anyways.
+          ;; We want it to source /etc/profile for some extra setup that
+          ;; kicks in if TERM=dumb, meaning we can't use dash/sh mode.
+          (setq shell-file-name "${pkgs.bash}/bin/bash"
+                explicit-bash-args '("-l"))
+
+          (provide 'nix-inject)
+        '';
+      })
+    ];
+    postBuild = ''
+      rm "$out/default.nix"
+    '';
+  };
+in
+
+# sadly we can't give an init-file via the command line
+(pkgs.writeShellScriptBin "emacs" ''
+  exec ${emacs}/bin/emacs          \
+    --no-init-file                 \
+    --directory ${configDirectory} \
+    --eval "(require 'init)"       \
+    "$@"
+'').overrideAttrs (super: {
+  buildCommand = ''
+    ${super.buildCommand}
+
+    ln -s "${emacs}/bin/emacsclient" "$out/bin/emacsclient"
+  '';
+})
diff --git a/users/sterni/emacs/init.el b/users/sterni/emacs/init.el
new file mode 100644
index 0000000000..0d3f9037f5
--- /dev/null
+++ b/users/sterni/emacs/init.el
@@ -0,0 +1,355 @@
+;; Set default font and fallback font via set-fontset-font
+;; TODO(sterni): Investigate why ZWJ sequences aren't shaped properly
+(let ((mono-font "Bitstream Vera Sans Mono-12")
+      (emoji-font "Noto Color Emoji-12"))
+  (setq default-frame-alist `((font . ,mono-font)))
+  (set-frame-font mono-font t t)
+  (set-fontset-font t nil emoji-font))
+
+(setq inhibit-startup-message t
+      display-time-24hr-format t
+      select-enable-clipboard t)
+
+;; Reload files
+(global-auto-revert-mode 1)
+
+;; Indent
+(setq-default indent-tabs-mode nil)
+(setq tab-width 2
+      css-indent-offset tab-width)
+
+;; UTF-8
+(setq locale-coding-system 'utf-8)
+(set-terminal-coding-system 'utf-8)
+(set-keyboard-coding-system 'utf-8)
+(set-selection-coding-system 'utf-8)
+(prefer-coding-system 'utf-8)
+
+;; Disable unnecessary GUI elements
+(scroll-bar-mode 0)
+(menu-bar-mode 0)
+(tool-bar-mode 0)
+
+(add-hook 'after-make-frame-functions
+          (lambda (frame) (scroll-bar-mode 0)))
+
+;; don't center on cursor when scrolling
+(setq scroll-conservatively 1)
+
+;; type less
+(defalias 'yes-or-no-p 'y-or-n-p)
+
+;; Extra settings when graphical session
+(when window-system
+  (setq frame-title-format '(buffer-file-name "%f" ("%b")))
+  (mouse-wheel-mode t)
+  (blink-cursor-mode -1))
+
+;; /tmp is a tmpfs, but we may want to recover from power loss
+(custom-set-variables
+ `(temporary-file-directory ,(concat (getenv "HOME") "/.emacs/tmp")))
+
+(setq auto-save-file-name-transforms
+      `((".*" ,temporary-file-directory t)))
+(setq backup-directory-alist
+      `((".*" . ,temporary-file-directory)))
+(setq undo-tree-history-directory-alist
+      `((".*" . ,temporary-file-directory)))
+(setq backup-by-copying t)
+(setq create-lockfiles nil)
+
+;; save history
+(savehist-mode)
+(setq savehist-additional-variables '(search-ring regexp-search-ring magit-cl-history))
+
+;; buffers
+
+;; performance migitations
+(global-so-long-mode)
+
+;; unique component should come first for better completion
+(setq uniquify-buffer-name-style 'forward)
+
+;; completions
+(ido-mode 1)
+(setq ido-enable-flex-matching t)
+(ido-everywhere)
+(fido-mode)
+
+;; Display column numbers
+(column-number-mode t)
+(setq-default fill-column 80)
+(setq display-fill-column-indicator-column t)
+(add-hook 'prog-mode-hook #'display-fill-column-indicator-mode)
+
+;; whitespace
+(setq whitespace-style '(face trailing tabs)
+      whitespace-line-column fill-column)
+(add-hook 'prog-mode-hook #'whitespace-mode)
+(setq-default indicate-empty-lines t)
+(setq-default indicate-buffer-boundaries 'left)
+(setq sentence-end-double-space nil)
+
+;;; Configure built in modes
+
+;; Perl
+(setq perl-indent-level 2)
+(setq perl-continued-statement-offset 0)
+(setq perl-continued-brace-offset 0)
+
+;; org mode
+
+(setq org-clock-persist 'history)
+(org-clock-persistence-insinuate)
+
+(let ((org-folder (concat (getenv "HOME") "/files/sync/org")))
+  (setq org-agenda-files (directory-files-recursively org-folder "\\.org$")
+        org-default-notes-file (concat org-folder "/inbox.org")
+        initial-buffer-choice org-default-notes-file))
+
+;; load org-tracker and mutable config on work laptop
+(let ((org-tracker-src (concat (getenv "HOME")
+                               "/src/el/org-tracker")))
+  (when (file-exists-p org-tracker-src)
+    (add-to-list 'load-path org-tracker-src)
+
+    (use-package org-tracker
+      :hook (org-mode . org-tracker-mode)
+      :config
+      (let ((jira-config (concat (getenv "HOME")
+                                 "/.config/emacs-custom/pa-jira.el")))
+        (when (file-exists-p jira-config) (load jira-config))))))
+
+;; latex
+
+(defun latex-word-count ()
+  "Calls texcount on the file the current buffer points to and displays the result."
+  (interactive)
+  (save-buffer)
+  (let* ((file (buffer-file-name)) ; needs to happen outside with-current-buffer
+         (word-count
+             (with-output-to-string
+               (with-current-buffer standard-output
+                 (call-process "texcount" nil t nil "-brief" "-utf8" file)))))
+      (message (string-trim-right word-count))))
+
+;; ediff
+; doesn't create new window for ediff controls which I always open accidentally
+(setq ediff-window-setup-function 'ediff-setup-windows-plain)
+
+;; man
+(setq Man-notify-method 'pushy) ; display man page in current window
+
+;; shell
+
+; default, but allows ';' as prompt
+(setq shell-prompt-pattern "^[^#$%>;\n]*[#$%>;] *")
+
+;; projects (see also evil config)
+
+(defun project-magit ()
+  "Run magit in the current project dir"
+  (interactive)
+  (magit (project-root (project-current t))))
+
+(define-key project-prefix-map (kbd "G") 'project-magit)
+
+(setq project-switch-commands
+      '((project-find-file "Find file")
+        (project-find-regexp "Find regexp")
+        (project-dired "Dired")
+        (project-shell "Shell")
+        (project-magit "Magit")))
+
+;;; Configure packages
+(require 'use-package)
+
+(package-initialize)
+
+(use-package undo-tree
+  :config
+  (global-undo-tree-mode)
+  (setq undo-tree-auto-save-history t))
+
+(use-package magit
+  :after evil
+  :config
+  ; reset (buffer-local) fill-column value to emacs' default
+  ; gerrit doesn't like 80 column commit messages…
+  (add-hook 'git-commit-mode-hook (lambda () (setq fill-column 72)))
+  (evil-define-key 'normal 'global (kbd "<leader>gr") 'magit-status))
+(use-package tvl
+  :after magit
+  :custom tvl-depot-path (concat (getenv "HOME") "/src/depot"))
+
+(setq ediff-split-window-function 'split-window-horizontally)
+
+(use-package evil
+  :init
+  (setq evil-want-integration t)
+  (setq evil-want-keybinding nil)
+  (setq evil-shift-width 2)
+  (setq evil-split-window-below t)
+  (setq evil-split-window-right t)
+  (setq evil-undo-system 'undo-tree)
+  :config
+  (evil-mode 1)
+  (evil-set-leader 'normal ",") ;; TODO(sterni): space would be nice, but…
+  (evil-set-leader 'visual ",")
+  ;; buffer management
+  (evil-define-key 'normal 'global (kbd "<leader>bk") 'kill-buffer)
+  (evil-define-key 'normal 'global (kbd "<leader>bb") 'switch-to-buffer)
+  (evil-define-key 'normal 'global (kbd "<leader>bo") 'switch-to-buffer-other-window)
+  (evil-define-key 'normal 'global (kbd "<leader>bl") 'list-buffers)
+  (evil-define-key 'normal 'global (kbd "<leader>br") 'revert-buffer)
+  ;; window management: C-w hjkl is annoying in neo
+  (define-key evil-window-map (kbd "<left>") 'evil-window-left)
+  (define-key evil-window-map (kbd "<right>") 'evil-window-right)
+  (define-key evil-window-map (kbd "<up>") 'evil-window-up)
+  (define-key evil-window-map (kbd "<down>") 'evil-window-down)
+  ;; projects
+  (evil-define-key 'normal 'global (kbd "<leader>pf") 'project-find-file)
+  (evil-define-key 'normal 'global (kbd "<leader>pg") 'project-find-regexp)
+  (evil-define-key 'normal 'global (kbd "<leader>pd") 'project-dired)
+  (evil-define-key 'normal 'global (kbd "<leader>ps") 'project-shell)
+  (evil-define-key 'normal 'global (kbd "<leader>pR") 'project-query-replace-regexp)
+  (evil-define-key 'normal 'global (kbd "<leader>pK") 'project-kill-buffers)
+  (evil-define-key 'normal 'global (kbd "<leader>pp") 'project-switch-project)
+  ;; emacs
+  (evil-define-key 'visual 'global (kbd "<leader>ee") 'eval-region)
+  (evil-define-key 'normal 'global (kbd "<leader>ee") 'eval-last-sexp)
+  (evil-define-key 'normal 'global (kbd "<leader>ep") 'eval-print-last-sexp)
+  (evil-define-key 'normal 'global (kbd "<leader>eh") 'help)
+  (evil-define-key 'normal 'global (kbd "<leader>em") 'man)
+  (evil-define-key '(normal visual) 'global (kbd "<leader>eu") 'browse-url-at-point)
+  (evil-define-key '(normal visual) 'global (kbd "<leader>ef") 'ffap)
+  ;; modify what is displayed
+  (evil-define-key 'normal 'global (kbd "<leader>dw")
+    (lambda ()
+      (interactive)
+      (whitespace-mode 'toggle)
+      (display-fill-column-indicator-mode 'toggle)))
+  ;; org-mode
+  (evil-define-key 'normal 'global (kbd "<leader>oa") 'org-agenda)
+  (evil-define-key 'normal 'global (kbd "<leader>oc") 'org-capture))
+
+(use-package evil-collection
+  :after evil
+  :config
+  (evil-collection-init))
+
+;; parens
+(use-package rainbow-delimiters
+  :hook ((prog-mode . rainbow-delimiters-mode)))
+
+(setq show-paren-delay 0)
+(show-paren-mode)
+
+(use-package paredit
+  :hook ((emacs-lisp-mode . paredit-mode)
+         (lisp-mode . paredit-mode)
+         (ielm-mode . paredit-mode)
+         (lisp-interaction-mode . paredit-mode)))
+
+(use-package which-key :config (which-key-mode t))
+
+(use-package nix-mode :mode "\\.nix\\'")
+(use-package nix-drv-mode :mode "\\.drv\\'")
+
+(use-package direnv
+  :config (direnv-mode))
+
+(use-package editorconfig
+  :config (editorconfig-mode 1))
+
+(use-package haskell-mode)
+(use-package lsp-mode
+  :hook ((haskell-mode . lsp-deferred))
+  :commands (lsp lsp-deferred)
+  :custom
+  lsp-modeline-code-actions-segments '() ; using lsp-ui-sideline instead
+  :config
+  (evil-define-key 'normal 'global
+    (kbd "<leader>lwr") 'lsp-workspace-restart
+    (kbd "<leader>lwq") 'lsp-workspace-shutdown
+    (kbd "<leader>la=") 'lsp-format-buffer
+    (kbd "<leader>lar") 'lsp-rename
+    (kbd "<leader>laa") 'lsp-execute-code-action))
+(use-package lsp-ui
+  :after lsp-mode
+  :custom
+  lsp-ui-doc-enable t
+  lsp-ui-doc-border "DimGray"
+  lsp-ui-doc-delay 0.5
+  :config
+  (set-face-background 'lsp-ui-doc-background "WhiteSmoke")
+  (set-face-foreground 'lsp-ui-sideline-code-action "SaddleBrown")
+  (setq lsp-ui-sideline-code-actions-prefix "🔨 "
+        lsp-ui-sideline-show-code-actions t) ; is :custom, but won't take effect?
+  (evil-define-key 'normal lsp-ui-mode-map
+    ;; TODO(sterni): emulate using xref for non-lsp?
+    (kbd "<leader>lgr") 'lsp-ui-peek-find-references
+    (kbd "<leader>lgd") 'lsp-ui-peek-find-definitions
+    (kbd "<leader>lc") 'lsp-ui-flycheck-list))
+(use-package lsp-haskell
+  :after lsp-mode
+  :custom
+  lsp-haskell-formatting-provider "ormolu")
+
+(use-package urweb-mode)
+(use-package bqn-mode
+  :mode "\\.bqn\\'"
+  :custom bqn-mode-map-prefix "C-s-") ; probably rather using C-\
+(use-package yaml-mode)
+(use-package dockerfile-mode)
+(use-package jq-mode
+  :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode)))
+(use-package rust-mode)
+(use-package sly
+  :after evil
+  :hook ((sly-mrepl-mode . (lambda ()
+                             (enable-paredit-mode)
+                             (rainbow-delimiters-mode-enable))))
+  :config
+  (evil-define-key '(normal insert) sly-mrepl-mode-map (kbd "C-r") 'isearch-backward))
+
+; TODO(sterni): https://github.com/NixOS/nixpkgs/pull/173893/files
+; (use-package ada-mode)
+
+(use-package rainbow-mode)
+(use-package hl-todo
+  :hook ((prog-mode . hl-todo-mode))
+  :config
+  (setq hl-todo-keyword-faces
+        '(("TODO"  . "#FF0000")
+          ("FIXME" . "#FF0000")
+          ("HACK"  . "#7f7f7f")
+          ("XXX"   . "#aa0000"))))
+
+(use-package markdown-mode
+  :commands (markdown-mode gfm-mode)
+  :mode (("\\.md\\'" . markdown-mode)))
+(use-package adoc-mode
+  :mode (("\\.adoc\\'" . adoc-mode)))
+(use-package languagetool
+  :after evil
+  :custom
+  languagetool-java-arguments '("-Dfile.encoding=UTF-8")
+  languagetool-default-language "en-GB"
+  languagetool-mother-tongue "de-DE"
+  :config
+  (evil-define-key 'normal 'global (kbd "<leader>mll") 'languagetool-check)
+  (evil-define-key 'normal 'global (kbd "<leader>mlc") 'languagetool-correct-at-point)
+  (evil-define-key 'normal 'global (kbd "<leader>mls") 'languagetool-set-language)
+  (evil-define-key 'normal 'global (kbd "<leader>mlr") 'languagetool-clear-suggestions)
+  ;; Fill background of issues instead of just underlining to make it easier to read
+  (set-face-background 'languagetool-issue-default "yellow")
+  (set-face-background 'languagetool-issue-misspelling "red"))
+
+(unless (server-running-p)
+  (server-start))
+
+(require 'subscriptions) ; elfeed config
+(require 'nix-inject)
+
+(provide 'init)
diff --git a/users/sterni/emacs/subscriptions.el b/users/sterni/emacs/subscriptions.el
new file mode 100644
index 0000000000..a67e237c73
--- /dev/null
+++ b/users/sterni/emacs/subscriptions.el
@@ -0,0 +1,95 @@
+;;; elfeed
+(use-package elfeed
+  :after evil
+  :config
+  ;; elfeed bindings for evil
+  (evil-define-key 'normal 'global (kbd "<leader>mf") 'elfeed)
+  (evil-define-key '(normal visual) elfeed-search-mode-map
+    (kbd "o") 'elfeed-search-browse-url
+    (kbd "r") 'elfeed-search-untag-all-unread
+    (kbd "u") 'elfeed-search-tag-all-unread
+    (kbd "ff") 'elfeed-search-fetch
+    (kbd "fc") 'elfeed-db-compact)
+  ;; elfeed subscriptions
+  (setq elfeed-feeds
+        (append
+         ;; immutable subscriptions tracked in git
+         '(("https://repology.org/maintainer/sternenseemann%40systemli.org/feed-for-repo/nix_unstable/atom" dashboard releases)
+           ("http://hundimbuero.blogspot.com/feeds/posts/default?alt=rss" blog cool-and-nice)
+           ("gopher://text.causal.agency/0feed.atom" blog)
+           ("http://xsteadfastx.org/feed/" blog cool-and-nice)
+           ("https://tvl.fyi/feed.atom" blog cool-and-nice)
+           ("https://hannes.robur.coop/atom" blog)
+           ("https://stevelosh.com/rss.xml" blog)
+           ("https://planet.lisp.org/rss20.xml" blog)
+           ("https://hyperthings.garden/rss/all-posts.xml" blog)
+           ("https://blog.benjojo.co.uk/rss.xml" blog)
+           ("https://leahneukirchen.org/blog/index.atom" blog cool-and-nice)
+           ("https://leahneukirchen.org/trivium/index.atom" blog links cool-and-nice)
+           ("https://firefly.nu/feeds/all.atom.xml" blog cool-and-nice)
+           ("https://tazj.in/feed.atom" blog cool-and-nice)
+           ("https://alyssa.is/feed.xml" blog cool-and-nice)
+           ("https://eta.st/feed.xml" blog cool-and-nice)
+           ("https://spectrum-os.org/git/www/atom/bibliography.html" links blog)
+           ("https://rachelbythebay.com/w/atom.xml" blog)
+           ("http://evrl.com/feed.xml" blog)
+           ("https://vulns.xyz/feed.xml" blog)
+           ("https://www.german-foreign-policy.com/?type=9818" news)
+           ("https://niedzejkob.p4.team/rss.xml" blog)
+           ("https://grahamc.com/feed/" blog)
+           ("https://michael.stapelberg.ch/feed.xml" blog)
+           ("https://kazu-yamamoto.hatenablog.jp/feed" blog)
+           ("https://ariadne.space/feed/" blog)
+           ("https://bodil.lol/rss.xml" blog)
+           ("http://blog.nullspace.io/feed.xml" blog)
+           ("https://blog.kingcons.io/rss.xml" blog)
+           ("http://jaspervdj.be/rss.xml" blog)
+           ("https://www.imperialviolet.org/iv-rss.xml" blog)
+           ("https://latacora.micro.blog/feed.xml" blog)
+           ("https://22gato.tumblr.com/rss" pictures cool-and-nice)
+           ("https://theprofoundprogrammer.com/rss" blog)
+           ("https://wiki.openlab-augsburg.de/_feed" openlab)
+           ("http://shitopenlabsays.tumblr.com/rss" openlab)
+           ("http://suckless.org/atom.xml" releases)
+           ("https://kristaps.bsd.lv/lowdown/atom.xml" releases)
+           ("http://0pointer.net/blog/index.atom" blog)
+           ("https://emacsninja.com/feed.atom" blog)
+           ("https://emacshorrors.com/feed.atom" blog)
+           ("http://therealmntmn.tumblr.com/rss" blog)
+           ("http://blog.duangle.com/feeds/posts/default" blog)
+           ("http://blog.johl.io/atom.xml" blog)
+           ("http://blog.z3bra.org/rss/feed.xml" blog)
+           ("http://ccc.de/de/rss/updates.xml" news)
+           ;; ("http://fabienne.us/feed/" blog) ; database error
+           ("http://feeds.feedburner.com/baschtcom" blog)
+           ("http://ffaaaffaffaffaa.tumblr.com/rss" pictures)
+           ("http://fnordig.de/feed.xml" blog)
+           ("http://fotografiona.tumblr.com/rss" pictures)
+           ("https://grandhotel-cosmopolis.org/de/feed" news)
+           ("http://guteaussicht.org/rss" pictures)
+           ("http://konvergenzfehler.de/feed/" blog)
+           ("https://markuscisler.com/feed.xml" blog)
+           ("http://n00bcore.de/feed/" podcast)
+           ("http://spacethatneverwas.tumblr.com/rss" pictures)
+           ("http://theresa.someserver.de/blog/?feed=rss2" blog)
+           ("http://www.frumble.de/blog/feed/" blog)
+           ("http://www.plomlompom.de/PlomRogue/plomwiki.php?action=Blog_Atom" blog)
+           ("http://www.whvrt.de/rss" pictures)
+           ("http://www.windytan.com/feeds/posts/default" blog)
+           ("https://echtsuppe.wordpress.com/feed/" blog defunct)
+           ("https://mgsloan.com/feed.xml" blog)
+           ("https://notes.sterni.lv/atom.xml" me)
+           ("http://arduina.net/feed/" defunct blog)
+           ("http://beza1e1.tuxen.de/blog_en.atom" blog)
+           ("https://anchor.fm/s/94bb000/podcast/rss" podcast))
+           ;; http://www.wollenzin.de/feed/ ;_;
+
+         ;; add more feeds from an untracked file in $HOME
+         (let ((file (concat (getenv "HOME")
+                             "/.config/emacs-custom/mutable-subscriptions.el")))
+           (when (file-exists-p file)
+             (read (with-temp-buffer
+                     (insert-file-contents file)
+                     (buffer-string))))))))
+
+(provide 'subscriptions)
diff --git a/users/sterni/exercises/aoc/.gitignore b/users/sterni/exercises/aoc/.gitignore
new file mode 100644
index 0000000000..de53cfc531
--- /dev/null
+++ b/users/sterni/exercises/aoc/.gitignore
@@ -0,0 +1 @@
+/*/input
\ No newline at end of file
diff --git a/users/sterni/exercises/aoc/2021/default.nix b/users/sterni/exercises/aoc/2021/default.nix
new file mode 100644
index 0000000000..d3ed563ec6
--- /dev/null
+++ b/users/sterni/exercises/aoc/2021/default.nix
@@ -0,0 +1,10 @@
+{ depot ? import ../../../../.. { }
+, pkgs ? depot.third_party.nixpkgs
+, ...
+}:
+
+pkgs.mkShell {
+  nativeBuildInputs = [
+    pkgs.cbqn
+  ];
+}
diff --git a/users/sterni/exercises/aoc/2021/solutions.bqn b/users/sterni/exercises/aoc/2021/solutions.bqn
new file mode 100755
index 0000000000..4cedb567f9
--- /dev/null
+++ b/users/sterni/exercises/aoc/2021/solutions.bqn
@@ -0,0 +1,490 @@
+#!/usr/bin/env BQN
+
+#
+# Utilities
+#
+
+IsAsciiNum ← ('0'⊸≤∧≤⟜'9')
+
+ReadInt ← {(𝕨⊸×+⊣)´∘⌽-⟜'0'𝕩} # stolen from leah2
+ReadDec ← 10⊸ReadInt
+
+ReadInput ← {•file.Lines ∾ •path‿"/input/day"‿(•Fmt 𝕩)}
+
+SplitOn ← ((⊢ (-1˙)⍟⊣¨ +`∘(1⊸»<⊢))∘(≡¨)⊔⊢)
+
+_fix ← {𝕩 𝕊∘⊢⍟≢ 𝔽 𝕩}
+
+#
+# 2021-12-01
+#
+
+# part 1
+
+day1ExampleInput ← 199‿200‿208‿210‿200‿207‿240‿269‿260‿263
+day1Input ← ReadDec¨ReadInput 1
+
+# NB: Because distance from the ground is never smaller than zero, it's
+# no problem that nudge inserts a zero at the end of the right list
+PositiveDeltaCount ← +´∘(⊢<«)+˝˘∘↕
+
+! 7 = 1 PositiveDeltaCount day1ExampleInput
+
+•Out "Day 1.1: "∾•Fmt 1 PositiveDeltaCount day1Input
+
+# part 2
+
+! 5 = 3 PositiveDeltaCount day1ExampleInput
+
+•Out "Day 1.2: "∾•Fmt 3 PositiveDeltaCount day1Input
+
+#
+# 2021-12-02
+#
+
+# part 1
+
+day2ExampleInput ← ⟨
+  "forward 5",
+  "down 5",
+  "forward 8",
+  "up 3",
+  "down 8",
+  "forward 2",
+⟩
+
+day2Input ← ReadInput 2
+
+ParseSubmarineCommand ← (((↕2)⊸((((-1)⊸⋆)∘(2⊸|))×(=⟜(⌊∘(÷⟜2))))∘("duf"⊸⊐)∘⊑)×ReadDec∘(IsAsciiNum/⊢))
+
+SubmarineDestProduct ← {×´+´ParseSubmarineCommand¨𝕩}
+
+! 150 = SubmarineDestProduct day2ExampleInput
+
+•Out "Day 2.1: "∾•Fmt SubmarineDestProduct day2Input
+
+# part 2
+
+SubmarineAimedDestProduct ← {
+  ×´+´((×´)∘(1⊸↓)≍(1⊸⊑))¨ (<0‿0‿0) (⊢∾((⊑∘⌽⊣)+(⊑⊢)))` ParseSubmarineCommand¨𝕩
+}
+
+! 900 = SubmarineAimedDestProduct day2ExampleInput
+
+•Out "Day 2.2: "∾•Fmt SubmarineAimedDestProduct day2Input
+
+#
+# 2021-12-03
+#
+
+BinTable ← '0'-˜>
+
+day3ExampleInput ← BinTable ⟨
+  "00100",
+  "11110",
+  "10110",
+  "10111",
+  "10101",
+  "01111",
+  "00111",
+  "11100",
+  "10000",
+  "11001",
+  "00010",
+  "01010",
+⟩
+
+day3Input ← BinTable ReadInput 3
+
+DeBinList ← ((2⊸×)+⊣)´⌽
+_tableAggr ← {((÷⟜2)∘(/⟜⥊)´∘⌽∘≢𝔽(+˝))𝕩}
+GammaRate ← < _tableAggr
+
+! 22 = DeBinList GammaRate day3ExampleInput
+! 9  = DeBinList ¬GammaRate day3ExampleInput
+
+•Out "Day 3.1: "∾•Fmt (¬×○DeBinList⊢) GammaRate day3Input
+
+_lifeSupportRating ← {
+  # Need to rename the arguments, otherwise the ternary expr becomes a function
+  bitPos ← 𝕨
+  Cmp ← 𝔽
+
+  crit ← Cmp _tableAggr 𝕩
+  matchPos ← bitPos ⊑˘ crit ((⥊˜⟜≢)=⊢) 𝕩
+  match ← matchPos/𝕩
+  {1=≠match?⊏match;(bitPos+1) Cmp _lifeSupportRating match}
+}
+
+OxygenGeneratorRating ← DeBinList 0 ≤_lifeSupportRating ⊢
+CO2ScrubberRating ← DebinList 0 >_lifeSupportRating ⊢
+
+! 23 = OxygenGeneratorRating day3ExampleInput
+! 10 = CO2ScrubberRating day3ExampleInput
+
+•Out "Day 3.2: "∾•Fmt (OxygenGeneratorRating×CO2ScrubberRating) day3Input
+
+#
+# 2021-12-04
+#
+
+day4Numbers ← ReadDec¨ ',' SplitOn ⊑ReadInput 4
+day4Boards ← ReadDec¨>˘(' '⊸SplitOn¨)> (<⟨⟩) SplitOn 2↓ReadInput 4
+
+BoardWins ← {C ← ∨´∘(∧´˘) ⋄ (C∨C∘⍉)𝕩}
+
+_CallNumber ← {(𝕗∊⥊𝕩) (∨⍟(¬∘BoardWins∘⊢))˘ 𝕨}
+
+BoardWinScores ← {
+  𝕩 (0⊸</×) (⊢-») (+´)∘(BoardWins˘/(+´⥊)˘∘(𝕨⊸×⟜¬))¨ (<0⥊˜≢𝕨) (𝕨 _CallNumber)`𝕩
+}
+
+day4WinScores ← day4Boards BoardWinScores day4Numbers
+
+•Out "Day 4.1: "∾•Fmt ⊑day4WinScores
+•Out "Day 4.2: "∾•Fmt ⊑⌽day4WinScores
+
+#
+# 2021-12-06
+#
+
+day6ExampleInput ← ⟨3,4,3,1,2⟩
+day6Input ← ReadDec¨ ',' SplitOn ⊑ReadInput 6
+
+LanternfishPopulation ← {+´ (1⊸⌽+(⊑×((6⊸=)∘↕∘≠)))⍟𝕨 9↑≠¨⊔ 𝕩}
+
+! 26 = 18 LanternfishPopulation day6ExampleInput
+! 5934 = 80 LanternfishPopulation day6ExampleInput
+
+•Out "Day 6.1: "∾•Fmt 80 LanternfishPopulation day6Input
+•Out "Day 6.2: "∾•Fmt 256 LanternfishPopulation day6Input
+
+#
+# 2021-12-07
+#
+
+# part 1
+
+day7ExampleInput ← ⟨16,1,2,0,4,2,7,1,2,14⟩
+day7Input ← ReadDec¨ ','  SplitOn ⊑ReadInput 7
+
+PossiblePositions ← (⌊´+⟜(↕1⊸+)⌈´)
+FuelConsumption ← +˝∘|∘(-⌜)
+_lowestFuelPossible ← {⌊´∘(𝔽⟜PossiblePositions)˜ 𝕩}
+
+! 37 = FuelConsumption _lowestFuelPossible day7ExampleInput
+
+•Out "Day 7.1: "∾•Fmt FuelConsumption _lowestFuelPossible day7Input
+
+# part 2
+
+TriNum ← 1⊸+×÷⟜2
+
+FuelConsumption2 ← +˝∘(TriNum¨)∘|∘(-⌜)
+
+! 168 = FuelConsumption2 _lowestFuelPossible day7ExampleInput
+
+•Out "Day 7.2: "∾•Fmt FuelConsumption2 _lowestFuelPossible day7Input
+
+#
+# 2021-12-09
+#
+
+# part 1
+
+ParseHeightMap ← ((≠≍(≠⊑))⥊∾)∘-⟜'0'
+
+day9ExampleInput ← ParseHeightMap ⟨
+  "2199943210",
+  "3987894921",
+  "9856789892",
+  "8767896789",
+  "9899965678"
+⟩
+day9Input ← ParseHeightMap ReadInput 9
+
+Rotate ← (⍉⌽)∘⊢⍟⊣ # counter clockwise
+LowPoints ← {∧´𝕩⊸(⊣<((-⊢) Rotate ∞⊸»˘∘Rotate˜))¨ ↕4}
+
+RiskLevelSum ← (+´⥊)∘(1⊸+×LowPoints)
+
+! 15 = RiskLevelSum day9ExampleInput
+
+•Out "Day 9.1: "∾•Fmt RiskLevelSum day9Input
+
+# part 2
+
+NumberBasins ← ((1⊸+⊒⌾⥊)×⊢)∘LowPoints
+Basins ← {𝕩⊸((<⟜9⊣)∧(«⌈»⌈«˘⌈»˘⌈⊢)∘⊢) _fix NumberBasins 𝕩}
+LargestBasinsProduct ← {×´ 3↑ ∨ 1↓ ≠¨ ⊔⥊Basins 𝕩}
+
+! 1134 = LargestBasinsProduct day9ExampleInput
+
+•Out "Day 9.2: "∾•Fmt LargestBasinsProduct day9Input
+
+#
+# 2021-12-10
+#
+
+day10ExampleInput ← ⟨
+  "[({(<(())[]>[[{[]{<()<>>",
+  "[(()[<>])]({[<{<<[]>>(",
+  "{([(<{}[<>[]}>{[]{[(<()>",
+  "(((({<>}<{<{<>}{[]{[]{}",
+  "[[<[([]))<([[{}[[()]]]",
+  "[{[{({}]{}}([{[{{{}}([]",
+  "{<[[]]>}<{[{[{[]{()[[[]",
+  "[<(<(<(<{}))><([]([]()",
+  "<{([([[(<>()){}]>(<<{{",
+  "<{([{{}}[<[[[<>{}]]]>[]]",
+⟩
+day10Input ← ReadInput 10
+
+# part 1
+
+opp ← "([{<"
+clp ← ")]}>"
+SwapParen ← (opp∾⌽clp)⊸((⊑⊐)⊑(⌽⊣))
+
+ParenStacks ← ((<⟨⟩)⊸(((⊑∊)⟜clp⊢)◶(∾˜⟜SwapParen)‿(1⊸↓⊣)`))
+LegalParens ← ((1⊸↑)¨∘»∘ParenStacks ((∊⟜opp⊢)∨(≡⟜⋈)¨) ⊢)
+
+_ScoreFor_ ← {𝕗⊸(𝕘⊸⊐⊏⊣) 𝕩}
+
+SyntaxScore ← +´∘(0‿3‿57‿1197‿25137 _ScoreFor_ (" "∾clp))∘∾∘(1⊸↑∘(¬∘LegalParens/⊢)¨)
+
+! 26397 = SyntaxScore day10ExampleInput
+•Out "Day 10.1: "∾•Fmt SyntaxScore day10Input
+
+# part 2
+
+AutocompleteScore ← {
+  Score ← (5⊸×⊸+)˜´∘⌽∘((1+↕4) _ScoreFor_ clp)
+  # TODO(sterni): we compute ParenStacks twice here
+  ((⌊÷⟜2)∘≠⊑⊢) ∧ Score∘(⊑⌽)∘ParenStacks¨ (∧´∘LegalParens¨/⊢) 𝕩
+}
+
+! 288957 = AutocompleteScore day10ExampleInput
+•Out "Day 10.2: "∾•Fmt AutocompleteScore day10Input
+
+#
+# 2021-12-11
+#
+
+day11Input ← '0'-˜> ReadInput 11
+day11ExampleInput ← >⟨
+  ⟨5,4,8,3,1,4,3,2,2,3,⟩,
+  ⟨2,7,4,5,8,5,4,7,1,1,⟩,
+  ⟨5,2,6,4,5,5,6,1,7,3,⟩,
+  ⟨6,1,4,1,3,3,6,1,4,6,⟩,
+  ⟨6,3,5,7,3,8,5,4,7,8,⟩,
+  ⟨4,1,6,7,5,2,4,6,4,5,⟩,
+  ⟨2,1,7,6,8,4,1,7,2,1,⟩,
+  ⟨6,8,8,2,8,8,1,1,3,4,⟩,
+  ⟨4,8,4,6,8,4,8,5,5,4,⟩,
+  ⟨5,2,8,3,7,5,1,5,2,6,⟩,
+⟩
+
+# part 1
+
+OctopusFlash ← {
+  ((⥊⟜0)∘≢𝕊⊢) 𝕩;
+  flashing ← (¬𝕨)∧9<𝕩
+  energy ← ((«˘»)+(»˘«)+(»˘»)+(«˘«)+(»˘)+(«˘)+«+») flashing
+  ((𝕨∨flashing)⊸𝕊)⍟(0<+´⥊flashing) energy+𝕩
+}
+
+OctopusStep ← ((9⊸≥)×⊢)∘OctopusFlash∘(1⊸+)
+OctopusFlashCount ← {+´⥊0=>(OctopusStep⊣)`(1+𝕨)⥊<𝕩}
+
+! 1656 = 100 OctopusFlashCount day11ExampleInput
+•Out "Day 11.1: "∾•Fmt 100 OctopusFlashCount day11Input
+
+# part 2
+
+_iterCountUntil_ ← {
+  0 𝕊 𝕩;
+  𝔾◶⟨((𝕨+1)⊸𝕊)∘𝔽, 𝕨˙⟩ 𝕩
+}
+
+OctopusAllFlashing ← OctopusStep _iterCountUntil_ (∧´∘⥊∘(0⊸=))
+
+! 195 = OctopusAllFlashing day11ExampleInput
+
+•Out "Day 11.2: "∾•Fmt OctopusAllFlashing day11Input
+
+#
+# 2021-12-13
+#
+
+SplitFoldingInstructions ← ("fold along"⊸(⊣≡≠⊸↑)¨⊔⊢)∘(0⊸(≠⟜≠¨/⊢))
+day13ExampleInput ← SplitFoldingInstructions ⟨
+  "6,10",
+  "0,14",
+  "9,10",
+  "0,3",
+  "10,4",
+  "4,11",
+  "6,0",
+  "6,12",
+  "4,1",
+  "0,13",
+  "10,12",
+  "3,4",
+  "3,0",
+  "8,4",
+  "1,10",
+  "2,14",
+  "8,10",
+  "9,0",
+  "",
+  "fold along y=7",
+  "fold along x=5",
+⟩
+day13Input ← SplitFoldingInstructions ReadInput 13
+
+ParseDots ← ReadDec¨∘(','⊸SplitOn)¨
+ParseFolds ← (⊑∘'y'⊸∊≍ReadDec∘(IsAsciiNum/⊢))¨
+day13ExampleDots ← ParseDots ⊑ day13ExampleInput
+
+# part 1
+
+# 𝕨=0 => x, 𝕨=1 => y
+# 𝕩 is coordinate to fold around
+# 𝕗 is input dot list (see ParseDots)
+_Fold ← {⍷∘((𝕩⊸(((2⊸×⊣)-⊢)⌊⊢)∘⊑≍1⊸⊑)¨⌾(⌽¨⍟𝕨)) 𝕗}
+
+! 17 = ≠ 1 day13ExampleDots _Fold 7
+
+day13Dots ← ParseDots ⊑ day13Input
+day13Folds ← ParseFolds 1 ⊑ day13Input
+
+•Out "Day 13.1: "∾•Fmt ≠ (day13Dots _Fold)´ ⊑day13Folds
+
+# part 2
+
+PerformAllFolds ← {𝕩 {(𝕨 _Fold)´𝕩}˜´ ⌽𝕨}
+DotMatrix ← {
+  ⟨width, height⟩ ← 1+⌈˝∘‿2⥊∾𝕩
+  {𝕩? '█';' '}¨ height‿width⥊≠¨⊔((⊣+(width⊸×)∘⊢)´)¨ 𝕩
+}
+
+•Out "Day 13.2:"
+•Out •Fmt DotMatrix day13Folds PerformAllFolds day13Dots
+
+#
+# 2021-12-14
+#
+
+day14Polymer ← ⊑ReadInput 14
+day14Mapping ← 2↓ReadInput 14
+
+lp ← (2⊸↑)¨ day14Mapping
+le ← ⍷∾lp
+
+# returns array as long as 𝕨 detailing how many times the element
+# at any given index occurs in 𝕩.
+Counts ← ((≠⊣)↑(/⁼)∘⊐)
+
+deltaPairs ← {
+  addedPairs ← ((-1)⊸⊑¨day14Mapping) (⌽⌾(0⊸⊑))∘(∾¨)¨ lp
+  removedPairs ← ⋈¨ (2⊸↑)¨ lp
+  addedPairs (-○(lp⊸Counts))¨ removedPairs
+}
+
+pairCount ← lp Counts ⥊∘(⋈˘) 2↕day14Polymer
+
+PairInsert ← {𝕩 +´ 𝕩רdeltaPairs}
+
+pairElementCount ← (le⊸Counts)¨lp
+
+ElementRarityDiff ← {
+ ((-1)⊸⊑-⊑)∧ ⌈2÷˜ +´ pairElementCount×PairInsert⍟𝕩 pairCount
+}
+
+•Out "Day 14.1: "∾•Fmt ElementRarityDiff 10
+•Out "Day 14.2: "∾•Fmt ElementRarityDiff 40
+
+#
+# 2021-12-15
+#
+
+day15ExampleInput ← >⟨
+  1‿1‿6‿3‿7‿5‿1‿7‿4‿2
+  1‿3‿8‿1‿3‿7‿3‿6‿7‿2
+  2‿1‿3‿6‿5‿1‿1‿3‿2‿8
+  3‿6‿9‿4‿9‿3‿1‿5‿6‿9
+  7‿4‿6‿3‿4‿1‿7‿1‿1‿1
+  1‿3‿1‿9‿1‿2‿8‿1‿3‿7
+  1‿3‿5‿9‿9‿1‿2‿4‿2‿1
+  3‿1‿2‿5‿4‿2‿1‿6‿3‿9
+  1‿2‿9‿3‿1‿3‿8‿5‿2‿1
+  2‿3‿1‿1‿9‿4‿4‿5‿8‿1
+⟩
+day15Input ← '0'-˜ ((≠⋈≠∘⊑)⥊∾)ReadInput 15
+
+LowestRiskLevel ← {
+  start ← 0˙⌾⊑ (⥊⟜∞) ≢𝕩
+  ir ← (1⊑≢𝕩)⥊∞
+  Step ← {𝕩 ⌊ 𝕨 + (ir⊸«⌊ir⊸»⌊∞⊸«˘⌊∞⊸»˘) 𝕩}
+  ⊑⌽⥊ 𝕩⊸Step _fix start
+}
+
+! 40 = LowestRiskLevel day15ExampleInput
+
+•Out "Day 15.1: "∾•Fmt LowestRiskLevel day15Input
+
+FiveByFiveMap ← {(9⊸|)⌾(-⟜1) ∾(<𝕩)+ +⌜˜↕5}
+
+! 315 = LowestRiskLevel FiveByFiveMap day15ExampleInput
+
+•Out "Day 15.2: "∾•Fmt LowestRiskLevel FiveByFiveMap day15Input
+
+#
+# 2021-12-20
+#
+
+ParsePic ← (⋈⟜0)∘('#'⊸=)∘>
+
+day20ExampleAlgo ← '#'="..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..#"
+day20ExamplePic ← ParsePic ⟨"#..#.", "#....", "##..#", "..#..", "..###"⟩
+day20Input ← ReadInput 20
+day20Algo ← '#'=⊑day20Input
+day20Pic ← ParsePic 2↓day20Input
+
+GrowAxis ← {(⊢ (-1)⊸⌽∘∾ (⥊⟜𝕨)∘(2˙⌾⊑)∘≢) 𝕩}
+Grow ← {𝕨 GrowAxis 𝕨 GrowAxis˘ 𝕩}
+
+Enhance ← {
+  inf ← 1⊑𝕩
+  npic ← ((⊑⟜𝕨)∘DebinList∘⥊)˘˘ 3‿3↕ (inf⊸Grow)⍟2 ⊑𝕩
+  ninf ← 𝕨⊑˜511×inf
+  npic⋈ninf
+}
+_EnhancedPixelCount ← {+´⥊⊑ (𝕨⊸Enhance)⍟𝕗 𝕩}
+
+! 35 = day20ExampleAlgo 2 _EnhancedPixelCount day20ExamplePic
+! 3351 = day20ExampleAlgo 50 _EnhancedPixelCount day20ExamplePic
+
+•Out "Day 20.1: "∾•Fmt day20Algo 2 _EnhancedPixelCount day20Pic
+•Out "Day 20.2: "∾•Fmt day20algo 50 _EnhancedPixelCount day20Pic
+
+#
+# 2021-12-25
+#
+
+day25Input ← ".>v" ⊐ > ReadInput 25
+day25ExampleInput ← ".>v"⊐∘‿10⥊"v...>>.vv>.vv>>.vv..>>.>v>...v>>v>>.>.v.v>v.vv.v..>.>>..v....vv..>.>v.v.v..>>v.v....v..v.>"
+
+Xor ← (¬⊸∧∨∧⟜¬)
+MoveHerd ← {(𝕩∧𝕩≠𝕨)+𝕨× (𝕨=𝕩) (Xor⟜(1⊸⌽)∨⊢) (0=𝕩)∧(-1)⌽𝕨=𝕩}
+
+_fixCount ← {
+  1 𝕊 𝕩;
+  𝕩 ≡◶⟨(𝕨+1)⊸𝕊, 𝕨˙⟩ 𝔽 𝕩
+}
+
+MoveAllHerds ← (2⊸MoveHerd)∘(1⊸MoveHerd˘)
+
+! 58 = MoveAllHerds _fixCount day25ExampleInput
+•Out "Day 25.1: "∾•Fmt MoveAllHerds _fixCount day25Input
diff --git a/users/sterni/htmlman/README.md b/users/sterni/htmlman/README.md
new file mode 100644
index 0000000000..258233d4c4
--- /dev/null
+++ b/users/sterni/htmlman/README.md
@@ -0,0 +1,36 @@
+# htmlman
+
+static site generator for man pages intended for
+rendering man page documentation viewable using
+a web browser.
+
+## usage
+
+If you have a nix expression, `doc.nix`, like this:
+
+```nix
+{ depot, ... }:
+
+depot.users.sterni.htmlman {
+  title = "foo project";
+  pages = [
+    {
+      name = "foo";
+      section = 1;
+    }
+    {
+      name = "foo";
+      section = 3;
+      path = ../devman/foo.3;
+    }
+  ];
+  manDir = ../man;
+}
+```
+
+You can run the following to directly deploy the resulting
+documentation output to a specific target directory:
+
+```sh
+nix-build -A deploy doc.nix && ./result target_directory
+```
diff --git a/users/sterni/htmlman/default.nix b/users/sterni/htmlman/default.nix
new file mode 100644
index 0000000000..6bf21ce2db
--- /dev/null
+++ b/users/sterni/htmlman/default.nix
@@ -0,0 +1,268 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  inherit (depot.nix)
+    getBins
+    runExecline
+    yants
+    ;
+
+  inherit (depot.tools)
+    cheddar
+    ;
+
+  inherit (pkgs)
+    mandoc
+    coreutils
+    fetchurl
+    writers
+    ;
+
+  bins = getBins cheddar [ "cheddar" ]
+    // getBins mandoc [ "mandoc" ]
+    // getBins coreutils [ "cat" "mv" "mkdir" ]
+  ;
+
+  normalizeDrv = fetchurl {
+    url = "https://necolas.github.io/normalize.css/8.0.1/normalize.css";
+    sha256 = "04jmvybwh2ks4dlnfa70sb3a3z3ig4cv0ya9rizjvm140xq1h22q";
+  };
+
+  execlineStdoutInto = target: line: [
+    "redirfd"
+    "-w"
+    "1"
+    target
+  ] ++ line;
+
+  # I will not write a pure nix markdown renderer
+  # I will not write a pure nix markdown renderer
+  # I will not write a pure nix markdown renderer
+  # I will not write a pure nix markdown renderer
+  # I will not write a pure nix markdown renderer
+  markdown = md:
+    let
+      html = runExecline.local "rendered-markdown"
+        {
+          stdin = md;
+        }
+        ([
+          "importas"
+          "-iu"
+          "out"
+          "out"
+        ] ++ execlineStdoutInto "$out" [
+          bins.cheddar
+          "--about-filter"
+          "description.md"
+        ]);
+    in
+    builtins.readFile html;
+
+  indexTemplate = { title, description, pages ? [ ] }: ''
+    <!doctype html>
+    <html>
+      <head>
+        <meta charset="utf-8">
+        <title>${title}</title>
+        <link rel="stylesheet" type="text/css" href="style.css"/>
+      </head>
+      <body>
+        <div class="index-text">
+          <h1>${title}</h1>
+          ${markdown description}
+          <h2>man pages</h2>
+          <ul>
+            ${lib.concatMapStrings ({ name, section, ... }: ''
+              <li><a href="${name}.${toString section}.html">${name}(${toString section})</a></li>
+            '') pages}
+          </ul>
+        </div>
+      </body>
+    </html>
+  '';
+
+  defaultStyle = import ./defaultStyle.nix { };
+
+  # This deploy script automatically copies the build result into
+  # a TARGET directory and marks it as writeable optionally.
+  # It is exposed as the deploy attribute of the result of
+  # htmlman, so an htmlman expression can be used like this:
+  # nix-build -A deploy htmlman.nix && ./result target_dir
+  deployScript = title: drv: writers.writeDash "deploy-${title}" ''
+    usage() {
+      printf 'Usage: %s [-w] TARGET\n\n' "$0"
+      printf 'Deploy htmlman documentation to TARGET directory.\n\n'
+      printf '  -h    Display this help message\n'
+      printf '  -w    Make TARGET directory writeable\n'
+    }
+
+    if test "$#" -lt 1; then
+      usage
+      exit 100
+    fi
+
+    writeable=false
+
+    while test "$#" -gt 0; do
+      case "$1" in
+        -h)
+          usage
+          exit 0
+          ;;
+        -w)
+          writeable=true
+          ;;
+        -*)
+          usage
+          exit 100
+          ;;
+        *)
+          if test -z "$target"; then
+            target="$1"
+          else
+            echo "Too many arguments"
+            exit 100
+          fi
+          ;;
+      esac
+
+      shift
+    done
+
+    if test -z "$target"; then
+      echo "Missing TARGET"
+      usage
+      exit 100
+    fi
+
+    set -ex
+
+    mkdir -p "$target"
+    cp -RTL --reflink=auto "${drv}" "$target"
+
+    if $writeable; then
+      chmod -R +w "$target"
+    fi
+  '';
+
+  htmlman =
+    { title
+      # title of the index page
+    , description ? ""
+      # description which is displayed after
+      # the main heading on the index page
+    , pages ? [ ]
+      # man pages of the following structure:
+      # {
+      #   name : string;
+      #   section : int;
+      #   path : either path string;
+      # }
+      # path is optional, if it is not given,
+      # the man page source must be located at
+      # "${manDir}/${name}.${toString section}"
+    , manDir ? null
+      # directory in which man page sources are located
+    , style ? defaultStyle
+      # CSS to use as a string
+    , normalizeCss ? true
+      # whether to include normalize.css before the custom CSS
+    , linkXr ? "all"
+      # How to handle cross references in the html output:
+      #
+      # * none:     don't convert cross references into hyperlinks
+      # * all:      link all cross references as if they were
+      #             rendered into $out by htmlman
+      # * inManDir: link to all man pages which have their source
+      #             in `manDir` and use the format string defined
+      #             in linkXrFallback for all other cross references.
+    , linkXrFallback ? "https://manpages.debian.org/unstable/%N.%S.en.html"
+      # fallback link to use if linkXr == "inManDir" and the man
+      # page is not in ${manDir}. Placeholders %N (name of page)
+      # and %S (section of page) can be used. See mandoc(1) for
+      # more information.
+    }:
+
+    let
+      linkXrEnum = yants.enum "linkXr" [ "all" "inManDir" "none" ];
+
+      index = indexTemplate {
+        inherit title description pages;
+      };
+
+      resolvePath = { path ? null, name, section }:
+        if path != null
+        then path
+        else "${manDir}/${name}.${toString section}";
+
+      mandocOpts = lib.concatStringsSep "," ([
+        "style=style.css"
+      ] ++ linkXrEnum.match linkXr {
+        all = [ "man=./%N.%S.html" ];
+        inManDir = [ "man=./%N.%S.html;${linkXrFallback}" ];
+        none = [ ];
+      });
+
+      html =
+        runExecline.local "htmlman-${title}"
+          {
+            derivationArgs = {
+              inherit index style;
+              passAsFile = [ "index" "style" ];
+            };
+          }
+          ([
+            "multisubstitute"
+            [
+              "importas"
+              "-iu"
+              "out"
+              "out"
+              "importas"
+              "-iu"
+              "index"
+              "indexPath"
+              "importas"
+              "-iu"
+              "style"
+              "stylePath"
+            ]
+            "if"
+            [ bins.mkdir "-p" "$out" ]
+            "if"
+            [ bins.mv "$index" "\${out}/index.html" ]
+            "if"
+            (execlineStdoutInto "\${out}/style.css" [
+              "if"
+              ([
+                bins.cat
+              ] ++ lib.optional normalizeCss normalizeDrv
+              ++ [
+                "$style"
+              ])
+            ])
+            # let mandoc check for available man pages
+            "execline-cd"
+            "${manDir}"
+          ] ++ lib.concatMap
+            ({ name, section, ... }@p:
+              execlineStdoutInto "\${out}/${name}.${toString section}.html" [
+                "if"
+                [
+                  bins.mandoc
+                  "-mdoc"
+                  "-T"
+                  "html"
+                  "-O"
+                  mandocOpts
+                  (resolvePath p)
+                ]
+              ])
+            pages);
+    in
+    html // {
+      deploy = deployScript title html;
+    };
+in
+htmlman
diff --git a/users/sterni/htmlman/defaultStyle.nix b/users/sterni/htmlman/defaultStyle.nix
new file mode 100644
index 0000000000..a44b5ef069
--- /dev/null
+++ b/users/sterni/htmlman/defaultStyle.nix
@@ -0,0 +1,49 @@
+{ ... }:
+
+''
+  body {
+    font-size: 1em;
+    line-height: 1.5;
+    font-family: serif;
+    background-color: #efefef;
+  }
+
+  h1, h2, h3, h4, h5, h6 {
+    font-family: sans-serif;
+    font-size: 1em;
+    margin: 5px 0;
+  }
+
+  h1 {
+    margin-top: 0;
+  }
+
+  a:link, a:visited {
+    color: #3e7eff;
+  }
+
+  h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
+    text-decoration: none;
+  }
+
+  .manual-text, .index-text {
+    padding: 20px;
+    max-width: 800px;
+    background-color: white;
+    margin: 0 auto;
+  }
+
+  table.head, table.foot {
+    display: none;
+  }
+
+  .Nd {
+    display: inline;
+  }
+
+  /* use same as cheddar for man pages */
+  pre {
+    padding: 16px;
+    background-color: #f6f8fa;
+  }
+''
diff --git a/users/sterni/keys.nix b/users/sterni/keys.nix
new file mode 100644
index 0000000000..815f62ee08
--- /dev/null
+++ b/users/sterni/keys.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  all = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJk+KvgvI2oJTppMASNUfMcMkA2G5ZNt+HnWDzaXKLlo lukas@wolfgang"
+  ];
+}
diff --git a/users/sterni/mblog/.gitignore b/users/sterni/mblog/.gitignore
new file mode 100644
index 0000000000..ae957fcad0
--- /dev/null
+++ b/users/sterni/mblog/.gitignore
@@ -0,0 +1,5 @@
+# local test data
+test-msg
+
+# sly C-c C-k
+*.fasl
diff --git a/users/sterni/mblog/cli.lisp b/users/sterni/mblog/cli.lisp
new file mode 100644
index 0000000000..9bc0681df0
--- /dev/null
+++ b/users/sterni/mblog/cli.lisp
@@ -0,0 +1,71 @@
+(in-package :cli)
+(declaim (optimize (safety 3)))
+
+;; TODO(sterni): nicer messages for various errors signaled?
+
+(defun partition-by (f seq)
+  "Split SEQ into two lists, returned as multiple values. The first list
+  contains all elements for which F returns T, the second one the remaining
+  elements."
+  (loop for x in seq
+        if (funcall f x)
+          collecting x into yes
+        else
+          collecting x into no
+        finally (return (values yes no))))
+
+(defparameter +help+ '(("mnote-html" . "FILE [FILE [ ... ]]")
+                       ("mblog"      . "MAILDIR OUT")))
+
+(defun mnote-html (name flags &rest args)
+  "Convert all note mime messages given as ARGS to HTML fragments."
+  (declare (ignore name flags))
+  (loop for arg in args
+        do (note:apple-note-html-fragment
+            (note:make-apple-note (mime:mime-message (pathname arg)))
+            *standard-output*)))
+
+(defun mblog (name flags maildir outdir)
+  "Read a MAILDIR and build an mblog in OUTDIR "
+  (declare (ignore name flags))
+  (build-mblog (pathname maildir) (pathname outdir)))
+
+(defun display-help (name flags &rest args)
+  "Print help message for current executable."
+  (declare (ignore args flags))
+  (format *error-output* "Usage: ~A ~A~%"
+          name
+          (or (cdr (assoc name +help+ :test #'string=))
+              (concatenate 'string "Unknown executable: " name))))
+
+(defun usage-error (name flags &rest args)
+  "Print help and exit with a non-zero exit code."
+  (format *error-output* "~A: usage error~%" name)
+  (display-help name args flags)
+  (uiop:quit 100))
+
+(defun main ()
+  "Dispatch to correct main function based on arguments and UIOP:ARGV0."
+  (multiple-value-bind (flags args)
+      (partition-by (lambda (x) (starts-with #\- x))
+                    (uiop:command-line-arguments))
+
+    (let ((prog-name (pathname-name (pathname (uiop:argv0))))
+          (help-requested-p (find-if (lambda (x)
+                                       (member x '("-h" "--help" "--usage")
+                                               :test #'string=))
+                                     args)))
+      (apply
+       (if help-requested-p
+           #'display-help
+           (cond
+             ((and (string= prog-name "mnote-html")
+                   (null flags))
+              #'mnote-html)
+             ((and (string= prog-name "mblog")
+                   (null flags)
+                   (= 2 (length args)))
+              #'mblog)
+             (t #'usage-error)))
+       (append (list prog-name flags)
+               args)))))
diff --git a/users/sterni/mblog/default.nix b/users/sterni/mblog/default.nix
new file mode 100644
index 0000000000..607d198930
--- /dev/null
+++ b/users/sterni/mblog/default.nix
@@ -0,0 +1,44 @@
+{ depot, pkgs, ... }:
+
+(depot.nix.buildLisp.program {
+  name = "mblog";
+
+  srcs = [
+    ./packages.lisp
+    ./maildir.lisp
+    ./transformer.lisp
+    ./note.lisp
+    ./mblog.lisp
+    ./cli.lisp
+  ];
+
+  deps = [
+    {
+      sbcl = depot.nix.buildLisp.bundled "uiop";
+      default = depot.nix.buildLisp.bundled "asdf";
+    }
+    depot.lisp.klatre
+    depot.third_party.lisp.alexandria
+    depot.third_party.lisp.closure-html
+    depot.third_party.lisp.cl-date-time-parser
+    depot.third_party.lisp.cl-who
+    depot.third_party.lisp.local-time
+    depot.third_party.lisp.mime4cl
+  ];
+
+  main = "cli:main";
+
+  # due to sclf
+  brokenOn = [
+    "ccl"
+    "ecl"
+  ];
+}).overrideAttrs (super: {
+  # The built binary dispatches based on argv[0]. Building two executables would
+  # waste a lot of space.
+  buildCommand = ''
+    ${super.buildCommand}
+
+    ln -s "$out/bin/mblog" "$out/bin/mnote-html"
+  '';
+})
diff --git a/users/sterni/mblog/maildir.lisp b/users/sterni/mblog/maildir.lisp
new file mode 100644
index 0000000000..aca014203e
--- /dev/null
+++ b/users/sterni/mblog/maildir.lisp
@@ -0,0 +1,17 @@
+(in-package :maildir)
+(declaim (optimize (safety 3)))
+
+(defun list (dir)
+  "Returns a list of pathnames to messages in a maildir. The messages are
+  returned in no guaranteed order. Note that this function doesn't fully
+  implement the behavior prescribed by maildir(5): It only looks at `cur`
+  and `new` and won't clean up `tmp` nor move files from `new` to `cur`,
+  since it is strictly read-only."
+  (flet ((subdir-contents (subdir)
+           (directory
+            (merge-pathnames
+             (make-pathname :directory `(:relative ,subdir)
+                            :name :wild :type :wild)
+             dir))))
+    (mapcan #'subdir-contents '("cur" "new"))))
+
diff --git a/users/sterni/mblog/mblog.lisp b/users/sterni/mblog/mblog.lisp
new file mode 100644
index 0000000000..1f971bc121
--- /dev/null
+++ b/users/sterni/mblog/mblog.lisp
@@ -0,0 +1,140 @@
+(in-package :mblog)
+
+;; util
+
+(defmacro with-overwrite-file ((&rest args) &body body)
+  "Like WITH-OPEN-FILE, but creates/supersedes the given file for writing."
+  `(with-open-file (,@args :direction :output
+                           :if-exists :supersede
+                           :if-does-not-exist :create)
+     ,@body))
+
+(defvar *copy-buffer-size* 4096)
+
+(defun redirect-stream (in out)
+  "Consume input stream IN and write all its content to output stream OUT.
+  The streams' element types need to match."
+  (let ((buf (make-array *copy-buffer-size* :element-type (stream-element-type in))))
+    (loop for pos = (read-sequence buf in)
+          while (> pos 0)
+          do (write-sequence buf out :end pos))))
+
+;; CSS
+
+(defvar *style* "
+header, main {
+  width: 100%;
+  max-width: 800px;
+}
+
+main img {
+  max-width: 100%;
+}
+
+a:link, a:visited {
+  color: blue;
+}
+")
+
+;; Templating
+
+(eval-when (:compile-toplevel :load-toplevel)
+  (setf (who:html-mode) :html5))
+
+(defmacro render-page ((stream title &key root) &body body)
+  "Surround BODY with standard mblog document skeleton and render it to STREAM
+  using CL-WHO. If :ROOT is T, assume that the page is the top level index page.
+  Otherwise it is assumed to be one level below the index page."
+  `(who:with-html-output (,stream nil :prologue t)
+    (:html
+     (:head
+      (:meta :charset "utf-8")
+      (:meta :viewport "width=device-width")
+      (:title (who:esc ,title))
+      (:link :rel "stylesheet"
+             :type "text/css"
+             :href ,(concatenate 'string (if root "" "../") "style.css"))
+      (:style "a:link, a:visited { color: blue; }"))
+     (:body
+      (:header
+       (:nav
+        (:a :href ,(who:escape-string (if root "" "..")) "index")))
+      (:main ,@body)))))
+
+;; Build Logic
+
+(defun build-note-page (note note-dir)
+  "Convert NOTE to HTML and write it to index.html in NOTE-DIR alongside any
+  extra attachments NOTE contains."
+  (with-overwrite-file (html-stream (merge-pathnames "index.html" note-dir))
+    (render-page (html-stream (apple-note-subject note))
+      (:article
+       (apple-note-html-fragment note html-stream))))
+
+  (mime:do-parts (part note)
+    (unless (string= (mime:mime-id part)
+                     (mime:mime-id (note:apple-note-text-part note)))
+      (let ((attachment-in (mime:mime-body-stream part))
+            (attachment-dst (merge-pathnames
+                             (mime:mime-part-file-name part)
+                             note-dir)))
+
+        (format *error-output* "Writing attachment ~A~%" attachment-dst)
+
+        (with-overwrite-file (attachment-out attachment-dst
+                              :element-type
+                              (stream-element-type attachment-in))
+          (redirect-stream attachment-in attachment-out)))))
+
+  (values))
+
+(defun build-index-page (notes-list destination)
+  "Write an overview page linking all notes in NOTE-LIST in the given order to
+  DESTINATION. The notes are assumed to be in a sibling directory named like the
+  each note's UUID."
+  (with-overwrite-file (listing-stream destination)
+    (render-page (listing-stream "mblog" :root t)
+      (:h1 "mblog")
+      (:table
+       (dolist (note notes-list)
+         (who:htm
+          (:tr
+           (:td (:a :href (who:escape-string (apple-note-uuid note))
+                    (who:esc (apple-note-subject note))))
+           (:td (who:esc
+                 (klatre:format-dottime
+                  (universal-to-timestamp (apple-note-time note)))))))))))
+  (values))
+
+(defun build-mblog (notes-dir html-dir)
+  "Take MIME messages from maildir NOTES-DIR and build a complete mblog in HTML-DIR."
+  (setf notes-dir (pathname-as-directory notes-dir))
+  (setf html-dir (pathname-as-directory html-dir))
+
+  ;; TODO(sterni): avoid rewriting if nothing was updated
+  ;; TODO(sterni): clean up deleted things
+  ;; TODO(sterni): atom feed
+
+  (let ((all-notes '()))
+    (dolist (message-path (maildir:list notes-dir))
+      (let* ((note (make-apple-note (mime:mime-message message-path)))
+             (note-dir  (merge-pathnames (make-pathname
+                                          :directory
+                                          `(:relative ,(apple-note-uuid note)))
+                                         html-dir)))
+
+        (format *error-output* "Writing note message ~A to ~A~%"
+                message-path note-dir)
+        (ensure-directories-exist note-dir)
+        (build-note-page note note-dir)
+        (push note all-notes)))
+
+    ;; reverse sort the entries by time for the index page
+    (setf all-notes (sort all-notes #'> :key #'apple-note-time))
+
+    (build-index-page all-notes (merge-pathnames "index.html" html-dir))
+
+    (with-overwrite-file (css-stream (merge-pathnames "style.css" html-dir))
+      (write-string *style* css-stream))
+
+    (values)))
diff --git a/users/sterni/mblog/note.lisp b/users/sterni/mblog/note.lisp
new file mode 100644
index 0000000000..45be0f4e88
--- /dev/null
+++ b/users/sterni/mblog/note.lisp
@@ -0,0 +1,118 @@
+(in-package :note)
+(declaim (optimize (safety 3)))
+
+;;; util
+
+;; TODO(sterni): merge this with mblog::*copy-buffer-size*
+(defvar *copy-buffer-size* 4096)
+
+(defun html-escape-stream (in out)
+  "Escape characters read from stream IN and write them to
+  stream OUT escaped using WHO:ESCAPE-STRING-MINIMAL."
+  (let ((buf (make-string *copy-buffer-size*)))
+    (loop for len = (read-sequence buf in)
+          while (> len 0)
+          do (write-string (who:escape-string-minimal (subseq buf 0 len)) out))))
+
+(defun cid-header-value (cid)
+  "Takes a Content-ID as present in Apple Notes' <object> tags and properly
+  surrounds them with angle brackets for a MIME header"
+  (concatenate 'string "<" cid ">"))
+
+(defun find-mime-message-date (message)
+  (when-let ((date-string (car (mime:mime-message-header-values "Date" message))))
+    (date-time-parser:parse-date-time date-string)))
+
+;;; main implementation
+
+(defun apple-note-mime-subtype-p (x)
+  (member x '("plain" "html") :test #'string-equal))
+
+(deftype apple-note-mime-subtype ()
+  '(satisfies apple-note-mime-subtype-p))
+
+(defclass apple-note (mime:mime-message)
+  ((text-part
+    :type mime:mime-text
+    :initarg :text-part
+    :reader apple-note-text-part)
+   (subject
+    :type string
+    :initarg :subject
+    :reader apple-note-subject)
+   (uuid
+    :type string
+    :initarg :uuid
+    :reader apple-note-uuid)
+   (time
+    :type integer
+    :initarg :time
+    :reader apple-note-time)
+   (mime-subtype
+    :type apple-note-mime-subtype
+    :initarg :mime-subtype
+    :reader apple-note-mime-subtype))
+  (:documentation
+   "Representation of a Note created using Apple's Notes via the IMAP backend"))
+
+(defun apple-note-p (msg)
+  "Checks X-Uniform-Type-Identifier of a MIME:MIME-MESSAGE
+  to determine if a given mime message claims to be an Apple Note."
+  (when-let (uniform-id (car (mime:mime-message-header-values
+                              "X-Uniform-Type-Identifier"
+                              msg)))
+    (string-equal uniform-id "com.apple.mail-note")))
+
+(defun make-apple-note (msg)
+  (check-type msg mime-message)
+
+  (unless (apple-note-p msg)
+    (error "Passed message is not an Apple Note according to headers"))
+
+  (let ((text-part (mime:find-mime-text-part msg))
+        (subject (car (mime:mime-message-header-values "Subject" msg :decode t)))
+        (uuid (when-let ((val (car (mime:mime-message-header-values
+                                    "X-Universally-Unique-Identifier"
+                                    msg))))
+                (string-downcase val)))
+        (time (find-mime-message-date msg)))
+    ;; The idea here is that we don't need to check a lot manually, instead
+    ;; the type annotation are going to do this for us (with sufficient safety?)
+    (change-class msg 'apple-note
+                  :text-part text-part
+                  :subject subject
+                  :uuid uuid
+                  :time time
+                  :mime-subtype (mime:mime-subtype text-part))))
+
+(defgeneric apple-note-html-fragment (note out)
+  (:documentation
+   "Takes an APPLE-NOTE and writes its text content as HTML to
+   the OUT stream. The <object> tags are resolved to <img> which
+   refer to the respective attachment's filename as a relative path,
+   but extraction of the attachments must be done separately. The
+   surrounding <html> and <body> tags are stripped and <head>
+   discarded completely, so only a fragment which can be included
+   in custom templates remains."))
+
+(defmethod apple-note-html-fragment ((note apple-note) (out stream))
+  (let ((text (apple-note-text-part note)))
+    (cond
+      ;; notemap creates text/plain notes we need to handle properly.
+      ;; Additionally we *could* check X-Mailer which notemap sets
+      ((string-equal (apple-note-mime-subtype note) "plain")
+       (html-escape-stream (mime:mime-body-stream text :binary nil) out))
+      ;; Notes.app creates text/html parts
+      ((string-equal (apple-note-mime-subtype note) "html")
+       (closure-html:parse
+        (mime:mime-body-stream text)
+        (make-instance
+         'apple-note-transformer
+         :cid-lookup
+         (lambda (cid)
+           (when-let* ((part (mime:find-mime-part-by-id note (cid-header-value cid)))
+                       (file (mime:mime-part-file-name part)))
+             file))
+         :next-handler
+         (closure-html:make-character-stream-sink out))))
+      (t (error "Internal error: unexpected MIME subtype")))))
diff --git a/users/sterni/mblog/packages.lisp b/users/sterni/mblog/packages.lisp
new file mode 100644
index 0000000000..e4fcb46728
--- /dev/null
+++ b/users/sterni/mblog/packages.lisp
@@ -0,0 +1,49 @@
+(defpackage :maildir
+  (:use :common-lisp)
+  (:shadow :list)
+  (:export :list)
+  (:documentation
+   "Very incomplete package for dealing with maildir(5)."))
+
+(defpackage :note
+  (:use
+   :common-lisp
+   :closure-html
+   :cl-date-time-parser
+   :mime4cl)
+  (:import-from
+   :alexandria
+   :when-let*
+   :when-let
+   :starts-with-subseq
+   :ends-with-subseq)
+  (:import-from :who :escape-string-minimal)
+  (:export
+   :apple-note
+   :apple-note-uuid
+   :apple-note-subject
+   :apple-note-time
+   :apple-note-text-part
+   :make-apple-note
+   :apple-note-html-fragment))
+
+(defpackage :mblog
+  (:use
+   :common-lisp
+   :klatre
+   :who
+   :maildir
+   :note)
+  (:export :build-mblog)
+  (:import-from :local-time :universal-to-timestamp)
+  (:import-from :sclf :pathname-as-directory)
+  (:shadowing-import-from :common-lisp :list))
+
+(defpackage :cli
+  (:use
+   :common-lisp
+   :uiop
+   :note
+   :mblog)
+  (:import-from :alexandria :starts-with)
+  (:export :main))
diff --git a/users/sterni/mblog/transformer.lisp b/users/sterni/mblog/transformer.lisp
new file mode 100644
index 0000000000..31fcda028a
--- /dev/null
+++ b/users/sterni/mblog/transformer.lisp
@@ -0,0 +1,127 @@
+(in-package :note)
+(declaim (optimize (safety 3)))
+
+;; Throw away these tags and all of their children
+(defparameter +discard-tags-with-children+ '("HEAD"))
+;; Only “strip” these tags and leave their content as is
+(defparameter +discard-tags-only+ '("BODY" "HTML"))
+
+;; This is basically the same as cxml's PROXY-HANDLER.
+;; Couldn't be bothered to make a BROADCAST-HANDLER because I
+;; only need to pass through to one handler. It accepts every
+;; event and passes it on to NEXT-HANDLER. This is useful for
+;; subclassing mostly where an event can be modified or passed
+;; on as is via CALL-NEXT-METHOD.
+(defclass hax-proxy-handler (hax:default-handler)
+  ((next-handler
+    :initarg :next-handler
+    :accessor proxy-next-handler)))
+
+;; Define the trivial handlers which just call themselves for NEXT-HANDLER
+(macrolet ((def-proxy-handler (name (&rest args))
+             `(defmethod ,name ((h hax-proxy-handler) ,@args)
+                (,name (proxy-next-handler h) ,@args))))
+  (def-proxy-handler hax:start-document (name p-id s-id))
+  (def-proxy-handler hax:end-document ())
+  (def-proxy-handler hax:start-element (name attrs))
+  (def-proxy-handler hax:end-element (name))
+  (def-proxy-handler hax:characters (data))
+  (def-proxy-handler hax:unescaped (data))
+  (def-proxy-handler hax:comment (data)))
+
+(defclass apple-note-transformer (hax-proxy-handler)
+  ((cid-lookup
+    :initarg :cid-lookup
+    :initform (lambda (cid) nil)
+    :accessor transformer-cid-lookup)
+   (discard-until
+    :initarg :discard-until
+    :initform nil
+    :accessor transformer-discard-until)
+   (depth
+    :initarg :depth
+    :initform 0
+    :accessor transformer-depth))
+  (:documentation
+   "HAX handler that strips unnecessary tags from the HTML of a com.apple.mail-note
+   and resolves references to attachments to IMG tags."))
+
+;; Define the “boring” handlers which just call the next method (i. e. the next
+;; handler) unless discard-until is not nil in which case the event is dropped.
+(macrolet ((def-filter-handler (name (&rest args))
+             `(defmethod ,name ((h apple-note-transformer) ,@args)
+                (when (not (transformer-discard-until h))
+                  (call-next-method)))))
+  (def-filter-handler hax:start-document (name p-id s-id))
+  (def-filter-handler hax:end-document ())
+  (def-filter-handler hax:characters (data))
+  (def-filter-handler hax:unescaped (data))
+  (def-filter-handler hax:comment (data)))
+
+(defun parse-content-id (attrlist)
+  (when-let (data (find-if (lambda (x)
+                             (string-equal (hax:attribute-name x) "DATA"))
+                           attrlist))
+    (multiple-value-bind (starts-with-cid-p suffix)
+        (starts-with-subseq "cid:" (hax:attribute-value data)
+                            :return-suffix t :test #'char=)
+      (if starts-with-cid-p suffix data))))
+
+(defmethod hax:start-element ((handler apple-note-transformer) name attrs)
+  (with-accessors ((discard-until transformer-discard-until)
+                   (next-handler proxy-next-handler)
+                   (cid-lookup transformer-cid-lookup)
+                   (depth transformer-depth))
+      handler
+
+    (cond
+      ;; If we are discarding, any started element is dropped,
+      ;; since the end-condition only is reached via END-ELEMENT.
+      (discard-until nil)
+      ;; If we are not discarding any outer elements, we can set
+      ;; up a new discard condition if we encounter an appropriate
+      ;; element.
+      ((member name +discard-tags-with-children+ :test #'string-equal)
+       (setf discard-until (cons name depth)))
+      ;; Only drop this event, must be mirrored in END-ELEMENT to
+      ;; avoid invalidly nested HTML.
+      ((member name +discard-tags-only+ :test #'string-equal) nil)
+      ;; If we encounter an object tag, we drop it and its contents,
+      ;; but only after inspecting its attributes and emitting new
+      ;; events representing an img tag which includes the respective
+      ;; attachment via its filename.
+      ((string-equal name "OBJECT")
+       (progn
+         (setf discard-until (cons "OBJECT" depth))
+         ;; TODO(sterni): check type and only resolve images, raise error
+         ;; otherwise. We should only encounter images anyways, since
+         ;; other types are only supported for iCloud which doesn't seem
+         ;; to use IMAP for sync these days.
+         (when-let* ((cid (parse-content-id attrs))
+                     (file (apply cid-lookup (list cid)))
+                     (src (hax:make-attribute "SRC" file)))
+           (hax:start-element next-handler "IMG" (list src))
+           (hax:end-element next-handler "IMG"))))
+      ;; In all other cases, we use HAX-PROXY-HANDLER to pass the event on.
+      (t (call-next-method)))
+    (setf depth (1+ depth))))
+
+(defmethod hax:end-element ((handler apple-note-transformer) name)
+  (with-accessors ((discard-until transformer-discard-until)
+                   (depth transformer-depth))
+      handler
+
+    (setf depth (1- depth))
+    (cond
+      ;; If we are discarding and encounter the same tag again at the same
+      ;; depth, we can stop, but still have to discard the current tag.
+      ((and discard-until
+            (string-equal (car discard-until) name)
+            (= (cdr discard-until) depth))
+       (setf discard-until nil))
+      ;; In all other cases, we drop properly.
+      (discard-until nil)
+      ;; Mirrored tag stripping as in START-ELEMENT
+      ((member name +discard-tags-only+ :test #'string-equal) nil)
+      ;; In all other cases, we use HAX-PROXY-HANDLER to pass the event on.
+      (t (call-next-method)))))
diff --git a/users/sterni/nix/char/all-chars.bin b/users/sterni/nix/char/all-chars.bin
new file mode 100644
index 0000000000..017b909e8e
--- /dev/null
+++ b/users/sterni/nix/char/all-chars.bin
@@ -0,0 +1,2 @@
+	
+
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
\ No newline at end of file
diff --git a/users/sterni/nix/char/default.nix b/users/sterni/nix/char/default.nix
new file mode 100644
index 0000000000..9c6ce2fb25
--- /dev/null
+++ b/users/sterni/nix/char/default.nix
@@ -0,0 +1,99 @@
+{ depot, lib, pkgs, ... }:
+
+let
+
+  inherit (depot.users.sterni.nix.flow)
+    cond
+    ;
+
+  inherit (depot.nix)
+    yants
+    ;
+
+  inherit (depot.users.sterni.nix)
+    string
+    ;
+
+  # A char is the atomic element of a nix string
+  # which is essentially an array of arbitrary bytes
+  # as long as they are not a NUL byte.
+  #
+  # A char is neither a byte nor a unicode codepoint!
+  char = yants.restrict "char" (s: builtins.stringLength s == 1) yants.string;
+
+  # integer representation of char
+  charval = yants.restrict "charval" (i: i >= 1 && i < 256) yants.int;
+
+  allChars = builtins.readFile ./all-chars.bin;
+
+  # Originally I searched a list for this, but came to the
+  # conclusion that this can never be fast enough in Nix.
+  # We therefore use a solution similar to infinisil's.
+  ordMap = builtins.listToAttrs
+    (lib.imap1 (i: v: { name = v; value = i; })
+      (string.toChars allChars));
+
+  # Note on performance:
+  # chr and ord have been benchmarked using the following cases:
+  #
+  #  builtins.map ord (lib.stringToCharacters allChars)
+  #  builtins.map chr (builtins.genList (int.add 1) 255
+  #
+  # The findings are as follows:
+  # 1. Searching through either strings using recursion is
+  #    unbearably slow in Nix, leading to evaluation times
+  #    of up to 3s for the following very small test case.
+  #    This is why we use the trusty attribute set for ord.
+  # 2. String indexing is much faster than list indexing which
+  #    is why we use the former for chr.
+  ord = c: ordMap."${c}";
+
+  chr = i: string.charAt (i - 1) allChars;
+
+  asciiAlpha = c:
+    let
+      v = ord c;
+    in
+    (v >= 65 && v <= 90)
+    || (v >= 97 && v <= 122);
+
+  asciiNum = c:
+    let
+      v = ord c;
+    in
+    v >= 48 && v <= 57;
+
+  asciiAlphaNum = c: asciiAlpha c || asciiNum c;
+
+in
+{
+  inherit
+    allChars
+    char
+    charval
+    ord
+    chr
+    asciiAlpha
+    asciiNum
+    asciiAlphaNum
+    ;
+
+  # originally I generated a nix file containing a list of
+  # characters, but infinisil uses a better way which I adapt
+  # which is using builtins.readFile instead of import.
+  __generateAllChars = pkgs.runCommandCC "generate-all-chars"
+    {
+      source = ''
+        #include <stdio.h>
+
+        int main(void) {
+          for(int i = 1; i <= 0xff; i++) {
+            putchar(i);
+          }
+        }
+      '';
+      passAsFile = [ "source" ];
+    } ''
+    $CC -o "$out" -x c "$sourcePath"
+  '';
+}
diff --git a/users/sterni/nix/char/tests/default.nix b/users/sterni/nix/char/tests/default.nix
new file mode 100644
index 0000000000..313df47451
--- /dev/null
+++ b/users/sterni/nix/char/tests/default.nix
@@ -0,0 +1,31 @@
+{ depot, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    it
+    assertEq
+    runTestsuite
+    ;
+
+  inherit (depot.users.sterni.nix)
+    char
+    string
+    int
+    fun
+    ;
+
+  charList = string.toChars char.allChars;
+
+  testAllCharConversion = it "tests conversion of all chars" [
+    (assertEq "char.chr converts to char.allChars"
+      (builtins.genList (fun.rl char.chr (int.add 1)) 255)
+      charList)
+    (assertEq "char.ord converts from char.allChars"
+      (builtins.genList (int.add 1) 255)
+      (builtins.map char.ord charList))
+  ];
+
+in
+runTestsuite "char" [
+  testAllCharConversion
+]
diff --git a/users/sterni/nix/flow/default.nix b/users/sterni/nix/flow/default.nix
new file mode 100644
index 0000000000..4bef0abb91
--- /dev/null
+++ b/users/sterni/nix/flow/default.nix
@@ -0,0 +1,83 @@
+{ depot, ... }:
+
+let
+
+  inherit (depot.nix)
+    yants
+    ;
+
+  inherit (depot.users.sterni.nix)
+    fun
+    ;
+
+  # we must avoid evaluating any of the sublists
+  # as they may contain conditions that throw
+  condition = yants.restrict "condition"
+    (ls: builtins.length ls == 2)
+    (yants.list yants.any);
+
+  /* Like the common lisp macro: takes a list
+     of two elemented lists whose first element
+     is a boolean. The second element of the
+     first list that has true as its first
+     element is returned.
+
+     Type: [ [ bool a ] ] -> a
+
+     Example:
+
+     cond [
+       [ (builtins.isString true) 12 ]
+       [ (3 == 2) 13 ]
+       [ true 42 ]
+     ]
+
+     => 42
+   */
+  cond = conds: switch true conds;
+
+  /* Generic pattern match-ish construct for nix.
+     Takes a bunch of lists which are of length
+     two and checks the first element for either
+     a predicate or a value. The second value of
+     the first list which either has a value equal
+     to or a function that evaluates to true for
+     the given value.
+
+     Type: a -> [ [ (function | a) b ] ] -> b
+
+     Example:
+
+     switch "foo" [
+       [ "smol" "SMOL!!!" ]
+       [ (x: builtins.stringLength x <= 3) "smol-ish" ]
+       [ (fun.const true) "not smol" ]
+      ]
+
+      => "smol-ish"
+  */
+  switch = x: conds:
+    if builtins.length conds == 0
+    then builtins.throw "exhausted all conditions"
+    else
+      let
+        c = condition (builtins.head conds);
+        s = builtins.head c;
+        b =
+          if builtins.isFunction s
+          then s x
+          else x == s;
+      in
+      if b
+      then builtins.elemAt c 1
+      else switch x (builtins.tail conds);
+
+
+
+in
+{
+  inherit
+    cond
+    switch
+    ;
+}
diff --git a/users/sterni/nix/flow/tests/default.nix b/users/sterni/nix/flow/tests/default.nix
new file mode 100644
index 0000000000..9f974a61c7
--- /dev/null
+++ b/users/sterni/nix/flow/tests/default.nix
@@ -0,0 +1,39 @@
+{ depot, ... }:
+
+let
+
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    ;
+
+  inherit (depot.users.sterni.nix.flow)
+    cond
+    match
+    ;
+
+  dontEval = builtins.throw "this should not get evaluated";
+
+  testCond = it "tests cond" [
+    (assertThrows "malformed cond list"
+      (cond [ [ true 1 2 ] [ false 1 ] ]))
+    (assertEq "last is true" "last"
+      (cond [
+        [ false dontEval ]
+        [ false dontEval ]
+        [ true "last" ]
+      ]))
+    (assertEq "first is true" 1
+      (cond [
+        [ true 1 ]
+        [ true dontEval ]
+        [ true dontEval ]
+      ]))
+  ];
+
+in
+runTestsuite "nix.flow" [
+  testCond
+]
diff --git a/users/sterni/nix/fun/default.nix b/users/sterni/nix/fun/default.nix
new file mode 100644
index 0000000000..bb10f9e6c1
--- /dev/null
+++ b/users/sterni/nix/fun/default.nix
@@ -0,0 +1,257 @@
+{ depot, lib, ... }:
+
+let
+
+  inherit (lib)
+    id
+    ;
+
+  # Simple function composition,
+  # application is right to left.
+  rl = f1: f2:
+    (x: f1 (f2 x));
+
+  # Compose a list of functions,
+  # application is right to left.
+  rls = fs:
+    builtins.foldl' (fOut: f: lr f fOut) id fs;
+
+  # Simple function composition,
+  # application is left to right.
+  lr = f1: f2:
+    (x: f2 (f1 x));
+
+  # Compose a list of functions,
+  # application is left to right
+  lrs = x: fs:
+    builtins.foldl' (v: f: f v) x fs;
+
+  # Warning: cursed function
+  #
+  # Check if a function has an attribute
+  # set pattern with an ellipsis as its argument.
+  #
+  # s/o to puck for discovering that you could use
+  # builtins.toXML to introspect functions more than
+  # you should be able to in Nix.
+  hasEllipsis = f:
+    builtins.isFunction f &&
+    builtins.match ".*<attrspat ellipsis=\"1\">.*"
+      (builtins.toXML f) != null;
+
+  /* Return the number of arguments the given function accepts or 0 if the value
+     is not a function.
+
+     Example:
+
+       argCount argCount
+       => 1
+
+       argCount builtins.add
+       => 2
+
+       argCount pkgs.stdenv.mkDerivation
+       => 1
+  */
+  argCount = f:
+    let
+      # N.B. since we are only interested if the result of calling is a function
+      # as opposed to a normal value or evaluation failure, we never need to
+      # check success, as value will be false (i.e. not a function) in the
+      # failure case.
+      called = builtins.tryEval (
+        f (builtins.throw "You should never see this error message")
+      );
+    in
+    if !(builtins.isFunction f || builtins.isFunction (f.__functor or null))
+    then 0
+    else 1 + argCount called.value;
+
+  /* Call a given function with a given list of arguments.
+
+     Example:
+
+       apply builtins.sub [ 20 10 ]
+       => 10
+  */
+  apply = f: args:
+    builtins.foldl' (f: x: f x) f args;
+
+  # TODO(sterni): think of a better name for unapply
+  /* Collect n arguments into a list and pass them to the given function.
+     Allows calling a function that expects a list by feeding it the list
+     elements individually as function arguments - the limitation is
+     that the list must be of constant length.
+
+     This is mainly useful for functions that wrap other, arbitrary functions
+     in conjunction with argCount and apply, since lists of arguments are
+     easier to deal with usually.
+
+     Example:
+
+       (unapply 3 lib.id) 1 2 3
+       => [ 1 2 3 ]
+
+       (unapply 5 lib.reverse) 1 2 null 4 5
+       => [ 5 4 null 2 1 ]
+
+       # unapply and apply compose the identity relation together
+
+       unapply (argCount f) (apply f)
+       # is equivalent to f (if the function has a constant number of arguments)
+
+       (unapply 2 (apply builtins.sub)) 20 10
+       => 10
+  */
+  unapply =
+    let
+      unapply' = acc: n: f: x:
+        if n == 1
+        then f (acc ++ [ x ])
+        else unapply' (acc ++ [ x ]) (n - 1) f;
+    in
+    unapply' [ ];
+
+  /* Optimize a tail recursive Nix function by intercepting the recursive
+     function application and expressing it in terms of builtins.genericClosure
+     instead. The main benefit of this optimization is that even a naively
+     written recursive algorithm won't overflow the stack.
+
+     For this to work the following things prerequisites are necessary:
+
+     - The passed function needs to be a fix point for its self reference,
+       i. e. the argument to tailCallOpt needs to be of the form
+       `self: # function body that uses self to call itself`.
+       This is because tailCallOpt needs to manipulate the call to self
+       which otherwise wouldn't be possible due to Nix's lexical scoping.
+
+     - The passed function may only call itself as a tail call, all other
+       forms of recursions will fail evaluation.
+
+     This function was mainly written to prove that builtins.genericClosure
+     can be used to express any (tail) recursive algorithm. It can be used
+     to avoid stack overflows for deeply recursive, but naively written
+     functions (in the context of Nix this mainly means using recursion
+     instead of (ab)using more performant and less limited builtins).
+     A better alternative to using this function is probably translating
+     the algorithm to builtins.genericClosure manually. Also note that
+     using tailCallOpt doesn't mean that the stack won't ever overflow:
+     Data structures, especially lazy ones, can still cause all the
+     available stack space to be consumed.
+
+     The optimization also only concerns avoiding stack overflows,
+     tailCallOpt will make functions slower if anything.
+
+     Type: (F -> F) -> F where F is any tail recursive function.
+
+     Example:
+
+     let
+       label' = self: acc: n:
+         if n == 0
+         then "This is " + acc + "cursed."
+         else self (acc + "very ") (n - 1);
+
+       # Equivalent to a naive recursive implementation in Nix
+       label = (lib.fix label') "";
+
+       labelOpt = (tailCallOpt label') "";
+     in
+
+     label 5
+     => "This is very very very very very cursed."
+
+     labelOpt 5
+     => "This is very very very very very cursed."
+
+     label 10000
+     => error: stack overflow (possible infinite recursion)
+
+     labelOpt 10000
+     => "This is very very very very very very very very very…
+  */
+  tailCallOpt = f:
+    let
+      argc = argCount (lib.fix f);
+
+      # This function simulates being f for f's self reference. Instead of
+      # recursing, it will just return the arguments received as a specially
+      # tagged set, so the recursion step can be performed later.
+      fakef = unapply argc (args: {
+        __tailCall = true;
+        inherit args;
+      });
+      # Pass fakef to f so that it'll be called instead of recursing, ensuring
+      # only one recursion step is performed at a time.
+      encodedf = f fakef;
+
+      opt = args:
+        let
+          steps = builtins.genericClosure {
+            # This is how we encode a (tail) call: A set with final == false
+            # and the list of arguments to pass to be found in args.
+            startSet = [
+              {
+                key = "0";
+                id = 0;
+                final = false;
+                inherit args;
+              }
+            ];
+
+            operator =
+              { id, final, ... }@state:
+              let
+                # Plumbing to make genericClosure happy
+                newIds = {
+                  key = toString (id + 1);
+                  id = id + 1;
+                };
+
+                # Perform recursion step
+                call = apply encodedf state.args;
+
+                # If call encodes a new call, return the new encoded call,
+                # otherwise signal that we're done.
+                newState =
+                  if builtins.isAttrs call && call.__tailCall or false
+                  then newIds // {
+                    final = false;
+                    inherit (call) args;
+                  } else newIds // {
+                    final = true;
+                    value = call;
+                  };
+              in
+
+              if final
+              then [ ] # end condition for genericClosure
+              else [ newState ];
+          };
+        in
+        # The returned list contains intermediate steps we ignore.
+        (builtins.head (builtins.filter (x: x.final) steps)).value;
+    in
+    unapply argc opt;
+in
+
+{
+  inherit (lib)
+    fix
+    flip
+    const
+    ;
+
+  inherit
+    id
+    rl
+    rls
+    lr
+    lrs
+    hasEllipsis
+    argCount
+    tailCallOpt
+    apply
+    unapply
+    ;
+}
diff --git a/users/sterni/nix/fun/tests/default.nix b/users/sterni/nix/fun/tests/default.nix
new file mode 100644
index 0000000000..6b1e6fcc7b
--- /dev/null
+++ b/users/sterni/nix/fun/tests/default.nix
@@ -0,0 +1,82 @@
+{ depot, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    ;
+
+  inherit (depot.nix) escapeExecline;
+
+  inherit (depot.users.sterni.nix)
+    fun
+    ;
+
+  hasEllipsisTests = it "checks fun.hasEllipsis" [
+    (assertEq "Malicious string" false
+      (fun.hasEllipsis (builtins.toXML ({ foo, ... }: 12))))
+    (assertEq "No function" false
+      (fun.hasEllipsis 23))
+    (assertEq "No attribute set pattern" false
+      (fun.hasEllipsis (a: a + 2)))
+    (assertEq "No ellipsis" false
+      (fun.hasEllipsis ({ foo, bar }: foo + bar)))
+    (assertEq "Ellipsis" true
+      (fun.hasEllipsis ({ depot, pkgs, ... }: 42)))
+  ];
+
+  argCountTests = it "checks fun.argCount" [
+    (assertEq "builtins.sub has two arguments" 2
+      (fun.argCount builtins.sub))
+    (assertEq "fun.argCount has one argument" 1
+      (fun.argCount fun.argCount))
+    (assertEq "runTestsuite has two arguments" 2
+      (fun.argCount runTestsuite))
+  ];
+
+  applyTests = it "checks that fun.apply is equivalent to calling" [
+    (assertEq "fun.apply builtins.sub" (builtins.sub 23 42)
+      (fun.apply builtins.sub [ 23 42 ]))
+    (assertEq "fun.apply escapeExecline" (escapeExecline [ "foo" [ "bar" ] ])
+      (fun.apply escapeExecline [ [ "foo" [ "bar" ] ] ]))
+  ];
+
+  unapplyTests = it "checks fun.unapply" [
+    (assertEq "fun.unapply 3 accepts 3 args" 3
+      (fun.argCount (fun.unapply 3 fun.id)))
+    (assertEq "fun.unapply 73 accepts 73 args" 73
+      (fun.argCount (fun.unapply 73 fun.id)))
+    (assertEq "fun.unapply 1 accepts 73 args" 1
+      (fun.argCount (fun.unapply 1 fun.id)))
+    (assertEq "fun.unapply collects arguments correctly"
+      (fun.unapply 5 fun.id 1 2 3 4 5)
+      [ 1 2 3 4 5 ])
+    (assertEq "fun.unapply calls the given function correctly" 1
+      (fun.unapply 1 builtins.head 1))
+  ];
+
+  fac' = self: acc: n: if n == 0 then acc else self (n * acc) (n - 1);
+
+  facPlain = fun.fix fac' 1;
+  facOpt = fun.tailCallOpt fac' 1;
+
+  tailCallOptTests = it "checks fun.tailCallOpt" [
+    (assertEq "optimized and unoptimized factorial have the same base case"
+      (facPlain 0)
+      (facOpt 0))
+    (assertEq "optimized and unoptimized factorial have same value for 1"
+      (facPlain 1)
+      (facOpt 1))
+    (assertEq "optimized and unoptimized factorial have same value for 100"
+      (facPlain 100)
+      (facOpt 100))
+  ];
+in
+runTestsuite "nix.fun" [
+  hasEllipsisTests
+  argCountTests
+  applyTests
+  unapplyTests
+  tailCallOptTests
+]
diff --git a/users/sterni/nix/html/README.md b/users/sterni/nix/html/README.md
new file mode 100644
index 0000000000..0349e466a1
--- /dev/null
+++ b/users/sterni/nix/html/README.md
@@ -0,0 +1,148 @@
+# html.nix — _the_ most cursed Nix HTML DSL
+
+A quick example to show you what it looks like:
+
+```nix
+# Note: this example is for standalone usage out of depot
+{ pkgs ? import <nixpkgs> {} }:
+
+let
+  # zero dependency, one file implementation
+  htmlNix = import ./path/to/html.nix { };
+
+  # make the magic work
+  inherit (htmlNix) __findFile esc withDoctype;
+in
+
+pkgs.writeText "example.html" (withDoctype (<html> {} [
+  (<head> {} [
+    (<meta> { charset = "utf-8"; } null)
+    (<title> {} (esc "hello world"))
+  ])
+  (<body> {} [
+    (<h1> {} (esc "hello world"))
+    (<p> { class = "intro"; } (esc ''
+      welcome to the land of sillyness!
+    ''))
+    (<ul> {} [
+      (<li> {} [
+        (esc "check out ")
+        (<a> { href = "https://code.tvl.fyi"; } "depot")
+      ])
+      (<li> {} [
+        (esc "find ")
+        (<a> { href = "https://cl.tvl.fyi/q/hashtag:cursed"; } "cursed things")
+      ])
+    ])
+  ])
+]))
+```
+
+Convince yourself it works:
+
+```console
+$ $BROWSER $(nix-build example.nix)
+```
+
+Alternatively, in depot:
+
+```console
+$ $BROWSER $(nix-build -A users.sterni.nix.html.tests)
+```
+
+## Creating tags
+
+An empty tag is passed `null` as its content argument:
+
+```nix
+<link> {
+  rel = "stylesheet";
+  href = "/main.css";
+  type = "text/css";
+} null
+
+# => "<link href=\"/main.css\" rel=\"stylesheet\" type=\"text/css\"/>"
+```
+
+Content is expected to be HTML:
+
+```nix
+<div> { class = "foo"; } "<strong>hi</strong>"
+
+# => "<div class=\"foo\"><strong>hi</strong></div>"
+```
+
+If it's not, be sure to escape it:
+
+```nix
+<p> {} (esc "A => B")
+
+# => "<p>A =&gt; B</p>"
+```
+
+Nesting tags works of course:
+
+```nix
+<div> {} (<strong> {} (<em> {} "hi"))
+
+# => "<div><strong><em>hi</em></strong></div>"
+```
+
+If the content of a tag is a list, it's concatenated:
+
+```nix
+<h1> {} [
+  (esc "The ")
+  (<strong> {} "Nix")
+  (esc " ")
+  (<em> {} "Expression")
+  (esc " Language")
+]
+
+# => "<h1>The <strong>Nix</strong> <em>Expression</em> Language</h1>"
+```
+
+More detailed documentation can be found in `nixdoc`-compatible
+comments in the source file (`default.nix` in this directory).
+
+## How does this work?
+
+*Theoretically* expressions like `<nixpkgs>` are just ordinary paths —
+their actual value is determined from `NIX_PATH`. `html.nix` works
+because of how this is actually implemented: At [parse time][spath-parsing]
+Nix transparently translates an expression like `<foo>` into
+`__findFile __nixPath "foo"`:
+
+```
+nix-repl> <nixpkgs>
+/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs
+
+nix-repl> __findFile __nixPath "nixpkgs"
+/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs
+```
+
+This translation doesn't take any scoping issues into account --
+so we can just shadow `__findFile` and make it return anything,
+even a function:
+
+```
+nix-repl> __findFile = nixPath: str:
+            /**/ if str == "double" then x: x * 2
+            else if str == "triple" then x: x * 3
+            else throw "what?"
+
+nix-repl> <double> 2
+4
+
+nix-repl> <triple> 3
+9
+
+nix-repl> <quadruple> 4
+error: what?
+```
+
+Exactly this is what we are doing in `html.nix`:
+Using `let inherit (htmlNix) __findFile; in` we shadow the builtin `__findFile`
+with a function which returns a function rendering a particular HTML tag.
+
+[spath-parsing]: https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/parser.y#L410-L416
diff --git a/users/sterni/nix/html/default.nix b/users/sterni/nix/html/default.nix
new file mode 100644
index 0000000000..d25a7ab8da
--- /dev/null
+++ b/users/sterni/nix/html/default.nix
@@ -0,0 +1,122 @@
+# Copyright © 2021 sterni
+# SPDX-License-Identifier: MIT
+#
+# This file provides a cursed HTML DSL for nix which works by overloading
+# the NIX_PATH lookup operation via angle bracket operations, e. g. `<nixpkgs>`.
+
+{ ... }:
+
+let
+  /* Escape everything we have to escape in an HTML document if either
+     in a normal context or an attribute string (`<>&"'`).
+
+     A shorthand for this function called `esc` is also provided.
+
+     Type: string -> string
+
+     Example:
+
+     escapeMinimal "<hello>"
+     => "&lt;hello&gt;"
+  */
+  escapeMinimal = builtins.replaceStrings
+    [ "<" ">" "&" "\"" "'" ]
+    [ "&lt;" "&gt;" "&amp;" "&quot;" "&#039;" ];
+
+  /* Return a string with a correctly rendered tag of the given name,
+     with the given attributes which are automatically escaped.
+
+     If the content argument is `null`, the tag will have no children nor a
+     closing element. If the content argument is a string it is used as the
+     content as is (unescaped). If the content argument is a list, its
+     elements are concatenated.
+
+     `renderTag` is only an internal function which is reexposed as `__findFile`
+     to allow for much neater syntax than calling `renderTag` everywhere:
+
+     ```nix
+     { depot, ... }:
+     let
+       inherit (depot.users.sterni.nix.html) __findFile esc;
+     in
+
+     <html> {} [
+       (<head> {} (<title> {} (esc "hello world")))
+       (<body> {} [
+         (<h1> {} (esc "hello world"))
+         (<p> {} (esc "foo bar"))
+       ])
+     ]
+
+     ```
+
+     As you can see, the need to call a function disappears, instead the
+     `NIX_PATH` lookup operation via `<foo>` is overloaded, so it becomes
+     `renderTag "foo"` automatically.
+
+     Since the content argument may contain the result of other `renderTag`
+     calls, we can't escape it automatically. Instead this must be done manually
+     using `esc`.
+
+     Type: string -> attrs<string> -> (list<string> | string | null) -> string
+
+     Example:
+
+     <link> {
+       rel = "stylesheet";
+       href = "/css/main.css";
+       type = "text/css";
+     } null
+
+     renderTag "link" {
+       rel = "stylesheet";
+       href = "/css/main.css";
+       type = "text/css";
+     } null
+
+     => "<link href=\"/css/main.css\" rel=\"stylesheet\" type=\"text/css\"/>"
+
+     <p> {} [
+       "foo "
+       (<strong> {} "bar")
+     ]
+
+     renderTag "p" {} "foo <strong>bar</strong>"
+     => "<p>foo <strong>bar</strong></p>"
+  */
+  renderTag = tag: attrs: content:
+    let
+      attrs' = builtins.concatStringsSep "" (
+        builtins.map
+          (n:
+            " ${escapeMinimal n}=\"${escapeMinimal (toString attrs.${n})}\""
+          )
+          (builtins.attrNames attrs)
+      );
+      content' =
+        if builtins.isList content
+        then builtins.concatStringsSep "" content
+        else content;
+    in
+    if content == null
+    then "<${tag}${attrs'}/>"
+    else "<${tag}${attrs'}>${content'}</${tag}>";
+
+  /* Prepend "<!DOCTYPE html>" to a string.
+
+     Type: string -> string
+
+     Example:
+
+     withDoctype (<body> {} (esc "hello"))
+     => "<!DOCTYPE html><body>hello</body>"
+  */
+  withDoctype = doc: "<!DOCTYPE html>" + doc;
+
+in
+{
+  inherit escapeMinimal renderTag withDoctype;
+
+  __findFile = _: renderTag;
+  esc = escapeMinimal;
+}
diff --git a/users/sterni/nix/html/tests/default.nix b/users/sterni/nix/html/tests/default.nix
new file mode 100644
index 0000000000..0d80f2f1cd
--- /dev/null
+++ b/users/sterni/nix/html/tests/default.nix
@@ -0,0 +1,93 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.users.sterni.nix.html)
+    __findFile
+    esc
+    withDoctype
+    ;
+
+  exampleDocument = withDoctype (<html> { lang = "en"; } [
+    (<head> { } [
+      (<meta> { charset = "utf-8"; } null)
+      (<title> { } "html.nix example document")
+      (<link>
+        {
+          rel = "license";
+          href = "https://code.tvl.fyi/about/LICENSE";
+          type = "text/html";
+        }
+        null)
+      (<style> { } (esc ''
+        hgroup h2 {
+          font-weight: normal;
+        }
+
+        dd {
+          margin: 0;
+        }
+      ''))
+    ])
+    (<body> { } [
+      (<main> { } [
+        (<hgroup> { } [
+          (<h1> { } (esc "html.nix"))
+          (<h2> { } [
+            (<em> { } "the")
+            (esc " most cursed HTML DSL ever!")
+          ])
+        ])
+        (<dl> { } [
+          (<dt> { } [
+            (esc "Q: Wait, it's all ")
+            (<a>
+              {
+                href = "https://cl.tvl.fyi/q/hashtag:cursed";
+              }
+              (esc "cursed"))
+            (esc " nix hacks?")
+          ])
+          (<dd> { } (esc "A: Always has been. 🔫"))
+          (<dt> { } (esc "Q: Why does this work?"))
+          (<dd> { } [
+            (esc "Because nix ")
+            (<a>
+              {
+                href = "https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/parser.y#L410-L416";
+              }
+              (esc "translates "))
+            (<a>
+              {
+                href = "https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/lexer.l#L100";
+              }
+              (esc "SPATH tokens"))
+            (esc " like ")
+            (<code> { } (esc "<nixpkgs>"))
+            (esc " into calls to ")
+            (<code> { } (esc "__findFile"))
+            (esc " in the ")
+            (<em> { } (esc "current"))
+            (esc " scope.")
+          ])
+        ])
+      ])
+    ])
+  ]);
+in
+
+pkgs.runCommandNoCC "html.nix.html"
+{
+  passAsFile = [ "exampleDocument" ];
+  inherit exampleDocument;
+  nativeBuildInputs = [ pkgs.html5validator ];
+} ''
+  set -x
+  test "${esc "<> && \" \'"}" = "&lt;&gt; &amp;&amp; &quot; &#039;"
+
+  # slow as hell unfortunately
+  html5validator "$exampleDocumentPath"
+
+  mv "$exampleDocumentPath" "$out"
+
+  set +x
+''
diff --git a/users/sterni/nix/int/default.nix b/users/sterni/nix/int/default.nix
new file mode 100644
index 0000000000..54b5596472
--- /dev/null
+++ b/users/sterni/nix/int/default.nix
@@ -0,0 +1,126 @@
+{ depot, lib, ... }:
+
+let
+
+  # TODO(sterni): implement nix.float and figure out which of these
+  #               functions can be split out into a common nix.num
+  #               library.
+
+  inherit (depot.users.sterni.nix)
+    string
+    ;
+
+  inherit (builtins)
+    bitOr
+    bitAnd
+    bitXor
+    mul
+    div
+    add
+    sub
+    ;
+
+  abs = i: if i < 0 then -i else i;
+
+  exp = base: pow:
+    if pow > 0
+    then base * (exp base (pow - 1))
+    else if pow < 0
+    then 1.0 / exp base (abs pow)
+    else 1;
+
+  bitShiftR = bit: count:
+    if count == 0
+    then bit
+    else div (bitShiftR bit (count - 1)) 2;
+
+  bitShiftL = bit: count:
+    if count == 0
+    then bit
+    else 2 * (bitShiftL bit (count - 1));
+
+  hexdigits = "0123456789ABCDEF";
+
+  toHex = int:
+    let
+      go = i:
+        if i == 0
+        then ""
+        else go (bitShiftR i 4)
+          + string.charAt (bitAnd i 15) hexdigits;
+      sign = lib.optionalString (int < 0) "-";
+    in
+    if int == 0
+    then "0"
+    else "${sign}${go (abs int)}";
+
+  fromHexMap = builtins.listToAttrs
+    (lib.imap0 (i: c: { name = c; value = i; })
+      (lib.stringToCharacters hexdigits));
+
+  fromHex = literal:
+    let
+      negative = string.charAt 0 literal == "-";
+      start = if negative then 1 else 0;
+      len = builtins.stringLength literal;
+      # reversed list of all digits
+      digits = builtins.genList
+        (i: string.charAt (len - 1 - i) literal)
+        (len - start);
+      parsed = builtins.foldl'
+        (v: d: {
+          val = v.val + (fromHexMap."${d}" * v.mul);
+          mul = v.mul * 16;
+        })
+        { val = 0; mul = 1; }
+        digits;
+    in
+    if negative
+    then -parsed.val
+    else parsed.val;
+
+  # A nix integer is a 64bit signed integer
+  maxBound = 9223372036854775807;
+
+  # fun fact: -9223372036854775808 is the lower bound
+  # for a nix integer (as you would expect), but you can't
+  # use it as an integer literal or you'll be greeted with:
+  # error: invalid integer '9223372036854775808'
+  # This is because all int literals when parsing are
+  # positive, negative "literals" are positive literals
+  # which are preceded by the arithmetric negation operator.
+  minBound = -9223372036854775807 - 1;
+
+  odd = x: bitAnd x 1 == 1;
+  even = x: bitAnd x 1 == 0;
+
+  # div and mod behave like quot and rem in Haskell,
+  # i. e. they truncate towards 0
+  mod = a: b: let res = a / b; in a - (res * b);
+
+  inRange = a: b: x: x >= a && x <= b;
+
+in
+{
+  inherit
+    maxBound
+    minBound
+    abs
+    exp
+    odd
+    even
+    add
+    sub
+    mul
+    div
+    mod
+    bitShiftR
+    bitShiftL
+    bitOr
+    bitAnd
+    bitXor
+    toHex
+    fromHex
+    inRange
+    ;
+}
diff --git a/users/sterni/nix/int/tests/default.nix b/users/sterni/nix/int/tests/default.nix
new file mode 100644
index 0000000000..8d2263b421
--- /dev/null
+++ b/users/sterni/nix/int/tests/default.nix
@@ -0,0 +1,459 @@
+{ depot, lib, ... }:
+
+let
+
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    ;
+
+  inherit (depot.users.sterni.nix)
+    int
+    string
+    fun
+    ;
+
+  testBounds = it "checks minBound and maxBound" [
+    # this is gonna blow up in my face because
+    # integer overflow is undefined behavior in
+    # C++, so most likely anything could happen?
+    (assertEq "maxBound is the maxBound" true
+      (int.maxBound + 1 < int.maxBound))
+    (assertEq "minBound is the minBound" true
+      (int.minBound - 1 > int.minBound))
+    (assertEq "maxBound overflows to minBound"
+      (int.maxBound + 1)
+      int.minBound)
+    (assertEq "minBound overflows to maxBound"
+      (int.minBound - 1)
+      int.maxBound)
+  ];
+
+  expectedBytes = [
+    "00"
+    "01"
+    "02"
+    "03"
+    "04"
+    "05"
+    "06"
+    "07"
+    "08"
+    "09"
+    "0A"
+    "0B"
+    "0C"
+    "0D"
+    "0E"
+    "0F"
+    "10"
+    "11"
+    "12"
+    "13"
+    "14"
+    "15"
+    "16"
+    "17"
+    "18"
+    "19"
+    "1A"
+    "1B"
+    "1C"
+    "1D"
+    "1E"
+    "1F"
+    "20"
+    "21"
+    "22"
+    "23"
+    "24"
+    "25"
+    "26"
+    "27"
+    "28"
+    "29"
+    "2A"
+    "2B"
+    "2C"
+    "2D"
+    "2E"
+    "2F"
+    "30"
+    "31"
+    "32"
+    "33"
+    "34"
+    "35"
+    "36"
+    "37"
+    "38"
+    "39"
+    "3A"
+    "3B"
+    "3C"
+    "3D"
+    "3E"
+    "3F"
+    "40"
+    "41"
+    "42"
+    "43"
+    "44"
+    "45"
+    "46"
+    "47"
+    "48"
+    "49"
+    "4A"
+    "4B"
+    "4C"
+    "4D"
+    "4E"
+    "4F"
+    "50"
+    "51"
+    "52"
+    "53"
+    "54"
+    "55"
+    "56"
+    "57"
+    "58"
+    "59"
+    "5A"
+    "5B"
+    "5C"
+    "5D"
+    "5E"
+    "5F"
+    "60"
+    "61"
+    "62"
+    "63"
+    "64"
+    "65"
+    "66"
+    "67"
+    "68"
+    "69"
+    "6A"
+    "6B"
+    "6C"
+    "6D"
+    "6E"
+    "6F"
+    "70"
+    "71"
+    "72"
+    "73"
+    "74"
+    "75"
+    "76"
+    "77"
+    "78"
+    "79"
+    "7A"
+    "7B"
+    "7C"
+    "7D"
+    "7E"
+    "7F"
+    "80"
+    "81"
+    "82"
+    "83"
+    "84"
+    "85"
+    "86"
+    "87"
+    "88"
+    "89"
+    "8A"
+    "8B"
+    "8C"
+    "8D"
+    "8E"
+    "8F"
+    "90"
+    "91"
+    "92"
+    "93"
+    "94"
+    "95"
+    "96"
+    "97"
+    "98"
+    "99"
+    "9A"
+    "9B"
+    "9C"
+    "9D"
+    "9E"
+    "9F"
+    "A0"
+    "A1"
+    "A2"
+    "A3"
+    "A4"
+    "A5"
+    "A6"
+    "A7"
+    "A8"
+    "A9"
+    "AA"
+    "AB"
+    "AC"
+    "AD"
+    "AE"
+    "AF"
+    "B0"
+    "B1"
+    "B2"
+    "B3"
+    "B4"
+    "B5"
+    "B6"
+    "B7"
+    "B8"
+    "B9"
+    "BA"
+    "BB"
+    "BC"
+    "BD"
+    "BE"
+    "BF"
+    "C0"
+    "C1"
+    "C2"
+    "C3"
+    "C4"
+    "C5"
+    "C6"
+    "C7"
+    "C8"
+    "C9"
+    "CA"
+    "CB"
+    "CC"
+    "CD"
+    "CE"
+    "CF"
+    "D0"
+    "D1"
+    "D2"
+    "D3"
+    "D4"
+    "D5"
+    "D6"
+    "D7"
+    "D8"
+    "D9"
+    "DA"
+    "DB"
+    "DC"
+    "DD"
+    "DE"
+    "DF"
+    "E0"
+    "E1"
+    "E2"
+    "E3"
+    "E4"
+    "E5"
+    "E6"
+    "E7"
+    "E8"
+    "E9"
+    "EA"
+    "EB"
+    "EC"
+    "ED"
+    "EE"
+    "EF"
+    "F0"
+    "F1"
+    "F2"
+    "F3"
+    "F4"
+    "F5"
+    "F6"
+    "F7"
+    "F8"
+    "F9"
+    "FA"
+    "FB"
+    "FC"
+    "FD"
+    "FE"
+    "FF"
+  ];
+
+  hexByte = i: string.fit { width = 2; char = "0"; } (int.toHex i);
+
+  hexInts = [
+    { left = 0; right = "0"; }
+    { left = 1; right = "1"; }
+    { left = 11; right = "B"; }
+    { left = 123; right = "7B"; }
+    { left = 9000; right = "2328"; }
+    { left = 2323; right = "913"; }
+    { left = 4096; right = "1000"; }
+    { left = int.maxBound; right = "7FFFFFFFFFFFFFFF"; }
+    { left = int.minBound; right = "-8000000000000000"; }
+  ];
+
+  testHex = it "checks conversion to hex" (lib.flatten [
+    (lib.imap0
+      (i: hex: [
+        (assertEq "hexByte ${toString i} == ${hex}" (hexByte i) hex)
+        (assertEq "${toString i} == fromHex ${hex}" i (int.fromHex hex))
+      ])
+      expectedBytes)
+    (builtins.map
+      ({ left, right }: [
+        (assertEq "toHex ${toString left} == ${right}" (int.toHex left) right)
+        (assertEq "${toString left} == fromHex ${right}" left (int.fromHex right))
+      ])
+      hexInts)
+  ]);
+
+  testBasic = it "checks basic int operations" [
+    (assertEq "122 is even" (int.even 122 && !(int.odd 122)) true)
+    (assertEq "123 is odd" (int.odd 123 && !(int.even 123)) true)
+    (assertEq "abs -4959" (int.abs (-4959)) 4959)
+  ];
+
+  expNumbers = [
+    { left = -3; right = 0.125; }
+    { left = -2; right = 0.25; }
+    { left = -1; right = 0.5; }
+    { left = 0; right = 1; }
+    { left = 1; right = 2; }
+    { left = 2; right = 4; }
+    { left = 3; right = 8; }
+    { left = 4; right = 16; }
+    { left = 5; right = 32; }
+    { left = 16; right = 65536; }
+  ];
+
+  testExp = it "checks exponentiation"
+    (builtins.map
+      ({ left, right }:
+        assertEq
+          "2 ^ ${toString left} == ${toString right}"
+          (int.exp 2 left)
+          right)
+      expNumbers);
+
+  shifts = [
+    { a = 2; b = 5; c = 64; op = "<<"; }
+    { a = -2; b = 5; c = -64; op = "<<"; }
+    { a = 123; b = 4; c = 1968; op = "<<"; }
+    { a = 1; b = 8; c = 256; op = "<<"; }
+    { a = 256; b = 8; c = 1; op = ">>"; }
+    { a = 374; b = 2; c = 93; op = ">>"; }
+    { a = 2; b = 2; c = 0; op = ">>"; }
+    { a = 99; b = 9; c = 0; op = ">>"; }
+  ];
+
+  checkShift = { a, b, c, op }@args:
+    let
+      f = string.match op {
+        "<<" = int.bitShiftL;
+        ">>" = int.bitShiftR;
+      };
+    in
+    assertEq "${toString a} ${op} ${toString b} == ${toString c}" (f a b) c;
+
+  checkShiftRDivExp = n:
+    assertEq "${toString n} >> 5 == ${toString n} / 2 ^ 5"
+      (int.bitShiftR n 5)
+      (int.div n (int.exp 2 5));
+
+  checkShiftLMulExp = n:
+    assertEq "${toString n} >> 6 == ${toString n} * 2 ^ 6"
+      (int.bitShiftL n 5)
+      (int.mul n (int.exp 2 5));
+
+  testBit = it "checks bitwise operations" (lib.flatten [
+    (builtins.map checkShift shifts)
+    (builtins.map checkShiftRDivExp [
+      1
+      2
+      3
+      5
+      7
+      23
+      1623
+      238
+      34
+      348
+      2834
+      834
+      348
+    ])
+    (builtins.map checkShiftLMulExp [
+      1
+      2
+      3
+      5
+      7
+      23
+      384
+      3
+      2
+      5991
+      85109
+      38
+    ])
+  ]);
+
+  divisions = [
+    { a = 2; b = 1; c = 2; mod = 0; }
+    { a = 2; b = 2; c = 1; mod = 0; }
+    { a = 20; b = 10; c = 2; mod = 0; }
+    { a = 12; b = 5; c = 2; mod = 2; }
+    { a = 23; b = 4; c = 5; mod = 3; }
+  ];
+
+  checkDiv = n: { a, b, c, mod }: [
+    (assertEq "${n}: div result" (int.div a b) c)
+    (assertEq "${n}: mod result" (int.mod a b) mod)
+    (assertEq "${n}: divMod law" ((int.div a b) * b + (int.mod a b)) a)
+  ];
+
+  testDivMod = it "checks integer division and modulo"
+    (lib.flatten [
+      (builtins.map (checkDiv "+a / +b") divisions)
+      (builtins.map
+        (fun.rl (checkDiv "-a / +b") (x: x // {
+          a = -x.a;
+          c = -x.c;
+          mod = -x.mod;
+        }))
+        divisions)
+      (builtins.map
+        (fun.rl (checkDiv "+a / -b") (x: x // {
+          b = -x.b;
+          c = -x.c;
+        }))
+        divisions)
+      (builtins.map
+        (fun.rl (checkDiv "-a / -b") (x: x // {
+          a = -x.a;
+          b = -x.b;
+          mod = -x.mod;
+        }))
+        divisions)
+    ]);
+
+in
+runTestsuite "nix.int" [
+  testBounds
+  testHex
+  testBasic
+  testExp
+  testBit
+  testDivMod
+]
diff --git a/users/sterni/nix/string/default.nix b/users/sterni/nix/string/default.nix
new file mode 100644
index 0000000000..852ef2538f
--- /dev/null
+++ b/users/sterni/nix/string/default.nix
@@ -0,0 +1,122 @@
+{ depot, lib, ... }:
+
+let
+
+  inherit (depot.users.sterni.nix.char)
+    chr
+    ord
+    ;
+
+  inherit (depot.users.sterni.nix)
+    int
+    flow
+    ;
+
+  take = n: s:
+    builtins.substring 0 n s;
+
+  drop = n: s:
+    builtins.substring n int.maxBound s;
+
+  charAt = i: s:
+    let
+      r = builtins.substring i 1 s;
+    in
+    if r == "" then null else r;
+
+  charIndex = char: s:
+    let
+      len = builtins.stringLength s;
+      go = i:
+        flow.cond [
+          [ (i >= len) null ]
+          [ (charAt i s == char) i ]
+          [ true (go (i + 1)) ]
+        ];
+    in
+    go 0;
+
+  toChars = lib.stringToCharacters;
+  fromChars = lib.concatStrings;
+
+  toBytes = str:
+    builtins.map ord (toChars str);
+
+  fromBytes = is: lib.concatMapStrings chr is;
+
+  pad = { left ? 0, right ? 0, char ? " " }: s:
+    let
+      leftS = fromChars (builtins.genList (_: char) left);
+      rightS = fromChars (builtins.genList (_: char) right);
+    in
+    "${leftS}${s}${rightS}";
+
+  fit = { char ? " ", width, side ? "left" }: s:
+    let
+      diff = width - builtins.stringLength s;
+    in
+    if diff <= 0
+    then s
+    else pad { inherit char; "${side}" = diff; } s;
+
+  # pattern matching for strings only
+  match = val: matcher: matcher."${val}";
+
+  /* Bare-bones printf implementation. Supported format specifiers:
+
+     * `%%` escapes `%`
+     * `%s` is substituted by a string
+
+     As expected, the first argument is a format string and the values
+     for its format specifiers need to provided as the next arguments
+     in order.
+
+     Type: string -> (printfVal : either string (a -> printfVal))
+  */
+  printf = formatString:
+    let
+      specifierWithArg = token: builtins.elem token [
+        "%s"
+      ];
+      isSpecifier = lib.hasPrefix "%";
+
+      tokens = lib.flatten (builtins.split "(%.)" formatString);
+      argsNeeded = builtins.length (builtins.filter specifierWithArg tokens);
+
+      format = args: (builtins.foldl'
+        ({ out ? "", argIndex ? 0 }: token: {
+          argIndex = argIndex + (if specifierWithArg token then 1 else 0);
+          out =
+            /**/
+            if token == "%s" then out + builtins.elemAt args argIndex
+            else if token == "%%" then out + "%"
+            else if isSpecifier token then throw "Unsupported format specifier ${token}"
+            else out + token;
+        })
+        { }
+        tokens).out;
+
+      accumulateArgs = argCount: args:
+        if argCount > 0
+        then arg: accumulateArgs (argCount - 1) (args ++ [ arg ])
+        else format args;
+    in
+    accumulateArgs argsNeeded [ ];
+
+in
+{
+  inherit
+    take
+    drop
+    charAt
+    charIndex
+    toBytes
+    fromBytes
+    toChars
+    fromChars
+    pad
+    fit
+    match
+    printf
+    ;
+}
diff --git a/users/sterni/nix/string/tests/default.nix b/users/sterni/nix/string/tests/default.nix
new file mode 100644
index 0000000000..e9015e95dc
--- /dev/null
+++ b/users/sterni/nix/string/tests/default.nix
@@ -0,0 +1,72 @@
+{ depot, ... }:
+
+let
+
+  inherit (depot.users.sterni.nix)
+    string
+    ;
+
+  inherit (depot.nix.runTestsuite)
+    it
+    assertEq
+    runTestsuite
+    ;
+
+  testTakeDrop = it "tests take and drop" [
+    (assertEq "take"
+      (string.take 5 "five and more")
+      "five ")
+    (assertEq "drop"
+      (string.drop 2 "coin")
+      "in")
+    (assertEq "take out of bounds"
+      (string.take 100 "foo")
+      "foo")
+    (assertEq "drop out of bounds"
+      (string.drop 42 "lol")
+      "")
+  ];
+
+  testIndexing = it "tests string indexing" [
+    (assertEq "normal charAt"
+      (string.charAt 3 "helo")
+      "o")
+    (assertEq "out of bounds charAt"
+      (string.charAt 5 "helo")
+      null)
+  ];
+
+  testFinding = it "tests finding in strings" [
+    (assertEq "normal charIndex"
+      (string.charIndex "d" "abcdefghijkl")
+      3)
+    (assertEq "charIndex no match"
+      (string.charIndex "w" "zZzZzzzZZZ")
+      null)
+  ];
+
+  dontEval = builtins.throw "this should not get evaluated";
+
+  testMatch = it "tests match" [
+    (assertEq "basic match usage" 42
+      (string.match "answer" {
+        "answer" = 42;
+        "banana" = dontEval;
+        "maleur" = dontEval;
+      }))
+  ];
+
+  f = "f";
+  testPrintf = it "prints f" [
+    (assertEq "basic %s usage" "print ${f}" (string.printf "print %s" f))
+    (assertEq "% escaping" "100%" (string.printf "100%%"))
+  ];
+
+in
+runTestsuite "nix.string" [
+  testTakeDrop
+  testIndexing
+  testFinding
+  testMatch
+  testPrintf
+]
diff --git a/users/sterni/nix/url/default.nix b/users/sterni/nix/url/default.nix
new file mode 100644
index 0000000000..4a401873a1
--- /dev/null
+++ b/users/sterni/nix/url/default.nix
@@ -0,0 +1,100 @@
+{ depot, lib, ... }:
+
+let
+
+  inherit (depot.users.sterni.nix)
+    char
+    int
+    string
+    flow
+    ;
+
+  reserved = c: builtins.elem c [
+    "!"
+    "#"
+    "$"
+    "&"
+    "'"
+    "("
+    ")"
+    "*"
+    "+"
+    ","
+    "/"
+    ":"
+    ";"
+    "="
+    "?"
+    "@"
+    "["
+    "]"
+  ];
+
+  unreserved = c: char.asciiAlphaNum c
+    || builtins.elem c [ "-" "_" "." "~" ];
+
+  percentEncode = c:
+    if unreserved c
+    then c
+    else "%" + (string.fit
+      {
+        width = 2;
+        char = "0";
+        side = "left";
+      }
+      (int.toHex (char.ord c)));
+
+  encode = { leaveReserved ? false }: s:
+    let
+      chars = lib.stringToCharacters s;
+      tr = c:
+        if leaveReserved && reserved c
+        then c
+        else percentEncode c;
+    in
+    lib.concatStrings (builtins.map tr chars);
+
+  decode = s:
+    let
+      tokens = builtins.split "%" s;
+      decodeStep =
+        { result ? ""
+        , inPercent ? false
+        }: s:
+        flow.cond [
+          [
+            (builtins.isList s)
+            {
+              inherit result;
+              inPercent = true;
+            }
+          ]
+          [
+            inPercent
+            {
+              inPercent = false;
+              # first two characters came after an %
+              # the rest is the string until the next %
+              result = result
+                + char.chr (int.fromHex (string.take 2 s))
+                + (string.drop 2 s);
+            }
+          ]
+          [
+            (!inPercent)
+            {
+              result = result + s;
+            }
+          ]
+        ];
+
+    in
+    (builtins.foldl' decodeStep { } tokens).result;
+
+in
+{
+  inherit
+    encode
+    decode
+    ;
+}
diff --git a/users/sterni/nix/url/tests/default.nix b/users/sterni/nix/url/tests/default.nix
new file mode 100644
index 0000000000..4eb6f95ccd
--- /dev/null
+++ b/users/sterni/nix/url/tests/default.nix
@@ -0,0 +1,58 @@
+{ depot, ... }:
+
+let
+
+  inherit (depot.nix.runTestsuite)
+    it
+    assertEq
+    runTestsuite
+    ;
+
+  inherit (depot.users.sterni.nix)
+    url
+    ;
+
+  checkEncoding = args: { left, right }:
+    assertEq "encode ${builtins.toJSON left} == ${builtins.toJSON right}"
+      (url.encode args left)
+      right;
+
+  checkDecoding = { left, right }:
+    assertEq "${builtins.toJSON left} == decode ${builtins.toJSON right}"
+      (url.decode left)
+      right;
+
+  unreserved = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_~";
+
+  encodeExpected = [
+    { left = "Laguna Beach"; right = "Laguna%20Beach"; }
+    { left = "👾 Exterminate!"; right = "%F0%9F%91%BE%20Exterminate%21"; }
+    { left = unreserved; right = unreserved; }
+    {
+      left = "`!@#$%^&*()+={}[]:;'\\|<>,?/ \"";
+      right = "%60%21%40%23%24%25%5E%26%2A%28%29%2B%3D%7B%7D%5B%5D%3A%3B%27%5C%7C%3C%3E%2C%3F%2F%20%22";
+    }
+  ];
+
+  testEncode = it "checks url.encode"
+    (builtins.map (checkEncoding { }) encodeExpected);
+
+  testDecode = it "checks url.decode"
+    (builtins.map checkDecoding encodeExpected);
+
+  testLeaveReserved = it "checks that leaveReserved is like id for valid URLs"
+    (builtins.map (x: checkEncoding { leaveReserved = true; } { left = x; right = x; }) [
+      "ftp://ftp.is.co.za/rfc/rfc1808.txt"
+      "http://www.ietf.org/rfc/rfc2396.txt"
+      "ldap://[2001:db8::7]/c=GB?objectClass?one"
+      "mailto:John.Doe@example.com"
+      "news:comp.infosystems.www.servers.unix"
+      "tel:+1-816-555-1212"
+      "telnet://192.0.2.16:80/"
+      "urn:oasis:names:specification:docbook:dtd:xml:4.1.2"
+    ]);
+in
+runTestsuite "nix.url" [
+  testEncode
+  testLeaveReserved
+]
diff --git a/users/sterni/nix/utf8/default.nix b/users/sterni/nix/utf8/default.nix
new file mode 100644
index 0000000000..71c846c042
--- /dev/null
+++ b/users/sterni/nix/utf8/default.nix
@@ -0,0 +1,325 @@
+{ depot, lib, ... }:
+
+let
+
+  inherit (depot.users.sterni.nix)
+    char
+    flow
+    fun
+    int
+    string
+    util
+    ;
+
+  /* (Internal) function to determine the amount
+     bytes left in a UTF-8 byte sequence from the
+     first byte.
+
+     This function will throw if the given first
+     byte is ill-formed, but will not detect all
+     cases of ill-formed-ness.
+
+     Based on table 3-6. from The Unicode Standard,
+     Version 13.0, section 3.9.
+
+     Type: integer -> integer
+  */
+  byteCount = i: flow.cond [
+    [ (int.bitAnd i 128 == 0) 1 ]
+    [ (int.bitAnd i 224 == 192) 2 ]
+    [ (int.bitAnd i 240 == 224) 3 ]
+    [ (int.bitAnd i 248 == 240) 4 ]
+    [ true (builtins.throw "Ill-formed first byte ${int.toHex i}") ]
+  ];
+
+  /* (Internal) function to check if a given byte in
+     an UTF-8 byte sequence is well-formed.
+
+     Based on table 3-7. from The Unicode Standard,
+     Version 13.0, section 3.9.
+
+     Type: integer -> integer -> integer -> bool
+  */
+  wellFormedByte =
+    # first byte's integer value
+    first:
+    # byte position as an index starting with 0
+    pos:
+    let
+      defaultRange = int.inRange 128 191;
+
+      secondBytePredicate = flow.switch first [
+        [ (int.inRange 194 223) defaultRange ] # C2..DF
+        [ 224 (int.inRange 160 191) ] # E0
+        [ (int.inRange 225 236) defaultRange ] # E1..EC
+        [ 237 (int.inRange 128 159) ] # ED
+        [ (int.inRange 238 239) defaultRange ] # EE..EF
+        [ 240 (int.inRange 144 191) ] # F0
+        [ (int.inRange 241 243) defaultRange ] # F1..F3
+        [ 244 (int.inRange 128 143) ] # F4
+        [ (fun.const true) null ]
+      ];
+
+      firstBytePredicate = byte: assert first == byte;
+        first < 128 || secondBytePredicate != null;
+    in
+    # Either ASCII or in one of the byte ranges of Table 3-6.
+    if pos == 0 then firstBytePredicate
+    # return predicate according to Table 3-6.
+    else if pos == 1 then assert secondBytePredicate != null; secondBytePredicate
+    # 3rd and 4th byte have only one validity rule
+    else defaultRange;
+
+  /* Iteration step for decoding an UTF-8 byte sequence.
+     It decodes incrementally, i. e. it has to be fed
+     one byte at a time and then returns either a
+     new state or a final result.
+
+     If the resulting attribute set contains the attribute
+     result, it is finished and the decoded codepoint is
+     contained in that attribute. In all other cases,
+     pass the returned set to step again along with
+     a new byte. The initial state to pass is the empty
+     set.
+
+     Extra attributes are always passed through, so you
+     can pass extra state. Be sure not to use result,
+     pos, code, first or count.
+
+     This function will throw with a fairly detailed
+     message if it encounters ill-formed bytes.
+
+     The implementation is based on The Unicode Standard,
+     Version 13.0, section 3.9, especially table 3-6.
+
+     Type: { ... } -> string -> ({ result :: integer, ... } | { ... })
+
+     Example: utf8.step {} "f"
+     => { result = 102; }
+  */
+  step = { pos ? 0, code ? 0, ... }@args: byte:
+    let
+      value = char.ord byte;
+      # first byte is context for well-formed-ness
+      first = args.first or value;
+      count = args.count or (byteCount first);
+      newCode =
+        if count == 1
+        then int.bitAnd 127 first # ascii character
+        else # multi byte UTF-8 sequence
+          let
+            # Calculate the bitmask for extracting the
+            # codepoint data in the current byte.
+            # If the codepoint is not ASCII, the bits
+            # used for codepoint data differ depending
+            # on the byte position and overall byte
+            # count. The first byte always ignores
+            # the (count + 1) most significant bits.
+            # For all subsequent bytes, the 2 most
+            # significant bits need to be ignored.
+            # See also table 3-6.
+            mask =
+              if pos == 0
+              then int.exp 2 (8 - (count + 1)) - 1
+              else 63;
+            # UTF-8 uses the 6 least significant bits in all
+            # subsequent bytes after the first one. Therefore
+            # We can determine the amount we need to shift
+            # the current value by the amount of bytes left.
+            offset = (count - (pos + 1)) * 6;
+          in
+          code + (int.bitShiftL (int.bitAnd mask value) offset);
+      illFormedMsg =
+        "Ill-formed byte ${int.toHex value} at position ${toString pos} in ${toString count} byte UTF-8 sequence";
+    in
+    if !(wellFormedByte first pos value) then builtins.throw illFormedMsg
+    else if pos + 1 == count
+    then (builtins.removeAttrs args [
+      # allow extra state being passed through
+      "count"
+      "code"
+      "pos"
+      "first"
+    ]) // { result = newCode; }
+    else (builtins.removeAttrs args [ "result" ]) // {
+      inherit count first;
+      code = newCode;
+      pos = pos + 1;
+    };
+
+  /* Decode an UTF-8 string into a list of codepoints.
+
+     Throws if the string is ill-formed UTF-8.
+
+     Type: string -> [ integer ]
+  */
+  # TODO(sterni): option to fallback to replacement char instead of failure
+  decode = s:
+    let
+      stringLength = builtins.stringLength s;
+      iterResult = builtins.genericClosure {
+        startSet = [
+          {
+            key = "start";
+            stringIndex = -1;
+            state = { };
+            codepoint = null;
+          }
+        ];
+        operator = { state, stringIndex, ... }:
+          let
+            # updated values for current iteration step
+            newIndex = stringIndex + 1;
+            newState = step state (builtins.substring newIndex 1 s);
+          in
+          lib.optional (newIndex < stringLength) {
+            # unique keys to make genericClosure happy
+            key = toString newIndex;
+            # carryover state for the next step
+            stringIndex = newIndex;
+            state = newState;
+            # actual payload for later, steps with value null are filtered out
+            codepoint = newState.result or null;
+          };
+      };
+    in
+    # extract all steps that yield a code point into a list
+    builtins.map (v: v.codepoint) (
+      builtins.filter
+        (
+          { codepoint, stringIndex, state, ... }:
+
+          let
+            # error message in case we are missing bytes at the end of input
+            earlyEndMsg =
+              if state ? count && state ? pos
+              then "Missing ${toString (with state; count - pos)} bytes at end of input"
+              else "Unexpected end of input";
+          in
+
+          # filter out all iteration steps without a codepoint value
+          codepoint != null
+          # if we are at the iteration step of a non-empty input string, throw
+          # an error if no codepoint was returned, as it indicates an incomplete
+          # UTF-8 sequence.
+          || (stringLength > 0 && stringIndex == stringLength - 1 && throw earlyEndMsg)
+
+        )
+        iterResult
+    );
+
+  /* Pretty prints a Unicode codepoint in the U+<HEX> notation.
+
+     Type: integer -> string
+  */
+  formatCodepoint = cp: "U+" + string.fit
+    {
+      width = 4;
+      char = "0";
+    }
+    (int.toHex cp);
+
+  encodeCodepoint = cp:
+    let
+      # Find the amount of bytes needed to encode the given codepoint.
+      # Note that this doesn't check if the Unicode codepoint is allowed,
+      # but rather allows all theoretically UTF-8-encodeable ones.
+      count = flow.switch cp [
+        [ (int.inRange 0 127) 1 ] # 00000000 0xxxxxxx
+        [ (int.inRange 128 2047) 2 ] # 00000yyy yyxxxxxx
+        [ (int.inRange 2048 65535) 3 ] # zzzzyyyy yyxxxxxx
+        [ (int.inRange 65536 1114111) 4 ] # 000uuuuu zzzzyyyy yyxxxxxx,
+        # capped at U+10FFFF
+
+        [ (fun.const true) (builtins.throw invalidCodepointMsg) ]
+      ];
+
+      invalidCodepointMsg = "${formatCodepoint cp} is not a Unicode codepoint";
+
+      # Extract the bit ranges x, y, z and u from the given codepoint
+      # according to Table 3-6. from The Unicode Standard, Version 13.0,
+      # section 3.9. u is split into uh and ul since they are used in
+      # different bytes in the end.
+      components = lib.mapAttrs
+        (_: { mask, offset }:
+          int.bitAnd (int.bitShiftR cp offset) mask
+        )
+        {
+          x = {
+            mask = if count > 1 then 63 else 127;
+            offset = 0;
+          };
+          y = {
+            mask = if count > 2 then 63 else 31;
+            offset = 6;
+          };
+          z = {
+            mask = 15;
+            offset = 12;
+          };
+          # u which belongs into the second byte
+          ul = {
+            mask = 3;
+            offset = 16;
+          };
+          # u which belongs into the first byte
+          uh = {
+            mask = 7;
+            offset = 18;
+          };
+        };
+      inherit (components) x y z ul uh;
+
+      # Finally construct the byte sequence for the given codepoint. This is
+      # usually done by using the component and adding a few bits as a prefix
+      # which depends on the length of the sequence. The longer the sequence,
+      # the further back each component is pushed. To simplify this, we
+      # always construct a 4 element list and take the last `count` elements.
+      # Thanks to laziness the bogus values created by this are never evaluated.
+      #
+      # Based on table 3-6. from The Unicode Standard,
+      # Version 13.0, section 3.9.
+      bytes = lib.sublist (4 - count) count [
+        # 11110uuu
+        (uh + 240)
+        # 10uuzzzz or 1110zzzz
+        (z + (if count > 3 then 128 + int.bitShiftL ul 4 else 224))
+        # 10yyyyyy or 110yyyyy
+        (y + (if count > 2 then 128 else 192))
+        # 10xxxxxx or 0xxxxxxx
+        (x + (if count > 1 then 128 else 0))
+      ];
+
+      firstByte = builtins.head bytes;
+
+      unableToEncodeMessage = "Can't encode ${formatCodepoint cp} as UTF-8";
+
+    in
+    string.fromBytes (
+      builtins.genList
+        (i:
+          let
+            byte = builtins.elemAt bytes i;
+          in
+          if wellFormedByte firstByte i byte
+          then byte
+          else builtins.throw unableToEncodeMessage
+        )
+        count
+    );
+
+  /* Encode a list of Unicode codepoints into an UTF-8 string.
+
+     Type: [ integer ] -> string
+  */
+  encode = lib.concatMapStrings encodeCodepoint;
+
+in
+{
+  inherit
+    encode
+    decode
+    step
+    formatCodepoint
+    ;
+}
diff --git a/users/sterni/nix/utf8/tests/default.nix b/users/sterni/nix/utf8/tests/default.nix
new file mode 100644
index 0000000000..40783eab24
--- /dev/null
+++ b/users/sterni/nix/utf8/tests/default.nix
@@ -0,0 +1,148 @@
+{ depot, pkgs, lib, ... }:
+
+let
+
+  inherit (pkgs)
+    runCommandLocal
+    ;
+
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    assertDoesNotThrow
+    ;
+
+  inherit (depot.nix.writers)
+    rustSimple
+    ;
+
+  inherit (depot.users.sterni.nix)
+    int
+    utf8
+    string
+    char
+    ;
+
+  rustDecoder = rustSimple
+    {
+      name = "utf8-decode";
+    } ''
+    use std::io::{self, Read};
+    fn main() -> std::io::Result<()> {
+      let mut buffer = String::new();
+      io::stdin().read_to_string(&mut buffer)?;
+
+      print!("[ ");
+
+      for c in buffer.chars() {
+        print!("{} ", u32::from(c));
+      }
+
+      print!("]");
+
+      Ok(())
+    }
+  '';
+
+  rustDecode = s:
+    let
+      expr = runCommandLocal "${s}-decoded" { } ''
+        printf '%s' ${lib.escapeShellArg s} | ${rustDecoder} > $out
+      '';
+    in
+    import expr;
+
+  hexDecode = l:
+    utf8.decode (string.fromBytes (builtins.map int.fromHex l));
+
+  hexEncode = l: utf8.encode (builtins.map int.fromHex l);
+
+  testFailures = it "checks UTF-8 decoding failures" ([
+    (assertThrows "truncated UTF-8 string throws" (hexDecode [ "F0" "9F" ]))
+    # examples from The Unicode Standard
+    (assertThrows "ill-formed: C0 AF" (hexDecode [ "C0" "AF" ]))
+    (assertThrows "ill-formed: E0 9F 80" (hexDecode [ "E0" "9F" "80" ]))
+    (assertEq "well-formed: F4 80 83 92" (hexDecode [ "F4" "80" "83" "92" ]) [ 1048786 ])
+    (assertThrows "Codepoint out of range: 0xFFFFFF" (hexEncode [ "FFFFFF" ]))
+    (assertThrows "Codepoint out of range: -0x02" (hexEncode [ "-02" ]))
+  ] ++ builtins.genList
+    (i:
+      let
+        cp = i + int.fromHex "D800";
+      in
+      assertThrows "Can't encode UTF-16 reserved characters: ${utf8.formatCodepoint cp}"
+        (utf8.encode [ cp ])
+    )
+    (int.fromHex "07FF"));
+
+  testAscii = it "checks decoding of ascii strings"
+    (builtins.map
+      (s: assertEq "ASCII decoding is equal to UTF-8 decoding for \"${s}\""
+        (string.toBytes s)
+        (utf8.decode s)) [
+      "foo bar"
+      "hello\nworld"
+      "carriage\r\nreturn"
+      "1238398494829304 []<><>({})[]!!)"
+      (string.take 127 char.allChars)
+    ]);
+
+  randomUnicode = [
+    "" # empty string should yield empty list
+    "🥰👨‍👨‍👧‍👦🐈‍⬛👩🏽‍🦰"
+    # https://kermitproject.org/utf8.html
+    "ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ"
+    "An preost wes on leoden, Laȝamon was ihoten"
+    "Sîne klâwen durh die wolken sint geslagen,"
+    "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ"
+    "На берегу пустынных волн"
+    "ვეპხის ტყაოსანი შოთა რუსთაველი"
+    "யாமறிந்த மொழிகளிலே தமிழ்மொழி போல் இனிதாவது எங்கும் காணோம், "
+    "ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸು "
+  ];
+
+  # https://kermitproject.org/utf8.html
+  glassSentences = [
+    "Euro Symbol: €."
+    "Greek: Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα."
+    "Íslenska / Icelandic: Ég get etið gler án þess að meiða mig."
+    "Polish: Mogę jeść szkło, i mi nie szkodzi."
+    "Romanian: Pot să mănânc sticlă și ea nu mă rănește."
+    "Ukrainian: Я можу їсти шкло, й воно мені не пошкодить."
+    "Armenian: Կրնամ ապակի ուտել և ինծի անհանգիստ չըներ։"
+    "Georgian: მინას ვჭამ და არა მტკივა."
+    "Hindi: मैं काँच खा सकता हूँ, मुझे उस से कोई पीडा नहीं होती."
+    "Hebrew(2): אני יכול לאכול זכוכית וזה לא מזיק לי."
+    "Yiddish(2): איך קען עסן גלאָז און עס טוט מיר נישט װײ."
+    "Arabic(2): أنا قادر على أكل الزجاج و هذا لا يؤلمني."
+    "Japanese: 私はガラスを食べられます。それは私を傷つけません。"
+    "Thai: ฉันกินกระจกได้ แต่มันไม่ทำให้ฉันเจ็บ "
+  ];
+
+  testDecoding = it "checks decoding of UTF-8 strings against Rust's String"
+    (builtins.map
+      (s: assertEq "Decoding of “${s}” is correct" (utf8.decode s) (rustDecode s))
+      (lib.flatten [
+        glassSentences
+        randomUnicode
+      ]));
+
+  testDecodingEncoding = it "checks that decoding and then encoding forms an identity"
+    (builtins.map
+      (s: assertEq "Decoding and then encoding “${s}” yields itself"
+        (utf8.encode (utf8.decode s))
+        s)
+      (lib.flatten [
+        glassSentences
+        randomUnicode
+      ]));
+
+in
+runTestsuite "nix.utf8" [
+  testFailures
+  testAscii
+  testDecoding
+  testDecodingEncoding
+]
diff --git a/users/sterni/nixpkgs-crate-holes/default.nix b/users/sterni/nixpkgs-crate-holes/default.nix
new file mode 100644
index 0000000000..c24200ff10
--- /dev/null
+++ b/users/sterni/nixpkgs-crate-holes/default.nix
@@ -0,0 +1,348 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  # dependency imports
+
+  inherit (depot.nix) getBins;
+  inherit (depot.third_party) rustsec-advisory-db;
+
+  bins = getBins pkgs.jq [
+    "jq"
+  ] // getBins pkgs.coreutils [
+    "cat"
+    "printf"
+    "tee"
+    "test"
+    "wc"
+  ] // getBins pkgs.gnugrep [
+    "grep"
+  ] // getBins pkgs.cargo-audit [
+    "cargo-audit"
+  ] // getBins pkgs.ansi2html [
+    "ansi2html"
+  ] // {
+    eprintf = depot.tools.eprintf;
+  };
+
+  # list of maintainers we may @mention on GitHub
+  maintainerWhitelist = builtins.attrValues {
+    inherit (lib.maintainers)
+      sternenseemann
+      qyliss
+      jk
+      symphorien
+      erictapen
+      expipiplus1
+      ;
+  };
+
+  # buildRustPackage handling
+
+  /* Predicate by which we identify rust packages we are interested in,
+     i. e. built using `buildRustPackage`.
+
+     Type :: drv -> bool
+  */
+  isRustPackage = v: v ? cargoDeps;
+
+  /* Takes a buildRustPackage derivation and returns a derivation which
+     builds extracts the `Cargo.lock` of its `cargoDeps` derivation or
+     `null` if it has none.
+
+     Type: drv -> option<drv>
+  */
+  # TODO(sterni): support cargoVendorDir?
+  extractCargoLock = drv:
+    if !(drv ? cargoDeps.outPath)
+    then null
+    else
+      pkgs.runCommandNoCC "${drv.name}-Cargo.lock" { } ''
+        if test -d "${drv.cargoDeps}"; then
+          cp "${drv.cargoDeps}/Cargo.lock" "$out"
+        fi
+
+        if test -f "${drv.cargoDeps}"; then
+          tar -xO \
+            --no-wildcards-match-slash --wildcards \
+            -f "${drv.cargoDeps}" \
+            '*/Cargo.lock' \
+            > "$out"
+        fi
+      '';
+
+  # nixpkgs traversal
+
+  # Condition for us to recurse: Either at top-level or recurseForDerivation.
+  recurseInto = path: x: path == [ ] ||
+    (lib.isAttrs x && (x.recurseForDerivations or false));
+
+  # Returns the value or false if an eval error occurs.
+  tryEvalOrFalse = v: (builtins.tryEval v).value;
+
+  /* Traverses nixpkgs as instructed by `recurseInto` and collects
+     the attribute and lockfile derivation of every rust package it
+     encounters into a list.
+
+     Type :: attrs
+          -> list {
+               attr :: list<str>;
+               lock :: option<drv>;
+               maintainers :: list<maintainer>;
+             }
+  */
+  allLockFiles =
+    let
+      go = path: x:
+        let
+          isDrv = tryEvalOrFalse (lib.isDerivation x);
+          doRec = tryEvalOrFalse (recurseInto path x);
+          isRust = tryEvalOrFalse (isRustPackage x);
+        in
+        if doRec then
+          lib.concatLists
+            (
+              lib.mapAttrsToList (n: go (path ++ [ n ])) x
+            ) else if isDrv && isRust then [
+          {
+            attr = path;
+            lock = extractCargoLock x;
+            maintainers = x.meta.maintainers or [ ];
+          }
+        ] else [ ];
+    in
+    go [ ];
+
+  # Report generation and formatting
+
+  reportFor = { attr, lock, maintainers ? [ ] }:
+    let
+      # naïve attribute path to Nix syntax conversion
+      strAttr = lib.concatStringsSep "." attr;
+      strMaintainers = lib.concatMapStringsSep " " (m: "@${m.github}") (
+        builtins.filter (x: builtins.elem x maintainerWhitelist) maintainers
+      );
+    in
+    if lock == null
+    then pkgs.emptyFile
+    else
+      depot.nix.runExecline "${strAttr}-vulnerability-report" { } [
+        "foreground"
+        [
+          "importas"
+          "out"
+          "out"
+          "redirfd"
+          "-w"
+          "1"
+          "$out"
+          depot.tools.rust-crates-advisory.lock-file-report
+          strAttr
+          lock
+          "true"
+          strMaintainers
+        ]
+        # ignore exit status of report
+        "exit"
+        "0"
+      ];
+
+  # GHMF in issues splits paragraphs on newlines
+  description = lib.concatMapStringsSep "\n\n"
+    (
+      builtins.replaceStrings [ "\n" ] [ " " ]
+    ) [
+    ''
+      The vulnerability report below was generated by
+      [nixpkgs-crate-holes](https://code.tvl.fyi/tree/users/sterni/nixpkgs-crate-holes)
+      which extracts the `Cargo.lock` file of each package in nixpkgs with a
+      `cargoDeps` attribute and passes it to
+      [cargo-audit](https://github.com/RustSec/rustsec/tree/main/cargo-audit)
+      using RustSec's
+      [advisory-db at ${builtins.substring 0 7 rustsec-advisory-db.rev}](https://github.com/RustSec/advisory-db/tree/${rustsec-advisory-db.rev}/).
+    ''
+    ''
+      Feel free to report any problems or suggest improvements (I have an email
+      address on my profile and hang out on Matrix/libera.chat as sterni)!
+      Tick off any reports that have been fixed in the meantime.
+    ''
+    ''
+      Note: A vulnerability in a dependency does not necessarily mean the dependent
+      package is vulnerable, e. g. when a vulnerable function isn't used.
+    ''
+  ];
+
+  runInstructions = ''
+    <details>
+    <summary>
+    Generating Cargo.lock vulnerability reports
+
+    </summary>
+
+    If you have a checkout of [depot](https://code.tvl.fyi/about/), you can generate this report using:
+
+    ```
+    nix-build -A users.sterni.nixpkgs-crate-holes.full \
+      --argstr nixpkgsPath /path/to/nixpkgs
+    ```
+
+    If you want a more detailed report for a single attribute of nixpkgs, use:
+
+    ```
+    nix-build -A users.sterni.nixpkgs-crate-holes.single \
+      --argstr nixpkgsPath /path/to/nixpkgs --arg attr '[ "ripgrep" ]'
+    ```
+
+    </details>
+  '';
+
+  defaultNixpkgsArgs = { allowBroken = false; };
+
+  reportForNixpkgs =
+    { nixpkgsPath
+    , nixpkgsArgs ? defaultNixpkgsArgs
+    }@args:
+
+    let
+      reports = builtins.map reportFor (
+        allLockFiles (import nixpkgsPath nixpkgsArgs)
+      );
+    in
+
+    depot.nix.runExecline "nixpkgs-rust-pkgs-vulnerability-report.md"
+      {
+        stdin = lib.concatMapStrings (report: "${report}\n") reports;
+      } [
+      "importas"
+      "out"
+      "out"
+      "redirfd"
+      "-w"
+      "1"
+      "$out"
+      # Print introduction paragraph for the issue
+      "if"
+      [ bins.printf "%s\n\n" description ]
+      # Print all reports
+      "foreground"
+      [
+        "forstdin"
+        "-E"
+        "report"
+        bins.cat
+        "$report"
+      ]
+      # Print stats at the end (mostly as a gimmick), we already know how many
+      # attributes there are and count the attributes with vulnerability by
+      # finding the number of checkable list entries in the output.
+      "backtick"
+      "-E"
+      "vulnerableCount"
+      [
+        "pipeline"
+        [
+          bins.grep
+          "^- \\[ \\]"
+          "$out"
+        ]
+        bins.wc
+        "-l"
+      ]
+      "if"
+      [
+        bins.printf
+        "\n%s of %s checked attributes have vulnerable dependencies.\n\n"
+        "$vulnerableCount"
+        (toString (builtins.length reports))
+      ]
+      "if"
+      [
+        bins.printf
+        "%s\n\n"
+        runInstructions
+      ]
+    ];
+
+  singleReport =
+    {
+      # Attribute to check: string or list of strings (attr path)
+      attr
+      # Path to importable nixpkgs checkout
+    , nixpkgsPath
+      # Arguments to pass to nixpkgs
+    , nixpkgsArgs ? defaultNixpkgsArgs
+    }:
+
+    let
+      attr' = if builtins.isString attr then [ attr ] else attr;
+      drv = lib.getAttrFromPath attr' (import nixpkgsPath nixpkgsArgs);
+      lockFile = extractCargoLock drv;
+      strAttr = lib.concatStringsSep "." attr';
+    in
+
+    depot.nix.runExecline "${strAttr}-report.html" { } [
+      "importas"
+      "out"
+      "out"
+      "backtick"
+      "-I"
+      "-E"
+      "-N"
+      "report"
+      [
+        bins.cargo-audit
+        "audit"
+        "--quiet"
+        "-n"
+        "--db"
+        rustsec-advisory-db
+        "-f"
+        lockFile
+      ]
+      "pipeline"
+      [
+        "ifte"
+        [
+          bins.printf
+          "%s"
+          "$report"
+        ]
+        [
+          bins.printf
+          "%s\n"
+          "No vulnerabilities found"
+        ]
+        bins.test
+        "-n"
+        "$report"
+      ]
+      "pipeline"
+      [
+        bins.tee
+        "/dev/stderr"
+      ]
+      "redirfd"
+      "-w"
+      "1"
+      "$out"
+      bins.ansi2html
+    ];
+
+in
+{
+  full = reportForNixpkgs;
+  single = singleReport;
+
+  inherit
+    extractCargoLock
+    allLockFiles
+    ;
+
+  # simple sanity check, doesn't cover everything, but testing the full report
+  # is quite expensive in terms of evaluation.
+  testSingle = singleReport {
+    nixpkgsPath = depot.third_party.nixpkgs.path;
+    attr = [ "ripgrep" ];
+  };
+
+  meta.ci.targets = [ "testSingle" ];
+}
diff --git a/users/tazjin/OWNERS b/users/tazjin/OWNERS
new file mode 100644
index 0000000000..c86f6eaa6a
--- /dev/null
+++ b/users/tazjin/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - tazjin
diff --git a/users/tazjin/aoc2019/default.nix b/users/tazjin/aoc2019/default.nix
new file mode 100644
index 0000000000..a1798f4001
--- /dev/null
+++ b/users/tazjin/aoc2019/default.nix
@@ -0,0 +1,26 @@
+# Solutions for Advent of Code 2019, written in Emacs Lisp.
+#
+# For each day a new file is created as "solution-day$n.el".
+{ depot, ... }:
+
+let
+  inherit (builtins) attrNames filter head listToAttrs match readDir;
+  dir = readDir ./.;
+  matchSolution = match "solution-(.*)\.el";
+  isSolution = f: (matchSolution f) != null;
+  getDay = f: head (matchSolution f);
+
+  solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir);
+  solutions = map
+    (f:
+      let day = getDay f; in {
+        name = day;
+        value = depot.nix.writeElispBin {
+          name = "aoc2019";
+          deps = p: with p; [ dash s ht ];
+          src = ./. + ("/" + f);
+        };
+      })
+    solutionFiles;
+in
+listToAttrs solutions
diff --git a/users/tazjin/aoc2019/solution-day1.el b/users/tazjin/aoc2019/solution-day1.el
new file mode 100644
index 0000000000..d805c22ec8
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day1.el
@@ -0,0 +1,28 @@
+;; Advent of Code 2019 - Day 1
+(require 'dash)
+
+;; Puzzle 1:
+
+(defvar day-1/input
+  '(83285 96868 121640 51455 128067 128390 141809 52325 68310 140707 124520 149678
+          87961 52040 133133 52203 117483 85643 84414 86558 65402 122692 88565 61895
+          126271 128802 140363 109764 53600 114391 98973 124467 99574 69140 144856
+          56809 149944 138738 128823 82776 77557 51994 74322 64716 114506 124074
+          73096 97066 96731 149307 135626 121413 69575 98581 50570 60754 94843 72165
+          146504 53290 63491 50936 79644 119081 70218 85849 133228 114550 131943
+          67288 68499 80512 148872 99264 119723 68295 90348 146534 52661 99146 95993
+          130363 78956 126736 82065 77227 129950 97946 132345 107137 79623 148477
+          88928 118911 75277 97162 80664 149742 88983 74518))
+
+(defun calculate-fuel (mass)
+  (- (/ mass 3) 2))
+
+(message "Solution to day1/1: %d" (apply #'+ (-map #'calculate-fuel day-1/input)))
+
+;; Puzzle 2:
+(defun calculate-recursive-fuel (mass)
+  (let ((fuel (calculate-fuel mass)))
+    (if (< fuel 0) 0
+      (+ fuel (calculate-recursive-fuel fuel)))))
+
+(message "Solution to day1/2: %d" (apply #'+ (-map #'calculate-recursive-fuel day-1/input)))
diff --git a/users/tazjin/aoc2019/solution-day2.el b/users/tazjin/aoc2019/solution-day2.el
new file mode 100644
index 0000000000..6ecac1e201
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day2.el
@@ -0,0 +1,53 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 2
+(require 'dash)
+(require 'ht)
+
+(defvar day2/input
+  [1 0 0 3 1 1 2 3 1 3 4 3 1 5 0 3 2 1 9 19 1 19 5 23 1 13 23 27 1 27 6 31
+     2 31 6 35 2 6 35 39 1 39 5 43 1 13 43 47 1 6 47 51 2 13 51 55 1 10 55
+     59 1 59 5 63 1 10 63 67 1 67 5 71 1 71 10 75 1 9 75 79 2 13 79 83 1 9
+     83 87 2 87 13 91 1 10 91 95 1 95 9 99 1 13 99 103 2 103 13 107 1 107 10
+     111 2 10 111 115 1 115 9 119 2 119 6 123 1 5 123 127 1 5 127 131 1 10
+     131 135 1 135 6 139 1 10 139 143 1 143 6 147 2 147 13 151 1 5 151 155 1
+     155 5 159 1 159 2 163 1 163 9 0 99 2 14 0 0])
+
+;; Puzzle 1
+
+(defun day2/single-op (f state idx)
+  (let* ((a (aref state (aref state (+ 1 idx))))
+         (b (aref state (aref state (+ 2 idx))))
+         (p (aref state (+ 3 idx)))
+         (result (funcall f a b)))
+    (aset state p (funcall f a b))))
+
+(defun day2/operate (state idx)
+  (pcase (aref state idx)
+    (99 (aref state 0))
+    (1 (day2/single-op #'+ state idx)
+       (day2/operate state (+ 4 idx)))
+    (2 (day2/single-op #'* state idx)
+       (day2/operate state (+ 4 idx)))
+    (other (error "Unknown opcode: %s" other))))
+
+(defun day2/program-with-inputs (noun verb)
+  (let* ((input (copy-tree day2/input t)))
+    (aset input 1 noun)
+    (aset input 2 verb)
+    (day2/operate input 0)))
+
+(message "Solution to day2/1: %s" (day2/program-with-inputs 12 2))
+
+;; Puzzle 2
+(let* ((used (ht))
+       (noun 0)
+       (verb 0)
+       (result (day2/program-with-inputs noun verb)))
+  (while (/= 19690720 result)
+    (setq noun (random 100))
+    (setq verb (random 100))
+    (unless (ht-get used (format "%d%d" noun verb))
+      (ht-set used (format "%d%d" noun verb) t)
+      (setq result (day2/program-with-inputs noun verb))))
+
+  (message "Solution to day2/2: %s%s" noun verb))
diff --git a/users/tazjin/aoc2019/solution-day3.el b/users/tazjin/aoc2019/solution-day3.el
new file mode 100644
index 0000000000..b7dfdd245f
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day3.el
@@ -0,0 +1,64 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 3
+
+(require 'cl-lib)
+(require 'dash)
+(require 'ht)
+(require 's)
+
+(defvar day3/input/wire1
+  "R1010,D422,L354,U494,L686,U894,R212,U777,L216,U9,L374,U77,R947,U385,L170,U916,R492,D553,L992,D890,L531,U360,R128,U653,L362,U522,R817,U198,L126,D629,L569,U300,L241,U145,R889,D196,L450,D576,L319,D147,R985,U889,L941,U837,L608,D77,L864,U911,L270,D869,R771,U132,L249,U603,L36,D328,L597,U992,L733,D370,L947,D595,L308,U536,L145,U318,R55,D773,R175,D505,R483,D13,R780,U778,R445,D107,R490,U245,L587,U502,R446,U639,R150,U35,L455,D522,R866,U858,R394,D975,R513,D378,R58,D646,L374,D675,R209,U228,R530,U543,L480,U677,L912,D164,L573,U587,L784,D626,L994,U250,L215,U985,R684,D79,L877,U811,L766,U617,L665,D246,L408,U800,L360,D272,L436,U138,R240,U735,L681,U68,L608,D59,R532,D808,L104,U968,R887,U819,R346,U698,L317,U582,R516,U55,L303,U607,L457,U479,L510,D366,L583,U519,R878,D195,R970,D267,R842,U784,R9,D946,R833,D238,L232,D94,L860,D47,L346,U951,R491,D745,R849,U273,R263,U392,L341,D808,R696,U326,R886,D296,L865,U833,R241,U644,R729,D216,R661,D712,L466,D699,L738,U5,L556,D693,R912,D13,R48,U63,L877,U628,L689,D929,R74,U924,R612,U153,R417,U425,L879,D378,R79,D248,L3,U519,R366,U281,R439,D823,R149,D668,R326,D342,L213,D735,R504,U265,L718,D842,L565,U105,L214,U963,R518,D681,R642,U170,L111,U6,R697,U572,R18,U331,L618,D255,R534,D322,L399,U595,L246,U651,L836,U757,R417,D795,R291,U759,L568,U965,R828,D570,R350,U317,R338,D173,L74,D833,L650,D844,L70,U913,R594,U407,R674,D684,L481,D564,L128,D277,R851,D274,L435,D582,R469,U729,R387,D818,R443,U504,R414,U8,L842,U845,R275,U986,R53,U660,R661,D225,R614,U159,R477")
+
+(defvar day3/input/wire2
+  "L1010,D698,R442,U660,L719,U702,L456,D86,R938,D177,L835,D639,R166,D285,L694,U468,L569,D104,L234,D574,L669,U299,L124,D275,L179,D519,R617,U72,L985,D248,R257,D276,L759,D834,R490,U864,L406,U181,R911,U873,R261,D864,R260,U759,R648,U158,R308,D386,L835,D27,L745,U91,R840,U707,R275,U543,L663,U736,L617,D699,R924,U103,R225,U455,R708,U319,R569,U38,R315,D432,L179,D975,R519,D546,L295,U680,L685,U603,R262,D250,R7,U171,R261,U519,L832,U534,L471,U431,L474,U886,R10,D179,L79,D555,R452,U452,L832,U863,L367,U538,L237,D160,R441,U605,R942,U259,L811,D552,R646,D353,L225,D94,L35,D307,R752,U23,R698,U610,L379,D932,R698,D751,R178,D347,R325,D156,R471,D555,R558,D593,R773,U2,L955,U764,L735,U438,R364,D640,L757,U534,R919,U409,R361,U407,R336,D808,R877,D648,R610,U198,R340,U94,R795,D667,R811,U975,L965,D224,R565,D681,L64,U567,R621,U922,L665,U329,R242,U592,L727,D481,L339,U402,R213,D280,R656,U169,R976,D962,L294,D505,L251,D689,L497,U133,R230,D441,L90,D220,L896,D657,L500,U331,R502,U723,R762,D613,L447,D256,L226,U309,L935,U384,L740,D459,R309,D707,R952,D747,L304,D105,R977,D539,R941,D21,R291,U216,R132,D543,R515,U453,L854,D42,R982,U102,L469,D639,R559,D68,R302,U734,R980,D214,R107,D191,L730,D793,L63,U17,R807,U196,R412,D592,R330,D941,L87,D291,L44,D94,L272,D780,R968,U837,L712,D704,R163,U981,R537,U778,R220,D303,L196,D951,R163,D446,R11,D623,L72,D778,L158,U660,L189,D510,L247,D716,L89,U887,L115,U114,L36,U81,R927,U293,L265,U183,R331,D267,R745,D298,L561,D918,R299,U810,L322,U679,L739,D854,L581,U34,L862,D779,R23")
+
+;; Puzzle 1
+
+(defun wire-from (raw)
+  (-map (lambda (s)
+          (cons (substring s 0 1) (string-to-number (substring s 1))))
+        (s-split "," raw)))
+
+(defun day3/move (x y next)
+  (cl-flet ((steps (by op)
+                   (-map op (reverse (number-sequence 1 by)))))
+    (pcase next
+      (`("L" . ,by) (steps by (lambda (n) (cons (- x n) y))))
+      (`("R" . ,by) (steps by (lambda (n) (cons (+ x n) y))))
+      (`("U" . ,by) (steps by (lambda (n) (cons x (+ y n)))))
+      (`("D" . ,by) (steps by (lambda (n) (cons x (- y n))))))))
+
+(defun day3/wire-points (wire)
+  (let ((points (ht))
+        (point-list (-reduce-from
+                     (lambda (acc point)
+                       (-let* (((x . y) (car acc))
+                               (next (day3/move x y point)))
+                         (-concat next acc)))
+                     '((0 . 0)) wire)))
+    (-map (-lambda ((s . p)) (ht-set! points p s))
+          (-zip (reverse (number-sequence 0 (- (length point-list) 1))) point-list))
+    (ht-remove! points '(0 . 0))
+    points))
+
+(defun day3/closest-intersection (crossed-points)
+  (car (-sort #'<
+              (-map (-lambda ((x . y))
+                      (+ (abs x) (abs y)))
+                    crossed-points))))
+
+(defun day3/minimum-steps (wire1 wire2 crossed)
+  (car (-sort #'<
+              (-map (-lambda (p)
+                      (+ (ht-get wire1 p) (ht-get wire2 p)))
+                    crossed))))
+
+;; Example:
+(let* ((wire1-points (day3/wire-points (wire-from day3/input/wire1)))
+       (wire2-points (day3/wire-points (wire-from day3/input/wire2)))
+       (crossed-points (-filter (lambda (p) (ht-contains? wire1-points p))
+                                (ht-keys wire2-points))))
+  (message "Solution for day3/1: %d" (day3/closest-intersection crossed-points))
+  (message "Solution for day3/2: %d" (day3/minimum-steps wire1-points
+                                                         wire2-points
+                                                         crossed-points)))
diff --git a/users/tazjin/aoc2019/solution-day4.el b/users/tazjin/aoc2019/solution-day4.el
new file mode 100644
index 0000000000..2805f3f4e9
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day4.el
@@ -0,0 +1,73 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 4
+
+(require 'cl-lib)
+(require 'dash)
+
+;; Puzzle 1
+
+(defun day4/to-digits (num)
+  "Convert NUM to a list of its digits."
+  (cl-labels ((steps (n digits)
+                     (if (= n 0) digits
+                       (steps (/ n 10) (cons (% n 10) digits)))))
+    (steps num '())))
+
+(defvar day4/input (-map #'day4/to-digits (number-sequence 128392 643281)))
+
+(defun day4/filter-password (digits)
+  "Determines whether the given rules match the supplied
+  number."
+
+  (and
+   ;; It is a six digit number
+   (= 6 (length digits))
+
+   ;; Value is within the range given in puzzle input
+   ;; (noop because the range is generated from the input)
+
+   ;; Two adjacent digits are the same (like 22 in 122345).
+   (car (-reduce-from (-lambda ((acc . prev) next)
+                        (cons (or acc (= prev next)) next))
+                      '(nil . 0) digits))
+
+   ;; Going from left to right, the digits never decrease; they only
+   ;; ever increase or stay the same (like 111123 or 135679).
+   (car (-reduce-from (-lambda ((acc . prev) next)
+                        (cons (and acc (>= next prev)) next))
+                      '(t . 0) digits))))
+
+;; Puzzle 2
+;;
+;; Additional criteria: If there's matching digits, they're not in a group.
+
+(cl-defstruct day4/acc state prev count)
+
+(defun day4/filter-longer-groups (digits)
+  (let ((res (-reduce-from
+              (lambda (acc next)
+                (cond ;; sequence is broken and count was at 1 ->
+                 ;; match!
+                 ((and (= (day4/acc-count acc) 2)
+                       (/= (day4/acc-prev acc) next))
+                  (setf (day4/acc-state acc) t))
+
+                 ;; sequence continues, counter increment!
+                 ((= (day4/acc-prev acc) next)
+                  (setf (day4/acc-count acc) (+ 1 (day4/acc-count acc))))
+
+                 ;; sequence broken, reset counter
+                 ((/= (day4/acc-prev acc) next)
+                  (setf (day4/acc-count acc) 1)))
+
+                (setf (day4/acc-prev acc) next)
+                acc)
+              (make-day4/acc :prev 0 :count 0) digits)))
+    (or (day4/acc-state res)
+        (= 2 (day4/acc-count res)))))
+
+(let* ((simple (-filter #'day4/filter-password day4/input))
+       (complex (-filter #'day4/filter-longer-groups simple)))
+  (message "Solution to day4/1: %d" (length simple))
+  (message "Solution to day4/2: %d" (length complex)))
+
diff --git a/users/tazjin/aoc2020/default.nix b/users/tazjin/aoc2020/default.nix
new file mode 100644
index 0000000000..cd89da7de4
--- /dev/null
+++ b/users/tazjin/aoc2020/default.nix
@@ -0,0 +1,26 @@
+# Solutions for Advent of Code 2020, written in Emacs Lisp.
+#
+# For each day a new file is created as "solution-day$n.el".
+{ depot, pkgs, ... }:
+
+let
+  inherit (builtins) attrNames filter head listToAttrs match readDir;
+  dir = readDir ./.;
+  matchSolution = match "solution-(.*)\.el";
+  isSolution = f: (matchSolution f) != null;
+  getDay = f: head (matchSolution f);
+
+  solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir);
+  solutions = map
+    (f:
+      let day = getDay f; in depot.nix.writeElispBin {
+        name = day;
+        deps = p: with p; [ dash s ht p.f ];
+        src = ./. + ("/" + f);
+      })
+    solutionFiles;
+in
+pkgs.symlinkJoin {
+  name = "aoc2020";
+  paths = solutions;
+}
diff --git a/users/tazjin/aoc2020/solution-day1.el b/users/tazjin/aoc2020/solution-day1.el
new file mode 100644
index 0000000000..a04f43d151
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day1.el
@@ -0,0 +1,44 @@
+;; Advent of Code 2020 - Day 1
+(require 'cl)
+(require 'ht)
+(require 'dash)
+
+(defmacro hash-set (&rest elements)
+  "Define a hash-table with empty values, for use as a set."
+  (cons 'ht (-map (lambda (x) (list x nil)) elements)))
+
+;; Puzzle 1:
+
+(defvar day1/input
+  (hash-set 1645 1995 1658 1062 1472 1710 1424 1823 1518 1656 1811 1511 1320 1521 1395
+            1996 1724 1666 1637 1504 1766 534 1738 1791 1372 1225 1690 1949 1495 1436 1166
+            1686 1861 1889 1887 997 1202 1478 833 1497 1459 1717 1272 1047 1751 1549 1204
+            1230 1260 1611 1506 1648 1354 1415 1615 1327 1622 1592 1807 1601 1026 1757 1376
+            1707 1514 1905 1660 1578 1963 1292 390 1898 1019 1580 1499 1830 1801 1881 1764
+            1442 1838 1088 1087 1040 1349 1644 1908 1697 1115 1178 1224 1810 1445 1594 1894
+            1287 1676 1435 1294 1796 1350 1685 1118 1488 1726 1696 1190 1538 1780 1806 1207
+            1346 1705 983 1249 1455 2002 1466 1723 1227 1390 1281 1715 1603 1862 1744 1774
+            1385 1312 1654 1872 1142 1273 1508 1639 1827 1461 1795 1533 1304 1417 1984 28
+            1693 1951 1391 1931 1179 1278 1400 1361 1369 1343 1416 1426 314 1510 1933 1239
+            1218 1918 1797 1255 1399 1229 723 1992 1595 1191 1916 1525 1605 1524 1869 1652
+            1874 1756 1246 1310 1219 1482 1429 1244 1554 1575 1123 1194 1408 1917 1613 1773
+            1809 1987 1733 1844 1423 1718 1714 1923 1503))
+
+(message "Solution to day1/1: %s"
+         (cl-loop for first being the hash-keys of day1/input
+                  for second = (- 2020 first)
+                  when (ht-contains? day1/input second)
+                  return (* first second)))
+
+;; Puzzle 2:
+
+(message "Solution to day1/1: %s"
+         (cl-loop for first being the hash-keys of day1/input
+                  for result =
+                  (cl-loop
+                   for second being the elements of (-drop 1 (ht-keys day1/input))
+                   for third = (- 2020 first second)
+                   when (ht-contains? day1/input third)
+                   return (* first second third))
+
+                  when result return result))
diff --git a/users/tazjin/aoc2020/solution-day2.el b/users/tazjin/aoc2020/solution-day2.el
new file mode 100644
index 0000000000..5993bf3407
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day2.el
@@ -0,0 +1,54 @@
+;; Advent of Code 2020 - Day 2
+
+(require 'cl-lib)
+(require 'f)
+(require 'ht)
+(require 's)
+(require 'seq)
+
+(defvar day2/input
+  ;; This one was too large to inline.
+  (s-lines (f-read "/tmp/aoc/day2.txt")))
+
+(defun day2/count-letters (password)
+  (let ((table (ht-create)))
+    (cl-loop for char across password
+             for current = (ht-get table char)
+             do (ht-set table char
+                        (if current (+ 1 current) 1)))
+    table))
+
+(defun day2/parse (input)
+  (let* ((split (s-split " " input))
+         (range (s-split "-" (car split))))
+    (list (string-to-number (car range))
+          (string-to-number (cadr range))
+          (string-to-char (cadr split))
+          (caddr split))))
+
+(defun day2/count-with-validation (func)
+  (length (-filter
+           (lambda (password)
+             (and (not (seq-empty-p password))
+                  (apply func (day2/parse password))))
+           day2/input)))
+
+;; Puzzle 1
+
+(defun day2/validate-oldjob (min max char password)
+  (let ((count (ht-get (day2/count-letters password) char)))
+    (when count
+      (and (>= count min)
+           (<= count max)))))
+
+(message "Solution to day2/1: %s"
+         (day2/count-with-validation #'day2/validate-oldjob))
+
+;; Puzzle 2
+
+(defun day2/validate-toboggan (pos1 pos2 char password)
+  (xor (= char (aref password (- pos1 1)))
+       (= char (aref password (- pos2 1)))))
+
+(message "Solution to day2/2: %s"
+         (day2/count-with-validation #'day2/validate-toboggan))
diff --git a/users/tazjin/aoc2020/solution-day3.el b/users/tazjin/aoc2020/solution-day3.el
new file mode 100644
index 0000000000..80ea4a2264
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day3.el
@@ -0,0 +1,43 @@
+;; Advent of Code 2020 - Day 3
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 's)
+(require 'seq)
+
+(setq day3/input
+      (-filter (lambda (s) (not (seq-empty-p s)))
+         (s-lines (f-read "/tmp/aoc/day3.txt"))))
+
+(setq day3/input-width (length (elt day3/input 0)))
+(setq day3/input-height (length day3/input))
+
+(defun day3/thing-at-point (x y)
+  "Pun intentional."
+  (when (>= day3/input-height y)
+    (let ((x-repeated (mod (- x 1) day3/input-width)))
+      (elt (elt day3/input (- y 1)) x-repeated))))
+
+(defun day3/slope (x-steps y-steps)
+  "Produce the objects encountered through this slope until the
+  bottom of the map."
+  (cl-loop for x from 1 by x-steps
+           for y from 1 to day3/input-height by y-steps
+           collect (day3/thing-at-point x y)))
+
+;; Puzzle 1
+
+(defun day3/count-trees (x-steps y-steps)
+  (cl-loop for thing being the elements of (day3/slope x-steps y-steps)
+           count (= thing ?#)))
+
+(message "Solution to day3/1: One encounters %s trees" (day3/count-trees 3 1))
+
+;; Puzzle 2
+
+(message "Solution to day3/2 %s" (* (day3/count-trees 1 1)
+                                    (day3/count-trees 3 1)
+                                    (day3/count-trees 5 1)
+                                    (day3/count-trees 7 1)
+                                    (day3/count-trees 1 2)))
diff --git a/users/tazjin/aoc2020/solution-day4.el b/users/tazjin/aoc2020/solution-day4.el
new file mode 100644
index 0000000000..034a40a955
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day4.el
@@ -0,0 +1,98 @@
+;; Advent of Code 2020 - Day 4
+
+(require 'cl-lib)
+(require 's)
+(require 'dash)
+(require 'f)
+
+(cl-defstruct day4/passport
+  byr ;; Birth Year
+  iyr ;; Issue Year
+  eyr ;; Expiration Year
+  hgt ;; Height
+  hcl ;; Hair Color
+  ecl ;; Eye Color
+  pid ;; Passport ID
+  cid ;; Country ID
+  )
+
+(defun day4/parse-passport (input)
+  (let* ((pairs (s-split " " (s-replace "\n" " " input) t))
+         (slots
+          (-map
+           (lambda (pair)
+             (pcase-let ((`(,key ,value) (s-split ":" (s-trim pair))))
+               (list (intern (format ":%s" key)) value)))
+           pairs)))
+    (apply #'make-day4/passport (-flatten slots))))
+
+(defun day4/parse-passports (input)
+  (-map #'day4/parse-passport (s-split "\n\n" input t)))
+
+(setq day4/input (day4/parse-passports (f-read "/tmp/aoc/day4.txt")))
+
+;; Puzzle 1
+
+(defun day4/validate (passport)
+  "Check that all fields except CID are present."
+  (cl-check-type passport day4/passport)
+  (and (day4/passport-byr passport)
+       (day4/passport-iyr passport)
+       (day4/passport-eyr passport)
+       (day4/passport-hgt passport)
+       (day4/passport-hcl passport)
+       (day4/passport-ecl passport)
+       (day4/passport-pid passport)))
+
+(message "Solution to day4/1: %s" (cl-loop for passport being the elements of day4/input
+                                           count (day4/validate passport)))
+
+;; Puzzle 2
+
+(defun day4/year-bound (min max value)
+  (and
+   (s-matches? (rx (= 4 digit)) value)
+   (<= min (string-to-number value) max)))
+
+(defun day4/check-unit (unit min max value)
+  (and
+   (string-match (rx (group (+? digit)) (literal unit)) value)
+   (<= min (string-to-number (match-string 1 value)) max)))
+
+(defun day4/properly-validate (passport)
+  "Opting for readable rather than clever here."
+  (and
+   (day4/validate passport)
+
+   ;; byr (Birth Year) - four digits; at least 1920 and at most 2002.
+   (day4/year-bound 1920 2002 (day4/passport-byr passport))
+
+   ;; iyr (Issue Year) - four digits; at least 2010 and at most 2020.
+   (day4/year-bound 2010 2020 (day4/passport-iyr passport))
+
+   ;; eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
+   (day4/year-bound 2020 2030 (day4/passport-eyr passport))
+
+   ;; hgt (Height) - a number followed by either cm or in:
+   ;; If cm, the number must be at least 150 and at most 193.
+   ;; If in, the number must be at least 59 and at most 76.
+   (or (day4/check-unit "cm" 150 193 (day4/passport-hgt passport))
+       (day4/check-unit "in" 59 76 (day4/passport-hgt passport)))
+
+   ;; hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
+   (s-matches? (rx ?# (= 6 hex)) (day4/passport-hcl passport))
+
+   ;; ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
+   (-contains? '("amb" "blu" "brn" "gry" "grn" "hzl" "oth")
+               (day4/passport-ecl passport))
+
+   ;; pid (Passport ID) - a nine-digit number, including leading zeroes.
+   (s-matches? (rx line-start (= 9 digit) line-end)
+               (day4/passport-pid passport))
+
+   ;; cid (Country ID) - ignored, missing or not.
+   ))
+
+(message "Solution to day4/2: %s"
+         (cl-loop for passport being the elements of day4/input
+                  count (day4/properly-validate passport)))
diff --git a/users/tazjin/aoc2020/solution-day5.el b/users/tazjin/aoc2020/solution-day5.el
new file mode 100644
index 0000000000..9bba322902
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day5.el
@@ -0,0 +1,61 @@
+;; Advent of Code 2020 - Day 5
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 'ht)
+(require 's)
+(require 'seq)
+
+(defvar day5/input
+  (-filter (lambda (s) (not (seq-empty-p s)))
+           (s-lines (f-read "/tmp/aoc/day5.txt"))))
+
+(defun day5/lower (sequence)
+  (seq-subseq sequence 0 (/ (length sequence) 2)))
+
+(defun day5/upper (sequence)
+  (seq-subseq sequence (/ (length sequence) 2)))
+
+(defun day5/seat-id (column row)
+  (+ column (* 8 row)))
+
+(defun day5/find-seat (boarding-pass)
+  (let ((rows (number-sequence 0 127))
+        (columns (number-sequence 0 7)))
+    (cl-loop for char across boarding-pass
+             do (pcase char
+                  (?F (setq rows (day5/lower rows)))
+                  (?B (setq rows (day5/upper rows)))
+                  (?R (setq columns (day5/upper columns)))
+                  (?L (setq columns (day5/lower columns))))
+             finally return (day5/seat-id (car columns) (car rows)))))
+
+;; Puzzle 1
+
+(message "Solution to day5/1: %s"
+         (cl-loop for boarding-pass in day5/input
+                  maximize (day5/find-seat boarding-pass)))
+
+;; Puzzle 2
+
+(defun day5/all-seats-in (row)
+  (-map (lambda (column) (day5/seat-id column row))
+        (number-sequence 0 7)))
+
+(message "Solution to day5/2: %s"
+         (let ((all-seats (ht-create)))
+           (-each (-mapcat #'day5/all-seats-in (number-sequence 1 126))
+             (lambda (seat) (ht-set all-seats seat nil)))
+
+           (cl-loop for boarding-pass in day5/input
+                    do (ht-remove all-seats (day5/find-seat boarding-pass))
+
+                    ;; Remove seats that lack adjacent entries, those
+                    ;; are missing on the plane.
+                    finally return
+                    (car
+                     (-filter (lambda (seat)
+                                (and (not (ht-contains? all-seats (- seat 1)))
+                                     (not (ht-contains? all-seats (+ seat 1)))))
+                              (ht-keys all-seats))))))
diff --git a/users/tazjin/aoc2020/solution-day6.el b/users/tazjin/aoc2020/solution-day6.el
new file mode 100644
index 0000000000..8179c79af2
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day6.el
@@ -0,0 +1,40 @@
+;; Advent of Code 2020 - Day 6
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 'ht)
+(require 's)
+
+(defvar day6/input (s-split "\n\n" (f-read "/tmp/aoc/day6.txt") t)
+  "Input, split into groups (with people in each group still distinct)")
+
+;; Puzzle 1
+
+(defun day6/count-answers (group-answers)
+  "I suspect doing it this way will be useful in puzzle 2."
+  (let ((table (ht-create)))
+    (-each group-answers
+      (lambda (answer)
+        (cl-loop for char across answer
+                 do (ht-set table char (+ 1 (or (ht-get table char)
+                                                0))))))
+    table))
+
+(message "Solution to day6/1: %s"
+         (cl-loop for group being the elements of day6/input
+                  sum (length
+                       (ht-keys
+                        (day6/count-answers (s-lines group))))))
+
+;; Puzzle 2
+
+(defun day6/count-unanimous-answers (answers)
+  (ht-reject (lambda (_key value) (not (= value (length answers))))
+             (day6/count-answers answers)))
+
+(message "Solution to day6/2: %s"
+         (cl-loop for group being the elements of day6/input
+                  sum (length
+                       (ht-keys
+                        (day6/count-unanimous-answers (s-split "\n" group t))))))
diff --git a/users/tazjin/aoc2020/solution-day7.el b/users/tazjin/aoc2020/solution-day7.el
new file mode 100644
index 0000000000..251a85fede
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day7.el
@@ -0,0 +1,92 @@
+;; Advent of Code 2020 - Day 7
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 's)
+(require 'ht)
+
+(defvar day7/input
+  (s-lines (s-chomp (f-read "/tmp/aoc/day7.txt"))))
+
+(defun day7/parse-bag (input)
+  (string-match (rx line-start
+                    (group (one-or-more (or letter space)))
+                    "s contain "
+                    (group (one-or-more anything))
+                    "." line-end)
+                input)
+  (cons (match-string 1 input)
+        (-map
+         (lambda (content)
+           (unless (equal content "no other bags")
+             (progn
+               (string-match
+                (rx (group (one-or-more digit))
+                    space
+                    (group (one-or-more anything) "bag"))
+                content)
+               (cons (match-string 2 content)
+                     (string-to-number (match-string 1 content))))))
+         (s-split ", " (match-string 2 input)))))
+
+(defun day7/id-or-next (table bag-type)
+  (unless (ht-contains? table bag-type)
+    (ht-set table bag-type (length (ht-keys table))))
+  (ht-get table bag-type))
+
+(defun day7/build-graph (input &optional flip)
+  "Represent graph mappings directionally using an adjacency
+  matrix, because that's probably easiest.
+
+  By default an edge means 'contains', with optional argument
+  FLIP edges are inverted and mean 'contained by'."
+
+  (let ((bag-mapping (ht-create))
+        (graph (let ((length (length input)))
+                 (apply #'vector
+                        (-map (lambda (_) (make-vector length 0)) input)))))
+    (cl-loop for bag in (-map #'day7/parse-bag input)
+             for bag-id = (day7/id-or-next bag-mapping (car bag))
+             do (-each (-filter #'identity (cdr bag))
+                  (pcase-lambda (`(,contained-type . ,count))
+                    (let ((contained-id (day7/id-or-next bag-mapping contained-type)))
+                      (if flip
+                          (aset (aref graph contained-id) bag-id count)
+                        (aset (aref graph bag-id) contained-id count))))))
+    (cons bag-mapping graph)))
+
+;; Puzzle 1
+
+(defun day7/find-ancestors (visited graph start)
+  (ht-set visited start t)
+  (cl-loop for bag-count being the elements of (aref graph start)
+           using (index bag-id)
+           when (and (> bag-count 0)
+                     (not (ht-contains? visited bag-id)))
+           do (day7/find-ancestors visited graph bag-id)))
+
+(message
+ "Solution to day7/1: %s"
+ (pcase-let* ((`(,mapping . ,graph) (day7/build-graph day7/input t))
+              (shiny-gold-id (ht-get mapping "shiny gold bag"))
+              (visited (ht-create)))
+   (day7/find-ancestors visited graph shiny-gold-id)
+   (- (length (ht-keys visited)) 1)))
+
+;; Puzzle 2
+
+(defun ht-find-by-value (table value)
+  (ht-find (lambda (_key item-value) (equal item-value value)) table))
+
+(defun day7/count-contained-bags (mapping graph start)
+  (cl-loop for bag-count being the elements of (aref graph start)
+           using (index bag-id)
+           when (> bag-count 0)
+           sum (+ bag-count
+                  (* bag-count (day7/count-contained-bags mapping graph bag-id)))))
+
+(message "Solution to day7/2: %s"
+         (pcase-let* ((`(,mapping . ,graph) (day7/build-graph day7/input))
+                      (shiny-gold-id (ht-get mapping "shiny gold bag")))
+           (day7/count-contained-bags mapping graph shiny-gold-id)))
diff --git a/users/tazjin/aoc2020/solution-day8.el b/users/tazjin/aoc2020/solution-day8.el
new file mode 100644
index 0000000000..591a07fbf3
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day8.el
@@ -0,0 +1,63 @@
+;; Advent of Code 2020 - Day
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 's)
+
+(setq day8/input
+      (apply #'vector
+             (-map (lambda (s)
+                     (pcase-let ((`(,op ,val) (s-split " " s t)))
+                       (cons (intern op) (string-to-number val))))
+                   (s-lines (s-chomp (f-read "/tmp/aoc/day8.txt"))))))
+
+(defun day8/step (code position acc)
+  (if (>= position (length code))
+      (cons 'final acc)
+
+    (let ((current (aref code position)))
+      (aset code position :done)
+      (pcase current
+        (:done (cons 'loop acc))
+        (`(nop . ,val) (cons (+ position 1) acc))
+        (`(acc . ,val) (cons (+ position 1) (+ acc val)))
+        (`(jmp . ,val) (cons (+ position val) acc))))))
+
+;; Puzzle 1
+
+(message "Solution to day8/1: %s"
+         (let ((code (copy-sequence day8/input))
+               (position 0)
+               (acc 0))
+           (cl-loop for next = (day8/step code position acc)
+                    when (equal 'loop (car next)) return (cdr next)
+                    do (setq position (car next))
+                    do (setq acc (cdr next)))))
+
+;; Puzzle 2
+
+(defun day8/flip-at (code pos)
+  (pcase (aref code pos)
+    (`(nop . ,val) (aset code pos `(jmp . ,val)))
+    (`(jmp . ,val) (aset code pos `(nop . ,val)))
+    (other (error "Unexpected flip op: %s" other))))
+
+(defun day8/try-flip (flip-at code position acc)
+  (day8/flip-at code flip-at)
+  (cl-loop for next = (day8/step code position acc)
+           when (equal 'loop (car next)) return nil
+           when (equal 'final (car next)) return (cdr next)
+           do (setq position (car next))
+           do (setq acc (cdr next))))
+
+(message "Solution to day8/2: %s"
+         (let ((flip-options (cl-loop for op being the elements of day8/input
+                                      using (index idx)
+                                      for opcode = (car op)
+                                      when (or (equal 'nop opcode)
+                                               (equal 'jmp opcode))
+                                      collect idx)))
+           (cl-loop for flip-at in flip-options
+                    for result = (day8/try-flip flip-at (copy-sequence day8/input) 0 0)
+                    when result return result)))
diff --git a/users/tazjin/avatar.jpeg b/users/tazjin/avatar.jpeg
new file mode 100644
index 0000000000..f6888e01c7
--- /dev/null
+++ b/users/tazjin/avatar.jpeg
Binary files differdiff --git a/users/tazjin/blog/.skip-subtree b/users/tazjin/blog/.skip-subtree
new file mode 100644
index 0000000000..e7fa50d49b
--- /dev/null
+++ b/users/tazjin/blog/.skip-subtree
@@ -0,0 +1 @@
+Subdirectories contain blog posts and static assets only
diff --git a/users/tazjin/blog/default.nix b/users/tazjin/blog/default.nix
new file mode 100644
index 0000000000..c8b3c31899
--- /dev/null
+++ b/users/tazjin/blog/default.nix
@@ -0,0 +1,46 @@
+{ depot, lib, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) hasAttr filter;
+
+  config = {
+    name = "tazjin's blog";
+    baseUrl = "https://tazj.in/blog";
+
+    footer = ''
+      <p class="footer">
+        <a class="uncoloured-link" href="https://tazj.in">homepage</a>
+        |
+        <a class="uncoloured-link" href="https://cs.tvl.fyi/">code</a>
+      </p>
+      <p class="lod">ಠ_ಠ</p>
+    '';
+  };
+
+  inherit (depot.web.blog) post includePost renderPost;
+
+  posts = filter includePost (list post (import ./posts.nix));
+
+  rendered = pkgs.runCommandNoCC "tazjins-blog" { } ''
+    mkdir -p $out
+
+    ${lib.concatStringsSep "\n" (map (post:
+      "cp ${renderPost config post} $out/${post.key}.html"
+    ) posts)}
+  '';
+
+in
+{
+  inherit posts rendered config;
+
+  # Generate embeddable nginx configuration for redirects from old post URLs
+  oldRedirects = lib.concatStringsSep "\n" (map
+    (post: ''
+      location ~* ^(/en)?/${post.oldKey} {
+        return 301 https://tazj.in/blog/${post.key};
+      }
+    '')
+    (filter (hasAttr "oldKey") posts));
+}
diff --git a/users/tazjin/blog/posts.nix b/users/tazjin/blog/posts.nix
new file mode 100644
index 0000000000..eeba600286
--- /dev/null
+++ b/users/tazjin/blog/posts.nix
@@ -0,0 +1,57 @@
+# This file defines all the blog posts.
+[
+  {
+    key = "emacs-is-underrated";
+    title = "Emacs is the most underrated tool";
+    date = 1581286656;
+    content = ./posts/emacs-is-underrated.md;
+    draft = true;
+  }
+  {
+    key = "best-tools";
+    title = "tazjin's best tools";
+    date = 1576800001;
+    content = ./posts/best-tools.md;
+  }
+  {
+    key = "nixery-layers";
+    title = "Nixery: Improved Layering Design";
+    date = 1565391600;
+    content = ./posts/nixery-layers.md;
+  }
+  {
+    key = "reversing-watchguard-vpn";
+    title = "Reverse-engineering WatchGuard Mobile VPN";
+    date = 1486830338;
+    content = ./posts/reversing-watchguard-vpn.md;
+    oldKey = "1486830338";
+  }
+  {
+    key = "make-object-t-again";
+    title = "Make Object <T> Again!";
+    date = 1476807384;
+    content = ./posts/make-object-t-again.md;
+    oldKey = "1476807384";
+  }
+  {
+    key = "the-smu-problem";
+    title = "The SMU-problem of messaging apps";
+    date = 1450354078;
+    content = ./posts/the-smu-problem.md;
+    oldKey = "1450354078";
+  }
+  {
+    key = "sick-in-sweden";
+    title = "Being sick in Sweden";
+    date = 1423995834;
+    content = ./posts/sick-in-sweden.md;
+    oldKey = "1423995834";
+  }
+  {
+    key = "nsa-zettabytes";
+    title = "The NSA's 5 zettabytes of data";
+    date = 1375310627;
+    content = ./posts/nsa-zettabytes.md;
+    oldKey = "1375310627";
+  }
+]
diff --git a/users/tazjin/blog/posts/best-tools.md b/users/tazjin/blog/posts/best-tools.md
new file mode 100644
index 0000000000..314d0a4419
--- /dev/null
+++ b/users/tazjin/blog/posts/best-tools.md
@@ -0,0 +1,183 @@
+In the spirit of various other "Which X do you use?"-pages I thought it would be
+fun to have a little post here that describes which tools I've found to work
+well for myself.
+
+When I say "tools" here, it's not about software - it's about real, physical
+tools!
+
+If something goes on this list that's because I think it's seriously a
+best-in-class type of product.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+- [Media & Tech](#media--tech)
+    - [Keyboard](#keyboard)
+    - [Speakers](#speakers)
+    - [Headphones](#headphones)
+        - [Earphones](#earphones)
+    - [Phone](#phone)
+- [Other stuff](#other-stuff)
+    - [Toothbrush](#toothbrush)
+    - [Shavers](#shavers)
+    - [Shoulder bag](#shoulder-bag)
+    - [Wallet](#wallet)
+<!-- markdown-toc end -->
+
+---------
+
+# Media & Tech
+
+## Keyboard
+
+The best keyboard that money will buy you at the moment is the [Kinesis
+Advantage][advantage]. There's a variety of contoured & similarly shaped
+keyboards on the market, but the Kinesis is the only one I've tried that has
+properly implemented the keywell concept.
+
+I struggle with RSI issues and the Kinesis actually makes it possible for me to
+type for longer periods of time, which always leads to extra discomfort on
+laptop keyboards and such.
+
+Honestly, the Kinesis is probably the best piece of equipment on this entire
+list. I own several of them and there will probably be more in the future. They
+last forever and your wrists will thank you in the future, even if you do not
+suffer from RSI yet.
+
+Kinesis have announced a split version of the Advantage. Once that is
+easily available, I will buy one and evaluate it.
+
+[advantage]: https://kinesis-ergo.com/shop/advantage2/
+
+## Speakers
+
+There are two sets of speakers I use, unfortunately one pair has been in storage
+since I left the UK.
+
+My original favourite speakers are the [Teufel Motiv 2][motiv-2], usually hooked
+up to a Chromecast and a record player. I've had these for over a decade and
+they're incredibly good, but unfortunately Teufel no longer makes them. Mine are
+currently in a warehouse somewhere in London, and I don't know when I will see
+them again ...
+
+It's possible to grab a pair on eBay occasionally, so keep an eye out if you're
+interested!
+
+In my Moscow flat, I have a pair of [Wharfedale Diamond 12][diamond-12]
+connected to a Philips amplifier older than myself. These provide an excellent,
+balanced, "Wharfedale-sound". Some people find it needs some getting used to,
+but don't want to go back after that initial phase.
+
+[motiv-2]: https://www.teufelaudio.com/uk/pc/motiv-2-p167.html
+[diamond-12]: https://www.wharfedaleusa.com/collections/diamond-12
+
+## Headphones
+
+I use the [Bose QC35][qc35] (note: link goes to a newer generation than the one
+I own) for their outstanding noise cancelling functionality and decent sound.
+
+When I first bought them I didn't expect them to end up on this list as the
+firmware had issues that made them only barely usable, but Bose has managed to
+iron these problems out over time.
+
+I avoid using Bluetooth when outside and fortunately the QC35 come with an
+optional cable that you can plug into any good old 3.5mm jack.
+
+[qc35]: https://www.bose.co.uk/en_gb/products/headphones/over_ear_headphones/quietcomfort-35-wireless-ii.html
+
+### Earphones
+
+Actually, to follow up on the above - most of the time I'm not using (over-ear)
+headphones, but (in-ear) earphones - specifically the (**wired!!!**) [Apple
+EarPods][earpods].
+
+Apple will probably stop selling these soon because they've gotten into the
+habit of cancelling all of their good products, so I have a stash of these
+around. You will usually find no fewer than 3-4 of them lying around in my
+flat.
+
+[earpods]: https://www.apple.com/uk/shop/product/MNHF2ZM/A/earpods-with-35mm-headphone-plug
+
+## Phone
+
+My current phone is the [Unihertz Atom L][atom-l]. It is a rugged, small-screen
+smartphone with a 3.5mm heapdhone jack. This phone is *weird* looking, getting
+one of these means you will constantly be asked what kind of device you have
+there. It does everything I need, and survives being thrown against a wall when
+the software misbehaves. As the Swedes would say, this phone is solidly *lagom*.
+
+The ruggedness is an additional bonus as it survives things like sauna / banya
+trips just fine.
+
+Up until a few years ago I used the original [iPhone SE][se]. This was the last
+truly good phone anyone made, but unfortunately it was discontinued and is iOS
+only.
+
+[atom-l]: https://www.unihertz.com/products/atom-l
+[se]: https://en.wikipedia.org/wiki/IPhone_SE
+
+# Other stuff
+
+## Toothbrush
+
+The [Philips Sonicare][sonicare] is excellent and well worth its price.
+
+I've had it for a few years and whereas I occasionally had minor teeth issues
+before, they seem to be mostly gone now. According to my dentist the state of my
+teeth is now usually pretty good and I draw a direct correlation back to this
+thing.
+
+It has an app and stuff, but I just ignore that.
+
+I first got one of these in about 2014, and it lasted until 2020, at which point
+I upgraded to whatever the current model was.
+
+[sonicare]: https://www.philips.co.uk/c-m-pe/electric-toothbrushes
+
+## Shavers
+
+The [Philipps SensoTouch 3D][sensotouch] is excellent. Super-comfortable close
+face shave in no time and leaves absolutely no mess around, as far as I can
+tell! I've had this for ~7 years and it's not showing any serious signs of aging
+yet.
+
+Another bonus is that its battery time is effectively infinite (in the order of
+months of use per charge). I've never had to worry when bringing it on a longer
+trip!
+
+[sensotouch]: https://www.philips.co.uk/c-p/1250X_40/norelco-sensotouch-3d-wet-and-dry-electric-razor-with-precision-trimmer
+
+## Shoulder bag
+
+When I moved to London I wanted to stop using backpacks most of the time, as
+those are just annoying to deal with when commuting on the tube.
+
+To work around this I wanted a good shoulder bag with a vertical format (to save
+space), but it turned out that there's very few of those around that reach any
+kind of quality standard.
+
+The one I settled on is the [Waterfield Muzetto][muzetto] leather bag. It's one
+of those things that comes with a bit of a price tag attached, but it's well
+worth it!
+
+**Unfortunately**, just like my speakers, this bag is now in storage somewhere
+in the UK since I left the country.
+
+After moving to Moscow I quickly ran into the same problem as in London when
+using the metro, but getting another Muzetto was kind of impractical.
+
+I couldn't find any other vertical messenger bags that I liked, and ended up
+going for a more traditional one: The [Brialdi Ostin][ostin].
+
+[muzetto]: https://www.sfbags.com/collections/shoulder-messenger-bags/products/muzetto-leather-bag
+[ostin]: https://www.brialdi.ru/shop/handbags/brialdi_ostin_brown/
+
+## Wallet
+
+My wallet is the [Bellroy Slim Sleeve][slim-sleeve]. It's near indestructible,
+looks great, is very slim and fits a ton of cards, business cards, receipts and
+whatever else you want to be lugging around with you!
+
+However, now that I'm spending a lot of time in Egypt (where cash is still
+king), not having a place to stash cash is a bit of an issue. So far, no
+solution!
+
+[slim-sleeve]: https://bellroy.com/products/slim-sleeve-wallet/default/charcoal
diff --git a/users/tazjin/blog/posts/emacs-is-underrated.md b/users/tazjin/blog/posts/emacs-is-underrated.md
new file mode 100644
index 0000000000..afb8dc889e
--- /dev/null
+++ b/users/tazjin/blog/posts/emacs-is-underrated.md
@@ -0,0 +1,233 @@
+TIP: Hello, and thanks for offering to review my draft! This post
+intends to convey to people what the point of Emacs is. Not to convert
+them to use it, but at least with opening their minds to the
+possibility that it might contain valuable things. I don't know if I'm
+on track in the right direction, and your input will help me figure it
+out. Thanks!
+
+TODO(tazjin): Restructure sections: Intro -> Introspectability (and
+story) -> text-based UIs (which lead to fluidity, muscle memory across
+programs and "translatability" of workflows) -> Outro. It needs more
+flow!
+
+TODO(tazjin): Highlight more that it's not about editing: People can
+derive useful things from Emacs by just using magit/org/notmuch/etc.!
+
+TODO(tazjin): Note that there's value in trying Emacs even if people
+don't end up using it, similar to how learning languages like Lisp or
+Haskell helps grow as a programmer even without using them day-to-day.
+
+*Real post starts below!*
+
+---------
+
+There are two kinds of people: Those who use Emacs, and those who
+think it is a text editor. This post is aimed at those in the second
+category.
+
+Emacs is the most critical piece of software I run. My [Emacs
+configuration][emacs-config] has steadily evolved for almost a decade.
+Emacs is my window manager, mail client, terminal, git client,
+information management system and - perhaps unsurprisingly - text
+editor.
+
+Before going into why I chose to invest so much into this program,
+follow me along on a little thought experiment:
+
+----------
+
+Lets say you use a proprietary spreadsheet program. You find that
+there are features in it that *almost, but not quite* do what you
+want.
+
+What can you do? You can file a feature request to the company that
+makes it and hope they listen, but for the likes of Apple and
+Microsoft chances are they won't and there is nothing you can do.
+
+Let's say you are also running an open-source program for image
+manipulation. You again find that some of its features are subtly
+different from what you would want them to do.
+
+Things look a bit different this time - after all, the program is
+open-source! You can go and fetch its source code, figure out its
+internal structure and wrangle various layers of code into submission
+until you find the piece that implements the functionality you want to
+change. If you know the language it is written in; you can modify the
+feature.
+
+Now all that's left is figuring out its build system[^1], building and
+installing it and moving over to the new version.
+
+Realistically you are not going to do this much in the real world. The
+friction to contributing to projects, especially complex ones, is
+often quite high. For minor inconveniences, you might often find
+yourself just shrugging and working around them.
+
+What if it didn't have to be this way?
+
+-------------
+
+One of the core properties of Emacs is that it is *introspective* and
+*self-documenting*.
+
+For example: A few years ago, I had just switched over to using
+[EXWM][], the Emacs X Window Manager. To launch applications I was
+using an Emacs program called Helm that let me select installed
+programs interactively and press <kbd>RET</kbd> to execute them.
+
+This was very useful - until I discovered that if I tried to open a
+second terminal window, it would display an error:
+
+    Error: urxvt is already running
+
+Had this been dmenu, I might have had to go through the whole process
+described above to fix the issue. But it wasn't dmenu - it was an
+Emacs program, and I did the following things:
+
+1. I pressed <kbd>C-h k</kbd>[^2] (which means "please tell me what
+   the following key does"), followed by <kbd>s-d</kbd> (which was my
+   keybinding for launching programs).
+
+2. Emacs displayed a new buffer saying, roughly:
+
+   ```
+   s-d runs the command helm-run-external-command (found in global-map),
+   which is an interactive autoloaded compiled Lisp function in
+   ‘.../helm-external.el’.
+
+   It is bound to s-d.
+   ```
+
+   I clicked on the filename.
+
+3. Emacs opened the file and jumped to the definition of
+   `helm-run-external-command`. After a few seconds of reading through
+   the code, I found this snippet:
+
+   ```lisp
+   (if (get-process proc)
+       (if helm-raise-command
+           (shell-command  (format helm-raise-command real-com))
+         (error "Error: %s is already running" real-com))
+     ;; ... the actual code to launch programs followed below ...
+     )
+   ```
+
+4. I deleted the outer if-expression which implemented the behaviour I
+   didn't want, pressed <kbd>C-M-x</kbd> to reload the code and saved
+   the file.
+
+The whole process took maybe a minute, and the problem was now gone.
+
+Emacs isn't just "open-source", it actively encourages the user to
+modify it, discover what to modify and experiment while it is running.
+
+In some sense it is like the experience of the old Lisp machines, a
+paradigm that we have completely forgotten.
+
+---------------
+
+Circling back to my opening statement: If Emacs is not a text editor,
+then what *is* it?
+
+The Emacs website says this:
+
+> [Emacs] is an interpreter for Emacs Lisp, a dialect of the Lisp
+> programming language with extensions to support text editing
+
+The core of Emacs implements the language and the functionality needed
+to evaluate and run it, as well as various primitives for user
+interface construction such as buffers, windows and frames.
+
+Every other feature of Emacs is implemented *in Emacs Lisp*.
+
+The Emacs distribution ships with rudimentary text editing
+functionality (and some language-specific support for the most popular
+languages), but it also brings with it two IRC clients, a Tetris
+implementation, a text-mode web browser, [org-mode][] and many other
+tools.
+
+Outside of the core distribution there is a myriad of available
+programs for Emacs: [magit][] (the famous git porcelain), text-based
+[HTTP clients][], even interactive [Kubernetes frontends][k8s].
+
+What all of these tools have in common is that they use text-based
+user interfaces (UI elements like images are used only sparingly in
+Emacs), and that they can be introspected and composed like everything
+else in Emacs.
+
+If magit does not expose a git flag I need, it's trivial to add. If I
+want a keybinding to jump from a buffer showing me a Kubernetes pod to
+a magit buffer for the source code of the container, it only takes a
+few lines of Emacs Lisp to implement.
+
+As proficiency with Emacs Lisp ramps up, the environment becomes
+malleable like clay and evolves along with the user's taste and needs.
+Muscle memory learned for one program translates seamlessly to others,
+and the overall effect is an improvement in *workflow fluidity* that
+is difficult to overstate.
+
+Also, workflows based on Emacs are *stable*. Moving my window
+management to Emacs has meant that I'm not subject to the whim of some
+third-party developer changing my window layouting features (as they
+often do on MacOS).
+
+To illustrate this: Emacs has development history back to the 1970s,
+continuous git history that survived multiple VCS migrations [since
+1985][first-commit] (that's 22 years before git itself was released!)
+and there is code[^3] implementing interactive functionality that has
+survived unmodified in Emacs *since then*.
+
+---------------
+
+Now, what is the point of this post?
+
+I decided to write this after a recent [tweet][] by @IanColdwater (in
+the context of todo-management apps):
+
+> The fact that it's 2020 and the most viable answer to this appears
+> to be Emacs might be the saddest thing I've ever heard
+
+What bothers me is that people see this as *sad*. Emacs being around
+for this long and still being unparalleled for many of the UX
+paradigms implemented by its programs is, in my book, incredible - and
+not sad.
+
+How many other paradigms have survived this long? How many other tools
+still have fervent followers, amazing [developer tooling][] and a
+[vibrant ecosystem][] at this age?
+
+Steve Yegge [said it best][babel][^5]: Emacs has the Quality Without a
+Name.
+
+What I wish you, the reader, should take away from this post is the
+following:
+
+TODO(tazjin): Figure out what people should take away from this post.
+I need to sleep on it. It's something about not dismissing tools just
+because of their age, urging them to explore paradigms that might seem
+unfamiliar and so on. Ideas welcome.
+
+---------------
+
+[^1]: Wouldn't it be a joy if every project just used Nix? I digress ...
+[^2]: These are keyboard shortcuts written in [Emacs Key Notation][ekn].
+[^3]: For example, [functionality for online memes][studly] that
+    wouldn't be invented for decades to come!
+[^4]: ... and some things wrong, but that is an issue for a separate post!
+[^5]: And I really *do* urge you to read that post's section on Emacs.
+
+[emacs-config]: https://git.tazj.in/tree/tools/emacs
+[EXWM]: https://github.com/ch11ng/exwm
+[helm]: https://github.com/emacs-helm/helm
+[ekn]: https://www.gnu.org/software/emacs/manual/html_node/efaq/Basic-keys.html
+[org-mode]: https://orgmode.org/
+[magit]: https://magit.vc
+[HTTP clients]: https://github.com/pashky/restclient.el
+[k8s]: https://github.com/jypma/kubectl
+[first-commit]: http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=ce5584125c44a1a2fbb46e810459c50b227a95e2
+[studly]: http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=47bdd84a0a9d20aab934482a64b84d0db63e7532
+[tweet]: https://twitter.com/IanColdwater/status/1220824466525229056
+[developer tooling]: https://github.com/alphapapa/emacs-package-dev-handbook
+[vibrant ecosystem]: https://github.com/emacs-tw/awesome-emacs
+[babel]: https://sites.google.com/site/steveyegge2/tour-de-babel#TOC-Lisp
diff --git a/users/tazjin/blog/posts/make-object-t-again.md b/users/tazjin/blog/posts/make-object-t-again.md
new file mode 100644
index 0000000000..420b57c0fd
--- /dev/null
+++ b/users/tazjin/blog/posts/make-object-t-again.md
@@ -0,0 +1,98 @@
+A few minutes ago I found myself debugging a strange Java issue related
+to Jackson, one of the most common Java JSON serialization libraries.
+
+The gist of the issue was that a short wrapper using some types from
+[Javaslang](http://www.javaslang.io/) was causing unexpected problems:
+
+```java
+public <T> Try<T> readValue(String json, TypeReference type) {
+  return Try.of(() -> objectMapper.readValue(json, type));
+}
+```
+
+The signature of this function was based on the original Jackson
+`readValue` type signature:
+
+```java
+public <T> T readValue(String content, TypeReference valueTypeRef)
+```
+
+While happily using my wrapper function I suddenly got an unexpected
+error telling me that `Object` is incompatible with the type I was
+asking Jackson to de-serialize, which got me to re-evaluate the above
+type signature again.
+
+Lets look for a second at some code that will *happily compile* if you
+are using Jackson\'s own `readValue`:
+
+```java
+// This shouldn't compile!
+Long l = objectMapper.readValue("\"foo\"", new TypeReference<String>(){});
+```
+
+As you can see there we ask Jackson to decode the JSON into a `String`
+as enclosed in the `TypeReference`, but assign the result to a `Long`.
+And it compiles. And it failes at runtime with
+`java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long`.
+Huh?
+
+Looking at the Jackson `readValue` implementation it becomes clear
+what\'s going on here:
+
+```java
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public <T> T readValue(String content, TypeReference valueTypeRef)
+    throws IOException, JsonParseException, JsonMappingException
+{
+    return (T) _readMapAndClose(/* whatever */);
+}
+```
+
+The function is parameterised over the type `T`, however the only place
+where `T` occurs in the signature is in the parameter declaration and
+the function return type. Java will happily let you use generic
+functions and types without specifying type parameters:
+
+```java
+// Compiles fine!
+final List myList = List.of(1,2,3);
+
+// Type is now myList : List<Object>
+```
+
+Meaning that those parameters default to `Object`. Now in the code above
+Jackson also explicitly casts the return value of its inner function
+call to `T`.
+
+What ends up happening is that Java infers the expected return type from
+the context of the `readValue` and then happily uses the unchecked cast
+to fit that return type. If the type hints of the context aren\'t strong
+enough we simply get `Object` back.
+
+So what\'s the fix for this? It\'s quite simple:
+
+```java
+public <T> T readValue(String content, TypeReference<T> valueTypeRef)
+```
+
+By also making the parameter appear in the `TypeReference` we \"bind\"
+`T` to the type enclosed in the type reference. The cast can then also
+safely be removed.
+
+The cherries on top of this are:
+
+1.  `@SuppressWarnings({ "rawtypes" })` explicitly disables a
+    warning that would\'ve caught this
+
+2.  the `readValue` implementation using the less powerful `Class`
+    class to carry the type parameter does this correctly: `public <T>
+    T readValue(String content, Class<T> valueType)`
+
+The big question I have about this is *why* does Jackson do it this way?
+Obviously the warning did not just appear there by chance, so somebody
+must have thought about this?
+
+If anyone knows what the reason is, I\'d be happy to hear from you.
+
+PS: Shoutout to David & Lucia for helping me not lose my sanity over
+this.
diff --git a/users/tazjin/blog/posts/nixery-layers.md b/users/tazjin/blog/posts/nixery-layers.md
new file mode 100644
index 0000000000..38ca2294a8
--- /dev/null
+++ b/users/tazjin/blog/posts/nixery-layers.md
@@ -0,0 +1,272 @@
+TIP: This blog post was originally published as a design document for
+[Nixery][] and is not written in the same style
+as other blog posts.
+
+Thanks to my colleagues at Google and various people from the Nix community for
+reviewing this.
+
+------
+
+# Nixery: Improved Layering
+
+**Authors**: tazjin@
+
+**Reviewers**: so...@, en...@, pe...@
+
+**Status**: Implemented
+
+**Last Updated**: 2019-08-10
+
+## Introduction
+
+This document describes a design for an improved image layering method for use
+in Nixery. The algorithm [currently used][grhmc] is designed for a slightly
+different use-case and we can improve upon it by making use of more of the
+available data.
+
+## Background / Motivation
+
+Nixery is a service that uses the [Nix package manager][nix] to build container
+images (for runtimes such as Docker), that are served on-demand via the
+container [registry protocols][]. A demo instance is available at
+[nixery.dev][].
+
+In practice this means users can simply issue a command such as `docker pull
+nixery.dev/shell/git` and receive an image that was built ad-hoc containing a
+shell environment and git.
+
+One of the major advantages of building container images via Nix (as described
+for `buildLayeredImage` in [this blog post][grhmc]) is that the
+content-addressable nature of container image layers can be used to provide more
+efficient caching characteristics (caching based on layer content) than what is
+common with Dockerfiles and other image creation methods (caching based on layer
+creation method).
+
+However, this is constrained by the maximum number of layers supported in an
+image (125). A naive approach such as putting each included package (any
+library, binary, etc.) in its own layer quickly runs into this limitation due to
+the large number of dependencies more complex systems tend to have. In addition,
+users wanting to extend images created by Nixery (e.g. via `FROM nixery.dev/…`)
+share this layer maximum with the created image - limiting extensibility if all
+layers are used up by Nixery.
+
+In theory the layering strategy of `buildLayeredImage` should already provide
+good caching characteristics, but in practice we are seeing many images with
+significantly more packages than the number of layers configured, leading to
+more frequent cache-misses than desired.
+
+The current implementation of `buildLayeredImage` inspects a graph of image
+dependencies and determines the total number of references (direct & indirect)
+to any node in the graph. It then sorts all dependencies by this popularity
+metric and puts the first `n - 2` (for `n` being the maximum number of layers)
+packages in their own layers, all remaining packages in one layer and the image
+configuration in the final layer.
+
+## Design / Proposal
+
+## (Close-to) ideal layer-layout using more data
+
+We start out by considering what a close to ideal layout of layers would look
+like for a simple use-case.
+
+![Ideal layout](/static/img/nixery/ideal_layout.webp)
+
+In this example, counting the total number of references to each node in the
+graph yields the following result:
+
+| pkg   | refs |
+|-------|------|
+| E     | 3    |
+| D     | 2    |
+| F     | 2    |
+| A,B,C | 1    |
+
+Assuming we are constrained to 4 layers, the current algorithm would yield these layers:
+
+```
+L1: E
+L2: D
+L3: F
+L4: A, B, C
+```
+
+The initial proposal for this design is that additional data should be
+considered in addition to the total number of references, in particular a
+distinction should be made between direct and indirect references. Packages that
+are only referenced indirectly should be merged with their parents.
+
+This yields the following table:
+
+| pkg   | direct | indirect |
+|-------|--------|----------|
+| E     | 3      | 3        |
+| D     | 2      | 2        |
+| F     | *1*    | 2        |
+| A,B,C | 1      | 1        |
+
+Despite having two indirect references, F is in fact only being referred to
+once. Assuming that we have no other data available outside of this graph, we
+have no reason to assume that F has any popularity outside of the scope of D.
+This might yield the following layers:
+
+```
+L1: E
+L2: D, F
+L3: A
+L4: B, C
+```
+
+D and F were grouped, while the top-level references (i.e. the packages
+explicitly requested by the user) were split up.
+
+An assumption is introduced here to justify this split: The top-level packages
+is what the user is modifying directly, and those groupings are likely
+unpredictable. Thus it is opportune to not group top-level packages in the same
+layer.
+
+This raises a new question: Can we make better decisions about where to split
+the top-level?
+
+## (Even closer to) ideal layering using (even) more data
+
+So far when deciding layer layouts, only information immediately available in
+the build graph of the image has been considered. We do however have much more
+information available, as we have both the entire nixpkgs-tree and potentially
+other information (such as download statistics).
+
+We can calculate the total number of references to any derivation in nixpkgs and
+use that to rank the popularity of each package. Packages within some percentile
+can then be singled out as good candidates for a separate layer.
+
+When faced with a splitting decision such as in the last section, this data can
+aid the decision. Assume for example that package B in the above is actually
+`openssl`, which is a very popular package. Taking this into account would
+instead yield the following layers:
+
+```
+L1: E,
+L2: D, F
+L3: B,
+L4: A, C
+```
+
+## Layer budgets and download size considerations
+
+As described in the introduction, there is a finite amount of layers available
+for each image (the “layer budget”). When calculating the layer distribution, we
+might end up with the “ideal” list of layers that we would like to create. Using
+our previous example:
+
+```
+L1: E,
+L2: D, F
+L3: A
+L4: B
+L5: C
+```
+
+If we only have a layer budget of 4 available, something needs to be merged into
+the same layer. To make a decision here we could consider only the package
+popularity, but there is in fact another piece of information that has not come
+up yet: The actual size of the package.
+
+Presumably a user would not mind downloading a library that is a few kilobytes
+in size repeatedly, but they would if it was a 200 megabyte binary instead.
+
+Conversely if a large binary was successfully cached, but an extremely popular
+small library is not, the total download size might also grow to irritating
+levels.
+
+To avoid this we can calculate a merge rating:
+
+    merge_rating(pkg) = popularity_percentile(pkg) × size(pkg.subtree)
+
+Packages with a low merge rating would be merged together before packages with
+higher merge ratings.
+
+## Implementation
+
+There are two primary components of the implementation:
+
+1. The layering component which, given an image specification, decides the image
+   layers.
+
+2. The popularity component which, given the entire nixpkgs-tree, calculates the
+   popularity of packages.
+
+## Layering component
+
+It turns out that graph theory’s concept of [dominator trees][] maps reasonably
+well onto the proposed idea of separating direct and indirect dependencies. This
+becomes visible when creating the dominator tree of a simple example:
+
+![Example without extra edges](/static/img/nixery/example_plain.webp)
+
+Before calculating the dominator tree, we inspect each node and insert extra
+edges from the root for packages that match a certain popularity or size
+threshold. In this example, G is popular and an extra edge is inserted:
+
+![Example with extra edges](/static/img/nixery/example_extra.webp)
+
+Calculating the dominator tree of this graph now yields our ideal layer
+distribution:
+
+![Dominator tree of example](/static/img/nixery/dominator.webp)
+
+The nodes immediately dominated by the root node can now be “harvested” as image
+layers, and merging can be performed as described above until the result fits
+into the layer budget.
+
+To implement this, the layering component uses the [gonum/graph][] library which
+supports calculating dominator trees. The program is fed with Nix’s
+`exportReferencesGraph` (which contains the runtime dependency graph and runtime
+closure size) as well as the popularity data and layer budget. It returns a list
+of layers, each specifying the paths it should contain.
+
+Nix invokes this program and uses the output to create a derivation for each
+layer, which is then built and returned to Nixery as usual.
+
+TIP: This is implemented in [`layers.go`][layers.go] in Nixery. The file starts
+with an explanatory comment that talks through the process in detail.
+
+## Popularity component
+
+The primary issue in calculating the popularity of each package in the tree is
+that we are interested in the runtime dependencies of a derivation, not its
+build dependencies.
+
+To access information about the runtime dependency, the derivation actually
+needs to be built by Nix - it can not be inferred because Nix does not know
+which store paths will still be referenced by the build output.
+
+However for packages that are cached in the NixOS cache, we can simply inspect
+the `narinfo`-files and use those to determine popularity.
+
+Not every package in nixpkgs is cached, but we can expect all *popular* packages
+to be cached. Relying on the cache should therefore be reasonable and avoids us
+having to rebuild/download all packages.
+
+The implementation will read the `narinfo` for each store path in the cache at a
+given commit and create a JSON-file containing the total reference count per
+package.
+
+For the public Nixery instance, these popularity files will be distributed via a
+GCS bucket.
+
+TIP: This is implemented in [popcount][] in Nixery.
+
+--------
+
+Hopefully this detailed design review was useful to you. You can also watch [my
+NixCon talk][talk] about Nixery for a review of some of this, and some demos.
+
+[Nixery]: https://cs.tvl.fyi/depot/-/tree/tools/nixery
+[grhmc]: https://grahamc.com/blog/nix-and-layered-docker-images
+[Nix]: https://nixos.org/nix
+[registry protocols]: https://github.com/opencontainers/distribution-spec/blob/master/spec.md
+[nixery.dev]: https://nixery.dev
+[dominator trees]: https://en.wikipedia.org/wiki/Dominator_(graph_theory)
+[gonum/graph]: https://godoc.org/gonum.org/v1/gonum/graph
+[layers.go]: https://cs.tvl.fyi/depot/-/blob/tools/nixery/builder/layers.go
+[popcount]: https://cs.tvl.fyi/depot/-/tree/tools/nixery/popcount
+[talk]: https://www.youtube.com/watch?v=pOI9H4oeXqA
diff --git a/users/tazjin/blog/posts/nsa-zettabytes.md b/users/tazjin/blog/posts/nsa-zettabytes.md
new file mode 100644
index 0000000000..f8b326f2fb
--- /dev/null
+++ b/users/tazjin/blog/posts/nsa-zettabytes.md
@@ -0,0 +1,93 @@
+I've been reading a few discussions on Reddit about the new NSA data
+centre that is being built and stumbled upon [this
+post](http://www.reddit.com/r/restorethefourth/comments/1jf6cx/the_guardian_releases_another_leaked_document_nsa/cbe5hnc),
+putting its alleged storage capacity at *5 zettabytes*.
+
+That seems to be a bit much which I tried to explain to that guy, but I
+was quickly blocked by the common conspiracy argument that government
+technology is somehow far beyond the wildest dreams of us mere mortals -
+thus I wrote a very long reply that will most likely never be seen by
+anybody. Therefore I've decided to repost it here.
+
+------------------------------------------------------------------------
+
+I feel like I've entered /r/conspiracy. Please have some facts (and do
+read them!)
+
+A one terabyte SSD (I assume that\'s what you meant by flash-drive)
+would require 5000000000 of those. That is *five billion* of those flash
+drives. Can you visualise how much five billion flash-drives are?
+
+A single SSD is roughly 2cm\*13cm\*13cm with an approximate weight of
+80g. That would make 400 000 metric tons of SSDs, a weight equivalent to
+*over one thousand Boeing 747 airplanes*. Even if we assume that they
+solder the flash chips directly onto some kind of controller (which also
+weighs something), the raw material for that would be completely insane.
+
+Another visualization: If you stacked 5 billion SSDs on top of each
+other you would get an SSD tower that is a hundred thousand kilometres
+high, that is equivalent to 2,5 x the equatorial circumference of
+*Earth* or 62000 miles.
+
+The volume of those SSDs would be clocking in at 1690000000 cubic
+metres, more than the Empire State building. Are you still with me?
+
+Lets speak cost. The Samsung SSD that I assume you are referring to will
+clock in at \$600, lets assume that the NSA gets a discount when buying
+*five billion* of those and gets them at the cheap price of \$250. That
+makes 1.25 trillion dollars. That would be a significant chunk of the
+current US national debt.
+
+And all of this is just SSDs to stick into servers and storage units,
+which need a whole bunch of other equipment as well to support them -
+the cost would probably shoot up to something like 8 trillion dollars if
+they were to build this. It would with very high certainty be more than
+the annual production of SSDs (I can\'t find numbers on that
+unfortunately) and take up *slightly* more space than they have in the
+Utah data centre (assuming you\'re not going to tell me that it is in
+fact attached to an underground base that goes down to the core of the
+Earth).
+
+Lets look at the \"But the government has better technologies!\" idea.
+
+Putting aside the fact that the military *most likely* does not have a
+secret base on Mars that deals with advanced science that the rest of us
+can only dream of, and doing this under the assumption that they do have
+this base, lets assume that they build a storage chip that stores 100TB.
+This reduces the amount of needed chips to \"just\" 50 million, lets say
+they get 10 of those into a server / some kind of specialized storage
+unit and we only need 5 million of those specially engineered servers,
+with custom connectors, software, chips, storage, most likely also power
+sources and whatever - 10 million completely custom units built with
+technology that is not available to the market. Google is estimated to
+have about a million servers in total, I don\'t know exactly in how many
+data centres those are placed but numbers I heard recently said that
+it\'s about 40. When Apple assembles a new iPhone model they need
+massive factories with thousands of workers and supplies from many
+different countries, over several months, to assemble just a few million
+units for their launch month.
+
+You are seriously proposing that the NSA is better than Google and Apple
+and the rest of the tech industry, world-wide, combined at designing
+*everything* in tech, manufacturing *everything* in tech, without *any*
+information about that leaking and without *any* of the science behind
+it being known? That\'s not just insane, that\'s outright impossible.
+
+And we haven\'t even touched upon how they would route the necessary
+amounts of bandwidth (crazy insane) to save *the entire internet* into
+that data center.
+
+------------------------------------------------------------------------
+
+I\'m not saying that the NSA is not building a data center to store
+surveillance information, to have more capacity to spy on people and all
+that - I\'m merely making the point that the extent in which conspiracy
+sites say they do this vastly overestimates their actual abilities. They
+don\'t have magic available to them! Instead of making up insane figures
+like that you should focus on what we actually know about their
+operations, because using those figures in a debate with somebody who is
+responsible for this (and knows what they\'re talking about) will end
+with you being destroyed - nobody will listen to the rest of what
+you\'re saying when that happens.
+
+\"Stick to the facts\" is valid for our side as well.
diff --git a/users/tazjin/blog/posts/reversing-watchguard-vpn.md b/users/tazjin/blog/posts/reversing-watchguard-vpn.md
new file mode 100644
index 0000000000..8968dc8645
--- /dev/null
+++ b/users/tazjin/blog/posts/reversing-watchguard-vpn.md
@@ -0,0 +1,158 @@
+TIP: WatchGuard has
+[responded](https://www.reddit.com/r/netsec/comments/5tg0f9/reverseengineering_watchguard_mobile_vpn/dds6knx/)
+to this post on Reddit. If you haven\'t read the post yet I\'d recommend
+doing that first before reading the response to have the proper context.
+
+------------------------------------------------------------------------
+
+One of my current clients makes use of
+[WatchGuard](http://www.watchguard.com/help/docs/fireware/11/en-US/Content/en-US/mvpn/ssl/mvpn_ssl_client-install_c.html)
+Mobile VPN software to provide access to the internal network.
+
+Currently WatchGuard only provides clients for OS X and Windows, neither
+of which I am very fond of. In addition an OpenVPN configuration file is
+provided, but it quickly turned out that this was only a piece of the
+puzzle.
+
+The problem is that this VPN setup is secured using 2-factor
+authentication (good!), but it does not use OpenVPN's default
+[challenge/response](https://openvpn.net/index.php/open-source/documentation/miscellaneous/79-management-interface.html)
+functionality to negotiate the credentials.
+
+Connecting with the OpenVPN config that the website supplied caused the
+VPN server to send me a token to my phone, but I simply couldn't figure
+out how to supply it back to the server. In a normal challenge/response
+setting the token would be supplied as the password on the second
+authentication round, but the VPN server kept rejecting that.
+
+Other possibilities were various combinations of username&password
+(I've seen a lot of those around) so I tried a whole bunch, for example
+`$password:$token` or even a `sha1(password, token)` - to no avail.
+
+At this point it was time to crank out
+[Hopper](https://www.hopperapp.com/) and see what's actually going on
+in the official OS X client - which uses OpenVPN under the hood!
+
+Diving into the client
+----------------------
+
+The first surprise came up right after opening the executable: It had
+debug symbols in it - and was written in Objective-C!
+
+![Debug symbols](/static/img/watchblob_1.webp)
+
+A good first step when looking at an application binary is going through
+the strings that are included in it, and the WatchGuard client had a lot
+to offer. Among the most interesting were a bunch of URIs that looked
+important:
+
+![Some URIs](/static/img/watchblob_2.webp)
+
+I started with the first one
+
+    %@?action=sslvpn_download&filename=%@&fw_password=%@&fw_username=%@
+
+and just curled it on the VPN host, replacing the username and
+password fields with bogus data and the filename field with
+`client.wgssl` - another string in the executable that looked like a
+filename.
+
+To my surprise this endpoint immediately responded with a GZIPed file
+containing the OpenVPN config, CA certificate, and the client
+*certificate and key*, which I previously thought was only accessible
+after logging in to the web UI - oh well.
+
+The next endpoint I tried ended up being a bit more interesting still:
+
+    /?action=sslvpn_logon&fw_username=%@&fw_password=%@&style=fw_logon_progress.xsl&fw_logon_type=logon&fw_domain=Firebox-DB
+
+Inserting the correct username and password into the query parameters
+actually triggered the process that sent a token to my phone. The
+response was a simple XML blob:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<resp>
+  <action>sslvpn_logon</action>
+  <logon_status>4</logon_status>
+  <auth-domain-list>
+    <auth-domain>
+      <name>RADIUS</name>
+    </auth-domain>
+  </auth-domain-list>
+  <logon_id>441</logon_id>
+  <chaStr>Enter Your 6 Digit Passcode </chaStr>
+</resp>
+```
+
+Somewhat unsurprisingly that `chaStr` field is actually the challenge
+string displayed in the client when logging in.
+
+This was obviously going in the right direction so I proceeded to the
+procedures making use of this string. The first step was a relatively
+uninteresting function called `-[VPNController sslvpnLogon]` which
+formatted the URL, opened it and checked whether the `logon_status` was
+`4` before proceeding with the `logon_id` and `chaStr` contained in the
+response.
+
+*(Code snippets from here on are Hopper's pseudo-Objective-C)*
+
+![sslvpnLogon](/static/img/watchblob_3.webp)
+
+It proceeded to the function `-[VPNController processTokenPrompt]` which
+showed the dialog window into which the user enters the token, sent it
+off to the next URL and checked the `logon_status` again:
+
+(`r12` is the reference to the `VPNController` instance, i.e. `self`).
+
+![processTokenPrompt](/static/img/watchblob_4.webp)
+
+If the `logon_status` was `1` (apparently \"success\" here) it proceeded
+to do something quite interesting:
+
+![processTokenPrompt2](/static/img/watchblob_5.webp)
+
+The user's password was overwritten with the (verified) OTP token -
+before OpenVPN had even been started!
+
+Reading a bit more of the code in the subsequent
+`-[VPNController doLogin]` method revealed that it shelled out to
+`openvpn` and enabled the management socket, which makes it possible to
+remotely control an `openvpn` process by sending it commands over TCP.
+
+It then simply sent the username and the OTP token as the credentials
+after configuring OpenVPN with the correct config file:
+
+![doLogin](/static/img/watchblob_6.webp)
+
+... and the OpenVPN connection then succeeds.
+
+TL;DR
+-----
+
+Rather than using OpenVPN's built-in challenge/response mechanism, the
+WatchGuard client validates user credentials *outside* of the VPN
+connection protocol and then passes on the OTP token, which seems to be
+temporarily in a 'blessed' state after verification, as the user's
+password.
+
+I didn't check to see how much verification of this token is performed
+(does it check the source IP against the IP that performed the challenge
+validation?), but this certainly seems like a bit of a security issue -
+considering that an attacker on the same network would, if they time the
+attack right, only need your username and 6-digit OTP token to
+authenticate.
+
+Don't roll your own security, folks!
+
+Bonus
+-----
+
+The whole reason why I set out to do this is so I could connect to this
+VPN from Linux, so this blog post wouldn't be complete without a
+solution for that.
+
+To make this process really easy I've written a [little
+tool](https://github.com/tazjin/watchblob) that performs the steps
+mentioned above from the CLI and lets users know when they can
+authenticate using their OTP token.
diff --git a/users/tazjin/blog/posts/sick-in-sweden.md b/users/tazjin/blog/posts/sick-in-sweden.md
new file mode 100644
index 0000000000..0c43c5832d
--- /dev/null
+++ b/users/tazjin/blog/posts/sick-in-sweden.md
@@ -0,0 +1,26 @@
+I\'ve been sick more in the two years in Sweden than in the ten years
+before that.
+
+Why? I have a theory about it and after briefly discussing it with one
+of my roommates (who is experiencing the same thing) I\'d like to share
+it with you:
+
+Normally when people get sick, are coughing, have a fever and so on they
+take a few days off from work and stay at home. The reasons are twofold:
+You want to rest a bit in order to get rid of the disease and you want
+to *avoid infecting your co-workers*.
+
+In Sweden people will drag themselves into work anyways, because of a
+concept called the
+[karensdag](https://www.forsakringskassan.se/wps/portal/sjukvard/sjukskrivning_och_sjukpenning/karensdag_och_forstadagsintyg).
+The TL;DR of this is \'if you take days off sick you won\'t get paid for
+the first day, and only 80% of your salary on the remaining days\'.
+
+Many people are not willing to take that financial hit. In combination
+with Sweden\'s rather mediocre healthcare system you end up constantly
+being surrounded by sick people, not just in your own office but also on
+public transport and basically all other public places.
+
+Oh and the best thing about this? Swedish politicians [often ignore
+this](https://www.aftonbladet.se/nyheter/article10506886.ab) rule and
+just don\'t report their sick days. Nice.
diff --git a/users/tazjin/blog/posts/the-smu-problem.md b/users/tazjin/blog/posts/the-smu-problem.md
new file mode 100644
index 0000000000..f411e31160
--- /dev/null
+++ b/users/tazjin/blog/posts/the-smu-problem.md
@@ -0,0 +1,151 @@
+After having tested countless messaging apps over the years, being
+unsatisfied with most of them and finally getting stuck with
+[Telegram](https://telegram.org/) I have developed a little theory about
+messaging apps.
+
+SMU stands for *Security*, *Multi-Device* and *Usability*. Quite like
+the [CAP-theorem](https://en.wikipedia.org/wiki/CAP_theorem) I believe
+that you can - using current models - only solve two out of three things
+on this list. Let me elaborate what I mean by the individual points:
+
+**Security**: This is mainly about encryption of messages, not so much
+about hiding identities to third-parties. Commonly some kind of
+asymmetric encryption scheme. Verification of keys used must be possible
+for the user.
+
+**Multi-Device**: Messaging-app clients for multiple devices, with
+devices being linked to the same identifier, receiving the same messages
+and being independent of each other. A nice bonus is also an open
+protocol (like Telegram\'s) that would let people write new clients.
+
+**Usability**: Usability is a bit of a broad term, but what I mean by it
+here is handling contacts and identities. It should be easy to create
+accounts, give contact information to people and have everything just
+work in a somewhat automated fashion.
+
+Some categorisation of popular messaging apps:
+
+**SU**: Threema
+
+**MU**: Telegram, Google Hangouts, iMessage, Facebook Messenger
+
+**SM**:
+[Signal](https://gist.github.com/TheBlueMatt/d2fcfb78d29faca117f5)
+
+*Side note: The most popular messaging app - WhatsApp - only scores a
+single letter (U). This makes it completely uninteresting to me.*
+
+Let\'s talk about **SM** - which might contain the key to solving SMU.
+Two approaches are interesting here.
+
+The single key model
+--------------------
+
+In Signal there is a single identity key which can be used to register a
+device on the server. There exists a process for sharing this identity
+key from a primary device to a secondary one, so that the secondary
+device can register itself (see the link above for a description).
+
+This *almost* breaks M because there is still a dependence on a primary
+device and newly onboarded devices can not be used to onboard further
+devices. However, for lack of a better SM example I\'ll give it a pass.
+
+The other thing it obviously breaks is U as the process for setting it
+up is annoying and having to rely on the primary device is a SPOF (there
+might be a way to recover from a lost primary device, but I didn\'t find
+any information so far).
+
+The multiple key model
+----------------------
+
+In iMessage every device that a user logs into creates a new key pair
+and submits its public key to a per-account key pool. Senders fetch all
+available public keys for a recipient and encrypt to all of the keys.
+
+Devices that join can catch up on history by receiving it from other
+devices that use its public key.
+
+This *almost* solves all of SMU, but its compliance with S breaks due to
+the fact that the key pool is not auditable, and controlled by a
+third-party (Apple). How can you verify that they don\'t go and add
+another key to your pool?
+
+A possible solution
+-------------------
+
+Out of these two approaches I believe the multiple key one looks more
+promising. If there was a third-party handling the key pool but in a way
+that is verifiable, transparent and auditable that model could be used
+to solve SMU.
+
+The technology I have been thinking about for this is some kind of
+blockchain model and here\'s how I think it could work:
+
+1.  Bob installs the app and begins onboarding. The first device
+    generates its keypair, submits the public key and an account
+    creation request.
+
+2.  Bob\'s account is created on the messaging apps\' servers and a
+    unique identifier plus the fingerprint of the first device\'s public
+    key is written to the chain.
+
+3.  Alice sends a message to Bob, her device asks the messaging service
+    for Bob\'s account\'s identity and public keys. Her device verifies
+    the public key fingerprint against the one in the blockchain before
+    encrypting to it and sending the message.
+
+4.  Bob receives Alice\'s message on his first device.
+
+5.  Bob logs in to his account on a second device. The device generates
+    a key pair and sends the public key to the service, the service
+    writes it to the blockchain using its identifier.
+
+6.  The messaging service requests that Bob\'s first device signs the
+    second device\'s key and triggers a simple confirmation popup.
+
+7.  Bob confirms the second device on his first device. It signs the key
+    and writes the signature to the chain.
+
+8.  Alice sends another message, her device requests Bob\'s current keys
+    and receives the new key. It verifies that both the messaging
+    service and one of Bob\'s older devices have confirmed this key in
+    the chain. It encrypts the message to both keys and sends it on.
+
+9.  Bob receives Alice\'s message on both devices.
+
+After this the second device can request conversation history from the
+first one to synchronise old messages.
+
+Further devices added to an account can be confirmed by any of the
+devices already in the account.
+
+The messaging service could not add new keys for an account on its own
+because it does not control any of the private keys confirmed by the
+chain.
+
+In case all devices were lost, the messaging service could associate the
+account with a fresh identity in the block chain. Message history
+synchronisation would of course be impossible.
+
+Feedback welcome
+----------------
+
+I would love to hear some input on this idea, especially if anyone knows
+of an attempt to implement a similar model already. Possible attack
+vectors would also be really interesting.
+
+Until something like this comes to fruition, I\'ll continue using
+Telegram with GPG as the security layer when needed.
+
+**Update:** WhatsApp has launched an integration with the Signal guys
+and added their protocol to the official WhatsApp app. This means
+WhatsApp now firmly sits in the SU-category, but it still does not solve
+this problem.
+
+**Update 2:** Facebook Messenger has also integrated with Signal, but
+their secret chats do not support multi-device well (it is Signal
+afterall). This means it scores either SU or MU depending on which mode
+you use it in.
+
+An interesting service I have not yet evaluated properly is
+[Matrix](http://matrix.org/).
diff --git a/users/tazjin/default.nix b/users/tazjin/default.nix
new file mode 100644
index 0000000000..1b68b7127a
--- /dev/null
+++ b/users/tazjin/default.nix
@@ -0,0 +1,30 @@
+# //users/tazjin-specific CI configuration.
+{ depot, pkgs, ... }:
+
+let
+  rustfmt = pkgs.writeShellScript "rustfmt-tazjin" ''
+    ${pkgs.fd}/bin/fd -e rs | \
+      ${pkgs.ripgrep}/bin/rg 'users/tazjin' | \
+      xargs ${pkgs.rustfmt}/bin/rustfmt --check --config-path users/tazjin
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  rustfmt = rustfmt.overrideAttrs (_: {
+    # rustfmt not respecting config atm, disable
+    meta.ci.skip = true;
+
+    meta.ci.extraSteps.rustfmt = {
+      command = rustfmt;
+    };
+  });
+
+  # Use a screen lock command that resets the keyboard layout
+  # before locking, to avoid locking me out when the layout is
+  # in Russian.
+  screenLock = pkgs.writeShellScriptBin "tazjin-screen-lock" ''
+    ${pkgs.xorg.setxkbmap}/bin/setxkbmap us
+    ${pkgs.xorg.setxkbmap}/bin/setxkbmap -option caps:super
+    exec ${pkgs.xsecurelock}/bin/xsecurelock
+  '';
+}
diff --git a/users/tazjin/dns/default.nix b/users/tazjin/dns/default.nix
new file mode 100644
index 0000000000..6c51cb5de4
--- /dev/null
+++ b/users/tazjin/dns/default.nix
@@ -0,0 +1,13 @@
+# Performs simple (local-only) validity checks on DNS zones.
+{ depot, pkgs, ... }:
+
+let
+  checkZone = zone: file: pkgs.runCommandNoCC "${zone}-check" { } ''
+    ${pkgs.bind}/bin/named-checkzone -i local ${zone} ${file} | tee $out
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  kontemplate-works = checkZone "kontemplate.works" ./kontemplate.works.zone;
+  tazj-in = checkZone "tazj.in" ./tazj.in.zone;
+}
diff --git a/users/tazjin/dns/import b/users/tazjin/dns/import
new file mode 100755
index 0000000000..8ea1d694c9
--- /dev/null
+++ b/users/tazjin/dns/import
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -ue
+
+# Imports a zone file into Google Cloud DNS
+readonly ZONE="${1}"
+readonly FILE="${2}"
+
+gcloud dns record-sets import "${FILE}" \
+       --project composite-watch-759 \
+       --zone-file-format \
+       --delete-all-existing \
+       --zone "${ZONE}"
diff --git a/users/tazjin/dns/kontemplate.works.zone b/users/tazjin/dns/kontemplate.works.zone
new file mode 100644
index 0000000000..326a129d21
--- /dev/null
+++ b/users/tazjin/dns/kontemplate.works.zone
@@ -0,0 +1,15 @@
+;;  -*- mode: zone; -*-
+;; Do not delete these
+kontemplate.works. 21600 IN NS ns-cloud-d1.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d2.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d3.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d4.googledomains.com.
+kontemplate.works. 21600 IN SOA ns-cloud-d1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 259200 300
+
+;; Github site setup
+kontemplate.works. 60 IN A 185.199.108.153
+kontemplate.works. 60 IN A 185.199.109.153
+kontemplate.works. 60 IN A 185.199.110.153
+kontemplate.works. 60 IN A 185.199.111.153
+
+www.kontemplate.works. 60 IN CNAME tazjin.github.io.
diff --git a/users/tazjin/dns/tazj.in.zone b/users/tazjin/dns/tazj.in.zone
new file mode 100644
index 0000000000..43db5834a0
--- /dev/null
+++ b/users/tazjin/dns/tazj.in.zone
@@ -0,0 +1,33 @@
+;; -*- mode: zone; -*-
+;; Do not delete these
+tazj.in. 21600 IN NS ns-cloud-a1.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a2.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a3.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a4.googledomains.com.
+tazj.in. 21600 IN SOA ns-cloud-a1.googledomains.com. cloud-dns-hostmaster.google.com. 123 21600 3600 1209600 300
+
+;; Email setup
+tazj.in. 300 IN MX 1 aspmx.l.google.com.
+tazj.in. 300 IN MX 5 alt1.aspmx.l.google.com.
+tazj.in. 300 IN MX 5 alt2.aspmx.l.google.com.
+tazj.in. 300 IN MX 10 alt3.aspmx.l.google.com.
+tazj.in. 300 IN MX 10 alt4.aspmx.l.google.com.
+tazj.in. 300 IN TXT "v=spf1 include:_spf.google.com ~all"
+google._domainkey.tazj.in. 21600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9AphX/WJf8zVXQB5Jk0Ry1MI6ARa6vEyAoJtpjpt9Nbm7XU4qVWFRJm+L0VFd5EZ5YDPJTIZ90lJE3/B8vae2ipnoGbJbj8LaVSzzIPMbWmhPhX3fkLJFdkv7xRDMDn730iYXRlfkgv6GsqbS8vZt7mzxx4mpnePTI323yjRVkwRW8nGVbsmB25ZoG1/0985" "kg4mSYxzWeJ2ozCPFhT4sfMtZMXe/4QEkJz/zkod29KZfFJmLgEaf73WLdBX8kdwbhuh2PYXt/PwzUrRzF5ujVCsSaTZwdRVPErcf+yo4NvedelTjjs8rFVfoJiaDD1q2bQ3w0gDEBWPdC2VP7k9zwIDAQAB"
+
+;; Site verifications
+tazj.in. 3600 IN TXT "keybase-site-verification=gC4kzEmnLzY7F669PjN-pw2Cf__xHqcxQ08Gb-W9dhE"
+tazj.in. 300 IN TXT "google-site-verification=d3_MI1OwD6q2OT42Vvh0I9w2u3Q5KFBu-PieNUE1Fig"
+www.tazj.in. 3600 IN TXT "keybase-site-verification=ER8m_byyqAhzeIy9TyzkAU1H2p2yHtpvImuB_XrRF2U"
+
+;; Blog "storage engine"
+blog.tazj.in. 21600 IN NS ns-cloud-c1.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c2.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c3.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c4.googledomains.com.
+
+;; Webpage records setup
+tazj.in.       300 IN A 34.98.120.189
+www.tazj.in.   300 IN A 34.98.120.189
+git.tazj.in.   300 IN A 34.98.120.189
+files.tazj.in. 300 IN CNAME c.storage.googleapis.com.
diff --git a/users/tazjin/docs/install-zfs.md b/users/tazjin/docs/install-zfs.md
new file mode 100644
index 0000000000..415af30fd4
--- /dev/null
+++ b/users/tazjin/docs/install-zfs.md
@@ -0,0 +1,116 @@
+Current steps for my NixOS-on-ZFS installs with impermanence.
+
+## Target layout (example from tverskoy):
+
+Partitioning:
+
+```
+nvme0n1     259:0    0 238.5G  0 disk
+├─nvme0n1p1 259:1    0   128M  0 part /boot (type: EFI system)
+└─nvme0n1p2 259:2    0 238.3G  0 part       (type: Solaris root)
+```
+
+ZFS layout:
+
+```
+NAME                   USED  AVAIL     REFER  MOUNTPOINT
+zpool                  212G  19.0G      248K  /zpool
+zpool/ephemeral        668M  19.0G      192K  /zpool/ephemeral
+zpool/ephemeral/home   667M  19.0G      667M  legacy
+zpool/local           71.3G  19.0G      192K  /zpool/local
+zpool/local/nix       71.3G  19.0G     71.3G  legacy
+zpool/safe             140G  19.0G      192K  /zpool/safe
+zpool/safe/depot       414M  19.0G      414M  legacy
+zpool/safe/persist     139G  19.0G      139G  legacy
+```
+
+With reset-snapshots:
+
+```
+NAME                                USED  AVAIL     REFER  MOUNTPOINT
+zpool/ephemeral/home@blank          144K      -      192K  -
+zpool/ephemeral/home@tazjin-clean   144K      -      200K  -
+```
+
+Legacy mountpoints are used because the NixOS wiki advises that using
+ZFS own mountpoints might lead to issues with the mount order during
+boot.
+
+## Install steps
+
+1. First, get internet.
+
+2. Use `fdisk` to set up the partition layout above (fwiw, EFI type
+   should be `1`, Solaris root should be `66`).
+
+3. Format the first partition for EFI: `mkfs.fat -F32 -n EFI $part1`
+
+4. Init ZFS stuff:
+
+   ```
+   zpool create \
+     # 2 SSD only settings
+     -o ashift=12 \
+     -o autotrim=on \
+     -R /mnt \
+     -O canmount=off \
+     -O mountpoint=none \
+     -O acltype=posixacl \
+     -O compression=lz4 \
+     -O atime=off \
+     -O xattr=sa \
+     -O encryption=aes-256-gcm \
+     -O keylocation=prompt \
+     -O keyformat=passphrase \
+     zpool $part2
+   ```
+
+   Reserve some space for deletions:
+
+   ```
+   zfs create -o refreservation=1G -o mountpoint=none zpool/reserved
+   ```
+
+   Create the datasets as per the target layout:
+
+   ```
+   # Throwaway datasets
+   zfs create -o canmount=off -o mountpoint=none zpool/ephemeral
+   zfs create -o mountpoint=legacy zpool/ephemeral/root
+   zfs create -o mountpoint=legacy zpool/ephemeral/home
+
+   # Persistent datasets
+   zfs create -o canmount=off -o mountpoint=none zpool/persistent
+   zfs create -o mountpoint=legacy zpool/persistent/nix
+   zfs create -o mountpoint=legacy zpool/persistent/depot
+   zfs create -o mountpoint=legacy zpool/persistent/data
+   ```
+
+   Create completely blank snapshots of the ephemeral datasets:
+
+   ```
+   zfs snapshot zpool/ephemeral/root@blank
+   zfs snapshot zpool/ephemeral/home@blank
+   ```
+
+   The ephemeral home volume needs the user folder already set up with
+   permissions. Mount it and create the folder there:
+
+   ```
+   mount -t zfs zpool/ephemeral/root /mnt
+   mkdir /mnt/home
+   mount -t zfs zpool/ephemeral/home /mnt/home
+   mkdir /mnt/home/tazjin
+   chmod 1000:100 /mnt/home/tazjin
+   zfs snapshot zpool/ephemeral/home@tazjin-clean
+   ```
+
+   Now the persistent Nix store volume can be mounted and installation
+   can begin.
+
+   ```
+   mkdir /mnt/nix
+   mount -t zfs zpool/persistent/nix /mnt/nix
+   ```
+
+4. Configure & install NixOS as usual.
diff --git a/users/tazjin/dotfiles/config.fish b/users/tazjin/dotfiles/config.fish
new file mode 100644
index 0000000000..de2c99ae60
--- /dev/null
+++ b/users/tazjin/dotfiles/config.fish
@@ -0,0 +1,40 @@
+# Configure classic prompt
+set fish_color_user --bold blue
+set fish_color_cwd --bold white
+
+# Enable colour hints in VCS prompt:
+set __fish_git_prompt_showcolorhints yes
+set __fish_git_prompt_color_prefix purple
+set __fish_git_prompt_color_suffix purple
+
+# Fish configuration
+set fish_greeting ""
+set PATH $HOME/.local/bin $HOME/.cargo/bin $PATH
+
+# Editor configuration
+set -gx EDITOR "emacsclient"
+set -gx ALTERNATE_EDITOR "emacs -q -nw"
+set -gx VISUAL "emacsclient"
+
+# Miscellaneous
+eval (direnv hook fish)
+
+# Useful command aliases
+alias gpr 'git pull --rebase'
+alias gco 'git checkout'
+alias gf 'git fetch'
+alias gap 'git add -p'
+alias pbcopy 'xclip -selection clipboard'
+alias edit 'emacsclient -n'
+alias servedir 'nix-shell -p haskellPackages.wai-app-static --run warp'
+
+# Old habits die hard (also ls is just easier to type):
+alias ls 'exa'
+
+# Fix up nix-env & friends for Nix 2.0
+export NIX_REMOTE=daemon
+
+# Fix display of fish in emacs' term-mode:
+function fish_title
+  true
+end
diff --git a/users/tazjin/dotfiles/default.nix b/users/tazjin/dotfiles/default.nix
new file mode 100644
index 0000000000..9b783a9c85
--- /dev/null
+++ b/users/tazjin/dotfiles/default.nix
@@ -0,0 +1,3 @@
+_: {
+  dunstrc = ./dunstrc;
+}
diff --git a/users/tazjin/dotfiles/dunstrc b/users/tazjin/dotfiles/dunstrc
new file mode 100644
index 0000000000..2aa1141b6e
--- /dev/null
+++ b/users/tazjin/dotfiles/dunstrc
@@ -0,0 +1,54 @@
+[global]
+font = Iosevka Term 11
+origin = top-left
+markup = yes
+plain_text = no
+format = "<b>%s</b>\n%b"
+sort = no
+indicate_hidden = yes
+alignment = center
+bounce_freq = 0
+show_age_threshold = -1
+word_wrap = yes
+ignore_newline = no
+stack_duplicates = yes
+hide_duplicate_count = yes
+geometry = "300x50-15+49"
+shrink = no
+transparency = 5
+idle_threshold = 0
+monitor = 0
+follow = keyboard
+sticky_history = yes
+history_length = 15
+show_indicators = no
+line_height = 3
+separator_height = 2
+padding = 6
+horizontal_padding = 6
+separator_color = frame
+startup_notification = false
+dmenu = /usr/bin/dmenu -p dunst:
+browser = /usr/bin/firefox -new-tab
+icon_position = off
+max_icon_size = 80
+frame_width = 3
+frame_color = "#8EC07C"
+
+[urgency_low]
+frame_color = "#3B7C87"
+foreground = "#3B7C87"
+background = "#191311"
+timeout = 4
+
+[urgency_normal]
+frame_color = "#5B8234"
+foreground = "#5B8234"
+background = "#191311"
+timeout = 6
+
+[urgency_critical]
+frame_color = "#B7472A"
+foreground = "#B7472A"
+background = "#191311"
+timeout = 8
diff --git a/users/tazjin/dotfiles/msmtprc b/users/tazjin/dotfiles/msmtprc
new file mode 100644
index 0000000000..2af3b9433a
--- /dev/null
+++ b/users/tazjin/dotfiles/msmtprc
@@ -0,0 +1,15 @@
+defaults
+port 587
+tls on
+tls_trust_file /etc/ssl/certs/ca-certificates.crt
+
+# GSuite for tazj.in
+account tazjin
+host smtp.gmail.com
+port 587
+from mail@tazj.in
+auth oauthbearer
+user mail@tazj.in
+passwordeval "cat ~/mail/account.tazjin/.credentials.gmailieer.json | jq -r '.access_token'"
+
+account default : tazjin
diff --git a/users/tazjin/dotfiles/notmuch-config b/users/tazjin/dotfiles/notmuch-config
new file mode 100644
index 0000000000..a490774e63
--- /dev/null
+++ b/users/tazjin/dotfiles/notmuch-config
@@ -0,0 +1,21 @@
+# .notmuch-config - Configuration file for the notmuch mail system
+#
+# For more information about notmuch, see https://notmuchmail.org
+
+[database]
+path=/home/vincent/mail
+
+[user]
+name=Vincent Ambo
+primary_email=mail@tazj.in
+other_email=tazjin@gmail.com;
+
+[new]
+tags=unread;inbox;
+ignore=
+
+[search]
+exclude_tags=deleted;spam;draft;
+
+[maildir]
+synchronize_flags=true
diff --git a/users/tazjin/emacs/.gitignore b/users/tazjin/emacs/.gitignore
new file mode 100644
index 0000000000..7b666905f8
--- /dev/null
+++ b/users/tazjin/emacs/.gitignore
@@ -0,0 +1,11 @@
+.smex-items
+*token*
+auto-save-list/
+clones/
+elpa/
+irc.el
+local.el
+other/
+scripts/
+themes/
+*.elc
diff --git a/users/tazjin/emacs/README.md b/users/tazjin/emacs/README.md
new file mode 100644
index 0000000000..5c66733396
--- /dev/null
+++ b/users/tazjin/emacs/README.md
@@ -0,0 +1,7 @@
+tools/emacs
+===========
+
+This sub-folder builds my Emacs configuration, supplying packages from
+Nix and configuration from this folder.
+
+I use Emacs for many things (including as my desktop environment).
diff --git a/users/tazjin/emacs/config/bindings.el b/users/tazjin/emacs/config/bindings.el
new file mode 100644
index 0000000000..916d947756
--- /dev/null
+++ b/users/tazjin/emacs/config/bindings.el
@@ -0,0 +1,65 @@
+;; Font size
+(define-key global-map (kbd "C-=") 'increase-default-text-scale) ;; '=' because there lies '+'
+(define-key global-map (kbd "C--") 'decrease-default-text-scale)
+(define-key global-map (kbd "C-x C-0") 'set-default-text-scale)
+
+;; What does <tab> do? Well, it depends ...
+(define-key prog-mode-map (kbd "<tab>") #'company-indent-or-complete-common)
+
+;; imenu instead of insert-file
+(global-set-key (kbd "C-x i") 'imenu)
+
+;; Window switching. (C-x o goes to the next window)
+(windmove-default-keybindings) ;; Shift+direction
+
+;; Start eshell or switch to it if it's active.
+(global-set-key (kbd "C-x m") 'eshell)
+
+(global-set-key (kbd "C-x C-p") 'browse-repositories)
+(global-set-key (kbd "M-g M-g") 'goto-line-with-feedback)
+
+;; Miscellaneous editing commands
+(global-set-key (kbd "C-c w") 'whitespace-cleanup)
+(global-set-key (kbd "C-c a") 'align-regexp)
+(global-set-key (kbd "C-c m") 'mc/mark-dwim)
+
+;; Browse URLs (very useful for Gitlab's SSH output!)
+(global-set-key (kbd "C-c b p") 'browse-url-at-point)
+(global-set-key (kbd "C-c b b") 'browse-url)
+
+;; C-x REALLY QUIT (idea by @magnars)
+(global-set-key (kbd "C-x r q") 'save-buffers-kill-terminal)
+(global-set-key (kbd "C-x C-c") 'ignore)
+
+;; Open a file in project:
+(global-set-key (kbd "C-c f") 'project-find-file)
+
+;; Search in a project
+(global-set-key (kbd "C-c r g") 'rg-in-project)
+
+;; Open a file via magit:
+(global-set-key (kbd "C-c C-f") #'magit-find-file-worktree)
+
+;; Insert TODO comments
+(global-set-key (kbd "C-c t") 'insert-todo-comment)
+
+;; Make sharing music easier
+(global-set-key (kbd "s-s w") #'songwhip-lookup-url)
+
+;; Open the depot
+(global-set-key (kbd "s-s d") #'tvl-depot-status)
+
+;; Open any repo through zoxide
+(global-set-key (kbd "s-s r") #'zoxide-open-magit)
+
+;; Add subthread collapsing to notmuch-show.
+;;
+;; C-, closes a thread, C-. opens a thread. This mirrors stepping
+;; in/out of definitions.
+(define-key notmuch-show-mode-map (kbd "C-,") 'notmuch-show-open-or-close-subthread)
+(define-key notmuch-show-mode-map (kbd "C-.")
+  (lambda ()
+    (interactive)
+    (notmuch-show-open-or-close-subthread t))) ;; open
+
+(provide 'bindings)
diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el
new file mode 100644
index 0000000000..91eaf69ae5
--- /dev/null
+++ b/users/tazjin/emacs/config/custom.el
@@ -0,0 +1,27 @@
+(custom-set-variables
+ ;; custom-set-variables was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ '(ac-auto-show-menu 0.8)
+ '(ac-delay 0.2)
+ '(avy-background t)
+ '(cargo-process--enable-rust-backtrace 1)
+ '(company-auto-complete (quote (quote company-explicit-action-p)))
+ '(company-idle-delay 0.5)
+ '(custom-safe-themes
+   (quote
+    ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default)))
+ '(display-time-default-load-average nil)
+ '(display-time-interval 30)
+ '(elnode-send-file-program "/run/current-system/sw/bin/cat")
+ '(frame-brackground-mode (quote dark))
+ '(global-auto-complete-mode t)
+ '(kubernetes-commands-display-buffer-function (quote display-buffer))
+ '(lsp-gopls-server-path "/home/tazjin/go/bin/gopls")
+ '(magit-log-show-gpg-status t)
+ '(ns-alternate-modifier (quote none))
+ '(ns-command-modifier (quote control))
+ '(ns-right-command-modifier (quote meta))
+ '(require-final-newline (quote visit-save))
+ '(tls-program (quote ("gnutls-cli --x509cafile %t -p %p %h"))))
diff --git a/users/tazjin/emacs/config/desktop.el b/users/tazjin/emacs/config/desktop.el
new file mode 100644
index 0000000000..15c0b1f5e1
--- /dev/null
+++ b/users/tazjin/emacs/config/desktop.el
@@ -0,0 +1,372 @@
+;; -*- lexical-binding: t; -*-
+;;
+;; Configure desktop environment settings, including both
+;; window-management (EXWM) as well as additional system-wide
+;; commands.
+
+(require 'dash)
+(require 'exwm)
+(require 'exwm-config)
+(require 'exwm-randr)
+(require 'exwm-systemtray)
+(require 'exwm-xim )
+(require 'f)
+(require 's)
+
+(defcustom tazjin--screen-lock-command "tazjin-screen-lock"
+  "Command to execute for locking the screen."
+  :group 'tazjin)
+
+(defcustom tazjin--backlight-increase-command "light -A 4"
+  "Command to increase screen brightness."
+  :group 'tazjin)
+
+(defcustom tazjin--backlight-decrease-command "light -U 4"
+  "Command to decrease screen brightness."
+  :group 'tazjin)
+
+(defun pactl (cmd)
+  (shell-command (concat "pactl " cmd))
+  (message "Volume command: %s" cmd))
+
+(defun volume-mute () (interactive) (pactl "set-sink-mute @DEFAULT_SINK@ toggle"))
+(defun volume-up () (interactive) (pactl "set-sink-volume @DEFAULT_SINK@ +5%"))
+(defun volume-down () (interactive) (pactl "set-sink-volume @DEFAULT_SINK@ -5%"))
+
+(defun brightness-up ()
+  (interactive)
+  (shell-command tazjin--backlight-increase-command)
+  (message "Brightness increased"))
+
+(defun brightness-down ()
+  (interactive)
+  (shell-command tazjin--backlight-decrease-command)
+  (message "Brightness decreased"))
+
+(defun set-xkb-layout (layout)
+  "Set the current X keyboard layout."
+
+  (shell-command (format "setxkbmap %s" layout))
+  (shell-command "setxkbmap -option caps:super")
+  (message "Set X11 keyboard layout to '%s'" layout))
+
+(defun lock-screen ()
+  (interactive)
+  (set-xkb-layout "us")
+  (deactivate-input-method)
+  (shell-command tazjin--screen-lock-command))
+
+(defun create-window-name ()
+  "Construct window names to be used for EXWM buffers by
+  inspecting the window's X11 class and title.
+
+  A lot of commonly used applications either create titles that
+  are too long by default, or in the case of web
+  applications (such as Cider) end up being constructed in
+  awkward ways.
+
+  To avoid this issue, some rewrite rules are applied for more
+  human-accessible titles."
+
+  (pcase (list (or exwm-class-name "unknown") (or exwm-title "unknown"))
+    ;; In Cider windows, rename the class and keep the workspace/file
+    ;; as the title.
+    (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider" title))) title))
+     (format "Cider<%s>" (s-chop-suffix " - Cider" title)))
+    (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider V" title))) title))
+     (format "Cider V<%s>" (s-chop-suffix " - Cider V" title)))
+
+    ;; Attempt to detect IRCCloud windows via their title, which is a
+    ;; combination of the channel name and network.
+    ;;
+    ;; This is what would often be referred to as a "hack". The regexp
+    ;; will not work if a network connection buffer is selected in
+    ;; IRCCloud, but since the title contains no other indication that
+    ;; we're dealing with an IRCCloud window
+    (`("Google-chrome"
+       ,(and (pred (lambda (title)
+                     (s-matches? "^[\*\+]\s#[a-zA-Z0-9/\-]+\s\|\s[a-zA-Z\.]+$" title)))
+             title))
+     (format "IRCCloud<%s>" title))
+
+    ;; For other Chrome windows, make the title shorter.
+    (`("Google-chrome" ,title)
+     (format "Chrome<%s>" (s-truncate 42 (s-chop-suffix " - Google Chrome" title))))
+
+    ;; Gnome-terminal -> Term
+    (`("Gnome-terminal" ,title)
+     ;; fish-shell buffers contain some unnecessary whitespace and
+     ;; such before the current working directory. This can be
+     ;; stripped since most of my terminals are fish shells anyways.
+     (format "Term<%s>" (s-trim-left (s-chop-prefix "fish" title))))
+
+    ;; Quassel buffers
+    ;;
+    ;; These have a title format that looks like:
+    ;; "Quassel IRC - #tvl (hackint) — Quassel IRC"
+    (`("quassel" ,title)
+     (progn
+       (if (string-match
+            (rx "Quassel IRC - "
+                (group (one-or-more (any alnum "[" "]" "&" "-" "#"))) ;; <-- channel name
+                " (" (group (one-or-more (any ascii space))) ")" ;; <-- network name
+                " — Quassel IRC")
+            title)
+           (format "Quassel<%s>" (match-string 2 title))
+         title)))
+
+    ;; For any other application, a name is constructed from the
+    ;; window's class and name.
+    (`(,class ,title) (format "%s<%s>" class (s-truncate 12 title)))))
+
+;; EXWM launch configuration
+;;
+;; This used to use use-package, but when something breaks use-package
+;; it doesn't exactly make debugging any easier.
+
+(let ((titlef (lambda ()
+                (exwm-workspace-rename-buffer (create-window-name)))))
+  (add-hook 'exwm-update-class-hook titlef)
+  (add-hook 'exwm-update-title-hook titlef))
+
+(fringe-mode 3)
+(exwm-enable)
+
+;; Create 10 EXWM workspaces
+(setq exwm-workspace-number 10)
+
+;; 's-N': Switch to certain workspace, but switch back to the previous
+;; one when tapping twice (emulates i3's `back_and_forth' feature)
+(defvar *exwm-workspace-from-to* '(-1 . -1))
+(defun exwm-workspace-switch-back-and-forth (target-idx)
+  ;; If the current workspace is the one we last jumped to, and we are
+  ;; asked to jump to it again, set the target back to the previous
+  ;; one.
+  (when (and (eq exwm-workspace-current-index (cdr *exwm-workspace-from-to*))
+             (eq target-idx exwm-workspace-current-index))
+    (setq target-idx (car *exwm-workspace-from-to*)))
+
+  (setq *exwm-workspace-from-to*
+        (cons exwm-workspace-current-index target-idx))
+
+  (exwm-workspace-switch-create target-idx))
+
+(dotimes (i 10)
+  (exwm-input-set-key (kbd (format "s-%d" i))
+                      `(lambda ()
+                         (interactive)
+                         (exwm-workspace-switch-back-and-forth ,i))))
+
+;; Implement MRU functionality for EXWM workspaces, making it possible
+;; to jump to the previous/next workspace very easily.
+(defvar *recent-workspaces* nil
+  "List of the most recently used EXWM workspaces.")
+
+(defvar *workspace-jumping-to* nil
+  "What offset in the workspace history are we jumping to?")
+
+(defvar *workspace-history-position* 0
+  "Where in the workspace history are we right now?")
+
+(defun update-recent-workspaces ()
+  "Hook to run on every workspace switch which will prepend the new
+workspace to the MRU list, unless we are already on that
+workspace. Does not affect the MRU list if a jump is
+in-progress."
+
+  (if *workspace-jumping-to*
+      (setq *workspace-history-position* *workspace-jumping-to*
+            *workspace-jumping-to* nil)
+
+    ;; reset the history position to the front on a normal jump
+    (setq *workspace-history-position* 0)
+
+    (unless (eq exwm-workspace-current-index (car *recent-workspaces*))
+      (setq *recent-workspaces* (cons exwm-workspace-current-index
+                                      (-take 9 *recent-workspaces*))))))
+
+(add-to-list 'exwm-workspace-switch-hook #'update-recent-workspaces)
+
+(defun switch-to-previous-workspace ()
+  "Switch to the previous workspace in the MRU workspace list."
+  (interactive)
+
+  (let* (;; the previous workspace is one position further down in the
+         ;; workspace history
+         (position (+ *workspace-history-position* 1))
+         (target-idx (elt *recent-workspaces* position)))
+    (if (not target-idx)
+        (message "No previous workspace in history!")
+
+      (setq *workspace-jumping-to* position)
+      (exwm-workspace-switch target-idx))))
+
+(exwm-input-set-key (kbd "s-b") #'switch-to-previous-workspace)
+
+(defun switch-to-next-workspace ()
+  "Switch to the next workspace in the MRU workspace list."
+  (interactive)
+
+  (if (= *workspace-history-position* 0)
+      (message "No next workspace in history!")
+    (let* (;; The next workspace is one position further up in the
+           ;; history. This always exists unless someone messed with
+           ;; it.
+           (position (- *workspace-history-position* 1))
+           (target-idx (elt *recent-workspaces* position)))
+      (setq *workspace-jumping-to* position)
+      (exwm-workspace-switch target-idx))))
+
+(exwm-input-set-key (kbd "s-f") #'switch-to-next-workspace)
+
+;; Provide a binding for jumping to a buffer on a workspace.
+(defun exwm-jump-to-buffer ()
+  "Jump to a workspace on which the target buffer is displayed."
+  (interactive)
+  (let ((exwm-layout-show-all-buffers nil)
+        (initial exwm-workspace-current-index))
+    (call-interactively #'exwm-workspace-switch-to-buffer)
+    ;; After jumping, update the back-and-forth list like on a direct
+    ;; index jump.
+    (when (not (eq initial exwm-workspace-current-index))
+      (setq *exwm-workspace-from-to*
+            (cons initial exwm-workspace-current-index)))))
+
+(exwm-input-set-key (kbd "C-c j") #'exwm-jump-to-buffer)
+
+;; Launch applications / any command with completion (dmenu style!)
+(exwm-input-set-key (kbd "s-d") #'counsel-linux-app)
+(exwm-input-set-key (kbd "s-x") #'run-external-command)
+(exwm-input-set-key (kbd "s-p") #'password-store-lookup)
+
+;; Add X11 terminal selector to a key
+(exwm-input-set-key (kbd "C-x t") #'ts/switch-to-terminal)
+
+;; Toggle between line-mode / char-mode
+(exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard)
+
+;; Volume keys
+(exwm-input-set-key (kbd "<XF86AudioMute>") #'volume-mute)
+(exwm-input-set-key (kbd "<XF86AudioRaiseVolume>") #'volume-up)
+(exwm-input-set-key (kbd "<XF86AudioLowerVolume>") #'volume-down)
+
+;; Brightness keys
+(exwm-input-set-key (kbd "<XF86MonBrightnessDown>") #'brightness-down)
+(exwm-input-set-key (kbd "<XF86MonBrightnessUp>") #'brightness-up)
+(exwm-input-set-key (kbd "<XF86Display>") #'lock-screen)
+
+;; Shortcuts for switching between keyboard layouts
+(defmacro bind-xkb (lang key)
+  `(exwm-input-set-key (kbd (format "s-%s" ,key))
+                       (lambda ()
+                         (interactive)
+                         (set-xkb-layout ,lang))))
+
+(bind-xkb "us" "k u")
+(bind-xkb "de" "k d")
+(bind-xkb "no" "k n")
+(bind-xkb "ru" "k r")
+(bind-xkb "se" "k s")
+
+;; These are commented out because Emacs no longer starts (??) if
+;; they're set at launch.
+;;
+(bind-xkb "us" "л г")
+(bind-xkb "de" "л в")
+(bind-xkb "no" "л т")
+(bind-xkb "ru" "л к")
+
+;; Configuration of EXWM input method handling for X applications
+(exwm-xim-enable)
+(setq default-input-method "russian-computer")
+(push ?\C-\\ exwm-input-prefix-keys)
+
+;; Line-editing shortcuts
+(exwm-input-set-simulation-keys
+ '(([?\C-d] . delete)
+   ([?\C-w] . ?\C-c)))
+
+;; Show time & battery status in the mode line
+(display-time-mode)
+(display-battery-mode)
+
+;; enable display of X11 system tray within Emacs
+(exwm-systemtray-enable)
+
+;; Configure xrandr (multi-monitor setup).
+
+(defun set-randr-config (screens)
+  (setq exwm-randr-workspace-monitor-plist
+        (-flatten (-map (lambda (screen)
+                          (-map (lambda (screen-id) (list screen-id (car screen))) (cdr screen)))
+                        screens))))
+
+;; Layouts for Tverskoy (X13 AMD laptop)
+(defun randr-tverskoy-layout-single ()
+  "Laptop screen only!"
+  (interactive)
+  (set-randr-config '(("eDP" (number-sequence 0 9))))
+  (shell-command "xrandr --output eDP --auto --primary")
+  (shell-command "xrandr --output HDMI-A-0 --off")
+  (exwm-randr-refresh))
+
+(defun randr-tverskoy-split-workspace ()
+  "Split the workspace across two screens, assuming external to the left."
+  (interactive)
+  (set-randr-config
+   '(("HDMI-A-0" 1 2 3 4 5 6 7)
+     ("eDP" 8 9 0)))
+
+  (shell-command "xrandr --output HDMI-A-0 --left-of eDP --auto")
+  (exwm-randr-refresh))
+
+(defun randr-tverskoy-tv ()
+  "Split off a workspace to the TV over HDMI."
+  (interactive)
+  (set-randr-config
+   '(("eDP" 1 2 3 4 5 6 7 8 9)
+     ("HDMI-A-0" 0)))
+
+  (shell-command "xrandr --output HDMI-A-0 --left-of eDP --mode 1920x1080")
+  (exwm-randr-refresh))
+
+;; Layouts for frog (desktop)
+
+(defun randr-frog-layout-right-only ()
+  "Use only the right screen on frog."
+  (interactive)
+  (set-randr-config `(("DisplayPort-0" ,(number-sequence 0 9))))
+  (shell-command "xrandr --output DisplayPort-0 --off")
+  (shell-command "xrandr --output DisplayPort-1 --auto --primary"))
+
+(defun randr-frog-layout-both ()
+  "Use the left and right screen on frog."
+  (interactive)
+  (set-randr-config `(("DisplayPort-0" 1 2 3 4 5)
+                      ("DisplayPort-1" 6 7 8 9 0)))
+
+  (shell-command "xrandr --output DisplayPort-0 --auto --primary --left-of DisplayPort-1")
+  (shell-command "xrandr --output DisplayPort-1 --auto --right-of DisplayPort-0 --rotate left"))
+
+(pcase (s-trim (shell-command-to-string "hostname"))
+  ("tverskoy"
+   (exwm-input-set-key (kbd "s-m s") #'randr-tverskoy-layout-single)
+   (exwm-input-set-key (kbd "s-m 2") #'randr-tverskoy-split-workspace))
+
+  ("frog"
+   (exwm-input-set-key (kbd "s-m b") #'randr-frog-layout-both)
+   (exwm-input-set-key (kbd "s-m r") #'randr-frog-layout-right-only)))
+
+;; Notmuch shortcuts as EXWM globals
+;; (g m => gmail)
+(exwm-input-set-key (kbd "s-g m") #'notmuch)
+(exwm-input-set-key (kbd "s-g M") #'counsel-notmuch)
+
+(exwm-randr-enable)
+
+;; Let buffers move seamlessly between workspaces by making them
+;; accessible in selectors on all frames.
+(setq exwm-workspace-show-all-buffers t)
+(setq exwm-layout-show-all-buffers t)
+
+(provide 'desktop)
diff --git a/users/tazjin/emacs/config/eshell-setup.el b/users/tazjin/emacs/config/eshell-setup.el
new file mode 100644
index 0000000000..0b23c5a2d1
--- /dev/null
+++ b/users/tazjin/emacs/config/eshell-setup.el
@@ -0,0 +1,68 @@
+;; EShell configuration
+
+(require 'eshell)
+
+;; Generic settings
+;; Hide banner message ...
+(setq eshell-banner-message "")
+
+;; Prompt configuration
+(defun clean-pwd (path)
+  "Turns a path of the form /foo/bar/baz into /f/b/baz
+   (inspired by fish shell)"
+  (let* ((hpath (replace-regexp-in-string home-dir
+                                          "~"
+                                          path))
+         (current-dir (split-string hpath "/"))
+	 (cdir (last current-dir))
+	 (head (butlast current-dir)))
+    (concat (mapconcat (lambda (s)
+			 (if (string= "" s) nil
+			   (substring s 0 1)))
+		       head
+		       "/")
+	    (if head "/" nil)
+	    (car cdir))))
+
+(defun vcprompt (&optional args)
+  "Call the external vcprompt command with optional arguments.
+   VCPrompt"
+  (replace-regexp-in-string
+   "\n" ""
+   (shell-command-to-string (concat  "vcprompt" args))))
+
+(defmacro with-face (str &rest properties)
+  `(propertize ,str 'face (list ,@properties)))
+
+(defun prompt-f ()
+  "EShell prompt displaying VC info and such"
+  (concat
+   (with-face (concat (clean-pwd (eshell/pwd)) " ") :foreground  "#96a6c8")
+   (if (= 0 (user-uid))
+       (with-face "#" :foreground "#f43841")
+     (with-face "$" :foreground "#73c936"))
+   (with-face " " :foreground "#95a99f")))
+
+
+(setq eshell-prompt-function 'prompt-f)
+(setq eshell-highlight-prompt nil)
+(setq eshell-prompt-regexp "^.+? \\((\\(git\\|svn\\|hg\\|darcs\\|cvs\\|bzr\\):.+?) \\)?[$#] ")
+
+;; Ignore version control folders in autocompletion
+(setq eshell-cmpl-cycle-completions nil
+      eshell-save-history-on-exit t
+      eshell-cmpl-dir-ignore "\\`\\(\\.\\.?\\|CVS\\|\\.svn\\|\\.git\\)/\\'")
+
+;; Load some EShell extensions
+(eval-after-load 'esh-opt
+  '(progn
+     (require 'em-term)
+     (require 'em-cmpl)
+     ;; More visual commands!
+     (add-to-list 'eshell-visual-commands "ssh")
+     (add-to-list 'eshell-visual-commands "tail")
+     (add-to-list 'eshell-visual-commands "sl")))
+
+(setq eshell-directory-name "~/.config/eshell/")
+
+(provide 'eshell-setup)
diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el
new file mode 100644
index 0000000000..602809138e
--- /dev/null
+++ b/users/tazjin/emacs/config/functions.el
@@ -0,0 +1,343 @@
+(require 'chart)
+(require 'dash)
+(require 'map)
+
+(defun load-file-if-exists (filename)
+  (if (file-exists-p filename)
+      (load filename)))
+
+(defun goto-line-with-feedback ()
+  "Show line numbers temporarily, while prompting for the line number input"
+  (interactive)
+  (unwind-protect
+      (progn
+        (setq-local display-line-numbers t)
+        (let ((target (read-number "Goto line: ")))
+          (avy-push-mark)
+          (goto-line target)))
+    (setq-local display-line-numbers nil)))
+
+;; These come from the emacs starter kit
+
+(defun esk-add-watchwords ()
+  (font-lock-add-keywords
+   nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)"
+          1 font-lock-warning-face t))))
+
+(defun esk-sudo-edit (&optional arg)
+  (interactive "p")
+  (if (or arg (not buffer-file-name))
+      (find-file (concat "/sudo:root@localhost:" (read-file-name "File: ")))
+    (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name))))
+
+;; Open the NixOS man page
+(defun nixos-man ()
+  (interactive)
+  (man "configuration.nix"))
+
+;; Get the nix store path for a given derivation.
+;; If the derivation has not been built before, this will trigger a build.
+(defun nix-store-path (derivation)
+  (let ((expr (concat "with import <nixos> {}; " derivation)))
+    (s-chomp (shell-command-to-string (concat "nix-build -E '" expr "'")))))
+
+(defun insert-nix-store-path ()
+  (interactive)
+  (let ((derivation (read-string "Derivation name (in <nixos>): ")))
+    (insert (nix-store-path derivation))))
+
+(defun toggle-force-newline ()
+  "Buffer-local toggle for enforcing final newline on save."
+  (interactive)
+  (setq-local require-final-newline (not require-final-newline))
+  (message "require-final-newline in buffer %s is now %s"
+           (buffer-name)
+           require-final-newline))
+
+(defun list-external-commands ()
+  "Creates a list of all external commands available on $PATH
+  while filtering NixOS wrappers."
+  (cl-loop
+   for dir in (split-string (getenv "PATH") path-separator)
+   when (and (file-exists-p dir) (file-accessible-directory-p dir))
+   for lsdir = (cl-loop for i in (directory-files dir t)
+                        for bn = (file-name-nondirectory i)
+                        when (and (not (s-contains? "-wrapped" i))
+                                  (not (member bn completions))
+                                  (not (file-directory-p i))
+                                  (file-executable-p i))
+                        collect bn)
+   append lsdir into completions
+   finally return (sort completions 'string-lessp)))
+
+(defvar external-command-flag-overrides
+  '(("google-chrome" . "--force-device-scale-factor=1.4"))
+
+  "This setting lets me add additional flags to specific commands
+  that are run interactively via `run-external-command'.")
+
+(defun run-external-command--handler (cmd)
+  "Execute the specified command and notify the user when it
+  finishes."
+    (let* ((extra-flags (cdr (assoc cmd external-command-flag-overrides)))
+           (cmd (if extra-flags (s-join " " (list cmd extra-flags)) cmd)))
+      (message "Starting %s..." cmd)
+      (set-process-sentinel
+       (start-process-shell-command cmd nil cmd)
+       (lambda (process event)
+         (when (string= event "finished\n")
+           (message "%s process finished." process))))))
+
+(defun run-external-command ()
+  "Prompts the user with a list of all installed applications and
+  lets them select one to launch."
+
+  (interactive)
+  (let ((external-commands-list (list-external-commands)))
+    (run-external-command--handler
+     (completing-read "Command: " external-commands-list
+                      nil                             ;; predicate
+                      t                               ;; require-match
+                      nil                             ;; initial-input
+                      ;; hist
+                      'external-commands-history))))
+
+(defun password-store-lookup (&optional password-store-dir)
+  "Interactive password-store lookup function that actually uses
+the GPG agent correctly."
+
+  (interactive)
+
+  (let* ((entry (completing-read "Copy password of entry: "
+                   (password-store-list (or password-store-dir
+                                            (password-store-dir)))
+                   nil ;; predicate
+                   t   ;; require-match
+                   ))
+         (password (auth-source-pass-get 'secret entry)))
+    (password-store-clear)
+    (kill-new password)
+    (setq password-store-kill-ring-pointer kill-ring-yank-pointer)
+    (message "Copied %s to the kill ring. Will clear in %s seconds."
+             entry (password-store-timeout))
+    (setq password-store-timeout-timer
+          (run-at-time (password-store-timeout)
+                       nil 'password-store-clear))))
+
+(defun browse-repositories ()
+  "Select a git repository and open its associated magit buffer."
+
+  (interactive)
+  (magit-status
+   (completing-read "Repository: " (magit-list-repos))))
+
+(defun bottom-right-window-p ()
+  "Determines whether the last (i.e. bottom-right) window of the
+  active frame is showing the buffer in which this function is
+  executed."
+  (let* ((frame (selected-frame))
+         (right-windows (window-at-side-list frame 'right))
+         (bottom-windows (window-at-side-list frame 'bottom))
+         (last-window (car (seq-intersection right-windows bottom-windows))))
+    (eq (current-buffer) (window-buffer last-window))))
+
+(defhydra mc/mark-more-hydra (:color pink)
+  ("<up>" mc/mmlte--up "Mark previous like this")
+  ("<down>" mc/mmlte--down "Mark next like this")
+  ("<left>" mc/mmlte--left (if (eq mc/mark-more-like-this-extended-direction 'up)
+                               "Skip past the cursor furthest up"
+                             "Remove the cursor furthest down"))
+  ("<right>" mc/mmlte--right (if (eq mc/mark-more-like-this-extended-direction 'up)
+                                 "Remove the cursor furthest up"
+                               "Skip past the cursor furthest down"))
+  ("f" nil "Finish selecting"))
+
+;; Mute the message that mc/mmlte wants to print on its own
+(advice-add 'mc/mmlte--message :around (lambda (&rest args) (ignore)))
+
+(defun mc/mark-dwim (arg)
+  "Select multiple things, but do what I mean."
+
+  (interactive "p")
+  (if (not (region-active-p)) (mc/mark-next-lines arg)
+    (if (< 1 (count-lines (region-beginning)
+                          (region-end)))
+        (mc/edit-lines arg)
+      ;; The following is almost identical to `mc/mark-more-like-this-extended',
+      ;; but uses a hydra (`mc/mark-more-hydra') instead of a transient key map.
+      (mc/mmlte--down)
+      (mc/mark-more-hydra/body))))
+
+(setq mc/cmds-to-run-for-all '(kill-region paredit-newline))
+
+(setq mc/cmds-to-run-once '(mc/mark-dwim
+                            mc/mark-more-hydra/mc/mmlte--down
+                            mc/mark-more-hydra/mc/mmlte--left
+                            mc/mark-more-hydra/mc/mmlte--right
+                            mc/mark-more-hydra/mc/mmlte--up
+                            mc/mark-more-hydra/mmlte--up
+                            mc/mark-more-hydra/nil))
+
+(defun memespace-region ()
+  "Make a meme out of it."
+
+  (interactive)
+  (let* ((start (region-beginning))
+         (end (region-end))
+         (memed
+          (message
+           (s-trim-right
+            (apply #'string
+                   (-flatten
+                    (nreverse
+                     (-reduce-from (lambda (acc x)
+                                     (cons (cons x (-repeat (+ 1 (length acc)) 32)) acc))
+                                   '()
+                                   (string-to-list (buffer-substring-no-properties start end))))))))))
+
+    (save-excursion (delete-region start end)
+                    (goto-char start)
+                    (insert memed))))
+
+(defun insert-todo-comment (prefix todo)
+  "Insert a comment at point with something for me to do."
+
+  (interactive "P\nsWhat needs doing? ")
+  (save-excursion
+    (move-end-of-line nil)
+    (insert (format " %s TODO(%s): %s"
+                    (s-trim-right comment-start)
+                    (if prefix (read-string "Who needs to do this? ")
+                      (getenv "USER"))
+                    todo))))
+
+;; Custom text scale adjustment functions that operate on the entire instance
+(defun modify-text-scale (factor)
+  (set-face-attribute 'default nil
+                      :height (+ (* factor 5) (face-attribute 'default :height))))
+
+(defun increase-default-text-scale (prefix)
+  "Increase default text scale in all Emacs frames, or just the
+  current frame if PREFIX is set."
+
+  (interactive "P")
+  (if prefix (text-scale-increase 1)
+    (modify-text-scale 1)))
+
+(defun decrease-default-text-scale (prefix)
+  "Increase default text scale in all Emacs frames, or just the
+  current frame if PREFIX is set."
+
+  (interactive "P")
+  (if prefix (text-scale-decrease 1)
+    (modify-text-scale -1)))
+
+(defun set-default-text-scale (prefix &optional to)
+  "Set the default text scale to the specified value, or the
+  default. Restores current frame's text scale only, if PREFIX is
+  set."
+
+  (interactive "P")
+  (if prefix (text-scale-adjust 0)
+    (set-face-attribute 'default nil :height (or to 120))))
+
+(defun scrot-select ()
+  "Take a screenshot based on a mouse-selection and save it to
+  ~/screenshots."
+  (interactive)
+  (shell-command "scrot '$a_%Y-%m-%d_%s.png' -s -e 'mv $f ~/screenshots/'"))
+
+(defun graph-unread-mails ()
+  "Create a bar chart of unread mails based on notmuch tags.
+  Certain tags are excluded from the overview."
+
+  (interactive)
+  (let ((tag-counts
+         (-keep (-lambda ((name . search))
+                  (let ((count
+                         (string-to-number
+                          (s-trim
+                           (notmuch-command-to-string "count" search "and" "tag:unread")))))
+                    (when (>= count 1) (cons name count))))
+                (notmuch-hello-generate-tag-alist '("unread" "signed" "attachment" "important")))))
+
+    (chart-bar-quickie
+     (if (< (length tag-counts) 6)
+         'vertical 'horizontal)
+     "Unread emails"
+     (-map #'car tag-counts) "Tag:"
+     (-map #'cdr tag-counts) "Count:")))
+
+(defun notmuch-show-open-or-close-subthread (&optional prefix)
+  "Open or close the subthread from (and including) the message at point."
+  (interactive "P")
+  (save-excursion
+    (let ((current-depth (map-elt (notmuch-show-get-message-properties) :depth 0)))
+      (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties) prefix)
+            until (or (not (notmuch-show-goto-message-next))
+                      (= (map-elt (notmuch-show-get-message-properties) :depth) current-depth)))))
+  (force-window-update))
+
+(defun vterm-send-ctrl-x ()
+  "Sends `C-x' to the libvterm."
+  (interactive)
+  (vterm-send-key "x" nil nil t))
+
+(defun find-depot-project (dir)
+  "Function used in the `project-find-functions' hook list to
+  determine the current project root of a depot project."
+  (when (s-starts-with? "/depot" dir)
+    (if (f-exists-p (f-join dir "default.nix"))
+        (cons 'transient dir)
+      (find-depot-project (f-parent dir)))))
+
+(add-to-list 'project-find-functions #'find-depot-project)
+
+(defun magit-find-file-worktree ()
+  (interactive)
+  "Find a file in the current (ma)git worktree."
+  (magit-find-file--internal "{worktree}"
+                             (magit-read-file-from-rev "HEAD" "Find file")
+                             #'pop-to-buffer-same-window))
+
+(defun songwhip--handle-result (status &optional cbargs)
+  ;; TODO(tazjin): Inspect status, which looks different in practice
+  ;; than the manual claims.
+  (if-let* ((response (json-parse-string
+                       (buffer-substring url-http-end-of-headers (point-max))))
+            (sw-path (ht-get* response "data" "path"))
+            (link (format "https://songwhip.com/%s" sw-path))
+            (select-enable-clipboard t))
+      (progn
+        (kill-new link)
+        (message "Copied Songwhip link (%s)" link))
+    (warn "Something went wrong while retrieving Songwhip link!")
+    ;; For debug purposes, the buffer is persisted in this case.
+    (setq songwhip--debug-buffer (current-buffer))))
+
+(defun songwhip-lookup-url (url)
+  "Look up URL on Songwhip and copy the resulting link to the clipboard."
+  (interactive "sEnter source URL: ")
+  (let ((songwhip-url "https://songwhip.com/api/")
+        (url-request-method "POST")
+        (url-request-extra-headers '(("Content-Type" . "application/json")))
+        (url-request-data
+         (json-serialize `((country . "GB")
+                           (url . ,url)))))
+    (url-retrieve "https://songwhip.com/api/" #'songwhip--handle-result nil t t)
+    (message "Requesting Songwhip URL ... please hold the line.")))
+
+(defun rg-in-project (&optional prefix)
+  "Interactively call ripgrep in the current project, or fall
+  back to ripgrep default behaviour if prefix is set."
+  (interactive "P")
+  (counsel-rg nil (unless prefix
+                    (if-let ((pr (project-current)))
+                        (project-root pr)))))
+
+(defun zoxide-open-magit ()
+  "Query Zoxide for paths and open magit in the result."
+  (interactive)
+  (zoxide-open-with nil #'magit-status-setup-buffer))
+
+(provide 'functions)
diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el
new file mode 100644
index 0000000000..b0e80998d6
--- /dev/null
+++ b/users/tazjin/emacs/config/init.el
@@ -0,0 +1,308 @@
+;;; init.el --- Package bootstrapping. -*- lexical-binding: t; -*-
+
+;; Disable annoying warnings from native compilation.
+(setq native-comp-async-report-warnings-errors nil
+      warning-suppress-log-types '((comp)))
+
+;; Packages are installed via Nix configuration, this file only
+;; initialises the newly loaded packages.
+
+(require 'use-package)
+(require 'seq)
+
+;; TODO(tazjin): Figure out what's up with vc.
+;;
+;; Leaving vc enabled breaks all find-file operations with messages
+;; about .git folders being absent, but in random places.
+(require 'vc)
+(setq vc-handled-backends nil)
+
+(package-initialize)
+
+;; Initialise all packages installed via Nix.
+;;
+;; TODO: Generate this section in Nix for all packages that do not
+;; require special configuration.
+
+;;
+;; Packages providing generic functionality.
+;;
+
+(use-package ace-window
+  :bind (("C-x o" . ace-window))
+  :config
+  (setq aw-keys '(?f ?j ?d ?k ?s ?l ?a)
+        aw-scope 'frame))
+
+(use-package auth-source-pass :config (auth-source-pass-enable))
+
+(use-package avy
+  :bind (("M-j" . avy-goto-char)
+         ("M-p" . avy-pop-mark)
+         ("M-g g" . avy-goto-line)))
+
+(use-package browse-kill-ring)
+
+(use-package company
+  :hook ((prog-mode . company-mode))
+  :config (setq company-tooltip-align-annotations t))
+
+(use-package counsel
+  :after (ivy)
+  :config (counsel-mode 1))
+
+(use-package dash)
+(use-package gruber-darker-theme)
+
+(use-package eglot
+  :custom
+  (eglot-autoshutdown t)
+  (eglot-send-changes-idle-time 0.3))
+
+(use-package elfeed
+  :config
+  (setq elfeed-feeds
+        '("https://lobste.rs/rss"
+          "https://www.anti-spiegel.ru/feed/"
+          "https://www.reddit.com/r/lockdownskepticism/.rss"
+          "https://www.reddit.com/r/rust/.rss"
+          "https://news.ycombinator.com/rss"
+          ("https://xkcd.com/atom.xml" media)
+
+          ;; vlogcreations
+          ("https://www.youtube.com/feeds/videos.xml?channel_id=UCR0VLWitB2xM4q7tjkoJUPw" media)
+          )))
+
+(use-package ht)
+
+(use-package hydra)
+(use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode)))
+
+(use-package ivy
+  :config
+  (ivy-mode 1)
+  (setq enable-recursive-minibuffers t)
+  (setq ivy-use-virtual-buffers t))
+
+(use-package ivy-prescient
+  :after (ivy prescient)
+  :config
+  (ivy-prescient-mode)
+  ;; Fixes an issue with how regexes are passed to ripgrep from counsel,
+  ;; see raxod502/prescient.el#43
+  (setf (alist-get 'counsel-rg ivy-re-builders-alist) #'ivy--regex-plus))
+
+(use-package multiple-cursors)
+
+(use-package notmuch
+  :custom
+  (notmuch-search-oldest-first nil)
+  (notmuch-show-all-tags-list t)
+  (notmuch-hello-tag-list-make-query "tag:unread"))
+
+(use-package paredit :hook ((lisp-mode . paredit-mode)
+                            (emacs-lisp-mode . paredit-mode)))
+
+(use-package pinentry
+  :config
+  (setq epa-pinentry-mode 'loopback)
+  (pinentry-start))
+
+(use-package prescient
+  :after (ivy counsel)
+  :config (prescient-persist-mode))
+
+(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode))
+(use-package rainbow-mode)
+(use-package s)
+(use-package string-edit)
+
+(use-package swiper
+  :after (counsel ivy)
+  :bind (("C-s" . swiper)))
+
+(use-package telephone-line) ;; configuration happens outside of use-package
+(use-package term-switcher)
+
+(use-package undo-tree
+  :config (global-undo-tree-mode)
+  :custom (undo-tree-auto-save-history nil))
+
+(use-package uuidgen)
+(use-package which-key :config (which-key-mode t))
+
+;;
+;; Applications in emacs
+;;
+
+(use-package magit
+  :bind ("C-c g" . magit-status)
+  :config (setq magit-repository-directories '(("/home/tazjin/projects" . 2)
+                                               ("/home/tazjin" . 1))))
+
+(use-package password-store)
+(use-package restclient)
+
+(use-package vterm
+  :config (progn
+            (setq vterm-shell "fish")
+            (setq vterm-exit-functions
+                  (lambda (&rest _) (kill-buffer (current-buffer))))
+            (setq vterm-kill-buffer-on-exit t)))
+
+;; vterm removed the ability to set a custom title generator function
+;; via the public API, so this overrides its private title generation
+;; function instead
+(defun vterm--set-title (title)
+  (rename-buffer
+   (generate-new-buffer-name
+    (format "vterm<%s>"
+            (s-trim-left
+             (s-chop-prefix "fish" title))))))
+
+;;
+;; Packages providing language-specific functionality
+;;
+
+(use-package cargo
+  :hook ((rust-mode . cargo-minor-mode)
+         (cargo-process-mode . visual-line-mode))
+  :bind (:map cargo-mode-map ("C-c C-c C-l" . ignore)))
+
+(use-package dockerfile-mode)
+
+(use-package erlang
+  :hook ((erlang-mode . (lambda ()
+                          ;; Don't indent after '>' while I'm writing
+                          (local-set-key ">" 'self-insert-command)))))
+
+(use-package f)
+
+(use-package go-mode
+  :bind (:map go-mode-map ("C-c C-r" . recompile))
+  :hook ((go-mode . (lambda ()
+                      (setq tab-width 2)
+                      (setq-local compile-command
+                                  (concat "go build " buffer-file-name))))))
+
+(use-package haskell-mode)
+
+(use-package ielm
+  :hook ((inferior-emacs-lisp-mode . (lambda ()
+                                       (paredit-mode)
+                                       (rainbow-delimiters-mode-enable)
+                                       (company-mode)))))
+
+(use-package jq-mode
+  :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode)))
+
+(use-package kotlin-mode
+  :hook ((kotlin-mode . (lambda ()
+                          (setq indent-line-function #'indent-relative)))))
+
+(use-package lsp-mode)
+
+(use-package markdown-mode
+  :config
+  (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode))
+  (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode)))
+
+(use-package markdown-toc)
+
+(use-package nix-mode
+  :hook ((nix-mode . (lambda ()
+                       (setq indent-line-function #'nix-indent-line)))))
+
+(use-package nix-util)
+(use-package nginx-mode)
+(use-package rust-mode)
+
+(use-package sly
+  :hook ((sly-mrepl-mode . (lambda ()
+                             (paredit-mode)
+                             (rainbow-delimiters-mode-enable)
+                             (company-mode))))
+  :config
+  (setq common-lisp-hyperspec-root "file:///home/tazjin/docs/lisp/"))
+
+(use-package telega
+  :bind (:map global-map ("s-t" . telega))
+  :config (telega-mode-line-mode 1))
+
+(use-package terraform-mode)
+(use-package toml-mode)
+
+(use-package tvl)
+
+(use-package web-mode)
+(use-package yaml-mode)
+(use-package zoxide)
+
+(use-package passively
+  :custom
+  (passively-store-state "/persist/tazjin/known-russian-words.el"))
+
+;; Note taking configuration for deft.
+(use-package deft
+  :custom
+  (deft-directory "/persist/tazjin/deft/")
+  (deft-extensions '("md" "org" "txt"))
+  (deft-default-extension "md"))
+
+(use-package zetteldeft
+  :custom
+  ;; Configure for Markdown
+  (zetteldeft-link-indicator "[[")
+  (zetteldeft-link-suffix "]]")
+  (zetteldeft-title-prefix "# ")
+  (zetteldeft-list-prefix "* "))
+
+;; Initialise midnight.el, which by default automatically cleans up
+;; unused buffers at midnight.
+(require 'midnight)
+
+(defgroup tazjin nil
+  "Settings related to my configuration")
+
+(defcustom depot-path "/depot"
+  "Local path to the depot checkout"
+  :group 'tazjin)
+
+;; Configuration changes in `customize` can not actually be persisted
+;; to the customise file that Emacs is currently using (since it comes
+;; from the Nix store).
+;;
+;; The way this will work for now is that Emacs will *write*
+;; configuration to the file tracked in my repository, while not
+;; actually *reading* it from there (unless Emacs is rebuilt).
+(setq custom-file (expand-file-name "~/depot/tools/emacs/config/custom.el"))
+(load-library "custom")
+
+(defvar home-dir (expand-file-name "~"))
+
+;; Seed RNG
+(random t)
+
+;; Load all other Emacs configuration. These configurations are
+;; added to `load-path' by Nix.
+(mapc 'require '(desktop
+                 mail-setup
+                 look-and-feel
+                 functions
+                 settings
+                 modes
+                 bindings
+                 eshell-setup))
+(telephone-line-setup)
+(ace-window-display-mode)
+
+;; If a local configuration library exists, it should be loaded.
+;;
+;; This can be provided by calling my Emacs derivation with
+;; `withLocalConfig'.
+(if-let (local-file (locate-library "local"))
+    (load local-file))
+
+(require 'dottime)
+
+(provide 'init)
diff --git a/users/tazjin/emacs/config/look-and-feel.el b/users/tazjin/emacs/config/look-and-feel.el
new file mode 100644
index 0000000000..72665d00c6
--- /dev/null
+++ b/users/tazjin/emacs/config/look-and-feel.el
@@ -0,0 +1,131 @@
+;;; -*- lexical-binding: t; -*-
+
+;; Hide those ugly tool bars:
+(tool-bar-mode 0)
+(scroll-bar-mode 0)
+(menu-bar-mode 0)
+(add-hook 'after-make-frame-functions
+          (lambda (frame) (scroll-bar-mode 0)))
+
+;; Don't do any annoying things:
+(setq ring-bell-function 'ignore)
+(setq initial-scratch-message "")
+
+;; Remember layout changes
+(winner-mode 1)
+
+;; Usually emacs will run as a proper GUI application, in which case a few
+;; extra settings are nice-to-have:
+(when window-system
+  (setq frame-title-format '(buffer-file-name "%f" ("%b")))
+  (mouse-wheel-mode t)
+  (blink-cursor-mode -1))
+
+;; Configure Emacs fonts.
+(let ((font (if (equal "frog" (s-trim (shell-command-to-string "hostname")))
+                ;; For unclear reasons, frog refuses to render the
+                ;; regular font weight - everything ends up bold,
+                ;; which makes it hard to distinguish e.g. read/unread
+                ;; emails.
+                ;;
+                ;; Semi-bold looks a little different than on vauxhall
+                ;; and other machines, but it's alright.
+                (format "JetBrains Mono Semi Light-%d" 12)
+              (format "JetBrains Mono-%d" 12))))
+  (setq default-frame-alist `((font . ,font)))
+  (set-frame-font font t t))
+
+;; Configure telephone-line
+(defun telephone-misc-if-last-window ()
+  "Renders the mode-line-misc-info string for display in the
+  mode-line if the currently active window is the last one in the
+  frame.
+
+  The idea is to not display information like the current time,
+  load, battery levels on all buffers."
+
+  (when (bottom-right-window-p)
+    (telephone-line-raw mode-line-misc-info t)))
+
+(defun telephone-line-setup ()
+  (telephone-line-defsegment telephone-line-last-window-segment ()
+    (telephone-misc-if-last-window))
+
+  ;; Display the current EXWM workspace index in the mode-line
+  (telephone-line-defsegment telephone-line-exwm-workspace-index ()
+    (when (bottom-right-window-p)
+      (format "[%s]" exwm-workspace-current-index)))
+
+  ;; Define a highlight font for ~ important ~ information in the last
+  ;; window.
+  (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "")
+  (add-to-list 'telephone-line-faces
+               '(highlight . (special-highlight . special-highlight)))
+
+  (setq telephone-line-lhs
+        '((nil . (telephone-line-position-segment))
+          (accent . (telephone-line-buffer-segment))))
+
+  (setq telephone-line-rhs
+        '((accent . (telephone-line-major-mode-segment))
+          (nil . (telephone-line-last-window-segment
+                  telephone-line-exwm-workspace-index))
+
+          ;; TODO(tazjin): lets not do this particular thing while I
+          ;; don't actually run notmuch, there are too many things
+          ;; that have a dependency on the modeline drawing correctly
+          ;; (including randr operations!)
+          ;;
+          ;; (highlight . (telephone-line-notmuch-counts))
+          ))
+
+  (setq telephone-line-primary-left-separator 'telephone-line-tan-left
+        telephone-line-primary-right-separator 'telephone-line-tan-right
+        telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left
+        telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right)
+
+  (telephone-line-mode 1))
+
+;; Auto refresh buffers
+(global-auto-revert-mode 1)
+
+;; Use clipboard properly
+(setq select-enable-clipboard t)
+
+;; Show in-progress chords in minibuffer
+(setq echo-keystrokes 0.1)
+
+;; Show column numbers in all buffers
+(column-number-mode t)
+
+(defalias 'yes-or-no-p 'y-or-n-p)
+(defalias 'auto-tail-revert-mode 'tail-mode)
+
+;; Style line numbers (shown with M-g g)
+(setq linum-format
+      (lambda (line)
+        (propertize
+         (format (concat " %"
+                         (number-to-string
+                          (length (number-to-string
+                                   (line-number-at-pos (point-max)))))
+                         "d ")
+                 line)
+         'face 'linum)))
+
+;; Display tabs as 2 spaces
+(setq tab-width 2)
+
+;; Don't wrap around when moving between buffers
+(setq windmove-wrap-around nil)
+
+;; Don't show me all emacs warnings immediately. Unfortunately this is
+;; not very granular, as emacs displays most of its warnings in the
+;; `emacs' "category", but without it every time I
+;; fullscreen/unfullscreen the warning buffer destroys my layout.
+;;
+;; Warnings suppressed by this are still logged to the warnings
+;; buffer.
+(setq warning-suppress-types '((emacs)))
+
+(provide 'look-and-feel)
diff --git a/users/tazjin/emacs/config/mail-setup.el b/users/tazjin/emacs/config/mail-setup.el
new file mode 100644
index 0000000000..7fbece1b10
--- /dev/null
+++ b/users/tazjin/emacs/config/mail-setup.el
@@ -0,0 +1,85 @@
+(require 'notmuch)
+(require 'counsel-notmuch)
+
+;; (global-set-key (kbd "C-c m") 'notmuch-hello)
+;; (global-set-key (kbd "C-c C-m") 'counsel-notmuch)
+;; (global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail)
+
+(setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME")))
+(make-directory notmuch-cache-dir t)
+
+;; Cache addresses for completion:
+(setq notmuch-address-save-filename (concat notmuch-cache-dir "/addresses"))
+
+;; Don't spam my home folder with drafts:
+(setq notmuch-draft-folder "drafts") ;; relative to notmuch database
+
+;; Mark things as read when archiving them:
+(setq notmuch-archive-tags '("-inbox" "-unread" "+archive"))
+
+;; Show me saved searches that I care about:
+(setq notmuch-saved-searches
+      '((:name "inbox" :query "tag:inbox" :count-query "tag:inbox AND tag:unread" :key "i")
+        (:name "sent" :query "tag:sent" :key "t")
+        (:name "drafts" :query "tag:draft")))
+(setq notmuch-show-empty-saved-searches t)
+
+;; Mail sending configuration
+(setq sendmail-program "gmi") ;; lieer binary supports sendmail emulation
+(setq message-sendmail-extra-arguments
+      '("send" "--quiet" "-t" "-C" "~/mail/account.tazjin"))
+(setq send-mail-function 'sendmail-send-it)
+(setq notmuch-mua-user-agent-function
+      (lambda () (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version)))
+(setq mail-host-address (system-name))
+(setq notmuch-mua-cite-function #'message-cite-original-without-signature)
+(setq notmuch-fcc-dirs nil) ;; Gmail does this server-side
+(setq message-signature nil) ;; Insert message signature manually with C-c C-w
+
+;; Close mail buffers after sending mail
+(setq message-kill-buffer-on-exit t)
+
+;; Ensure sender is correctly passed to msmtp
+(setq mail-specify-envelope-from t
+      message-sendmail-envelope-from 'header
+      mail-envelope-from 'header)
+
+;; Store sent mail in the correct folder per account
+(setq notmuch-maildir-use-notmuch-insert nil)
+
+;; I don't use drafts but I instinctively hit C-x C-s constantly, lets
+;; handle that gracefully.
+(define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore)
+
+;; Define a telephone-line segment for displaying the count of unread,
+;; important mails in the last window's mode-line:
+(defvar *last-notmuch-count-redraw* 0)
+(defvar *current-notmuch-count* nil)
+
+(defun update-display-notmuch-counts ()
+  "Update and render the current state of the notmuch unread
+  count for display in the mode-line.
+
+  The offlineimap-timer runs every 2 minutes, so it does not make
+  sense to refresh this much more often than that."
+
+  (when (> (- (float-time) *last-notmuch-count-redraw*) 30)
+    (setq *last-notmuch-count-redraw* (float-time))
+    (let* ((inbox-unread (notmuch-saved-search-count "tag:inbox and tag:unread"))
+           (notmuch-count (format "I: %s; D: %s" inbox-unread)))
+      (setq *current-notmuch-count* notmuch-count)))
+
+  (when (and (bottom-right-window-p)
+             ;; Only render if the initial update is done and there
+             ;; are unread mails:
+             *current-notmuch-count*
+             (not (equal *current-notmuch-count* "I: 0; D: 0")))
+    *current-notmuch-count*))
+
+(telephone-line-defsegment telephone-line-notmuch-counts ()
+  "This segment displays the count of unread notmuch messages in
+  the last window's mode-line (if unread messages are present)."
+
+  (update-display-notmuch-counts))
+
+(provide 'mail-setup)
diff --git a/users/tazjin/emacs/config/modes.el b/users/tazjin/emacs/config/modes.el
new file mode 100644
index 0000000000..69fb523d0d
--- /dev/null
+++ b/users/tazjin/emacs/config/modes.el
@@ -0,0 +1,37 @@
+;; Initializes modes I use.
+
+(add-hook 'prog-mode-hook 'esk-add-watchwords)
+(add-hook 'prog-mode-hook 'hl-line-mode)
+
+;; Use auto-complete as completion at point
+(defun set-auto-complete-as-completion-at-point-function ()
+  (setq completion-at-point-functions '(auto-complete)))
+
+(add-hook 'auto-complete-mode-hook
+          'set-auto-complete-as-completion-at-point-function)
+
+;; Enable rainbow-delimiters for all things programming
+(add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
+
+;; Enable Paredit & Company in Emacs Lisp mode
+(add-hook 'emacs-lisp-mode-hook 'company-mode)
+
+;; Always highlight matching brackets
+(show-paren-mode 1)
+
+;; Always auto-close parantheses and other pairs
+(electric-pair-mode)
+
+;; Keep track of recent files
+(recentf-mode)
+
+;; Easily navigate sillycased words
+(global-subword-mode 1)
+
+;; Transparently open compressed files
+(auto-compression-mode t)
+
+;; Configure go-mode for Go2 Alpha
+(add-to-list 'auto-mode-alist '("\\.go2$" . go-mode))
+
+(provide 'modes)
diff --git a/users/tazjin/emacs/config/settings.el b/users/tazjin/emacs/config/settings.el
new file mode 100644
index 0000000000..8b15b6cda1
--- /dev/null
+++ b/users/tazjin/emacs/config/settings.el
@@ -0,0 +1,48 @@
+(require 'uniquify)
+
+;; We don't live in the 80s, but we're also not a shitty web app.
+(setq gc-cons-threshold 20000000)
+
+(setq uniquify-buffer-name-style 'forward)
+
+; Fix some defaults
+(setq visible-bell nil
+      inhibit-startup-message t
+      color-theme-is-global t
+      sentence-end-double-space nil
+      shift-select-mode nil
+      uniquify-buffer-name-style 'forward
+      whitespace-style '(face trailing lines-tail tabs)
+      whitespace-line-column 80
+      default-directory "~"
+      fill-column 80
+      ediff-split-window-function 'split-window-horizontally
+      initial-major-mode 'emacs-lisp-mode)
+
+(add-to-list 'safe-local-variable-values '(lexical-binding . t))
+(add-to-list 'safe-local-variable-values '(whitespace-line-column . 80))
+
+(set-default 'indent-tabs-mode nil)
+
+;; UTF-8 please
+(setq locale-coding-system 'utf-8) ; pretty
+(set-terminal-coding-system 'utf-8) ; pretty
+(set-keyboard-coding-system 'utf-8) ; pretty
+(set-selection-coding-system 'utf-8) ; please
+(prefer-coding-system 'utf-8) ; with sugar on top
+
+;; Make emacs behave sanely (overwrite selected text)
+(delete-selection-mode 1)
+
+;; Keep your temporary files in tmp, emacs!
+(setq auto-save-file-name-transforms
+      `((".*" ,temporary-file-directory t)))
+(setq backup-directory-alist
+      `((".*" . ,temporary-file-directory)))
+
+(remove-hook 'kill-buffer-query-functions 'server-kill-buffer-query-function)
+
+;; Show time in 24h format
+(setq display-time-24hr-format t)
+
+(provide 'settings)
diff --git a/users/tazjin/emacs/default.nix b/users/tazjin/emacs/default.nix
new file mode 100644
index 0000000000..5b778be1d9
--- /dev/null
+++ b/users/tazjin/emacs/default.nix
@@ -0,0 +1,179 @@
+# This file builds an Emacs pre-configured with the packages I need
+# and my personal Emacs configuration.
+{ lib, pkgs, ... }:
+
+pkgs.makeOverridable
+  ({ emacs ? pkgs.emacsNativeComp }:
+  let
+    emacsWithPackages = (pkgs.emacsPackagesFor emacs).emacsWithPackages;
+
+    # If switching telega versions, use this variable because it will
+    # keep the version check, binary path and so on in sync.
+    currentTelega = epkgs: epkgs.melpaPackages.telega;
+
+    # $PATH for binaries that need to be available to Emacs
+    emacsBinPath = lib.makeBinPath [
+      (currentTelega pkgs.emacsPackages)
+      pkgs.libwebp # for dwebp, required by telega
+    ];
+
+    identity = x: x;
+
+    tazjinsEmacs = pkgfun: (emacsWithPackages (epkgs: pkgfun (with epkgs; [
+      ace-link
+      ace-window
+      avy
+      bazel
+      browse-kill-ring
+      cargo
+      clojure-mode
+      cmake-mode
+      company
+      counsel
+      counsel-notmuch
+      d-mode
+      deft
+      direnv
+      dockerfile-mode
+      eglot
+      elfeed
+      elixir-mode
+      elm-mode
+      erlang
+      exwm
+      flymake
+      go-mode
+      google-c-style
+      gruber-darker-theme
+      haskell-mode
+      ht
+      hydra
+      idle-highlight-mode
+      ivy
+      ivy-prescient
+      jq-mode
+      kotlin-mode
+      lsp-mode
+      magit
+      markdown-toc
+      meson-mode
+      multi-term
+      multiple-cursors
+      nginx-mode
+      nix-mode
+      notmuch
+      paredit
+      password-store
+      pinentry
+      polymode
+      prescient
+      protobuf-mode
+      rainbow-delimiters
+      rainbow-mode
+      refine
+      request
+      restclient
+      rust-mode
+      sly
+      string-edit
+      swiper
+      telephone-line
+      terraform-mode
+      toml-mode
+      transient
+      undo-tree
+      use-package
+      uuidgen
+      vterm
+      web-mode
+      websocket
+      which-key
+      xelb
+      yaml-mode
+      yasnippet
+      zetteldeft
+      zoxide
+
+      # Wonky stuff
+      (currentTelega epkgs)
+
+      # Custom depot packages (either ours, or overridden ones)
+      tvlPackages.dottime
+      tvlPackages.nix-util
+      tvlPackages.passively
+      tvlPackages.rcirc
+      tvlPackages.term-switcher
+      tvlPackages.tvl
+    ])));
+
+    # Tired of telega.el runtime breakages through tdlib
+    # incompatibility. Target to make that a build failure instead.
+    tdlibCheck =
+      let
+        tgEmacs = emacsWithPackages (epkgs: [ (currentTelega epkgs) ]);
+        verifyTdlibVersion = builtins.toFile "verify-tdlib-version.el" ''
+          (require 'telega)
+          (defvar tdlib-version "${pkgs.tdlib.version}")
+          (when (or (version< tdlib-version
+                              telega-tdlib-min-version)
+                    (and telega-tdlib-max-version
+                          (version< telega-tdlib-max-version
+                                    tdlib-version)))
+             (message "Found TDLib version %s, but require %s to %s"
+                     tdlib-version telega-tdlib-min-version telega-tdlib-max-version)
+            (kill-emacs 1))
+        '';
+      in
+      pkgs.runCommandNoCC "tdlibCheck" { } ''
+        export PATH="${emacsBinPath}:$PATH"
+        ${tgEmacs}/bin/emacs --script ${verifyTdlibVersion} && touch $out
+      '';
+  in
+  lib.fix
+    (self: l: f: (pkgs.writeShellScriptBin "tazjins-emacs" ''
+      export PATH="${emacsBinPath}:$PATH"
+      exec ${tazjinsEmacs f}/bin/emacs \
+        --debug-init \
+        --no-site-file \
+        --no-site-lisp \
+        --no-init-file \
+        --directory ${./config} ${if l != null then "--directory ${l}" else ""} \
+        --eval "(require 'init)" $@
+    '').overrideAttrs
+      (_: {
+        passthru = {
+          # Call overrideEmacs with a function (pkgs -> pkgs) to modify the
+          # packages that should be included in this Emacs distribution.
+          overrideEmacs = f': self l f';
+
+          # Call withLocalConfig with the path to a *folder* containing a
+          # `local.el` which provides local system configuration.
+          withLocalConfig = confDir: self confDir f;
+
+          # Build a derivation that uses the specified local Emacs (i.e.
+          # built outside of Nix) instead
+          withLocalEmacs = emacsBin: pkgs.writeShellScriptBin "tazjins-emacs" ''
+            export PATH="${emacsBinPath}:$PATH"
+            export EMACSLOADPATH="${(tazjinsEmacs f).deps}/share/emacs/site-lisp:"
+            exec ${emacsBin} \
+              --debug-init \
+              --no-site-file \
+              --no-site-lisp \
+              --no-init-file \
+              --directory ${./config} \
+              ${if l != null then "--directory ${l}" else ""} \
+              --eval "(require 'init)" $@
+          '';
+
+          # Expose telega/tdlib version check as a target that is built in
+          # CI.
+          #
+          # TODO(tazjin): uncomment when telega works again
+          inherit tdlibCheck;
+          meta.ci.targets = [ "tdlibCheck" ];
+        };
+      }))
+    null
+    identity
+  )
+{ }
diff --git a/users/tazjin/finito/.gitignore b/users/tazjin/finito/.gitignore
new file mode 100644
index 0000000000..548206b0b2
--- /dev/null
+++ b/users/tazjin/finito/.gitignore
@@ -0,0 +1,3 @@
+.envrc
+/target/
+**/*.rs.bk
diff --git a/users/tazjin/finito/Cargo.lock b/users/tazjin/finito/Cargo.lock
new file mode 100644
index 0000000000..7427a6b11c
--- /dev/null
+++ b/users/tazjin/finito/Cargo.lock
@@ -0,0 +1,773 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "addr2line"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler32"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
+
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "autocfg"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
+
+[[package]]
+name = "backtrace"
+version = "0.3.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9"
+dependencies = [
+ "byteorder",
+ "safemem",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "block-buffer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab"
+dependencies = [
+ "arrayref",
+ "byte-tools",
+]
+
+[[package]]
+name = "byte-tools"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+
+[[package]]
+name = "bytes"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
+dependencies = [
+ "byteorder",
+ "iovec",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "chrono"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
+dependencies = [
+ "num-integer",
+ "num-traits",
+ "time",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
+name = "crypto-mac"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0999b4ff4d3446d4ddb19a63e9e00c1876e75cd7000d20e57a693b4b3f08d958"
+dependencies = [
+ "constant_time_eq",
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "failure"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
+dependencies = [
+ "backtrace",
+ "failure_derive",
+]
+
+[[package]]
+name = "failure_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
+dependencies = [
+ "proc-macro2 1.0.18",
+ "quote 1.0.7",
+ "syn 1.0.33",
+ "synstructure",
+]
+
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
+[[package]]
+name = "fallible-iterator"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb7217124812dc5672b7476d0c2d20cfe9f7c0f1ba0904b674a9762a0212f72e"
+
+[[package]]
+name = "finito"
+version = "0.1.0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "finito-door"
+version = "0.1.0"
+dependencies = [
+ "failure",
+ "finito",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "finito-postgres"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "finito",
+ "finito-door",
+ "postgres",
+ "postgres-derive",
+ "r2d2_postgres",
+ "serde",
+ "serde_json",
+ "uuid",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "generic-array"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "gimli"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
+
+[[package]]
+name = "hex"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa"
+
+[[package]]
+name = "hmac"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f3bdb08579d99d7dc761c0e266f13b5f2ab8c8c703b9fc9ef333cd8f48f55e"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
+
+[[package]]
+name = "libc"
+version = "0.2.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
+
+[[package]]
+name = "lock_api"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
+[[package]]
+name = "md5"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
+
+[[package]]
+name = "memchr"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
+
+[[package]]
+name = "parking_lot"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
+dependencies = [
+ "cfg-if",
+ "cloudabi",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "phf"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "postgres"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115dde90ef51af573580c035857badbece2aa5cde3de1dfb3c932969ca92a6c5"
+dependencies = [
+ "bytes",
+ "fallible-iterator",
+ "log",
+ "postgres-protocol",
+ "postgres-shared",
+ "socket2",
+]
+
+[[package]]
+name = "postgres-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44ef42ae50f1547dde36aa78d5e44189cbf21f4e77ce6ddc2bbaa068337fc221"
+dependencies = [
+ "quote 0.5.2",
+ "syn 0.13.11",
+]
+
+[[package]]
+name = "postgres-protocol"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2487e66455bf88a1b247bf08a3ce7fe5197ac6d67228d920b0ee6a0e97fd7312"
+dependencies = [
+ "base64",
+ "byteorder",
+ "bytes",
+ "fallible-iterator",
+ "generic-array",
+ "hmac",
+ "md5",
+ "memchr",
+ "rand 0.3.23",
+ "sha2",
+ "stringprep",
+]
+
+[[package]]
+name = "postgres-shared"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffac35b3e0029b404c24a3b82149b4e904f293e8ca4a327eefa24d3ca50df36f"
+dependencies = [
+ "chrono",
+ "fallible-iterator",
+ "hex",
+ "phf",
+ "postgres-protocol",
+ "serde_json",
+ "uuid",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
+dependencies = [
+ "unicode-xid 0.2.1",
+]
+
+[[package]]
+name = "quote"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
+dependencies = [
+ "proc-macro2 0.3.8",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+dependencies = [
+ "proc-macro2 1.0.18",
+]
+
+[[package]]
+name = "r2d2"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af"
+dependencies = [
+ "log",
+ "parking_lot",
+ "scheduled-thread-pool",
+]
+
+[[package]]
+name = "r2d2_postgres"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c7fe9c0c3d2c298cf262bc3ce4b89cdf0eab620fd9fe759f65b34a1a00fb93"
+dependencies = [
+ "postgres",
+ "postgres-shared",
+ "r2d2",
+]
+
+[[package]]
+name = "rand"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
+dependencies = [
+ "libc",
+ "rand 0.4.6",
+]
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "safemem"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
+
+[[package]]
+name = "scheduled-thread-pool"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6"
+dependencies = [
+ "parking_lot",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "serde"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
+dependencies = [
+ "proc-macro2 1.0.18",
+ "quote 1.0.7",
+ "syn 1.0.33",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0"
+dependencies = [
+ "block-buffer",
+ "byte-tools",
+ "digest",
+ "fake-simd",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "smallvec"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
+
+[[package]]
+name = "socket2"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi",
+]
+
+[[package]]
+name = "stringprep"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "syn"
+version = "0.13.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b"
+dependencies = [
+ "proc-macro2 0.3.8",
+ "quote 0.5.2",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
+dependencies = [
+ "proc-macro2 1.0.18",
+ "quote 1.0.7",
+ "unicode-xid 0.2.1",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
+dependencies = [
+ "proc-macro2 1.0.18",
+ "quote 1.0.7",
+ "syn 1.0.33",
+ "unicode-xid 0.2.1",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed"
+
+[[package]]
+name = "typenum"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "uuid"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
+dependencies = [
+ "rand 0.3.23",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/users/tazjin/finito/Cargo.toml b/users/tazjin/finito/Cargo.toml
new file mode 100644
index 0000000000..310133abee
--- /dev/null
+++ b/users/tazjin/finito/Cargo.toml
@@ -0,0 +1,6 @@
+[workspace]
+members = [
+  "finito-core",
+  "finito-door",
+  "finito-postgres"
+]
diff --git a/users/tazjin/finito/README.md b/users/tazjin/finito/README.md
new file mode 100644
index 0000000000..5acd67d3be
--- /dev/null
+++ b/users/tazjin/finito/README.md
@@ -0,0 +1,27 @@
+Finito
+======
+
+This is a Rust port of the Haskell state-machine library Finito. It is
+slightly less featureful because it loses the ability to ensure that
+side-effects are contained and because of a slight reduction in
+expressivity, which makes it a bit more restrictive.
+
+However, it still implements the FSM model well enough.
+
+# Components
+
+Finito is split up into multiple independent components (note: not all
+of these exist yet), separating functionality related to FSM
+persistence from other things.
+
+* `finito`: Core abstraction implemented by Finito
+* `finito-door`: Example implementation of a simple, lockable door
+* `finito-postgres`: Persistent state-machines using Postgres
+
+**Note**: The `finito` core library does not contain any tests. Its
+coverage is instead provided by the `finito-door` library, which
+actually implements an example FSM.
+
+These are split out because the documentation for `finito-door` is
+interesting regardless and because other Finito packages also need an
+example implementation.
diff --git a/users/tazjin/finito/default.nix b/users/tazjin/finito/default.nix
new file mode 100644
index 0000000000..e50ac32be4
--- /dev/null
+++ b/users/tazjin/finito/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+}
diff --git a/users/tazjin/finito/finito-core/Cargo.toml b/users/tazjin/finito/finito-core/Cargo.toml
new file mode 100644
index 0000000000..1d7bdb8b01
--- /dev/null
+++ b/users/tazjin/finito/finito-core/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "finito"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+
+[dependencies]
+serde = "1.0"
diff --git a/users/tazjin/finito/finito-core/src/lib.rs b/users/tazjin/finito/finito-core/src/lib.rs
new file mode 100644
index 0000000000..aaec03a77b
--- /dev/null
+++ b/users/tazjin/finito/finito-core/src/lib.rs
@@ -0,0 +1,248 @@
+//! Finito's core finite-state machine abstraction.
+//!
+//! # What & why?
+//!
+//! Most processes that occur in software applications can be modeled
+//! as finite-state machines (FSMs), however the actual states, the
+//! transitions between them and the model's interaction with the
+//! external world is often implicit.
+//!
+//! Making the states of a process explicit using a simple language
+//! that works for both software developers and other people who may
+//! have opinions on processes makes it easier to synchronise thoughts,
+//! extend software and keep a good level of control over what is going
+//! on.
+//!
+//! This library aims to provide functionality for implementing
+//! finite-state machines in a way that balances expressivity and
+//! safety.
+//!
+//! Finito does not aim to prevent every possible incorrect
+//! transition, but aims for somewhere "safe-enough" (please don't
+//! lynch me) that is still easily understood.
+//!
+//! # Conceptual overview
+//!
+//! The core idea behind Finito can be expressed in a single line and
+//! will potentially look familiar if you have used Erlang in a
+//! previous life. The syntax used here is the type-signature notation
+//! of Haskell.
+//!
+//! ```text
+//! advance :: state -> event -> (state, [action])
+//! ```
+//!
+//! In short, every FSM is made up of three distinct types:
+//!
+//!   * a state type representing all possible states of the machine
+//!
+//!   * an event type representing all possible events in the machine
+//!
+//!   * an action type representing a description of all possible side-effects
+//!     of the machine
+//!
+//! Using the definition above we can now say that a transition in a
+//! state-machine, involving these three types, takes an initial state
+//! and an event to apply it to and returns a new state and a list of
+//! actions to execute.
+//!
+//! With this definition most processes can already be modeled quite
+//! well. Two additional functions are required to make it all work:
+//!
+//! ```text
+//! -- | The ability to cause additional side-effects after entering
+//! -- a new state.
+//! > enter :: state -> [action]
+//! ```
+//!
+//! as well as
+//!
+//! ```text
+//! -- | An interpreter for side-effects
+//! act :: action -> m [event]
+//! ```
+//!
+//! **Note**: This library is based on an original Haskell library. In
+//! Haskell, side-effects can be controlled via the type system which
+//! is impossible in Rust.
+//!
+//! Some parts of Finito make assumptions about the programmer not
+//! making certain kinds of mistakes, which are pointed out in the
+//! documentation. Unfortunately those assumptions are not
+//! automatically verifiable in Rust.
+//!
+//! ## Example
+//!
+//! Please consult `finito-door` for an example representing a simple,
+//! lockable door as a finite-state machine. This gives an overview
+//! over Finito's primary features.
+//!
+//! If you happen to be the kind of person who likes to learn about
+//! libraries by reading code, you should familiarise yourself with the
+//! door as it shows up as the example in other finito-related
+//! libraries, too.
+//!
+//! # Persistence, side-effects and mud
+//!
+//! These three things are inescapable in the fateful realm of
+//! computers, but Finito separates them out into separate libraries
+//! that you can drag in as you need them.
+//!
+//! Currently, those libraries include:
+//!
+//!   * `finito`: Core components and classes of Finito
+//!
+//!   * `finito-in-mem`: In-memory implementation of state machines that do not
+//!     need to live longer than an application using standard library
+//!     concurrency primitives.
+//!
+//!   * `finito-postgres`: Postgres-backed, persistent implementation of state
+//!     machines that, well, do need to live longer. Uses Postgres for
+//!     concurrency synchronisation, so keep that in mind.
+//!
+//! Which should cover most use-cases. Okay, enough prose, lets dive
+//! in.
+//!
+//! # Does Finito make you want to scream?
+//!
+//! Please reach out! I want to know why!
+
+extern crate serde;
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use std::fmt::Debug;
+use std::mem;
+
+/// Primary trait that needs to be implemented for every state type
+/// representing the states of an FSM.
+///
+/// This trait is used to implement transition logic and to "tie the
+/// room together", with the room being our triplet of types.
+pub trait FSM
+where
+    Self: Sized,
+{
+    /// A human-readable string uniquely describing what this FSM
+    /// models. This is used in log messages, database tables and
+    /// various other things throughout Finito.
+    const FSM_NAME: &'static str;
+
+    /// The associated event type of an FSM represents all possible
+    /// events that can occur in the state-machine.
+    type Event;
+
+    /// The associated action type of an FSM represents all possible
+    /// actions that can occur in the state-machine.
+    type Action;
+
+    /// The associated error type of an FSM represents failures that
+    /// can occur during action processing.
+    type Error: Debug;
+
+    /// The associated state type of an FSM describes the state that
+    /// is made available to the implementation of action
+    /// interpretations.
+    type State;
+
+    /// `handle` deals with any incoming events to cause state
+    /// transitions and emit actions. This function is the core logic
+    /// of any state machine.
+    ///
+    /// Implementations of this function **must not** cause any
+    /// side-effects to avoid breaking the guarantees of Finitos
+    /// conceptual model.
+    fn handle(self, event: Self::Event) -> (Self, Vec<Self::Action>);
+
+    /// `enter` is called when a new state is entered, allowing a
+    /// state to produce additional side-effects.
+    ///
+    /// This is useful for side-effects that event handlers do not
+    /// need to know about and for resting assured that a certain
+    /// action has been caused when a state is entered.
+    ///
+    /// FSM state types are expected to be enum (i.e. sum) types. A
+    /// state is considered "new" and enter calls are run if is of a
+    /// different enum variant.
+    fn enter(&self) -> Vec<Self::Action>;
+
+    /// `act` interprets and executes FSM actions. This is the only
+    /// part of an FSM in which side-effects are allowed.
+    fn act(action: Self::Action, state: &Self::State) -> Result<Vec<Self::Event>, Self::Error>;
+}
+
+/// This function is the primary function used to advance a state
+/// machine. It takes care of both running the event handler as well
+/// as possible state-enter calls and returning the result.
+///
+/// Users of Finito should basically always use this function when
+/// advancing state-machines manually, and never call FSM-trait
+/// methods directly.
+pub fn advance<S: FSM>(state: S, event: S::Event) -> (S, Vec<S::Action>) {
+    // Determine the enum variant of the initial state (used to
+    // trigger enter calls).
+    let old_discriminant = mem::discriminant(&state);
+
+    let (new_state, mut actions) = state.handle(event);
+
+    // Compare the enum variant of the resulting state to the old one
+    // and run `enter` if they differ.
+    let new_discriminant = mem::discriminant(&new_state);
+    let mut enter_actions = if old_discriminant != new_discriminant {
+        new_state.enter()
+    } else {
+        vec![]
+    };
+
+    actions.append(&mut enter_actions);
+
+    (new_state, actions)
+}
+
+/// This trait is implemented by Finito backends. Backends are
+/// expected to be able to keep track of the current state of an FSM
+/// and retrieve it / apply updates transactionally.
+///
+/// See the `finito-postgres` and `finito-in-mem` crates for example
+/// implementations of this trait.
+///
+/// Backends must be parameterised over an additional (user-supplied)
+/// state type which can be used to track application state that must
+/// be made available to action handlers, for example to pass along
+/// database connections.
+pub trait FSMBackend<S: 'static> {
+    /// Key type used to identify individual state machines in this
+    /// backend.
+    ///
+    /// TODO: Should be parameterised over FSM type after rustc
+    /// #44265.
+    type Key;
+
+    /// Error type for all potential failures that can occur when
+    /// interacting with this backend.
+    type Error: Debug;
+
+    /// Insert a new state-machine into the backend's storage and
+    /// return its newly allocated key.
+    fn insert_machine<F>(&self, initial: F) -> Result<Self::Key, Self::Error>
+    where
+        F: FSM + Serialize + DeserializeOwned;
+
+    /// Retrieve the current state of an FSM by its key.
+    fn get_machine<F: FSM>(&self, key: Self::Key) -> Result<F, Self::Error>
+    where
+        F: FSM + Serialize + DeserializeOwned;
+
+    /// Advance a state machine by applying an event and persisting it
+    /// as well as any resulting actions.
+    ///
+    /// **Note**: Whether actions are automatically executed depends
+    /// on the backend used. Please consult the backend's
+    /// documentation for details.
+    fn advance<'a, F: FSM>(&'a self, key: Self::Key, event: F::Event) -> Result<F, Self::Error>
+    where
+        F: FSM + Serialize + DeserializeOwned,
+        F::State: From<&'a S>,
+        F::Event: Serialize + DeserializeOwned,
+        F::Action: Serialize + DeserializeOwned;
+}
diff --git a/users/tazjin/finito/finito-door/Cargo.toml b/users/tazjin/finito/finito-door/Cargo.toml
new file mode 100644
index 0000000000..32c0a5a7c4
--- /dev/null
+++ b/users/tazjin/finito/finito-door/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "finito-door"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+
+[dependencies]
+failure = "0.1"
+serde = "1.0"
+serde_derive = "1.0"
+
+[dependencies.finito]
+path = "../finito-core"
diff --git a/users/tazjin/finito/finito-door/src/lib.rs b/users/tazjin/finito/finito-door/src/lib.rs
new file mode 100644
index 0000000000..441ab0e3d2
--- /dev/null
+++ b/users/tazjin/finito/finito-door/src/lib.rs
@@ -0,0 +1,333 @@
+//! Example implementation of a lockable door in Finito
+//!
+//! # What & why?
+//!
+//! This module serves as a (hopefully simple) example of how to
+//! implement finite-state machines using Finito. Note that the
+//! concepts of Finito itself won't be explained in detail here,
+//! consult its library documentation for that.
+//!
+//! Reading through this module should give you a rough idea of how to
+//! work with Finito and get you up and running modeling things
+//! *quickly*.
+//!
+//! Note: The generated documentation for this module will display the
+//! various components of the door, but it will not inform you about
+//! the actual transition logic and all that stuff. Read the source,
+//! too!
+//!
+//! # The Door
+//!
+//! My favourite example when explaining these state-machines
+//! conceptually has been to use a simple, lockable door. Our door has
+//! a keypad next to it which can be used to lock the door by entering
+//! a code, after which the same code must be entered to unlock it
+//! again.
+//!
+//! The door can only be locked if it is closed. Oh, and it has a few
+//! extra features:
+//!
+//! * whenever the door's state changes, an IRC channel receives a message about
+//!   that
+//!
+//! * the door calls the police if the code is intered incorrectly more than a
+//!   specified number of times (mhm, lets say, three)
+//!
+//! * if the police is called the door can not be interacted with anymore (and
+//!   honestly, for the sake of this example, we don't care how its
+//!   functionality is restored)
+//!
+//! ## The Door - Visualized
+//!
+//! Here's a rough attempt at drawing a state diagram in ASCII. The
+//! bracketed words denote states, the arrows denote events:
+//!
+//! ```text
+//!          <--Open---    <--Unlock-- correct code? --Unlock-->
+//!      [Opened]    [Closed]            [Locked]            [Disabled]
+//!          --Close-->    ----Lock-->
+//! ```
+//!
+//! I'm so sorry for that drawing.
+//!
+//! ## The Door - Usage example
+//!
+//! An interaction session with our final door could look like this:
+//!
+//! ```rust,ignore
+//! use finito_postgres::{insert_machine, advance};
+//!
+//! let door = insert_machine(&conn, &DoorState::Opened)?;
+//!
+//! advance(&conn, &door, DoorEvent::Close)?;
+//! advance(&conn, &door, DoorEvent::Lock(1337))?;
+//!
+//! format!("Door is now: {}", get_machine(&conn, &door)?);
+//! ```
+//!
+//! Here we have created, closed and then locked a door and inspected
+//! its state. We will see that it is locked, has the locking code we
+//! gave it and three remaining attempts to open it.
+//!
+//! Alright, enough foreplay, lets dive in!
+
+#[macro_use]
+extern crate serde_derive;
+
+extern crate failure;
+extern crate finito;
+
+use finito::FSM;
+
+/// Type synonym to represent the code with which the door is locked. This
+/// exists only for clarity in the signatures below and please do not email me
+/// about the fact that an integer is not actually a good representation of
+/// numerical digits. Thanks!
+type Code = usize;
+
+/// Type synonym to represent the remaining number of unlock attempts.
+type Attempts = usize;
+
+/// This type represents the possible door states and the data that they carry.
+/// We can infer this from the "diagram" in the documentation above.
+///
+/// This type is the one for which `finito::FSM` will be implemented, making it
+/// the wooden (?) heart of our door.
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub enum DoorState {
+    /// In `Opened` state, the door is wide open and anyone who fits through can
+    /// go through.
+    Opened,
+
+    /// In `Closed` state, the door is shut but does not prevent anyone from
+    /// opening it.
+    Closed,
+
+    /// In `Locked` state, the door is locked and waiting for someone to enter
+    /// its locking code on the keypad.
+    ///
+    /// This state contains the code that the door is locked with, as well as
+    /// the remaining number of attempts before the door calls the police and
+    /// becomes unusable.
+    Locked { code: Code, attempts: Attempts },
+
+    /// This state represents a disabled door after the police has been called.
+    /// The police will need to unlock it manually!
+    Disabled,
+}
+
+/// This type represents the events that can occur in our door, i.e. the input
+/// and interactions it receives.
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub enum DoorEvent {
+    /// `Open` means someone is opening the door!
+    Open,
+
+    /// `Close` means, you guessed it, the exact opposite.
+    Close,
+
+    /// `Lock` means somebody has entered a locking code on the
+    /// keypad.
+    Lock(Code),
+
+    /// `Unlock` means someone has attempted to unlock the door.
+    Unlock(Code),
+}
+
+/// This type represents the possible actions, a.k.a. everything our door "does"
+/// that does not just impact itself, a.k.a. side-effects.
+///
+/// **Note**: This type by itself *is not* a collection of side-effects, it
+/// merely describes the side-effects we want to occur (which are then
+/// interpreted by the machinery later).
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub enum DoorAction {
+    /// `NotifyIRC` is used to display some kind of message on the
+    /// aforementioned IRC channel that is, for some reason, very interested in
+    /// the state of the door.
+    NotifyIRC(String),
+
+    /// `CallThePolice` does what you think it does.
+    ///
+    /// **Note**: For safety reasons, causing this action is not recommended for
+    /// users inside the US!
+    CallThePolice,
+}
+
+/// This trait implementation turns our 'DoorState' into a type actually
+/// representing a finite-state machine. To implement it, we need to do three
+/// main things:
+///
+/// * Define what our associated `Event` and `Action` type should be
+///
+/// * Define the event-handling and state-entering logic (i.e. the meat of the
+/// ... door)
+///
+/// * Implement the interpretation of our actions, i.e. implement actual
+///   side-effects
+impl FSM for DoorState {
+    const FSM_NAME: &'static str = "door";
+
+    // As you might expect, our `Event` type is 'DoorEvent' and our `Action`
+    // type is 'DoorAction'.
+    type Event = DoorEvent;
+    type Action = DoorAction;
+    type State = ();
+
+    // For error handling, the door simply uses `failure` which provides a
+    // generic, chainable error type. In real-world implementations you may want
+    // to use a custom error type or similar.
+    type Error = failure::Error;
+
+    // The implementation of `handle` provides us with the actual transition
+    // logic of the door.
+    //
+    // The door is conceptually not that complicated so it is relatively short.
+    fn handle(self, event: DoorEvent) -> (Self, Vec<DoorAction>) {
+        match (self, event) {
+            // An opened door can be closed:
+            (DoorState::Opened, DoorEvent::Close) => return (DoorState::Closed, vec![]),
+
+            // A closed door can be opened:
+            (DoorState::Closed, DoorEvent::Open) => return (DoorState::Opened, vec![]),
+
+            // A closed door can also be locked, in which case the locking code
+            // is stored with the next state and the unlock attempts default to
+            // three:
+            (DoorState::Closed, DoorEvent::Lock(code)) => {
+                return (DoorState::Locked { code, attempts: 3 }, vec![])
+            }
+
+            // A locked door receiving an `Unlock`-event can do several
+            // different things ...
+            (DoorState::Locked { code, attempts }, DoorEvent::Unlock(unlock_code)) => {
+                // In the happy case, entry of a correct code leads to the door
+                // becoming unlocked (i.e. transitioning back to `Closed`).
+                if code == unlock_code {
+                    return (DoorState::Closed, vec![]);
+                }
+
+                // If the code wasn't correct and the fraudulent unlocker ran
+                // out of attempts (i.e. there was only one attempt remaining),
+                // it's time for some consequences.
+                if attempts == 1 {
+                    return (DoorState::Disabled, vec![DoorAction::CallThePolice]);
+                }
+
+                // If the code wasn't correct, but there are still some
+                // remaining attempts, the user doesn't have to face the police
+                // quite yet but IRC gets to laugh about it.
+                return (
+                    DoorState::Locked {
+                        code,
+                        attempts: attempts - 1,
+                    },
+                    vec![DoorAction::NotifyIRC("invalid code entered".into())],
+                );
+            }
+
+            // This actually already concludes our event-handling logic. Our
+            // uncaring door does absolutely nothing if you attempt to do
+            // something with it that it doesn't support, so the last handler is
+            // a simple fallback.
+            //
+            // In a real-world state machine, especially one that receives
+            // events from external sources, you may want fallback handlers to
+            // actually do something. One example could be creating an action
+            // that logs information about unexpected events, alerts a
+            // monitoring service, or whatever else.
+            (current, _) => (current, vec![]),
+        }
+    }
+
+    // The implementation of `enter` lets door states cause additional actions
+    // they are transitioned to. In the door example we use this only to notify
+    // IRC about what is going on.
+    fn enter(&self) -> Vec<DoorAction> {
+        let msg = match self {
+            DoorState::Opened => "door was opened",
+            DoorState::Closed => "door was closed",
+            DoorState::Locked { .. } => "door was locked",
+            DoorState::Disabled => "door was disabled",
+        };
+
+        vec![DoorAction::NotifyIRC(msg.into())]
+    }
+
+    // The implementation of `act` lets us perform actual side-effects.
+    //
+    // Again, for the sake of educational simplicity, this does not deal with
+    // all potential (or in fact any) error cases that can occur during this toy
+    // implementation of actions.
+    //
+    // Additionally the `act` function can return new events. This is useful for
+    // a sort of "callback-like" pattern (cause an action to fetch some data,
+    // receive it as an event) but is not used in this example.
+    fn act(action: DoorAction, _state: &()) -> Result<Vec<DoorEvent>, failure::Error> {
+        match action {
+            DoorAction::NotifyIRC(msg) => {
+                use std::fs::OpenOptions;
+                use std::io::Write;
+
+                let mut file = OpenOptions::new()
+                    .append(true)
+                    .create(true)
+                    .open("/tmp/door-irc.log")?;
+
+                write!(file, "<doorbot> {}\n", msg)?;
+                Ok(vec![])
+            }
+
+            DoorAction::CallThePolice => {
+                // TODO: call the police
+                println!("The police was called! For real!");
+                Ok(vec![])
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use finito::advance;
+
+    fn test_fsm<S: FSM>(initial: S, events: Vec<S::Event>) -> (S, Vec<S::Action>) {
+        events
+            .into_iter()
+            .fold((initial, vec![]), |(state, mut actions), event| {
+                let (new_state, mut new_actions) = advance(state, event);
+                actions.append(&mut new_actions);
+                (new_state, actions)
+            })
+    }
+
+    #[test]
+    fn test_door() {
+        let initial = DoorState::Opened;
+        let events = vec![
+            DoorEvent::Close,
+            DoorEvent::Open,
+            DoorEvent::Close,
+            DoorEvent::Lock(1234),
+            DoorEvent::Unlock(1234),
+            DoorEvent::Lock(4567),
+            DoorEvent::Unlock(1234),
+        ];
+        let (final_state, actions) = test_fsm(initial, events);
+
+        assert_eq!(final_state, DoorState::Locked {
+            code: 4567,
+            attempts: 2
+        });
+        assert_eq!(actions, vec![
+            DoorAction::NotifyIRC("door was closed".into()),
+            DoorAction::NotifyIRC("door was opened".into()),
+            DoorAction::NotifyIRC("door was closed".into()),
+            DoorAction::NotifyIRC("door was locked".into()),
+            DoorAction::NotifyIRC("door was closed".into()),
+            DoorAction::NotifyIRC("door was locked".into()),
+            DoorAction::NotifyIRC("invalid code entered".into()),
+        ]);
+    }
+}
diff --git a/users/tazjin/finito/finito-postgres/Cargo.toml b/users/tazjin/finito/finito-postgres/Cargo.toml
new file mode 100644
index 0000000000..dd8d1d0003
--- /dev/null
+++ b/users/tazjin/finito/finito-postgres/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "finito-postgres"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+
+[dependencies]
+chrono = "0.4"
+postgres-derive = "0.3"
+serde = "1.0"
+serde_json = "1.0"
+r2d2_postgres = "0.14"
+
+[dependencies.postgres]
+version = "0.15"
+features = [ "with-uuid", "with-chrono", "with-serde_json" ]
+
+[dependencies.uuid]
+version = "0.5"
+features = [ "v4" ]
+
+[dependencies.finito]
+path = "../finito-core"
+
+[dev-dependencies.finito-door]
+path = "../finito-door"
diff --git a/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql
new file mode 100644
index 0000000000..9b56f9d35a
--- /dev/null
+++ b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql
@@ -0,0 +1,4 @@
+DROP TABLE actions;
+DROP TYPE ActionStatus;
+DROP TABLE events;
+DROP TABLE machines;
diff --git a/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql
new file mode 100644
index 0000000000..18ace393b8
--- /dev/null
+++ b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql
@@ -0,0 +1,37 @@
+-- Creates the initial schema required by finito-postgres.
+
+CREATE TABLE machines (
+  id UUID PRIMARY KEY,
+  created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+  fsm TEXT NOT NULL,
+  state JSONB NOT NULL
+);
+
+CREATE TABLE events (
+  id UUID PRIMARY KEY,
+  created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+  fsm TEXT NOT NULL,
+  fsm_id UUID NOT NULL REFERENCES machines(id),
+  event JSONB NOT NULL
+);
+CREATE INDEX idx_events_machines ON events(fsm_id);
+
+CREATE TYPE ActionStatus AS ENUM (
+  'Pending',
+  'Completed',
+  'Failed'
+);
+
+CREATE TABLE actions (
+  id UUID PRIMARY KEY,
+  created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+  fsm TEXT NOT NULL,
+  fsm_id UUID NOT NULL REFERENCES machines(id),
+  event_id UUID NOT NULL REFERENCES events(id),
+  content JSONB NOT NULL,
+  status ActionStatus NOT NULL,
+  error TEXT
+);
+
+CREATE INDEX idx_actions_machines ON actions(fsm_id);
+CREATE INDEX idx_actions_events ON actions(event_id);
diff --git a/users/tazjin/finito/finito-postgres/src/error.rs b/users/tazjin/finito/finito-postgres/src/error.rs
new file mode 100644
index 0000000000..ed33775cd7
--- /dev/null
+++ b/users/tazjin/finito/finito-postgres/src/error.rs
@@ -0,0 +1,103 @@
+//! This module defines error types and conversions for issue that can
+//! occur while dealing with persisted state machines.
+
+use std::error::Error as StdError;
+use std::{fmt, result};
+use uuid::Uuid;
+
+// errors to chain:
+use postgres::Error as PgError;
+use r2d2_postgres::r2d2::Error as PoolError;
+use serde_json::Error as JsonError;
+
+pub type Result<T> = result::Result<T, Error>;
+
+#[derive(Debug)]
+pub struct Error {
+    pub kind: ErrorKind,
+    pub context: Option<String>,
+}
+
+#[derive(Debug)]
+pub enum ErrorKind {
+    /// Errors occuring during JSON serialization of FSM types.
+    Serialization(String),
+
+    /// Errors occuring during communication with the database.
+    Database(String),
+
+    /// Errors with the database connection pool.
+    DBPool(String),
+
+    /// State machine could not be found.
+    FSMNotFound(Uuid),
+
+    /// Action could not be found.
+    ActionNotFound(Uuid),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use ErrorKind::*;
+        let msg = match &self.kind {
+            Serialization(err) => format!("JSON serialization error: {}", err),
+
+            Database(err) => format!("PostgreSQL error: {}", err),
+
+            DBPool(err) => format!("Database connection pool error: {}", err),
+
+            FSMNotFound(id) => format!("FSM with ID {} not found", id),
+
+            ActionNotFound(id) => format!("Action with ID {} not found", id),
+        };
+
+        match &self.context {
+            None => write!(f, "{}", msg),
+            Some(ctx) => write!(f, "{}: {}", ctx, msg),
+        }
+    }
+}
+
+impl StdError for Error {}
+
+impl<E: Into<ErrorKind>> From<E> for Error {
+    fn from(err: E) -> Error {
+        Error {
+            kind: err.into(),
+            context: None,
+        }
+    }
+}
+
+impl From<JsonError> for ErrorKind {
+    fn from(err: JsonError) -> ErrorKind {
+        ErrorKind::Serialization(err.to_string())
+    }
+}
+
+impl From<PgError> for ErrorKind {
+    fn from(err: PgError) -> ErrorKind {
+        ErrorKind::Database(err.to_string())
+    }
+}
+
+impl From<PoolError> for ErrorKind {
+    fn from(err: PoolError) -> ErrorKind {
+        ErrorKind::DBPool(err.to_string())
+    }
+}
+
+/// Helper trait that makes it possible to supply contextual
+/// information with an error.
+pub trait ResultExt<T> {
+    fn context<C: fmt::Display>(self, ctx: C) -> Result<T>;
+}
+
+impl<T, E: Into<Error>> ResultExt<T> for result::Result<T, E> {
+    fn context<C: fmt::Display>(self, ctx: C) -> Result<T> {
+        self.map_err(|err| Error {
+            context: Some(format!("{}", ctx)),
+            ..err.into()
+        })
+    }
+}
diff --git a/users/tazjin/finito/finito-postgres/src/lib.rs b/users/tazjin/finito/finito-postgres/src/lib.rs
new file mode 100644
index 0000000000..ea63cc9dfd
--- /dev/null
+++ b/users/tazjin/finito/finito-postgres/src/lib.rs
@@ -0,0 +1,456 @@
+//! PostgreSQL-backed persistence for Finito state machines
+//!
+//! This module implements ... TODO when I can write again.
+//!
+//! TODO: events & actions should have `SERIAL` keys
+
+#[macro_use]
+extern crate postgres;
+#[macro_use]
+extern crate postgres_derive;
+
+extern crate chrono;
+extern crate finito;
+extern crate r2d2_postgres;
+extern crate serde;
+extern crate serde_json;
+extern crate uuid;
+
+#[cfg(test)]
+mod tests;
+#[cfg(test)]
+extern crate finito_door;
+
+mod error;
+pub use error::{Error, ErrorKind, Result};
+
+use chrono::prelude::{DateTime, Utc};
+use error::ResultExt;
+use finito::{FSMBackend, FSM};
+use postgres::transaction::Transaction;
+use postgres::GenericConnection;
+use r2d2_postgres::{r2d2, PostgresConnectionManager};
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use serde_json::Value;
+use std::marker::PhantomData;
+use uuid::Uuid;
+
+type DBPool = r2d2::Pool<PostgresConnectionManager>;
+type DBConn = r2d2::PooledConnection<PostgresConnectionManager>;
+
+/// This struct represents rows in the database table in which events
+/// are persisted.
+#[derive(Debug, ToSql, FromSql)]
+struct EventT {
+    /// ID of the persisted event.
+    id: Uuid,
+
+    /// Timestamp at which the event was stored.
+    created: DateTime<Utc>,
+
+    /// Name of the type of FSM that this state belongs to.
+    fsm: String,
+
+    /// ID of the state machine belonging to this event.
+    fsm_id: Uuid,
+
+    /// Serialised content of the event.
+    event: Value,
+}
+
+/// This enum represents the possible statuses an action can be in.
+#[derive(Debug, PartialEq, ToSql, FromSql)]
+#[postgres(name = "actionstatus")]
+enum ActionStatus {
+    /// The action was requested but has not run yet.
+    Pending,
+
+    /// The action completed successfully.
+    Completed,
+
+    /// The action failed to run. Information about the error will
+    /// have been persisted in Postgres.
+    Failed,
+}
+
+/// This struct represents rows in the database table in which actions
+/// are persisted.
+#[derive(Debug, ToSql, FromSql)]
+struct ActionT {
+    /// ID of the persisted event.
+    id: Uuid,
+
+    /// Timestamp at which the event was stored.
+    created: DateTime<Utc>,
+
+    /// Name of the type of FSM that this state belongs to.
+    fsm: String,
+
+    /// ID of the state machine belonging to this event.
+    fsm_id: Uuid,
+
+    /// ID of the event that resulted in this action.
+    event_id: Uuid,
+
+    /// Serialised content of the action.
+    #[postgres(name = "content")] // renamed because 'action' is a keyword in PG
+    action: Value,
+
+    /// Current status of the action.
+    status: ActionStatus,
+
+    /// Detailed (i.e. Debug-trait formatted) error message, if an
+    /// error occured during action processing.
+    error: Option<String>,
+}
+
+// The following functions implement the public interface of
+// `finito-postgres`.
+
+/// TODO: Write docs for this type, brain does not want to do it right
+/// now.
+pub struct FinitoPostgres<S> {
+    state: S,
+
+    db_pool: DBPool,
+}
+
+impl<S> FinitoPostgres<S> {
+    pub fn new(state: S, db_pool: DBPool, _pool_size: usize) -> Self {
+        FinitoPostgres { state, db_pool }
+    }
+}
+
+impl<State: 'static> FSMBackend<State> for FinitoPostgres<State> {
+    type Key = Uuid;
+    type Error = Error;
+
+    fn insert_machine<S: FSM + Serialize>(&self, initial: S) -> Result<Uuid> {
+        let query = r#"
+          INSERT INTO machines (id, fsm, state)
+          VALUES ($1, $2, $3)
+        "#;
+
+        let id = Uuid::new_v4();
+        let fsm = S::FSM_NAME.to_string();
+        let state = serde_json::to_value(initial).context("failed to serialise FSM")?;
+
+        self.conn()?
+            .execute(query, &[&id, &fsm, &state])
+            .context("failed to insert FSM")?;
+
+        return Ok(id);
+    }
+
+    fn get_machine<S: FSM + DeserializeOwned>(&self, key: Uuid) -> Result<S> {
+        get_machine_internal(&*self.conn()?, key, false)
+    }
+
+    /// Advance a persisted state machine by applying an event, and
+    /// storing the event as well as all resulting actions.
+    ///
+    /// This function holds a database-lock on the state's row while
+    /// advancing the machine.
+    ///
+    /// **Note**: This function returns the new state of the machine
+    /// immediately after applying the event, however this does not
+    /// necessarily equate to the state of the machine after all related
+    /// processing is finished as running actions may result in additional
+    /// transitions.
+    fn advance<'a, S>(&'a self, key: Uuid, event: S::Event) -> Result<S>
+    where
+        S: FSM + Serialize + DeserializeOwned,
+        S::State: From<&'a State>,
+        S::Event: Serialize + DeserializeOwned,
+        S::Action: Serialize + DeserializeOwned,
+    {
+        let conn = self.conn()?;
+        let tx = conn.transaction().context("could not begin transaction")?;
+        let state = get_machine_internal(&tx, key, true)?;
+
+        // Advancing the FSM consumes the event, so it is persisted first:
+        let event_id = insert_event::<_, S>(&tx, key, &event)?;
+
+        // Core advancing logic is run:
+        let (new_state, actions) = finito::advance(state, event);
+
+        // Resulting actions are persisted (TODO: and interpreted)
+        let mut action_ids = vec![];
+        for action in actions {
+            let action_id = insert_action::<_, S>(&tx, key, event_id, &action)?;
+            action_ids.push(action_id);
+        }
+
+        // And finally the state is updated:
+        update_state(&tx, key, &new_state)?;
+        tx.commit().context("could not commit transaction")?;
+
+        self.run_actions::<S>(key, action_ids);
+
+        Ok(new_state)
+    }
+}
+
+impl<State: 'static> FinitoPostgres<State> {
+    /// Execute several actions at the same time, each in a separate
+    /// thread. Note that actions returning further events, causing
+    /// further transitions, returning further actions and so on will
+    /// potentially cause multiple threads to get created.
+    fn run_actions<'a, S>(&'a self, fsm_id: Uuid, action_ids: Vec<Uuid>)
+    where
+        S: FSM + Serialize + DeserializeOwned,
+        S::Event: Serialize + DeserializeOwned,
+        S::Action: Serialize + DeserializeOwned,
+        S::State: From<&'a State>,
+    {
+        let state: S::State = (&self.state).into();
+        let conn = self.conn().expect("TODO");
+
+        for action_id in action_ids {
+            let tx = conn.transaction().expect("TODO");
+
+            // TODO: Determine which concurrency setup we actually want.
+            if let Ok(events) = run_action(tx, action_id, &state, PhantomData::<S>) {
+                for event in events {
+                    self.advance::<S>(fsm_id, event).expect("TODO");
+                }
+            }
+        }
+    }
+
+    /// Retrieve a single connection from the database connection pool.
+    fn conn(&self) -> Result<DBConn> {
+        self.db_pool
+            .get()
+            .context("failed to retrieve connection from pool")
+    }
+}
+
+/// Insert a single state-machine into the database and return its
+/// newly allocated, random UUID.
+pub fn insert_machine<C, S>(conn: &C, initial: S) -> Result<Uuid>
+where
+    C: GenericConnection,
+    S: FSM + Serialize,
+{
+    let query = r#"
+      INSERT INTO machines (id, fsm, state)
+      VALUES ($1, $2, $3)
+    "#;
+
+    let id = Uuid::new_v4();
+    let fsm = S::FSM_NAME.to_string();
+    let state = serde_json::to_value(initial).context("failed to serialize FSM")?;
+
+    conn.execute(query, &[&id, &fsm, &state])?;
+
+    return Ok(id);
+}
+
+/// Insert a single event into the database and return its UUID.
+fn insert_event<C, S>(conn: &C, fsm_id: Uuid, event: &S::Event) -> Result<Uuid>
+where
+    C: GenericConnection,
+    S: FSM,
+    S::Event: Serialize,
+{
+    let query = r#"
+      INSERT INTO events (id, fsm, fsm_id, event)
+      VALUES ($1, $2, $3, $4)
+    "#;
+
+    let id = Uuid::new_v4();
+    let fsm = S::FSM_NAME.to_string();
+    let event_value = serde_json::to_value(event).context("failed to serialize event")?;
+
+    conn.execute(query, &[&id, &fsm, &fsm_id, &event_value])?;
+    return Ok(id);
+}
+
+/// Insert a single action into the database and return its UUID.
+fn insert_action<C, S>(conn: &C, fsm_id: Uuid, event_id: Uuid, action: &S::Action) -> Result<Uuid>
+where
+    C: GenericConnection,
+    S: FSM,
+    S::Action: Serialize,
+{
+    let query = r#"
+      INSERT INTO actions (id, fsm, fsm_id, event_id, content, status)
+      VALUES ($1, $2, $3, $4, $5, $6)
+    "#;
+
+    let id = Uuid::new_v4();
+    let fsm = S::FSM_NAME.to_string();
+    let action_value = serde_json::to_value(action).context("failed to serialize action")?;
+
+    conn.execute(query, &[
+        &id,
+        &fsm,
+        &fsm_id,
+        &event_id,
+        &action_value,
+        &ActionStatus::Pending,
+    ])?;
+
+    return Ok(id);
+}
+
+/// Update the state of a specified machine.
+fn update_state<C, S>(conn: &C, fsm_id: Uuid, state: &S) -> Result<()>
+where
+    C: GenericConnection,
+    S: FSM + Serialize,
+{
+    let query = r#"
+      UPDATE machines SET state = $1 WHERE id = $2
+    "#;
+
+    let state_value = serde_json::to_value(state).context("failed to serialize FSM")?;
+    let res_count = conn.execute(query, &[&state_value, &fsm_id])?;
+
+    if res_count != 1 {
+        Err(ErrorKind::FSMNotFound(fsm_id).into())
+    } else {
+        Ok(())
+    }
+}
+
+/// Conditionally alter SQL statement to append locking clause inside
+/// of a transaction.
+fn alter_for_update(alter: bool, query: &str) -> String {
+    match alter {
+        false => query.to_string(),
+        true => format!("{} FOR UPDATE", query),
+    }
+}
+
+/// Retrieve the current state of a state machine from the database,
+/// optionally locking the machine state for the duration of some
+/// enclosing transaction.
+fn get_machine_internal<C, S>(conn: &C, id: Uuid, for_update: bool) -> Result<S>
+where
+    C: GenericConnection,
+    S: FSM + DeserializeOwned,
+{
+    let query = alter_for_update(
+        for_update,
+        r#"
+      SELECT state FROM machines WHERE id = $1
+    "#,
+    );
+
+    let rows = conn
+        .query(&query, &[&id])
+        .context("failed to retrieve FSM")?;
+
+    if let Some(row) = rows.into_iter().next() {
+        Ok(serde_json::from_value(row.get(0)).context("failed to deserialize FSM")?)
+    } else {
+        Err(ErrorKind::FSMNotFound(id).into())
+    }
+}
+
+/// Retrieve an action from the database, optionally locking it for
+/// the duration of some enclosing transaction.
+fn get_action<C, S>(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)>
+where
+    C: GenericConnection,
+    S: FSM,
+    S::Action: DeserializeOwned,
+{
+    let query = alter_for_update(
+        true,
+        r#"
+      SELECT status, content FROM actions
+      WHERE id = $1 AND fsm = $2
+    "#,
+    );
+
+    let rows = conn.query(&query, &[&id, &S::FSM_NAME])?;
+
+    if let Some(row) = rows.into_iter().next() {
+        let action =
+            serde_json::from_value(row.get(1)).context("failed to deserialize FSM action")?;
+        Ok((row.get(0), action))
+    } else {
+        Err(ErrorKind::ActionNotFound(id).into())
+    }
+}
+
+/// Update the status of an action after an attempt to run it.
+fn update_action_status<C, S>(
+    conn: &C,
+    id: Uuid,
+    status: ActionStatus,
+    error: Option<String>,
+    _fsm: PhantomData<S>,
+) -> Result<()>
+where
+    C: GenericConnection,
+    S: FSM,
+{
+    let query = r#"
+      UPDATE actions SET status = $1, error = $2
+      WHERE id = $3 AND fsm = $4
+    "#;
+
+    let result = conn.execute(&query, &[&status, &error, &id, &S::FSM_NAME])?;
+
+    if result != 1 {
+        Err(ErrorKind::ActionNotFound(id).into())
+    } else {
+        Ok(())
+    }
+}
+
+/// Execute a single action in case it is pending or retryable. Holds
+/// a lock on the action's database row while performing the action
+/// and writes back the status afterwards.
+///
+/// Should the execution of an action fail cleanly (i.e. without a
+/// panic), the error will be persisted. Should it fail by panicking
+/// (which developers should never do explicitly in action
+/// interpreters) its status will not be changed.
+fn run_action<S>(
+    tx: Transaction,
+    id: Uuid,
+    state: &S::State,
+    _fsm: PhantomData<S>,
+) -> Result<Vec<S::Event>>
+where
+    S: FSM,
+    S::Action: DeserializeOwned,
+{
+    let (status, action) = get_action::<Transaction, S>(&tx, id)?;
+
+    let result = match status {
+        ActionStatus::Pending => {
+            match S::act(action, state) {
+                // If the action succeeded, update its status to
+                // completed and return the created events.
+                Ok(events) => {
+                    update_action_status(&tx, id, ActionStatus::Completed, None, PhantomData::<S>)?;
+                    events
+                }
+
+                // If the action failed, persist the debug message and
+                // return nothing.
+                Err(err) => {
+                    let msg = Some(format!("{:?}", err));
+                    update_action_status(&tx, id, ActionStatus::Failed, msg, PhantomData::<S>)?;
+                    vec![]
+                }
+            }
+        }
+
+        _ => {
+            // TODO: Currently only pending actions are run because
+            // retryable actions are not yet implemented.
+            vec![]
+        }
+    };
+
+    tx.commit().context("failed to commit transaction")?;
+    Ok(result)
+}
diff --git a/users/tazjin/finito/finito-postgres/src/tests.rs b/users/tazjin/finito/finito-postgres/src/tests.rs
new file mode 100644
index 0000000000..dd270c3875
--- /dev/null
+++ b/users/tazjin/finito/finito-postgres/src/tests.rs
@@ -0,0 +1,54 @@
+use super::*;
+
+use finito_door::*;
+use postgres::{Connection, TlsMode};
+
+// TODO: read config from environment
+fn open_test_connection() -> Connection {
+    Connection::connect("postgres://finito:finito@localhost/finito", TlsMode::None)
+        .expect("Failed to connect to test database")
+}
+
+#[test]
+fn test_insert_machine() {
+    let conn = open_test_connection();
+    let initial = DoorState::Opened;
+    let door = insert_machine(&conn, initial).expect("Failed to insert door");
+    let result = get_machine(&conn, &door, false).expect("Failed to fetch door");
+
+    assert_eq!(
+        result,
+        DoorState::Opened,
+        "Inserted door state should match"
+    );
+}
+
+#[test]
+fn test_advance() {
+    let conn = open_test_connection();
+
+    let initial = DoorState::Opened;
+    let events = vec![
+        DoorEvent::Close,
+        DoorEvent::Open,
+        DoorEvent::Close,
+        DoorEvent::Lock(1234),
+        DoorEvent::Unlock(1234),
+        DoorEvent::Lock(4567),
+        DoorEvent::Unlock(1234),
+    ];
+
+    let door = insert_machine(&conn, initial).expect("Failed to insert door");
+
+    for event in events {
+        advance(&conn, &door, event).expect("Failed to advance door FSM");
+    }
+
+    let result = get_machine(&conn, &door, false).expect("Failed to fetch door");
+    let expected = DoorState::Locked {
+        code: 4567,
+        attempts: 2,
+    };
+
+    assert_eq!(result, expected, "Advanced door state should match");
+}
diff --git a/users/tazjin/gruber-darker.qss b/users/tazjin/gruber-darker.qss
new file mode 100644
index 0000000000..16f4c2f329
--- /dev/null
+++ b/users/tazjin/gruber-darker.qss
@@ -0,0 +1,508 @@
+/**
+** Gruber Darker theme for Quassel.
+**
+** This theme derives from multiple different things:
+**
+** - Quassel DarkSolarized (https://gist.github.com/Zren/e91ad5197f9d6b6d410f)
+** - Quassel Dracula (https://github.com/dracula/quassel)
+** - gruber-darker for Emacs (https://github.com/rexim/gruber-darker-theme)
+** - Original Gruber theme for BBEdit (https://daringfireball.net/projects/bbcolors/schemes/)
+**
+** This is a work-in-progress as I haven't figured out the point of
+** all of the colours yet, and what I want them to be instead.
+**
+**/
+
+/**
+** Helpful Links:
+**  - QT:
+**      http://qt-project.org/doc/qt-4.8/stylesheet-syntax.html
+**      http://doc.qt.nokia.com/4.7-snapshot/stylesheet-reference.html
+**      http://doc.qt.nokia.com/4.7-snapshot/stylesheet-examples.html
+**  - Plastique Client Style:
+**      https://qt.gitorious.org/qt/qt/source/src/gui/styles/qplastiquestyle.cpp
+**      https://github.com/mirror/qt/blob/4.8/src/gui/styles/qplastiquestyle.cpp
+**  - Quassel Stylesheet Gallery:
+**      http://bugs.quassel-irc.org/projects/1/wiki/Stylesheet_Gallery
+**      http://bugs.quassel-irc.org/projects/1/wiki/Stylesheet_Gallery#DarkMonokaiqss
+*/
+
+/**
+**  - QSS Notes:
+**      Quassel stylesheets also support Palette { role: color; } for setting the system
+**      palette. See the QPalette docs for available roles, and convert them into qss-style
+**      attributes, so ButtonText would become button-text or see qssparser.cpp In fact,
+**      qssparser.cpp is the authorative source for Quassel's qss syntax that contains all
+**      the extensions over standard Qt qss syntax.
+**      See:
+**          http://qt-project.org/doc/qt-4.8/qpalette.html#ColorRole-enum
+**          https://github.com/quassel/quassel/blob/master/src/uisupport/qssparser.cpp
+**
+*/
+
+Palette {
+    /* Window colors */
+    window: #282828;
+    background: #181818;
+    foreground: #f4f4f4;
+
+    base: #181818;
+    alternate-base: #282828;
+
+    /* Just setting palette(tooltip-base) doesn't work as intended so we set it in
+    ** a QTooltip{} rule as well.
+    */
+    tooltip-base: #282a36; // palette(base) TODO
+    tooltip-text: white; // palette(text) TODO
+
+    /* The following attributes should be done in a scale */
+    light: #444444; // Tab Borders, Scrollbar handle grips, Titled Panel border (Settings)
+    midlight: #333333; // ?
+    button: #292929; // Menu BG, Scrollbar and Button base.
+    mid: #252525; // Titled Panel border (Settings)
+    dark: #202020; // TreeView [-] and ... color (Also various borders in Windows Client Style)
+    shadow: #1d1d1d; // ?
+
+
+    /* Text colors */
+    text: white;
+    button-text: #f8f8f2;
+
+    highlight: #44475a;
+
+    /* Link colors */
+    link: #ff79c6;
+    link-visited: #bd93f9;
+
+    /* Color of the marker line in the chat view. BG Node that is overlayed on the first new ChatLine. */
+    // 0 -> 0.1 (sharp line)
+    marker-line: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #586e75, stop: 0.1 #586e75, stop: 0.1 transparent);
+}
+
+/*
+** Base Object Colors
+*/
+
+/* Tables */
+// QTreeView#settingsTree -> Tree in the Settings popup.
+
+QTreeView, QTableView {
+    alternate-background-color: #282a36;
+    // background-color: palette(shadow);
+    border: 0px;
+}
+
+QTreeView {
+  selection-background-color: transparent;
+}
+
+QTreeView::item {
+  border-left: 2px solid palette(base);
+}
+
+QTreeView::item:focus {
+  border-width: 0 0 0 2px;
+  outline: none;
+}
+
+QTreeView::item:selected {
+  border-width: 0 0 0 2px;
+  color: palette(button-text);
+}
+
+QTreeView::item:hover {
+  background: palette(dark);
+}
+
+
+QTreeView::item:selected:active{
+  color: palette(button-text);
+  background: palette(dark);
+  border-color: palette(highlight);
+}
+
+QTreeView::item:selected:!active {
+  color: palette(button-text);
+  background: palette(dark);
+  border-color: palette(highlight);
+}
+
+/* Scrollbar */
+/* From Quassel Wiki: http://sprunge.us/iZGB */
+QScrollBar {
+    //background: transparent;
+    background: palette(base);
+    margin: 0;
+}
+QScrollBar:hover {
+    /* Optional: Subtle accent of scrolling area on hover */
+    background: #161616; /* base +2 */
+}
+QScrollBar:vertical {
+    width: 8px;
+}
+QScrollBar:horizontal {
+    height: 8px;
+}
+
+QScrollBar::handle {
+    padding: 0;
+    margin: 2px;
+    border-radius: 2px;
+    border: 2px solid palette(midlight);
+    background: palette(midlight);
+}
+
+QScrollBar::handle:vertical {
+    min-height: 20px;
+    min-width: 0px;
+}
+
+QScrollBar::handle:horizontal {
+    min-width: 20px;
+    min-height: 0px;
+}
+QScrollBar::handle:hover {
+    border-color: palette(light);
+    background: palette(light);
+}
+QScrollBar::handle:pressed {
+    background: palette(highlight);
+    border-color: palette(highlight);
+}
+
+QScrollBar::add-line , QScrollBar::sub-line {
+    height: 0px;
+    border: 0px;
+}
+QScrollBar::up-arrow, QScrollBar::down-arrow {
+    border: 0px;
+    width: 0px;
+    height: 0px;
+}
+
+QScrollBar::add-page, QScrollBar::sub-page {
+    background: none;
+}
+
+/* Input Box */
+MultiLineEdit {
+    //background: palette(base);
+    //color: palette(foreground);
+}
+
+/* Widgets */
+/* http://doc.qt.nokia.com/4.7-snapshot/qdockwidget.html */
+//QMainWindow,
+QMainWindow QAbstractScrollArea {
+    //border: 0; // Remove borders.
+    border: 1px solid palette(shadow);
+}
+
+QMainWindow {
+    //background: palette(mid); // Main window trim
+}
+
+/* Splitter */
+/* The splits between QDockWidgets and QMainWindow is a different element. */
+QSplitter::handle,
+QMainWindow::separator {
+	background: palette(dark);
+}
+QSplitter::handle:horizontal:hover,
+QMainWindow::separator:vertical:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 palette(window), stop: 0.5 palette(light), stop: 1 palette(window));
+}
+
+QSplitter::handle:vertical:hover,
+QMainWindow::separator:horizontal:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(window), stop: 0.5 palette(light), stop: 1 palette(window));
+}
+
+/* Menu Bar / Context Menues */
+QMenu {
+    margin: 5px; // A bit of nice padding around menu items.
+}
+
+/* ToolTip */
+/* Note: You cannot create transparent sections in the popup box without a mask set. Thus the black edges outside the rounded borders. */
+QToolTip {
+    border: 2px solid #202020; // palette(dark)
+    border-radius: 2px;
+    background: #282a36; // palette(base)
+    color: white; // palette(text)
+}
+
+/* Tabs */
+/*
+    The palette is designed for the selected one to be darker. So we need to change it. Decided to do a simple line.
+    tab:bottom and tab:top reverse y1 and y2 on the linear gradients.
+
+    Tab Shadow: #444444 (light)
+    Tab Hover: #666
+    Tab Selected: palette(highlight)
+*/
+
+QTabWidget::tab-bar {
+    alignment: center;
+}
+
+QTabBar::tab {
+    min-width: 30px;
+    height: 20px;
+}
+
+QTabBar::tab:bottom:selected {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(highlight), stop: 0.2 palette(highlight), stop: 0.2 transparent);
+}
+
+QTabBar::tab:top:selected {
+    background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 palette(highlight), stop: 0.2 palette(highlight), stop: 0.2 transparent);
+}
+
+QTabBar::tab:!selected {
+    color: #888;
+}
+
+QTabBar::tab:bottom:!selected {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(light), stop: 0.2 palette(light), stop: 0.2 transparent);
+}
+
+QTabBar::tab:top:!selected {
+    background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 palette(light), stop: 0.2 palette(light), stop: 0.2 transparent);
+}
+
+QTabBar::tab:!selected:hover {
+    color: #aaa;
+}
+
+QTabBar::tab:bottom:!selected:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #666, stop: 0.2 #666, stop: 0.2 transparent);
+}
+
+QTabBar::tab:top:!selected:hover {
+    background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 #666, stop: 0.2 #666, stop: 0.2 transparent);
+}
+
+/*
+** Quassel CSS
+*/
+
+/* Main Chat Background Override */
+ChatView {
+    background: #181818;
+}
+ChatView QScrollBar {
+    background: #282a36;
+}
+ChatView QScrollBar:hover {
+    background: #282a36;
+}
+
+ChatView QScrollBar::handle {
+    border-color: #44475a;
+    background: #44475a;
+}
+
+ChatView QScrollBar::handle:hover {
+    border-color: #44475a;
+    background: #44475a;
+}
+
+/**/
+QStatusBar {}
+QStatusBar::item {
+    border: none;
+}
+QStatusBar QLabel {
+    color: #888;
+}
+
+/* https://github.com/quassel/quassel/blob/master/src/qtui/ui/msgprocessorstatuswidget.ui */
+QStatusBar MsgProcessorStatusWidget {}
+QStatusBar MsgProcessorStatusWidget QLabel#label {}
+QStatusBar MsgProcessorStatusWidget QProgressBar#progressBar {}
+
+/* https://github.com/quassel/quassel/blob/master/src/qtui/ui/coreconnectionstatuswidget.ui */
+QStatusBar CoreConnectionStatusWidget {}
+QStatusBar CoreConnectionStatusWidget QLabel#messageLabel {}
+QStatusBar CoreConnectionStatusWidget QProgressBar#progressBar {}
+QStatusBar CoreConnectionStatusWidget QLabel#lagLabel {}
+QStatusBar CoreConnectionStatusWidget QLabel#sslLabel {
+    qproperty-pixmap: none; /* Hide the SSL status icon */
+}
+
+
+/* Font */
+// Will not override if selectors are doubled up eg: "ChatLine, MultiLineEdit {}"
+// These will override anything set in Quassel's Settings.
+/**
+ * Don't bold or style MultiLineEdit text in any way otherwise you will be
+ * prone to get weird behaviour in submitting from the Input box.
+ * It will randomly bold your input if you do.
+ */
+ChatLine {
+    //font-family: "MingLiU_HKSCS-ExtB", "Courier New", Courier, Monotype;
+
+    //font-size: 13pt;
+    //font-weight: bold;
+    }
+MultiLineEdit {
+    //font-family: "MingLiU_HKSCS-ExtB", "Courier New", Courier, Monotype;
+
+    //font-size: 20px;
+    //font-weight: normal;
+    }
+ChatLine#plain {
+    //font-weight: bold;
+    }
+
+/* Font: UI Global Font */
+QWidget {
+    //font-family: consolas;
+    }
+ChatListItem {
+    font-family: consolas;
+    }
+NickListItem {
+    font-family: consolas;
+    }
+StyledLabel#topicLabel {
+    font-family: consolas;
+    font-size: 14px;
+    }
+
+
+/* Topic Box */
+StyledLabel#topicLabel { background: palette(base);  font-family: consolas; }
+
+/* Buffer / Channel List */
+/**
+    state: inactive, channel-event, unread-message, highlighted
+    type: query, channel, network
+**/
+ChatListItem { foreground: #f8f8f2; }
+ChatListItem[state="inactive"] { foreground: #44475a; }
+ChatListItem[state="channel-event"] { foreground: #6272a4; } /* palette(button-text) */
+ChatListItem[state="unread-message"] { foreground: #f8f8f2; }
+ChatListItem[state="highlighted"] { foreground: #44475a; }
+
+ChatListItem[type="network", state="unread-message"] { foreground: #44475a; }
+ChatListItem[type="network", state="highlighted"] { foreground: #44475a; }
+ChatListItem[type="query", state="unread-message"] { foreground: #44475a; }
+
+
+/* Nick List */
+/**
+    state: away
+    type: user, category
+**/
+NickListItem[type="category"] { foreground: #6272a4; }
+NickListItem[type="user"] { foreground: #f8f8f2 }
+NickListItem[type="user", state="away"] { foreground: #44475a; }
+
+
+
+/* Chatbox Line Formatting */
+ChatLine[label="highlight"] {
+    foreground: #f5f5f5;
+    background: #282828;
+}
+
+/*
+** Option: Bold highlighted text, but not the timestamp.
+*/
+/*
+ChatLine[label="highlight"] { font-weight: bold; }
+ChatLine::timestamp[label="highlight"]{ font-weight: normal; }
+*/
+
+ChatLine::timestamp[label="highlight"] { foreground: #44475a; }
+
+ChatLine::timestamp {  }
+
+/* ::contents == Message */
+ChatLine::contents {
+    /* Can only set background */
+}
+
+ChatLine#plain { foreground: #f8f8f2; }
+ChatLine#notice { foreground: #44475a; }
+ChatLine#action { foreground: #565f73; font-style: italic; font-weight: bold; }
+ChatLine#nick { foreground: #6272a4; }
+ChatLine#mode { foreground: #6272a4; }
+ChatLine#join { foreground: #6272a4; }
+ChatLine#part { foreground: #6272a4; }
+ChatLine#quit { foreground: #6272a4; }
+ChatLine#kick { foreground: #6272a4; }
+ChatLine#kill { foreground: #6272a4; }
+ChatLine#server { foreground: #44475a; }
+ChatLine#info { foreground: #44475a; }
+ChatLine#error { foreground: #ff5555; }
+ChatLine#daychange { foreground: #44475a; }
+ChatLine#topic { foreground: #f1fa8c; }
+ChatLine#netsplit-join { foreground: #44475a; }
+ChatLine#netsplit-quit { foreground: #44475a; }
+
+ChatLine::timestamp {
+    foreground: #586e75;
+    // Resets the timestemp font during #action and other possible formatting.
+    font-style: normal;
+    font-weight: normal;
+}
+
+ChatLine::url {
+    foreground: palette(link);
+    //font-style: underline; // Uncomment if you always want an underline on links.
+}
+
+/* Sender Colors */
+ChatLine::sender#plain[sender="self"] { foreground: #586e75; }
+
+/**
+ * The following are the sixteen colours used for the senders.
+ * The names are calculated by taking the hash of the nickname.
+ * Then take the modulo (the remainder) when divided by 16.
+ * Preview: http://i.imgur.com/xeRKI4H.png
+ */
+ChatLine::sender#plain[sender="0"] { foreground: #96a6c8; }
+ChatLine::sender#plain[sender="1"] { foreground: #73c936; }
+ChatLine::sender#plain[sender="2"] { foreground: #ffdd33; }
+ChatLine::sender#plain[sender="3"] { foreground: #cc8c3c; }
+ChatLine::sender#plain[sender="4"] { foreground: #ff4f58; }
+ChatLine::sender#plain[sender="5"] { foreground: #9e95c7; }
+ChatLine::sender#plain[sender="6"] { foreground: #95a99f; }
+ChatLine::sender#plain[sender="7"] { foreground: #8be9fd; }
+
+/* +32 */
+ChatLine::sender#plain[sender="8"] { foreground: #96a6c8; }
+ChatLine::sender#plain[sender="9"] { foreground: #73c936; }
+ChatLine::sender#plain[sender="a"] { foreground: #ffdd33; }
+ChatLine::sender#plain[sender="b"] { foreground: #cc8c3c; }
+ChatLine::sender#plain[sender="c"] { foreground: #ff4f58; }
+ChatLine::sender#plain[sender="d"] { foreground: #9e95c7; }
+ChatLine::sender#plain[sender="e"] { foreground: #95a99f; }
+ChatLine::sender#plain[sender="f"] { foreground: #8be9fd; }
+
+/*
+** mIRC formats
+*/
+ChatLine[format="bold"] { font-weight: bold;}
+ChatLine[format="italic"] { font-style: italic; }
+ChatLine[format="underline"] { font-style: underline; }
+
+/* Blues are hard to read. */
+ChatLine[fg-color="2"] { foreground: #15a; }
+ChatLine[bg-color="2"] { background: #15a; }
+ChatLine[fg-color="c"] { foreground: #15f; }
+ChatLine[bg-color="c"] { background: #15f; }
+
+/*
+** Experimental
+*/
+BufferViewDock[active=true] {
+    /* The circle is hardcoded into the title. */
+    /* Color only changes on a refresh (F5) (so it's pointless). */
+    /* Also colors the border in Breeze. */
+    //color: palette(highlight);
+}
diff --git a/users/tazjin/hanebuschtag.txt b/users/tazjin/hanebuschtag.txt
new file mode 100644
index 0000000000..daeb41c9aa
--- /dev/null
+++ b/users/tazjin/hanebuschtag.txt
@@ -0,0 +1,66 @@
+bazurschnaburkini
+buchweizengrütze
+burkischnurkischnurzelwutz
+burwurgurken
+burwurka
+gaschnurzel
+gezwurkel
+grunzelgewunzel
+gurzelschnurzelgurke
+hanemazurka
+hanemazurkelgurkel
+haneschlawitzka
+haneschnaburkeln
+haneschnawurkagurka
+haneschnawurkel
+haneschnuren
+haneschnurkissima
+hanewurka
+hanewurkini
+hanewurzeln
+ronzelschlawonzel
+ronzelwonzel
+schabernackel
+schabernackensteak
+schlagurkelwini
+schlaraffenwurburzel
+schlawiburschnurschlakini
+schlawonzel
+schlawurkinischnagurka
+schlawurzelgegurkel
+schlawurzeltrollurzel
+schlunzelgarfunzel
+schmonzelgafonzel
+schmotzrotzel
+schnaburka
+schnaburkel
+schnaburkini
+schnackel
+schnarkelbarkel
+schnarwurzelka
+schnawurkeln
+schnawurzelgackschnurschnacksschnicks
+schnawurzini
+schniepel
+schnirkelschini
+schnöckel
+schnockelgockel
+schnorchel
+schnörk
+schnorkelbusch
+schnörkelknörkel
+schnorkelorgel
+schnörks
+schnotzelgekrotzel
+schnudelwurkini
+schnurburka
+schnurkini
+schnurkinihanfini
+schnurzelgawurzel
+schnurzelwurzelwutz
+schnurzelwutz
+strazurkeln
+wazurka
+wurkelgurkel
+wurkelschnurrini
+wurzelchakramahurka
diff --git a/users/tazjin/home/shared.nix b/users/tazjin/home/shared.nix
new file mode 100644
index 0000000000..03c7ee10b7
--- /dev/null
+++ b/users/tazjin/home/shared.nix
@@ -0,0 +1,87 @@
+# Shared home configuration for all machines.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [ "${depot.third_party.impermanence}/home-manager.nix" ];
+
+  home.persistence."/persist/tazjin/home" = {
+    allowOther = true;
+
+    directories = [
+      ".cargo"
+      ".config/audacity"
+      ".config/google-chrome"
+      ".config/quassel-irc.org"
+      ".config/unity3d"
+      ".electrum"
+      ".gnupg"
+      ".local/share/audacity"
+      ".local/share/direnv"
+      ".local/share/fish"
+      ".local/share/keyrings"
+      ".local/share/zoxide"
+      ".mozilla/firefox"
+      ".password-store"
+      ".rustup"
+      ".ssh"
+      ".steam"
+      ".telega"
+      "go"
+      "mail"
+    ];
+
+    files = [
+      ".notmuch-config"
+    ];
+  };
+
+  home.activation.screenshots = lib.hm.dag.entryAnywhere ''
+    $DRY_RUN_CMD mkdir -p $HOME/screenshots
+  '';
+
+  programs.git = {
+    enable = true;
+    userName = "Vincent Ambo";
+    userEmail = "mail@tazj.in";
+    extraConfig = {
+      pull.rebase = true;
+      init.defaultBranch = "canon";
+      safe.directory = [ "/depot" ];
+    };
+  };
+
+  programs.fish = {
+    enable = true;
+    interactiveShellInit = ''
+      ${pkgs.zoxide}/bin/zoxide init fish | source
+    '';
+  };
+
+  services.screen-locker = {
+    enable = true;
+    enableDetectSleep = true;
+    inactiveInterval = 10; # minutes
+    lockCmd = "${depot.users.tazjin.screenLock}/bin/tazjin-screen-lock";
+  };
+
+  services.picom = {
+    enable = true;
+    vSync = true;
+    backend = "glx";
+  };
+
+  # Enable the dunst notification daemon, but force the
+  # configuration file separately instead of going via the strange
+  # Nix->dunstrc encoding route.
+  services.dunst.enable = true;
+  xdg.configFile."dunst/dunstrc" = {
+    source = depot.users.tazjin.dotfiles.dunstrc;
+    onChange = ''
+      ${pkgs.procps}/bin/pkill -u "$USER" ''${VERBOSE+-e} dunst || true
+    '';
+  };
+
+  systemd.user.startServices = true;
+}
diff --git a/users/tazjin/home/tverskoy.nix b/users/tazjin/home/tverskoy.nix
new file mode 100644
index 0000000000..d72734920e
--- /dev/null
+++ b/users/tazjin/home/tverskoy.nix
@@ -0,0 +1,17 @@
+# Home manage configuration for tverskoy.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [
+    depot.users.tazjin.home.shared
+  ];
+
+  home.persistence."/persist/tazjin/home" = {
+    directories = [
+      ".config/spotify"
+      ".local/share/Steam"
+    ];
+  };
+}
diff --git a/users/tazjin/home/zamalek.nix b/users/tazjin/home/zamalek.nix
new file mode 100644
index 0000000000..6bac67eb1c
--- /dev/null
+++ b/users/tazjin/home/zamalek.nix
@@ -0,0 +1,10 @@
+# Home manage configuration for zamalek.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [
+    depot.users.tazjin.home.shared
+  ];
+}
diff --git a/users/tazjin/homepage/default.nix b/users/tazjin/homepage/default.nix
new file mode 100644
index 0000000000..0edb75d609
--- /dev/null
+++ b/users/tazjin/homepage/default.nix
@@ -0,0 +1,78 @@
+# Assembles the website index and configures an nginx instance to
+# serve it.
+#
+# The website is made up of a simple header&footer and content
+# elements for things such as blog posts and projects.
+#
+# Content for the blog is in //users/tazjin/blog instead of here.
+{ depot, lib, pkgs, ... }@args:
+
+with depot;
+with nix.yants;
+
+let
+  inherit (builtins) readFile replaceStrings sort;
+  inherit (pkgs) writeFile runCommandNoCC;
+
+  # The different types of entries on the homepage.
+  entryClass = enum "entryClass" [ "blog" "project" "misc" ];
+
+  # The definition of a single entry.
+  entry = struct "entry" {
+    class = entryClass;
+    title = string;
+    url = string;
+    date = int; # epoch
+    description = option string;
+  };
+
+  escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
+
+  postToEntry = defun [ web.blog.post entry ] (post: {
+    class = "blog";
+    title = post.title;
+    url = "/blog/${post.key}";
+    date = post.date;
+  });
+
+  formatDate = defun [ int string ] (date: readFile (runCommandNoCC "date" { } ''
+    date --date='@${toString date}' '+%Y-%m-%d' > $out
+  ''));
+
+  formatEntryDate = defun [ entry string ] (entry: entryClass.match entry.class {
+    blog = "Blog post from ${formatDate entry.date}";
+    project = "Project from ${formatDate entry.date}";
+    misc = "Posted on ${formatDate entry.date}";
+  });
+
+  entryToDiv = defun [ entry string ] (entry: ''
+    <a href="${entry.url}" class="entry ${entry.class}">
+      <div>
+        <p class="entry-title">${escape entry.title}</p>
+        ${
+          lib.optionalString ((entry ? description) && (entry.description != null))
+          "<p class=\"entry-description\">${escape entry.description}</p>"
+        }
+        <p class="entry-date">${formatEntryDate entry}</p>
+      </div>
+    </a>
+  '');
+
+  index = entries: pkgs.writeText "index.html" (lib.concatStrings (
+    [ (builtins.readFile ./header.html) ]
+    ++ (map entryToDiv (sort (a: b: a.date > b.date) entries))
+    ++ [ (builtins.readFile ./footer.html) ]
+  ));
+
+  pageEntries = import ./entries.nix;
+  homepage = index ((map postToEntry users.tazjin.blog.posts) ++ pageEntries);
+  atomFeed = import ./feed.nix (args // { inherit entry pageEntries; });
+in
+runCommandNoCC "website" { } ''
+  mkdir $out
+  cp ${homepage} $out/index.html
+  cp ${atomFeed} $out/feed.atom
+  mkdir $out/static
+  cp -r ${depot.web.static}/* $out/static
+  cp -rf ${./static}/* $out/static
+''
diff --git a/users/tazjin/homepage/entries.nix b/users/tazjin/homepage/entries.nix
new file mode 100644
index 0000000000..9e43516e53
--- /dev/null
+++ b/users/tazjin/homepage/entries.nix
@@ -0,0 +1,103 @@
+[
+  {
+    class = "misc";
+    title = "@tazlog on Telegram";
+    url = "https://t.me/tazlog";
+    date = 1643321164;
+    description = ''
+      My new channel on Telegram, for occasional updates smaller (and
+      more frequent) than what ends up being posted here.
+    '';
+  }
+  {
+    class = "project";
+    title = "Ship It! #37";
+    url = "https://changelog.com/shipit/37";
+    date = 1641819600;
+    description = ''
+      Episode #37 of Ship It!, a podcast about systems, featuring me.
+      We talk about TVL, Nix, monorepos and related things.
+    '';
+  }
+  {
+    class = "project";
+    title = "Tvix";
+    url = "https://tvl.fyi/blog/rewriting-nix";
+    date = 1638381387;
+    description = ''
+      TVL is rewriting Nix with funding from NLNet.
+    '';
+  }
+  {
+    class = "misc";
+    title = "Interview with Joscha Bach";
+    url = "https://www.youtube.com/watch?v=P-2P3MSZrBM";
+    date = 1594594800;
+    description = ''
+      A fascinating, mind-bending interview by Lex Fridman with Joscha
+      Bach about the Nature of the Universe.
+    '';
+  }
+  {
+    class = "misc";
+    title = "The Virus Lounge";
+    url = "https://tvl.fyi";
+    date = 1587435629;
+    description = "A community around Nix, monorepos, build tooling and the like!";
+  }
+  {
+    class = "project";
+    title = "depot";
+    url = "https://code.tvl.fyi/about";
+    date = 1576800000;
+    description = "Merging all of my projects into a single, Nix-based monorepo";
+  }
+  {
+    class = "project";
+    title = "Nixery";
+    url = "https://github.com/google/nixery";
+    date = 1565132400;
+    description = "A Nix-backed container registry that builds container images on demand";
+  }
+  {
+    class = "project";
+    title = "kontemplate";
+    url = "https://code.tvl.fyi/about/ops/kontemplate";
+    date = 1486550940;
+    description = "Simple file templating tool built for Kubernetes resources";
+  }
+  {
+    class = "misc";
+    title = "dottime";
+    url = "https://dotti.me/";
+    date = 1560898800;
+    description = "A universal convention for conveying time (by edef <3)";
+  }
+  {
+    class = "project";
+    title = "journaldriver";
+    url = "https://code.tvl.fyi/about/ops/journaldriver";
+    date = 1527375600;
+    description = "Small daemon to forward logs from journald to Stackdriver Logging";
+  }
+  {
+    class = "misc";
+    title = "Principia Discordia";
+    url = "https://principiadiscordia.com/book/1.php";
+    date = 1495494000;
+    description = ''
+      The Principia is a short book I read as a child, and didn't
+      understand until much later. It shaped much of my world view.
+    '';
+  }
+  {
+    class = "misc";
+    title = "This Week in Virology";
+    url = "http://www.microbe.tv/twiv/";
+    date = 1585517557;
+    description = ''
+      Podcast with high-quality information about virology,
+      epidemiology and so on. Highly relevant to COVID19.
+    '';
+  }
+]
diff --git a/users/tazjin/homepage/feed.nix b/users/tazjin/homepage/feed.nix
new file mode 100644
index 0000000000..09bc363414
--- /dev/null
+++ b/users/tazjin/homepage/feed.nix
@@ -0,0 +1,43 @@
+# Creates the Atom feed for my homepage.
+{ depot, lib, pkgs, entry, pageEntries, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) map readFile;
+  inherit (lib) max singleton;
+  inherit (pkgs) writeText;
+  inherit (depot.web) blog atom-feed;
+
+  pageEntryToEntry = defun [ entry atom-feed.entry ] (e: {
+    id = "tazjin:${e.class}:${toString e.date}";
+    updated = e.date;
+    published = e.date;
+    title = e.title;
+    summary = e.description;
+
+    links = singleton {
+      rel = "alternate";
+      href = e.url;
+    };
+  });
+
+  allEntries = (with depot.users.tazjin.blog; map (blog.toFeedEntry config) posts)
+    ++ (map pageEntryToEntry pageEntries);
+
+  feed = {
+    id = "https://tazj.in/";
+    title = "tazjin's interblag";
+    subtitle = "my posts, projects and other interesting things";
+    rights = "© 2020 tazjin";
+    authors = [ "tazjin" ];
+
+    links = singleton {
+      rel = "self";
+      href = "https://tazjin/feed.atom";
+    };
+
+    entries = allEntries;
+  };
+in
+writeText "feed.atom" (atom-feed.renderFeed feed)
diff --git a/users/tazjin/homepage/footer.html b/users/tazjin/homepage/footer.html
new file mode 100644
index 0000000000..2f17135066
--- /dev/null
+++ b/users/tazjin/homepage/footer.html
@@ -0,0 +1,2 @@
+  </div>
+</body>
diff --git a/users/tazjin/homepage/header.html b/users/tazjin/homepage/header.html
new file mode 100644
index 0000000000..5a22d9eb7b
--- /dev/null
+++ b/users/tazjin/homepage/header.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<head><meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="description" content="tazjin&#39;s blog">
+  <link rel="stylesheet" type="text/css" href="static/tvl.css" media="all">
+  <link rel="icon" type="image/webp" href="/static/favicon.webp">
+  <link rel="alternate" type="application/atom+xml" href="/feed.atom">
+  <title>tazjin&#39;s interblag</title>
+</head>
+<body class="dark">
+  <header>
+    <h1>
+      <a class="interblag-title" href="/">tazjin&#39;s interblag</a>
+    </h1>
+    <hr>
+  </header>
+  <div class="introduction">
+    <p>Hi, I'm tazjin.</p>
+    <p>
+      I spend a lot of my time hacking on the
+      <a class="dark-link" href="https://tvl.fyi">TVL</a> monorepo and
+      doing other computer-related things. Follow me
+      on <a class="dark-link" href="https://t.me/tazlog">Telegram</a>,
+      via the feed here or (occasionally) catch me in-person
+      at <a href="https://undef.club/#about" class="dark-link">
+      undef.club</a>.
+    </p>
+    <p>
+      Below is a collection of
+      my <span class="project">projects</span>, <span class="blog">blog
+      posts</span> and some <span class="misc">random things</span> by
+      me or others. If you'd like to get in touch about anything, send
+      me a mail at mail@[this domain] or ping me on IRC.
+    </p>
+  </div>
+  <div class="entry-container">
diff --git a/users/tazjin/homepage/static/favicon.webp b/users/tazjin/homepage/static/favicon.webp
new file mode 100644
index 0000000000..f99c908534
--- /dev/null
+++ b/users/tazjin/homepage/static/favicon.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/dominator.webp b/users/tazjin/homepage/static/img/nixery/dominator.webp
new file mode 100644
index 0000000000..2d8569a6ca
--- /dev/null
+++ b/users/tazjin/homepage/static/img/nixery/dominator.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/example_extra.webp b/users/tazjin/homepage/static/img/nixery/example_extra.webp
new file mode 100644
index 0000000000..101f0f633a
--- /dev/null
+++ b/users/tazjin/homepage/static/img/nixery/example_extra.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/example_plain.webp b/users/tazjin/homepage/static/img/nixery/example_plain.webp
new file mode 100644
index 0000000000..a2b90b3e21
--- /dev/null
+++ b/users/tazjin/homepage/static/img/nixery/example_plain.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/ideal_layout.webp b/users/tazjin/homepage/static/img/nixery/ideal_layout.webp
new file mode 100644
index 0000000000..0e9f745566
--- /dev/null
+++ b/users/tazjin/homepage/static/img/nixery/ideal_layout.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_1.webp b/users/tazjin/homepage/static/img/watchblob_1.webp
new file mode 100644
index 0000000000..27e588e1a1
--- /dev/null
+++ b/users/tazjin/homepage/static/img/watchblob_1.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_2.webp b/users/tazjin/homepage/static/img/watchblob_2.webp
new file mode 100644
index 0000000000..b2dea98b4f
--- /dev/null
+++ b/users/tazjin/homepage/static/img/watchblob_2.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_3.webp b/users/tazjin/homepage/static/img/watchblob_3.webp
new file mode 100644
index 0000000000..99b49373b5
--- /dev/null
+++ b/users/tazjin/homepage/static/img/watchblob_3.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_4.webp b/users/tazjin/homepage/static/img/watchblob_4.webp
new file mode 100644
index 0000000000..41dbdb6be1
--- /dev/null
+++ b/users/tazjin/homepage/static/img/watchblob_4.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_5.webp b/users/tazjin/homepage/static/img/watchblob_5.webp
new file mode 100644
index 0000000000..c42a4ce1bc
--- /dev/null
+++ b/users/tazjin/homepage/static/img/watchblob_5.webp
Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_6.webp b/users/tazjin/homepage/static/img/watchblob_6.webp
new file mode 100644
index 0000000000..1440761859
--- /dev/null
+++ b/users/tazjin/homepage/static/img/watchblob_6.webp
Binary files differdiff --git a/users/tazjin/keys/default.nix b/users/tazjin/keys/default.nix
new file mode 100644
index 0000000000..1212c37d36
--- /dev/null
+++ b/users/tazjin/keys/default.nix
@@ -0,0 +1,11 @@
+# My SSH public keys
+{ ... }:
+
+let withAll = keys: keys // { all = builtins.attrValues keys; };
+in withAll {
+  # frog = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKMZzRdcrHTuCPoaFy36MPr5IW/hnImlse/OBOn6udL/ tazjin@frog";
+  tverskoy = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBAWvA3RpXpMAqruUbB+eVgvvHCzhs5R9khFRza3YSLeFiIqOxVVgyhzW/BnCSD9t/5JrqRdJIGQLnkQU9m4REhUAAAAEc3NoOg== tazjin@tverskoy";
+  tverskoy_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1fGWz/gsq+ZeZXjvUrV+pBlanw1c3zJ9kLTax9FWQy tazjin@tverskoy";
+  unihertz_sk = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMgLwumRAp7u9sgaFR5aneNbQo07ZXXAFWV45pwzUiP2 unihertz-sk";
+  zamalek_sk = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOAw3OaPAjnC6hArGYEmBoXhPf7aZdRGlDZcSqm6gbB8AAAABHNzaDo= tazjin@zamalek";
+}
diff --git a/users/tazjin/nisp/transform.el b/users/tazjin/nisp/transform.el
new file mode 100644
index 0000000000..89b2bb104d
--- /dev/null
+++ b/users/tazjin/nisp/transform.el
@@ -0,0 +1,137 @@
+;; Nix as a Lisp
+
+(require 'cl-lib)
+(require 'json)
+(require 's)
+(require 'dash)
+
+(defun nisp/expr (form)
+  "Entrypoint for Nisp->Nix transformation. Will translate FORM
+into Nix code, if it is a valid Nisp expression.
+
+To make code generation slightly easier, each
+expression (including literals) is wrapped in an extra pair of
+parens."
+  (concat
+   "("
+   (pcase form
+     ;; Special keywords
+     ('() "null")
+     (`(let . ,rest) (nisp/let form))
+     (`(fn . ,rest) (nisp/fn form))
+     (`(if ,cond ,then ,else) (nisp/if cond then else))
+
+     ;; Nix operators & builtins that need special handling
+     (`(or  ,lhs ,rhs) (nisp/infix "||" lhs rhs))
+     (`(and ,lhs ,rhs) (nisp/infix "&&" lhs rhs))
+     (`(> ,lhs ,rhs) (nisp/infix ">" lhs rhs))
+     (`(< ,lhs ,rhs) (nisp/infix "<" lhs rhs))
+     (`(>= ,lhs ,rhs) (nisp/infix ">=" lhs rhs))
+     (`(<= ,lhs ,rhs) (nisp/infix "<=" lhs rhs))
+     (`(+ ,lhs ,rhs) (nisp/infix "+" lhs rhs))
+     (`(- ,lhs ,rhs) (nisp/infix "-" lhs rhs))
+     (`(* ,lhs ,rhs) (nisp/infix "*" lhs rhs))
+     (`(/ ,lhs ,rhs) (nisp/infix "/" lhs rhs))
+     (`(-> ,lhs ,rhs) (nisp/infix "->" lhs rhs))
+     (`(? ,lhs ,rhs) (nisp/infix "?" lhs rhs))
+     (`(// ,lhs ,rhs) (nisp/infix "//" lhs rhs))
+     (`(++ ,lhs ,rhs) (nisp/infix "++" lhs rhs))
+     (`(== ,lhs ,rhs) (nisp/infix "==" lhs rhs))
+     (`(!= ,lhs ,rhs) (nisp/infix "!=" lhs rhs))
+     (`(! ,term) (concat "!" (nisp/expr term)))
+     (`(- ,term) (concat "-" (nisp/expr term)))
+
+     ;; Attribute sets
+     (`(attrs . ,rest) (nisp/attribute-set form))
+
+     ;; Function calls
+     ((and `(,func . ,args)
+           (guard (symbolp func)))
+      (nisp/funcall func args))
+
+     ;; Primitives
+     ((pred stringp) (json-encode-string form))
+     ((pred numberp) (json-encode-number form))
+     ((pred keywordp) (substring (symbol-name form) 1))
+     ((pred symbolp) (symbol-name form))
+
+     ;; Lists
+     ((pred arrayp) (nisp/list form))
+
+     (other (error "Encountered unhandled form: %s" other)))
+   ")"))
+
+(defun nisp/infix (op lhs rhs)
+  (concat (nisp/expr lhs) " " op " " (nisp/expr rhs)))
+
+(defun nisp/funcall (func args)
+  (concat (symbol-name func) " " (s-join " " (-map #'nisp/expr args))))
+
+(defun nisp/let (form)
+  (pcase form
+    (`(let . (,bindings . (,body . ()))) (concat "let "
+                                                 (nisp/let bindings)
+                                                 (nisp/expr body)))
+    (`((:inherit . ,inherits) . ,rest) (concat (nisp/inherit (car form))
+                                               " "
+                                               (nisp/let rest)))
+    (`((,name . (,value . ())) .,rest) (concat (symbol-name name) " = "
+                                               (nisp/expr value) "; "
+                                               (nisp/let rest)))
+    ('() "in ")
+    (other (error "malformed form '%s' in let expression" other))))
+
+(defun nisp/inherit (form)
+  (pcase form
+    (`(:inherit . ,rest) (concat "inherit " (nisp/inherit rest)))
+    (`((,source) . ,rest) (concat "(" (symbol-name source) ") " (nisp/inherit rest)))
+    (`(,item . ,rest) (concat (symbol-name item) " " (nisp/inherit rest)))
+    ('() ";")))
+
+(defun nisp/if (cond then else)
+  (concat "if " (nisp/expr cond)
+          " then " (nisp/expr then)
+          " else " (nisp/expr else)))
+
+(defun nisp/list (form)
+  (cl-check-type form array)
+  (concat "[ "
+          (mapconcat #'nisp/expr form " ")
+          "]"))
+
+
+(defun nisp/attribute-set (form)
+  "Attribute sets have spooky special handling because they are
+not supported by the reader."
+  (pcase form
+    (`(attrs . ,rest) (concat "{ " (nisp/attribute-set rest)))
+    ((and `(,name . (,value . ,rest))
+          (guard (keywordp name)))
+     (concat (substring (symbol-name name) 1) " = "
+             (nisp/expr value) "; "
+             (nisp/attribute-set rest)))
+    ('() "}")))
+
+(defun nisp/fn (form)
+  (pcase form
+    (`(fn ,args ,body) (concat
+                              (cl-loop for arg in args
+                                       concat (format "%s: " arg))
+                              (nisp/expr body)))))
+
+;; The following functions are not part of the transform.
+
+(defun nisp/eval (form)
+  (interactive "sExpression: ")
+  (when (stringp form)
+    (setq form (read form)))
+
+  (message
+   ;; TODO(tazjin): Construct argv manually to avoid quoting issues.
+   (s-chomp
+    (shell-command-to-string
+     (concat "nix-instantiate --eval -E '" (nisp/expr form) "'")))))
+
+(defun nisp/eval-last-sexp ()
+  (interactive)
+  (nisp/eval (edebug-last-sexp)))
diff --git a/users/tazjin/nix.svg b/users/tazjin/nix.svg
new file mode 100644
index 0000000000..4da795a436
--- /dev/null
+++ b/users/tazjin/nix.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg"
+   width="141.5919mm"
+   height="122.80626mm"
+   viewBox="0 0 501.70361 435.14028"
+   id="svg2"
+   version="1.1">
+  <g transform="translate(-156.33871,933.1905)" visibility="hidden">
+    <path
+       id="lambda-path"
+       d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z"
+    />
+    <use
+       id="lambda-1"
+       href="#lambda-path"
+       visibility="visible"
+       transform="rotate(180,407.41868,-715.7565)"
+       fill="#f8f8ff" />
+    <use
+       id="lambda-2"
+       visibility="visible"
+       transform="rotate(-120,407.28823,-715.86995)"
+       href="#lambda-path"
+       fill="#0039a6" />
+    <use
+       id="lambda-3"
+       transform="rotate(-60,407.31177,-715.70016)"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#d52b1e" />
+    <use
+       id="lambda-4"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#f8f8ff" />
+    <use
+       id="lambda-5"
+       transform="rotate(60,407.11155,-715.78724)"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#0039a6" />
+    <use
+       transform="rotate(120,407.33916,-716.08356)"
+       id="lambda-6"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#d52b1e" />
+  </g>
+</svg>
diff --git a/users/tazjin/nixos/.gitignore b/users/tazjin/nixos/.gitignore
new file mode 100644
index 0000000000..212d3ad270
--- /dev/null
+++ b/users/tazjin/nixos/.gitignore
@@ -0,0 +1 @@
+local-config.nix
diff --git a/users/tazjin/nixos/README.md b/users/tazjin/nixos/README.md
new file mode 100644
index 0000000000..662f2a36ac
--- /dev/null
+++ b/users/tazjin/nixos/README.md
@@ -0,0 +1,17 @@
+NixOS configuration
+===================
+
+My NixOS configurations! It configures most of the packages I require
+on my systems, sets up Emacs the way I need and does a bunch of other
+interesting things.
+
+System configuration lives in folders, and some of the modules stem
+from `//ops/modules`.
+
+Machines are deployed with the script at `ops.nixos.rebuild-system`.
+
+## Configured hosts:
+
+* `tverskoy` - X13 AMD that's travelling around with me
+* `frog` - weapon of mass computation (in storage in London)
+* `camden` - NUC formerly serving tazj.in (in storage in London)
diff --git a/users/tazjin/nixos/camden/default.nix b/users/tazjin/nixos/camden/default.nix
new file mode 100644
index 0000000000..6568d6341e
--- /dev/null
+++ b/users/tazjin/nixos/camden/default.nix
@@ -0,0 +1,346 @@
+# This file configures camden.tazj.in, my homeserver.
+{ depot, pkgs, lib, ... }:
+
+config:
+let
+  nginxRedirect = { from, to, acmeHost }: {
+    serverName = from;
+    useACMEHost = acmeHost;
+    forceSSL = true;
+
+    extraConfig = "return 301 https://${to}$request_uri;";
+  };
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+in
+lib.fix (self: {
+  imports = [
+    (mod "quassel.nix")
+    (mod "smtprelay.nix")
+  ];
+
+  # camden is intended to boot unattended, despite having an encrypted
+  # root partition.
+  #
+  # The below configuration uses an externally connected USB drive
+  # that contains a LUKS key file to unlock the disk automatically at
+  # boot.
+  #
+  # TODO(tazjin): Configure LUKS unlocking via SSH instead.
+  boot = {
+    initrd = {
+      availableKernelModules = [
+        "ahci"
+        "xhci_pci"
+        "usbhid"
+        "usb_storage"
+        "sd_mod"
+        "sdhci_pci"
+        "rtsx_usb_sdmmc"
+        "r8169"
+      ];
+
+      kernelModules = [ "dm-snapshot" ];
+
+      luks.devices.camden-crypt = {
+        fallbackToPassword = true;
+        device = "/dev/disk/by-label/camden-crypt";
+        keyFile = "/dev/sdb";
+        keyFileSize = 4096;
+      };
+    };
+
+    loader = {
+      systemd-boot.enable = true;
+      efi.canTouchEfiVariables = true;
+    };
+
+    cleanTmpDir = true;
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "/dev/disk/by-label/camden-root";
+      fsType = "ext4";
+    };
+
+    "/home" = {
+      device = "/dev/disk/by-label/camden-home";
+      fsType = "ext4";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-label/BOOT";
+      fsType = "vfat";
+    };
+  };
+
+  nix = {
+    maxJobs = lib.mkDefault 4;
+
+    trustedUsers = [ "root" "tazjin" ];
+
+    binaryCaches = [
+      "https://tazjin.cachix.org"
+    ];
+
+    binaryCachePublicKeys = [
+      "tazjin.cachix.org-1:IZkgLeqfOr1kAZjypItHMg1NoBjm4zX9Zzep8oRSh7U="
+    ];
+  };
+
+  powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
+
+  networking = {
+    hostName = "camden";
+    interfaces.enp1s0.useDHCP = true;
+    interfaces.enp1s0.ipv6.addresses = [
+      {
+        address = "2a01:4b00:821a:ce02::5";
+        prefixLength = 64;
+      }
+    ];
+
+    firewall.enable = false;
+  };
+
+  time.timeZone = "UTC";
+
+  # System-wide application setup
+  programs.fish.enable = true;
+  programs.mosh.enable = true;
+
+  fonts = {
+    fonts = [ pkgs.jetbrains-mono ];
+    fontconfig.defaultFonts.monospace = [ "JetBrains Mono" ];
+  };
+
+  environment.systemPackages =
+    # programs from the depot
+    (with depot; [
+      fun.idual.script
+      fun.idual.setAlarm
+    ]) ++
+
+    # programs from nixpkgs
+    (with pkgs; [
+      bat
+      curl
+      direnv
+      emacs28-nox
+      fswebcam
+      git
+      gnupg
+      google-cloud-sdk
+      htop
+      jq
+      pass
+      pciutils
+      restic
+      ripgrep
+      screen
+    ]);
+
+  users = {
+    # Set up my own user for logging in and doing things ...
+    users.tazjin = {
+      isNormalUser = true;
+      uid = 1000;
+      extraGroups = [ "git" "wheel" "quassel" "video" ];
+      shell = pkgs.fish;
+    };
+
+    # Set up a user & group for general git shenanigans
+    groups.git = { };
+    users.git = {
+      group = "git";
+      isSystemUser = true;
+    };
+  };
+
+  # Services setup
+  services.openssh.enable = true;
+  services.haveged.enable = true;
+
+  # Join Tailscale into home network
+  services.tailscale.enable = true;
+
+  # Allow sudo-ing via the forwarded SSH agent.
+  security.pam.enableSSHAgentAuth = true;
+
+  # NixOS 20.03 broke nginx and I can't be bothered to debug it
+  # anymore, all solution attempts have failed, so here's a
+  # brute-force fix.
+  systemd.services.fix-nginx = {
+    script = "${pkgs.coreutils}/bin/chown -R nginx: /var/spool/nginx /var/cache/nginx";
+
+    serviceConfig = {
+      User = "root";
+      Type = "oneshot";
+    };
+  };
+
+  systemd.timers.fix-nginx = {
+    wantedBy = [ "multi-user.target" ];
+    timerConfig = {
+      OnCalendar = "minutely";
+    };
+  };
+
+  # Provision a TLS certificate outside of nginx to avoid
+  # nixpkgs#38144
+  security.acme = {
+    acceptTerms = true;
+
+    certs."tazj.in" = {
+      email = "mail@tazj.in";
+      group = "nginx";
+      webroot = "/var/lib/acme/acme-challenge";
+      extraDomains = {
+        "cs.tazj.in" = null;
+        "git.tazj.in" = null;
+        "www.tazj.in" = null;
+
+        # Local domains (for this machine only)
+        "camden.tazj.in" = null;
+      };
+      postRun = "systemctl reload nginx";
+    };
+
+    certs."quassel.tazj.in" = {
+      email = "mail@tazj.in";
+      webroot = "/var/lib/acme/challenge-quassel";
+      group = "quassel";
+    };
+  };
+
+  # Forward logs to Google Cloud Platform
+  services.journaldriver = {
+    enable = true;
+    logStream = "home";
+    googleCloudProject = "tazjins-infrastructure";
+    applicationCredentials = "/etc/gcp/key.json";
+  };
+
+  services.depot.quassel = {
+    enable = true;
+    acmeHost = "quassel.tazj.in";
+    bindAddresses = [
+      "0.0.0.0"
+    ];
+  };
+
+  services.bitlbee = {
+    enable = false;
+    portNumber = 2337; # bees
+  };
+
+  # serve my website(s)
+  services.nginx = {
+    enable = true;
+    enableReload = true;
+    package = with pkgs; nginx.override {
+      modules = [ nginxModules.rtmp ];
+    };
+
+    recommendedTlsSettings = true;
+    recommendedGzipSettings = true;
+    recommendedProxySettings = true;
+
+    appendConfig = ''
+      rtmp_auto_push on;
+      rtmp {
+        server {
+          listen 1935;
+          chunk_size 4000;
+
+          application tvl {
+            live on;
+
+            allow publish 88.98.195.213;
+            allow publish 10.0.1.0/24;
+            deny publish all;
+
+            allow play all;
+          }
+        }
+      }
+    '';
+
+    commonHttpConfig = ''
+      log_format json_combined escape=json
+      '{'
+          '"remote_addr":"$remote_addr",'
+          '"method":"$request_method",'
+          '"uri":"$request_uri",'
+          '"status":$status,'
+          '"request_size":$request_length,'
+          '"response_size":$body_bytes_sent,'
+          '"response_time":$request_time,'
+          '"referrer":"$http_referer",'
+          '"user_agent":"$http_user_agent"'
+      '}';
+
+      access_log syslog:server=unix:/dev/log,nohostname json_combined;
+    '';
+
+    virtualHosts.homepage = {
+      serverName = "tazj.in";
+      serverAliases = [ "camden.tazj.in" ];
+      default = true;
+      useACMEHost = "tazj.in";
+      root = depot.users.tazjin.homepage;
+      forceSSL = true;
+
+      extraConfig = ''
+        ${depot.users.tazjin.blog.oldRedirects}
+
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+        location ~* \.(webp|woff2)$ {
+          add_header Cache-Control "public, max-age=31536000";
+        }
+
+        location /blog/ {
+          alias ${depot.users.tazjin.blog.rendered}/;
+
+          if ($request_uri ~ ^/(.*)\.html$) {
+            return 302 /$1;
+          }
+
+          try_files $uri $uri.html $uri/ =404;
+        }
+
+        location = /tazjin {
+          return 200 "tazjin";
+        }
+
+        location /blobs/ {
+          alias /var/www/blobs/;
+        }
+      '';
+    };
+
+    virtualHosts.cgit-old = nginxRedirect {
+      from = "git.tazj.in";
+      to = "code.tvl.fyi";
+      acmeHost = "tazj.in";
+    };
+
+    virtualHosts.cs-old = nginxRedirect {
+      from = "cs.tazj.in";
+      to = "cs.tvl.fyi";
+      acmeHost = "tazj.in";
+    };
+  };
+
+  # Timer units that can be started with systemd-run to set my alarm.
+  systemd.user.services.light-alarm = {
+    script = "${depot.fun.idual.script}/bin/idualctl wakey";
+    postStart = "${pkgs.systemd}/bin/systemctl --user stop light-alarm.timer";
+    serviceConfig = {
+      Type = "oneshot";
+    };
+  };
+
+  system.stateVersion = "19.09";
+})
diff --git a/users/tazjin/nixos/default.nix b/users/tazjin/nixos/default.nix
new file mode 100644
index 0000000000..b9cae51d7f
--- /dev/null
+++ b/users/tazjin/nixos/default.nix
@@ -0,0 +1,10 @@
+{ depot, lib, ... }:
+
+let systemFor = sys: (depot.ops.nixos.nixosFor sys).system;
+in depot.nix.readTree.drvTargets {
+  camdenSystem = systemFor depot.users.tazjin.nixos.camden;
+  frogSystem = systemFor depot.users.tazjin.nixos.frog;
+  tverskoySystem = systemFor depot.users.tazjin.nixos.tverskoy;
+  polyankaSystem = (depot.ops.nixos.nixosFor depot.users.tazjin.nixos.polyanka).system;
+  zamalekSystem = systemFor depot.users.tazjin.nixos.zamalek;
+}
diff --git a/users/tazjin/nixos/frog/default.nix b/users/tazjin/nixos/frog/default.nix
new file mode 100644
index 0000000000..35d7f9c775
--- /dev/null
+++ b/users/tazjin/nixos/frog/default.nix
@@ -0,0 +1,287 @@
+{ depot, lib, pkgs, ... }:
+
+config:
+let
+  inherit (pkgs) lieer;
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+lib.fix (self: {
+  imports = [
+    (depot.path.origSrc + "/ops/modules/v4l2loopback.nix")
+  ];
+
+  boot = {
+    tmpOnTmpfs = true;
+    kernelModules = [ "kvm-amd" ];
+
+    loader = {
+      systemd-boot.enable = true;
+      efi.canTouchEfiVariables = true;
+    };
+
+    initrd = {
+      luks.devices.frog-crypt.device = "/dev/disk/by-label/frog-crypt";
+      availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "usbhid" "sd_mod" ];
+      kernelModules = [ "dm-snapshot" ];
+    };
+
+    kernelPackages = pkgs.linuxPackages_latest;
+    kernel.sysctl = {
+      "kernel.perf_event_paranoid" = -1;
+    };
+
+    # Enable this again if frog is put back into use ...
+    #
+    # kernelPatches = [
+    #   depot.third_party.kernelPatches.trx40_usb_audio
+    # ];
+  };
+
+  hardware = {
+    cpu.amd.updateMicrocode = true;
+    enableRedistributableFirmware = true;
+    opengl = {
+      enable = true;
+      driSupport = true;
+      driSupport32Bit = true;
+    };
+
+    pulseaudio = {
+      enable = true;
+      package = pkgs.pulseaudioFull;
+    };
+
+    bluetooth = {
+      enable = true;
+    };
+  };
+
+  nix = {
+    maxJobs = 48;
+    binaryCaches = [ "ssh://nix-ssh@whitby.tvl.fyi" ];
+    binaryCachePublicKeys = [ "cache.tvl.fyi:fd+9d1ceCPvDX/xVhcfv8nAa6njEhAGAEe+oGJDEeoc=" ];
+  };
+
+  networking = {
+    hostName = "frog";
+    useDHCP = true;
+
+    # Don't use ISP's DNS servers:
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+
+    firewall.enable = false;
+  };
+
+  # Generate an immutable /etc/resolv.conf from the nameserver settings
+  # above (otherwise DHCP overwrites it):
+  environment.etc."resolv.conf" = with lib; {
+    source = pkgs.writeText "resolv.conf" ''
+      ${concatStringsSep "\n" (map (ns: "nameserver ${ns}") self.networking.nameservers)}
+      options edns0
+    '';
+  };
+
+  time.timeZone = "Europe/London";
+
+  fileSystems = {
+    "/".device = "/dev/disk/by-label/frog-root";
+    "/boot".device = "/dev/disk/by-label/BOOT";
+    "/home".device = "/dev/disk/by-label/frog-home";
+  };
+
+  # Configure user account
+  users.extraUsers.tazjin = {
+    extraGroups = [ "wheel" "audio" "docker" ];
+    isNormalUser = true;
+    uid = 1000;
+    shell = pkgs.fish;
+  };
+
+  security.sudo = {
+    enable = true;
+    extraConfig = "wheel ALL=(ALL:ALL) SETENV: ALL";
+  };
+
+  fonts = {
+    fonts = with pkgs; [
+      corefonts
+      dejavu_fonts
+      jetbrains-mono
+      noto-fonts-cjk
+      noto-fonts-emoji
+    ];
+
+    fontconfig = {
+      hinting.enable = true;
+      subpixel.lcdfilter = "light";
+
+      defaultFonts = {
+        monospace = [ "JetBrains Mono" ];
+      };
+    };
+  };
+
+  # Configure location (Vauxhall, London) for services that need it.
+  location = {
+    latitude = 51.4819109;
+    longitude = -0.1252998;
+  };
+
+  programs.fish.enable = true;
+  programs.ssh.startAgent = true;
+
+  services.redshift.enable = true;
+  services.openssh.enable = true;
+  services.fstrim.enable = true;
+  services.blueman.enable = true;
+
+  # Required for Yubikey usage as smartcard
+  services.pcscd.enable = true;
+  services.udev.packages = [
+    pkgs.yubikey-personalization
+  ];
+
+  # Enable Docker for Nixery testing
+  virtualisation.docker = {
+    enable = true;
+    autoPrune.enable = true;
+  };
+
+  services.xserver = {
+    enable = true;
+    layout = "us";
+    xkbOptions = "caps:super";
+    exportConfiguration = true;
+    videoDrivers = [ "amdgpu" ];
+    displayManager = {
+      # Give EXWM permission to control the session.
+      sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER";
+
+      lightdm.enable = true;
+      lightdm.greeters.gtk.clock-format = "%H·%M"; # TODO(tazjin): TZ?
+    };
+
+    windowManager.session = lib.singleton {
+      name = "exwm";
+      start = "${depot.users.tazjin.emacs}/bin/tazjins-emacs";
+    };
+  };
+
+  # Do not restart the display manager automatically
+  systemd.services.display-manager.restartIfChanged = lib.mkForce false;
+
+  # clangd needs more than ~2GB in the runtime directory to start up
+  services.logind.extraConfig = ''
+    RuntimeDirectorySize=16G
+  '';
+
+  # Configure email setup
+  systemd.user.services.lieer-tazjin = {
+    description = "Synchronise mail@tazj.in via lieer";
+    script = "${lieer}/bin/gmi sync";
+
+    serviceConfig = {
+      WorkingDirectory = "%h/mail/account.tazjin";
+      Type = "oneshot";
+    };
+  };
+
+  systemd.user.timers.lieer-tazjin = {
+    wantedBy = [ "timers.target" ];
+
+    timerConfig = {
+      OnActiveSec = "1";
+      OnUnitActiveSec = "180";
+    };
+  };
+
+  environment.systemPackages =
+    # programs from the depot
+    (with depot; [
+      fun.idual.script
+      fun.uggc
+      lieer
+      ops.kontemplate
+      quasselClient
+      third_party.git
+      tools.nsfv-setup
+      users.tazjin.emacs
+    ]) ++
+
+    # programs from nixpkgs
+    (with pkgs; [
+      age
+      bat
+      chromium
+      clang-manpages
+      clang-tools_11
+      clang_11
+      curl
+      direnv
+      dnsutils
+      emacs28 # mostly for emacsclient
+      exa
+      fd
+      file
+      gdb
+      gnupg
+      go
+      google-chrome
+      google-cloud-sdk
+      htop
+      hyperfine
+      i3lock
+      iftop
+      imagemagick
+      jq
+      kubectl
+      linuxPackages.perf
+      man-pages
+      miller
+      msmtp
+      nix-prefetch-github
+      notmuch
+      obs-studio
+      openssh
+      openssl
+      pass
+      pavucontrol
+      pciutils
+      pinentry
+      pinentry-emacs
+      pmutils
+      pwgen
+      ripgrep
+      rustup
+      screen
+      scrot
+      spotify
+      tokei
+      transmission
+      tree
+      unzip
+      usbutils
+      v4l-utils
+      vlc
+      xclip
+      xsecurelock
+      yubico-piv-tool
+      yubikey-personalization
+      zoxide
+
+      # Commented out because of interim breakage:
+      # steam
+      # lutris
+    ]);
+
+  # ... and other nonsense.
+  system.stateVersion = "20.03";
+})
diff --git a/users/tazjin/nixos/modules/default.nix b/users/tazjin/nixos/modules/default.nix
new file mode 100644
index 0000000000..d747e8e131
--- /dev/null
+++ b/users/tazjin/nixos/modules/default.nix
@@ -0,0 +1,2 @@
+# Make readTree happy at this level.
+_: { }
diff --git a/users/tazjin/nixos/modules/desktop.nix b/users/tazjin/nixos/modules/desktop.nix
new file mode 100644
index 0000000000..c78463386c
--- /dev/null
+++ b/users/tazjin/nixos/modules/desktop.nix
@@ -0,0 +1,53 @@
+# EXWM and other desktop configuration.
+{ depot, lib, pkgs, ... }:
+
+{
+  services = {
+    pipewire = {
+      enable = true;
+      alsa.enable = true;
+      pulse.enable = true;
+    };
+
+    redshift.enable = true;
+    blueman.enable = true;
+
+    xserver = {
+      enable = true;
+      layout = "us";
+      xkbOptions = "caps:super";
+
+      libinput.enable = true;
+
+      displayManager = {
+        # Give EXWM permission to control the session.
+        sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER";
+        lightdm.enable = true;
+        # lightdm.greeters.gtk.clock-format = "%H:%M"; # TODO(tazjin): TZ?
+      };
+
+      windowManager.session = lib.singleton {
+        name = "exwm";
+        start = "${depot.users.tazjin.emacs}/bin/tazjins-emacs";
+      };
+    };
+  };
+
+  # Set variables to enable EXWM-XIM and other Emacs features.
+  environment.sessionVariables = {
+    XMODIFIERS = "@im=exwm-xim";
+    GTK_IM_MODULE = "xim";
+    QT_IM_MODULE = "xim";
+    CLUTTER_IM_MODULE = "xim";
+    EDITOR = "emacsclient";
+  };
+
+  # Do not restart the display manager automatically
+  systemd.services.display-manager.restartIfChanged = lib.mkForce false;
+
+  # If something needs more than 10s to stop it should probably be
+  # killed.
+  systemd.extraConfig = ''
+    DefaultTimeoutStopSec=10s
+  '';
+}
diff --git a/users/tazjin/nixos/modules/fonts.nix b/users/tazjin/nixos/modules/fonts.nix
new file mode 100644
index 0000000000..3b4461056f
--- /dev/null
+++ b/users/tazjin/nixos/modules/fonts.nix
@@ -0,0 +1,24 @@
+# Attempt at configuring reasonable font-rendering.
+
+{ pkgs, ... }:
+
+{
+  fonts = {
+    fonts = with pkgs; [
+      corefonts
+      dejavu_fonts
+      jetbrains-mono
+      noto-fonts-cjk
+      noto-fonts-emoji
+    ];
+
+    fontconfig = {
+      hinting.enable = true;
+      subpixel.lcdfilter = "light";
+
+      defaultFonts = {
+        monospace = [ "JetBrains Mono" ];
+      };
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/hidpi.nix b/users/tazjin/nixos/modules/hidpi.nix
new file mode 100644
index 0000000000..7fa3e41933
--- /dev/null
+++ b/users/tazjin/nixos/modules/hidpi.nix
@@ -0,0 +1,17 @@
+# Configuration for machines with HiDPI displays, which are a total
+# mess, of course.
+{ ... }:
+
+{
+  # Expose a variable to all programs that might be interested in the
+  # screen settings to do conditional initialisation (mostly for Emacs).
+  environment.variables.HIDPI_SCREEN = "true";
+
+  # Ensure a larger font size in early boot stage.
+  hardware.video.hidpi.enable = true;
+
+  # Bump DPI across the board.
+  # TODO(tazjin): This should actually be set per monitor, but I
+  # haven't yet figured out the right interface for doing that.
+  services.xserver.dpi = 161;
+}
diff --git a/users/tazjin/nixos/modules/home-config.nix b/users/tazjin/nixos/modules/home-config.nix
new file mode 100644
index 0000000000..2445afbb52
--- /dev/null
+++ b/users/tazjin/nixos/modules/home-config.nix
@@ -0,0 +1,21 @@
+# Inject the right home-manager config for the machine.
+
+{ config, depot, pkgs, ... }:
+
+{
+  users.users.tazjin = {
+    isNormalUser = true;
+    createHome = true;
+    extraGroups = [ "wheel" "networkmanager" "video" "adbusers" ];
+    uid = 1000;
+    shell = pkgs.fish;
+    initialHashedPassword = "$6$d3FywUNCuZnJ4l.$ZW2ul59MLYon1v1xhC3lTJZfZ91lWW6Tpi13MpME0cJcYZNrsx7ABdgQRn.K05awruG2Y9ARAzURnmiJ31WTS1h";
+  };
+
+  nix = {
+    trustedUsers = [ "tazjin" ];
+  };
+
+  home-manager.useGlobalPkgs = true;
+  home-manager.users.tazjin = depot.users.tazjin.home."${config.networking.hostName}";
+}
diff --git a/users/tazjin/nixos/modules/laptop.nix b/users/tazjin/nixos/modules/laptop.nix
new file mode 100644
index 0000000000..da277dd3d6
--- /dev/null
+++ b/users/tazjin/nixos/modules/laptop.nix
@@ -0,0 +1,14 @@
+# Configuration specifically for laptops that move around.
+{ ... }:
+
+{
+  # Automatically detect location for redshift & timezone settings.
+  services.geoclue2.enable = true;
+  location.provider = "geoclue2";
+  services.localtime.enable = true;
+
+  # Enable power-saving features.
+  services.tlp.enable = true;
+
+  programs.light.enable = true;
+}
diff --git a/users/tazjin/nixos/modules/persistence.nix b/users/tazjin/nixos/modules/persistence.nix
new file mode 100644
index 0000000000..c81958161f
--- /dev/null
+++ b/users/tazjin/nixos/modules/persistence.nix
@@ -0,0 +1,26 @@
+# Configuration for persistent (non-home) data.
+{ depot, pkgs, lib, ... }:
+
+{
+  imports = [
+    "${depot.third_party.impermanence}/nixos.nix"
+  ];
+
+  environment.persistence."/persist" = {
+    directories = [
+      "/etc/NetworkManager/system-connections"
+      "/etc/mullvad-vpn"
+      "/var/cache/mullvad-vpn"
+      "/var/lib/bluetooth"
+      "/var/lib/systemd/coredump"
+      "/var/lib/tailscale"
+      "/var/log"
+    ];
+
+    files = [
+      "/etc/machine-id"
+    ];
+  };
+
+  programs.fuse.userAllowOther = true;
+}
diff --git a/users/tazjin/nixos/modules/physical.nix b/users/tazjin/nixos/modules/physical.nix
new file mode 100644
index 0000000000..8b11e1bf08
--- /dev/null
+++ b/users/tazjin/nixos/modules/physical.nix
@@ -0,0 +1,90 @@
+# Default configuration settings for physical machines that I use.
+{ pkgs, depot, ... }:
+
+let
+  pass-otp = pkgs.pass.withExtensions (e: [ e.pass-otp ]);
+in
+{
+  # Install all the default software.
+  environment.systemPackages =
+    # programs from the depot
+    (with depot; [
+      users.tazjin.screenLock
+      users.tazjin.emacs
+      third_party.agenix.cli
+    ]) ++
+
+    # programs from nixpkgs
+    (with pkgs; [
+      amber
+      audacity
+      bat
+      curl
+      ddcutil
+      direnv
+      # dmd # TODO(tazjin): temporarily broken in nixpkgs, reinstall when it works again
+      dnsutils
+      electrum
+      emacsNativeComp # emacsclient
+      exa
+      fd
+      file
+      firefox
+      fractal
+      gdb
+      gh
+      git
+      gnupg
+      google-chrome
+      gtk3 # for gtk-launch
+      htop
+      hyperfine
+      iftop
+      imagemagick
+      jq
+      lieer
+      man-pages
+      mosh
+      msmtp
+      mullvad-vpn
+      networkmanagerapplet
+      nix-prefetch-github
+      nmap
+      notmuch
+      openssh
+      openssl
+      paperlike-go
+      pass-otp
+      pavucontrol
+      pinentry
+      pinentry-emacs
+      pulseaudio # for pactl
+      pwgen
+      quasselClient
+      rink
+      ripgrep
+      rustup
+      screen
+      scrot
+      tig
+      tokei
+      tree
+      unzip
+      vlc
+      whois
+      xsecurelock
+      zoxide
+    ]);
+
+  # Run services & configure programs for all machines.
+  services = {
+    mullvad-vpn.enable = true;
+    fwupd.enable = true;
+  };
+
+  programs = {
+    fish.enable = true;
+    mosh.enable = true;
+    ssh.startAgent = true;
+  };
+}
diff --git a/users/tazjin/nixos/modules/tgsa.nix b/users/tazjin/nixos/modules/tgsa.nix
new file mode 100644
index 0000000000..ac6d940c2a
--- /dev/null
+++ b/users/tazjin/nixos/modules/tgsa.nix
@@ -0,0 +1,24 @@
+{ config, depot, lib, pkgs, ... }:
+
+{
+  systemd.services.tgsa = {
+    description = "telegram -> SA bbcode thing";
+    wantedBy = [ "multi-user.target" ];
+
+    serviceConfig = {
+      DynamicUser = true;
+      Restart = "always";
+      ExecStart = "${depot.users.tazjin.tgsa}/bin/tgsa";
+    };
+  };
+
+  services.nginx.virtualHosts."tgsa" = {
+    serverName = "tgsa.tazj.in";
+    enableACME = true;
+    forceSSL = true;
+
+    locations."/" = {
+      proxyPass = "http://127.0.0.1:8472";
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/zerotier.nix b/users/tazjin/nixos/modules/zerotier.nix
new file mode 100644
index 0000000000..bd503cf8f0
--- /dev/null
+++ b/users/tazjin/nixos/modules/zerotier.nix
@@ -0,0 +1,14 @@
+# Configuration for my Zerotier network.
+
+{
+  environment.persistence."/persist".directories = [
+    "/var/lib/zerotier-one"
+  ];
+
+  services.zerotierone.enable = true;
+  services.zerotierone.joinNetworks = [
+    "35c192ce9bd4c8c7"
+  ];
+
+  networking.firewall.trustedInterfaces = [ "zt7nnembs4" ];
+}
diff --git a/users/tazjin/nixos/polyanka/default.nix b/users/tazjin/nixos/polyanka/default.nix
new file mode 100644
index 0000000000..fc30b82373
--- /dev/null
+++ b/users/tazjin/nixos/polyanka/default.nix
@@ -0,0 +1,122 @@
+# VPS hosted at GleSYS, running my Quassel and some random network
+# stuff.
+
+_: # ignore readTree options
+
+{ config, depot, lib, pkgs, ... }:
+
+let
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+in
+{
+  imports = [
+    (mod "quassel.nix")
+    (mod "www/base.nix")
+    (usermod "tgsa.nix")
+  ];
+
+  # Use the GRUB 2 boot loader.
+  boot.loader.grub.enable = true;
+  boot.loader.grub.version = 2;
+  boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
+  boot.initrd.availableKernelModules = [ "ata_piix" "vmw_pvscsi" "sd_mod" "sr_mod" ];
+
+  # Adjust to disk size increases
+  boot.growPartition = true;
+
+  virtualisation.vmware.guest.enable = true;
+  virtualisation.vmware.guest.headless = true;
+
+  nix.settings.trusted-users = [ "tazjin" ];
+
+  fileSystems."/" =
+    {
+      device = "/dev/disk/by-uuid/4c51357a-1e34-4b59-b169-63af1fcdce71";
+      fsType = "ext4";
+    };
+
+  networking = {
+    hostName = "polyanka";
+    domain = "tazj.in";
+    useDHCP = false;
+
+    # Required for VPN usage
+    networkmanager.enable = true;
+
+    interfaces.ens192 = {
+      ipv4.addresses = lib.singleton {
+        address = "159.253.30.129";
+        prefixLength = 24;
+      };
+
+      ipv6.addresses = lib.singleton {
+        address = "2a02:750:7:3305::308";
+        prefixLength = 64;
+      };
+    };
+
+    defaultGateway = "159.253.30.1";
+    defaultGateway6.address = "2a02:750:7:3305::1";
+
+    firewall.enable = true;
+    firewall.allowedTCPPorts = [ 22 80 443 ];
+
+    nameservers = [
+      "79.99.4.100"
+      "79.99.4.101"
+      "2a02:751:aaaa::1"
+      "2a02:751:aaaa::2"
+    ];
+  };
+
+  time.timeZone = "UTC";
+
+  security.acme.acceptTerms = true;
+  security.acme.certs."polyanka.tazj.in" = {
+    listenHTTP = ":80";
+    email = "mail@tazj.in";
+    group = "quassel";
+  };
+
+  users.users.tazjin = {
+    isNormalUser = true;
+    extraGroups = [ "wheel" ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = depot.users.tazjin.keys.all;
+  };
+
+  security.sudo.wheelNeedsPassword = false;
+
+  services.depot.quassel = {
+    enable = true;
+    acmeHost = "polyanka.tazj.in";
+    bindAddresses = [
+      "0.0.0.0"
+    ];
+  };
+
+  # List packages installed in system profile. To search, run:
+  # $ nix search wget
+  environment.systemPackages = with pkgs; [
+    curl
+    htop
+    jq
+    nmap
+    bat
+    emacs-nox
+    nano
+    wget
+  ];
+
+  programs.mtr.enable = true;
+  programs.mosh.enable = true;
+  services.openssh.enable = true;
+
+  services.zerotierone.enable = true;
+  services.zerotierone.joinNetworks = [
+    "35c192ce9bd4c8c7"
+  ];
+
+  system.stateVersion = "20.09";
+}
diff --git a/users/tazjin/nixos/tverskoy/default.nix b/users/tazjin/nixos/tverskoy/default.nix
new file mode 100644
index 0000000000..4a94e58c5f
--- /dev/null
+++ b/users/tazjin/nixos/tverskoy/default.nix
@@ -0,0 +1,163 @@
+# tverskoy is my Thinkpad X13 AMD 1st gen
+{ depot, lib, pkgs, ... }:
+
+config:
+let
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+in
+lib.fix (self: {
+  imports = [
+    (mod "open_eid.nix")
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "persistence.nix")
+    (usermod "physical.nix")
+    (usermod "zerotier.nix")
+
+    (pkgs.home-manager.src + "/nixos")
+  ] ++ lib.optional (builtins.pathExists ./local-config.nix) ./local-config.nix;
+
+  tvl.cache.enable = true;
+
+  boot = rec {
+    initrd.availableKernelModules = [ "nvme" "ehci_pci" "xhci_pci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
+    initrd.kernelModules = [ ];
+
+    # Restore /home to the blank snapshot, erasing all ephemeral data.
+    initrd.postDeviceCommands = lib.mkAfter ''
+      zfs rollback -r zpool/ephemeral/home@tazjin-clean
+    '';
+
+    # Install thinkpad modules for TLP
+    extraModulePackages = [ kernelPackages.acpi_call ];
+
+    kernelModules = [ "kvm-amd" "i2c_dev" ];
+    kernelPackages = pkgs.linuxPackages_latest;
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+    zfs.enableUnstable = true;
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "tmpfs";
+      fsType = "tmpfs";
+      options = [ "defaults" "size=8G" "mode=755" ];
+    };
+
+    "/home" = {
+      device = "zpool/ephemeral/home";
+      fsType = "zfs";
+    };
+
+    "/nix" = {
+      device = "zpool/local/nix";
+      fsType = "zfs";
+    };
+
+    "/depot" = {
+      device = "zpool/safe/depot";
+      fsType = "zfs";
+    };
+
+    "/persist" = {
+      device = "zpool/safe/persist";
+      fsType = "zfs";
+      neededForBoot = true;
+    };
+
+    # SD card
+    "/mnt" = {
+      device = "/dev/disk/by-uuid/c602d703-f1b9-4a44-9e45-94dfe24bdaa8";
+      fsType = "ext4";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/BF4F-388B";
+      fsType = "vfat";
+    };
+  };
+
+  hardware = {
+    cpu.amd.updateMicrocode = true;
+    enableRedistributableFirmware = true;
+    bluetooth.enable = true;
+
+    opengl = {
+      enable = true;
+      extraPackages = with pkgs; [
+        vaapiVdpau
+        libvdpau-va-gl
+      ];
+    };
+  };
+
+  networking = {
+    hostName = "tverskoy";
+    hostId = "3c91827f";
+    domain = "tvl.su";
+    useDHCP = false;
+    networkmanager.enable = true;
+    firewall.enable = false;
+
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+  };
+
+  security.rtkit.enable = true;
+
+  services = {
+    printing.enable = true;
+
+    # expose i2c device as /dev/i2c-amdgpu-dm and make it user-accessible
+    # this is required for sending control commands to the Dasung screen.
+    udev.extraRules = ''
+      SUBSYSTEM=="i2c-dev", ACTION=="add", DEVPATH=="/devices/pci0000:00/0000:00:08.1/0000:06:00.0/i2c-5/i2c-dev/i2c-5", SYMLINK+="i2c-amdgpu-dm", TAG+="uaccess"
+    '';
+
+    xserver.videoDrivers = [ "amdgpu" ];
+
+    # Automatically collect garbage from the Nix store.
+    depot.automatic-gc = {
+      enable = true;
+      interval = "1 hour";
+      diskThreshold = 16; # GiB
+      maxFreed = 10; # GiB
+      preserveGenerations = "14d";
+    };
+  };
+
+  systemd.user.services.lieer-tazjin = {
+    description = "Synchronise mail@tazj.in via lieer";
+    script = "${pkgs.lieer}/bin/gmi sync";
+
+    serviceConfig = {
+      WorkingDirectory = "%h/mail/account.tazjin";
+      Type = "oneshot";
+    };
+  };
+
+  systemd.user.timers.lieer-tazjin = {
+    wantedBy = [ "timers.target" ];
+
+    timerConfig = {
+      OnActiveSec = "1";
+      OnUnitActiveSec = "180";
+    };
+  };
+
+  services.tailscale.enable = true;
+
+  system.stateVersion = "20.09";
+})
diff --git a/users/tazjin/nixos/zamalek/default.nix b/users/tazjin/nixos/zamalek/default.nix
new file mode 100644
index 0000000000..be5b204277
--- /dev/null
+++ b/users/tazjin/nixos/zamalek/default.nix
@@ -0,0 +1,82 @@
+# zamalek is my Huawei MateBook X (unknown year)
+{ depot, lib, pkgs, ... }:
+
+config:
+let
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+
+  zdevice = device: {
+    inherit device;
+    fsType = "zfs";
+  };
+in
+{
+  imports = [
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "hidpi.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "persistence.nix")
+    (usermod "physical.nix")
+    (usermod "zerotier.nix")
+
+    (depot.third_party.impermanence + "/nixos.nix")
+    (pkgs.home-manager.src + "/nixos")
+  ] ++ lib.optional (builtins.pathExists ./local-config.nix) ./local-config.nix;
+
+  tvl.cache.enable = true;
+
+  boot = {
+    initrd.availableKernelModules = [ "nvme" "xhci_pci" ];
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+    supportedFilesystems = [ "zfs" ];
+    zfs.devNodes = "/dev/";
+
+    extraModprobeConfig = ''
+      options snd_hda_intel power_save=1
+      options iwlwifi power_save=1
+      options iwldvm force_cam=0
+      options i915 enable_guc=3 enable_fbc=1
+    '';
+  };
+
+  fileSystems = {
+    "/" = zdevice "zpool/ephemeral/root";
+    "/home" = zdevice "zpool/ephemeral/home";
+    "/persist" = zdevice "zpool/persistent/data" // { neededForBoot = true; };
+    "/nix" = zdevice "zpool/persistent/nix";
+    "/depot" = zdevice "zpool/persistent/depot";
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/2487-3908";
+      fsType = "vfat";
+    };
+  };
+
+  networking = {
+    hostName = "zamalek";
+    domain = "tvl.su";
+    hostId = "ee399356";
+    networkmanager.enable = true;
+
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+  };
+
+  hardware = {
+    cpu.intel.updateMicrocode = true;
+    bluetooth.enable = true;
+    enableRedistributableFirmware = true;
+    opengl.enable = true;
+  };
+
+  services.xserver.libinput.touchpad.clickMethod = "clickfinger";
+  services.tailscale.enable = true;
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/tazjin/presentations/bootstrapping-2018/README.md b/users/tazjin/presentations/bootstrapping-2018/README.md
new file mode 100644
index 0000000000..e9573ae3f2
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/README.md
@@ -0,0 +1,5 @@
+These are the slides for a talk I gave at the Norwegian Unix User Group on
+2018-03-13.
+
+There is more information and a recording on the [event
+page](https://www.nuug.no/aktiviteter/20180313-reproduible-compiler/).
diff --git a/users/tazjin/presentations/bootstrapping-2018/default.nix b/users/tazjin/presentations/bootstrapping-2018/default.nix
new file mode 100644
index 0000000000..2775d0b3fb
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/default.nix
@@ -0,0 +1,52 @@
+# This derivation builds the LaTeX presentation.
+
+{ pkgs, ... }:
+
+with pkgs;
+
+let
+  tex = texlive.combine {
+    inherit (texlive)
+      beamer
+      beamertheme-metropolis
+      etoolbox
+      euenc
+      extsizes
+      fontspec
+      lualibs
+      luaotfload
+      luatex
+      minted
+      ms
+      pgfopts
+      scheme-basic
+      translator;
+  };
+in
+stdenv.mkDerivation {
+  name = "nuug-bootstrapping-slides";
+  src = ./.;
+
+  FONTCONFIG_FILE = makeFontsConf {
+    fontDirectories = [ fira fira-code fira-mono ];
+  };
+
+  buildInputs = [ tex fira fira-code fira-mono ];
+  buildPhase = ''
+    # LaTeX needs a cache folder in /home/ ...
+    mkdir home
+    export HOME=$PWD/home
+    # ${tex}/bin/luaotfload-tool -ufv
+
+    # As usual, TeX needs to be run twice ...
+    function run() {
+      ${tex}/bin/lualatex presentation.tex
+    }
+    run && run
+  '';
+
+  installPhase = ''
+    mkdir -p $out
+    cp presentation.pdf $out/
+  '';
+}
diff --git a/users/tazjin/presentations/bootstrapping-2018/drake-meme.png b/users/tazjin/presentations/bootstrapping-2018/drake-meme.png
new file mode 100644
index 0000000000..4b03675438
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/drake-meme.png
Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/nixos-logo.png b/users/tazjin/presentations/bootstrapping-2018/nixos-logo.png
new file mode 100644
index 0000000000..ce0c98c2ca
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/nixos-logo.png
Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/notes.org b/users/tazjin/presentations/bootstrapping-2018/notes.org
new file mode 100644
index 0000000000..363d75352e
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/notes.org
@@ -0,0 +1,89 @@
+#+TITLE: Bootstrapping, reproducibility, etc.
+#+AUTHOR: Vincent Ambo
+#+DATE: <2018-03-10 Sat>
+
+* Compiler bootstrapping
+  This section contains notes about compiler bootstrapping, the
+  history thereof, which compilers need it - and so on:
+
+** C
+
+** Haskell
+   - self-hosted compiler (GHC)
+
+** Common Lisp
+   CL is fairly interesting in this space because it is a language
+   that is defined via an ANSI standard that compiler implementations
+   normally actually follow!
+
+   CL has several ecosystem components that focus on making
+   abstracting away implementation-specific calls and if a self-hosted
+   compiler is written in CL using those components it can be
+   cross-bootstrapped.
+
+** Python
+
+* A note on runtimes
+  Sometimes the compiler just isn't enough ...
+
+** LLVM
+** JVM
+
+* References
+  https://github.com/mame/quine-relay
+  https://manishearth.github.io/blog/2016/12/02/reflections-on-rusting-trust/
+  https://tests.reproducible-builds.org/debian/reproducible.html
+
+* Slide thoughts:
+  1. Hardware trust has been discussed here a bunch, most recently
+     during the puri.sm talk. Hardware trust is important, as we see
+     with IME, but it's striking that people often take a leap to "I'm
+     now on my trusted Debian with free software".
+
+     Unless you built it yourself from scratch (Spoiler: you haven't)
+     you're placing trust in what is basically foreign binary blobs.
+
+     Agenda: Implications/attack vectors of this, state of the chicken
+     & egg, the topic of reproducibility, what can you do? (Nix!)
+
+  2. Chicken-and-egg issue
+
+     It's an important milestone for a language to become self-hosted:
+     You begin doing a kind of dogfeeding, you begin to enforce
+     reliability & consistency guarantees to avoid having to redo your
+     own codebase constantly and so on.
+
+     However, the implication is now that you need your own compiler
+     to compile itself.
+
+     Common examples:
+     - C/C++ compilers needed to build C/C++ compilers:
+
+       GCC 4.7 was the last version of GCC that could be built with a
+       standard C-compiler, nowadays it is mostly written in C++.
+
+       Certain versions of GCC can be built with LLVM/Clang.
+
+       Clang/LLVM can be compiled by itself and also GCC.
+
+     - Rust was originally written in OCAML but moved to being
+       self-hosted in 2011. Currently rustc-releases are always built
+       with a copy of the previous release.
+
+       It's relatively new so we can build the chain all the way.
+
+     Notable exceptions: Some popular languages are not self-hosted,
+     for example Clojure. Languages also have runtimes, which may be
+     written in something else (e.g. Haskell -> C runtime)
+* How to help:
+  Most of this advice is about reproducible builds, not bootstrapping,
+  as that is a much harder project.
+
+  - fix reproducibility issues listed in Debian's issue tracker (focus
+    on non-Debian specific ones though)
+  - experiment with NixOS / GuixSD to get a better grasp on the
+    problem space of reproducibility
+
+  If you want to contribute to bootstrapping, look at
+  bootstrappable.org and their wiki. Several initiatives such as MES
+  could need help!
diff --git a/users/tazjin/presentations/bootstrapping-2018/presentation.pdf b/users/tazjin/presentations/bootstrapping-2018/presentation.pdf
new file mode 100644
index 0000000000..7f435fe5b5
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/presentation.pdf
Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/presentation.tex b/users/tazjin/presentations/bootstrapping-2018/presentation.tex
new file mode 100644
index 0000000000..d3aa613375
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/presentation.tex
@@ -0,0 +1,251 @@
+\documentclass[12pt]{beamer}
+\usetheme{metropolis}
+\newenvironment{code}{\ttfamily}{\par}
+\title{Where does \textit{your} compiler come from?}
+\date{2018-03-13}
+\author{Vincent Ambo}
+\institute{Norwegian Unix User Group}
+\begin{document}
+  \maketitle
+
+  %% Slide 1:
+  \section{Introduction}
+
+  %% Slide 2:
+  \begin{frame}{Chicken and egg}
+    Self-hosted compilers are often built using themselves, for example:
+
+    \begin{itemize}
+    \item C-family compilers bootstrap themselves \& each other
+    \item (Some!) Common Lisp compilers can bootstrap each other
+    \item \texttt{rustc} bootstraps itself with a previous version
+    \item ... same for many other languages!
+    \end{itemize}
+  \end{frame}
+
+  \begin{frame}{Chicken, egg and ... lizard?}
+    It's not just compilers: Languages have runtimes, too.
+
+    \begin{itemize}
+    \item JVM is implemented in C++
+    \item Erlang-VM is C
+    \item Haskell runtime is C
+    \end{itemize}
+
+    ... we can't ever get away from C, can we?
+  \end{frame}
+
+  %% Slide 3:
+  \begin{frame}{Trusting Trust}
+    \begin{center}
+      \huge{Could this be exploited?}
+    \end{center}
+  \end{frame}
+
+  %% Slide 4:
+  \begin{frame}{Short interlude: A quine}
+    \begin{center}
+      \begin{code}
+        ((lambda (x) (list x (list 'quote x)))
+        \newline\vspace*{6mm} '(lambda (x) (list x (list 'quote x))))
+      \end{code}
+    \end{center}
+  \end{frame}
+
+  %% Slide 5:
+  \begin{frame}{Short interlude: Quine Relay}
+    \begin{center}
+      \includegraphics[
+        keepaspectratio=true,
+        height=\textheight
+      ]{quine-relay.png}
+    \end{center}
+  \end{frame}
+
+  %% Slide 6:
+  \begin{frame}{Trusting Trust}
+    An attack described by Ken Thompson in 1983:
+
+    \begin{enumerate}
+    \item Modify a compiler to detect when it's compiling itself.
+    \item Let the modification insert \textit{itself} into the new compiler.
+    \item Add arbitrary attack code to the modification.
+    \item \textit{Optional!} Remove the attack from the source after compilation.
+    \end{enumerate}
+  \end{frame}
+
+  %% Slide 7:
+  \begin{frame}{Damage potential?}
+    \begin{center}
+      \large{Let your imagination run wild!}
+    \end{center}
+  \end{frame}
+
+  %% Slide 8:
+  \section{Countermeasures}
+
+  %% Slide 9:
+  \begin{frame}{Diverse Double-Compiling}
+    Assume we have:
+
+    \begin{itemize}
+    \item Target language compilers $A$ and $T$
+    \item The source code of $A$: $ S_{A} $
+    \end{itemize}
+  \end{frame}
+
+  %% Slide 10:
+  \begin{frame}{Diverse Double-Compiling}
+    Apply the first stage (functional equivalence):
+
+    \begin{itemize}
+    \item $ X = A(S_{A})$
+    \item $ Y = T(S_{A})$
+    \end{itemize}
+
+    Apply the second stage (bit-for-bit equivalence):
+
+    \begin{itemize}
+    \item $ V = X(S_{A})$
+    \item $ W = Y(S_{A})$
+    \end{itemize}
+
+    Now we have a new problem: Reproducibility!
+  \end{frame}
+
+  %% Slide 11:
+  \begin{frame}{Reproducibility}
+    Bit-for-bit equivalent output is hard, for example:
+
+    \begin{itemize}
+    \item Timestamps in output artifacts
+    \item Non-deterministic linking order in concurrent builds
+    \item Non-deterministic VM \& memory states in outputs
+    \item Randomness in builds (sic!)
+    \end{itemize}
+  \end{frame}
+
+  \begin{frame}{Reproducibility}
+    \begin{center}
+      Without reproducibility, we can never trust that any shipped
+      binary matches the source code!
+    \end{center}
+  \end{frame}
+
+  %% Slide 12:
+  \section{(Partial) State of the Union}
+
+  \begin{frame}{The Desired State}
+    \begin{center}
+      \begin{enumerate}
+      \item Full-source bootstrap!
+      \item All packages reproducible!
+      \end{enumerate}
+    \end{center}
+  \end{frame}
+
+  %% Slide 13:
+  \begin{frame}{Bootstrapping Debian}
+    \begin{itemize}
+    \item Sparse information on the Debian-wiki
+    \item Bootstrapping discussions mostly resolve around new architectures
+    \item GCC is compiled by depending on previous versions of GCC
+    \end{itemize}
+  \end{frame}
+
+  \begin{frame}{Reproducing Debian}
+    Debian has a very active effort for reproducible builds:
+
+    \begin{itemize}
+    \item Organised information about reproducibility status
+    \item Over 90\% reproducibility in Debian package base!
+    \end{itemize}
+  \end{frame}
+
+  \begin{frame}{Short interlude: Nix}
+    \begin{center}
+      \includegraphics[
+        keepaspectratio=true,
+        height=0.7\textheight
+      ]{nixos-logo.png}
+    \end{center}
+  \end{frame}
+
+  \begin{frame}{Short interlude: Nix}
+    \begin{center}
+      \includegraphics[
+        keepaspectratio=true,
+        height=0.90\textheight
+      ]{drake-meme.png}
+    \end{center}
+  \end{frame}
+
+  \begin{frame}{Short interlude: Nix}
+    \begin{center}
+      \includegraphics[
+        keepaspectratio=true,
+        height=0.7\textheight
+      ]{nixos-logo.png}
+    \end{center}
+  \end{frame}
+
+  \begin{frame}{Bootstrapping NixOS}
+    Nix evaluation can not recurse forever: The bootstrap can not
+    simply depend on a previous GCC.
+
+    Workaround: \texttt{bootstrap-tools} tarball from a previous
+    binary cache is fetched and used.
+
+    An unfortunate magic binary blob ...
+  \end{frame}
+
+  \begin{frame}{Reproducing NixOS}
+    Not all reproducibility patches have been ported from Debian.
+
+    However: Builds are fully repeatable via the Nix fundamentals!
+  \end{frame}
+
+  \section{Future Developments}
+
+  \begin{frame}{Bootstrappable: stage0}
+    Hand-rolled ``Cthulhu's Path to Madness'' hex-programs:
+
+    \begin{itemize}
+    \item No non-auditable binary blobs
+    \item Aims for understandability by 70\% of programmers
+    \item End goal is a full-source bootstrap of GCC
+    \end{itemize}
+  \end{frame}
+
+
+  \begin{frame}{Bootstrappable: MES}
+    Bootstrapping the ``Maxwell Equations of Software'':
+
+    \begin{itemize}
+    \item Minimal C-compiler written in Scheme
+    \item Minimal Scheme-interpreter (currently in C, but intended to
+      be rewritten in stage0 macros)
+    \item End goal is full-source bootstrap of the entire GuixSD
+    \end{itemize}
+  \end{frame}
+
+  \begin{frame}{Other platforms}
+    \begin{itemize}
+    \item Nix for Darwin is actively maintained
+    \item F-Droid Android repository works towards fully reproducible
+      builds of (open) Android software
+    \item Mobile devices (phones, tablets, etc.) are a lost cause at
+      the moment
+    \end{itemize}
+  \end{frame}
+
+  \begin{frame}{Thanks!}
+    Resources:
+    \begin{itemize}
+    \item bootstrappable.org
+    \item reproducible-builds.org
+    \end{itemize}
+
+    @tazjin | mail@tazj.in
+  \end{frame}
+\end{document}
diff --git a/users/tazjin/presentations/bootstrapping-2018/quine-relay.png b/users/tazjin/presentations/bootstrapping-2018/quine-relay.png
new file mode 100644
index 0000000000..5644dc3900
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/quine-relay.png
Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/result.pdfpc b/users/tazjin/presentations/bootstrapping-2018/result.pdfpc
new file mode 100644
index 0000000000..b0fa6c9a0e
--- /dev/null
+++ b/users/tazjin/presentations/bootstrapping-2018/result.pdfpc
@@ -0,0 +1,142 @@
+[file]
+result
+[last_saved_slide]
+10
+[font_size]
+20000
+[notes]
+### 1
+- previous discussions of hardware trust (e.g. purism presentation)
+- people leap to "now I'm on my trusted Debian!"
+- unless you built it from scratch (spoiler: you haven't) you're *trusting* someone
+
+Agenda: Implications of trust with focus on bootstrap paths and reproducibility, plus how you can help.### 2
+self-hosting:
+- C-family: GCC pre/post 4.7, Clang
+- Common Lisp: Sunshine land! (with SBCL)
+- rustc: Bootstrap based on previous versions (C++ transpiler underway!)
+- many other languages also work this way!
+
+(Noteable counterexample: Clojure is written in Java!)### 3
+
+- compilers are just one bit, the various runtimes exist, too!### 4
+
+Could this be exploited?
+
+People don't think about where their compiler comes from.
+
+Even if they do, they may only go so far as to say "I'll just recompile it using <other compiler>".
+
+Unfortunately, spoiler alert, life isn't that easy in the computer world and yes, exploitation is possible.### 5
+
+- describe what a quine is
+- classic Lisp quine
+- explain demo quine
+- demo demo quine
+
+- this is interesting, but not useful - can quines do more than that?### 6
+
+- quine-relay: "art project" with 128-language circular quine
+
+- show source of quine-relay
+
+- (demo quine relay?)
+
+- side-note: this program is very, very trustworthy!### 7
+
+Ken Thompson (designer of UNIX and a couple other things!) received Turing award in 1983, and described attack in speech.
+
+- figure out how to detect self-compilation
+- make that modification a quine
+- insert modification into new compiler
+- add attack code to modification
+- remove attack from source, distributed binary will still be compromised! it's like evolution :)### 8
+
+damage potential is basically infinite:
+
+- classic "login" attack
+=> also applicable to other credentials
+
+- attack (weaken) crypto algorithms
+
+- you can probably think of more!### 10
+
+idea being: potential vulnerability would have to work across compilers:
+
+the more compilers we can introduce (e.g. more architectures, different versions, different compilers), the harder it gets for a vulnerability to survive all of those
+
+The more compilers, the merrier! Lisps are pretty good at this.### 11
+
+if we get a bit-mismatch after DDC, not all hope is lost: Maybe the thing just isn't reproducible!
+
+- many reasons for failures
+- timestamps are a classic! artifacts can be build logs, metadata in ZIP-files or whatever
+- non-determinism is the devil
+- sometimes people actively introduce build-randomness (NaCl)### 12
+
+- Does that binary download on the project's website really match the source?
+
+- Your Linux packages are signed by someone - cool - but what does that mean?### 13
+
+Two things should be achieved - gross oversimplification - to get to the ideal "desired state of the union":
+
+1. full-source bootstrap: without ever introducing any binaries, go from nothing to a full Linux distribution
+
+2. when packages are distributed, we should be able to know the expected output of a source package beforehand
+
+=> suddenly binary distributions become a cache! But more on Nix later.### 14
+
+- Debian project does not seem as concerned with bootstrapping as with reproducibility
+- Debian mostly bootstraps on new architectures (using cross-compilation and similar techniques, from an existing binary base)
+- core bootstrap (GCC & friends) is performed with previous Debian version and depending on GCC### 15
+
+... however! Debian cares about reproducibility.
+
+- automated testing of reproducibility
+- information about the status of all packages is made available in repos
+- Over 90% packages of packages are reproducible!
+
+< show reproducible builds website >
+
+Debian is still fundamentally a binary distribution though, but it doesn't have to be that way.### 16
+
+Nix - a purely functional package manager
+
+It's not a new project (10+ years), been discussed here before, has multiple components: package manager, language, NixOS.
+
+Instead of describing *how* to build a thing, Nix describes *what* to build:### 17
+### 19
+
+In Nix, it's impossible to say "GCC is the result of applying GCC to the GCC source", because that happens to be infinite recursion.
+
+Bootstrapping in Nix works by introducing a binary pinned by its full-hash, which was built on some previous Nix version.
+
+Unfortunately also just a magic binary blob ... ### 20
+
+NixOS is not actively porting all of Debian's reproducibility patches, but builds are fully repeatable:
+
+- introducing a malicious compiler would produce a different input hash -> different package
+
+Future slide: hope is not lost! Things are underway.### 21
+
+- bootstrappable.org (demo?) is an umbrella page for several projects working on bootstrappability
+
+- stage0 is an important piece: manually, small, auditable Hex programs to get to a Hex macro expander
+
+- end goal is a full-source bootrap, but pieces are missing### 22
+
+MES is out of the GuixSD circles (explain Guix, GNU Hurd joke)
+
+- idea being that once you have a Lisp, you have all of computing (as Alan Key said)
+
+- includes MesCC in Scheme -> can *almost* make a working tinyCC -> can *almost* make a working gcc 4.7
+
+- minimal Scheme interpreter, currently built in C to get the higher-level stuff to work, goal is rewrite in hex
+- bootstrapping Guix is the end goal### 23
+
+- userspace in Darwin has a Nix project
+- unsure about other BSDs, but if anyone knows - input welcome!
+- F-Droid has reproducible Android packages, but that's also userspace only
+- All other mobile platforms are a lost cause
+
+Generally, all closed-source software is impossible to trust.
diff --git a/users/tazjin/presentations/erlang-2016/.skip-subtree b/users/tazjin/presentations/erlang-2016/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/.skip-subtree
diff --git a/users/tazjin/presentations/erlang-2016/README.md b/users/tazjin/presentations/erlang-2016/README.md
new file mode 100644
index 0000000000..e1b6c83b99
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/README.md
@@ -0,0 +1,6 @@
+These are the slides for a presentation I gave for the Oslo javaBin meetup in
+2016.
+
+Unfortunately there is no recording of the presentation due to a technical error
+(video was recorded, but no audio). This is a bit of a shame because I think
+these are some of the best slides I've ever made.
diff --git a/users/tazjin/presentations/erlang-2016/presentation.md b/users/tazjin/presentations/erlang-2016/presentation.md
new file mode 100644
index 0000000000..526564b882
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/presentation.md
@@ -0,0 +1,222 @@
+slidenumbers: true
+Erlang.
+======
+
+### Fault-tolerant, concurrent programming.
+
+---
+
+## A brief history of Erlang
+
+---
+
+![](https://www.ericsson.com/thinkingahead/the-networked-society-blog/wp-content/uploads/2014/09/bfW5FSr.jpg)
+
+
+^ Telefontornet in Stockholm, around 1890. Used until 1913. 
+
+---
+
+![](https://3.bp.blogspot.com/-UF7W9yTUO2g/VBqw-1HNTzI/AAAAAAAAPeg/KvsMbNSAcII/s1600/6835942484_1531372d8f_b.jpg)
+
+^ Telephones were operated manually at Switchboards. Anyone old enough to remember? I'm certainly not. 
+
+---
+
+![fit](https://russcam.github.io/fsharp-akka-talk/images/ericsson-301-AXD.png)
+
+^ Eventually we did that in software, and we got better at it over time. Ericsson AXD 301, first commercial Erlang switch. But lets take a step back.
+
+---
+
+## Phone switches must be ...
+
+Highly concurrent
+
+Fault-tolerant
+
+Distributed
+
+(Fast!)
+
+![right 150%](http://learnyousomeerlang.com/static/img/erlang-the-movie.png)
+
+---
+
+## ... and so is Erlang!
+
+---
+
+## Erlang as a whole:
+
+- Unique process model (actors!)
+- Built-in fault-tolerance & error handling
+- Distributed processes
+- Three parts!
+
+---
+
+## Part 1: Erlang, the language
+
+- Functional
+- Prolog-inspired syntax
+- Everything is immutable
+- *Extreme* pattern-matching
+
+---
+### Hello Joe
+
+```erlang
+hello_joe.
+```
+
+---
+### Hello Joe
+
+```erlang
+-module(hello1).
+-export([hello_joe/0]).
+
+hello_joe() ->
+    hello_joe.
+```
+
+---
+### Hello Joe
+
+```erlang
+-module(hello1).
+-export([hello_joe/0]).
+
+hello_joe() ->
+    hello_joe.
+    
+% 1> c(hello1).
+% {ok,hello1}
+% 2> hello1:hello_joe().
+% hello_joe
+```
+
+---
+### Hello Joe
+
+```erlang
+-module(hello2).
+-export([hello/1]).
+
+hello(Name) ->
+    io:format("Hello ~s!~n", [Name]).
+
+% 3> c(hello2).
+% {ok,hello2}
+% 4> hello2:hello("Joe").
+% Hello Joe!
+% ok
+```
+
+---
+
+## [fit] Hello ~~world~~ Joe is boring!
+## [fit] Lets do it with processes.
+
+---
+### Hello Server
+
+```erlang
+-module(hello_server).
+-export([start_server/0]).
+
+start_server() ->
+    spawn(fun() -> server() end).
+
+server() ->
+    receive
+        {greet, Name} ->
+            io:format("Hello ~s!~n", [Name]),
+            server()
+    end.
+```
+
+---
+
+## [fit] Some issues with that ...
+
+- What about unused messages?
+- What if the server crashes?
+
+---
+
+## [fit] Part 2: Open Telecom Platform
+
+### **It's called Erlang/OTP for a reason.**
+
+---
+
+# OTP: An Application Framework
+
+- Supervision - keep processes alive!
+
+- OTP Behaviours - common process patterns
+
+- Extensive standard library
+
+- Error handling, debuggers, testing, ...
+
+- Lots more!
+
+^ Standard library includes lots of things from simple network libraries over testing frameworks to cryptography, complete LDAP clients etc.
+
+---
+
+# Supervision
+
+![inline](http://erlang.org/doc/design_principles/sup6.gif)
+
+^ Supervision keeps processes alive, different restart behaviours, everything should be supervised to avoid "process" (and therefore memory) leaks
+
+---
+
+# OTP Behaviours
+
+* `gen_server`
+* `gen_statem` 
+* `gen_event`
+* `supervisor`
+
+^ gen = generic. explain server, explain statem, event = event handling with registered handlers, supervisor ...
+
+---
+
+`gen_server`
+
+---
+
+## [fit] Part 3: BEAM
+
+### Bogdan/Bjørn Erlang Abstract machine
+
+---
+
+## A VM for Erlang
+
+* Many were written, BEAM survived
+* Concurrent garbage-collection
+* Lower-level bytecode than JVM
+* Very open to new languages
+  (Elixir, LFE, Joxa, ...)
+
+---
+
+## What next?
+
+* Ole's talk, obviously!
+* Learn You Some Erlang!
+  www.learnyousomeerlang.com
+* Watch *Erlang the Movie*
+* (soon!) Join the Oslo BEAM meetup group
+
+---
+
+# [fit] Questions?
+
+`@tazjin`
diff --git a/users/tazjin/presentations/erlang-2016/presentation.pdf b/users/tazjin/presentations/erlang-2016/presentation.pdf
new file mode 100644
index 0000000000..ec8d996704
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/presentation.pdf
Binary files differdiff --git a/users/tazjin/presentations/erlang-2016/src/hello.erl b/users/tazjin/presentations/erlang-2016/src/hello.erl
new file mode 100644
index 0000000000..56404a0c5a
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/src/hello.erl
@@ -0,0 +1,5 @@
+-module(hello).
+-export([hello_joe/0]).
+
+hello_joe() ->
+    hello_joe.
diff --git a/users/tazjin/presentations/erlang-2016/src/hello1.erl b/users/tazjin/presentations/erlang-2016/src/hello1.erl
new file mode 100644
index 0000000000..ca78261399
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/src/hello1.erl
@@ -0,0 +1,5 @@
+-module(hello1).
+-export([hello_joe/0]).
+
+hello_joe() ->
+    hello_joe.
diff --git a/users/tazjin/presentations/erlang-2016/src/hello2.erl b/users/tazjin/presentations/erlang-2016/src/hello2.erl
new file mode 100644
index 0000000000..2d1f6c84c4
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/src/hello2.erl
@@ -0,0 +1,11 @@
+-module(hello2).
+-export([hello/1]).
+
+hello(Name) ->
+    io:format("Hey ~s!~n", [Name]).
+
+% 3> c(hello2).
+% {ok,hello2}
+% 4> hello2:hello("Joe").
+% Hello Joe!
+% ok
diff --git a/users/tazjin/presentations/erlang-2016/src/hello_server.erl b/users/tazjin/presentations/erlang-2016/src/hello_server.erl
new file mode 100644
index 0000000000..01df14ac57
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/src/hello_server.erl
@@ -0,0 +1,12 @@
+-module(hello_server).
+-export([start_server/0, server/0]).
+
+start_server() ->
+    spawn(fun() -> server() end).
+
+server() ->
+    receive
+        {greet, Name} ->
+            io:format("Hello ~s!~n", [Name]),
+            hello_server:server()
+    end.
diff --git a/users/tazjin/presentations/erlang-2016/src/hello_server2.erl b/users/tazjin/presentations/erlang-2016/src/hello_server2.erl
new file mode 100644
index 0000000000..24bb934ee5
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/src/hello_server2.erl
@@ -0,0 +1,36 @@
+-module(hello_server2).
+-behaviour(gen_server).
+-compile(export_all).
+
+%%% Start callback for supervisor
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+%%% gen_server callbacks
+
+init([]) ->
+    {ok, sets:new()}.
+
+handle_call({greet, Name}, _From, State) ->
+    io:format("Hello ~s!~n", [Name]),
+    NewState = sets:add_element(Name, State),
+    {reply, ok, NewState};
+
+handle_call({bye, Name}, _From, State) ->
+    io:format("Goodbye ~s!~n", [Name]),
+    NewState = sets:del_element(Name, State),
+    {reply, ok, NewState}.
+
+terminate(normal, State) ->
+    [io:format("Goodbye ~s!~n", [Name]) || Name <- State],
+    ok.
+
+%%% Unused gen_server callbacks
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+handle_cast(_Request, State) ->
+    {noreply, State}.
diff --git a/users/tazjin/presentations/erlang-2016/src/hello_sup.erl b/users/tazjin/presentations/erlang-2016/src/hello_sup.erl
new file mode 100644
index 0000000000..7fee0928c5
--- /dev/null
+++ b/users/tazjin/presentations/erlang-2016/src/hello_sup.erl
@@ -0,0 +1,24 @@
+-module(hello_sup).
+-behaviour(supervisor).
+-export([start_link/0, init/1]).
+
+%%% Module API
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%%% Supervisor callbacks
+
+init([]) ->
+    Children = [hello_spec()],
+    {ok, { {one_for_one, 5, 10}, Children}}.
+
+%%% Private
+
+hello_spec() ->
+    #{id       => hello_server2,
+      start    => {hello_server2, start_link, []},
+      restart  => permanent,
+      shutdown => 5000,
+      type     => worker,
+      module   => [hello_server2]}.
diff --git a/users/tazjin/presentations/servant-2016/Makefile b/users/tazjin/presentations/servant-2016/Makefile
new file mode 100644
index 0000000000..96115ec2cb
--- /dev/null
+++ b/users/tazjin/presentations/servant-2016/Makefile
@@ -0,0 +1,8 @@
+all: slides
+
+slides:
+	lualatex --shell-escape slides.tex
+
+clean:
+	rm -f slides.aux slides.log slides.nav \
+	slides.out slides.toc slides.snm
diff --git a/users/tazjin/presentations/servant-2016/README.md b/users/tazjin/presentations/servant-2016/README.md
new file mode 100644
index 0000000000..8cfb04a424
--- /dev/null
+++ b/users/tazjin/presentations/servant-2016/README.md
@@ -0,0 +1,7 @@
+These are the slides for my presentation about [servant][] at [Oslo Haskell][].
+
+A full video recording of the presentation is available [on Vimeo][].
+
+[servant]: https://haskell-servant.github.io/
+[Oslo Haskell]: http://www.meetup.com/Oslo-Haskell/events/227107530/
+[on Vimeo]: https://vimeo.com/153901805
diff --git a/users/tazjin/presentations/servant-2016/slides.pdf b/users/tazjin/presentations/servant-2016/slides.pdf
new file mode 100644
index 0000000000..842a667e1b
--- /dev/null
+++ b/users/tazjin/presentations/servant-2016/slides.pdf
Binary files differdiff --git a/users/tazjin/presentations/servant-2016/slides.pdfpc b/users/tazjin/presentations/servant-2016/slides.pdfpc
new file mode 100644
index 0000000000..ed46003768
--- /dev/null
+++ b/users/tazjin/presentations/servant-2016/slides.pdfpc
@@ -0,0 +1,75 @@
+[file]
+slides.pdf
+[font_size]
+10897
+[notes]
+### 1
+13### 2
+Let's talk about servant, which is several things:
+API description DSL, we'll speak about how this DSL works
+and why it's at the type level
+
+Interpretations of the types resulting from that DSL, for example in
+web servers or API clients
+
+Servant is commonly used or implementing services with APIs, or for accessing
+other APIs with a simple, typed client
+### 3
+Why type-level DSLs?
+Type-level DSL:  express *something*, e.g. endpoints of API, on  type level by combining types. Types can be uninhabited
+
+Phil Wadler's: expression problem: things should be extensible both in the cases of a type, and in the functions operating on the type
+Normal data types: can't add new constructors easily
+Servant lifts thisup to simply allow the declaration of new types that can be included in the DSL, and new interpretations that can be attached to the types through typeclasses
+
+APIs become first-class citizens, can pass them around, combine them etc, they are separate from interpretations such as server implementations. In contrast, in most webframeworks, API declaration is implicit
+
+(Mention previous attemps at type-safe web, Yesod / web-routes + boomerang etc)
+### 4
+Three extensions are necessary:
+TypeOperators lets us use infix operators on the type level as constructors
+DataKinds promotes new type declarations to the kind level, makes type-level literals (strings and natural numbers) available, lets us use type-level lists and pairs in combination with typeoperators
+TypeFamilies: Type-level functions, map one set of types to another, come in two forms (type families, non-injective; data families, injective), more powerful than associated types
+### 5
+Here you can see servant's general syntax, we define an API type as a simple alias of some other type combinations
+strings are type-level strings, not actually values, represent path elements
+endpoints are separated by :<|>, all endpoints end in a method with content types and return types
+Capture captures path segments, but there are other combinators, for example for headers
+Everything that is used from the request is expressed in types, enforcing checkability, no "escape hatch" inside handlers to get request
+Every combinator has associated interpretations through typeclasses
+### 6
+Explain type alias, point out Capture
+Server is a type level function (type family), as mentioned earlier
+### 7
+If we expand server (in ghci with kind!) we can see the actual type of the
+function
+### 8
+Lets speak about some interpretations of these things
+### 9
+Servant server is the main interpretation that people are interested in, it's used
+for taking a type specification and creating a server from it
+Based on WAI, the web application interface, common abstraction for web servers which came out of the Yesod project. Implemented by the web server warp, which Yesod runs on
+### 10
+Explain snippet, path gets removed from server type (irrelevant for handler),
+route extracts string to value level
+### 11
+Explain echo server quickly
+### 12
+servant client allows generation of Haskell functions that query the API with the same types
+this makes for easy to use RPC for example
+### 13
+A lot of other interpretations exist for all kinds of things, mock servers for testing, foreign functions in various languages, documentation ...
+### 14
+Demo!
+1. Go quickly through code
+2. Run server, query with curl
+3. Open javascript function
+4. Show JS code in the thing
+5. Open the map itself
+6. Open GHCi, use client
+7. Generate docs
+### 15
+Conclusion
+Servant is pretty good, it's very easy to get started and it's great to raise the level of things that the compiler can tell you about when you do them wrong.
+### 16
+Drawbacks.
diff --git a/users/tazjin/presentations/servant-2016/slides.tex b/users/tazjin/presentations/servant-2016/slides.tex
new file mode 100644
index 0000000000..d5947eb942
--- /dev/null
+++ b/users/tazjin/presentations/servant-2016/slides.tex
@@ -0,0 +1,137 @@
+\documentclass[12pt]{beamer}
+\usetheme{metropolis}
+\usepackage{minted}
+
+\newenvironment{code}{\ttfamily}{\par}
+
+\title{servant}
+\subtitle{Defining web APIs at the type-level}
+
+\begin{document}
+\metroset{titleformat frame=smallcaps}
+\setminted{fontsize=\scriptsize}
+
+
+\maketitle
+
+\section{Introduction}
+
+\begin{frame}{Type-level DSLs?}
+  \begin{itemize}
+  \item (Uninhabited) types with attached ``meaning''
+  \item The Expression Problem (Wadler 1998)
+  \item API representation and interpretation are separated
+  \item APIs become first-class citizens
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Haskell extensions}
+  \begin{itemize}
+  \item TypeOperators
+  \item DataKinds
+  \item TypeFamilies
+  \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]{A servant example}
+  \begin{minted}{haskell}
+    type PubAPI = "pubs" :> Get ’[JSON] [Pub]
+             :<|> "pubs" :> "tagged"
+                         :> Capture "tag" Text
+                         :> Get ’[JSON] [Pub]
+  \end{minted}
+\end{frame}
+
+\begin{frame}[fragile]{Computed types}
+  \begin{minted}{haskell}
+    type TaggedPubs = "tagged" :> Capture "tag" Text :> ...
+
+    taggedPubsHandler :: Server TaggedPubs
+    taggedPubsHandler tag = ...
+  \end{minted}
+\end{frame}
+
+\begin{frame}[fragile]{Computed types}
+  \begin{minted}{haskell}
+    type TaggedPubs = "tagged" :> Capture "tag" Text :> ...
+
+    taggedPubsHandler :: Server TaggedPubs
+    taggedPubsHandler tag = ...
+
+    Server TaggedPubs ~
+    Text -> EitherT ServantErr IO [Pub]
+  \end{minted}
+\end{frame}
+
+\section{Interpretations}
+
+\begin{frame}{servant-server}
+  The one everyone is interested in!
+
+  \begin{itemize}
+  \item Based on WAI, can run on warp
+  \item Interprets combinators with a simple \texttt{HasServer c} class
+  \item Easy to use!
+  \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]{HasServer ...}
+  \begin{minted}{haskell}
+    instance (KnownSymbol path, HasServer sublayout)
+             => HasServer (path :> sublayout) where
+      type ServerT (path :> sublayout) m = ServerT sublayout m
+
+      route ...
+        where
+          pathString = symbolVal (Proxy :: Proxy path)
+  \end{minted}
+\end{frame}
+
+\begin{frame}[fragile]{Server example}
+  \begin{minted}{haskell}
+    type Echo = Capture "echo" Text :> Get ’[PlainText] Text
+
+    echoAPI :: Proxy Echo
+    echoAPI = Proxy
+
+    echoServer :: Server Echo
+    echoServer = return
+  \end{minted}
+\end{frame}
+
+\begin{frame}{servant-client}
+  \begin{itemize}
+  \item Generates Haskell client functions for API
+  \item Same types as API specification: For RPC the whole ``web layer'' is abstracted away
+  \item Also easy to use!
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{servant-docs, servant-js ...}
+  Many other interpretations exist already, for example:
+  \begin{itemize}
+  \item Documentation generation
+  \item Foreign function export (e.g. Elm, JavaScript)
+  \item Mock-server generation
+  \end{itemize}
+\end{frame}
+
+\section{Demo}
+
+\section{Conclusion}
+
+\begin{frame}{Drawbacks}
+  \begin{itemize}
+  \item Haskell has no custom open kinds (yet)
+  \item Proxies are ugly
+  \item Errors can be a bit daunting
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Questions?}
+  Ølkartet: github.com/tazjin/pubkartet \\
+  Slides: github.com/tazjin/servant-presentation
+
+  @tazjin
+\end{frame}
+\end{document}
diff --git a/users/tazjin/presentations/systemd-2016/.gitignore b/users/tazjin/presentations/systemd-2016/.gitignore
new file mode 100644
index 0000000000..1a38620fe9
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/.gitignore
@@ -0,0 +1,6 @@
+slides.aux
+slides.log
+slides.nav
+slides.out
+slides.snm
+slides.toc
diff --git a/users/tazjin/presentations/systemd-2016/.skip-subtree b/users/tazjin/presentations/systemd-2016/.skip-subtree
new file mode 100644
index 0000000000..108b3507dd
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/.skip-subtree
@@ -0,0 +1 @@
+No Nix files will ever be under this tree ...
diff --git a/users/tazjin/presentations/systemd-2016/Makefile b/users/tazjin/presentations/systemd-2016/Makefile
new file mode 100644
index 0000000000..ac5dde3cb3
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/Makefile
@@ -0,0 +1,11 @@
+all: slides.pdf
+
+slides.toc:
+	lualatex slides.tex
+
+slides.pdf: slides.toc
+	lualatex slides.tex
+
+clean:
+	rm -f slides.aux slides.log slides.nav \
+	slides.out slides.toc slides.snm
diff --git a/users/tazjin/presentations/systemd-2016/README.md b/users/tazjin/presentations/systemd-2016/README.md
new file mode 100644
index 0000000000..7f004b7d14
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/README.md
@@ -0,0 +1,6 @@
+This repository contains the slides for my systemd presentation at Hackeriet.
+
+Requires LaTeX, [beamer][] and the [metropolis][] theme.
+
+[beamer]: http://mirror.hmc.edu/ctan/macros/latex/contrib/beamer/
+[metropolis]: https://github.com/matze/mtheme
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-error.service b/users/tazjin/presentations/systemd-2016/demo/demo-error.service
new file mode 100644
index 0000000000..b2d4c9d347
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo-error.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Demonstrate failing units
+OnFailure=demo-notify@%n.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/false
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-limits.slice b/users/tazjin/presentations/systemd-2016/demo/demo-limits.slice
new file mode 100644
index 0000000000..998185d261
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo-limits.slice
@@ -0,0 +1,7 @@
+[Unit]
+Description=Limited resources demo
+DefaultDependencies=no
+Before=slices.target
+
+[Slice]
+CPUQuota=10%
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-notify@.service b/users/tazjin/presentations/systemd-2016/demo/demo-notify@.service
new file mode 100644
index 0000000000..e25524b4e2
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo-notify@.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Demonstrate systemd templating by sending a notification
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/notify-send 'Systemd notification' '%i'
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-path.path b/users/tazjin/presentations/systemd-2016/demo/demo-path.path
new file mode 100644
index 0000000000..87f1342da9
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo-path.path
@@ -0,0 +1,6 @@
+[Unit]
+Description=Demonstrate systemd path units
+
+[Path]
+DirectoryNotEmpty=/tmp/hackeriet
+Unit=demo.service
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-stress.service b/users/tazjin/presentations/systemd-2016/demo/demo-stress.service
new file mode 100644
index 0000000000..7e14f13e29
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo-stress.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Stress test CPU
+
+[Service]
+Slice=demo.slice
+ExecStart=/usr/bin/stress -c 5
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-timer.timer b/users/tazjin/presentations/systemd-2016/demo/demo-timer.timer
new file mode 100644
index 0000000000..34eccb98b0
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo-timer.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=Demonstrate systemd timers
+
+[Timer]
+OnActiveSec=2
+OnUnitActiveSec=5
+AccuracySec=5
+Unit=demo.service
+# OnCalendar=Thu,Fri 2016-*-1,5 11:12:13
+
+[Install]
+WantedBy=multi-user.target
diff --git a/users/tazjin/presentations/systemd-2016/demo/demo.service b/users/tazjin/presentations/systemd-2016/demo/demo.service
new file mode 100644
index 0000000000..fcc710ad93
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/demo.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Demo unit for systemd
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/echo "Systemd unit activated. Hello Hackeriet."
diff --git a/users/tazjin/presentations/systemd-2016/demo/notes.md b/users/tazjin/presentations/systemd-2016/demo/notes.md
new file mode 100644
index 0000000000..b4866b1642
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/demo/notes.md
@@ -0,0 +1,27 @@
+# simple oneshot
+
+Run `demo-notify@hello.service`
+
+# simple timer
+
+Run `demo-timer.timer`, show both
+
+# enabling
+
+Enable `demo-timer.timer`, go to symlink folder, disable
+
+# OnError
+
+Show & run `demo-error.service`
+
+# cgroups demo
+
+Start `demo-stress.service` without, show in htop, stop
+Show slice unit, start slice unit
+Add Slice=demo-limits.slice
+daemon-reload
+Start stress again
+
+# Proper service
+
+Look at nginx unit
diff --git a/users/tazjin/presentations/systemd-2016/slides.pdf b/users/tazjin/presentations/systemd-2016/slides.pdf
new file mode 100644
index 0000000000..384db2a6e0
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/slides.pdf
Binary files differdiff --git a/users/tazjin/presentations/systemd-2016/slides.pdfpc b/users/tazjin/presentations/systemd-2016/slides.pdfpc
new file mode 100644
index 0000000000..99326bd8bf
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/slides.pdfpc
@@ -0,0 +1,85 @@
+[file]
+slides.pdf
+[notes]
+### 1
+### 2
+Let's start off by looking at what an init system is, how they used to work and what systemd does different before we go into more systemd-specific details.
+### 3
+system processes that are started include for example FS mounts, network settings, powertop...
+system services are long-running processes such as daemons, e.g. SSH, database or web servers, session managers, udev ...
+
+orphans: Process whose parent has finished somehow, gets adopted by init system
+-> when a process terminates its parent must call wait() to get its exit() code, if there is no init system adopting orphans the process would become a zombie
+### 4
+Before systemd there were simple init systems that just did the tasks listed on the previous slide.
+Init scripts -> increased greatly in complexity over time, look at incomprehensible skeleton for Debian service init scripts
+Runlevels -> things such as single-user mode, full multiuser mode, reboot, halt
+
+Init will run all the scripts, but it will not do much more than print information on success/failure of started scripts
+
+Init scripts run strictly sequential
+
+Init is unaware of inter-service dependencies, expressed through prefixing scripts with numbers etc.
+
+Init will not watch processes after system is booted -> crashing daemons will not automatically restart
+### 5
+### 6
+How systemd came to be
+
+Considering the lack of process monitoring, problematic things about init scripts -> legacy init systems have drawbacks
+
+Apple had already built launchd, a more featured init system that monitored running processes, could automatically restart them and allowed for certain advanced features -> however it is awful to use and wrap your head around
+
+Lennart Poettering of Pulseaudio fame and Kay Sievers decided to implement a new init system to address these problems, while taking certain clues from Apple's design
+### 7
+Systemd's design goals
+### 8
+No more init scripts with opaque effects -> services are clearly defined units
+Unit dependencies -> systemd can figure out what can be started in parallel
+Process supervision: Unit can be configured in many ways, e.g. always restart, only restart on success etc
+Service logs: We'll talk more about this later
+### 9
+Units are the core component of systemd that users deal with. They define services and everything else that systemd needs to start and manage.
+Note that all these are the names of the respective man page on a system with systemd installed
+Types:
+systemd.service - processes controlled by systemd
+systemd.target - equivalent to "runlevels", grouping of units for synchronisation
+systemd.timer - more powerful replacement of cron that starts other units
+systemd.path - systemd equvialent of inotify, watches files/folders -> launches units
+systemd.socket - expose local IPC or network sockets, launch units on connections
+systemd.device - trigger units when certain devices are connected
+systemd.mount - systemd equivalent of fstab entries
+systemd.swap - like mount
+systemd.slice - unit groups for resource management purposes
+... and a few more specialised ones
+### 10
+Linux cgroups are a new resource management feature added quite a long time ago, but not used much.
+Cgroups can be created manually and processes can be moved into them in order to control resource utilisation
+Few people used them before systemd, limits.conf was often much easier but not as fine-grained
+Systemd changed this
+### 11
+Systemd collects standard output and stderr from all processes into its journal system
+they provide a tool for querying the log, for example grouping service logs together with correct timestamps, querying,
+### 12
+Systemd tooling, most important one is systemctl for general service management
+journalctl is the query and management tool for journald
+systemd-analyze is used for figuring out performance issues, for example by analysing the boot process, can make cool graphs of dependencies
+systemd-cgtop is like top, but not on a process level - it's on a cgroup/slice level, shows combined usage of cgroups
+systemd-cgls lists contents of systemd's cgroups to see which services are in what group
+there also exist a bunch of others that we'll skip for now
+### 13
+### 14
+### 15
+Systemd criticism comes from many directions and usually focuses on a few points
+feature-creep: systemd is absorbing a lot of different services
+### 16
+explain diagram a bit
+### 17
+opaque: as a result, systemd has a lot more internal complexity that people can't easily wrap your mind around. However I argue that unless you're using something like suckless' sinit with your own scripts, you probably have no idea what your init does today anyways
+unstable: this was definitely true even in the first stable release, with the binary log format getting corrupted for example. I haven't personally experienced any trouble with it recently though.
+Another thing is that services start depending on systemd when they shouldn't, a problem for the BSD world (who cares (hey christoph!))
+### 18
+Despite criticism, systemd was adopted rapidly by large portions of the Linux
+Initially in RedHat, because Poettering and co work there and it was clear from the beginning that it would be there
+ArchLinux (which I'm using) and a few others followed suit quite quickly
+Eventually, the big Debian init system discussion - after a lot of flaming - led to Debian adopting it as well, which had a ripple effect for related distros such as Ubuntu which abandoned upstart for it.
\ No newline at end of file
diff --git a/users/tazjin/presentations/systemd-2016/slides.tex b/users/tazjin/presentations/systemd-2016/slides.tex
new file mode 100644
index 0000000000..c613cefd7e
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/slides.tex
@@ -0,0 +1,160 @@
+\documentclass[12pt]{beamer}
+\usetheme{metropolis}
+
+\newenvironment{code}{\ttfamily}{\par}
+
+\title{systemd}
+\subtitle{The standard Linux init system}
+
+\begin{document}
+\metroset{titleformat frame=smallcaps}
+
+\maketitle
+
+\section{Introduction}
+
+\begin{frame}{What is an init system?}
+  An init system is the first userspace process (PID 1) started in a UNIX-like system. It handles:
+
+  \begin{itemize}
+  \item Starting system processes and services to prepare the environment
+  \item Adopting and ``reaping'' orphaned processes
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Classical init systems}
+  Init systems before systemd - such as SysVinit - were very simple.
+
+  \begin{itemize}
+  \item Services and processes to run are organised into ``init scripts''
+  \item Scripts are linked to specific runlevels
+  \item Init system is configured to boot into a runlevel
+  \end{itemize}
+
+\end{frame}
+
+\section{systemd}
+
+\begin{frame}{Can we do better?}
+  \begin{itemize}
+  \item ``legacy'' init systems have a lot of drawbacks
+  \item Apple is taking a different approach on OS X
+  \item Systemd project was founded to address these issues
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Systemd design goals}
+  \begin{itemize}
+  \item Expressing service dependencies
+  \item Monitoring service status
+  \item Enable parallel service startups
+  \item Ease of use
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Systemd - the basics}
+  \begin{itemize}
+  \item No scripts are executed, only declarative units
+  \item Units have explicit dependencies
+  \item Processes are supervised
+  \item cgroups are utilised to apply resource limits
+  \item Service logs are managed and centrally queryable
+  \item Much more!
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Systemd units}
+  Units specify how and what to start. Several types exist:
+  \begin{code}
+    \small
+    \begin{columns}[T,onlytextwidth]
+      \column{0.5\textwidth}
+      \begin{itemize}
+      \item systemd.service
+      \item systemd.target
+      \item systemd.timer
+      \item systemd.path
+      \item systemd.socket
+      \end{itemize}
+      \column{0.5\textwidth}
+      \begin{itemize}
+      \item systemd.device
+      \item systemd.mount
+      \item systemd.swap
+      \item systemd.slice
+      \end{itemize}
+    \end{columns}
+  \end{code}
+\end{frame}
+
+
+\begin{frame}{Resource management}
+  Systemd utilises Linux \texttt{cgroups} for resource management, specifically CPU, disk I/O and memory usage.
+
+  \begin{itemize}
+  \item Hierarchical setup of groups makes it easy to limit resources for a set of services
+  \item Units can be attached to a \texttt{systemd.slice} for controlling resources for a group of services
+  \item Resource limits can also be specified directly in the unit
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{journald}
+  Systemd comes with an integrated log management solution, replacing software such as \texttt{syslog-ng}.
+  \begin{itemize}
+  \item All process output is collected in the journal
+  \item \texttt{journalctl} tool provides many options for querying and tailing logs
+  \item Children of processes automatically log to the journal as well
+  \item \textbf{Caveat:} Hard to learn initially
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Systemd tooling}
+  A variety of CLI-tools exist for managing systemd systems.
+  \begin{code}
+    \begin{itemize}
+    \item systemctl
+    \item journalctl
+    \item systemd-analyze
+    \item systemd-cgtop
+    \item systemd-cgls
+    \end{itemize}
+  \end{code}
+
+  Let's look at some of them.
+\end{frame}
+
+\section{Demo}
+
+\section{Controversies}
+
+\begin{frame}{Systemd criticism}
+  Systemd has been heavily criticised, usually focusing around a few points:
+  \begin{itemize}
+  \item Feature-creep: Systemd absorbs more and more other services
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Systemd criticism}
+  \includegraphics[keepaspectratio=true,width=\textwidth]{systemdcomponents.png}
+\end{frame}
+
+\begin{frame}{Systemd criticism}
+  Systemd has been heavily criticised, usually focusing around a few points:
+  \begin{itemize}
+  \item Feature-creep: Systemd absorbs more and more other services
+  \item Opaque: systemd's inner workings are harder to understand than old \texttt{init}
+  \item Unstable: development is quick and breakage happens
+  \end{itemize}
+\end{frame}
+
+\begin{frame}{Systemd adoption}
+  Systemd was initially adopted by RedHat (and related distributions).
+
+  It spread quickly to others, for example ArchLinux.
+
+  Debian and Ubuntu were the last major players who decided to adopt it, but not without drama.
+\end{frame}
+
+\section{Questions?}
+
+\end{document}
diff --git a/users/tazjin/presentations/systemd-2016/systemdcomponents.png b/users/tazjin/presentations/systemd-2016/systemdcomponents.png
new file mode 100644
index 0000000000..a22c762f7e
--- /dev/null
+++ b/users/tazjin/presentations/systemd-2016/systemdcomponents.png
Binary files differdiff --git a/users/tazjin/rlox/.gitignore b/users/tazjin/rlox/.gitignore
new file mode 100644
index 0000000000..29e65519ba
--- /dev/null
+++ b/users/tazjin/rlox/.gitignore
@@ -0,0 +1,3 @@
+result
+/target
+**/*.rs.bk
diff --git a/users/tazjin/rlox/Cargo.lock b/users/tazjin/rlox/Cargo.lock
new file mode 100644
index 0000000000..d8107726e0
--- /dev/null
+++ b/users/tazjin/rlox/Cargo.lock
@@ -0,0 +1,6 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "rlox"
+version = "0.1.0"
+
diff --git a/users/tazjin/rlox/Cargo.toml b/users/tazjin/rlox/Cargo.toml
new file mode 100644
index 0000000000..b66af6ba85
--- /dev/null
+++ b/users/tazjin/rlox/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "rlox"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+edition = "2018"
+
+[features]
+# Enables debugging/disassembling in the bytecode interpreter. Off by
+# default as it is quite spammy.
+disassemble = []
diff --git a/users/tazjin/rlox/README.md b/users/tazjin/rlox/README.md
new file mode 100644
index 0000000000..1d2692d09c
--- /dev/null
+++ b/users/tazjin/rlox/README.md
@@ -0,0 +1,7 @@
+This is an interpreter for the Lox language, based on the book "[Crafting
+Interpreters](https://craftinginterpreters.com/)".
+
+The book's original code uses Java, but I don't want to use Java, so I've
+decided to take on the extra complexity of porting it to Rust.
+
+Note: This implements the first of two Lox interpreters.
diff --git a/users/tazjin/rlox/default.nix b/users/tazjin/rlox/default.nix
new file mode 100644
index 0000000000..e50ac32be4
--- /dev/null
+++ b/users/tazjin/rlox/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+}
diff --git a/users/tazjin/rlox/examples/builtins.lox b/users/tazjin/rlox/examples/builtins.lox
new file mode 100644
index 0000000000..39af1d73c4
--- /dev/null
+++ b/users/tazjin/rlox/examples/builtins.lox
@@ -0,0 +1 @@
+print clock();
diff --git a/users/tazjin/rlox/examples/fib.lox b/users/tazjin/rlox/examples/fib.lox
new file mode 100644
index 0000000000..1b91e9db94
--- /dev/null
+++ b/users/tazjin/rlox/examples/fib.lox
@@ -0,0 +1,6 @@
+fun fib(n) {
+  if (n <= 1) return n;
+  return fib(n - 2) + fib(n - 1);
+}
+
+print fib(30);
diff --git a/users/tazjin/rlox/examples/func.lox b/users/tazjin/rlox/examples/func.lox
new file mode 100644
index 0000000000..d197ad1138
--- /dev/null
+++ b/users/tazjin/rlox/examples/func.lox
@@ -0,0 +1,5 @@
+fun foo(name) {
+  print("hello " + name);
+}
+
+foo("bar");
diff --git a/users/tazjin/rlox/examples/hello.lox b/users/tazjin/rlox/examples/hello.lox
new file mode 100644
index 0000000000..31752d9e2f
--- /dev/null
+++ b/users/tazjin/rlox/examples/hello.lox
@@ -0,0 +1,34 @@
+var a = 12;
+var b = a * 2;
+
+{
+  var b = a * 3;
+  a = 42;
+  print b;
+}
+
+print a;
+print b;
+
+if (5 > 4)
+  print "it's true";
+else
+  print "it's false";
+
+if (false)
+  print "it's not true";
+
+if (true and false)
+  print "won't happen";
+
+if (true or false)
+  print "will happen";
+
+var n = 5;
+while (n > 0) {
+  print "counting down";
+  n = n - 1;
+}
+
+for(var i = 0; i < 10; i = i + 1)
+  print "bla";
diff --git a/users/tazjin/rlox/examples/if.lox b/users/tazjin/rlox/examples/if.lox
new file mode 100644
index 0000000000..5f335c0e8b
--- /dev/null
+++ b/users/tazjin/rlox/examples/if.lox
@@ -0,0 +1,7 @@
+if (false) {
+  print "yes";
+} else {
+  print "no";
+}
+
+print "afterwards";
diff --git a/users/tazjin/rlox/examples/scope.lox b/users/tazjin/rlox/examples/scope.lox
new file mode 100644
index 0000000000..d563807943
--- /dev/null
+++ b/users/tazjin/rlox/examples/scope.lox
@@ -0,0 +1,19 @@
+var a = "global a";
+var b = "global b";
+var c = "global c";
+{
+  var a = "outer a";
+  var b = "outer b";
+  {
+    var a = "inner a";
+    print a;
+    print b;
+    print c;
+  }
+  print a;
+  print b;
+  print c;
+}
+print a;
+print b;
+print c;
diff --git a/users/tazjin/rlox/examples/scope2.lox b/users/tazjin/rlox/examples/scope2.lox
new file mode 100644
index 0000000000..f826c86588
--- /dev/null
+++ b/users/tazjin/rlox/examples/scope2.lox
@@ -0,0 +1,10 @@
+var a = "global";
+{
+  fun showA() {
+    print a;
+  }
+
+  showA();
+  var a = "block";
+  showA();
+}
diff --git a/users/tazjin/rlox/examples/slow.lox b/users/tazjin/rlox/examples/slow.lox
new file mode 100644
index 0000000000..dd6fb5e4bf
--- /dev/null
+++ b/users/tazjin/rlox/examples/slow.lox
@@ -0,0 +1,9 @@
+fun fib(n) {
+  if (n < 2) return n;
+  return fib(n - 1) + fib(n - 2);
+}
+
+var before = clock();
+print fib(40);
+var after = clock();
+print after - before;
diff --git a/users/tazjin/rlox/examples/var.lox b/users/tazjin/rlox/examples/var.lox
new file mode 100644
index 0000000000..7af90b3f0b
--- /dev/null
+++ b/users/tazjin/rlox/examples/var.lox
@@ -0,0 +1,8 @@
+var a = 10;
+var b = 5;
+
+{
+  var b = 10;
+  var c = 2;
+  a * b * c;
+}
diff --git a/users/tazjin/rlox/rustfmt.toml b/users/tazjin/rlox/rustfmt.toml
new file mode 100644
index 0000000000..df99c69198
--- /dev/null
+++ b/users/tazjin/rlox/rustfmt.toml
@@ -0,0 +1 @@
+max_width = 80
diff --git a/users/tazjin/rlox/src/bytecode/chunk.rs b/users/tazjin/rlox/src/bytecode/chunk.rs
new file mode 100644
index 0000000000..fc5cd34fdf
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/chunk.rs
@@ -0,0 +1,93 @@
+use std::ops::Index;
+
+use super::opcode::{CodeIdx, ConstantIdx, OpCode};
+use super::value;
+
+// In the book, this type is a hand-rolled dynamic array
+// implementation in C. The main benefit of following that approach
+// would be avoiding issues with OpCode variants not having equal
+// sizes, but for the purpose of this I'm going to ignore that
+// problem.
+#[derive(Debug, Default)]
+pub struct Chunk {
+    pub code: Vec<OpCode>,
+    lines: Vec<Span>,
+    constants: Vec<value::Value>,
+}
+
+#[derive(Debug)]
+struct Span {
+    /// Source code line
+    line: usize,
+
+    /// Number of instructions derived from this line
+    count: usize,
+}
+
+impl Chunk {
+    pub fn add_op(&mut self, data: OpCode, line: usize) -> CodeIdx {
+        let idx = self.code.len();
+        self.code.push(data);
+        self.add_line(line);
+        CodeIdx(idx)
+    }
+
+    pub fn add_constant(&mut self, data: value::Value) -> usize {
+        let idx = self.constants.len();
+        self.constants.push(data);
+        idx
+    }
+
+    pub fn constant(&self, idx: ConstantIdx) -> &value::Value {
+        self.constants.index(idx.0)
+    }
+
+    fn add_line(&mut self, line: usize) {
+        match self.lines.last_mut() {
+            Some(span) if span.line == line => span.count += 1,
+            _ => self.lines.push(Span { line, count: 1 }),
+        }
+    }
+
+    pub fn get_line(&self, offset: usize) -> usize {
+        let mut pos = 0;
+        for span in &self.lines {
+            pos += span.count;
+            if pos > offset {
+                return span.line;
+            }
+        }
+
+        panic!("invalid chunk state: line missing for offset {}", offset);
+    }
+}
+
+// Disassembler
+
+/// Print a single disassembled instruction at the specified offset.
+/// Some instructions are printed "raw", others have special handling.
+#[cfg(feature = "disassemble")]
+pub fn disassemble_instruction(chunk: &Chunk, offset: usize) {
+    print!("{:04} ", offset);
+
+    let line = chunk.get_line(offset);
+    if offset > 0 && line == chunk.get_line(offset - 1) {
+        print!("   | ");
+    } else {
+        print!("{:4} ", line);
+    }
+
+    match chunk.code.index(offset) {
+        OpCode::OpConstant(idx) => {
+            println!("OpConstant({:?}) '{:?}'", idx, chunk.constant(*idx))
+        }
+        op => println!("{:?}", op),
+    }
+}
+
+#[cfg(feature = "disassemble")]
+pub fn disassemble_chunk(chunk: &Chunk) {
+    for (idx, _) in chunk.code.iter().enumerate() {
+        disassemble_instruction(chunk, idx);
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/compiler.rs b/users/tazjin/rlox/src/bytecode/compiler.rs
new file mode 100644
index 0000000000..89584f19d7
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/compiler.rs
@@ -0,0 +1,702 @@
+use super::chunk::Chunk;
+use super::errors::{Error, ErrorKind, LoxResult};
+use super::interner::{InternedStr, Interner};
+use super::opcode::{CodeIdx, CodeOffset, ConstantIdx, OpCode, StackIdx};
+use super::value::Value;
+use crate::scanner::{self, Token, TokenKind};
+
+#[cfg(feature = "disassemble")]
+use super::chunk;
+
+#[derive(Debug)]
+enum Depth {
+    Unitialised,
+    At(usize),
+}
+
+impl Depth {
+    fn above(&self, theirs: usize) -> bool {
+        match self {
+            Depth::Unitialised => false,
+            Depth::At(ours) => *ours > theirs,
+        }
+    }
+
+    fn below(&self, theirs: usize) -> bool {
+        match self {
+            Depth::Unitialised => false,
+            Depth::At(ours) => *ours < theirs,
+        }
+    }
+}
+
+#[derive(Debug)]
+struct Local {
+    name: Token,
+    depth: Depth,
+}
+
+#[derive(Debug, Default)]
+struct Locals {
+    locals: Vec<Local>,
+    scope_depth: usize,
+}
+
+struct Compiler<T: Iterator<Item = Token>> {
+    tokens: T,
+    chunk: Chunk,
+    panic: bool,
+    errors: Vec<Error>,
+    strings: Interner,
+    locals: Locals,
+
+    current: Option<Token>,
+    previous: Option<Token>,
+}
+
+#[derive(Debug, PartialEq, PartialOrd)]
+enum Precedence {
+    None,
+    Assignment, // =
+    Or,         // or
+    And,        // and
+    Equality,   // == !=
+    Comparison, // < > <= >=
+    Term,       // + -
+    Factor,     //
+    // 
+    // * /
+    Unary, // ! -
+    Call,  // . ()
+    Primary,
+}
+
+type ParseFn<T> = fn(&mut Compiler<T>) -> LoxResult<()>;
+
+struct ParseRule<T: Iterator<Item = Token>> {
+    prefix: Option<ParseFn<T>>,
+    infix: Option<ParseFn<T>>,
+    precedence: Precedence,
+}
+
+impl<T: Iterator<Item = Token>> ParseRule<T> {
+    fn new(prefix: Option<ParseFn<T>>, infix: Option<ParseFn<T>>, precedence: Precedence) -> Self {
+        ParseRule {
+            prefix,
+            infix,
+            precedence,
+        }
+    }
+}
+
+impl Precedence {
+    // Return the next highest precedence, if there is one.
+    fn next(&self) -> Self {
+        match self {
+            Precedence::None => Precedence::Assignment,
+            Precedence::Assignment => Precedence::Or,
+            Precedence::Or => Precedence::And,
+            Precedence::And => Precedence::Equality,
+            Precedence::Equality => Precedence::Comparison,
+            Precedence::Comparison => Precedence::Term,
+            Precedence::Term => Precedence::Factor,
+            Precedence::Factor => Precedence::Unary,
+            Precedence::Unary => Precedence::Call,
+            Precedence::Call => Precedence::Primary,
+            Precedence::Primary => {
+                panic!("invalid parser state: no higher precedence than Primary")
+            }
+        }
+    }
+}
+
+fn rule_for<T: Iterator<Item = Token>>(token: &TokenKind) -> ParseRule<T> {
+    match token {
+        TokenKind::LeftParen => ParseRule::new(Some(Compiler::grouping), None, Precedence::None),
+
+        TokenKind::Minus => ParseRule::new(
+            Some(Compiler::unary),
+            Some(Compiler::binary),
+            Precedence::Term,
+        ),
+
+        TokenKind::Plus => ParseRule::new(None, Some(Compiler::binary), Precedence::Term),
+
+        TokenKind::Slash => ParseRule::new(None, Some(Compiler::binary), Precedence::Factor),
+
+        TokenKind::Star => ParseRule::new(None, Some(Compiler::binary), Precedence::Factor),
+
+        TokenKind::Number(_) => ParseRule::new(Some(Compiler::number), None, Precedence::None),
+
+        TokenKind::True => ParseRule::new(Some(Compiler::literal), None, Precedence::None),
+
+        TokenKind::False => ParseRule::new(Some(Compiler::literal), None, Precedence::None),
+
+        TokenKind::Nil => ParseRule::new(Some(Compiler::literal), None, Precedence::None),
+
+        TokenKind::Bang => ParseRule::new(Some(Compiler::unary), None, Precedence::None),
+
+        TokenKind::BangEqual => ParseRule::new(None, Some(Compiler::binary), Precedence::Equality),
+
+        TokenKind::EqualEqual => ParseRule::new(None, Some(Compiler::binary), Precedence::Equality),
+
+        TokenKind::Greater => ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison),
+
+        TokenKind::GreaterEqual => {
+            ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison)
+        }
+
+        TokenKind::Less => ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison),
+
+        TokenKind::LessEqual => {
+            ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison)
+        }
+
+        TokenKind::Identifier(_) => {
+            ParseRule::new(Some(Compiler::variable), None, Precedence::None)
+        }
+
+        TokenKind::String(_) => ParseRule::new(Some(Compiler::string), None, Precedence::None),
+
+        _ => ParseRule::new(None, None, Precedence::None),
+    }
+}
+
+macro_rules! consume {
+    ( $self:ident, $expected:pat, $err:expr ) => {
+        match $self.current().kind {
+            $expected => $self.advance(),
+            _ => $self.error_at($self.current().line, $err),
+        }
+    };
+}
+
+impl<T: Iterator<Item = Token>> Compiler<T> {
+    fn compile(&mut self) -> LoxResult<()> {
+        self.advance();
+
+        while !self.match_token(&TokenKind::Eof) {
+            self.declaration()?;
+        }
+
+        self.end_compiler()
+    }
+
+    fn advance(&mut self) {
+        self.previous = self.current.take();
+        self.current = self.tokens.next();
+    }
+
+    fn expression(&mut self) -> LoxResult<()> {
+        self.parse_precedence(Precedence::Assignment)
+    }
+
+    fn var_declaration(&mut self) -> LoxResult<()> {
+        let idx = self.parse_variable()?;
+
+        if self.match_token(&TokenKind::Equal) {
+            self.expression()?;
+        } else {
+            self.emit_op(OpCode::OpNil);
+        }
+
+        self.expect_semicolon("expect ';' after variable declaration")?;
+        self.define_variable(idx)
+    }
+
+    fn define_variable(&mut self, var: Option<ConstantIdx>) -> LoxResult<()> {
+        if self.locals.scope_depth == 0 {
+            self.emit_op(OpCode::OpDefineGlobal(var.expect("should be global")));
+        } else {
+            self.locals
+                .locals
+                .last_mut()
+                .expect("fatal: variable not yet added at definition")
+                .depth = Depth::At(self.locals.scope_depth);
+        }
+
+        Ok(())
+    }
+
+    fn declaration(&mut self) -> LoxResult<()> {
+        if self.match_token(&TokenKind::Var) {
+            self.var_declaration()?;
+        } else {
+            self.statement()?;
+        }
+
+        if self.panic {
+            self.synchronise();
+        }
+
+        Ok(())
+    }
+
+    fn statement(&mut self) -> LoxResult<()> {
+        if self.match_token(&TokenKind::Print) {
+            self.print_statement()
+        } else if self.match_token(&TokenKind::If) {
+            self.if_statement()
+        } else if self.match_token(&TokenKind::LeftBrace) {
+            self.begin_scope();
+            self.block()?;
+            self.end_scope();
+            Ok(())
+        } else {
+            self.expression_statement()
+        }
+    }
+
+    fn print_statement(&mut self) -> LoxResult<()> {
+        self.expression()?;
+        self.expect_semicolon("expect ';' after print statement")?;
+        self.emit_op(OpCode::OpPrint);
+        Ok(())
+    }
+
+    fn begin_scope(&mut self) {
+        self.locals.scope_depth += 1;
+    }
+
+    fn end_scope(&mut self) {
+        debug_assert!(self.locals.scope_depth > 0, "tried to end global scope");
+        self.locals.scope_depth -= 1;
+
+        while self.locals.locals.len() > 0
+            && self.locals.locals[self.locals.locals.len() - 1]
+                .depth
+                .above(self.locals.scope_depth)
+        {
+            self.emit_op(OpCode::OpPop);
+            self.locals.locals.remove(self.locals.locals.len() - 1);
+        }
+    }
+
+    fn block(&mut self) -> LoxResult<()> {
+        while !self.check(&TokenKind::RightBrace) && !self.check(&TokenKind::Eof) {
+            self.declaration()?;
+        }
+
+        consume!(
+            self,
+            TokenKind::RightBrace,
+            ErrorKind::ExpectedToken("Expected '}' after block.")
+        );
+        Ok(())
+    }
+
+    fn expression_statement(&mut self) -> LoxResult<()> {
+        self.expression()?;
+        self.expect_semicolon("expect ';' after expression")?;
+        // TODO(tazjin): Why did I add this originally?
+        // self.emit_op(OpCode::OpPop);
+        Ok(())
+    }
+
+    fn if_statement(&mut self) -> LoxResult<()> {
+        consume!(
+            self,
+            TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'if'")
+        );
+
+        self.expression()?;
+
+        consume!(
+            self,
+            TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after condition")
+        );
+
+        let then_jump = self.emit_op(OpCode::OpJumpPlaceholder(false));
+        self.emit_op(OpCode::OpPop);
+        self.statement()?;
+        let else_jump = self.emit_op(OpCode::OpJumpPlaceholder(true));
+        self.patch_jump(then_jump);
+        self.emit_op(OpCode::OpPop);
+
+        if self.match_token(&TokenKind::Else) {
+            self.statement()?;
+        }
+
+        self.patch_jump(else_jump);
+
+        Ok(())
+    }
+
+    fn number(&mut self) -> LoxResult<()> {
+        if let TokenKind::Number(num) = self.previous().kind {
+            self.emit_constant(Value::Number(num), true);
+            return Ok(());
+        }
+
+        unreachable!("internal parser error: entered number() incorrectly")
+    }
+
+    fn grouping(&mut self) -> LoxResult<()> {
+        self.expression()?;
+        consume!(
+            self,
+            TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after expression")
+        );
+        Ok(())
+    }
+
+    fn unary(&mut self) -> LoxResult<()> {
+        // TODO(tazjin): Avoid clone
+        let kind = self.previous().kind.clone();
+
+        // Compile the operand
+        self.parse_precedence(Precedence::Unary)?;
+
+        // Emit operator instruction
+        match kind {
+            TokenKind::Bang => self.emit_op(OpCode::OpNot),
+            TokenKind::Minus => self.emit_op(OpCode::OpNegate),
+            _ => unreachable!("only called for unary operator tokens"),
+        };
+
+        Ok(())
+    }
+
+    fn binary(&mut self) -> LoxResult<()> {
+        // Remember the operator
+        let operator = self.previous().kind.clone();
+
+        // Compile the right operand
+        let rule: ParseRule<T> = rule_for(&operator);
+        self.parse_precedence(rule.precedence.next())?;
+
+        // Emit operator instruction
+        match operator {
+            TokenKind::Minus => self.emit_op(OpCode::OpSubtract),
+            TokenKind::Plus => self.emit_op(OpCode::OpAdd),
+            TokenKind::Star => self.emit_op(OpCode::OpMultiply),
+            TokenKind::Slash => self.emit_op(OpCode::OpDivide),
+
+            TokenKind::BangEqual => {
+                self.emit_op(OpCode::OpEqual);
+                self.emit_op(OpCode::OpNot)
+            }
+
+            TokenKind::EqualEqual => self.emit_op(OpCode::OpEqual),
+            TokenKind::Greater => self.emit_op(OpCode::OpGreater),
+
+            TokenKind::GreaterEqual => {
+                self.emit_op(OpCode::OpLess);
+                self.emit_op(OpCode::OpNot)
+            }
+
+            TokenKind::Less => self.emit_op(OpCode::OpLess),
+            TokenKind::LessEqual => {
+                self.emit_op(OpCode::OpGreater);
+                self.emit_op(OpCode::OpNot)
+            }
+
+            _ => unreachable!("only called for binary operator tokens"),
+        };
+
+        Ok(())
+    }
+
+    fn literal(&mut self) -> LoxResult<()> {
+        match self.previous().kind {
+            TokenKind::Nil => self.emit_op(OpCode::OpNil),
+            TokenKind::True => self.emit_op(OpCode::OpTrue),
+            TokenKind::False => self.emit_op(OpCode::OpFalse),
+            _ => unreachable!("only called for literal value tokens"),
+        };
+
+        Ok(())
+    }
+
+    fn string(&mut self) -> LoxResult<()> {
+        let val = match &self.previous().kind {
+            TokenKind::String(s) => s.clone(),
+            _ => unreachable!("only called for strings"),
+        };
+
+        let id = self.strings.intern(val);
+        self.emit_constant(Value::String(id.into()), true);
+
+        Ok(())
+    }
+
+    fn named_variable(&mut self, name: Token) -> LoxResult<()> {
+        let local_idx = self.resolve_local(&name);
+
+        let ident = if local_idx.is_some() {
+            None
+        } else {
+            Some(self.identifier_constant(&name)?)
+        };
+
+        if self.match_token(&TokenKind::Equal) {
+            self.expression()?;
+            match local_idx {
+                Some(idx) => self.emit_op(OpCode::OpSetLocal(idx)),
+                None => self.emit_op(OpCode::OpSetGlobal(ident.unwrap())),
+            };
+        } else {
+            match local_idx {
+                Some(idx) => self.emit_op(OpCode::OpGetLocal(idx)),
+                None => self.emit_op(OpCode::OpGetGlobal(ident.unwrap())),
+            };
+        }
+
+        Ok(())
+    }
+
+    fn variable(&mut self) -> LoxResult<()> {
+        let name = self.previous().clone();
+        self.named_variable(name)
+    }
+
+    fn parse_precedence(&mut self, precedence: Precedence) -> LoxResult<()> {
+        self.advance();
+        let rule: ParseRule<T> = rule_for(&self.previous().kind);
+        let prefix_fn = match rule.prefix {
+            None => unimplemented!("expected expression or something, unclear"),
+            Some(func) => func,
+        };
+
+        prefix_fn(self)?;
+
+        while precedence <= rule_for::<T>(&self.current().kind).precedence {
+            self.advance();
+            match rule_for::<T>(&self.previous().kind).infix {
+                Some(func) => {
+                    func(self)?;
+                }
+                None => {
+                    unreachable!("invalid compiler state: error in parse rules")
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn identifier_str(&mut self, token: &Token) -> LoxResult<InternedStr> {
+        let ident = match &token.kind {
+            TokenKind::Identifier(ident) => ident.to_string(),
+            _ => {
+                return Err(Error {
+                    line: self.current().line,
+                    kind: ErrorKind::ExpectedToken("Expected identifier"),
+                })
+            }
+        };
+
+        Ok(self.strings.intern(ident))
+    }
+
+    fn identifier_constant(&mut self, name: &Token) -> LoxResult<ConstantIdx> {
+        let ident = self.identifier_str(name)?;
+        Ok(self.emit_constant(Value::String(ident.into()), false))
+    }
+
+    fn resolve_local(&self, name: &Token) -> Option<StackIdx> {
+        for (idx, local) in self.locals.locals.iter().enumerate().rev() {
+            if name.lexeme == local.name.lexeme {
+                if let Depth::Unitialised = local.depth {
+                    // TODO(tazjin): *return* err
+                    panic!("can't read variable in its own initialiser");
+                }
+                return Some(StackIdx(idx));
+            }
+        }
+
+        None
+    }
+
+    fn add_local(&mut self, name: Token) {
+        let local = Local {
+            name,
+            depth: Depth::Unitialised,
+        };
+
+        self.locals.locals.push(local);
+    }
+
+    fn declare_variable(&mut self) -> LoxResult<()> {
+        if self.locals.scope_depth == 0 {
+            return Ok(());
+        }
+
+        let name = self.previous().clone();
+
+        for local in self.locals.locals.iter().rev() {
+            if local.depth.below(self.locals.scope_depth) {
+                break;
+            }
+
+            if name.lexeme == local.name.lexeme {
+                return Err(Error {
+                    kind: ErrorKind::VariableShadowed(name.lexeme.into()),
+                    line: name.line,
+                });
+            }
+        }
+
+        self.add_local(name);
+        Ok(())
+    }
+
+    fn parse_variable(&mut self) -> LoxResult<Option<ConstantIdx>> {
+        consume!(
+            self,
+            TokenKind::Identifier(_),
+            ErrorKind::ExpectedToken("expected identifier")
+        );
+
+        self.declare_variable()?;
+        if self.locals.scope_depth > 0 {
+            return Ok(None);
+        }
+
+        let name = self.previous().clone();
+        let id = self.identifier_str(&name)?;
+        Ok(Some(self.emit_constant(Value::String(id.into()), false)))
+    }
+
+    fn current_chunk(&mut self) -> &mut Chunk {
+        &mut self.chunk
+    }
+
+    fn end_compiler(&mut self) -> LoxResult<()> {
+        self.emit_op(OpCode::OpReturn);
+
+        #[cfg(feature = "disassemble")]
+        {
+            chunk::disassemble_chunk(&self.chunk);
+            println!("== compilation finished ==");
+        }
+
+        Ok(())
+    }
+
+    fn emit_op(&mut self, op: OpCode) -> CodeIdx {
+        let line = self.previous().line;
+        self.current_chunk().add_op(op, line)
+    }
+
+    fn emit_constant(&mut self, val: Value, with_op: bool) -> ConstantIdx {
+        let idx = ConstantIdx(self.chunk.add_constant(val));
+
+        if with_op {
+            self.emit_op(OpCode::OpConstant(idx));
+        }
+
+        idx
+    }
+
+    fn patch_jump(&mut self, idx: CodeIdx) {
+        let offset = CodeOffset(self.chunk.code.len() - idx.0 - 1);
+
+        if let OpCode::OpJumpPlaceholder(true) = self.chunk.code[idx.0] {
+            self.chunk.code[idx.0] = OpCode::OpJump(offset);
+            return;
+        }
+
+        if let OpCode::OpJumpPlaceholder(false) = self.chunk.code[idx.0] {
+            self.chunk.code[idx.0] = OpCode::OpJumpIfFalse(offset);
+            return;
+        }
+
+        panic!(
+            "attempted to patch unsupported op: {:?}",
+            self.chunk.code[idx.0]
+        );
+    }
+
+    fn previous(&self) -> &Token {
+        self.previous
+            .as_ref()
+            .expect("invalid internal compiler state: missing previous token")
+    }
+
+    fn current(&self) -> &Token {
+        self.current
+            .as_ref()
+            .expect("invalid internal compiler state: missing current token")
+    }
+
+    fn error_at(&mut self, line: usize, kind: ErrorKind) {
+        if self.panic {
+            return;
+        }
+
+        self.panic = true;
+        self.errors.push(Error { kind, line })
+    }
+
+    fn match_token(&mut self, token: &TokenKind) -> bool {
+        if !self.check(token) {
+            return false;
+        }
+
+        self.advance();
+        true
+    }
+
+    fn check(&self, token: &TokenKind) -> bool {
+        return self.current().kind == *token;
+    }
+
+    fn synchronise(&mut self) {
+        self.panic = false;
+
+        while self.current().kind != TokenKind::Eof {
+            if self.previous().kind == TokenKind::Semicolon {
+                return;
+            }
+
+            match self.current().kind {
+                TokenKind::Class
+                | TokenKind::Fun
+                | TokenKind::Var
+                | TokenKind::For
+                | TokenKind::If
+                | TokenKind::While
+                | TokenKind::Print
+                | TokenKind::Return => return,
+
+                _ => {
+                    self.advance();
+                }
+            }
+        }
+    }
+
+    fn expect_semicolon(&mut self, msg: &'static str) -> LoxResult<()> {
+        consume!(self, TokenKind::Semicolon, ErrorKind::ExpectedToken(msg));
+        Ok(())
+    }
+}
+
+pub fn compile(code: &str) -> Result<(Interner, Chunk), Vec<Error>> {
+    let chars = code.chars().collect::<Vec<char>>();
+    let tokens = scanner::scan(&chars)
+        .map_err(|errors| errors.into_iter().map(Into::into).collect::<Vec<Error>>())?;
+
+    let mut compiler = Compiler {
+        tokens: tokens.into_iter().peekable(),
+        chunk: Default::default(),
+        panic: false,
+        errors: vec![],
+        strings: Interner::with_capacity(1024),
+        locals: Default::default(),
+        current: None,
+        previous: None,
+    };
+
+    compiler.compile()?;
+
+    if compiler.errors.is_empty() {
+        Ok((compiler.strings, compiler.chunk))
+    } else {
+        Err(compiler.errors)
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/errors.rs b/users/tazjin/rlox/src/bytecode/errors.rs
new file mode 100644
index 0000000000..988031f763
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/errors.rs
@@ -0,0 +1,51 @@
+use crate::scanner::ScannerError;
+
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ErrorKind {
+    UnexpectedChar(char),
+    UnterminatedString,
+    ExpectedToken(&'static str),
+    InternalError(&'static str),
+    TypeError(String),
+    VariableShadowed(String),
+}
+
+#[derive(Debug)]
+pub struct Error {
+    pub kind: ErrorKind,
+    pub line: usize,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "[line NYI] Error: {:?}", self.kind)
+    }
+}
+
+impl From<ScannerError> for Error {
+    fn from(err: ScannerError) -> Self {
+        match err {
+            ScannerError::UnexpectedChar { line, unexpected } => Error {
+                line,
+                kind: ErrorKind::UnexpectedChar(unexpected),
+            },
+
+            ScannerError::UnterminatedString { line } => Error {
+                line,
+                kind: ErrorKind::UnterminatedString,
+            },
+        }
+    }
+}
+
+// Convenience implementation as we're often dealing with vectors of
+// errors (to report as many issues as possible before terminating)
+impl From<Error> for Vec<Error> {
+    fn from(err: Error) -> Self {
+        vec![err]
+    }
+}
+
+pub type LoxResult<T> = Result<T, Error>;
diff --git a/users/tazjin/rlox/src/bytecode/interner/mod.rs b/users/tazjin/rlox/src/bytecode/interner/mod.rs
new file mode 100644
index 0000000000..1da1a24b2c
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/interner/mod.rs
@@ -0,0 +1,87 @@
+//! String-interning implementation for values that are likely to
+//! benefit from fast comparisons and deduplication (e.g. instances of
+//! variable names).
+//!
+//! This uses a trick from the typed-arena crate for guaranteeing
+//! stable addresses by never resizing the existing String buffer, and
+//! collecting full buffers in a vector.
+
+use std::collections::HashMap;
+
+#[cfg(test)]
+mod tests;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct InternedStr {
+    id: usize,
+}
+
+#[derive(Default)]
+pub struct Interner {
+    map: HashMap<&'static str, InternedStr>,
+    vec: Vec<&'static str>,
+    buf: String,
+    full: Vec<String>,
+}
+
+impl Interner {
+    pub fn with_capacity(cap: usize) -> Self {
+        Interner {
+            buf: String::with_capacity(cap),
+            ..Default::default()
+        }
+    }
+
+    pub fn intern<S: AsRef<str>>(&mut self, name: S) -> InternedStr {
+        let name = name.as_ref();
+        if let Some(&id) = self.map.get(name) {
+            return id;
+        }
+
+        let name = self.alloc(name);
+        let id = InternedStr {
+            id: self.vec.len() as usize,
+        };
+
+        self.map.insert(name, id);
+        self.vec.push(name);
+
+        debug_assert!(self.lookup(id) == name);
+        debug_assert!(self.intern(name) == id);
+
+        id
+    }
+
+    pub fn lookup<'a>(&'a self, id: InternedStr) -> &'a str {
+        self.vec[id.id]
+    }
+
+    fn alloc<'a>(&'a mut self, name: &str) -> &'static str {
+        let cap = self.buf.capacity();
+        if cap < self.buf.len() + name.len() {
+            let new_cap = (cap.max(name.len()) + 1).next_power_of_two();
+            let new_buf = String::with_capacity(new_cap);
+            let old_buf = std::mem::replace(&mut self.buf, new_buf);
+            self.full.push(old_buf);
+        }
+
+        let interned: &'a str = {
+            let start = self.buf.len();
+            self.buf.push_str(name);
+            &self.buf[start..]
+        };
+
+        unsafe {
+            // This is sound for two reasons:
+            //
+            // 1. This function (Interner::alloc) is private, which
+            //    prevents users from allocating a supposedly static
+            //    reference.
+            //
+            // 2. Interner::lookup explicitly shortens the lifetime of
+            //    references that are handed out to that of the
+            //    reference to self.
+            return &*(interned as *const str);
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/interner/tests.rs b/users/tazjin/rlox/src/bytecode/interner/tests.rs
new file mode 100644
index 0000000000..b34bf68353
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/interner/tests.rs
@@ -0,0 +1,24 @@
+use super::*;
+
+#[test]
+fn interns_strings() {
+    let mut interner = Interner::with_capacity(128);
+    let id = interner.intern("hello world");
+    assert_eq!("hello world", interner.lookup(id));
+}
+
+#[test]
+fn deduplicates_strings() {
+    let mut interner = Interner::with_capacity(128);
+    let id_1 = interner.intern("hello world");
+    let id_2 = interner.intern("hello world");
+    assert_eq!(id_1, id_2);
+}
+
+#[test]
+fn ids_survive_growing() {
+    let mut interner = Interner::with_capacity(16);
+    let id = interner.intern("hello");
+    interner.intern("excessively large string that will cause eallocation");
+    assert_eq!("hello", interner.lookup(id));
+}
diff --git a/users/tazjin/rlox/src/bytecode/mod.rs b/users/tazjin/rlox/src/bytecode/mod.rs
new file mode 100644
index 0000000000..117f17824a
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/mod.rs
@@ -0,0 +1,30 @@
+//! Bytecode interpreter for Lox.
+//!
+//! https://craftinginterpreters.com/chunks-of-bytecode.html
+
+mod chunk;
+mod compiler;
+mod errors;
+mod interner;
+mod opcode;
+mod value;
+mod vm;
+
+#[cfg(test)]
+mod tests;
+
+pub struct Interpreter {}
+
+impl crate::Lox for Interpreter {
+    type Error = errors::Error;
+    type Value = value::Value;
+
+    fn create() -> Self {
+        Interpreter {}
+    }
+
+    fn interpret(&mut self, code: String) -> Result<Self::Value, Vec<Self::Error>> {
+        let (strings, chunk) = compiler::compile(&code)?;
+        vm::interpret(strings, chunk).map_err(|e| vec![e])
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/opcode.rs b/users/tazjin/rlox/src/bytecode/opcode.rs
new file mode 100644
index 0000000000..8a106f9691
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/opcode.rs
@@ -0,0 +1,56 @@
+#[derive(Clone, Copy, Debug)]
+pub struct ConstantIdx(pub usize);
+
+#[derive(Clone, Copy, Debug)]
+pub struct StackIdx(pub usize);
+
+#[derive(Clone, Copy, Debug)]
+pub struct CodeIdx(pub usize);
+
+#[derive(Clone, Copy, Debug)]
+pub struct CodeOffset(pub usize);
+
+#[derive(Debug)]
+pub enum OpCode {
+    /// Push a constant onto the stack.
+    OpConstant(ConstantIdx),
+
+    // Literal pushes
+    OpNil,
+    OpTrue,
+    OpFalse,
+
+    /// Return from the current function.
+    OpReturn,
+
+    // Boolean & comparison operators
+    OpNot,
+    OpEqual,
+    OpGreater,
+    OpLess,
+
+    /// Unary negation
+    OpNegate,
+
+    // Arithmetic operators
+    OpAdd,
+    OpSubtract,
+    OpMultiply,
+    OpDivide,
+
+    // Built in operations
+    OpPrint,
+    OpPop,
+
+    // Variable management
+    OpDefineGlobal(ConstantIdx),
+    OpGetGlobal(ConstantIdx),
+    OpSetGlobal(ConstantIdx),
+    OpGetLocal(StackIdx),
+    OpSetLocal(StackIdx),
+
+    // Control flow
+    OpJumpPlaceholder(bool),
+    OpJump(CodeOffset),
+    OpJumpIfFalse(CodeOffset),
+}
diff --git a/users/tazjin/rlox/src/bytecode/tests.rs b/users/tazjin/rlox/src/bytecode/tests.rs
new file mode 100644
index 0000000000..bc7d6cb878
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/tests.rs
@@ -0,0 +1,152 @@
+use super::value::Value;
+use super::*;
+
+use crate::Lox;
+
+fn expect(code: &str, value: Value) {
+    let result = Interpreter::create()
+        .interpret(code.into())
+        .expect("evaluation failed");
+    assert_eq!(result, value);
+}
+
+fn expect_num(code: &str, value: f64) {
+    expect(code, Value::Number(value))
+}
+
+fn expect_bool(code: &str, value: bool) {
+    expect(code, Value::Bool(value))
+}
+
+fn expect_str(code: &str, value: &str) {
+    expect(code, Value::String(value.to_string().into()))
+}
+
+#[test]
+fn numbers() {
+    expect_num("1;", 1.0);
+    expect_num("13.37;", 13.37);
+}
+
+#[test]
+fn negative_numbers() {
+    // Note: This technically tests unary operators.
+    expect_num("-1;", -1.0);
+    expect_num("-13.37;", -13.37);
+}
+
+#[test]
+fn terms() {
+    expect_num("1 + 2;", 3.0);
+    expect_num("3 - 1;", 2.0);
+    expect_num("0.7 + 0.3;", 1.0);
+    expect_num("1 + -3;", -2.0);
+    expect_num("-1 - -1;", 0.0);
+    expect_num("10 - -10 + 10;", 30.0);
+}
+
+#[test]
+fn factors() {
+    expect_num("1 * 2;", 2.0);
+    expect_num("10 / 5;", 2.0);
+    expect_num("0.7 * 4 / 1.4;", 2.0);
+    expect_num("10 * -10 / 10;", -10.0);
+}
+
+#[test]
+fn arithmetic() {
+    expect_num("10 - 3 * 2;", 4.0);
+    expect_num("-4 * -4 + (14 - 5);", 25.0);
+    expect_num("(702 + 408) - ((239 - 734) / -5) + -4;", 1007.0);
+}
+
+#[test]
+fn trivial_literals() {
+    expect("true;", Value::Bool(true));
+    expect("false;", Value::Bool(false));
+    expect("nil;", Value::Nil);
+}
+
+#[test]
+fn negation() {
+    expect_bool("!true;", false);
+    expect_bool("!false;", true);
+    expect_bool("!nil;", true);
+    expect_bool("!13.5;", false);
+    expect_bool("!-42;", false);
+}
+
+#[test]
+fn equality() {
+    expect_bool("42 == 42;", true);
+    expect_bool("42 != 42;", false);
+    expect_bool("42 == 42.0;", true);
+
+    expect_bool("true == true;", true);
+    expect_bool("true == false;", false);
+    expect_bool("true == !false;", true);
+    expect_bool("true != true;", false);
+    expect_bool("true != false;", true);
+
+    expect_bool("42 == false;", false);
+    expect_bool("42 == true;", false);
+    expect_bool("!42 == !true;", true);
+}
+
+#[test]
+fn comparisons() {
+    expect_bool("42 > 23;", true);
+    expect_bool("42 < 23;", false);
+    expect_bool("42 <= 42;", true);
+    expect_bool("42 <= 23;", false);
+    expect_bool("42 >= 42;", true);
+    expect_bool("42 >= 23;", true);
+}
+
+#[test]
+fn strings() {
+    expect_str("\"hello\";", "hello");
+    expect_str("\"hello\" + \" world\";", "hello world");
+}
+
+#[test]
+fn global_variables() {
+    expect_num("var a = 5; a;", 5.0);
+    expect_num("var a = 5; var b = 2; a * b;", 10.0);
+    expect_str(
+        "var greeting = \"hello\"; var name = \"Zubnog\"; greeting + \" \" + name;",
+        "hello Zubnog",
+    );
+}
+
+#[test]
+fn global_assignment() {
+    expect_str(
+        r#"
+          var breakfast = "beignets";
+          var beverage = "cafe au lait";
+          breakfast = "beignets with " + beverage;
+          breakfast;
+        "#,
+        "beignets with cafe au lait",
+    );
+}
+
+#[test]
+fn local_variables() {
+    expect_num(
+        r#"
+          var a = 10;
+          var b = 5;
+          var result = 0;
+          {
+            var b = 10;
+            var c = 2;
+            result = a * b * c;
+          }
+
+          result;
+        "#,
+        200.0,
+    );
+}
diff --git a/users/tazjin/rlox/src/bytecode/value.rs b/users/tazjin/rlox/src/bytecode/value.rs
new file mode 100644
index 0000000000..4170efadf8
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/value.rs
@@ -0,0 +1,37 @@
+use super::interner::InternedStr;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Value {
+    Nil,
+    Bool(bool),
+    Number(f64),
+    String(LoxString),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum LoxString {
+    Heap(String),
+    Interned(InternedStr),
+}
+
+impl From<String> for LoxString {
+    fn from(s: String) -> Self {
+        LoxString::Heap(s)
+    }
+}
+
+impl From<InternedStr> for LoxString {
+    fn from(s: InternedStr) -> Self {
+        LoxString::Interned(s)
+    }
+}
+
+impl Value {
+    pub fn is_falsey(&self) -> bool {
+        match self {
+            Value::Nil => true,
+            Value::Bool(false) => true,
+            _ => false,
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/vm.rs b/users/tazjin/rlox/src/bytecode/vm.rs
new file mode 100644
index 0000000000..30ffebc79c
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/vm.rs
@@ -0,0 +1,272 @@
+use std::collections::HashMap;
+
+use super::chunk;
+use super::errors::*;
+use super::interner::Interner;
+use super::opcode::OpCode;
+use super::value::{LoxString, Value};
+
+pub struct VM {
+    chunk: chunk::Chunk,
+
+    // TODO(tazjin): Accessing array elements constantly is not ideal,
+    // lets see if something clever can be done with iterators.
+    ip: usize,
+
+    stack: Vec<Value>,
+    strings: Interner,
+
+    globals: HashMap<LoxString, Value>,
+
+    // Operations that consume values from the stack without pushing
+    // anything leave their last value in this slot, which makes it
+    // possible to return values from interpreters that ran code which
+    // ended with a statement.
+    last_drop: Option<Value>,
+}
+
+impl VM {
+    fn push(&mut self, value: Value) {
+        self.stack.push(value)
+    }
+
+    fn pop(&mut self) -> Value {
+        self.stack.pop().expect("fatal error: stack empty!")
+    }
+}
+
+macro_rules! with_type {
+    ( $self:ident, $val:ident, $type:pat, $body:expr ) => {
+        match $val {
+            $type => $body,
+            _ => {
+                return Err(Error {
+                    line: $self.chunk.get_line($self.ip - 1),
+                    kind: ErrorKind::TypeError(format!(
+                        "Expected type {}, but found value: {:?}",
+                        stringify!($type),
+                        $val,
+                    )),
+                })
+            }
+        }
+    };
+}
+
+macro_rules! binary_op {
+    ( $vm:ident, $type:tt, $op:tt ) => {
+        binary_op!($vm, $type, $type, $op)
+    };
+
+    ( $vm:ident, $in_type:tt, $out_type:tt, $op:tt ) => {{
+        let b = $vm.pop();
+        let a = $vm.pop();
+
+        with_type!($vm, b, Value::$in_type(val_b), {
+            with_type!($vm, a, Value::$in_type(val_a), {
+                $vm.push(Value::$out_type(val_a $op val_b))
+            })
+        })
+    }};
+}
+
+impl VM {
+    fn run(&mut self) -> LoxResult<Value> {
+        loop {
+            let op = &self.chunk.code[self.ip];
+
+            #[cfg(feature = "disassemble")]
+            chunk::disassemble_instruction(&self.chunk, self.ip);
+
+            self.ip += 1;
+
+            match op {
+                OpCode::OpReturn => {
+                    if !self.stack.is_empty() {
+                        let val = self.pop();
+                        return Ok(self.return_value(val));
+                    } else if self.last_drop.is_some() {
+                        let val = self.last_drop.take().unwrap();
+                        return Ok(self.return_value(val));
+                    } else {
+                        return Ok(Value::Nil);
+                    }
+                }
+
+                OpCode::OpConstant(idx) => {
+                    let c = self.chunk.constant(*idx).clone();
+                    self.push(c);
+                }
+
+                OpCode::OpNil => self.push(Value::Nil),
+                OpCode::OpTrue => self.push(Value::Bool(true)),
+                OpCode::OpFalse => self.push(Value::Bool(false)),
+
+                OpCode::OpNot => {
+                    let v = self.pop();
+                    self.push(Value::Bool(v.is_falsey()));
+                }
+
+                OpCode::OpEqual => {
+                    let b = self.pop();
+                    let a = self.pop();
+                    self.push(Value::Bool(a == b));
+                }
+
+                OpCode::OpLess => binary_op!(self, Number, Bool, <),
+                OpCode::OpGreater => binary_op!(self, Number, Bool, >),
+
+                OpCode::OpNegate => {
+                    let v = self.pop();
+                    with_type!(self, v, Value::Number(num), self.push(Value::Number(-num)));
+                }
+
+                OpCode::OpSubtract => binary_op!(self, Number, -),
+                OpCode::OpMultiply => binary_op!(self, Number, *),
+                OpCode::OpDivide => binary_op!(self, Number, /),
+
+                OpCode::OpAdd => {
+                    let b = self.pop();
+                    let a = self.pop();
+
+                    match (a, b) {
+                        (Value::String(s_a), Value::String(s_b)) => {
+                            let mut new_s = self.resolve_str(&s_a).to_string();
+                            new_s.push_str(self.resolve_str(&s_b));
+                            self.push(Value::String(new_s.into()));
+                        }
+
+                        (Value::Number(n_a), Value::Number(n_b)) => {
+                            self.push(Value::Number(n_a + n_b))
+                        }
+
+                        _ => {
+                            return Err(Error {
+                                line: self.chunk.get_line(self.ip - 1),
+                                kind: ErrorKind::TypeError(
+                                    "'+' operator only works on strings and numbers".into(),
+                                ),
+                            })
+                        }
+                    }
+                }
+
+                OpCode::OpPrint => {
+                    let val = self.pop();
+                    println!("{}", self.print_value(val));
+                }
+
+                OpCode::OpPop => {
+                    self.last_drop = Some(self.pop());
+                }
+
+                OpCode::OpDefineGlobal(name_idx) => {
+                    let name = self.chunk.constant(*name_idx);
+                    with_type!(self, name, Value::String(name), {
+                        let name = name.clone();
+                        let val = self.pop();
+                        self.globals.insert(name, val);
+                    });
+                }
+
+                OpCode::OpGetGlobal(name_idx) => {
+                    let name = self.chunk.constant(*name_idx);
+                    with_type!(self, name, Value::String(name), {
+                        let val = match self.globals.get(name) {
+                            None => unimplemented!("variable not found error"),
+                            Some(val) => val.clone(),
+                        };
+                        self.push(val)
+                    });
+                }
+
+                OpCode::OpSetGlobal(name_idx) => {
+                    let name = self.chunk.constant(*name_idx).clone();
+                    let new_val = self.pop();
+                    with_type!(self, name, Value::String(name), {
+                        match self.globals.get_mut(&name) {
+                            None => unimplemented!("variable not found error"),
+                            Some(val) => {
+                                *val = new_val;
+                            }
+                        }
+                    });
+                }
+
+                OpCode::OpGetLocal(local_idx) => {
+                    let value = self.stack[local_idx.0].clone();
+                    self.push(value);
+                }
+
+                OpCode::OpSetLocal(local_idx) => {
+                    debug_assert!(
+                        self.stack.len() > local_idx.0,
+                        "stack is not currently large enough for local"
+                    );
+                    self.stack[local_idx.0] = self.stack.last().unwrap().clone();
+                }
+
+                OpCode::OpJumpPlaceholder(_) => {
+                    panic!("unpatched jump detected - this is a fatal compiler error!");
+                }
+
+                OpCode::OpJump(offset) => {
+                    self.ip += offset.0;
+                }
+
+                OpCode::OpJumpIfFalse(offset) => {
+                    if self
+                        .stack
+                        .last()
+                        .expect("condition should leave a value on the stack")
+                        .is_falsey()
+                    {
+                        self.ip += offset.0;
+                    }
+                }
+            }
+
+            #[cfg(feature = "disassemble")]
+            println!("=> {:?}", self.stack);
+        }
+    }
+
+    // For some types of values (e.g. interned strings), returns
+    // should no longer include any references into the interpreter.
+    fn return_value(&self, val: Value) -> Value {
+        match val {
+            Value::String(string @ LoxString::Interned(_)) => {
+                Value::String(self.resolve_str(&string).to_string().into())
+            }
+            _ => val,
+        }
+    }
+
+    fn resolve_str<'a>(&'a self, string: &'a LoxString) -> &'a str {
+        match string {
+            LoxString::Heap(s) => s.as_str(),
+            LoxString::Interned(id) => self.strings.lookup(*id),
+        }
+    }
+
+    fn print_value(&self, val: Value) -> String {
+        match val {
+            Value::String(LoxString::Heap(s)) => s,
+            Value::String(LoxString::Interned(id)) => self.strings.lookup(id).into(),
+            _ => format!("{:?}", val),
+        }
+    }
+}
+
+pub fn interpret(strings: Interner, chunk: chunk::Chunk) -> LoxResult<Value> {
+    let mut vm = VM {
+        chunk,
+        strings,
+        globals: HashMap::new(),
+        ip: 0,
+        stack: vec![],
+        last_drop: None,
+    };
+
+    vm.run()
+}
diff --git a/users/tazjin/rlox/src/main.rs b/users/tazjin/rlox/src/main.rs
new file mode 100644
index 0000000000..ee61ae01a1
--- /dev/null
+++ b/users/tazjin/rlox/src/main.rs
@@ -0,0 +1,71 @@
+use std::io::Write;
+use std::{env, fs, io, process};
+
+mod bytecode;
+mod scanner;
+mod treewalk;
+
+/// Trait for making the different interpreters callable in the same
+/// way.
+pub trait Lox {
+    type Value: std::fmt::Debug;
+    type Error: std::fmt::Display;
+
+    fn create() -> Self;
+    fn interpret(&mut self, source: String) -> Result<Self::Value, Vec<Self::Error>>;
+}
+
+fn main() {
+    let mut args = env::args();
+    if args.len() > 2 {
+        println!("Usage: rlox [script]");
+        process::exit(1);
+    }
+
+    match env::var("LOX_INTERPRETER").as_ref().map(String::as_str) {
+        Ok("treewalk") => pick::<treewalk::interpreter::Interpreter>(args.nth(1)),
+        _ => pick::<bytecode::Interpreter>(args.nth(1)),
+    }
+}
+
+fn pick<I: Lox>(file_arg: Option<String>) {
+    if let Some(file) = file_arg {
+        run_file::<I>(&file);
+    } else {
+        run_prompt::<I>();
+    }
+}
+
+// Run Lox code from a file and print results to stdout
+fn run_file<I: Lox>(file: &str) {
+    let contents = fs::read_to_string(file).expect("failed to read the input file");
+    let mut lox = I::create();
+    run(&mut lox, contents);
+}
+
+// Evaluate Lox code interactively in a shitty REPL.
+fn run_prompt<I: Lox>() {
+    let mut line = String::new();
+    let mut lox = I::create();
+
+    loop {
+        print!("> ");
+        io::stdout().flush().unwrap();
+        io::stdin()
+            .read_line(&mut line)
+            .expect("failed to read user input");
+        run(&mut lox, std::mem::take(&mut line));
+        line.clear();
+    }
+}
+
+fn run<I: Lox>(lox: &mut I, code: String) {
+    match lox.interpret(code) {
+        Ok(result) => println!("=> {:?}", result),
+        Err(errors) => {
+            for error in errors {
+                eprintln!("{}", error);
+            }
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/scanner.rs b/users/tazjin/rlox/src/scanner.rs
new file mode 100644
index 0000000000..314b56d6d3
--- /dev/null
+++ b/users/tazjin/rlox/src/scanner.rs
@@ -0,0 +1,284 @@
+#[derive(Clone, Debug, PartialEq)]
+pub enum TokenKind {
+    // Single-character tokens.
+    LeftParen,
+    RightParen,
+    LeftBrace,
+    RightBrace,
+    Comma,
+    Dot,
+    Minus,
+    Plus,
+    Semicolon,
+    Slash,
+    Star,
+
+    // One or two character tokens.
+    Bang,
+    BangEqual,
+    Equal,
+    EqualEqual,
+    Greater,
+    GreaterEqual,
+    Less,
+    LessEqual,
+
+    // Literals.
+    Identifier(String),
+    String(String),
+    Number(f64),
+    True,
+    False,
+    Nil,
+
+    // Keywords.
+    And,
+    Class,
+    Else,
+    Fun,
+    For,
+    If,
+    Or,
+    Print,
+    Return,
+    Super,
+    This,
+    Var,
+    While,
+
+    // Special things
+    Eof,
+}
+
+#[derive(Clone, Debug)]
+pub struct Token {
+    pub kind: TokenKind,
+    pub lexeme: String,
+    pub line: usize,
+}
+
+pub enum ScannerError {
+    UnexpectedChar { line: usize, unexpected: char },
+    UnterminatedString { line: usize },
+}
+
+struct Scanner<'a> {
+    source: &'a [char],
+    tokens: Vec<Token>,
+    errors: Vec<ScannerError>,
+    start: usize,   // offset of first character in current lexeme
+    current: usize, // current offset into source
+    line: usize,    // current line in source
+}
+
+impl<'a> Scanner<'a> {
+    fn is_at_end(&self) -> bool {
+        return self.current >= self.source.len();
+    }
+
+    fn advance(&mut self) -> char {
+        self.current += 1;
+        self.source[self.current - 1]
+    }
+
+    fn add_token(&mut self, kind: TokenKind) {
+        let lexeme = &self.source[self.start..self.current];
+        self.tokens.push(Token {
+            kind,
+            lexeme: lexeme.into_iter().collect(),
+            line: self.line,
+        })
+    }
+
+    fn scan_token(&mut self) {
+        match self.advance() {
+            // simple single-character tokens
+            '(' => self.add_token(TokenKind::LeftParen),
+            ')' => self.add_token(TokenKind::RightParen),
+            '{' => self.add_token(TokenKind::LeftBrace),
+            '}' => self.add_token(TokenKind::RightBrace),
+            ',' => self.add_token(TokenKind::Comma),
+            '.' => self.add_token(TokenKind::Dot),
+            '-' => self.add_token(TokenKind::Minus),
+            '+' => self.add_token(TokenKind::Plus),
+            ';' => self.add_token(TokenKind::Semicolon),
+            '*' => self.add_token(TokenKind::Star),
+
+            // possible multi-character tokens
+            '!' => self.add_if_next('=', TokenKind::BangEqual, TokenKind::Bang),
+            '=' => self.add_if_next('=', TokenKind::EqualEqual, TokenKind::Equal),
+            '<' => self.add_if_next('=', TokenKind::LessEqual, TokenKind::Less),
+            '>' => self.add_if_next('=', TokenKind::GreaterEqual, TokenKind::Greater),
+
+            '/' => {
+                // support comments until EOL by discarding characters
+                if self.match_next('/') {
+                    while self.peek() != '\n' && !self.is_at_end() {
+                        self.advance();
+                    }
+                } else {
+                    self.add_token(TokenKind::Slash);
+                }
+            }
+
+            // ignore whitespace
+            ws if ws.is_whitespace() => {
+                if ws == '\n' {
+                    self.line += 1
+                }
+            }
+
+            '"' => self.scan_string(),
+
+            digit if digit.is_digit(10) => self.scan_number(),
+
+            chr if chr.is_alphabetic() || chr == '_' => self.scan_identifier(),
+
+            unexpected => self.errors.push(ScannerError::UnexpectedChar {
+                line: self.line,
+                unexpected,
+            }),
+        };
+    }
+
+    fn match_next(&mut self, expected: char) -> bool {
+        if self.is_at_end() || self.source[self.current] != expected {
+            false
+        } else {
+            self.current += 1;
+            true
+        }
+    }
+
+    fn add_if_next(&mut self, expected: char, then: TokenKind, or: TokenKind) {
+        if self.match_next(expected) {
+            self.add_token(then);
+        } else {
+            self.add_token(or);
+        }
+    }
+
+    fn peek(&self) -> char {
+        if self.is_at_end() {
+            return '\0';
+        } else {
+            return self.source[self.current];
+        }
+    }
+
+    fn peek_next(&self) -> char {
+        if self.current + 1 >= self.source.len() {
+            return '\0';
+        } else {
+            return self.source[self.current + 1];
+        }
+    }
+
+    fn scan_string(&mut self) {
+        while self.peek() != '"' && !self.is_at_end() {
+            if self.peek() == '\n' {
+                self.line += 1;
+            }
+
+            self.advance();
+        }
+
+        if self.is_at_end() {
+            self.errors
+                .push(ScannerError::UnterminatedString { line: self.line });
+            return;
+        }
+
+        // closing '"'
+        self.advance();
+
+        // add token without surrounding quotes
+        let string: String = self.source[(self.start + 1)..(self.current - 1)]
+            .iter()
+            .collect();
+        self.add_token(TokenKind::String(string));
+    }
+
+    fn scan_number(&mut self) {
+        while self.peek().is_digit(10) {
+            self.advance();
+        }
+
+        // Look for a fractional part
+        if self.peek() == '.' && self.peek_next().is_digit(10) {
+            // consume '.'
+            self.advance();
+
+            while self.peek().is_digit(10) {
+                self.advance();
+            }
+        }
+
+        let num: f64 = self.source[self.start..self.current]
+            .iter()
+            .collect::<String>()
+            .parse()
+            .expect("float parsing should always work");
+
+        self.add_token(TokenKind::Number(num));
+    }
+
+    fn scan_identifier(&mut self) {
+        while self.peek().is_alphanumeric() || self.peek() == '_' {
+            self.advance();
+        }
+
+        let ident: String = self.source[self.start..self.current].iter().collect();
+
+        // Determine whether this is an identifier, or a keyword:
+        let token_kind = match ident.as_str() {
+            "and" => TokenKind::And,
+            "class" => TokenKind::Class,
+            "else" => TokenKind::Else,
+            "false" => TokenKind::False,
+            "for" => TokenKind::For,
+            "fun" => TokenKind::Fun,
+            "if" => TokenKind::If,
+            "nil" => TokenKind::Nil,
+            "or" => TokenKind::Or,
+            "print" => TokenKind::Print,
+            "return" => TokenKind::Return,
+            "super" => TokenKind::Super,
+            "this" => TokenKind::This,
+            "true" => TokenKind::True,
+            "var" => TokenKind::Var,
+            "while" => TokenKind::While,
+            _ => TokenKind::Identifier(ident),
+        };
+
+        self.add_token(token_kind);
+    }
+
+    fn scan_tokens(&mut self) {
+        while !self.is_at_end() {
+            self.start = self.current;
+            self.scan_token();
+        }
+
+        self.add_token(TokenKind::Eof);
+    }
+}
+
+pub fn scan<'a>(input: &'a [char]) -> Result<Vec<Token>, Vec<ScannerError>> {
+    let mut scanner = Scanner {
+        source: &input,
+        tokens: vec![],
+        errors: vec![],
+        start: 0,
+        current: 0,
+        line: 0,
+    };
+
+    scanner.scan_tokens();
+
+    if !scanner.errors.is_empty() {
+        return Err(scanner.errors);
+    }
+
+    return Ok(scanner.tokens);
+}
diff --git a/users/tazjin/rlox/src/treewalk/errors.rs b/users/tazjin/rlox/src/treewalk/errors.rs
new file mode 100644
index 0000000000..391663d51b
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/errors.rs
@@ -0,0 +1,59 @@
+use crate::scanner::ScannerError;
+use crate::treewalk::interpreter::Value;
+
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ErrorKind {
+    UnexpectedChar(char),
+    UnterminatedString,
+    UnmatchedParens,
+    ExpectedExpression(String),
+    ExpectedSemicolon,
+    ExpectedClosingBrace,
+    ExpectedToken(&'static str),
+    TypeError(String),
+    UndefinedVariable(String),
+    InternalError(String),
+    InvalidAssignmentTarget(String),
+    RuntimeError(String),
+    StaticError(String),
+
+    // This variant is not an error, rather it is used for
+    // short-circuiting out of a function body that hits a `return`
+    // statement.
+    //
+    // It's implemented this way because in the original book the
+    // author uses exceptions for control flow, and this is the
+    // closest equivalent that I had available without diverging too
+    // much.
+    FunctionReturn(Value),
+}
+
+#[derive(Debug)]
+pub struct Error {
+    pub line: usize,
+    pub kind: ErrorKind,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "[line {}] Error: {:?}", self.line, self.kind)
+    }
+}
+
+impl From<ScannerError> for Error {
+    fn from(err: ScannerError) -> Self {
+        match err {
+            ScannerError::UnexpectedChar { line, unexpected } => Error {
+                line,
+                kind: ErrorKind::UnexpectedChar(unexpected),
+            },
+
+            ScannerError::UnterminatedString { line } => Error {
+                line,
+                kind: ErrorKind::UnterminatedString,
+            },
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/treewalk/interpreter.rs b/users/tazjin/rlox/src/treewalk/interpreter.rs
new file mode 100644
index 0000000000..3285775bbe
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/interpreter.rs
@@ -0,0 +1,498 @@
+use crate::treewalk::errors::{Error, ErrorKind};
+use crate::treewalk::parser::{self, Block, Expr, Literal, Statement};
+use crate::treewalk::resolver;
+use crate::treewalk::scanner::{self, TokenKind};
+use crate::Lox;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::sync::RwLock;
+
+// Implementation of built-in functions.
+mod builtins;
+
+#[cfg(test)]
+mod tests;
+
+// Tree-walk interpreter
+
+// Representation of all callables, including builtins & user-defined
+// functions.
+#[derive(Clone, Debug)]
+pub enum Callable {
+    Builtin(&'static dyn builtins::Builtin),
+    Function {
+        func: Rc<parser::Function>,
+        closure: Rc<RwLock<Environment>>,
+    },
+}
+
+impl Callable {
+    fn arity(&self) -> usize {
+        match self {
+            Callable::Builtin(builtin) => builtin.arity(),
+            Callable::Function { func, .. } => func.params.len(),
+        }
+    }
+
+    fn call(&self, lox: &mut Interpreter, args: Vec<Value>) -> Result<Value, Error> {
+        match self {
+            Callable::Builtin(builtin) => builtin.call(args),
+
+            Callable::Function { func, closure } => {
+                let mut fn_env: Environment = Default::default();
+                fn_env.enclosing = Some(closure.clone());
+
+                for (param, value) in func.params.iter().zip(args.into_iter()) {
+                    fn_env.define(param, value)?;
+                }
+
+                let result =
+                    lox.interpret_block_with_env(Some(Rc::new(RwLock::new(fn_env))), &func.body);
+
+                match result {
+                    // extract returned values if applicable
+                    Err(Error {
+                        kind: ErrorKind::FunctionReturn(value),
+                        ..
+                    }) => Ok(value),
+
+                    // otherwise just return the result itself
+                    _ => result,
+                }
+            }
+        }
+    }
+}
+
+// Representation of an in-language value.
+#[derive(Clone, Debug)]
+pub enum Value {
+    Literal(Literal),
+    Callable(Callable),
+}
+
+impl PartialEq for Value {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Value::Literal(lhs), Value::Literal(rhs)) => lhs == rhs,
+            // functions do not have equality
+            _ => false,
+        }
+    }
+}
+
+impl From<Literal> for Value {
+    fn from(lit: Literal) -> Value {
+        Value::Literal(lit)
+    }
+}
+
+impl Value {
+    fn expect_literal(self) -> Result<Literal, Error> {
+        match self {
+            Value::Literal(lit) => Ok(lit),
+            _ => unimplemented!(), // which error? which line?
+        }
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct Environment {
+    enclosing: Option<Rc<RwLock<Environment>>>,
+    values: HashMap<String, Value>,
+}
+
+impl Environment {
+    fn define(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        let ident = identifier_str(name)?;
+        self.values.insert(ident.into(), value);
+        Ok(())
+    }
+
+    fn get(&self, ident: &str, line: usize, depth: usize) -> Result<Value, Error> {
+        if depth > 0 {
+            match &self.enclosing {
+                None => {
+                    return Err(Error {
+                        line,
+                        kind: ErrorKind::InternalError(format!(
+                            "invalid depth {} for {}",
+                            depth, ident
+                        )),
+                    })
+                }
+                Some(parent) => {
+                    let env = parent.read().expect("fatal: environment lock poisoned");
+                    return env.get(ident, line, depth - 1);
+                }
+            }
+        }
+
+        self.values
+            .get(ident)
+            .map(Clone::clone)
+            .ok_or_else(|| Error {
+                line,
+                kind: ErrorKind::UndefinedVariable(ident.into()),
+            })
+    }
+
+    fn assign(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        let ident = identifier_str(name)?;
+
+        match self.values.get_mut(ident) {
+            Some(target) => {
+                *target = value;
+                Ok(())
+            }
+            None => {
+                if let Some(parent) = &self.enclosing {
+                    return parent.write().unwrap().assign(name, value);
+                }
+
+                Err(Error {
+                    line: name.line,
+                    kind: ErrorKind::UndefinedVariable(ident.into()),
+                })
+            }
+        }
+    }
+}
+
+fn identifier_str(name: &scanner::Token) -> Result<&str, Error> {
+    if let TokenKind::Identifier(ident) = &name.kind {
+        Ok(ident)
+    } else {
+        Err(Error {
+            line: name.line,
+            kind: ErrorKind::InternalError("unexpected identifier kind".into()),
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct Interpreter {
+    env: Rc<RwLock<Environment>>,
+}
+
+impl Lox for Interpreter {
+    type Value = Value;
+    type Error = Error;
+
+    /// Create a new interpreter and configure the initial global
+    /// variable set.
+    fn create() -> Self {
+        let mut globals = HashMap::new();
+
+        globals.insert(
+            "clock".into(),
+            Value::Callable(Callable::Builtin(&builtins::Clock {})),
+        );
+
+        Interpreter {
+            env: Rc::new(RwLock::new(Environment {
+                enclosing: None,
+                values: globals,
+            })),
+        }
+    }
+
+    fn interpret(&mut self, code: String) -> Result<Value, Vec<Error>> {
+        let chars: Vec<char> = code.chars().collect();
+
+        let mut program = scanner::scan(&chars)
+            .map_err(|errors| errors.into_iter().map(Into::into).collect())
+            .and_then(|tokens| parser::parse(tokens))?;
+
+        let globals = self
+            .env
+            .read()
+            .expect("static globals lock poisoned")
+            .values
+            .keys()
+            .map(Clone::clone)
+            .collect::<Vec<String>>();
+
+        resolver::resolve(&globals, &mut program).map_err(|e| vec![e])?;
+        self.interpret_block_with_env(None, &program)
+            .map_err(|e| vec![e])
+    }
+}
+
+impl Interpreter {
+    // Environment modification helpers
+    fn define_var(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        self.env
+            .write()
+            .expect("environment lock is poisoned")
+            .define(name, value)
+    }
+
+    fn assign_var(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        self.env
+            .write()
+            .expect("environment lock is poisoned")
+            .assign(name, value)
+    }
+
+    fn get_var(&mut self, var: &parser::Variable) -> Result<Value, Error> {
+        let ident = identifier_str(&var.name)?;
+        let depth = var.depth.ok_or_else(|| Error {
+            line: var.name.line,
+            kind: ErrorKind::UndefinedVariable(ident.into()),
+        })?;
+
+        self.env
+            .read()
+            .expect("environment lock is poisoned")
+            .get(ident, var.name.line, depth)
+    }
+
+    /// Interpret the block in the supplied environment. If no
+    /// environment is supplied, a new one is created using the
+    /// current one as its parent.
+    fn interpret_block_with_env(
+        &mut self,
+        env: Option<Rc<RwLock<Environment>>>,
+        block: &parser::Block,
+    ) -> Result<Value, Error> {
+        let env = match env {
+            Some(env) => env,
+            None => {
+                let env: Rc<RwLock<Environment>> = Default::default();
+                set_enclosing_env(&env, self.env.clone());
+                env
+            }
+        };
+
+        let previous = std::mem::replace(&mut self.env, env);
+        let result = self.interpret_block(block);
+
+        // Swap it back, discarding the child env.
+        self.env = previous;
+
+        return result;
+    }
+
+    fn interpret_block(&mut self, program: &Block) -> Result<Value, Error> {
+        let mut value = Value::Literal(Literal::Nil);
+
+        for stmt in program {
+            value = self.interpret_stmt(stmt)?;
+        }
+
+        Ok(value)
+    }
+
+    fn interpret_stmt(&mut self, stmt: &Statement) -> Result<Value, Error> {
+        let value = match stmt {
+            Statement::Expr(expr) => self.eval(expr)?,
+            Statement::Print(expr) => {
+                let result = self.eval(expr)?;
+                let output = format!("{:?}", result);
+                println!("{}", output);
+                Value::Literal(Literal::String(output))
+            }
+            Statement::Var(var) => return self.interpret_var(var),
+            Statement::Block(block) => return self.interpret_block_with_env(None, block),
+            Statement::If(if_stmt) => return self.interpret_if(if_stmt),
+            Statement::While(while_stmt) => return self.interpret_while(while_stmt),
+            Statement::Function(func) => return self.interpret_function(func.clone()),
+            Statement::Return(ret) => {
+                return Err(Error {
+                    line: 0,
+                    kind: ErrorKind::FunctionReturn(self.eval(&ret.value)?),
+                })
+            }
+        };
+
+        Ok(value)
+    }
+
+    fn interpret_var(&mut self, var: &parser::Var) -> Result<Value, Error> {
+        let init = var.initialiser.as_ref().ok_or_else(|| Error {
+            line: var.name.line,
+            kind: ErrorKind::InternalError("missing variable initialiser".into()),
+        })?;
+        let value = self.eval(init)?;
+        self.define_var(&var.name, value.clone())?;
+        Ok(value)
+    }
+
+    fn interpret_if(&mut self, if_stmt: &parser::If) -> Result<Value, Error> {
+        let condition = self.eval(&if_stmt.condition)?;
+
+        if eval_truthy(&condition) {
+            self.interpret_stmt(&if_stmt.then_branch)
+        } else if let Some(else_branch) = &if_stmt.else_branch {
+            self.interpret_stmt(else_branch)
+        } else {
+            Ok(Value::Literal(Literal::Nil))
+        }
+    }
+
+    fn interpret_while(&mut self, stmt: &parser::While) -> Result<Value, Error> {
+        let mut value = Value::Literal(Literal::Nil);
+        while eval_truthy(&self.eval(&stmt.condition)?) {
+            value = self.interpret_stmt(&stmt.body)?;
+        }
+
+        Ok(value)
+    }
+
+    fn interpret_function(&mut self, func: Rc<parser::Function>) -> Result<Value, Error> {
+        let name = func.name.clone();
+        let value = Value::Callable(Callable::Function {
+            func,
+            closure: self.env.clone(),
+        });
+        self.define_var(&name, value.clone())?;
+        Ok(value)
+    }
+
+    fn eval(&mut self, expr: &Expr) -> Result<Value, Error> {
+        match expr {
+            Expr::Assign(assign) => self.eval_assign(assign),
+            Expr::Literal(lit) => Ok(lit.clone().into()),
+            Expr::Grouping(grouping) => self.eval(&*grouping.0),
+            Expr::Unary(unary) => self.eval_unary(unary),
+            Expr::Binary(binary) => self.eval_binary(binary),
+            Expr::Variable(var) => self.get_var(var),
+            Expr::Logical(log) => self.eval_logical(log),
+            Expr::Call(call) => self.eval_call(call),
+        }
+    }
+
+    fn eval_unary(&mut self, expr: &parser::Unary) -> Result<Value, Error> {
+        let right = self.eval(&*expr.right)?;
+
+        match (&expr.operator.kind, right) {
+            (TokenKind::Minus, Value::Literal(Literal::Number(num))) => {
+                Ok(Literal::Number(-num).into())
+            }
+            (TokenKind::Bang, right) => Ok(Literal::Boolean(!eval_truthy(&right)).into()),
+
+            (op, right) => Err(Error {
+                line: expr.operator.line,
+                kind: ErrorKind::TypeError(format!(
+                    "Operator '{:?}' can not be called with argument '{:?}'",
+                    op, right
+                )),
+            }),
+        }
+    }
+
+    fn eval_binary(&mut self, expr: &parser::Binary) -> Result<Value, Error> {
+        let left = self.eval(&*expr.left)?.expect_literal()?;
+        let right = self.eval(&*expr.right)?.expect_literal()?;
+
+        let result = match (&expr.operator.kind, left, right) {
+            // Numeric
+            (TokenKind::Minus, Literal::Number(l), Literal::Number(r)) => Literal::Number(l - r),
+            (TokenKind::Slash, Literal::Number(l), Literal::Number(r)) => Literal::Number(l / r),
+            (TokenKind::Star, Literal::Number(l), Literal::Number(r)) => Literal::Number(l * r),
+            (TokenKind::Plus, Literal::Number(l), Literal::Number(r)) => Literal::Number(l + r),
+
+            // Strings
+            (TokenKind::Plus, Literal::String(l), Literal::String(r)) => {
+                Literal::String(format!("{}{}", l, r))
+            }
+
+            // Comparators (on numbers only?)
+            (TokenKind::Greater, Literal::Number(l), Literal::Number(r)) => Literal::Boolean(l > r),
+            (TokenKind::GreaterEqual, Literal::Number(l), Literal::Number(r)) => {
+                Literal::Boolean(l >= r)
+            }
+            (TokenKind::Less, Literal::Number(l), Literal::Number(r)) => Literal::Boolean(l < r),
+            (TokenKind::LessEqual, Literal::Number(l), Literal::Number(r)) => {
+                Literal::Boolean(l <= r)
+            }
+
+            // Equality
+            (TokenKind::Equal, l, r) => Literal::Boolean(l == r),
+            (TokenKind::BangEqual, l, r) => Literal::Boolean(l != r),
+
+            (op, left, right) => {
+                return Err(Error {
+                    line: expr.operator.line,
+                    kind: ErrorKind::TypeError(format!(
+                        "Operator '{:?}' can not be called with arguments '({:?}, {:?})'",
+                        op, left, right
+                    )),
+                })
+            }
+        };
+
+        Ok(result.into())
+    }
+
+    fn eval_assign(&mut self, assign: &parser::Assign) -> Result<Value, Error> {
+        let value = self.eval(&assign.value)?;
+        self.assign_var(&assign.name, value.clone())?;
+        Ok(value)
+    }
+
+    fn eval_logical(&mut self, logical: &parser::Logical) -> Result<Value, Error> {
+        let left = eval_truthy(&self.eval(&logical.left)?);
+        let right = eval_truthy(&self.eval(&logical.right)?);
+
+        match &logical.operator.kind {
+            TokenKind::And => Ok(Literal::Boolean(left && right).into()),
+            TokenKind::Or => Ok(Literal::Boolean(left || right).into()),
+            kind => Err(Error {
+                line: logical.operator.line,
+                kind: ErrorKind::InternalError(format!("Invalid logical operator: {:?}", kind)),
+            }),
+        }
+    }
+
+    fn eval_call(&mut self, call: &parser::Call) -> Result<Value, Error> {
+        let callable = match self.eval(&call.callee)? {
+            Value::Callable(c) => c,
+            Value::Literal(v) => {
+                return Err(Error {
+                    line: call.paren.line,
+                    kind: ErrorKind::RuntimeError(format!("not callable: {:?}", v)),
+                })
+            }
+        };
+
+        let mut args = vec![];
+        for arg in &call.args {
+            args.push(self.eval(arg)?);
+        }
+
+        if callable.arity() != args.len() {
+            return Err(Error {
+                line: call.paren.line,
+                kind: ErrorKind::RuntimeError(format!(
+                    "Expected {} arguments, but got {}",
+                    callable.arity(),
+                    args.len(),
+                )),
+            });
+        }
+
+        callable.call(self, args)
+    }
+}
+
+// Interpreter functions not dependent on interpreter-state.
+
+fn eval_truthy(lit: &Value) -> bool {
+    if let Value::Literal(lit) = lit {
+        match lit {
+            Literal::Nil => false,
+            Literal::Boolean(b) => *b,
+            _ => true,
+        }
+    } else {
+        false
+    }
+}
+
+fn set_enclosing_env(this: &RwLock<Environment>, parent: Rc<RwLock<Environment>>) {
+    this.write()
+        .expect("environment lock is poisoned")
+        .enclosing = Some(parent);
+}
diff --git a/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs b/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs
new file mode 100644
index 0000000000..c502d2a171
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs
@@ -0,0 +1,25 @@
+use std::fmt;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use crate::treewalk::errors::Error;
+use crate::treewalk::interpreter::Value;
+use crate::treewalk::parser::Literal;
+
+pub trait Builtin: fmt::Debug {
+    fn arity(&self) -> usize;
+    fn call(&self, args: Vec<Value>) -> Result<Value, Error>;
+}
+
+// Builtin to return the current timestamp.
+#[derive(Debug)]
+pub struct Clock {}
+impl Builtin for Clock {
+    fn arity(&self) -> usize {
+        0
+    }
+
+    fn call(&self, _args: Vec<Value>) -> Result<Value, Error> {
+        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+        Ok(Value::Literal(Literal::Number(now.as_secs() as f64)))
+    }
+}
diff --git a/users/tazjin/rlox/src/treewalk/interpreter/tests.rs b/users/tazjin/rlox/src/treewalk/interpreter/tests.rs
new file mode 100644
index 0000000000..2fc6f4fee9
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/interpreter/tests.rs
@@ -0,0 +1,97 @@
+use super::*;
+
+/// Evaluate a code snippet, returning a value.
+fn parse_eval(code: &str) -> Value {
+    Interpreter::create()
+        .interpret(code.into())
+        .expect("could not interpret code")
+}
+
+#[test]
+fn test_if() {
+    let result = parse_eval(
+        r#"
+if (42 > 23)
+  "pass";
+else
+  "fail";
+"#,
+    );
+
+    assert_eq!(Value::Literal(Literal::String("pass".into())), result,);
+}
+
+#[test]
+fn test_scope() {
+    let result = parse_eval(
+        r#"
+var result = "";
+
+var a = "global a, ";
+var b = "global b, ";
+var c = "global c";
+
+{
+  var a = "outer a, ";
+  var b = "outer b, ";
+
+  {
+    var a = "inner a, ";
+    result = a + b + c;
+  }
+}
+"#,
+    );
+
+    assert_eq!(
+        Value::Literal(Literal::String("inner a, outer b, global c".into())),
+        result,
+    );
+}
+
+#[test]
+fn test_binary_operators() {
+    assert_eq!(Value::Literal(Literal::Number(42.0)), parse_eval("40 + 2;"));
+
+    assert_eq!(
+        Value::Literal(Literal::String("foobar".into())),
+        parse_eval("\"foo\" + \"bar\";")
+    );
+}
+
+#[test]
+fn test_functions() {
+    let result = parse_eval(
+        r#"
+fun add(a, b, c) {
+  a + b + c;
+}
+
+add(1, 2, 3);
+"#,
+    );
+
+    assert_eq!(Value::Literal(Literal::Number(6.0)), result);
+}
+
+#[test]
+fn test_closure() {
+    let result = parse_eval(
+        r#"
+fun makeCounter() {
+  var i = 0;
+  fun count() {
+    i = i + 1;
+  }
+
+  return count;
+}
+
+var counter = makeCounter();
+counter(); // "1".
+counter(); // "2".
+"#,
+    );
+
+    assert_eq!(Value::Literal(Literal::Number(2.0)), result);
+}
diff --git a/users/tazjin/rlox/src/treewalk/mod.rs b/users/tazjin/rlox/src/treewalk/mod.rs
new file mode 100644
index 0000000000..2d82b3320a
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/mod.rs
@@ -0,0 +1,6 @@
+use crate::scanner;
+
+mod errors;
+pub mod interpreter;
+mod parser;
+mod resolver;
diff --git a/users/tazjin/rlox/src/treewalk/parser.rs b/users/tazjin/rlox/src/treewalk/parser.rs
new file mode 100644
index 0000000000..5794b42d15
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/parser.rs
@@ -0,0 +1,700 @@
+// This implements the grammar of Lox as described starting in the
+// Crafting Interpreters chapter "Representing Code". Note that the
+// upstream Java implementation works around Java being bad at value
+// classes by writing a code generator for Java.
+//
+// My Rust implementation skips this step because it's unnecessary, we
+// have real types.
+use crate::treewalk::errors::{Error, ErrorKind};
+use crate::treewalk::scanner::{Token, TokenKind};
+use std::rc::Rc;
+
+// AST
+
+#[derive(Debug)]
+pub struct Assign {
+    pub name: Token,
+    pub value: Box<Expr>,
+    pub depth: Option<usize>,
+}
+
+#[derive(Debug)]
+pub struct Binary {
+    pub left: Box<Expr>,
+    pub operator: Token,
+    pub right: Box<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Logical {
+    pub left: Box<Expr>,
+    pub operator: Token,
+    pub right: Box<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Grouping(pub Box<Expr>);
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Literal {
+    Boolean(bool),
+    Number(f64),
+    String(String),
+    Nil,
+}
+
+#[derive(Debug)]
+pub struct Unary {
+    pub operator: Token,
+    pub right: Box<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Call {
+    pub callee: Box<Expr>,
+    pub paren: Token,
+    pub args: Vec<Expr>,
+}
+
+// Not to be confused with `Var`, which is for assignment.
+#[derive(Debug)]
+pub struct Variable {
+    pub name: Token,
+    pub depth: Option<usize>,
+}
+
+#[derive(Debug)]
+pub enum Expr {
+    Assign(Assign),
+    Binary(Binary),
+    Grouping(Grouping),
+    Literal(Literal),
+    Unary(Unary),
+    Call(Call),
+    Variable(Variable),
+    Logical(Logical),
+}
+
+// Variable assignment. Not to be confused with `Variable`, which is
+// for access.
+#[derive(Debug)]
+pub struct Var {
+    pub name: Token,
+    pub initialiser: Option<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Return {
+    pub value: Expr,
+}
+
+#[derive(Debug)]
+pub struct If {
+    pub condition: Expr,
+    pub then_branch: Box<Statement>,
+    pub else_branch: Option<Box<Statement>>,
+}
+
+#[derive(Debug)]
+pub struct While {
+    pub condition: Expr,
+    pub body: Box<Statement>,
+}
+
+pub type Block = Vec<Statement>;
+
+#[derive(Debug)]
+pub struct Function {
+    pub name: Token,
+    pub params: Vec<Token>,
+    pub body: Block,
+}
+
+#[derive(Debug)]
+pub enum Statement {
+    Expr(Expr),
+    Print(Expr),
+    Var(Var),
+    Block(Block),
+    If(If),
+    While(While),
+    Function(Rc<Function>),
+    Return(Return),
+}
+
+// Parser
+
+// program        → declaration* EOF ;
+//
+// declaration    → funDecl
+// | varDecl
+// | statement ;
+//
+// funDecl        → "fun" function ;
+// function       → IDENTIFIER "(" parameters? ")" block ;
+// parameters     → IDENTIFIER ( "," IDENTIFIER )* ;
+//
+//
+// statement      → exprStmt
+// | forStmt
+// | ifStmt
+// | printStmt
+// | returnStmt
+// | whileStmt
+// | block ;
+//
+// forStmt        → "for" "(" ( varDecl | exprStmt | ";" )
+// expression? ";"
+// expression? ")" statement ;
+//
+// returnStmt     → "return" expression? ";" ;
+//
+// whileStmt      → "while" "(" expression ")" statement ;
+//
+// exprStmt       → expression ";" ;
+//
+// ifStmt         → "if" "(" expression ")" statement
+// ( "else" statement )? ;
+//
+// printStmt      → "print" expression ";" ;
+//
+// expression     → assignment ;
+// assignment     → IDENTIFIER "=" assignment
+// | logic_or ;
+// logic_or       → logic_and ( "or" logic_and )* ;
+// logic_and      → equality ( "and" equality )* ;
+// equality       → comparison ( ( "!=" | "==" ) comparison )* ;
+// comparison     → term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
+// term           → factor ( ( "-" | "+" ) factor )* ;
+// factor         → unary ( ( "/" | "*" ) unary )* ;
+// unary          → ( "!" | "-" ) unary | call ;
+// call           → primary ( "(" arguments? ")" )* ;
+// arguments      → expression ( "," expression )* ;
+// primary        → NUMBER | STRING | "true" | "false" | "nil"
+// | "(" expression ")" ;
+
+struct Parser {
+    tokens: Vec<Token>,
+    current: usize,
+}
+
+type ExprResult = Result<Expr, Error>;
+type StmtResult = Result<Statement, Error>;
+
+impl Parser {
+    // recursive-descent parser functions
+
+    fn declaration(&mut self) -> StmtResult {
+        if self.match_token(&TokenKind::Fun) {
+            return self.function();
+        }
+
+        if self.match_token(&TokenKind::Var) {
+            return self.var_declaration();
+        }
+
+        self.statement()
+    }
+
+    fn function(&mut self) -> StmtResult {
+        let name = self.identifier("Expected function name.")?;
+
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expect '(' after function name."),
+        )?;
+
+        let mut params = vec![];
+
+        if !self.check_token(&TokenKind::RightParen) {
+            loop {
+                if params.len() >= 255 {
+                    return Err(Error {
+                        line: self.peek().line,
+                        kind: ErrorKind::InternalError("255 parameter limit exceeded.".into()),
+                    });
+                }
+
+                params.push(self.identifier("Expected parameter name.")?);
+
+                if !self.match_token(&TokenKind::Comma) {
+                    break;
+                }
+            }
+        }
+
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expect ')' after parameters."),
+        )?;
+
+        self.consume(
+            &TokenKind::LeftBrace,
+            ErrorKind::ExpectedToken("Expect '{' before function body."),
+        )?;
+
+        Ok(Statement::Function(Rc::new(Function {
+            name,
+            params,
+            body: self.block_statement()?,
+        })))
+    }
+
+    fn var_declaration(&mut self) -> StmtResult {
+        // Since `TokenKind::Identifier` carries data, we can't use
+        // `consume`.
+        let mut var = Var {
+            name: self.identifier("Expected variable name.")?,
+            initialiser: None,
+        };
+
+        if self.match_token(&TokenKind::Equal) {
+            var.initialiser = Some(self.expression()?);
+        }
+
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Var(var))
+    }
+
+    fn statement(&mut self) -> StmtResult {
+        if self.match_token(&TokenKind::Print) {
+            self.print_statement()
+        } else if self.match_token(&TokenKind::LeftBrace) {
+            Ok(Statement::Block(self.block_statement()?))
+        } else if self.match_token(&TokenKind::If) {
+            self.if_statement()
+        } else if self.match_token(&TokenKind::While) {
+            self.while_statement()
+        } else if self.match_token(&TokenKind::For) {
+            self.for_statement()
+        } else if self.match_token(&TokenKind::Return) {
+            self.return_statement()
+        } else {
+            self.expr_statement()
+        }
+    }
+
+    fn print_statement(&mut self) -> StmtResult {
+        let expr = self.expression()?;
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Print(expr))
+    }
+
+    fn block_statement(&mut self) -> Result<Block, Error> {
+        let mut block: Block = vec![];
+
+        while !self.check_token(&TokenKind::RightBrace) && !self.is_at_end() {
+            block.push(self.declaration()?);
+        }
+
+        self.consume(&TokenKind::RightBrace, ErrorKind::ExpectedClosingBrace)?;
+
+        Ok(block)
+    }
+
+    fn if_statement(&mut self) -> StmtResult {
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'if'"),
+        )?;
+        let condition = self.expression()?;
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after condition"),
+        )?;
+
+        let then_branch = Box::new(self.statement()?);
+
+        let mut stmt = If {
+            condition,
+            then_branch,
+            else_branch: Option::None,
+        };
+
+        if self.match_token(&TokenKind::Else) {
+            stmt.else_branch = Some(Box::new(self.statement()?));
+        }
+
+        Ok(Statement::If(stmt))
+    }
+
+    fn while_statement(&mut self) -> StmtResult {
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'while'"),
+        )?;
+
+        let condition = self.expression()?;
+
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after 'while'"),
+        )?;
+
+        Ok(Statement::While(While {
+            condition,
+            body: Box::new(self.statement()?),
+        }))
+    }
+
+    fn for_statement(&mut self) -> StmtResult {
+        // Parsing of clauses ...
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'for'"),
+        )?;
+
+        let initialiser = if self.match_token(&TokenKind::Semicolon) {
+            None
+        } else if self.match_token(&TokenKind::Var) {
+            Some(self.var_declaration()?)
+        } else {
+            Some(self.expr_statement()?)
+        };
+
+        let condition = if self.check_token(&TokenKind::Semicolon) {
+            // unspecified condition => infinite loop
+            Expr::Literal(Literal::Boolean(true))
+        } else {
+            self.expression()?
+        };
+
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+
+        let increment = if self.check_token(&TokenKind::RightParen) {
+            None
+        } else {
+            Some(self.expression()?)
+        };
+
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after for clauses"),
+        )?;
+
+        let mut body = self.statement()?;
+
+        // ... desugaring to while
+
+        if let Some(inc) = increment {
+            body = Statement::Block(vec![body, Statement::Expr(inc)]);
+        }
+
+        body = Statement::While(While {
+            condition,
+            body: Box::new(body),
+        });
+
+        if let Some(init) = initialiser {
+            body = Statement::Block(vec![init, body]);
+        }
+
+        Ok(body)
+    }
+
+    fn return_statement(&mut self) -> StmtResult {
+        let value = self.expression()?;
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Return(Return { value }))
+    }
+
+    fn expr_statement(&mut self) -> StmtResult {
+        let expr = self.expression()?;
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Expr(expr))
+    }
+
+    fn expression(&mut self) -> ExprResult {
+        self.assignment()
+    }
+
+    fn assignment(&mut self) -> ExprResult {
+        let expr = self.logic_or()?;
+
+        if self.match_token(&TokenKind::Equal) {
+            let equals = self.previous().clone();
+            let value = self.assignment()?;
+
+            if let Expr::Variable(Variable { name, .. }) = expr {
+                return Ok(Expr::Assign(Assign {
+                    name,
+                    value: Box::new(value),
+                    depth: None,
+                }));
+            }
+
+            return Err(Error {
+                line: equals.line,
+                kind: ErrorKind::InvalidAssignmentTarget(format!("{:?}", equals)),
+            });
+        }
+
+        Ok(expr)
+    }
+
+    fn logic_or(&mut self) -> ExprResult {
+        let mut expr = self.logic_and()?;
+
+        while self.match_token(&TokenKind::Or) {
+            expr = Expr::Logical(Logical {
+                left: Box::new(expr),
+                operator: self.previous().clone(),
+                right: Box::new(self.logic_and()?),
+            })
+        }
+
+        Ok(expr)
+    }
+
+    fn logic_and(&mut self) -> ExprResult {
+        let mut expr = self.equality()?;
+
+        while self.match_token(&TokenKind::And) {
+            expr = Expr::Logical(Logical {
+                left: Box::new(expr),
+                operator: self.previous().clone(),
+                right: Box::new(self.equality()?),
+            })
+        }
+
+        Ok(expr)
+    }
+
+    fn equality(&mut self) -> ExprResult {
+        self.binary_operator(
+            &[TokenKind::BangEqual, TokenKind::EqualEqual],
+            Self::comparison,
+        )
+    }
+
+    fn comparison(&mut self) -> ExprResult {
+        self.binary_operator(
+            &[
+                TokenKind::Greater,
+                TokenKind::GreaterEqual,
+                TokenKind::Less,
+                TokenKind::LessEqual,
+            ],
+            Self::term,
+        )
+    }
+
+    fn term(&mut self) -> ExprResult {
+        self.binary_operator(&[TokenKind::Minus, TokenKind::Plus], Self::factor)
+    }
+
+    fn factor(&mut self) -> ExprResult {
+        self.binary_operator(&[TokenKind::Slash, TokenKind::Star], Self::unary)
+    }
+
+    fn unary(&mut self) -> ExprResult {
+        if self.match_token(&TokenKind::Bang) || self.match_token(&TokenKind::Minus) {
+            return Ok(Expr::Unary(Unary {
+                operator: self.previous().clone(),
+                right: Box::new(self.unary()?),
+            }));
+        }
+
+        return self.call();
+    }
+
+    fn call(&mut self) -> ExprResult {
+        let mut expr = self.primary()?;
+
+        loop {
+            if self.match_token(&TokenKind::LeftParen) {
+                expr = self.finish_call(expr)?;
+            } else {
+                break;
+            }
+        }
+
+        Ok(expr)
+    }
+
+    fn finish_call(&mut self, callee: Expr) -> ExprResult {
+        let mut args = vec![];
+
+        if !self.check_token(&TokenKind::RightParen) {
+            loop {
+                // TODO(tazjin): Check for max args count
+                args.push(self.expression()?);
+                if !self.match_token(&TokenKind::Comma) {
+                    break;
+                }
+            }
+        }
+
+        let paren = self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expect ')' after arguments."),
+        )?;
+
+        Ok(Expr::Call(Call {
+            args,
+            callee: Box::new(callee),
+            paren,
+        }))
+    }
+
+    fn primary(&mut self) -> ExprResult {
+        let next = self.advance();
+        let literal = match next.kind {
+            TokenKind::True => Literal::Boolean(true),
+            TokenKind::False => Literal::Boolean(false),
+            TokenKind::Nil => Literal::Nil,
+            TokenKind::Number(num) => Literal::Number(num),
+            TokenKind::String(string) => Literal::String(string),
+
+            TokenKind::LeftParen => {
+                let expr = self.expression()?;
+                self.consume(&TokenKind::RightParen, ErrorKind::UnmatchedParens)?;
+                return Ok(Expr::Grouping(Grouping(Box::new(expr))));
+            }
+
+            TokenKind::Identifier(_) => {
+                return Ok(Expr::Variable(Variable {
+                    name: next,
+                    depth: None,
+                }))
+            }
+
+            unexpected => {
+                eprintln!("encountered {:?}", unexpected);
+                return Err(Error {
+                    line: next.line,
+                    kind: ErrorKind::ExpectedExpression(next.lexeme),
+                });
+            }
+        };
+
+        Ok(Expr::Literal(literal))
+    }
+
+    // internal helpers
+
+    fn identifier(&mut self, err: &'static str) -> Result<Token, Error> {
+        if let TokenKind::Identifier(_) = self.peek().kind {
+            Ok(self.advance())
+        } else {
+            Err(Error {
+                line: self.peek().line,
+                kind: ErrorKind::ExpectedToken(err),
+            })
+        }
+    }
+
+    /// Check if the next token is in `oneof`, and advance if it is.
+    fn match_token(&mut self, token: &TokenKind) -> bool {
+        if self.check_token(token) {
+            self.advance();
+            return true;
+        }
+
+        false
+    }
+
+    /// Return the next token and advance parser state.
+    fn advance(&mut self) -> Token {
+        if !self.is_at_end() {
+            self.current += 1;
+        }
+
+        return self.previous().clone();
+    }
+
+    fn is_at_end(&self) -> bool {
+        self.check_token(&TokenKind::Eof)
+    }
+
+    /// Is the next token `token`?
+    fn check_token(&self, token: &TokenKind) -> bool {
+        self.peek().kind == *token
+    }
+
+    fn peek(&self) -> &Token {
+        &self.tokens[self.current]
+    }
+
+    fn previous(&self) -> &Token {
+        &self.tokens[self.current - 1]
+    }
+
+    fn consume(&mut self, kind: &TokenKind, err: ErrorKind) -> Result<Token, Error> {
+        if self.check_token(kind) {
+            return Ok(self.advance());
+        }
+
+        Err(Error {
+            line: self.peek().line,
+            kind: err,
+        })
+    }
+
+    fn synchronise(&mut self) {
+        self.advance();
+
+        while !self.is_at_end() {
+            if self.previous().kind == TokenKind::Semicolon {
+                return;
+            }
+
+            match self.peek().kind {
+                TokenKind::Class
+                | TokenKind::Fun
+                | TokenKind::Var
+                | TokenKind::For
+                | TokenKind::If
+                | TokenKind::While
+                | TokenKind::Print
+                | TokenKind::Return => return,
+
+                _ => {
+                    self.advance();
+                }
+            }
+        }
+    }
+
+    fn binary_operator(
+        &mut self,
+        oneof: &[TokenKind],
+        each: fn(&mut Parser) -> ExprResult,
+    ) -> ExprResult {
+        let mut expr = each(self)?;
+
+        while oneof.iter().any(|t| self.match_token(t)) {
+            expr = Expr::Binary(Binary {
+                left: Box::new(expr),
+                operator: self.previous().clone(),
+                right: Box::new(each(self)?),
+            })
+        }
+
+        return Ok(expr);
+    }
+}
+
+pub fn parse(tokens: Vec<Token>) -> Result<Block, Vec<Error>> {
+    let mut parser = Parser { tokens, current: 0 };
+    let mut program: Block = vec![];
+    let mut errors: Vec<Error> = vec![];
+
+    while !parser.is_at_end() {
+        match parser.declaration() {
+            Err(err) => {
+                errors.push(err);
+                parser.synchronise();
+            }
+            Ok(decl) => {
+                program.push(decl);
+            }
+        }
+    }
+
+    if errors.is_empty() {
+        Ok(program)
+    } else {
+        Err(errors)
+    }
+}
diff --git a/users/tazjin/rlox/src/treewalk/resolver.rs b/users/tazjin/rlox/src/treewalk/resolver.rs
new file mode 100644
index 0000000000..3d12973aa0
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/resolver.rs
@@ -0,0 +1,199 @@
+// Resolves variable access to their specific instances in the
+// environment chain.
+//
+// https://craftinginterpreters.com/resolving-and-binding.html
+
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use crate::treewalk::errors::{Error, ErrorKind};
+use crate::treewalk::parser::{self, Expr, Statement};
+use crate::treewalk::scanner::Token;
+
+#[derive(Default)]
+struct Resolver<'a> {
+    scopes: Vec<HashMap<&'a str, bool>>,
+}
+
+impl<'a> Resolver<'a> {
+    // AST traversal
+    fn resolve(&mut self, program: &'a mut parser::Block) -> Result<(), Error> {
+        self.begin_scope();
+        for stmt in program {
+            self.resolve_stmt(stmt)?;
+        }
+        self.end_scope();
+
+        Ok(())
+    }
+
+    fn resolve_stmt(&mut self, stmt: &'a mut Statement) -> Result<(), Error> {
+        match stmt {
+            Statement::Expr(expr) => self.resolve_expr(expr),
+            Statement::Print(expr) => self.resolve_expr(expr),
+            Statement::Var(var) => self.resolve_var(var),
+            Statement::Return(ret) => self.resolve_expr(&mut ret.value),
+            Statement::Block(block) => self.resolve(block),
+
+            Statement::If(if_stmt) => {
+                self.resolve_expr(&mut if_stmt.condition)?;
+                self.resolve_stmt(&mut if_stmt.then_branch)?;
+
+                if let Some(branch) = if_stmt.else_branch.as_mut() {
+                    self.resolve_stmt(branch)?;
+                }
+
+                Ok(())
+            }
+
+            Statement::While(while_stmt) => {
+                self.resolve_expr(&mut while_stmt.condition)?;
+                self.resolve_stmt(&mut while_stmt.body)
+            }
+
+            Statement::Function(func) => match Rc::get_mut(func) {
+                Some(func) => self.resolve_function(func),
+                // The resolver does not clone references, so unless
+                // the interpreter is called before the resolver this
+                // case should never happen.
+                None => {
+                    return Err(Error {
+                        line: 0,
+                        kind: ErrorKind::InternalError(
+                            "multiple function references before interpretation".into(),
+                        ),
+                    })
+                }
+            },
+        }
+    }
+
+    fn resolve_var(&mut self, var: &'a mut parser::Var) -> Result<(), Error> {
+        self.declare(&var.name.lexeme);
+
+        if let Some(init) = &mut var.initialiser {
+            self.resolve_expr(init)?;
+        }
+
+        self.define(&var.name.lexeme);
+
+        Ok(())
+    }
+
+    fn resolve_function(&mut self, func: &'a mut parser::Function) -> Result<(), Error> {
+        self.declare(&func.name.lexeme);
+        self.define(&func.name.lexeme);
+
+        self.begin_scope();
+
+        for param in &func.params {
+            self.declare(&param.lexeme);
+            self.define(&param.lexeme);
+        }
+
+        for stmt in &mut func.body {
+            self.resolve_stmt(stmt)?;
+        }
+
+        self.end_scope();
+
+        Ok(())
+    }
+
+    fn resolve_expr(&mut self, expr: &'a mut Expr) -> Result<(), Error> {
+        match expr {
+            Expr::Variable(var) => self.resolve_variable(var),
+            Expr::Assign(assign) => self.resolve_assign(assign),
+            Expr::Grouping(grouping) => self.resolve_expr(&mut grouping.0),
+            Expr::Call(call) => self.resolve_call(call),
+            Expr::Literal(_) => Ok(()),
+            Expr::Unary(unary) => self.resolve_expr(&mut unary.right),
+
+            Expr::Logical(log) => {
+                self.resolve_expr(&mut log.left)?;
+                self.resolve_expr(&mut log.right)
+            }
+
+            Expr::Binary(binary) => {
+                self.resolve_expr(&mut binary.left)?;
+                self.resolve_expr(&mut binary.right)
+            }
+        }
+    }
+
+    fn resolve_variable(&mut self, var: &'a mut parser::Variable) -> Result<(), Error> {
+        if let Some(scope) = self.scopes.last_mut() {
+            if let Some(false) = scope.get(var.name.lexeme.as_str()) {
+                return Err(Error {
+                    line: var.name.line,
+                    kind: ErrorKind::StaticError(
+                        "can't read local variable in its own initialiser".into(),
+                    ),
+                });
+            }
+        }
+
+        var.depth = self.resolve_local(&var.name);
+        Ok(())
+    }
+
+    fn resolve_assign(&mut self, assign: &'a mut parser::Assign) -> Result<(), Error> {
+        self.resolve_expr(&mut assign.value)?;
+        assign.depth = self.resolve_local(&assign.name);
+        Ok(())
+    }
+
+    fn resolve_local(&mut self, name: &'a Token) -> Option<usize> {
+        for (c, scope) in self.scopes.iter().rev().enumerate() {
+            if scope.contains_key(name.lexeme.as_str()) {
+                return Some(c);
+            }
+        }
+
+        None
+    }
+
+    fn resolve_call(&mut self, call: &'a mut parser::Call) -> Result<(), Error> {
+        self.resolve_expr(&mut call.callee)?;
+
+        for arg in call.args.iter_mut() {
+            self.resolve_expr(arg)?;
+        }
+
+        Ok(())
+    }
+
+    // Internal helpers
+
+    fn declare(&mut self, name: &'a str) {
+        if let Some(scope) = self.scopes.last_mut() {
+            scope.insert(&name, false);
+        }
+    }
+
+    fn define(&mut self, name: &'a str) {
+        if let Some(scope) = self.scopes.last_mut() {
+            scope.insert(&name, true);
+        }
+    }
+
+    fn begin_scope(&mut self) {
+        self.scopes.push(Default::default());
+    }
+
+    fn end_scope(&mut self) {
+        self.scopes.pop();
+    }
+}
+
+pub fn resolve(globals: &[String], block: &mut parser::Block) -> Result<(), Error> {
+    let mut resolver: Resolver = Default::default();
+
+    // Scope for static globals only starts, never ends.
+    resolver.begin_scope();
+    for global in globals {
+        resolver.define(global);
+    }
+
+    resolver.resolve(block)
+}
diff --git a/users/tazjin/russian/helpers.el b/users/tazjin/russian/helpers.el
new file mode 100644
index 0000000000..41d4aa34f4
--- /dev/null
+++ b/users/tazjin/russian/helpers.el
@@ -0,0 +1,7 @@
+;; Helper functions for creating the other files.
+
+(defun wiktionary-lookup-at-point (ask-lang)
+  (interactive "P")
+  (let ((language (if ask-lang (read-string "Language code? ") "ru")))
+    (eww (concat "https://ru.wiktionary.org/wiki/"
+                 (thing-at-point 'word)))))
diff --git a/users/tazjin/russian/roots.el b/users/tazjin/russian/roots.el
new file mode 100644
index 0000000000..77d09b4726
--- /dev/null
+++ b/users/tazjin/russian/roots.el
@@ -0,0 +1,28 @@
+;; '(root explanation)
+;;
+;; All roots without explanations are TODOs.
+;;
+;; In some cases, roots are not direct morphological roots of their
+;; descendent words (e.g. -голов- => главный)
+
+'(("-весь-" "everything, all, every, etc.")
+  ("-вид-" "seeing, viewing etc.")
+  ("-врем-" "time")
+  ("-говор-" "related to talking")
+  ("-голов-" "head, main, etc.")
+  ("-друг-" nil)
+  ("-дум-" "thinking, thoughts")
+  ("-жи-" "life")
+  ("-зна-" "knowing, knowledge")
+  ("-имя-" "name")
+  ("-й-" "walking, moving to")
+  ("-мочь-" "ability, permission")
+  ("-нов-" "new")
+  ("-общ-" "common?")
+  ("-правд-" "truth")
+  ("-прос-" "question")
+  ("-сказ-" nil)
+  ("-смотр-" "watching, viewing")
+  ("-стран-" "country?")
+  ("-ход-" "movement")
+  ("-хорош-" "goodness, niceness"))
diff --git a/users/tazjin/russian/russian.el b/users/tazjin/russian/russian.el
new file mode 100644
index 0000000000..28f1addeaa
--- /dev/null
+++ b/users/tazjin/russian/russian.el
@@ -0,0 +1,97 @@
+(require 'cl-macs)
+(require 'ht)
+(require 'seq)
+(require 's)
+
+;; Type definitions for Russian structures
+
+(cl-defstruct russian-word
+  "Definition and metadata of a single Russian word."
+  (word nil :type string)
+  (translations :type list
+                :documentation "List of lists of strings, each a set of translations.")
+
+  (notes nil :type list ;; of string
+         :documentation "free-form notes about this word")
+
+  (roots nil :type list ;; of string
+         :documentation "list of strings that correspond with roots (exact string match)"))
+
+(defun russian--merge-words (previous new)
+  "Merge two Russian word definitions together. If no previous
+  definition exists, only the new one will be returned."
+  (if (not previous) new
+    (cl-assert (equal (russian-word-word previous)
+                      (russian-word-word new))
+               "different words passed into merge function")
+    (make-russian-word :word (russian-word-word previous)
+                       :translations (-concat (russian-word-translations previous)
+                                              (russian-word-translations new))
+                       :notes (-concat (russian-word-notes previous)
+                                       (russian-word-notes new))
+                       :roots (-concat (russian-word-roots previous)
+                                       (russian-word-roots new)))))
+
+;; Definitions for creating a data structure of all Russian words.
+
+(defvar russian-words (make-hash-table)
+  "Table of all Russian words in the corpus.")
+
+(defun russian--define-word (word)
+  "Define a single word in the corpus, optionally merging it with
+  another entry."
+  (let ((key (russian-word-word word)))
+    (ht-set russian-words key (russian--merge-words
+                               (ht-get russian-words key)
+                               word))))
+
+(defmacro define-russian-words (&rest words)
+  "Define the list of all available words. There may be more than
+  one entry for a word in some cases."
+  (declare (indent defun))
+
+  ;; Clear the table before proceeding with insertion
+  (setq russian-words (make-hash-table))
+
+  (seq-map
+   (lambda (word)
+     (russian--define-word (make-russian-word :word (car word)
+                                              :translations (cadr word)
+                                              :notes (caddr word)
+                                              :roots (cadddr word))))
+   words)
+
+  '(message "Defined %s unique words." (ht-size russian-words)))
+
+;; Helpers to train Russian words through passively.
+
+(defun russian--format-word (word)
+  "Format a Russian word suitable for echo display."
+  (apply #'s-concat
+         (-flatten
+          (list (russian-word-word word)
+                " - "
+                (s-join ", " (russian-word-translations word))
+                (when-let ((roots (russian-word-roots word)))
+                  (list " [" (s-join ", " roots) "]"))
+                (when-let ((notes (russian-word-notes word)))
+                  (list " (" (s-join "; " notes) ")"))))))
+
+(defun display-russian-words ()
+  "Convert Russian words to passively terms and start passively."
+  (interactive)
+  (setq passively-learn-terms (make-hash-table))
+  (ht-map
+   (lambda (k v)
+     (ht-set passively-learn-terms k (russian--format-word v)))
+   russian-words)
+  (passively-enable))
+
+(defun lookup-last-russian-word (in-eww)
+  "Look up the last Russian word in Wiktionary"
+  (interactive "P")
+  (let ((url (concat "https://ru.wiktionary.org/wiki/" passively-last-displayed)))
+    (if in-eww (eww url)
+      (browse-url url))))
+
+(provide 'russian)
diff --git a/users/tazjin/russian/words.el b/users/tazjin/russian/words.el
new file mode 100644
index 0000000000..784e5bddde
--- /dev/null
+++ b/users/tazjin/russian/words.el
@@ -0,0 +1,723 @@
+;; entries :: '(entry ...)'
+;; entry :: '(word translations note roots)
+;; note :: (or nil string)
+;; translations :: '(translation ...)
+;; roots :: '(root ...)
+
+(require 'russian)
+
+(define-russian-words
+  ;; 1-50
+  ("и" ("and" "though"))
+  ("в" ("in" "at"))
+  ("не" ("not"))
+  ("он" ("he"))
+  ("на" ("on" "it" "at" "to"))
+  ("я" ("I"))
+  ("что" ("what" "that" "why"))
+  ("тот" ("that"))
+  ("быть" ("to be"))
+  ("с" ("with" "and" "from" "of"))
+  ("а" ("while" "and" "but"))
+  ("весь" ("all" "everything") nil ("-весь-"))
+  ("это" ("that" "this" "it"))
+  ("как" ("how" "what" "as" "like"))
+  ("она" ("she"))
+  ("по" ("on" "along" "by"))
+  ("но" ("but"))
+  ("они" ("they"))
+  ("к" ("to" "for" "by"))
+  ("у" ("by" "with" "of"))
+  ("ты" ("you"))
+  ("из" ("from" "of" "in"))
+  ("мы" ("we"))
+  ("за" ("behind" "over" "at" "after"))
+  ("вы" ("you"))
+  ("так" ("so" "thus" "then"))
+  ("же" ("and" "as for" "but" "same"))
+  ("от" ("from" "of" "for"))
+  ("сказать" ("to say" "to speak") nil ("-сказ-"))
+  ("этот" ("this"))
+  ("который" ("which" "who" "that"))
+  ("мочь" ("be able" "can") nil ("-мочь-"))
+  ("человек" ("man" "person"))
+  ("о" ("of" "about" "against"))
+  ("один" ("one" "some" "alone"))
+  ("ещё" ("still" "yet"))
+  ("бы" ("would"))
+  ("такой" ("such" "so" "some"))
+  ("только" ("only" "merely" "but"))
+  ("себя" ("myself" "himself" "herself"))
+  ("своё" ("one's own" "my" "our"))
+  ("какой" ("what" "which" "how"))
+  ("когда" ("when" "while" "as"))
+  ("уже" ("already" "by now"))
+  ("для" ("for" "to"))
+  ("вот" ("here" "there" "this is" "that's")
+   ("calling attention to something"))
+  ("кто" ("who" "that" "some"))
+  ("да" ("yes" "but") ("affirmation (..., right?)"))
+  ("говорить" ("to say" "to tell" "to speak") nil ("-говор-"))
+  ("год" ("year"))
+
+  ;; 51 - 100
+  ("знать" ("to know" "be aware") nil ("-зна-"))
+  ("мой" ("my" "mine"))
+  ("до" ("to" "up to" "about" "before"))
+  ("или" ("or"))
+  ("если" ("if"))
+  ("время" ("time" "season") nil ("-врем-"))
+  ("рука" ("hand" "arm"))
+  ("нет" ("no" "not" "but"))
+  ("самый" ("most" "the very" "the same"))
+  ("ни" ("not a" "not" "neither ... nor"))
+  ("стать" ("to become" "begin" "come"))
+  ("большой" ("big" "large" "important"))
+  ("даже" ("even"))
+  ("другой" ("other" "another" "different") nil ("-друг-"))
+  ("наш" ("our" "ours"))
+  ("свой" ("one's own"))
+  ("ну" ("now" "right" "well" "come on"))
+  ("под" ("under" "for" "towards" "to"))
+  ("где" ("where"))
+  ("дело" ("business" "affair" "matter"))
+  ("есть" ("to eat" "to be"))
+  ("сам" ("oneself"))
+  ("раз" ("time" "once" "since"))
+  ("чтобы" ("that" "in order that"))
+  ("два" ("two"))
+  ("там" ("there" "then"))
+  ("чем" ("than" "instead of")
+   ("чем ..., тем ..."))
+  ("глаз" ("eye" "sight"))
+  ("жизнь" ("life") nil ("-жи-"))
+  ("первый" ("first" "front" "former"))
+  ("день" ("day"))
+  ("тут" ("here" "now" "then"))
+  ("во" ("in" "at")
+   ("as particle also: wow, exactly, ..."))
+  ("ничто" ("nothing"))
+  ("потом" ("afterwards" "then"))
+  ("очень" ("very"))
+  ("со" ("with"))
+  ("хотеть" ("to want"))
+  ("ли" ("whether" "if"))
+  ("при" ("attached to" "in the presence of" "by" "about"))
+  ("голова" ("head" "mind" "brains") nil ("-голов-"))
+  ("надо" ("over" "above" "ought to"))
+  ("без" ("without"))
+  ("видеть" ("to see") nil ("-вид-"))
+  ("идти" ("to go" "to come"))
+  ("теперь" ("now" "nowadays"))
+  ("тоже" ("also" "as well" "too"))
+  ("стоять" ("to stand" "be" "stand up"))
+  ("друг" ("friend"))
+  ("дом" ("house" "home"))
+
+  ;; 101-150
+  ("сейчас" ("now" "presently" "soon"))
+  ("можно" ("possible" "permitted") nil ("-мочь-"))
+  ("после" ("after" "afterwards"))
+  ("слово" ("word"))
+  ("здесь" ("here"))
+  ("думать" ("to think" "to believe") nil ("-дум-"))
+  ("место" ("place" "seat"))
+  ("спросить" ("to ask") nil ("-прос-"))
+  ("через" ("through" "across"))
+  ("лицо" ("face" "person"))
+  ("что" ("what" "which" "that"))
+  ("тогда" ("then"))
+  ("хороший" ("good" "nice") nil ("-хорош-"))
+  ("каждый" ("every" "each"))
+  ("новый" ("new" "modern") nil ("-нов-"))
+  ("жить" ("to live") nil ("-жи-"))
+  ("должный" ("due" "proper" "should"))
+  ("смотреть" ("to look" "watch"))
+  ("почему" ("why"))
+  ("потому" ("that's why"))
+  ("сторона" ("side" "party"))
+  ("просто" ("simply"))
+  ("нога" ("foot" "leg"))
+  ("сидеть" ("to sit"))
+  ("понять" ("to understand" "to realise"))
+  ("иметь" ("to own" "to have"))
+  ("конечный" ("final" "last"))
+  ("делать" ("to do" "make"))
+  ("вдруг" ("suddenly"))
+  ("над" ("above" "over"))
+  ("взять" ("to take"))
+  ("никто" ("nobody"))
+  ("понимать" ("to understand"))
+  ("казаться" ("to seem" "to appear"))
+  ("работа" ("work" "job"))
+  ("три" ("three"))
+  ("ваш" ("yours"))
+  ("уж" ("really" "already"))
+  ("земля" ("earth" "land" "soil"))
+  ("конец" ("end" "distance"))
+  ("несколько" ("several" "some"))
+  ("час" ("hour" "time"))
+  ("голос" ("voice"))
+  ("город" ("town" "city"))
+  ("последний" ("last" "the latest" "new"))
+
+  ;; 151-200
+  ("пока" ("for the present")) ;; TODO(tazjin): review
+  ("хорошо" ("well") nil ("-хорош-"))
+  ("давать" ("to give" "to grant"))
+  ("вода" ("water"))
+  ("более" ("more"))
+  ("хотя" ("although"))
+  ("всегда" ("always"))
+  ("второй" ("second"))
+  ("куда" ("where" "what for" "much"))
+  ("пойти" ("to go") nil ("-й-"))
+  ("стол" ("table" "desk" "board"))
+  ("ребёнок" ("child" "kid" "infant"))
+  ("увидеть" ("to see"))
+  ("сила" ("strength" "force"))
+  ("отец" ("father"))
+  ("женщина" ("woman"))
+  ("машина" ("car" "machine" "engine"))
+  ("случай" ("case" "occasion" "incident"))
+  ("ночь" ("night"))
+  ("сразу" ("at once" "right away" "just"))
+  ("мир" ("world" "peace"))
+  ("совсем" ("quite" "entirely" "totally"))
+  ("остаться" ("to remain" "to stay"))
+  ("об" ("about" "of"))
+  ("вид" ("appearance" "look" "view"))
+  ("выйти" ("to go out" "to exit" "to come out" "to appear") nil ("-й-"))
+  ("дать" ("to give"))
+  ("работать" ("to work"))
+  ("любить" ("to work"))
+  ("старый" ("old"))
+  ("почти" ("almost"))
+  ("ряд" ("row" "line"))
+  ("оказаться" ("find oneself" "turn out"))
+  ("начало" ("beginning" "origin" "source"))
+  ("твой" ("your" "yours"))
+  ("вопрос" ("question" "matter" "problem") nil ("-прос-"))
+  ("много" ("many" "much"))
+  ("война" ("war"))
+  ("снова" ("again"))
+  ("ответить" ("to answer" "to reply"))
+  ("между" ("between" "among"))
+  ("подумать" ("to think"))
+  ("опять" ("again"))
+  ("белый" ("white"))
+  ("деньги" ("money"))
+  ("значить" ("to mean" "to signify") nil ("-зна-"))
+  ("про" ("about" "for"))
+  ("лишь" ("only" "as soon as"))
+  ("минута" ("minute" "moment"))
+  ("жена" ("wife"))
+
+  ;; 201-300
+  ("посмотреть" ("to watch" "to look" "to inspect") nil ("-смотр-"))
+  ("правда" ("truth") nil ("-правд-"))
+  ("главный" ("main" "chief") nil ("-голов-"))
+  ("страна" ("country") nil ("-стран-"))
+  ("свет" ("light" "world"))
+  ("ждать" ("to wait"))
+  ("мать" ("mother"))
+  ("будто" ("as if" "as though"))
+  ("никогда" ("never"))
+  ("товариш" ("comrade" "friend"))
+  ("дорога" ("road" "way" "journey"))
+  ("однако" ("however" "although"))
+  ("лежать" ("to lie" "to be situated"))
+  ("именно" ("namely" "just" "exactly") nil ("-имя-"))
+  ("окно" ("window"))
+  ("никакой" ("no" "none"))
+  ("найти" ("to find" "to discover") nil ("-й-"))
+  ("писать" ("to write"))
+  ("комната" ("room"))
+  ("Москва" ("Moscow"))
+  ("часть" ("part" "share" "department"))
+  ("вообще" ("in general" "altogether" "on the whole") nil ("-общ-"))
+  ("книга" ("book"))
+  ("маленький" ("small" "little"))
+  ("улица" ("street"))
+  ("режить" ("to decide" "to solve"))
+  ("далекий" ("distant" "remote"))
+  ("душа" ("soul" "spirit"))
+  ("чуть" ("hardly" "slightly"))
+  ("вернуться" ("to return"))
+  ("утро" ("morning"))
+  ("некоторый" ("some"))
+  ("считать" ("to count" "to consider"))
+  ("сколько" ("how much" "how many"))
+  ("помнить" ("to remember"))
+  ("вечер" ("evening"))
+  ("пол" ("floor" "gender"))
+  ("таки" ("after all"))
+  ("получить" ("to receive" "to get" "to obtain"))
+  ("народ" ("people" "nation"))
+  ("плечо" ("shoulder" "upper arm"))
+  ("хоть" ("even" "if you want" "though"))
+  ("сегодня" ("today"))
+  ("бог" ("god"))
+  ("вместе" ("together"))
+  ("взгляд" ("look" "glance" "view"))
+  ("ходить" ("to go" "to walk") nil ("-ход-"))
+  ("зачем" ("what for" "why"))
+  ("советский" ("Soviet"))
+  ("русский" ("Russian"))
+  ("бывать" ("to be" "to visit" "to happen"))
+  ("полный" ("full" "complete" "whole"))
+  ("прийти" ("to arrive" "to come") nil ("-й-"))
+  ("палец" ("finger" "toe"))
+  ("Россия" ("Russia"))
+  ("любой" ("any" "every"))
+  ("история" ("history" "story" "event"))
+  ("наконец" ("finally" "at least"))
+  ("мысль" ("thought" "idea"))
+  ("узнать" ("to know" "to learn" "to recognise") nil ("-зна-"))
+  ("назад" ("back" "backwards" "ago"))
+  ("общий" ("general" "common") nil ("-общ-"))
+  ("заметить" ("to notice" "to observe"))
+  ("словно" ("as if" "like"))
+  ("прошлый" ("past" "vergangen") nil ("-й-"))
+  ("уйти" ("to leave" "to go away") nil ("-й-"))
+  ("известный" ("well-known" "famous"))
+  ("давно" ("long ago"))
+  ("слышать" ("to hear"))
+  ("слушать" ("to listen" "to hear"))
+  ("бояться" ("to be afraid" "fear"))
+  ("сын" ("son"))
+  ("нельзя" ("it is impossible" "can't"))
+  ("прямо" ("straight" "frankly"))
+  ("долго" ("for a long time"))
+  ("быстро" ("fast" "quickly"))
+  ("лес" ("forest"))
+  ("похожий" ("similar" "alike") nil ("-ход-"))
+  ("пора" ("time" "pore"))
+  ("пять" ("five"))
+  ("глядеть" ("to look" "to gaze"))
+  ("оно" ("it"))
+  ("сесть" ("to sit"))
+  ("имя" ("name") nil ("-имя-"))
+  ("ж" ("and" "as for" "but"))
+  ("разговор" ("conversation" "talk") nil ("-говор-"))
+  ("тело" ("body"))
+  ("молодой" ("young"))
+  ("стена" ("wall"))
+  ("красный" ("red"))
+  ("читать" ("to read"))
+  ("право" ("right"))
+  ("старик" ("old man"))
+  ("ранний" ("early"))
+  ("хотеться" ("to want" "to like"))
+  ("мама" ("mummy" "mum"))
+  ("оставаться" ("to remain" "to stay"))
+  ("высокий" ("tall" "high"))
+  ("путь" ("way" "track" "path"))
+  ("поэтому" ("therefore"))
+
+  ;; 301-400
+  ("совершенно" ("absolutely" "quite"))
+  ("кроме" ("except" "besides"))
+  ("тысяча" ("a thousand"))
+  ("месяц" ("month"))
+  ("брать" ("to take" "to hire"))
+  ("написать" ("to write"))
+  ("целый" ("intact" "whole" "entire"))
+  ("огромный" ("huge" "enormous"))
+  ("начинать" ("to begin"))
+  ("спина" ("back"))
+  ("настоящий" ("present" "real" "true"))
+  ("пусть" ("let's" "though"))
+  ("язык" ("tongue" "language"))
+  ("точно" ("exactly"))
+  ("среди" ("among"))
+  ("чуствовать" ("to feel"))
+  ("сердце" ("heart"))
+  ("вести" ("to lead"))
+  ("иногда" ("sometimes"))
+  ("мальчик" ("boy"))
+  ("успеть" ("to be in time" "to be successful"))
+  ("небо" ("sky"))
+  ("живой" ("living" "lively" "alive"))
+  ("смерть" ("death"))
+  ("продолжать" ("to continue"))
+  ("девушка" ("girl"))
+  ("образ" ("shape" "form" "image"))
+  ("ко" ("to" "towards" "by"))
+  ("забыть" ("to forget"))
+  ("вокруг" ("around"))
+  ("письмо" ("letter"))
+  ("власть" ("power"))
+  ("чёрный" ("black"))
+  ("пройти" ("to pass" "go by" "be over") nil ("-й-"))
+  ("появиться" ("to appear" "to show up"))
+  ("воздух" ("air"))
+  ("разный" ("different"))
+  ("выходить" ("to go out" "to exit") ("MR says 'to nurse'??") ("-ход-"))
+  ("просить" ("to ask"))
+  ("брат" ("brat"))
+  ("собственный" ("one's own"))
+  ("отношение" ("relationship" "attitude"))
+  ("затем" ("then" "after that"))
+  ("пытаться" ("to try"))
+  ("показать" ("to show" "to display"))
+  ("вспомнить" ("to remember" "to recall"))
+  ("система" ("system"))
+  ("четыре" ("four"))
+  ("квартира" ("flat" "apartment"))
+  ("держать" ("to hold" "to keep"))
+  ("также" ("also" "as well" "too"))
+  ("любовь" ("love"))
+  ("солдат" ("soldier"))
+  ("откуда" ("from where"))
+  ("чтоб" ("that" "in order that"))
+  ("называть" ("to call" "to name"))
+  ("третий" ("third"))
+  ("хозяин" ("master" "boss" "host"))
+  ("вроде" ("like" "not unlike"))
+  ("уходить" ("to leave" "to go away") nil ("-ход-"))
+  ("подойти" ("to approach" "to come up") nil ("-й-"))
+  ("поднять" ("to lift" "to raise"))
+  ("спрашивать" ("to ask" "to inquire"))
+  ("начальник" ("chief" "head" "superior"))
+  ("оба" ("both"))
+  ("бросить" ("to throw"))
+  ("школа" ("school"))
+  ("парень" ("boy" "fellow" "guy"))
+  ("кровь" ("blood"))
+  ("двадцать" ("twenty"))
+  ("солнце" ("sun"))
+  ("неделя" ("week"))
+  ("послать" ("to send" "to dispatch"))
+  ("находиться" ("to be found" "to turn up") nil ("-ход-"))
+  ("ребята" ("guys" "children"))
+  ("поставить" ("to put" "to place" "to set"))
+  ("встать" ("to get up" "to rise" "to stand up"))
+  ("например" ("for example" "for instance"))
+  ("шаг" ("step"))
+  ("мужчина" ("man" "male"))
+  ("равно" ("alike" "in like manner"))
+  ("нос" ("nose"))
+  ("мало" ("little" "few"))
+  ("внимание" ("attention"))
+  ("капитан" ("captain" "master"))
+  ("ухо" ("ear"))
+  ("туда" ("to there"))
+  ("сюда" ("to here"))
+  ("играть" ("to play"))
+  ("следовать" ("to follow" "to come next"))
+  ("рассказать" ("to tell" "to narrate"))
+  ("великий" ("great"))
+  ("действительно" ("indeed" "really"))
+  ("слишком" ("too much"))
+  ("тяжёлый" ("heavy"))
+  ("спать" ("to sleep"))
+  ("оставить" ("to leave" "to abandon"))
+  ("войти" ("to enter" "to come in") nil ("-й-"))
+  ("длинный" ("long"))
+
+  ;; 401 - 500
+  ("чувство" ("feeling"))
+  ("иолчать" ("to keep silence" "make no complaint" "say nothing"))
+  ("рассказывать" ("to tell" "narrate"))
+  ("отвечать" ("to answer" "to reply"))
+  ("становиться" ("to stand" "to become"))
+  ("остановиться" ("to stop"))
+  ("берег" ("bank" "shore" "coast"))
+  ("семья" ("family"))
+  ("искать" ("to search"))
+  ("генерал" ("general"))
+  ("момент" ("moment" "instant"))
+  ("десять" ("ten"))
+  ("начать" ("to begin"))
+  ("следуюший" ("next" "following"))
+  ("личный" ("personal"))
+  ("труд" ("labour" "work"))
+  ("верить" ("to believe"))
+  ("группа" ("group"))
+  ("немного" ("a little"))
+  ("впрочем" ("however" "though"))
+  ("видно" ("evidently" "obviously"))
+  ("являться" ("to appear"))
+  ("муж" ("husband"))
+  ("разве" ("really?" "perhaps") ("when pondering something"))
+  ("движение" ("movement" "motion"))
+  ("порядок" ("order"))
+  ("ответ" ("answer" "reply"))
+  ("тихо" ("quietly" "silently") ("also as exclamation"))
+  ("знакомый" ("familiar" "acquainted"))
+  ("газета" ("newspaper"))
+  ("помощь" ("help"))
+  ("сильный" ("strong" "powerful"))
+  ("скорый" ("quick" "fast"))
+  ("собака" ("dog"))
+  ("дерево" ("tree"))
+  ("снег" ("snow"))
+  ("сон" ("dream"))
+  ("смысл" ("sense" "meaning" "purpose") ("making sense" "in the sense"))
+  ("смочь" ("to be able") ("св"))
+  ("против" ("against" "opposite" "contrary to"))
+  ("бежать" ("to run" "to hurry"))
+  ("двор" ("yard" "court"))
+  ("форма" ("form" "shape" "uniform"))
+  ("простой" ("simple" "easy" "plain"))
+  ("приехать" ("to arrive" "to come"))
+  ("иной" ("different" "other"))
+  ("кричать" ("to cry" "to shout"))
+  ("возможность" ("possibility" "opportunity" "chance"))
+  ("общество" ("society"))
+  ("зелёный" ("green"))
+  ("грудь" ("breast" "chest"))
+  ("угол" ("corner" "angle"))
+  ("открыть" ("to open"))
+  ("происходить" ("to happen" "to occur" "to take place"))
+  ("ладно" ("well" "all right" "okay"))
+  ("чёрный" ("black") ("noun (m.): as in 'she wears black'"))
+  ("век" ("century" "age"))
+  ("карман" ("pocket"))
+  ("ехать" ("to go" "ride" "drive" "travel"))
+  ("немец" ("German"))
+  ("наверное" ("probably" "most likely"))
+  ("губа" ("lip"))
+  ("дядя" ("uncle"))
+  ("приходить" ("to come" "to arrive"))
+  ("часто" ("often"))
+  ("домой" ("home") ("as in direction"))
+  ("огонь" ("fire"))
+  ("писатель" ("writer"))
+  ("армия" ("army"))
+  ("состояние" ("state" "condition" "fortune"))
+  ("зуб" ("tooth"))
+  ("очередь" ("queue" "line" "turn"))
+  ("кой" ("which") ("old-fashioned, literary (in set expressions)"))
+  ("подняться" ("to rise" "to climb"))
+  ("камень" ("stone"))
+  ("гость" ("guest"))
+  ("показаться" ("to appear" "to come in sight"))
+  ("ветер" ("window"))
+  ("собираться" ("to gather" "to assemble" "to intend") ("TODO: intend??"))
+  ("попасть" ("to hit" "to find oneself") ("to get (in phrases)"))
+  ("принять" ("to take" "to admit" "to accept"))
+  ("сначала" ("at first" "from the beginning"))
+  ("либо" ("or"))
+  ("поехать" ("to depart" "to set off"))
+  ("услышать" ("to hear"))
+  ("уметь" ("to be able" "know" "can"))
+  ("случиться" ("to happen"))
+  ("странный" ("strange"))
+  ("единственный" ("only" "sole"))
+  ("рота" ("company") ("(military)"))
+  ("закон" ("law" "act" "statute"))
+  ("короткий" ("short"))
+  ("море" ("sea"))
+  ("добрый" ("kind"))
+  ("тёмный" ("dark"))
+  ("гора" ("mountain" "hill"))
+  ("врач" ("doctor"))
+  ("край" ("border, edge" "land, country"))
+  ("стараться" ("to try" "to endeavour"))
+  ("лучший" ("better" "best"))
+
+  ;; 501 - 600
+  ("река" ("river"))
+  ("военный" ("military"))
+  ("мера" ("measure" "step"))
+  ("страшный" ("terrible" "frightful"))
+  ("вполне" ("quite" "fully"))
+  ("звать" ("to call"))
+  ("произойти" ("to happen" "to occur" "take place"))
+  ("вперед" ("forward"))
+  ("медленно" ("slowly"))
+  ("возле" ("by" "near" "close by"))
+  ("никак" ("in no way" "by no means"))
+  ("заниматься" ("to be occupied" "to engage"))
+  ("действие" ("action" "effort"))
+  ("довольно" ("enough" "rather"))
+  ("вещь" ("thing"))
+  ("необходимый" ("necessary") ("not possible to go around"))
+  ("ход" ("move"))
+  ("боль" ("pain"))
+  ("судьба" ("fate" "fortune" "destiny"))
+  ("причина" ("cause" "reason" "motive"))
+  ("положить" ("to lay down" "put down" "place"))
+  ("едва" ("hardly" "just" "barely"))
+  ("черта" ("line" "boundary" "trait"))
+  ("девочка" ("girl" "little girl"))
+  ("лёгкий" ("light" "easy"))
+  ("волос" ("hair"))
+  ("купить" ("to buy" "purchase"))
+  ("номер" ("number" "size" "room" "issue"))
+  ("основной" ("main"))
+  ("широкий" ("wide"))
+  ("умереть" ("to die"))
+  ("далеко" ("far" "far off"))
+  ("плохо" ("badly"))
+  ("глава" ("head" "chief"))
+  ("красивый" ("beautiful"))
+  ("серый" ("grey" "dull"))
+  ("пить" ("to drink"))
+  ("командир" ("commander" "officer"))
+  ("обычно" ("usually"))
+  ("партия" ("party"))
+  ("проблема" ("problem" "issue"))
+  ("страх" ("fear"))
+  ("проходить" ("to pass" "go" "study"))
+  ("ясно" ("clear" "clearly"))
+  ("снять" ("to take away" "take off"))
+  ("бумага" ("paper"))
+  ("герой" ("hero"))
+  ("пара" ("pair" "couple"))
+  ("государство" ("State"))
+  ("деревня" ("village"))
+  ("речь" ("speech"))
+  ("начаться" ("to begin"))
+  ("средство" ("means" "remedy"))
+  ("положение" ("position" "posture" "condition" "state"))
+  ("связь" ("tie, bond" "connection, relation"))
+  ("небольшой" ("small" "not great"))
+  ("представлять" ("to present" "introduce" "imagine"))
+  ("завтра" ("tomorrow"))
+  ("объяснить" ("to explain"))
+  ("пустой" ("empty" "hollow" "idle"))
+  ("произнести" ("to pronounce" "say" "utter"))
+  ("человеческий" ("human"))
+  ("нравиться" ("to please" "be likeable to"))
+  ("однажды" ("once" "one day"))
+  ("мимо" ("past" "by"))
+  ("иначе" ("otherwise" "differently|"))
+  ("существровать" ("to exist" "to be"))
+  ("класс" ("class"))
+  ("удаться" ("turn out well" "succeed" "manage"))
+  ("толстый" ("thick" "heavy" "fat"))
+  ("цель" ("goal" "object" "target"))
+  ("сквозь" ("through"))
+  ("прийтись" ("to fit" "fall" "have to") ("тебе придётся - you have to"))
+  ("чистый" ("clean" "pure"))
+  ("знать" ("to know"))
+  ("прежний" ("former"))
+  ("профессор" ("professor"))
+  ("господин" ("gentleman" "Mr."))
+  ("счастье" ("happiness" "luck"))
+  ("худой" ("thin" "skinny"))
+  ("дух" ("spirit"))
+  ("план" ("plan"))
+  ("чужой" ("somebody else's" "strange" "foreign"))
+  ("зал" ("hall"))
+  ("представить" ("to present" "produce" "introduce"))
+  ("особый" ("special"))
+  ("директор" ("director" "manager"))
+  ("бывший" ("former" "ex-"))
+  ("память" ("memory"))
+  ("близкий" ("near" "similar" "intimate"))
+  ("сей" ("this"))
+  ("результат" ("result" "outcome"))
+  ("больной" ("sick"))
+  ("данный" ("given" "present"))
+  ("кстати" ("to the point" "at the same time"))
+  ("назвать" ("to call" "name"))
+  ("след" ("track" "footprint"))
+  ("улыбаться" ("to smile") ("нсв"))
+  ("бутылка" ("bottle"))
+
+  ;; 601 - 700
+  ("трудно" ("with difficulty"))
+  ("условие" ("condition" "term"))
+  ("прежде" ("before"))
+  ("ум" ("mind" "brains" "intellect"))
+  ("улыбнуться" ("to smile"))
+  ("процесс" ("process"))
+  ("картина" ("picture" "painting"))
+  ("вместо" ("instead"))
+  ("старший" ("elder" "senior"))
+  ("легко" ("easily" "lightly"))
+  ("центр" ("center"))
+  ("подобный" ("similar" "like"))
+  ("возможно" ("possible") ("as ... as possible"))
+  ("около" ("by" "near"))
+  ("смеяться" ("to laugh"))
+  ("сто" ("hundred"))
+  ("будущее" ("future"))
+  ("хватать" ("to snatch" "to seize" "to suffice") ("нсв"))
+  ("число" ("number"))
+  ("всякое" ("any" "every"))
+  ("рубль" ("ruble"))
+  ("почувствовать" ("to feel") ("св"))
+  ("принести" ("to bring"))
+  ("вера" ("faith" "belief"))
+  ("вовсе" ("quiet" "not ... at all"))
+  ("удар" ("blow" "stroke"))
+  ("телефон" ("telephone"))
+  ("колено" ("knee"))
+  ("согласиться" ("to agree" "to consent"))
+  ("мало" ("little" "few" "not enough"))
+  ("коридор" ("corridor" "passage"))
+  ("мужик" ("man"))
+  ("правый" ("right"))
+  ("автор" ("author"))
+  ("холодный" ("cold" "cool"))
+  ("хватит" ("to snatch" "to seize" "to suffice") ("св"))
+  ("многие" ("many"))
+  ("встреча" ("meeting" "reception"))
+  ("кабинет" ("study" "room" "office suite"))
+  ("документ" ("document"))
+  ("самолёт" ("airplane"))
+  ("вниз" ("down" "downwards"))
+  ("принимать" ("to take" "to admit" "to accept"))
+  ("игра" ("game" "play"))
+  ("рассказ" ("story"))
+  ("хлеб" ("bread"))
+  ("развитие" ("development"))
+  ("убить" ("to kill"))
+  ("родной" ("own" "native" "dear"))
+  ("открытый" ("open"))
+  ("менее" ("less"))
+  ("предложить" ("to offer" "to propose" "to suggest"))
+  ("жёлтый" ("yellow"))
+  ("приходиться" ("to fit" "to fall" "to have to"))
+  ("выпить" ("to drink"))
+  ("крикнуть" ("to cry" "to shout"))
+  ("трубка" ("tube" "roll" "pipe"))
+  ("враг" ("enemy"))
+  ("показывать" ("to show" "to display"))
+  ("двое" ("two") ("cardinal number"))
+  ("доктор" ("doctor"))
+  ("ладонь" ("palm"))
+  ("вызвать" ("to call" "to send"))
+  ("спокойно" ("quietly"))
+  ("попросить" ("to ask"))
+  ("наука" ("science"))
+  ("лейтенант" ("lieutenant"))
+  ("служба" ("service" "work"))
+  ("оказываться" ("to turn out" "to find oneself"))
+  ("привести" ("to bring"))
+  ("сорок" ("forty"))
+  ("счёт" ("bill" "account"))
+  ("возвращаться" ("to return"))
+  ("золотой" ("golden"))
+  ("местный" ("local"))
+  ("кухня" ("kitchen"))
+  ("крупный" ("large" "big" "prominent"))
+  ("решение" ("decision" "conclusion"))
+  ("молодая" ("bride" "young"))
+  ("тридцать" ("thirty"))
+  ("роман" ("novel" "romance"))
+  ("компания" ("company"))
+  ("частый" ("frequent"))
+  ("российский" ("Russian"))
+  ("рабочий" ("worky"))
+  ("потерять" ("to lose"))
+  ("течение" ("current"))
+  ("синий" ("dark blue"))
+  ("столько" ("so much" "so many"))
+  ("тёплый" ("warm"))
+  ("метр" ("metre"))
+  ("достать" ("to reach" "get" "obtain"))
+  ("железный" ("ferreous" "iron"))
+  ("институт" ("institute"))
+  ("сообщить" ("to report" "to let know"))
+  ("интерес" ("interest"))
+  ("обычный" ("usual" "ordinary"))
+  ("появляться" ("to appear" "to show up"))
+  ("упасть" ("to fall")))
+
+(provide 'russian-words)
diff --git a/users/tazjin/rustfmt.toml b/users/tazjin/rustfmt.toml
new file mode 100644
index 0000000000..0c719dcfec
--- /dev/null
+++ b/users/tazjin/rustfmt.toml
@@ -0,0 +1,22 @@
+edition = "2021"
+newline_style = "Unix"
+
+# Default code with is 100 characters, comments should follow
+# suit.
+wrap_comments = true
+
+# The default of this option creates hard-to-read nesting of
+# conditionals, turn it off.
+combine_control_expr = false
+
+# Group imports by module, but no higher. This avoids hard-to-read
+# nested use statements.
+imports_granularity = "Module"
+
+# Avoid vertical visual clutter by unnecessarily exploding
+# block-like arguments.
+overflow_delimited_expr = true
+
+# Miscellaneous
+format_code_in_doc_comments = true
+normalize_comments = true
diff --git a/users/tazjin/tgsa/.gitignore b/users/tazjin/tgsa/.gitignore
new file mode 100644
index 0000000000..29e65519ba
--- /dev/null
+++ b/users/tazjin/tgsa/.gitignore
@@ -0,0 +1,3 @@
+result
+/target
+**/*.rs.bk
diff --git a/users/tazjin/tgsa/Cargo.lock b/users/tazjin/tgsa/Cargo.lock
new file mode 100644
index 0000000000..d5d034dde4
--- /dev/null
+++ b/users/tazjin/tgsa/Cargo.lock
@@ -0,0 +1,1296 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
+
+[[package]]
+name = "ascii"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crimp"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe8f9a320ad9c1a2e3bacedaa281587bd297fb10a10179fd39f777049d04794"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa 0.4.8",
+ "matches",
+ "phf",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.53+curl-7.82.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8092905a5a9502c312f223b2775f57ec5c5b715f9a15ee9d2a8591d1364a0352"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "deflate"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70"
+dependencies = [
+ "adler32",
+ "gzip-header",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "dtoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "ego-tree"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591"
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gzip-header"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba"
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand 0.8.5",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom 0.2.6",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rouille"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18b2380c42510ef4a28b5f228a174c801e0dec590103e215e60812e2e2f34d05"
+dependencies = [
+ "base64",
+ "brotli",
+ "chrono",
+ "deflate",
+ "filetime",
+ "multipart",
+ "num_cpus",
+ "percent-encoding",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "threadpool",
+ "time",
+ "tiny_http",
+ "url",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "scraper"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e02aa790c80c2e494130dec6a522033b6a23603ffc06360e9fe6c611ea2c12"
+dependencies = [
+ "cssparser",
+ "ego-tree",
+ "getopts",
+ "html5ever",
+ "matches",
+ "selectors",
+ "smallvec",
+ "tendril",
+]
+
+[[package]]
+name = "selectors"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+dependencies = [
+ "bitflags",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "matches",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4"
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+dependencies = [
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
+dependencies = [
+ "sha1_smol",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "string_cache"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "tgsa"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "crimp",
+ "ego-tree",
+ "rouille",
+ "scraper",
+ "url",
+]
+
+[[package]]
+name = "thin-slice"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+dependencies = [
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "tiny_http"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39"
+dependencies = [
+ "ascii",
+ "chrono",
+ "chunked_transfer",
+ "log",
+ "url",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
diff --git a/users/tazjin/tgsa/Cargo.toml b/users/tazjin/tgsa/Cargo.toml
new file mode 100644
index 0000000000..105333c942
--- /dev/null
+++ b/users/tazjin/tgsa/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "tgsa"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0"
+crimp = "0.2"
+ego-tree = "0.6" # in tandem with 'scraper'
+rouille = "3.5"
+scraper = "0.12"
+url = "2.2"
diff --git a/users/tazjin/tgsa/default.nix b/users/tazjin/tgsa/default.nix
new file mode 100644
index 0000000000..ef8842ea26
--- /dev/null
+++ b/users/tazjin/tgsa/default.nix
@@ -0,0 +1,10 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+
+  buildInputs = with pkgs; [
+    pkgconfig
+    openssl
+  ];
+}
diff --git a/users/tazjin/tgsa/src/main.rs b/users/tazjin/tgsa/src/main.rs
new file mode 100644
index 0000000000..ed72569f92
--- /dev/null
+++ b/users/tazjin/tgsa/src/main.rs
@@ -0,0 +1,343 @@
+use anyhow::{anyhow, Context, Result};
+use std::collections::HashMap;
+use std::sync::RwLock;
+use std::time::{Duration, Instant};
+
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+struct TgLink {
+    username: String,
+    message_id: usize,
+}
+
+impl TgLink {
+    fn human_friendly_url(&self) -> String {
+        format!("t.me/{}/{}", self.username, self.message_id)
+    }
+
+    fn to_url(&self) -> String {
+        format!("https://t.me/{}/{}?embed=1", self.username, self.message_id)
+    }
+
+    fn parse(url: &str) -> Option<Self> {
+        let url = url.strip_prefix("/")?;
+        let parsed = url::Url::parse(url).ok()?;
+
+        if parsed.host()? != url::Host::Domain("t.me") {
+            // only t.me links are supported
+            return None;
+        }
+
+        let parts = parsed.path_segments()?.collect::<Vec<&str>>();
+        if parts.len() != 2 {
+            // only message links are supported
+            return None;
+        }
+
+        Some(TgLink {
+            username: parts[0].into(),
+            message_id: parts[1].parse().ok()?,
+        })
+    }
+}
+
+fn fetch_embed(link: &TgLink) -> Result<String> {
+    println!("fetching {}#{}", link.username, link.message_id);
+    let response = crimp::Request::get(&link.to_url())
+        .send()
+        .context("failed to fetch embed data")?
+        .as_string()
+        .context("failed to decode embed data")?
+        .error_for_status(|resp| {
+            anyhow!("telegram request failed: {} ({})", resp.body, resp.status)
+        })?;
+
+    Ok(response.body)
+}
+
+#[derive(Debug)]
+struct TgMessage {
+    author: String,
+    message: Option<String>,
+    photos: Vec<String>,
+    videos: Vec<String>,
+    has_audio: bool,
+}
+
+fn extract_photo_url(style: &str) -> Option<&str> {
+    let url_start = style.find("url('")? + 5;
+    let url_end = style.find("')")?;
+
+    Some(&style[url_start..url_end])
+}
+
+fn parse_tgmessage(embed: &str) -> Result<TgMessage> {
+    use scraper::{Html, Selector};
+
+    let doc = Html::parse_document(embed);
+
+    let author_sel = Selector::parse("a.tgme_widget_message_owner_name").unwrap();
+    let author = doc
+        .select(&author_sel)
+        .next()
+        .ok_or_else(|| anyhow!("failed to find message author"))?
+        .text()
+        .collect::<Vec<&str>>()
+        .concat();
+
+    let msg_sel = Selector::parse("div.tgme_widget_message_text.js-message_text").unwrap();
+
+    // The ElementRef::text() iterator does not yield newlines present
+    // in the message, so it is partially reimplemented here.
+    let message = if let Some(msg_elem) = doc.select(&msg_sel).next() {
+        use ego_tree::iter::Edge;
+        use scraper::node::Node;
+
+        let mut out = String::new();
+
+        for edge in &mut msg_elem.traverse() {
+            if let Edge::Open(node) = edge {
+                match node.value() {
+                    Node::Text(ref text) => out.push_str(&*text),
+                    Node::Element(elem) if elem.name() == "br" => out.push_str("\n"),
+                    _ => {}
+                }
+            }
+        }
+
+        Some(out)
+    } else {
+        // Not all Telegram messages have a textual message.
+        None
+    };
+
+    let photo_sel = Selector::parse("a.tgme_widget_message_photo_wrap").unwrap();
+    let mut photos = vec![];
+
+    for photo in doc.select(&photo_sel) {
+        if let Some(style) = photo.value().attr("style") {
+            if let Some(url) = extract_photo_url(style) {
+                photos.push(url.to_string())
+            }
+        }
+    }
+
+    let video_sel = Selector::parse("i.tgme_widget_message_video_thumb").unwrap();
+    let mut videos = vec![];
+
+    for video in doc.select(&video_sel) {
+        if let Some(style) = video.value().attr("style") {
+            if let Some(url) = extract_photo_url(style) {
+                videos.push(url.to_string())
+            }
+        }
+    }
+
+    let audio_sel = Selector::parse("audio.tgme_widget_message_voice.js-message_voice").unwrap();
+    let mut has_audio = false;
+    if doc.select(&audio_sel).next().is_some() {
+        has_audio = true;
+    }
+
+    Ok(TgMessage {
+        author,
+        message,
+        photos,
+        videos,
+        has_audio,
+    })
+}
+
+// create a permanent media url that tgsa can redirect if telegram
+// changes its upstream links.
+//
+// assumes that tgsa lives at tgsa.tazj.in (which it does)
+fn media_url(link: &TgLink, idx: usize) -> String {
+    format!(
+        "https://tgsa.tazj.in/img/{}/{}/{}",
+        link.username, link.message_id, idx
+    )
+}
+
+fn to_bbcode(link: &TgLink, msg: &TgMessage) -> String {
+    let mut out = String::new();
+
+    out.push_str(&format!("[quote=\"{}\"]\n", msg.author));
+
+    for video in 0..msg.videos.len() {
+        out.push_str(&format!("[url=\"{}\"]", link.to_url()));
+
+        // video thumbnail links are appended to the photos, hence the
+        // addition here
+        out.push_str(&format!(
+            "[img]{}[/img]",
+            media_url(link, video + msg.photos.len())
+        ));
+
+        out.push_str("[/url]\n");
+        out.push_str("[sub](Click thumbnail to open video)[/sub]\n")
+    }
+
+    for photo in 0..msg.photos.len() {
+        out.push_str(&format!("[timg]{}[/timg]\n", media_url(link, photo)));
+    }
+
+    if msg.has_audio {
+        out.push_str(&format!(
+            "[i]This message has audio attached. Go [url=\"{}\"]to Telegram[/url] to listen.[/i]",
+            link.to_url(),
+        ));
+    }
+
+    if let Some(message) = &msg.message {
+        out.push_str(message);
+    }
+
+    out.push_str("\n[/quote]\n");
+
+    out.push_str(&format!(
+        "[sub](from [url=\"{}\"]{}[/url], via [url=\"https://tgsa.tazj.in\"]tgsa[/url])[/sub]\n",
+        link.to_url(),
+        link.human_friendly_url(),
+    ));
+
+    out
+}
+
+// cache everything for one hour
+const CACHE_EXPIRY: Duration = Duration::from_secs(60 * 60);
+
+#[derive(Clone)]
+struct TgPost {
+    bbcode: String,
+    at: Instant,
+    media: Vec<String>,
+}
+
+type Cache = RwLock<HashMap<TgLink, TgPost>>;
+
+fn fetch_with_cache(cache: &Cache, link: &TgLink) -> Result<TgPost> {
+    if let Some(entry) = cache.read().unwrap().get(&link) {
+        if Instant::now() - entry.at < CACHE_EXPIRY {
+            println!("serving {}#{} from cache", link.username, link.message_id);
+            return Ok(entry.clone());
+        }
+    }
+
+    // limit concurrent fetching
+    // TODO(tazjin): per link?
+    let mut writer = cache.write().unwrap();
+
+    let embed = fetch_embed(&link)?;
+    let mut msg = parse_tgmessage(&embed)?;
+    let bbcode = to_bbcode(&link, &msg);
+
+    let mut media = vec![];
+    media.append(&mut msg.photos);
+    media.append(&mut msg.videos);
+
+    let post = TgPost {
+        bbcode,
+        media,
+        at: Instant::now(),
+    };
+
+    writer.insert(link.clone(), post.clone());
+
+    Ok(post)
+}
+
+fn handle_img_redirect(cache: &Cache, img_path: &str) -> Result<rouille::Response> {
+    // img_path:
+    //
+    // RWApodcast/113/1
+    // ^          ^   ^
+    // |          |   |
+    // |          |   image (0-indexed)
+    // |          post ID
+    // username
+
+    let img_parts: Vec<&str> = img_path.split("/").collect();
+
+    if img_parts.len() != 3 {
+        println!("invalid image link: {}", img_path);
+        return Err(anyhow!("not a valid image link: {}", img_path));
+    }
+
+    let link = TgLink {
+        username: img_parts[0].into(),
+        message_id: img_parts[1].parse().context("failed to parse message_id")?,
+    };
+
+    let img_idx: usize = img_parts[2].parse().context("failed to parse img_idx")?;
+    let post = fetch_with_cache(cache, &link)?;
+
+    if img_idx >= post.media.len() {
+        return Err(anyhow!(
+            "there is no {}. image in {}/{}",
+            img_idx,
+            link.username,
+            link.message_id
+        ));
+    }
+
+    Ok(rouille::Response::redirect_303(post.media[img_idx].clone()))
+}
+
+fn handle_tg_link(cache: &Cache, link: &TgLink) -> Result<rouille::Response> {
+    let post = fetch_with_cache(cache, link)?;
+    Ok(rouille::Response::text(post.bbcode))
+}
+
+fn main() {
+    crimp::init();
+
+    let cache: Cache = RwLock::new(HashMap::new());
+
+    rouille::start_server("0.0.0.0:8472", move |request| {
+        let response = loop {
+            if request.raw_url().starts_with("/img/") {
+                break handle_img_redirect(&cache, &request.raw_url()[5..]);
+            }
+
+            break match TgLink::parse(request.raw_url()) {
+                None => Ok(rouille::Response::text(
+                    r#"tgsa
+----
+
+this is a stupid program that lets you turn telegram message links
+into BBcode suitable for pasting on somethingawful dot com
+
+you can use it by putting a valid telegram message link in the url and
+waiting for some bbcode to show up.
+
+for example:
+
+  https://tgsa.tazj.in/https://t.me/RWApodcast/113
+
+yes, that looks stupid, but it works
+
+if you see this message and think you did the above correctly, you
+didn't. try again. idiot.
+
+pm me on the forums if this makes you mad or something.
+"#,
+                )),
+                Some(link) => handle_tg_link(&cache, &link),
+            };
+        };
+
+        match response {
+            Ok(resp) => resp,
+            Err(err) => {
+                println!("something failed: {}", err);
+                rouille::Response::text(format!(
+                    r#"ugh, something broke: {}
+
+nobody has been alerted about this and it has probably not been
+logged. pm me on the forums if you think it's important."#,
+                    err
+                ))
+            }
+        }
+    });
+}
diff --git a/users/tazjin/wallpapers/bio_thehost_1920.webp b/users/tazjin/wallpapers/bio_thehost_1920.webp
new file mode 100644
index 0000000000..1b904c06fa
--- /dev/null
+++ b/users/tazjin/wallpapers/bio_thehost_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/busride2_1920.webp b/users/tazjin/wallpapers/busride2_1920.webp
new file mode 100644
index 0000000000..ad6ec446f6
--- /dev/null
+++ b/users/tazjin/wallpapers/busride2_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/by_belltowers_2880.webp b/users/tazjin/wallpapers/by_belltowers_2880.webp
new file mode 100644
index 0000000000..f7477f1689
--- /dev/null
+++ b/users/tazjin/wallpapers/by_belltowers_2880.webp
Binary files differdiff --git a/users/tazjin/wallpapers/by_crossing_2560.webp b/users/tazjin/wallpapers/by_crossing_2560.webp
new file mode 100644
index 0000000000..efa263790b
--- /dev/null
+++ b/users/tazjin/wallpapers/by_crossing_2560.webp
Binary files differdiff --git a/users/tazjin/wallpapers/by_gathering3_2880.webp b/users/tazjin/wallpapers/by_gathering3_2880.webp
new file mode 100644
index 0000000000..e6b83bdcd4
--- /dev/null
+++ b/users/tazjin/wallpapers/by_gathering3_2880.webp
Binary files differdiff --git a/users/tazjin/wallpapers/by_mainservers1_1920.webp b/users/tazjin/wallpapers/by_mainservers1_1920.webp
new file mode 100644
index 0000000000..f88d237e2b
--- /dev/null
+++ b/users/tazjin/wallpapers/by_mainservers1_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/by_warmachines1_2560.webp b/users/tazjin/wallpapers/by_warmachines1_2560.webp
new file mode 100644
index 0000000000..848bf62bd7
--- /dev/null
+++ b/users/tazjin/wallpapers/by_warmachines1_2560.webp
Binary files differdiff --git a/users/tazjin/wallpapers/by_warmachines3_1920.webp b/users/tazjin/wallpapers/by_warmachines3_1920.webp
new file mode 100644
index 0000000000..6002ad695a
--- /dev/null
+++ b/users/tazjin/wallpapers/by_warmachines3_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/clever-man_2880.webp b/users/tazjin/wallpapers/clever-man_2880.webp
new file mode 100644
index 0000000000..eb4d3f1bfa
--- /dev/null
+++ b/users/tazjin/wallpapers/clever-man_2880.webp
Binary files differdiff --git a/users/tazjin/wallpapers/december1994_1920.webp b/users/tazjin/wallpapers/december1994_1920.webp
new file mode 100644
index 0000000000..d2c4da8018
--- /dev/null
+++ b/users/tazjin/wallpapers/december1994_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/flyby_1920.webp b/users/tazjin/wallpapers/flyby_1920.webp
new file mode 100644
index 0000000000..8df5b1132e
--- /dev/null
+++ b/users/tazjin/wallpapers/flyby_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/gaussfraktarna_1920_badge.webp b/users/tazjin/wallpapers/gaussfraktarna_1920_badge.webp
new file mode 100644
index 0000000000..3274a3a2d2
--- /dev/null
+++ b/users/tazjin/wallpapers/gaussfraktarna_1920_badge.webp
Binary files differdiff --git a/users/tazjin/wallpapers/kraftahq_1920.webp b/users/tazjin/wallpapers/kraftahq_1920.webp
new file mode 100644
index 0000000000..62a6debf47
--- /dev/null
+++ b/users/tazjin/wallpapers/kraftahq_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/peripheral2_1920.webp b/users/tazjin/wallpapers/peripheral2_1920.webp
new file mode 100644
index 0000000000..e454072ac4
--- /dev/null
+++ b/users/tazjin/wallpapers/peripheral2_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/ship14_1920.webp b/users/tazjin/wallpapers/ship14_1920.webp
new file mode 100644
index 0000000000..502f5dac90
--- /dev/null
+++ b/users/tazjin/wallpapers/ship14_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/shipyard_1920.webp b/users/tazjin/wallpapers/shipyard_1920.webp
new file mode 100644
index 0000000000..3d4115305d
--- /dev/null
+++ b/users/tazjin/wallpapers/shipyard_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/specky_1920.webp b/users/tazjin/wallpapers/specky_1920.webp
new file mode 100644
index 0000000000..b8246618be
--- /dev/null
+++ b/users/tazjin/wallpapers/specky_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/summerlove2_1920.webp b/users/tazjin/wallpapers/summerlove2_1920.webp
new file mode 100644
index 0000000000..d64a1cb867
--- /dev/null
+++ b/users/tazjin/wallpapers/summerlove2_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/t50_1920_badge.webp b/users/tazjin/wallpapers/t50_1920_badge.webp
new file mode 100644
index 0000000000..f8cb6107f3
--- /dev/null
+++ b/users/tazjin/wallpapers/t50_1920_badge.webp
Binary files differdiff --git a/users/tazjin/wallpapers/theflood1_1920.webp b/users/tazjin/wallpapers/theflood1_1920.webp
new file mode 100644
index 0000000000..335efb0571
--- /dev/null
+++ b/users/tazjin/wallpapers/theflood1_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/thelan_1920.webp b/users/tazjin/wallpapers/thelan_1920.webp
new file mode 100644
index 0000000000..55e6c22ad2
--- /dev/null
+++ b/users/tazjin/wallpapers/thelan_1920.webp
Binary files differdiff --git a/users/tazjin/wallpapers/vadrare_1920_badge.webp b/users/tazjin/wallpapers/vadrare_1920_badge.webp
new file mode 100644
index 0000000000..887c891da3
--- /dev/null
+++ b/users/tazjin/wallpapers/vadrare_1920_badge.webp
Binary files differdiff --git a/users/tvlbot.jpg b/users/tvlbot.jpg
new file mode 100644
index 0000000000..f0811418df
--- /dev/null
+++ b/users/tvlbot.jpg
Binary files differdiff --git a/users/wpcarro/.envrc b/users/wpcarro/.envrc
new file mode 100644
index 0000000000..b23a41fbd7
--- /dev/null
+++ b/users/wpcarro/.envrc
@@ -0,0 +1,3 @@
+source_up
+export PATH="${PWD}/bin:${PATH}"
+export WPCARRO="${REPO_ROOT}/users/wpcarro"
diff --git a/users/wpcarro/.gitignore b/users/wpcarro/.gitignore
new file mode 100644
index 0000000000..2380eb7b66
--- /dev/null
+++ b/users/wpcarro/.gitignore
@@ -0,0 +1,32 @@
+.vim
+./configs/secrets
+**/*/.emacs.d/quelpa/**/*
+**/*/.emacs.d/elpa/**/*
+**/*/.emacs.d/emojis
+**/*/.emacs.d/auto-save-list/**/*
+**/*/.emacs.d/eshell/
+**/*/.emacs.d/var/**/*
+**/*/.emacs.d/.cache/**/*
+**/*/.emacs.d/request
+**/*/.emacs.d/network-security.data
+**/*/.emacs.d/smex-items
+**/*/.gnupg/random_seed
+**/*/.gnupg/private-keys-v1.d
+.netrwhist
+Vundle.vim
+**/*/.emacs.d/custom.el
+**/*/.emacs.d/projectile-bookmarks.eld
+**/*/.emacs.d/bookmarks
+**/*/transient/history.el
+*.hi
+*.o
+__pycache__
+*.class
+node_modules/
+/configs/.config/fish/config.fish
+/configs/.config/fish/fish_variables
+/website/blog/public/
+/emacs/.emacs.d/tramp
+.gitsecret/keys/random_seed
+!*.secret
+secrets.json
diff --git a/users/wpcarro/.gitsecret/keys/pubring.kbx b/users/wpcarro/.gitsecret/keys/pubring.kbx
new file mode 100644
index 0000000000..692d5c67b0
--- /dev/null
+++ b/users/wpcarro/.gitsecret/keys/pubring.kbx
Binary files differdiff --git a/users/wpcarro/.gitsecret/keys/pubring.kbx~ b/users/wpcarro/.gitsecret/keys/pubring.kbx~
new file mode 100644
index 0000000000..c0a748ce2c
--- /dev/null
+++ b/users/wpcarro/.gitsecret/keys/pubring.kbx~
Binary files differdiff --git a/users/wpcarro/.gitsecret/keys/trustdb.gpg b/users/wpcarro/.gitsecret/keys/trustdb.gpg
new file mode 100644
index 0000000000..369485be06
--- /dev/null
+++ b/users/wpcarro/.gitsecret/keys/trustdb.gpg
Binary files differdiff --git a/users/wpcarro/.gitsecret/paths/mapping.cfg b/users/wpcarro/.gitsecret/paths/mapping.cfg
new file mode 100644
index 0000000000..fda2c84fb3
--- /dev/null
+++ b/users/wpcarro/.gitsecret/paths/mapping.cfg
@@ -0,0 +1 @@
+secrets.json:7d596a3ed16403040d89dd7e033a2af58e7aaabb6f246f44751b80a1863a2949
diff --git a/users/wpcarro/OWNERS b/users/wpcarro/OWNERS
new file mode 100644
index 0000000000..4dbd390b67
--- /dev/null
+++ b/users/wpcarro/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - wpcarro
diff --git a/users/wpcarro/README.md b/users/wpcarro/README.md
new file mode 100644
index 0000000000..5c5ce61c00
--- /dev/null
+++ b/users/wpcarro/README.md
@@ -0,0 +1,48 @@
+# wpcarro
+
+Welcome to my monorepo.
+
+Herein you will find a variety of libraries, packages, and documents. Some of
+this work in finished and other work is incomplete or just a sketch for a
+future project.
+
+Where applicable, I try to include `README.md` files in some of the
+subdirectories to help orient both myself and any onlookers.
+
+## Sign posts
+
+Below I have outlined a few projects that you might find interesting.
+
+- `boilerplate`: scaffolding for projects. Boilerplate's goal is to reduce the
+  startup costs of a project.
+- `configs`: my dotfiles (e.g. `config.fish`, `init.vim`).
+- `emacs`: Emacs is both my preferred text editor and my window manager; with
+  tens of thousands of lines of Emacs Lisp, you can safely assume that this
+  directory hosts a lot of libraries and packages.
+- `monzo_ynab`: `systemd` timer unit that imports my Monzo (i.e. a U.K.-based
+  online bank) transactions into the personal finance tool YNAB (i.e.
+  youneedabudget.com).
+- `nixos`: my declarative configuration for my NixOS machines. If you are
+  unfamiliar with Nix, I recommend reading about the NixOS project.
+- `tools`: some scripts and projects that simplify my life.
+- `website`: everything required to build my website, https://wpcarro.dev.
+
+## Installation
+
+### Google Machine
+
+- ensure `/google-briefcase` exists
+- read `/google-briefcase/README.md`
+
+### NixOS Machine
+
+```shell
+$ nix-shell -p nixos.{git,direnv}
+$ git clone https://code.tvl.fyi/depot.git /depot
+$ cd /depot
+$ eval "$(direnv hook bash)"
+$ HOSTNAME=base rebuild-system
+$ sudo tailscale up
+$ git clone 'user@host:~/.password-store' ~/.password-store
+$ scp -r 'user@host:~/.gnupg' ~/.gnupg
+```
diff --git a/users/wpcarro/assessments/brilliant/.ghci b/users/wpcarro/assessments/brilliant/.ghci
new file mode 100644
index 0000000000..efc88e630c
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/.ghci
@@ -0,0 +1,2 @@
+:set prompt "> "
+:set -Wall
diff --git a/users/wpcarro/assessments/brilliant/App.hs b/users/wpcarro/assessments/brilliant/App.hs
new file mode 100644
index 0000000000..0272988f37
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/App.hs
@@ -0,0 +1,41 @@
+--------------------------------------------------------------------------------
+module App where
+--------------------------------------------------------------------------------
+import Keyboard (Keyboard(..))
+import Transforms (Transform(..))
+import Utils ((|>))
+
+import qualified Data.Char as Char
+import qualified Utils
+import qualified Data.List.Split as Split
+import qualified Keyboard
+import qualified Data.HashMap.Strict as HM
+--------------------------------------------------------------------------------
+
+transform :: Keyboard -> Transform -> Keyboard
+
+transform (Keyboard xs) xform =
+  case xform of
+    HorizontalFlip ->
+      xs
+      |> fmap reverse
+      |> Keyboard
+
+    VerticalFlip ->
+      xs
+      |> reverse
+      |> Keyboard
+
+    Shift n ->
+      xs
+      |> concat
+      |> Utils.rotate n
+      |> Split.chunksOf 10
+      |> Keyboard
+
+retypePassage :: String -> Keyboard -> Maybe String
+retypePassage passage newKeyboard =
+  passage
+  |> fmap Char.toUpper
+  |> traverse (\c -> HM.lookup c Keyboard.charToCoord)
+  >>= traverse (Keyboard.coordToChar newKeyboard)
diff --git a/users/wpcarro/assessments/brilliant/Keyboard.hs b/users/wpcarro/assessments/brilliant/Keyboard.hs
new file mode 100644
index 0000000000..13b5de0145
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/Keyboard.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+--------------------------------------------------------------------------------
+module Keyboard where
+--------------------------------------------------------------------------------
+import Utils
+import Data.Coerce
+import Data.Hashable (Hashable)
+import GHC.Generics (Generic)
+
+import qualified Data.List as List
+import qualified Data.HashMap.Strict as HM
+--------------------------------------------------------------------------------
+
+newtype Keyboard = Keyboard [[Char]]
+  deriving (Eq)
+
+instance Show Keyboard where
+  show (Keyboard xxs) =
+    xxs |> fmap printRow |> List.intercalate "\n"
+    where
+      printRow :: [Char] -> String
+      printRow xs =
+        xs |> fmap (\x -> '[':x:']':"") |> List.intercalate ""
+
+data Coord = Coord
+  { row :: Int
+  , col :: Int
+  } deriving (Eq, Show, Generic)
+
+instance Hashable Coord
+
+-- | List of characters to their QWERTY coordinatees.
+coords :: [(Char, Coord)]
+coords =
+  qwerty
+  |> coerce
+  |> fmap (zip [0..])
+  |> zip [0..]
+  |> fmap (\(row, xs) -> xs |> fmap (\(col, char) -> (char, Coord row col)))
+  |> mconcat
+
+-- | Mapping of characters to their coordinates on a QWERTY keyboard with the
+-- top-left corner as 0,0.
+charToCoord :: HM.HashMap Char Coord
+charToCoord = HM.fromList coords
+
+coordToChar :: Keyboard -> Coord -> Maybe Char
+coordToChar (Keyboard xxs) Coord{..} =
+  Just $ xxs !! row !! col
+
+qwerty :: Keyboard
+qwerty = Keyboard [ ['1','2','3','4','5','6','7','8','9','0']
+                  , ['Q','W','E','R','T','Y','U','I','O','P']
+                  , ['A','S','D','F','G','H','J','K','L',';']
+                  , ['Z','X','C','V','B','N','M',',','.','/']
+                  ]
diff --git a/users/wpcarro/assessments/brilliant/Main.hs b/users/wpcarro/assessments/brilliant/Main.hs
new file mode 100644
index 0000000000..e94c73bea2
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/Main.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Main where
+--------------------------------------------------------------------------------
+import Options.Applicative
+import Data.Semigroup ((<>))
+
+import qualified Transforms
+import qualified Keyboard
+import qualified App
+--------------------------------------------------------------------------------
+
+data CommandArgs = CommandArgs
+  { transforms :: String
+  , passage :: String
+  } deriving (Eq, Show)
+
+parseArgs :: Parser CommandArgs
+parseArgs =
+  CommandArgs <$> strOption
+                  ( long "transforms"
+                 <> short 't'
+                 <> help "String of transforms where (e.g. \"HHVS12VHVHS3\")" )
+              <*> strOption
+                  ( long "passage"
+                 <> short 'p'
+                 <> help "Input text to re-type" )
+
+main :: IO ()
+main = do
+  CommandArgs{..} <- execParser opts
+  case Transforms.fromString transforms of
+    Nothing -> putStrLn "You must provide valid input (e.g. \"HHVS12VHVHS3\")"
+    Just xs -> do
+      let keyboard = foldl App.transform Keyboard.qwerty (Transforms.optimize xs)
+      putStrLn $ "Typing: \"" ++ passage ++ "\"\nOn this keyboard:\n" ++ show keyboard
+      case App.retypePassage passage keyboard of
+        Nothing -> putStrLn $ "Looks like at least one of the characters in your input passage doesn't fit on our QWERTY keyboard: \n" ++ show Keyboard.qwerty
+        Just result -> putStrLn $ "Result: " ++ result
+  where
+    opts = info (parseArgs <**> helper)
+      ( fullDesc
+     <> progDesc "Transform a QWERTY keyboard using a string of commands")
diff --git a/users/wpcarro/assessments/brilliant/README.md b/users/wpcarro/assessments/brilliant/README.md
new file mode 100644
index 0000000000..60d7de4e25
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/README.md
@@ -0,0 +1,82 @@
+# Transform QWERTY
+
+Apply a series of transforms to a QWERTY keyboard then use the new keyboard to
+re-type a passage of text.
+
+## Environment
+
+You will need [Nix][nix] to build this program on your machine. The good news is
+that you won't need any Haskell-specific dependencies like `ghc`, `cabal`, or
+`stack`: just Nix.
+
+Once you have Nix installed, to build the program, run the following from this
+project's top-level directory:
+
+```shell
+$ nix-build
+```
+
+This should output an executable named `transform-keyboard` within a `result`
+directory:
+
+```shell
+$ tree result
+result
+└── transform-keyboard
+```
+
+### Testing
+
+To run the test suite, run the following from the project's top-level directory:
+
+```shell
+$ nix-shell
+$ runhaskell Spec.hs
+```
+
+[nix]: https://nixos.org/download.html
+
+## Usage
+
+Here are some `--help` and usage examples:
+
+```shell
+$ ./result/transform-keyboard --help
+Usage: transform-keyboard (-t|--transforms ARG) (-p|--passage ARG)
+  Transform a QWERTY keyboard using a string of commands
+
+Available options:
+  -t,--transforms ARG      String of transforms where (e.g. "HHVS12VHVHS3")
+  -p,--passage ARG         Input text to re-type
+  -h,--help                Show this help text
+```
+
+Now a working example:
+
+```shell
+$ ./result/transform-keyboard --transforms=HHVS12VHVHS3 --passage='Hello,Brilliant.'
+Typing: "Hello,Brilliant."
+On this keyboard:
+[H][J][K][L][;][Q][W][E][R][T]
+[Y][U][I][O][P][1][2][3][4][5]
+[6][7][8][9][0][Z][X][C][V][B]
+[N][M][,][.][/][A][S][D][F][G]
+Result: ZIVV4D/O3VV36APF
+```
+
+...and an example with an erroneous input (i.e. `!`):
+
+```shell
+$ ./result/transform-keyboard --transforms=HHVS12VHVHS3 --passage='Hello,Brilliant!'
+Typing: "Hello,Brilliant!"
+On this keyboard:
+[H][J][K][L][;][Q][W][E][R][T]
+[Y][U][I][O][P][1][2][3][4][5]
+[6][7][8][9][0][Z][X][C][V][B]
+[N][M][,][.][/][A][S][D][F][G]
+Looks like at least one of the characters in your input passage doesn't fit on our QWERTY keyboard:
+[1][2][3][4][5][6][7][8][9][0]
+[Q][W][E][R][T][Y][U][I][O][P]
+[A][S][D][F][G][H][J][K][L][;]
+[Z][X][C][V][B][N][M][,][.][/]
+```
diff --git a/users/wpcarro/assessments/brilliant/Spec.hs b/users/wpcarro/assessments/brilliant/Spec.hs
new file mode 100644
index 0000000000..e99e025641
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/Spec.hs
@@ -0,0 +1,103 @@
+--------------------------------------------------------------------------------
+module Spec where
+--------------------------------------------------------------------------------
+import Test.Hspec
+import Test.QuickCheck
+import Keyboard (Keyboard(..))
+import Transforms (Transform(..))
+import Data.Coerce
+import Utils
+
+import qualified App
+import qualified Keyboard
+import qualified Transforms
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = hspec $ do
+  describe "Keyboard.print" $ do
+    it "pretty-prints the keyboard" $ do
+      show Keyboard.qwerty == "[1][2][3][4][5][6][7][8][9][0]\n[Q][W][E][R][T][Y][U][I][O][P]\n[A][S][D][F][G][H][J][K][L][;]\n[Z][X][C][V][B][N][M][,][.][/]"
+
+  describe "Transforms.fromString" $ do
+    it "successfully parses a string of commands" $ do
+      Transforms.fromString "HHVS-12VHVHS3" ==
+        Just [ HorizontalFlip
+             , HorizontalFlip
+             , VerticalFlip
+             , Shift (-12)
+             , VerticalFlip
+             , HorizontalFlip
+             , VerticalFlip
+             , HorizontalFlip
+             , Shift 3
+             ]
+
+    it "returns Nothing when the input is invalid" $ do
+      Transforms.fromString "potato" == Nothing
+
+    it "return Nothing when the input is valid except for the end" $ do
+      Transforms.fromString "HVS10potato" == Nothing
+
+  describe "App.transform" $ do
+    it "flips any keyboard horizontally" $ do
+      property $ \first second third fourth ->
+        App.transform (Keyboard [first, second, third, fourth]) HorizontalFlip == do
+          Keyboard [ reverse first
+                   , reverse second
+                   , reverse third
+                   , reverse fourth
+                   ]
+
+    it "flips any keyboard vertically" $ do
+      property $ \first second third fourth ->
+        App.transform (Keyboard [first, second, third, fourth]) VerticalFlip == do
+          Keyboard $ reverse [first, second, third, fourth]
+
+    it "shifts any keyboard" $ do
+      property $ \first second third fourth n ->
+        App.transform (Keyboard [first, second, third, fourth]) (Shift n)
+        |> (coerce :: Keyboard -> [[Char]])
+        |> concat ==
+          [first, second, third, fourth]
+            |> concat
+            |> Utils.rotate n
+
+    it "flips a QWERTY keyboard horizontally" $ do
+      App.transform Keyboard.qwerty HorizontalFlip == do
+        Keyboard [ ['0','9','8','7','6','5','4','3','2','1']
+                 , ['P','O','I','U','Y','T','R','E','W','Q']
+                 , [';','L','K','J','H','G','F','D','S','A']
+                 , ['/','.',',','M','N','B','V','C','X','Z']
+                 ]
+
+    it "flips a keyboard vertically" $ do
+      App.transform Keyboard.qwerty VerticalFlip == do
+        Keyboard [ ['Z','X','C','V','B','N','M',',','.','/']
+                 , ['A','S','D','F','G','H','J','K','L',';']
+                 , ['Q','W','E','R','T','Y','U','I','O','P']
+                 , ['1','2','3','4','5','6','7','8','9','0']
+                 ]
+
+    it "shifts a keyboard left N times" $ do
+      App.transform Keyboard.qwerty (Shift 2) == do
+        Keyboard [ ['3','4','5','6','7','8','9','0','Q','W']
+                 , ['E','R','T','Y','U','I','O','P','A','S']
+                 , ['D','F','G','H','J','K','L',';','Z','X']
+                 , ['C','V','B','N','M',',','.','/','1','2']
+                 ]
+
+    it "shifts right negative amounts" $ do
+      App.transform Keyboard.qwerty (Shift (-3)) == do
+        Keyboard [ [',','.','/','1','2','3','4','5','6','7']
+                 , ['8','9','0','Q','W','E','R','T','Y','U']
+                 , ['I','O','P','A','S','D','F','G','H','J']
+                 , ['K','L',';','Z','X','C','V','B','N','M']
+                 ]
+
+  describe "Transforms.optimize" $ do
+    it "removes superfluous horizontal transformations" $ do
+      Transforms.optimize [HorizontalFlip, HorizontalFlip] == []
+
+    it "removes superfluous vertical transformations" $ do
+      Transforms.optimize [VerticalFlip, VerticalFlip] == []
diff --git a/users/wpcarro/assessments/brilliant/Transforms.hs b/users/wpcarro/assessments/brilliant/Transforms.hs
new file mode 100644
index 0000000000..d8df8f8372
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/Transforms.hs
@@ -0,0 +1,52 @@
+--------------------------------------------------------------------------------
+module Transforms where
+--------------------------------------------------------------------------------
+import Control.Applicative ((<|>))
+import Text.ParserCombinators.ReadP
+--------------------------------------------------------------------------------
+
+data Transform = VerticalFlip
+               | HorizontalFlip
+               | Shift Int
+               deriving (Eq, Show)
+
+digit :: ReadP Char
+digit =
+  satisfy (\c -> c >= '0' && c <= '9')
+
+command :: ReadP Transform
+command = vertical
+      <|> horizontal
+      <|> shift
+  where
+    vertical =
+      char 'V' >> pure VerticalFlip
+
+    horizontal =
+      char 'H' >> pure HorizontalFlip
+
+    shift = do
+      _ <- char 'S'
+      negative <- option Nothing $ fmap Just (satisfy (== '-'))
+      n <- read <$> many1 digit
+      case negative of
+        Nothing -> pure $ Shift n
+        Just _  -> pure $ Shift (-1 * n)
+
+-- | Attempt to remove redundant transformations.
+-- | Here are some rules that I'd like to support but may not have time for:
+-- | - All even-numbered flips (w/o intermittent shifts) can become zero
+-- | - All odd-numbered flips (w/o intermittent shifts) can become 1
+-- | - All shifts can be be reduce to the absolute value of shifts
+optimize :: [Transform] -> [Transform]
+optimize [] = []
+optimize [x] = [x]
+optimize (VerticalFlip:VerticalFlip:xs) = optimize xs
+optimize (HorizontalFlip:HorizontalFlip:xs) = optimize xs
+optimize xs = xs
+
+fromString :: String -> Maybe [Transform]
+fromString x =
+  case readP_to_S (manyTill command eof) x of
+   [(res, "")] -> Just res
+   _           -> Nothing
diff --git a/users/wpcarro/assessments/brilliant/Utils.hs b/users/wpcarro/assessments/brilliant/Utils.hs
new file mode 100644
index 0000000000..c69d00333b
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/Utils.hs
@@ -0,0 +1,13 @@
+--------------------------------------------------------------------------------
+module Utils where
+--------------------------------------------------------------------------------
+import Data.Function ((&))
+--------------------------------------------------------------------------------
+
+(|>) :: a -> (a -> b) -> b
+(|>) = (&)
+
+-- | Rotate `xs` as a cycle `n` times.
+rotate :: Int -> [a] -> [a]
+rotate n xs = take size . drop (n `mod` size) . cycle $ xs
+  where size = length xs
diff --git a/users/wpcarro/assessments/brilliant/default.nix b/users/wpcarro/assessments/brilliant/default.nix
new file mode 100644
index 0000000000..0628679c01
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/default.nix
@@ -0,0 +1,16 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.program {
+  name = "transform-keyboard";
+  srcs = builtins.path {
+    path = ./.;
+    name = "transform-keyboard-src";
+  };
+  deps = hpkgs: with hpkgs; [
+    optparse-applicative
+    unordered-containers
+    split
+    rio
+  ];
+  ghcExtensions = [ ];
+}
diff --git a/users/wpcarro/assessments/brilliant/shell.nix b/users/wpcarro/assessments/brilliant/shell.nix
new file mode 100644
index 0000000000..e08399c093
--- /dev/null
+++ b/users/wpcarro/assessments/brilliant/shell.nix
@@ -0,0 +1,12 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    (haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
+      hspec
+      optparse-applicative
+      unordered-containers
+      split
+    ]))
+  ];
+}
diff --git a/users/wpcarro/assessments/dotted-squares/.envrc b/users/wpcarro/assessments/dotted-squares/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/assessments/dotted-squares/.ghci b/users/wpcarro/assessments/dotted-squares/.ghci
new file mode 100644
index 0000000000..b100af4432
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/.ghci
@@ -0,0 +1 @@
+:set -Wall
diff --git a/users/wpcarro/assessments/dotted-squares/Main.hs b/users/wpcarro/assessments/dotted-squares/Main.hs
new file mode 100644
index 0000000000..44f91e2b23
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/Main.hs
@@ -0,0 +1,218 @@
+{-# LANGUAGE DeriveGeneric #-}
+--------------------------------------------------------------------------------
+module Main where
+--------------------------------------------------------------------------------
+import Data.Hashable
+import Data.Function ((&))
+import GHC.Generics
+import Text.ParserCombinators.ReadP
+import Control.Applicative
+
+import qualified Data.HashSet as HS
+--------------------------------------------------------------------------------
+
+data Direction
+  = DirLeft
+  | DirRight
+  | DirUp
+  | DirDown
+  deriving (Eq, Show)
+
+data Point = Point Int Int
+  deriving (Eq, Show, Ord, Generic)
+instance Hashable Point
+
+data Orientation
+  = Horizontal
+  | Vertical
+  deriving (Eq, Show)
+
+data Anchor
+  = Beg
+  | End
+  deriving (Eq, Show)
+
+data Rotation
+  = CW
+  | CCW
+  deriving (Eq, Show)
+
+data Line = Line Point Point
+  deriving (Show, Generic)
+instance Hashable Line
+
+instance Eq Line where
+  Line begA endA == Line begB endB =
+    (begA == begB && endA == endB) ||
+    (begA == endB && endA == begB)
+
+data Game = Game (HS.HashSet Line) [Line]
+  deriving (Eq, Show)
+
+data Scoreboard = Scoreboard Int Int
+  deriving (Eq)
+
+instance Semigroup Scoreboard where
+  (Scoreboard a b) <> (Scoreboard x y) =
+    Scoreboard (a + x) (b + y)
+
+instance Monoid Scoreboard where
+  mempty = Scoreboard 0 0
+
+data Turn
+  = Player1
+  | Player2
+  deriving (Eq, Show)
+
+next :: Turn -> Turn
+next Player1 = Player2
+next Player2 = Player1
+
+instance Show Scoreboard where
+  show (Scoreboard p1 p2) =
+    "Player 1: " ++ show (p1) ++ " Player 2: " ++ show (p2)
+
+digit :: ReadP Char
+digit = satisfy (\c -> c >= '0' && c <= '9')
+
+int :: ReadP Int
+int = read <$> many1 digit
+
+inputLine :: ReadP String
+inputLine = manyTill get (char '\n')
+
+direction :: ReadP Direction
+direction = do
+  c <- char 'L' <|> char 'R' <|> char 'U' <|> char 'D'
+  case c of
+    'L' -> pure DirLeft
+    'R' -> pure DirRight
+    'U' -> pure DirUp
+    'D' -> pure DirDown
+    _   -> fail $ "Unexpected direction: " ++ show c
+
+validMove :: Int -> Int -> ReadP Line
+validMove w h = do
+  x <- int
+  skipSpaces
+  y <- int
+  skipSpaces
+  dir <- direction
+  _ <- char '\n'
+  if x >= 0 && x <= w &&  y >= 0 && y <= h then do
+    let beg = Point x y
+    pure $ mkLine beg (shiftPoint dir beg)
+  else
+    fail "Expected a move on the game board"
+
+game :: ReadP Game
+game = do
+  w <- read <$> inputLine
+  h <- read <$> inputLine
+  locs <- read <$> inputLine
+  moves <- count locs (validMove w h)
+  eof
+  pure $ Game mempty moves
+
+parseInput :: String -> Maybe Game
+parseInput x = do
+  case readP_to_S game x of
+    [(res, "")] -> Just res
+    _ -> Nothing
+
+-- | Smart constructor to ensure that beg is always < end.
+mkLine :: Point -> Point -> Line
+mkLine beg end =
+  if beg < end then Line beg end else Line end beg
+
+mkLineDir :: Int -> Int -> Direction -> Line
+mkLineDir x y dir =
+  let beg = Point x y
+  in mkLine beg (shiftPoint dir beg)
+
+mkLineDir' :: Point -> Direction -> Line
+mkLineDir' (Point x y) dir = mkLineDir x y dir
+
+shiftPoint :: Direction -> Point -> Point
+shiftPoint DirLeft  (Point x y) = Point (x - 1) y
+shiftPoint DirRight (Point x y) = Point (x + 1) y
+shiftPoint DirUp    (Point x y) = Point x (y + 1)
+shiftPoint DirDown  (Point x y) = Point x (y - 1)
+
+shiftLine :: Direction -> Line -> Line
+shiftLine dir (Line beg end) =
+  mkLine (shiftPoint dir beg) (shiftPoint dir end)
+
+rotateLine :: Anchor -> Rotation -> Line -> Line
+rotateLine anchor rotation line =
+  doRotateLine (classifyOrientation line) anchor rotation line
+
+doRotateLine :: Orientation -> Anchor -> Rotation -> Line -> Line
+doRotateLine Horizontal Beg CW  (Line beg _) = mkLineDir' beg DirDown
+doRotateLine Horizontal Beg CCW (Line beg _) = mkLineDir' beg DirUp
+doRotateLine Horizontal End CW  (Line _ end) = mkLineDir' end DirUp
+doRotateLine Horizontal End CCW (Line _ end) = mkLineDir' end DirDown
+doRotateLine Vertical   Beg CW  (Line beg _) = mkLineDir' beg DirRight
+doRotateLine Vertical   Beg CCW (Line beg _) = mkLineDir' beg DirLeft
+doRotateLine Vertical   End CW  (Line _ end) = mkLineDir' end DirLeft
+doRotateLine Vertical   End CCW (Line _ end) = mkLineDir' end DirRight
+
+classifyOrientation :: Line -> Orientation
+classifyOrientation (Line (Point _ y1) (Point _ y2)) =
+  if y1 == y2 then Horizontal else Vertical
+
+closesAnySquare :: HS.HashSet Line -> Line -> Bool
+closesAnySquare allMoves line = do
+  let alreadyDrawn x = HS.member x allMoves
+  case classifyOrientation line of
+    Horizontal ->
+      all alreadyDrawn
+        [ shiftLine DirUp line
+        , rotateLine Beg CCW line
+        , rotateLine End CW line
+        ] ||
+      all alreadyDrawn
+        [ shiftLine DirDown line
+        , rotateLine Beg CW line
+        , rotateLine End CCW line
+        ]
+    Vertical ->
+      all alreadyDrawn
+        [ shiftLine DirLeft line
+        , rotateLine Beg CCW line
+        , rotateLine End CW line
+        ] ||
+      all alreadyDrawn
+        [ shiftLine DirRight line
+        , rotateLine Beg CW line
+        , rotateLine End CCW line
+        ]
+
+incScoreboard :: Turn -> Scoreboard -> Scoreboard
+incScoreboard Player1 score = score <> Scoreboard 1 0
+incScoreboard Player2 score = score <> Scoreboard 0 1
+
+scoreGame :: Turn -> Game -> Scoreboard -> Maybe Scoreboard
+scoreGame _ (Game _ []) score = Just $ score
+scoreGame player (Game allMoves (line:rest)) score =
+  if HS.member line allMoves then
+    Nothing
+  else do
+    let allMoves' = HS.insert line allMoves
+        score' = if closesAnySquare allMoves line then
+                   incScoreboard player score
+                 else score
+    scoreGame (next player) (Game allMoves' rest) score'
+
+(|>) :: a -> (a -> b) -> b
+(|>) = (&)
+
+main :: IO ()
+main = do
+  input <- readFile "game.txt"
+  case parseInput input of
+    Nothing -> putStrLn "invalid"
+    Just parsedGame ->
+      case scoreGame Player1 parsedGame mempty of
+        Nothing -> putStrLn "invalid"
+        Just score -> print score
diff --git a/users/wpcarro/assessments/dotted-squares/README.md b/users/wpcarro/assessments/dotted-squares/README.md
new file mode 100644
index 0000000000..3d13da1cb1
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/README.md
@@ -0,0 +1,21 @@
+# Dotted Squares
+
+This is my second attempt at solving this problem. I had an hour to solve it the
+first time, and I unfortunately came up short although I made good progress.
+
+The problem asks to read input from a text file that looks like this:
+
+```
+1     -- board width
+1     -- board height
+4     -- number of lines of "moves" (below)
+0 0 R -- create a unit vector (0,0) facing right
+0 0 U -- create a unit vector (0,0) facing up
+0 1 L -- create a unit vector (0,1) facing left
+1 1 D -- create a unit vector (1,1) facing down
+```
+
+After parsing and validating the input, score the outcome a game where players
+one and two alternatively take turns drawing lines on a board. Anytime one of
+the players draws a line that creates a square from existing lines, they get a
+point.
diff --git a/users/wpcarro/assessments/dotted-squares/Spec.hs b/users/wpcarro/assessments/dotted-squares/Spec.hs
new file mode 100644
index 0000000000..b5d604085b
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/Spec.hs
@@ -0,0 +1,80 @@
+--------------------------------------------------------------------------------
+module Spec where
+--------------------------------------------------------------------------------
+import Test.Hspec
+import Main hiding (main)
+import qualified Data.HashSet as HS
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = hspec $ do
+  describe "dotted-squares" $ do
+    describe "parseInput" $ do
+      it "works as expected" $ do
+        input <- readFile "input-a.txt"
+        parseInput input `shouldBe` Just (Game mempty [ mkLine (Point 0 0) (Point 1 0)
+                                                      , mkLine (Point 0 0) (Point 0 1)
+                                                      ])
+
+      it "fails when the game has too many user moves" $ do
+        input <- readFile "too-many-moves.txt"
+        parseInput input `shouldBe` Nothing
+
+      it "fails when the game has too few user moves" $ do
+        input <- readFile "too-few-moves.txt"
+        parseInput input `shouldBe` Nothing
+
+    describe "shiftLine" $ do
+      let horizontal = mkLineDir 1 1 DirRight
+          vertical   = mkLineDir 1 1 DirUp
+      it "can move a horizontal line up" $
+        shiftLine DirUp horizontal `shouldBe` mkLineDir 1 2 DirRight
+      it "can move a horizontal line down" $
+        shiftLine DirDown horizontal `shouldBe` mkLineDir 1 0 DirRight
+      it "can move a horizontal line left" $
+        shiftLine DirLeft horizontal `shouldBe` mkLineDir 0 1 DirRight
+      it "can move a horizontal line right" $
+        shiftLine DirRight horizontal `shouldBe` mkLineDir 2 1 DirRight
+      it "can move a vertical line up" $
+        shiftLine DirUp vertical `shouldBe` mkLineDir 1 2 DirUp
+      it "can move a vertical line down" $
+        shiftLine DirDown vertical `shouldBe` mkLineDir 1 0 DirUp
+      it "can move a vertical line left" $
+        shiftLine DirLeft vertical `shouldBe` mkLineDir 0 1 DirUp
+      it "can move a vertical line right" $
+        shiftLine DirRight vertical `shouldBe` mkLineDir 2 1 DirUp
+
+    describe "rotateLine" $ do
+      let horizontal = mkLineDir 1 1 DirRight -- 1,1;2,1
+          vertical   = mkLineDir 1 1 DirUp    -- 1,1;1,2
+      it "can rotate a horizontal line CW anchored at its beginning" $
+        rotateLine Beg CW horizontal `shouldBe` mkLineDir 1 1 DirDown
+      it "can rotate a horizontal line CCW anchored at its beginning" $
+        rotateLine Beg CCW horizontal `shouldBe` mkLineDir 1 1 DirUp
+      it "can rotate a horizontal line CW anchored at its end" $
+        rotateLine End CW horizontal `shouldBe` mkLineDir 2 1 DirUp
+      it "can rotate a horizontal line CCW anchored at its end" $
+        rotateLine End CCW horizontal `shouldBe` mkLineDir 2 1 DirDown
+
+      it "can rotate a vertical line CW anchored at its beginning" $
+        rotateLine Beg CW vertical `shouldBe` mkLineDir 1 1 DirRight
+      it "can rotate a vertical line CCW anchored at its beginning" $
+        rotateLine Beg CCW vertical `shouldBe` mkLineDir 1 1 DirLeft
+      it "can rotate a vertical line CW anchored at its end" $
+        rotateLine End CW vertical `shouldBe` mkLineDir 1 2 DirLeft
+      it "can rotate a vertical line CCW anchored at its end" $
+        rotateLine End CCW vertical `shouldBe` mkLineDir 1 2 DirRight
+
+    describe "closesAnySquare" $ do
+      let threeSides = [ (0, 0, DirRight)
+                       , (0, 0, DirUp)
+                       , (0, 1, DirRight)
+                       ]
+                       |> fmap (\(x, y, dir) -> mkLineDir x y dir)
+                       |> HS.fromList
+      it "returns true the line we supply makes a square" $
+        closesAnySquare threeSides (mkLineDir 1 1 DirDown) `shouldBe` True
+      it "returns false the line we supply doesn't make a square" $
+        closesAnySquare threeSides (mkLineDir 1 1 DirUp) `shouldBe` False
+      it "returns false when we have no existing lines" $
+        closesAnySquare mempty (mkLineDir 1 1 DirUp) `shouldBe` False
diff --git a/users/wpcarro/assessments/dotted-squares/colliding-moves.txt b/users/wpcarro/assessments/dotted-squares/colliding-moves.txt
new file mode 100644
index 0000000000..a831fa95c0
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/colliding-moves.txt
@@ -0,0 +1,7 @@
+1
+1
+4
+0 0 R
+0 0 R
+0 1 R
+0 1 R
diff --git a/users/wpcarro/assessments/dotted-squares/game.txt b/users/wpcarro/assessments/dotted-squares/game.txt
new file mode 100644
index 0000000000..0af71d1f5b
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/game.txt
@@ -0,0 +1,7 @@
+1
+1
+4
+0 0 R
+0 0 U
+0 1 R
+1 1 D
diff --git a/users/wpcarro/assessments/dotted-squares/input-a.txt b/users/wpcarro/assessments/dotted-squares/input-a.txt
new file mode 100644
index 0000000000..b9e871eced
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/input-a.txt
@@ -0,0 +1,5 @@
+1
+1
+2
+0 0 R
+0 0 U
diff --git a/users/wpcarro/assessments/dotted-squares/shell.nix b/users/wpcarro/assessments/dotted-squares/shell.nix
new file mode 100644
index 0000000000..868668ca50
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/shell.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: with hpkgs; [
+    hspec
+    unordered-containers
+  ];
+}
diff --git a/users/wpcarro/assessments/dotted-squares/too-few-moves.txt b/users/wpcarro/assessments/dotted-squares/too-few-moves.txt
new file mode 100644
index 0000000000..d684679d26
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/too-few-moves.txt
@@ -0,0 +1,6 @@
+1
+1
+4
+0 0 R
+0 0 U
+0 1 R
diff --git a/users/wpcarro/assessments/dotted-squares/too-many-moves.txt b/users/wpcarro/assessments/dotted-squares/too-many-moves.txt
new file mode 100644
index 0000000000..bfcced43b9
--- /dev/null
+++ b/users/wpcarro/assessments/dotted-squares/too-many-moves.txt
@@ -0,0 +1,7 @@
+1
+1
+3
+0 0 R
+0 0 U
+0 1 R
+1 1 D
diff --git a/users/wpcarro/assessments/ramp/solution-emacs-elixir-format.py b/users/wpcarro/assessments/ramp/solution-emacs-elixir-format.py
new file mode 100644
index 0000000000..d0d9484020
--- /dev/null
+++ b/users/wpcarro/assessments/ramp/solution-emacs-elixir-format.py
@@ -0,0 +1,29 @@
+# The file '2010.census.txt' contains summary statistics from the 2010 United
+# States census including household income. The data is in an unspecified
+# format.
+
+# Find the average of the column called:
+
+#     'MEDIAN HOUSEHOLD INCOME'
+
+# Ideally the solution should be a command line script, of the form:
+
+#     $ ./solution [options] [file...]
+
+# The solution may be written in any language, Python is preferred but not
+# required.
+
+# Google, stack overflow, etc. usage is allowed.
+
+import requests
+
+url = "https://assets.tryramp.com/interview/census/2010.census.txt"
+
+def main():
+    res = requests.get(url)
+    if res.status not in {200}:
+        raise Exception("Unexpected status code: {}".format(res.status_code))
+    # download the content
+    # parse row
+    # select 'MEDIAN HOUSEHOLD INCOME' column
+    pass
diff --git a/users/wpcarro/assessments/ramp/solution.py b/users/wpcarro/assessments/ramp/solution.py
new file mode 100644
index 0000000000..28060bfb3c
--- /dev/null
+++ b/users/wpcarro/assessments/ramp/solution.py
@@ -0,0 +1,87 @@
+# The file '2010.census.txt' contains summary statistics from the 2010 United
+# States census including household income. The data is in an unspecified
+# format.
+
+# Find the average of the column called:
+
+#     'MEDIAN HOUSEHOLD INCOME'
+
+# Ideally the solution should be a command line script, of the form:
+
+#     $ ./solution [options] [file...]
+
+# The solution may be written in any language, Python is preferred but not
+# required.
+
+# Google, stack overflow, etc. usage is allowed.
+
+import requests
+import csv
+
+url = "https://assets.tryramp.com/interview/census/2010.census.txt"
+column = 'MEDIAN HOUSEHOLD INCOME'
+columns = [
+    'CENSUS YEAR',
+    'TRACT',
+    'BLOCK GROUP',
+    'FIPS ID',
+    'TOTAL POPULATION',
+    'POPULATION WHITE',
+    'POPULATION BLACK',
+    'POPULATION ASIAN',
+    'POPULATION OTHER',
+    'POPULATION AMERICAN INDIAN',
+    'POPULATION PACIFIC ISLANDER',
+    'POPULATION ONE RACE',
+    'POPULATION MULTI RACE',
+    'POPULATION 25 OLDER',
+    'MEDIAN AGE',
+    'MEDIAN HOUSEHOLD INCOME',
+    'HIGH SCHOOL MALE',
+    'HIGH SCHOOL MORE MALE',
+    'COLLEGE 1 YR LESS MALE',
+    'COLLEGE 1 YR MORE MALE',
+    'ASSOCIATES DEGREE MALE',
+    'BACHELORS DEGREE MALE',
+    'MASTERS DEGREE MALE',
+    'PROFESSIONAL DEGREE MALE',
+    'DOCTORAL DEGREE MALE',
+    'HIGH SCHOOL FEMALE',
+    'HIGH SCHOOL MORE FEMALE',
+    'COLLEGE 1 YR LESS FEMALE',
+    'COLLEGE 1 YR MORE FEMALE',
+    'ASSOCIATES DEGREE FEMALE',
+    'BACHELORS DEGREE FEMALE',
+    'MASTERS DEGREE FEMALE',
+    'PROFESSIONAL DEGREE FEMALE',
+    'DOCTORAL DEGREE FEMALE',
+    'PERCENT 25 YR OVER HIGH SCHOOL MORE',
+    'HOUSING UNITS',
+    'OCCUPIED HOUSING UNITS',
+    'OWNER OCCUPIED HOUSING',
+    'RENTER OCCUPIED HOUSING',
+    'PERCENT OWNER OCCUPIED',
+    'PERCENT RENTER OCCUPIED',
+    'MEDIAN HOUSE VALUE OWNER OCCUPIED',
+    'MEDIAN YEAR BUILT',
+    'VACANCY RATES',
+]
+
+
+def average(xs):
+    return sum(xs) / len(xs)
+
+
+def parse_body(body):
+    return list(csv.DictReader(body.split('\n')[1:], delimiter='|', fieldnames=columns))
+
+
+def main():
+    res = requests.get(url)
+    if res.status_code not in {200}:
+        raise Exception("Unexpected status code: {}".format(res.status_code))
+    return average([int(d.get(column))
+                    for d in parse_body(res.text)
+                    if int(d.get(column)) >= 0])
+
+print(main())
diff --git a/users/wpcarro/assessments/semiprimes/.gitignore b/users/wpcarro/assessments/semiprimes/.gitignore
new file mode 100644
index 0000000000..b5b25bd648
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/.gitignore
@@ -0,0 +1 @@
+default.nix
diff --git a/users/wpcarro/assessments/semiprimes/README.md b/users/wpcarro/assessments/semiprimes/README.md
new file mode 100644
index 0000000000..7d5a15482a
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/README.md
@@ -0,0 +1,44 @@
+# Semiprimes Service
+
+## Introduction
+
+A **composite** is a number containing at least two prime factors. For example:
+
+```
+15 = 3 × 5
+9 = 3 × 3
+12 = 2 × 2 × 3
+```
+
+There are ten composites below thirty containing precisely two, not necessarily
+distinct, prime factors: `4, 6, 9, 10, 14, 15, 21, 22, 25, 26`. Let’s call such
+numbers *Semiprimes*.
+
+## Task
+
+- Write a module which provides a function to tell whether a given number, `N`,
+  is a semiprime. `N` will be less than 100,000
+- Please implement an API (RESTful or GraphQL) to factor a given number into two
+  prime numbers if it’s a semiprime, otherwise, return an error message.
+
+## Stretch Goals
+
+- Handle the invalid inputs.
+- Support batch requests: i.e. users could provide 100 numbers, and the API
+  return the answer for all.
+- Considering this module will be used by a long running service, could you
+  optimize it to give answers faster?
+
+## Usage
+
+To run the application you'll need to have `elixir` installed. Assuming `elixir`
+is already installed, consult the following steps to start the application:
+
+```shell
+$ cd server
+$ mix deps.get
+$ iex -S mix
+```
+
+Now open a web browser and visit `http://localhost:8080`!
+
diff --git a/users/wpcarro/assessments/semiprimes/server/.formatter.exs b/users/wpcarro/assessments/semiprimes/server/.formatter.exs
new file mode 100644
index 0000000000..d2cda26edd
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/.formatter.exs
@@ -0,0 +1,4 @@
+# Used by "mix format"
+[
+  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
+]
diff --git a/users/wpcarro/assessments/semiprimes/server/.gitignore b/users/wpcarro/assessments/semiprimes/server/.gitignore
new file mode 100644
index 0000000000..db9704a85f
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/.gitignore
@@ -0,0 +1,24 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where third-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+server-*.tar
+
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/app.ex b/users/wpcarro/assessments/semiprimes/server/lib/app.ex
new file mode 100644
index 0000000000..7a6fa5ea24
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/app.ex
@@ -0,0 +1,8 @@
+defmodule App do
+  use Application
+
+  @impl true
+  def start(_type, _args) do
+    Sup.start_link()
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/cache.ex b/users/wpcarro/assessments/semiprimes/server/lib/cache.ex
new file mode 100644
index 0000000000..cd064cc1ae
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/cache.ex
@@ -0,0 +1,41 @@
+defmodule Cache do
+  @moduledoc """
+  Cache is an in-memory key-value store.
+  """
+  use Agent
+
+  @doc """
+  Inititalize the key-value store.
+  """
+  def start_link(_) do
+    Agent.start_link(fn -> %{} end, name: __MODULE__)
+  end
+
+  @doc """
+  Attempt to return the value stored at `key`
+  """
+  def get(key) do
+    Agent.get(__MODULE__, &Map.get(&1, key))
+  end
+
+  @doc """
+  Write the `value` under the `key`. Last writer wins.
+  """
+  def put(key, value) do
+    Agent.update(__MODULE__, &Map.put(&1, key, value))
+  end
+
+  @doc """
+  List the contents of the cache. Useful for debugging purposes.
+  """
+  def list() do
+    Agent.get(__MODULE__, & &1)
+  end
+
+  @doc """
+  Invalidate the entire cache.
+  """
+  def clear() do
+    Agent.update(__MODULE__, fn _ -> %{} end)
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/extras.ex b/users/wpcarro/assessments/semiprimes/server/lib/extras.ex
new file mode 100644
index 0000000000..f0c2ea4b9e
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/extras.ex
@@ -0,0 +1,22 @@
+defmodule Extras do
+  @moduledoc """
+  Hosts utility functions intended to supplement the standard library.
+  """
+
+  @doc """
+  Return an ascending range starting at `a` and ending at `b` (exclusive).
+
+  ## Examples
+
+      iex> Extras.range(2, 5)
+      [2, 3, 4]
+
+  """
+  def range(a, b) do
+    if b <= a do
+      []
+    else
+      [a] ++ range(a + 1, b)
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/math.ex b/users/wpcarro/assessments/semiprimes/server/lib/math.ex
new file mode 100644
index 0000000000..8a33be4753
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/math.ex
@@ -0,0 +1,26 @@
+defmodule Math do
+  @moduledoc """
+  Math utilities.
+  """
+  alias Extras
+
+  @doc """
+  Returns the prime factors for `n`.
+
+  ## Examples
+
+      iex> Math.factor(15)
+      [3, 5]
+
+  """
+  def factor(1), do: []
+
+  def factor(n) do
+    Extras.range(2, n - 1)
+    |> Enum.find(&(rem(n, &1) == 0))
+    |> case do
+      nil -> [n]
+      x -> [x | factor(div(n, x))]
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/router.ex b/users/wpcarro/assessments/semiprimes/server/lib/router.ex
new file mode 100644
index 0000000000..cb55520920
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/router.ex
@@ -0,0 +1,86 @@
+defmodule Router do
+  use Plug.Router
+  use Plug.Debugger
+  require Logger
+
+  plug(Plug.Logger, log: :debug)
+  plug(Plug.Parsers, parsers: [:urlencoded])
+  plug(:match)
+  plug(:dispatch)
+
+  @usage """
+  Usage: Try querying some of the following endpoints...
+    GET /
+    GET /help
+    GET /semiprime?number=<integer>
+    GET /semiprimes?numbers=<comma-separated-integers>
+  """
+
+  get "/" do
+    send_resp(conn, 200, "Welcome to Semiprimes Service!\n\n#{@usage}")
+  end
+
+  get "/help" do
+    send_resp(conn, 200, @usage)
+  end
+
+  get "/semiprime" do
+    case conn |> Map.get(:query_params) |> Map.get("number") do
+      nil ->
+        send_resp(conn, 400, "You must pass an integer as a query parameter. #{@usage}")
+
+      val ->
+        case Integer.parse(val) do
+          {n, ""} ->
+            send_resp(conn, 200, semiprime_response(n))
+
+          _ ->
+            send_resp(conn, 400, "We could not parse the number you provided.\n\n#{@usage}")
+        end
+    end
+  end
+
+  get "/semiprimes" do
+    case conn |> Map.get(:query_params) |> Map.get("numbers") do
+      nil ->
+        send_resp(
+          conn,
+          400,
+          "You must pass a comma-separated list of integers as a query parameter.\n\n#{@usage}"
+        )
+
+      xs ->
+        response =
+          xs
+          |> String.split(",")
+          |> Stream.map(&Integer.parse/1)
+          |> Stream.filter(fn
+            {n, ""} -> true
+            _ -> false
+          end)
+          |> Stream.map(fn {n, ""} -> semiprime_response(n) end)
+          |> Enum.join("\n")
+
+        send_resp(conn, 200, response)
+    end
+  end
+
+  match _ do
+    send_resp(conn, 404, "Not found.")
+  end
+
+  ################################################################################
+  # Utils
+  ################################################################################
+
+  defp semiprime_response(n) do
+    case Server.semiprime(n) do
+      nil ->
+        "#{n} is not a semiprime. Try another number!"
+
+      {hit_or_miss, factors} ->
+        response = "#{n} is a semiprime! Its factors are #{Enum.join(factors, " and ")}."
+        "Cache #{Atom.to_string(hit_or_miss)} - #{response}"
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/server.ex b/users/wpcarro/assessments/semiprimes/server/lib/server.ex
new file mode 100644
index 0000000000..7ab5e905b5
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/server.ex
@@ -0,0 +1,33 @@
+defmodule Server do
+  @moduledoc """
+  Documentation for `Server`.
+  """
+
+  @doc """
+  If `n` contains exactly two prime factors, return those prime factors;
+  otherwise, return nothing.
+  """
+  def semiprime(n) do
+    case Cache.get(n) do
+      nil ->
+        case do_semiprime(n) do
+          nil ->
+            nil
+
+          res ->
+            Cache.put(n, res)
+            {:miss, res}
+        end
+
+      hit ->
+        {:hit, hit}
+    end
+  end
+
+  defp do_semiprime(n) do
+    case Math.factor(n) do
+      [_, _] = res -> res
+      _ -> nil
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/sup.ex b/users/wpcarro/assessments/semiprimes/server/lib/sup.ex
new file mode 100644
index 0000000000..13a6ab374f
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/sup.ex
@@ -0,0 +1,23 @@
+defmodule Sup do
+  @moduledoc """
+  Top-level supervisor for our OTP application. For now, this supervisor starts
+  and monitors our cache.
+  """
+
+  use Supervisor
+  alias Plug.Adapters.Cowboy
+
+  def start_link(opts \\ []) do
+    Supervisor.start_link(__MODULE__, :ok, opts)
+  end
+
+  @impl true
+  def init(:ok) do
+    children = [
+      Cache,
+      Cowboy.child_spec(scheme: :http, plug: Router, options: [port: 8000])
+    ]
+
+    Supervisor.init(children, strategy: :one_for_one)
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/mix.exs b/users/wpcarro/assessments/semiprimes/server/mix.exs
new file mode 100644
index 0000000000..9062f927e7
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/mix.exs
@@ -0,0 +1,32 @@
+defmodule Server.MixProject do
+  use Mix.Project
+
+  def project do
+    [
+      app: :server,
+      version: "0.1.0",
+      elixir: "~> 1.10",
+      start_permanent: Mix.env() == :prod,
+      deps: deps()
+    ]
+  end
+
+  # Run "mix help compile.app" to learn about applications.
+  def application do
+    [
+      extra_applications: [:logger],
+      mod: {App, []}
+    ]
+  end
+
+  # Run "mix help deps" to learn about dependencies.
+  defp deps do
+    [
+      {:cortex, "~> 0.1", only: [:dev, :test]},
+      {:plug_cowboy, "~> 2.4.1"},
+      {:cowboy, "~> 2.8.0"},
+      {:plug, "~> 1.11.0"},
+      {:poison, "~> 4.0.1"}
+    ]
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/mix.lock b/users/wpcarro/assessments/semiprimes/server/mix.lock
new file mode 100644
index 0000000000..2ae7efbb3f
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/mix.lock
@@ -0,0 +1,14 @@
+%{
+  "cortex": {:hex, :cortex, "0.6.0", "8094830fae266eb0ae34d1a58983c0c49484341f5044fb4dfb81746647bd2993", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "d0ef5a2b1269626149118684dc4ea77dbfbc67017f4b4065b71dcefa26cfcc49"},
+  "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
+  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
+  "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
+  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+  "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
+  "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
+  "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
+  "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
+  "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
+  "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
+  "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
+}
diff --git a/users/wpcarro/assessments/semiprimes/server/test/extras_test.exs b/users/wpcarro/assessments/semiprimes/server/test/extras_test.exs
new file mode 100644
index 0000000000..67d0b8875c
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/test/extras_test.exs
@@ -0,0 +1,18 @@
+defmodule ExtrasTest do
+  use ExUnit.Case
+  doctest Extras
+
+  describe "range" do
+    test "returns an empty list for descending sequences" do
+      assert Extras.range(0, -2) == []
+    end
+
+    test "returns an empty list for non-ascending sequences" do
+      assert Extras.range(8, 8) == []
+    end
+
+    test "returns an exclusive range" do
+      assert Extras.range(3, 6) == [3, 4, 5]
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/test/math_test.exs b/users/wpcarro/assessments/semiprimes/server/test/math_test.exs
new file mode 100644
index 0000000000..c7186c824a
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/test/math_test.exs
@@ -0,0 +1,30 @@
+defmodule MathTest do
+  use ExUnit.Case
+  doctest Math
+
+  describe "factor" do
+    test "returns the prime factors for an input" do
+      [
+        {15, [3, 5]},
+        {12, [2, 2, 3]},
+        {9, [3, 3]},
+        {21, [3, 7]}
+      ]
+      |> Enum.map(fn {input, expected} ->
+        assert Math.factor(input) == expected
+      end)
+    end
+
+    test "handles large numbers" do
+      assert Math.factor(104_023) == [17, 29, 211]
+    end
+
+    test "returns an empty list for 1" do
+      assert Math.factor(1) == []
+    end
+
+    test "returns the prime number itself when the input is prime" do
+      assert Math.factor(7) == [7]
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/test/server_test.exs b/users/wpcarro/assessments/semiprimes/server/test/server_test.exs
new file mode 100644
index 0000000000..08d559734b
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/test/server_test.exs
@@ -0,0 +1,34 @@
+defmodule ServerTest do
+  use ExUnit.Case
+  doctest Server
+
+  describe "semiprime" do
+    test "returns the factors when the number is semiprime" do
+      Cache.clear()
+      # Semiprimes below 30
+      [
+        {4, [2, 2]},
+        {6, [2, 3]},
+        {9, [3, 3]},
+        {10, [2, 5]},
+        {14, [2, 7]},
+        {15, [3, 5]},
+        {21, [3, 7]},
+        {22, [2, 11]},
+        {25, [5, 5]},
+        {26, [2, 13]}
+      ]
+      |> Enum.each(fn {input, expected} ->
+        assert Server.semiprime(input) == {:miss, expected}
+      end)
+    end
+
+    test "returns nothing when the number is a composite number" do
+      # Composite numbers below 30
+      [1, 2, 3, 5, 7, 8, 11, 12, 13, 16, 17, 18, 19, 20, 23, 24, 27, 28, 29]
+      |> Enum.each(fn x ->
+        assert Server.semiprime(x) == nil
+      end)
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/test/test_helper.exs b/users/wpcarro/assessments/semiprimes/server/test/test_helper.exs
new file mode 100644
index 0000000000..869559e709
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start()
diff --git a/users/wpcarro/assessments/tt/.gitignore b/users/wpcarro/assessments/tt/.gitignore
new file mode 100644
index 0000000000..d4d62d436b
--- /dev/null
+++ b/users/wpcarro/assessments/tt/.gitignore
@@ -0,0 +1,6 @@
+.envrc
+*.db
+*.sqlite3
+!populate.sqlite3
+*.db-shm
+*.db-wal
\ No newline at end of file
diff --git a/users/wpcarro/assessments/tt/README.md b/users/wpcarro/assessments/tt/README.md
new file mode 100644
index 0000000000..0231ef3ab8
--- /dev/null
+++ b/users/wpcarro/assessments/tt/README.md
@@ -0,0 +1,50 @@
+# TT
+
+All of the commands defined herein should be run from the top-level directory of
+this repository (i.e. the directory in which this file exists).
+
+## Server
+
+To create the environment that contains all of this application's dependencies,
+run:
+
+```shell
+$ nix-shell
+```
+
+To run the server interactively, run:
+
+```shell
+$ cd src/
+$ ghci
+```
+
+Now compile and load the server with:
+
+```
+Prelude> :l Main.hs
+*Main> main
+```
+
+## Database
+
+Create a new database named `db.sqlite3` with:
+
+```shell
+$ sqlite3 db.sqlite3
+```
+
+Populate the database with:
+
+```
+sqlite3> .read populate.sqlite3
+```
+
+You can verify that everything is setup with:
+
+```
+sqlite3> .tables
+sqlite3> .schema
+sqlite3> SELECT * FROM Accounts;
+sqlite3> SELECT * FROM Trips;
+```
diff --git a/users/wpcarro/assessments/tt/client/.gitignore b/users/wpcarro/assessments/tt/client/.gitignore
new file mode 100644
index 0000000000..1cb4f3034c
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/.gitignore
@@ -0,0 +1,3 @@
+/elm-stuff
+/Main.min.js
+/output.css
diff --git a/users/wpcarro/assessments/tt/client/README.md b/users/wpcarro/assessments/tt/client/README.md
new file mode 100644
index 0000000000..04804ad94f
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/README.md
@@ -0,0 +1,18 @@
+# Elm
+
+Elm has one of the best developer experiences that I'm aware of. The error
+messages are helpful and the entire experience is optimized to improve the ease
+of writing web applications.
+
+## Developing
+
+If you're interested in contributing, the following will create an environment
+in which you can develop:
+
+```shell
+$ nix-shell
+$ npx tailwindcss build index.css -o output.css
+$ elm-live -- src/Main.elm --output=Main.min.js
+```
+
+You can now view your web client at `http://localhost:8000`!
diff --git a/users/wpcarro/assessments/tt/client/elm.json b/users/wpcarro/assessments/tt/client/elm.json
new file mode 100644
index 0000000000..c4095e118e
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/elm.json
@@ -0,0 +1,40 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "CurrySoftware/elm-datepicker": "4.0.0",
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/http": "2.0.0",
+            "elm/json": "1.1.3",
+            "elm/random": "1.0.0",
+            "elm/svg": "1.0.1",
+            "elm/time": "1.0.0",
+            "elm/url": "1.0.0",
+            "elm-community/json-extra": "4.2.0",
+            "elm-community/list-extra": "8.2.3",
+            "elm-community/maybe-extra": "5.2.0",
+            "elm-community/random-extra": "3.1.0",
+            "justinmimbs/date": "3.2.1",
+            "krisajenkins/remotedata": "6.0.1",
+            "ryannhg/date-format": "2.3.0"
+        },
+        "indirect": {
+            "elm/bytes": "1.0.8",
+            "elm/file": "1.0.5",
+            "elm/parser": "1.1.0",
+            "elm/virtual-dom": "1.0.2",
+            "owanturist/elm-union-find": "1.0.0",
+            "rtfeldman/elm-iso8601-date-strings": "1.1.3"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}
diff --git a/users/wpcarro/assessments/tt/client/index.css b/users/wpcarro/assessments/tt/client/index.css
new file mode 100644
index 0000000000..52114e0e9f
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/index.css
@@ -0,0 +1,142 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+.elm-datepicker--container {
+  position: relative;
+}
+
+.elm-datepicker--input:focus {
+  outline: 0;
+}
+
+.elm-datepicker--picker {
+  position: absolute;
+  border: 1px solid #CCC;
+  z-index: 10;
+  background-color: white;
+}
+
+.elm-datepicker--picker-header,
+.elm-datepicker--weekdays {
+  background: #F2F2F2;
+}
+
+.elm-datepicker--picker-header {
+  display: flex;
+  align-items: center;
+}
+
+.elm-datepicker--prev-container,
+.elm-datepicker--next-container {
+  flex: 0 1 auto;
+  cursor: pointer;
+}
+
+.elm-datepicker--month-container {
+  flex: 1 1 auto;
+  padding: 0.5em;
+  display: flex;
+  flex-direction: column;
+}
+
+.elm-datepicker--month,
+.elm-datepicker--year {
+  flex: 1 1 auto;
+  cursor: default;
+  text-align: center;
+}
+
+.elm-datepicker--year {
+  font-size: 0.6em;
+  font-weight: 700;
+}
+
+.elm-datepicker--prev,
+.elm-datepicker--next {
+  border: 6px solid transparent;
+  background-color: inherit;
+  display: block;
+  width: 0;
+  height: 0;
+  padding: 0 0.2em;
+}
+
+.elm-datepicker--prev {
+  border-right-color: #AAA;
+}
+
+.elm-datepicker--prev:hover {
+  border-right-color: #BBB;
+}
+
+.elm-datepicker--next {
+  border-left-color: #AAA;
+}
+
+.elm-datepicker--next:hover {
+  border-left-color: #BBB;
+}
+
+.elm-datepicker--table {
+  border-spacing: 0;
+  border-collapse: collapse;
+  font-size: 0.8em;
+}
+
+.elm-datepicker--table td {
+  width: 2em;
+  height: 2em;
+  text-align: center;
+}
+
+.elm-datepicker--row {
+  border-top: 1px solid #F2F2F2;
+}
+
+.elm-datepicker--dow {
+  border-bottom: 1px solid #CCC;
+  cursor: default;
+}
+
+.elm-datepicker--day {
+  cursor: pointer;
+}
+
+.elm-datepicker--day:hover {
+  background: #F2F2F2;
+}
+
+.elm-datepicker--disabled {
+  cursor: default;
+  color: #DDD;
+}
+
+.elm-datepicker--disabled:hover {
+  background: inherit;
+}
+
+.elm-datepicker--picked {
+  color: white;
+  background: darkblue;
+}
+
+.elm-datepicker--picked:hover {
+  background: darkblue;
+}
+
+.elm-datepicker--today {
+  font-weight: bold;
+}
+
+.elm-datepicker--other-month {
+  color: #AAA;
+}
+
+.elm-datepicker--other-month.elm-datepicker--disabled {
+  color: #EEE;
+}
+
+.elm-datepicker--other-month.elm-datepicker--picked {
+  color: white;
+}
diff --git a/users/wpcarro/assessments/tt/client/index.html b/users/wpcarro/assessments/tt/client/index.html
new file mode 100644
index 0000000000..9e6cef70db
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="google-signin-client_id" content="580018768696-4beppspj6cu7rhjnfgok8lbmh9a4n3ok.apps.googleusercontent.com">
+    <title>Elm SPA</title>
+    <link rel="stylesheet" type="text/css" href="./output.css" />
+    <link rel="stylesheet" type="text/css" href="./print.css" media="print" />
+    <script src="https://apis.google.com/js/platform.js" async defer></script>
+    <script src="./Main.min.js"></script>
+  </head>
+  <body class="font-serif">
+    <div id="mount"></div>
+    <script>
+     function onSignIn(googleUser) {
+       console.log(googleUser);
+     }
+
+     var app = Elm.Main.init({node: document.getElementById("mount")});
+
+     app.ports.printPage.subscribe(function() {
+       window.print();
+     });
+
+     app.ports.googleSignIn.subscribe(function() {
+       var auth2 = gapi.auth2.getAuthInstance();
+       var googleUser = auth2.signIn();
+     });
+
+     app.ports.googleSignOut.subscribe(function() {
+       var auth2 = gapi.auth2.getAuthInstance();
+       auth2.signOut().then(function() {
+         console.log('Google user successfully signed out.');
+       });
+     });
+    </script>
+  </body>
+</html>
diff --git a/users/wpcarro/assessments/tt/client/print.css b/users/wpcarro/assessments/tt/client/print.css
new file mode 100644
index 0000000000..3cfb279230
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/print.css
@@ -0,0 +1,3 @@
+.no-print {
+  display: none;
+}
diff --git a/users/wpcarro/assessments/tt/client/shell.nix b/users/wpcarro/assessments/tt/client/shell.nix
new file mode 100644
index 0000000000..78f55385db
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/shell.nix
@@ -0,0 +1,10 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    nodejs
+    elmPackages.elm
+    elmPackages.elm-format
+    elmPackages.elm-live
+  ];
+}
diff --git a/users/wpcarro/assessments/tt/client/src/Admin.elm b/users/wpcarro/assessments/tt/client/src/Admin.elm
new file mode 100644
index 0000000000..d95609ee15
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Admin.elm
@@ -0,0 +1,189 @@
+module Admin exposing (render)
+
+import Common
+import Date
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Maybe.Extra as ME
+import RemoteData
+import State
+import Tailwind
+import UI
+import Utils
+
+
+roleToggle : State.Model -> State.Role -> Html State.Msg
+roleToggle model role =
+    div [ [ "px-1", "inline" ] |> Tailwind.use |> class ]
+        [ UI.toggleButton
+            { toggled = model.inviteRole == Just role
+            , label = State.roleToString role
+            , handleEnable = State.UpdateInviteRole (Just role)
+            , handleDisable = State.UpdateInviteRole Nothing
+            }
+        ]
+
+
+inviteUser : State.Model -> Html State.Msg
+inviteUser model =
+    div [ [ "pb-6" ] |> Tailwind.use |> class ]
+        [ UI.header 3 "Invite a user"
+        , UI.textField
+            { handleInput = State.UpdateInviteEmail
+            , inputId = "invite-email"
+            , inputValue = model.inviteEmail
+            , pholder = "Email..."
+            }
+        , div [ [ "pt-4" ] |> Tailwind.use |> class ]
+            [ roleToggle model State.User
+            , roleToggle model State.Manager
+            , roleToggle model State.Admin
+            ]
+        , UI.baseButton
+            { enabled =
+                List.all
+                    identity
+                    [ String.length model.inviteEmail > 0
+                    , ME.isJust model.inviteRole
+                    ]
+            , extraClasses = [ "my-4" ]
+            , label =
+                case model.inviteResponseStatus of
+                    RemoteData.Loading ->
+                        "Sending..."
+
+                    _ ->
+                        "Send invitation"
+            , handleClick =
+                case model.inviteRole of
+                    Nothing ->
+                        State.DoNothing
+
+                    Just role ->
+                        State.AttemptInviteUser role
+            }
+        ]
+
+
+allTrips : State.Model -> Html State.Msg
+allTrips model =
+    case model.trips of
+        RemoteData.NotAsked ->
+            UI.absentData { handleFetch = State.AttemptGetTrips }
+
+        RemoteData.Loading ->
+            UI.paragraph "Loading..."
+
+        RemoteData.Failure e ->
+            UI.paragraph ("Error: " ++ Utils.explainHttpError e)
+
+        RemoteData.Success xs ->
+            ul []
+                (xs
+                    |> List.map
+                        (\trip ->
+                            li []
+                                [ UI.paragraph (Date.toIsoString trip.startDate ++ " - " ++ Date.toIsoString trip.endDate ++ ", " ++ trip.username ++ " is going " ++ trip.destination)
+                                , UI.textButton
+                                    { label = "delete"
+                                    , handleClick = State.AttemptDeleteTrip trip
+                                    }
+                                ]
+                        )
+                )
+
+
+allUsers : State.Model -> Html State.Msg
+allUsers model =
+    case model.accounts of
+        RemoteData.NotAsked ->
+            UI.absentData { handleFetch = State.AttemptGetAccounts }
+
+        RemoteData.Loading ->
+            UI.paragraph "Loading..."
+
+        RemoteData.Failure e ->
+            UI.paragraph ("Error: " ++ Utils.explainHttpError e)
+
+        RemoteData.Success xs ->
+            ul []
+                (xs
+                    |> List.map
+                        (\account ->
+                            li []
+                                [ UI.paragraph
+                                    (account.username
+                                        ++ " - "
+                                        ++ State.roleToString account.role
+                                    )
+                                , UI.textButton
+                                    { label = "delete"
+                                    , handleClick = State.AttemptDeleteAccount account.username
+                                    }
+                                ]
+                        )
+                )
+
+
+users : List String -> Html State.Msg
+users xs =
+    ul []
+        (xs
+            |> List.map
+                (\x ->
+                    li [ [ "py-4", "flex" ] |> Tailwind.use |> class ]
+                        [ p [ [ "flex-1" ] |> Tailwind.use |> class ] [ text x ]
+                        , div [ [ "flex-1" ] |> Tailwind.use |> class ]
+                            [ UI.simpleButton
+                                { label = "Delete"
+                                , handleClick = State.AttemptDeleteAccount x
+                                }
+                            ]
+                        ]
+                )
+        )
+
+
+render : State.Model -> Html State.Msg
+render model =
+    div
+        [ [ "container"
+          , "mx-auto"
+          , "text-center"
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ UI.header 2 "Welcome!"
+        , div []
+            [ UI.textButton
+                { label = "Logout"
+                , handleClick = State.AttemptLogout
+                }
+            ]
+        , div [ [ "py-3" ] |> Tailwind.use |> class ]
+            [ case model.adminTab of
+                State.Accounts ->
+                    UI.textButton
+                        { label = "Switch to trips"
+                        , handleClick = State.UpdateAdminTab State.Trips
+                        }
+
+                State.Trips ->
+                    UI.textButton
+                        { label = "Switch to accounts"
+                        , handleClick = State.UpdateAdminTab State.Accounts
+                        }
+            ]
+        , case model.adminTab of
+            State.Accounts ->
+                div []
+                    [ inviteUser model
+                    , allUsers model
+                    ]
+
+            State.Trips ->
+                allTrips model
+        , Common.allErrors model
+        ]
diff --git a/users/wpcarro/assessments/tt/client/src/Common.elm b/users/wpcarro/assessments/tt/client/src/Common.elm
new file mode 100644
index 0000000000..63ba97b794
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Common.elm
@@ -0,0 +1,37 @@
+module Common exposing (..)
+
+import Html exposing (..)
+import Maybe.Extra as ME
+import State
+import UI
+import Utils
+
+
+allErrors : State.Model -> Html State.Msg
+allErrors model =
+    div []
+        (State.allErrors
+            model
+            |> List.map
+                (\( mError, title ) ->
+                    case mError of
+                        Nothing ->
+                            text ""
+
+                        Just err ->
+                            UI.errorBanner
+                                { title = title
+                                , body = Utils.explainHttpError err
+                                }
+                )
+        )
+
+
+withSession : State.Model -> (State.Session -> Html State.Msg) -> Html State.Msg
+withSession model renderWithSession =
+    case model.session of
+        Nothing ->
+            div [] [ UI.paragraph "You need a valid session to view this page. Please attempt to log in." ]
+
+        Just session ->
+            renderWithSession session
diff --git a/users/wpcarro/assessments/tt/client/src/Login.elm b/users/wpcarro/assessments/tt/client/src/Login.elm
new file mode 100644
index 0000000000..b1a436098a
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Login.elm
@@ -0,0 +1,199 @@
+module Login exposing (render)
+
+import Common
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import State
+import Tailwind
+import UI
+import Utils
+
+
+googleSignIn : Html State.Msg
+googleSignIn =
+    div
+        [ class "g-signin2"
+        , attribute "onsuccess" "onSignIn"
+        , onClick State.GoogleSignIn
+        ]
+        []
+
+
+loginForm : State.Model -> Html State.Msg
+loginForm model =
+    div
+        [ [ "w-full"
+          , "max-w-xs"
+          , "mx-auto"
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ div
+            [ [ "bg-white"
+              , "shadow-md"
+              , "rounded"
+              , "px-8"
+              , "pt-6"
+              , "pb-8"
+              , "mb-4"
+              , "text-left"
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ div [ [ "text-center", "pb-6" ] |> Tailwind.use |> class ]
+                [ UI.textButton
+                    { handleClick = State.ToggleLoginForm
+                    , label =
+                        case model.loginTab of
+                            State.LoginForm ->
+                                "Switch to sign up"
+
+                            State.SignUpForm ->
+                                "Switch to login"
+                    }
+                ]
+            , div
+                [ [ "mb-4" ] |> Tailwind.use |> class ]
+                [ UI.label_ { for_ = "username", text_ = "Username" }
+                , UI.textField
+                    { inputId = "Username"
+                    , pholder = "Username"
+                    , handleInput = State.UpdateUsername
+                    , inputValue = model.username
+                    }
+                ]
+            , case model.loginTab of
+                State.LoginForm ->
+                    text ""
+
+                State.SignUpForm ->
+                    div
+                        [ [ "mb-4" ] |> Tailwind.use |> class ]
+                        [ UI.label_ { for_ = "email", text_ = "Email" }
+                        , input
+                            [ [ "shadow"
+                              , "appearance-none"
+                              , "border"
+                              , "rounded"
+                              , "w-full"
+                              , "py-2"
+                              , "px-3"
+                              , "text-gray-700"
+                              , "leading-tight"
+                              , "focus:outline-none"
+                              , "focus:shadow-outline"
+                              ]
+                                |> Tailwind.use
+                                |> class
+                            , id "email"
+                            , placeholder "who@domain.tld"
+                            , onInput State.UpdateEmail
+                            ]
+                            []
+                        ]
+            , div
+                [ [ "mb-4" ] |> Tailwind.use |> class ]
+                [ UI.label_ { for_ = "password", text_ = "Password" }
+                , input
+                    [ [ "shadow"
+                      , "appearance-none"
+                      , "border"
+                      , "rounded"
+                      , "w-full"
+                      , "py-2"
+                      , "px-3"
+                      , "text-gray-700"
+                      , "leading-tight"
+                      , "focus:outline-none"
+                      , "focus:shadow-outline"
+                      ]
+                        |> Tailwind.use
+                        |> class
+                    , id "password"
+                    , type_ "password"
+                    , placeholder "******************"
+                    , onInput State.UpdatePassword
+                    ]
+                    []
+                ]
+            , case model.loginTab of
+                State.LoginForm ->
+                    div [ [ "flex", "space-around" ] |> Tailwind.use |> class ]
+                        [ UI.simpleButton
+                            { handleClick = State.AttemptLogin
+                            , label = "Login"
+                            }
+                        , div [ [ "pl-4" ] |> Tailwind.use |> class ] [ googleSignIn ]
+                        ]
+
+                State.SignUpForm ->
+                    if
+                        List.all identity
+                            [ String.length model.username > 0
+                            , String.length model.email > 0
+                            , String.length model.password > 0
+                            ]
+                    then
+                        div []
+                            [ UI.simpleButton
+                                { handleClick = State.AttemptSignUp
+                                , label = "Sign up"
+                                }
+                            ]
+
+                    else
+                        UI.disabledButton { label = "Sign up" }
+            ]
+        ]
+
+
+login :
+    State.Model
+    -> Html State.Msg
+login model =
+    div
+        [ [ "text-center"
+          , "py-20"
+          , "bg-gray-200"
+          , "h-screen"
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ UI.header 3 "Welcome to Trip Planner"
+        , loginForm model
+        , Common.allErrors model
+        ]
+
+
+logout : State.Model -> Html State.Msg
+logout model =
+    div
+        [ [ "text-center"
+          , "py-20"
+          , "bg-gray-200"
+          , "h-screen"
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ UI.header 3 "Looks like you're already signed in..."
+        , UI.simpleButton
+            { label = "Logout"
+            , handleClick = State.AttemptLogout
+            }
+        , Common.allErrors model
+        ]
+
+
+render : State.Model -> Html State.Msg
+render model =
+    case model.session of
+        Nothing ->
+            login model
+
+        Just x ->
+            logout model
diff --git a/users/wpcarro/assessments/tt/client/src/Main.elm b/users/wpcarro/assessments/tt/client/src/Main.elm
new file mode 100644
index 0000000000..de71a72db0
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Main.elm
@@ -0,0 +1,62 @@
+module Main exposing (main)
+
+import Admin
+import Browser
+import Html exposing (..)
+import Login
+import Manager
+import State
+import Url
+import User
+
+
+viewForRoute : State.Route -> (State.Model -> Html State.Msg)
+viewForRoute route =
+    case route of
+        State.Login ->
+            Login.render
+
+        State.UserHome ->
+            User.render
+
+        State.ManagerHome ->
+            Manager.render
+
+        State.AdminHome ->
+            Admin.render
+
+
+view : State.Model -> Browser.Document State.Msg
+view model =
+    { title = "TripPlanner"
+    , body =
+        [ case ( model.session, model.route ) of
+            -- Redirect to /login when someone is not authenticated.
+            -- TODO(wpcarro): We should ensure that /login shows in the URL
+            -- bar.
+            ( Nothing, _ ) ->
+                Login.render model
+
+            ( Just session, Nothing ) ->
+                Login.render model
+
+            -- Authenticated
+            ( Just session, Just route ) ->
+                if State.isAuthorized session.role route then
+                    viewForRoute route model
+
+                else
+                    text "Access denied. You are not authorized to be here. Evacuate the area immediately"
+        ]
+    }
+
+
+main =
+    Browser.application
+        { init = State.init
+        , onUrlChange = State.UrlChanged
+        , onUrlRequest = State.LinkClicked
+        , subscriptions = \_ -> Sub.none
+        , update = State.update
+        , view = view
+        }
diff --git a/users/wpcarro/assessments/tt/client/src/Manager.elm b/users/wpcarro/assessments/tt/client/src/Manager.elm
new file mode 100644
index 0000000000..cd15c99a34
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Manager.elm
@@ -0,0 +1,70 @@
+module Manager exposing (render)
+
+import Array
+import Common
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import RemoteData
+import State
+import Tailwind
+import UI
+import Utils
+
+
+allUsers : State.Model -> Html State.Msg
+allUsers model =
+    case model.accounts of
+        RemoteData.NotAsked ->
+            UI.absentData { handleFetch = State.AttemptGetAccounts }
+
+        RemoteData.Loading ->
+            UI.paragraph "Loading..."
+
+        RemoteData.Failure e ->
+            UI.paragraph ("Error: " ++ Utils.explainHttpError e)
+
+        RemoteData.Success xs ->
+            ul []
+                (xs
+                    |> List.map
+                        (\account ->
+                            li []
+                                [ UI.paragraph
+                                    (account.username
+                                        ++ " - "
+                                        ++ State.roleToString account.role
+                                    )
+                                , UI.textButton
+                                    { label = "delete"
+                                    , handleClick = State.AttemptDeleteAccount account.username
+                                    }
+                                ]
+                        )
+                )
+
+
+render : State.Model -> Html State.Msg
+render model =
+    Common.withSession model
+        (\session ->
+            div
+                [ class
+                    ([ "container"
+                     , "mx-auto"
+                     , "text-center"
+                     ]
+                        |> Tailwind.use
+                    )
+                ]
+                [ h1 []
+                    [ UI.header 2 ("Welcome back, " ++ session.username ++ "!")
+                    , UI.textButton
+                        { label = "Logout"
+                        , handleClick = State.AttemptLogout
+                        }
+                    , allUsers model
+                    , Common.allErrors model
+                    ]
+                ]
+        )
diff --git a/users/wpcarro/assessments/tt/client/src/Shared.elm b/users/wpcarro/assessments/tt/client/src/Shared.elm
new file mode 100644
index 0000000000..addb0a4ffd
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Shared.elm
@@ -0,0 +1,7 @@
+module Shared exposing (..)
+
+clientOrigin =
+    "http://localhost:8000"
+
+serverOrigin =
+    "http://localhost:3000"
diff --git a/users/wpcarro/assessments/tt/client/src/State.elm b/users/wpcarro/assessments/tt/client/src/State.elm
new file mode 100644
index 0000000000..b3f78bb169
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/State.elm
@@ -0,0 +1,1014 @@
+port module State exposing (..)
+
+import Array exposing (Array)
+import Browser
+import Browser.Navigation as Nav
+import Date
+import DatePicker
+import Http
+import Json.Decode as JD
+import Json.Decode.Extra as JDE
+import Json.Encode as JE
+import Json.Encode.Extra as JEE
+import Process
+import RemoteData exposing (WebData)
+import Shared
+import Task
+import Time
+import Url
+import Url.Builder as UrlBuilder
+import Url.Parser exposing ((</>), Parser, int, map, oneOf, s, string)
+import Utils
+
+
+
+--------------------------------------------------------------------------------
+-- Types
+--------------------------------------------------------------------------------
+
+
+type Msg
+    = DoNothing
+    | UpdateUsername String
+    | UpdateEmail String
+    | UpdatePassword String
+    | UpdateRole String
+    | UpdateAdminTab AdminTab
+    | UpdateTripDestination String
+    | UpdateTripStartDate DatePicker.Msg
+    | UpdateTripEndDate DatePicker.Msg
+    | UpdateTripComment String
+    | UpdateEditTripDestination String
+    | UpdateEditTripComment String
+    | ClearErrors
+    | ToggleLoginForm
+    | PrintPage
+    | GoogleSignIn
+    | GoogleSignOut
+    | UpdateInviteEmail String
+    | UpdateInviteRole (Maybe Role)
+    | ReceiveTodaysDate Date.Date
+    | EditTrip Trip
+    | CancelEditTrip
+      -- SPA
+    | LinkClicked Browser.UrlRequest
+    | UrlChanged Url.Url
+      -- Outbound network
+    | AttemptGetAccounts
+    | AttemptGetTrips
+    | AttemptSignUp
+    | AttemptLogin
+    | AttemptLogout
+    | AttemptDeleteAccount String
+    | AttemptCreateTrip Date.Date Date.Date
+    | AttemptDeleteTrip Trip
+    | AttemptInviteUser Role
+    | AttemptUpdateTrip TripPK Trip
+      -- Inbound network
+    | GotAccounts (WebData (List Account))
+    | GotTrips (WebData (List Trip))
+    | GotSignUp (Result Http.Error Session)
+    | GotLogin (Result Http.Error Session)
+    | GotLogout (Result Http.Error String)
+    | GotDeleteAccount (Result Http.Error String)
+    | GotCreateTrip (Result Http.Error ())
+    | GotDeleteTrip (Result Http.Error ())
+    | GotInviteUser (Result Http.Error ())
+    | GotUpdateTrip (Result Http.Error ())
+
+
+type Route
+    = Login
+    | UserHome
+    | ManagerHome
+    | AdminHome
+
+
+type Role
+    = User
+    | Manager
+    | Admin
+
+
+type alias Account =
+    { username : String
+    , role : Role
+    }
+
+
+type alias Session =
+    { role : Role
+    , username : String
+    }
+
+
+type alias Review =
+    { rowid : Int
+    , content : String
+    , rating : Int
+    , user : String
+    , dateOfVisit : String
+    }
+
+
+type AdminTab
+    = Accounts
+    | Trips
+
+
+type LoginTab
+    = LoginForm
+    | SignUpForm
+
+
+type alias Trip =
+    { username : String
+    , destination : String
+    , startDate : Date.Date
+    , endDate : Date.Date
+    , comment : String
+    }
+
+
+type alias TripPK =
+    { username : String
+    , destination : String
+    , startDate : Date.Date
+    }
+
+
+type alias Model =
+    { route : Maybe Route
+    , url : Url.Url
+    , key : Nav.Key
+    , session : Maybe Session
+    , todaysDate : Maybe Date.Date
+    , username : String
+    , email : String
+    , password : String
+    , role : Maybe Role
+    , accounts : WebData (List Account)
+    , startDatePicker : DatePicker.DatePicker
+    , endDatePicker : DatePicker.DatePicker
+    , tripDestination : String
+    , tripStartDate : Maybe Date.Date
+    , tripEndDate : Maybe Date.Date
+    , tripComment : String
+    , trips : WebData (List Trip)
+    , editingTrip : Maybe Trip
+    , editTripDestination : String
+    , editTripComment : String
+    , adminTab : AdminTab
+    , loginTab : LoginTab
+    , inviteEmail : String
+    , inviteRole : Maybe Role
+    , inviteResponseStatus : WebData ()
+    , updateTripStatus : WebData ()
+    , loginError : Maybe Http.Error
+    , logoutError : Maybe Http.Error
+    , signUpError : Maybe Http.Error
+    , deleteUserError : Maybe Http.Error
+    , createTripError : Maybe Http.Error
+    , deleteTripError : Maybe Http.Error
+    , inviteUserError : Maybe Http.Error
+    }
+
+
+allErrors : Model -> List ( Maybe Http.Error, String )
+allErrors model =
+    [ ( model.loginError, "Error attempting to authenticate" )
+    , ( model.logoutError, "Error attempting to log out" )
+    , ( model.signUpError, "Error attempting to create your account" )
+    , ( model.deleteUserError, "Error attempting to delete a user" )
+    , ( model.createTripError, "Error attempting to create a trip" )
+    , ( model.inviteUserError, "Error attempting to invite a user" )
+    ]
+
+
+
+--------------------------------------------------------------------------------
+-- Functions
+--------------------------------------------------------------------------------
+
+
+roleToString : Role -> String
+roleToString role =
+    case role of
+        User ->
+            "user"
+
+        Manager ->
+            "manager"
+
+        Admin ->
+            "admin"
+
+
+endpoint : List String -> List UrlBuilder.QueryParameter -> String
+endpoint =
+    UrlBuilder.crossOrigin Shared.serverOrigin
+
+
+encodeRole : Role -> JE.Value
+encodeRole x =
+    case x of
+        User ->
+            JE.string "user"
+
+        Manager ->
+            JE.string "manager"
+
+        Admin ->
+            JE.string "admin"
+
+
+decodeRole : JD.Decoder Role
+decodeRole =
+    let
+        toRole : String -> JD.Decoder Role
+        toRole s =
+            case s of
+                "user" ->
+                    JD.succeed User
+
+                "manager" ->
+                    JD.succeed Manager
+
+                "admin" ->
+                    JD.succeed Admin
+
+                x ->
+                    JD.fail ("Invalid input: " ++ x)
+    in
+    JD.string |> JD.andThen toRole
+
+
+decodeSession : JD.Decoder Session
+decodeSession =
+    JD.map2
+        Session
+        (JD.field "role" decodeRole)
+        (JD.field "username" JD.string)
+
+
+encodeLoginRequest : String -> String -> JE.Value
+encodeLoginRequest username password =
+    JE.object
+        [ ( "username", JE.string username )
+        , ( "password", JE.string password )
+        ]
+
+
+login : String -> String -> Cmd Msg
+login username password =
+    Utils.postWithCredentials
+        { url = endpoint [ "login" ] []
+        , body = Http.jsonBody (encodeLoginRequest username password)
+        , expect = Http.expectJson GotLogin decodeSession
+        }
+
+
+logout : Cmd Msg
+logout =
+    Utils.getWithCredentials
+        { url = endpoint [ "logout" ] []
+        , expect = Http.expectString GotLogout
+        }
+
+
+signUp :
+    { username : String
+    , email : String
+    , password : String
+    }
+    -> Cmd Msg
+signUp { username, email, password } =
+    Utils.postWithCredentials
+        { url = endpoint [ "accounts" ] []
+        , body =
+            Http.jsonBody
+                (JE.object
+                    [ ( "username", JE.string username )
+                    , ( "email", JE.string username )
+                    , ( "password", JE.string password )
+                    , ( "role", JE.string "user" )
+                    ]
+                )
+        , expect = Http.expectJson GotSignUp decodeSession
+        }
+
+
+updateTrip : TripPK -> Trip -> Cmd Msg
+updateTrip tripKey trip =
+    Utils.putWithCredentials
+        { url = endpoint [ "trips" ] []
+        , body =
+            Http.jsonBody
+                (JE.object
+                    [ ( "tripKey", encodeTripKey tripKey )
+                    , ( "destination", JE.string trip.destination )
+                    , ( "startDate", encodeDate trip.startDate )
+                    , ( "endDate", encodeDate trip.endDate )
+                    , ( "comment", JE.string trip.comment )
+                    ]
+                )
+        , expect = Http.expectWhatever GotUpdateTrip
+        }
+
+
+inviteUser : { email : String, role : Role } -> Cmd Msg
+inviteUser { email, role } =
+    Utils.postWithCredentials
+        { url = endpoint [ "invite" ] []
+        , body =
+            Http.jsonBody
+                (JE.object
+                    [ ( "email", JE.string email )
+                    , ( "role", encodeRole role )
+                    ]
+                )
+        , expect = Http.expectWhatever GotInviteUser
+        }
+
+
+createTrip :
+    { username : String
+    , destination : String
+    , startDate : Date.Date
+    , endDate : Date.Date
+    , comment : String
+    }
+    -> Cmd Msg
+createTrip { username, destination, startDate, endDate, comment } =
+    Utils.postWithCredentials
+        { url = endpoint [ "trips" ] []
+        , body =
+            Http.jsonBody
+                (JE.object
+                    [ ( "username", JE.string username )
+                    , ( "destination", JE.string destination )
+                    , ( "startDate", encodeDate startDate )
+                    , ( "endDate", encodeDate endDate )
+                    , ( "comment", JE.string comment )
+                    ]
+                )
+        , expect = Http.expectWhatever GotCreateTrip
+        }
+
+
+deleteTrip :
+    { username : String
+    , destination : String
+    , startDate : Date.Date
+    }
+    -> Cmd Msg
+deleteTrip { username, destination, startDate } =
+    Utils.deleteWithCredentials
+        { url = endpoint [ "trips" ] []
+        , body =
+            Http.jsonBody
+                (JE.object
+                    [ ( "username", JE.string username )
+                    , ( "destination", JE.string destination )
+                    , ( "startDate", encodeDate startDate )
+                    ]
+                )
+        , expect = Http.expectWhatever GotDeleteTrip
+        }
+
+
+deleteAccount : String -> Cmd Msg
+deleteAccount username =
+    Utils.deleteWithCredentials
+        { url = endpoint [ "accounts" ] [ UrlBuilder.string "username" username ]
+        , body = Http.emptyBody
+        , expect = Http.expectString GotDeleteAccount
+        }
+
+
+decodeReview : JD.Decoder Review
+decodeReview =
+    JD.map5
+        Review
+        (JD.field "rowid" JD.int)
+        (JD.field "content" JD.string)
+        (JD.field "rating" JD.int)
+        (JD.field "user" JD.string)
+        (JD.field "timestamp" JD.string)
+
+
+encodeTripKey : TripPK -> JE.Value
+encodeTripKey tripKey =
+    JE.object
+        [ ( "username", JE.string tripKey.username )
+        , ( "destination", JE.string tripKey.destination )
+        , ( "startDate", encodeDate tripKey.startDate )
+        ]
+
+
+encodeDate : Date.Date -> JE.Value
+encodeDate date =
+    date |> Date.toIsoString |> JE.string
+
+
+decodeDate : JD.Decoder Date.Date
+decodeDate =
+    JD.string |> JD.andThen (Date.fromIsoString >> JDE.fromResult)
+
+
+fetchTrips : Cmd Msg
+fetchTrips =
+    Utils.getWithCredentials
+        { url = endpoint [ "trips" ] []
+        , expect =
+            Http.expectJson
+                (RemoteData.fromResult >> GotTrips)
+                (JD.list
+                    (JD.map5
+                        Trip
+                        (JD.field "username" JD.string)
+                        (JD.field "destination" JD.string)
+                        (JD.field "startDate" decodeDate)
+                        (JD.field "endDate" decodeDate)
+                        (JD.field "comment" JD.string)
+                    )
+                )
+        }
+
+
+fetchAccounts : Cmd Msg
+fetchAccounts =
+    Utils.getWithCredentials
+        { url = endpoint [ "accounts" ] []
+        , expect =
+            Http.expectJson
+                (RemoteData.fromResult >> GotAccounts)
+                (JD.list
+                    (JD.map2
+                        Account
+                        (JD.field "username" JD.string)
+                        (JD.field "role" decodeRole)
+                    )
+                )
+        }
+
+
+sleepAndClearErrors : Cmd Msg
+sleepAndClearErrors =
+    Process.sleep 4000
+        |> Task.perform (\_ -> ClearErrors)
+
+
+isAuthorized : Role -> Route -> Bool
+isAuthorized role route =
+    case ( role, route ) of
+        ( User, _ ) ->
+            True
+
+        ( Manager, _ ) ->
+            True
+
+        ( Admin, _ ) ->
+            True
+
+
+homeRouteForRole : Role -> String
+homeRouteForRole role =
+    case role of
+        User ->
+            "/user"
+
+        Manager ->
+            "/manager"
+
+        Admin ->
+            "/admin"
+
+
+routeParser : Parser (Route -> a) a
+routeParser =
+    oneOf
+        [ map Login (s "topic")
+        , map UserHome (s "user")
+        , map ManagerHome (s "manager")
+        , map AdminHome (s "admin")
+        ]
+
+
+{-| Set init to `prod` when going live.
+-}
+prod : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
+prod _ url key =
+    let
+        ( startDatePicker, startDatePickerCmd ) =
+            DatePicker.init
+
+        ( endDatePicker, endDatePickerCmd ) =
+            DatePicker.init
+    in
+    ( { route = Nothing
+      , url = url
+      , key = key
+      , session = Nothing
+      , todaysDate = Nothing
+      , username = ""
+      , email = ""
+      , password = ""
+      , role = Nothing
+      , accounts = RemoteData.NotAsked
+      , tripDestination = ""
+      , tripStartDate = Nothing
+      , tripEndDate = Nothing
+      , tripComment = ""
+      , trips = RemoteData.NotAsked
+      , editingTrip = Nothing
+      , editTripDestination = ""
+      , editTripComment = ""
+      , startDatePicker = startDatePicker
+      , endDatePicker = endDatePicker
+      , adminTab = Accounts
+      , loginTab = LoginForm
+      , inviteEmail = ""
+      , inviteRole = Nothing
+      , inviteResponseStatus = RemoteData.NotAsked
+      , updateTripStatus = RemoteData.NotAsked
+      , loginError = Nothing
+      , logoutError = Nothing
+      , signUpError = Nothing
+      , deleteUserError = Nothing
+      , createTripError = Nothing
+      , deleteTripError = Nothing
+      , inviteUserError = Nothing
+      }
+    , Cmd.batch
+        [ Cmd.map UpdateTripStartDate startDatePickerCmd
+        , Cmd.map UpdateTripEndDate endDatePickerCmd
+        , Date.today |> Task.perform ReceiveTodaysDate
+        ]
+    )
+
+
+{-| When working on a feature for the UserHome, use this.
+-}
+userHome : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
+userHome flags url key =
+    let
+        ( model, cmd ) =
+            prod flags url key
+    in
+    ( { model
+        | route = Just UserHome
+        , session = Just { username = "mimi", role = User }
+        , trips =
+            RemoteData.Success
+                [ { username = "mimi"
+                  , destination = "Barcelona"
+                  , startDate = Date.fromCalendarDate 2020 Time.Sep 25
+                  , endDate = Date.fromCalendarDate 2020 Time.Oct 5
+                  , comment = "Blah"
+                  }
+                , { username = "mimi"
+                  , destination = "Paris"
+                  , startDate = Date.fromCalendarDate 2021 Time.Jan 1
+                  , endDate = Date.fromCalendarDate 2021 Time.Feb 1
+                  , comment = "Bon voyage!"
+                  }
+                ]
+      }
+    , cmd
+    )
+
+
+managerHome : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
+managerHome flags url key =
+    let
+        ( model, cmd ) =
+            prod flags url key
+    in
+    ( { model
+        | route = Just ManagerHome
+        , session = Just { username = "bill", role = Manager }
+      }
+    , cmd
+    )
+
+
+adminHome : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
+adminHome flags url key =
+    let
+        ( model, cmd ) =
+            prod flags url key
+    in
+    ( { model
+        | route = Just AdminHome
+        , session = Just { username = "wpcarro", role = Admin }
+      }
+    , cmd
+    )
+
+
+port printPage : () -> Cmd msg
+
+
+port googleSignIn : () -> Cmd msg
+
+
+port googleSignOut : () -> Cmd msg
+
+
+{-| The initial state for the application.
+-}
+init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
+init flags url key =
+    prod flags url key
+
+
+{-| Now that we have state, we need a function to change the state.
+-}
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        DoNothing ->
+            ( model, Cmd.none )
+
+        UpdateUsername x ->
+            ( { model | username = x }, Cmd.none )
+
+        UpdatePassword x ->
+            ( { model | password = x }, Cmd.none )
+
+        UpdateEmail x ->
+            ( { model | email = x }, Cmd.none )
+
+        UpdateAdminTab x ->
+            ( { model | adminTab = x }, Cmd.none )
+
+        UpdateRole x ->
+            let
+                maybeRole =
+                    case x of
+                        "user" ->
+                            Just User
+
+                        "manager" ->
+                            Just Manager
+
+                        "admin" ->
+                            Just Admin
+
+                        _ ->
+                            Nothing
+            in
+            ( { model | role = maybeRole }, Cmd.none )
+
+        UpdateTripDestination x ->
+            ( { model | tripDestination = x }, Cmd.none )
+
+        UpdateTripStartDate dpMsg ->
+            let
+                ( newDatePicker, dateEvent ) =
+                    DatePicker.update DatePicker.defaultSettings dpMsg model.startDatePicker
+
+                newDate =
+                    case dateEvent of
+                        DatePicker.Picked changedDate ->
+                            Just changedDate
+
+                        _ ->
+                            model.tripStartDate
+            in
+            ( { model
+                | tripStartDate = newDate
+                , startDatePicker = newDatePicker
+              }
+            , Cmd.none
+            )
+
+        UpdateTripEndDate dpMsg ->
+            let
+                ( newDatePicker, dateEvent ) =
+                    DatePicker.update DatePicker.defaultSettings dpMsg model.endDatePicker
+
+                newDate =
+                    case dateEvent of
+                        DatePicker.Picked changedDate ->
+                            Just changedDate
+
+                        _ ->
+                            model.tripEndDate
+            in
+            ( { model
+                | tripEndDate = newDate
+                , endDatePicker = newDatePicker
+              }
+            , Cmd.none
+            )
+
+        UpdateTripComment x ->
+            ( { model | tripComment = x }, Cmd.none )
+
+        UpdateEditTripDestination x ->
+            ( { model | editTripDestination = x }, Cmd.none )
+
+        UpdateEditTripComment x ->
+            ( { model | editTripComment = x }, Cmd.none )
+
+        ClearErrors ->
+            ( { model
+                | loginError = Nothing
+                , logoutError = Nothing
+                , signUpError = Nothing
+                , deleteUserError = Nothing
+                , createTripError = Nothing
+              }
+            , Cmd.none
+            )
+
+        ToggleLoginForm ->
+            ( { model
+                | loginTab =
+                    case model.loginTab of
+                        LoginForm ->
+                            SignUpForm
+
+                        SignUpForm ->
+                            LoginForm
+              }
+            , Cmd.none
+            )
+
+        PrintPage ->
+            ( model, printPage () )
+
+        GoogleSignIn ->
+            ( model, googleSignIn () )
+
+        GoogleSignOut ->
+            ( model, googleSignOut () )
+
+        UpdateInviteEmail x ->
+            ( { model | inviteEmail = x }, Cmd.none )
+
+        UpdateInviteRole mRole ->
+            ( { model | inviteRole = mRole }, Cmd.none )
+
+        ReceiveTodaysDate date ->
+            ( { model | todaysDate = Just date }, Cmd.none )
+
+        EditTrip trip ->
+            ( { model
+                | editingTrip = Just trip
+                , editTripDestination = trip.destination
+                , editTripComment = trip.comment
+              }
+            , Cmd.none
+            )
+
+        CancelEditTrip ->
+            ( { model
+                | editingTrip = Nothing
+                , editTripDestination = ""
+                , editTripComment = ""
+              }
+            , Cmd.none
+            )
+
+        LinkClicked urlRequest ->
+            case urlRequest of
+                Browser.Internal url ->
+                    ( model, Nav.pushUrl model.key (Url.toString url) )
+
+                Browser.External href ->
+                    ( model, Nav.load href )
+
+        UrlChanged url ->
+            let
+                route =
+                    Url.Parser.parse routeParser url
+            in
+            case route of
+                Just UserHome ->
+                    ( { model
+                        | url = url
+                        , route = route
+                        , trips = RemoteData.Loading
+                      }
+                    , fetchTrips
+                    )
+
+                Just ManagerHome ->
+                    ( { model
+                        | url = url
+                        , route = route
+                        , accounts = RemoteData.Loading
+                      }
+                    , fetchAccounts
+                    )
+
+                Just AdminHome ->
+                    ( { model
+                        | url = url
+                        , route = route
+                        , accounts = RemoteData.Loading
+                        , trips = RemoteData.Loading
+                      }
+                    , Cmd.batch
+                        [ fetchAccounts
+                        , fetchTrips
+                        ]
+                    )
+
+                _ ->
+                    ( { model
+                        | url = url
+                        , route = route
+                      }
+                    , Cmd.none
+                    )
+
+        -- GET /accounts
+        AttemptGetAccounts ->
+            ( { model | accounts = RemoteData.Loading }, fetchAccounts )
+
+        GotAccounts xs ->
+            ( { model | accounts = xs }, Cmd.none )
+
+        -- DELETE /accounts
+        AttemptDeleteAccount username ->
+            ( model, deleteAccount username )
+
+        GotDeleteAccount result ->
+            case result of
+                Ok _ ->
+                    ( model, fetchAccounts )
+
+                Err e ->
+                    ( { model | deleteUserError = Just e }
+                    , sleepAndClearErrors
+                    )
+
+        -- POST /trips
+        AttemptCreateTrip startDate endDate ->
+            ( model
+            , case model.session of
+                Nothing ->
+                    Cmd.none
+
+                Just session ->
+                    createTrip
+                        { username = session.username
+                        , destination = model.tripDestination
+                        , startDate = startDate
+                        , endDate = endDate
+                        , comment = model.tripComment
+                        }
+            )
+
+        GotCreateTrip result ->
+            case result of
+                Ok _ ->
+                    ( { model
+                        | tripDestination = ""
+                        , tripStartDate = Nothing
+                        , tripEndDate = Nothing
+                        , tripComment = ""
+                      }
+                    , fetchTrips
+                    )
+
+                Err e ->
+                    ( { model
+                        | createTripError = Just e
+                        , tripDestination = ""
+                        , tripStartDate = Nothing
+                        , tripEndDate = Nothing
+                        , tripComment = ""
+                      }
+                    , sleepAndClearErrors
+                    )
+
+        -- DELETE /trips
+        AttemptDeleteTrip trip ->
+            ( model
+            , deleteTrip
+                { username = trip.username
+                , destination = trip.destination
+                , startDate = trip.startDate
+                }
+            )
+
+        GotDeleteTrip result ->
+            case result of
+                Ok _ ->
+                    ( model, fetchTrips )
+
+                Err e ->
+                    ( { model | deleteTripError = Just e }
+                    , sleepAndClearErrors
+                    )
+
+        AttemptInviteUser role ->
+            ( { model | inviteResponseStatus = RemoteData.Loading }
+            , inviteUser
+                { email = model.inviteEmail
+                , role = role
+                }
+            )
+
+        GotInviteUser result ->
+            case result of
+                Ok _ ->
+                    ( { model
+                        | inviteEmail = ""
+                        , inviteRole = Nothing
+                        , inviteResponseStatus = RemoteData.Success ()
+                      }
+                    , Cmd.none
+                    )
+
+                Err e ->
+                    ( { model
+                        | inviteUserError = Just e
+                        , inviteResponseStatus = RemoteData.Failure e
+                      }
+                    , sleepAndClearErrors
+                    )
+
+        -- PATCH /trips
+        AttemptUpdateTrip tripKey trip ->
+            ( { model | updateTripStatus = RemoteData.Loading }
+            , updateTrip tripKey trip
+            )
+
+        GotUpdateTrip result ->
+            case result of
+                Ok _ ->
+                    ( { model | updateTripStatus = RemoteData.Success () }
+                    , fetchTrips
+                    )
+
+                Err e ->
+                    ( { model | updateTripStatus = RemoteData.Failure e }
+                    , Cmd.none
+                    )
+
+        -- POST /accounts
+        AttemptSignUp ->
+            ( model
+            , signUp
+                { username = model.username
+                , email = model.email
+                , password = model.password
+                }
+            )
+
+        GotSignUp result ->
+            case result of
+                Ok session ->
+                    ( { model | session = Just session }
+                    , Nav.pushUrl model.key (homeRouteForRole session.role)
+                    )
+
+                Err x ->
+                    ( { model | signUpError = Just x }
+                    , sleepAndClearErrors
+                    )
+
+        -- GET /trips
+        AttemptGetTrips ->
+            ( { model | trips = RemoteData.Loading }, fetchTrips )
+
+        GotTrips xs ->
+            ( { model | trips = xs }, Cmd.none )
+
+        -- POST /login
+        AttemptLogin ->
+            ( model, login model.username model.password )
+
+        GotLogin result ->
+            case result of
+                Ok session ->
+                    ( { model | session = Just session }
+                    , Nav.pushUrl model.key (homeRouteForRole session.role)
+                    )
+
+                Err x ->
+                    ( { model | loginError = Just x }
+                    , sleepAndClearErrors
+                    )
+
+        -- GET /logout
+        AttemptLogout ->
+            ( model, logout )
+
+        GotLogout result ->
+            case result of
+                Ok _ ->
+                    ( { model | session = Nothing }
+                    , Nav.pushUrl model.key "/login"
+                    )
+
+                Err e ->
+                    ( { model | logoutError = Just e }
+                    , sleepAndClearErrors
+                    )
diff --git a/users/wpcarro/assessments/tt/client/src/Tailwind.elm b/users/wpcarro/assessments/tt/client/src/Tailwind.elm
new file mode 100644
index 0000000000..57d419db5a
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Tailwind.elm
@@ -0,0 +1,29 @@
+module Tailwind exposing (..)
+
+{-| Functions to make Tailwind development in Elm even more pleasant.
+-}
+
+
+{-| Conditionally use `class` selection when `condition` is true.
+-}
+when : Bool -> String -> String
+when condition class =
+    if condition then
+        class
+
+    else
+        ""
+
+
+if_ : Bool -> String -> String -> String
+if_ condition whenTrue whenFalse =
+    if condition then
+        whenTrue
+
+    else
+        whenFalse
+
+
+use : List String -> String
+use styles =
+    String.join " " styles
diff --git a/users/wpcarro/assessments/tt/client/src/UI.elm b/users/wpcarro/assessments/tt/client/src/UI.elm
new file mode 100644
index 0000000000..7f8f379795
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/UI.elm
@@ -0,0 +1,318 @@
+module UI exposing (..)
+
+import Date
+import DatePicker exposing (defaultSettings)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import State
+import Tailwind
+
+
+label_ : { for_ : String, text_ : String } -> Html msg
+label_ { for_, text_ } =
+    label
+        [ [ "block"
+          , "text-gray-700"
+          , "text-sm"
+          , "font-bold"
+          , "mb-2"
+          ]
+            |> Tailwind.use
+            |> class
+        , for for_
+        ]
+        [ text text_ ]
+
+
+errorBanner : { title : String, body : String } -> Html msg
+errorBanner { title, body } =
+    div
+        [ [ "text-left"
+          , "fixed"
+          , "container"
+          , "top-0"
+          , "mt-6"
+          ]
+            |> Tailwind.use
+            |> class
+        , style "left" "50%"
+
+        -- TODO(wpcarro): Consider supporting breakpoints, but for now
+        -- don't.
+        , style "width" "800px"
+        , style "margin-left" "-400px"
+        ]
+        [ div
+            [ [ "bg-red-500"
+              , "text-white"
+              , "font-bold"
+              , "rounded-t"
+              , "px-4"
+              , "py-2"
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ text title ]
+        , div
+            [ [ "border"
+              , "border-t-0"
+              , "border-red-400"
+              , "rounded-b"
+              , "bg-red-100"
+              , "px-4"
+              , "py-3"
+              , "text-red-700"
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ p [] [ text body ] ]
+        ]
+
+
+baseButton :
+    { label : String
+    , enabled : Bool
+    , handleClick : msg
+    , extraClasses : List String
+    }
+    -> Html msg
+baseButton { label, enabled, handleClick, extraClasses } =
+    button
+        [ [ if enabled then
+                "bg-blue-500"
+
+            else
+                "bg-gray-500"
+          , if enabled then
+                "hover:bg-blue-700"
+
+            else
+                ""
+          , if enabled then
+                ""
+
+            else
+                "cursor-not-allowed"
+          , "text-white"
+          , "font-bold"
+          , "py-1"
+          , "shadow-lg"
+          , "px-4"
+          , "rounded"
+          , "focus:outline-none"
+          , "focus:shadow-outline"
+          ]
+            ++ extraClasses
+            |> Tailwind.use
+            |> class
+        , onClick handleClick
+        , disabled (not enabled)
+        ]
+        [ text label ]
+
+
+simpleButton :
+    { label : String
+    , handleClick : msg
+    }
+    -> Html msg
+simpleButton { label, handleClick } =
+    baseButton
+        { label = label
+        , enabled = True
+        , handleClick = handleClick
+        , extraClasses = []
+        }
+
+
+disabledButton :
+    { label : String }
+    -> Html State.Msg
+disabledButton { label } =
+    baseButton
+        { label = label
+        , enabled = False
+        , handleClick = State.DoNothing
+        , extraClasses = []
+        }
+
+
+textButton :
+    { label : String
+    , handleClick : msg
+    }
+    -> Html msg
+textButton { label, handleClick } =
+    button
+        [ [ "text-blue-600"
+          , "hover:text-blue-500"
+          , "font-bold"
+          , "hover:underline"
+          , "focus:outline-none"
+          ]
+            |> Tailwind.use
+            |> class
+        , onClick handleClick
+        ]
+        [ text label ]
+
+
+textField :
+    { pholder : String
+    , inputId : String
+    , handleInput : String -> msg
+    , inputValue : String
+    }
+    -> Html msg
+textField { pholder, inputId, handleInput, inputValue } =
+    input
+        [ [ "shadow"
+          , "appearance-none"
+          , "border"
+          , "rounded"
+          , "w-full"
+          , "py-2"
+          , "px-3"
+          , "text-gray-700"
+          , "leading-tight"
+          , "focus:outline-none"
+          , "focus:shadow-outline"
+          ]
+            |> Tailwind.use
+            |> class
+        , id inputId
+        , value inputValue
+        , placeholder pholder
+        , onInput handleInput
+        ]
+        []
+
+
+toggleButton :
+    { toggled : Bool
+    , label : String
+    , handleEnable : msg
+    , handleDisable : msg
+    }
+    -> Html msg
+toggleButton { toggled, label, handleEnable, handleDisable } =
+    button
+        [ [ if toggled then
+                "bg-blue-700"
+
+            else
+                "bg-blue-500"
+          , "hover:bg-blue-700"
+          , "text-white"
+          , "font-bold"
+          , "py-2"
+          , "px-4"
+          , "rounded"
+          , "focus:outline-none"
+          , "focus:shadow-outline"
+          ]
+            |> Tailwind.use
+            |> class
+        , onClick
+            (if toggled then
+                handleDisable
+
+             else
+                handleEnable
+            )
+        ]
+        [ text label ]
+
+
+paragraph : String -> Html msg
+paragraph x =
+    p [ [ "text-xl" ] |> Tailwind.use |> class ] [ text x ]
+
+
+header : Int -> String -> Html msg
+header which x =
+    let
+        hStyles =
+            case which of
+                1 ->
+                    [ "text-6xl"
+                    , "py-12"
+                    ]
+
+                2 ->
+                    [ "text-3xl"
+                    , "py-6"
+                    ]
+
+                _ ->
+                    [ "text-2xl"
+                    , "py-2"
+                    ]
+    in
+    h1
+        [ hStyles
+            ++ [ "font-bold"
+               , "text-gray-700"
+               ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ text x ]
+
+
+link : String -> String -> Html msg
+link path label =
+    a
+        [ href path
+        , [ "underline"
+          , "text-blue-600"
+          , "text-xl"
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ text label ]
+
+
+absentData : { handleFetch : msg } -> Html msg
+absentData { handleFetch } =
+    div []
+        [ paragraph "Welp... it looks like you've caught us in a state that we considered impossible: we did not fetch the data upon which this page depends. Maybe you can help us out by clicking the super secret, highly privileged \"Fetch data\" button below (we don't normally show people this)."
+        , div [ [ "py-4" ] |> Tailwind.use |> class ]
+            [ simpleButton
+                { label = "Fetch data"
+                , handleClick = handleFetch
+                }
+            ]
+        ]
+
+
+datePicker :
+    { mDate : Maybe Date.Date
+    , prompt : String
+    , prefix : String
+    , picker : DatePicker.DatePicker
+    , onUpdate : DatePicker.Msg -> State.Msg
+    }
+    -> Html State.Msg
+datePicker { mDate, prompt, prefix, picker, onUpdate } =
+    let
+        settings =
+            { defaultSettings
+                | placeholder = prompt
+                , inputClassList =
+                    [ ( "text-center", True )
+                    , ( "py-2", True )
+                    ]
+            }
+    in
+    div [ [ "w-1/2", "py-4", "mx-auto" ] |> Tailwind.use |> class ]
+        [ DatePicker.view mDate settings picker |> Html.map onUpdate ]
+
+
+wrapNoPrint : Html State.Msg -> Html State.Msg
+wrapNoPrint component =
+    div [ [ "no-print" ] |> Tailwind.use |> class ] [ component ]
diff --git a/users/wpcarro/assessments/tt/client/src/User.elm b/users/wpcarro/assessments/tt/client/src/User.elm
new file mode 100644
index 0000000000..87871b78db
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/User.elm
@@ -0,0 +1,245 @@
+module User exposing (render)
+
+import Common
+import Date
+import DatePicker
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Maybe.Extra as ME
+import RemoteData
+import State
+import Tailwind
+import UI
+import Utils
+
+
+createTrip : State.Model -> Html State.Msg
+createTrip model =
+    div []
+        [ UI.header 3 "Plan Upcoming Trip"
+        , UI.textField
+            { pholder = "Where are you going?"
+            , inputId = "destination"
+            , handleInput = State.UpdateTripDestination
+            , inputValue = model.tripDestination
+            }
+        , div [ [ "flex" ] |> Tailwind.use |> class ]
+            [ UI.datePicker
+                { mDate = model.tripStartDate
+                , prompt = "Set departure date"
+                , prefix = "Departure: "
+                , picker = model.startDatePicker
+                , onUpdate = State.UpdateTripStartDate
+                }
+            , UI.datePicker
+                { mDate = model.tripEndDate
+                , prompt = "Set return date"
+                , prefix = "Return: "
+                , picker = model.endDatePicker
+                , onUpdate = State.UpdateTripEndDate
+                }
+            ]
+        , UI.textField
+            { pholder = "Comments?"
+            , inputId = "comment"
+            , handleInput = State.UpdateTripComment
+            , inputValue = model.tripComment
+            }
+        , UI.baseButton
+            { enabled =
+                List.all
+                    identity
+                    [ String.length model.tripDestination > 0
+                    , String.length model.tripComment > 0
+                    , ME.isJust model.tripStartDate
+                    , ME.isJust model.tripEndDate
+                    ]
+            , extraClasses = [ "my-4" ]
+            , handleClick =
+                case ( model.tripStartDate, model.tripEndDate ) of
+                    ( Nothing, _ ) ->
+                        State.DoNothing
+
+                    ( _, Nothing ) ->
+                        State.DoNothing
+
+                    ( Just startDate, Just endDate ) ->
+                        State.AttemptCreateTrip startDate endDate
+            , label = "Schedule trip"
+            }
+        ]
+
+
+renderEditTrip : State.Model -> State.Trip -> Html State.Msg
+renderEditTrip model trip =
+    li []
+        [ div []
+            [ UI.textField
+                { handleInput = State.UpdateEditTripDestination
+                , inputId = "edit-trip-destination"
+                , inputValue = model.editTripDestination
+                , pholder = "Destination"
+                }
+            , UI.textField
+                { handleInput = State.UpdateEditTripComment
+                , inputId = "edit-trip-comment"
+                , inputValue = model.editTripComment
+                , pholder = "Comment"
+                }
+            ]
+        , div []
+            [ UI.baseButton
+                { enabled =
+                    case model.updateTripStatus of
+                        RemoteData.Loading ->
+                            False
+
+                        _ ->
+                            True
+                , extraClasses = []
+                , label =
+                    case model.updateTripStatus of
+                        RemoteData.Loading ->
+                            "Saving..."
+
+                        _ ->
+                            "Save"
+                , handleClick =
+                    State.AttemptUpdateTrip
+                        { username = trip.username
+                        , destination = trip.destination
+                        , startDate = trip.startDate
+                        }
+                        { username = trip.username
+                        , destination = model.editTripDestination
+                        , startDate = trip.startDate
+                        , endDate = trip.endDate
+                        , comment = model.editTripComment
+                        }
+                }
+            , UI.simpleButton
+                { label = "Cancel"
+                , handleClick = State.CancelEditTrip
+                }
+            ]
+        ]
+
+
+renderTrip : Date.Date -> State.Trip -> Html State.Msg
+renderTrip today trip =
+    li
+        [ [ "py-2" ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ if Date.compare today trip.startDate == GT then
+            UI.paragraph
+                (String.fromInt (Date.diff Date.Days trip.startDate today)
+                    ++ " days until you're travelling to "
+                    ++ trip.destination
+                    ++ " for "
+                    ++ String.fromInt
+                        (Date.diff
+                            Date.Days
+                            trip.startDate
+                            trip.endDate
+                        )
+                    ++ " days."
+                )
+
+          else
+            UI.paragraph
+                (String.fromInt (Date.diff Date.Days today trip.endDate)
+                    ++ " days ago you returned from your trip to "
+                    ++ trip.destination
+                )
+        , UI.paragraph ("\"" ++ trip.comment ++ "\"")
+        , UI.wrapNoPrint
+            (UI.textButton
+                { label = "Edit"
+                , handleClick = State.EditTrip trip
+                }
+            )
+        , UI.wrapNoPrint
+            (UI.textButton
+                { label = "Delete"
+                , handleClick = State.AttemptDeleteTrip trip
+                }
+            )
+        ]
+
+
+trips : State.Model -> Html State.Msg
+trips model =
+    div []
+        [ UI.header 3 "Your Trips"
+        , case model.trips of
+            RemoteData.NotAsked ->
+                UI.paragraph "Somehow we've reached the user home page without requesting your trips data. Please report this to our engineering team at bugs@tripplaner.tld"
+
+            RemoteData.Loading ->
+                UI.paragraph "Loading your trips..."
+
+            RemoteData.Failure e ->
+                UI.paragraph ("Error: " ++ Utils.explainHttpError e)
+
+            RemoteData.Success xs ->
+                case model.todaysDate of
+                    Nothing ->
+                        text ""
+
+                    Just today ->
+                        div [ [ "mb-10" ] |> Tailwind.use |> class ]
+                            [ ul [ [ "my-4" ] |> Tailwind.use |> class ]
+                                (xs
+                                    |> List.sortWith (\x y -> Date.compare y.startDate x.startDate)
+                                    |> List.map
+                                        (\trip ->
+                                            case model.editingTrip of
+                                                Nothing ->
+                                                    renderTrip today trip
+
+                                                Just x ->
+                                                    if x == trip then
+                                                        renderEditTrip model trip
+
+                                                    else
+                                                        renderTrip today trip
+                                        )
+                                )
+                            , UI.wrapNoPrint
+                                (UI.simpleButton
+                                    { label = "Print iternary"
+                                    , handleClick = State.PrintPage
+                                    }
+                                )
+                            ]
+        ]
+
+
+render : State.Model -> Html State.Msg
+render model =
+    Common.withSession model
+        (\session ->
+            div
+                [ class
+                    ([ "container"
+                     , "mx-auto"
+                     , "text-center"
+                     ]
+                        |> Tailwind.use
+                    )
+                ]
+                [ UI.wrapNoPrint (UI.header 2 ("Welcome, " ++ session.username ++ "!"))
+                , UI.wrapNoPrint (createTrip model)
+                , trips model
+                , UI.wrapNoPrint
+                    (UI.textButton
+                        { label = "Logout"
+                        , handleClick = State.AttemptLogout
+                        }
+                    )
+                , Common.allErrors model
+                ]
+        )
diff --git a/users/wpcarro/assessments/tt/client/src/Utils.elm b/users/wpcarro/assessments/tt/client/src/Utils.elm
new file mode 100644
index 0000000000..60343cd870
--- /dev/null
+++ b/users/wpcarro/assessments/tt/client/src/Utils.elm
@@ -0,0 +1,109 @@
+module Utils exposing (..)
+
+import DateFormat
+import Http
+import Time
+import Shared
+
+
+explainHttpError : Http.Error -> String
+explainHttpError e =
+    case e of
+        Http.BadUrl _ ->
+            "Bad URL: you may have supplied an improperly formatted URL"
+
+        Http.Timeout ->
+            "Timeout: the resource you requested did not arrive within the interval of time that you claimed it should"
+
+        Http.BadStatus s ->
+            "Bad Status: the server returned a bad status code: " ++ String.fromInt s
+
+        Http.BadBody b ->
+            "Bad Body: our application had trouble decoding the body of the response from the server: " ++ b
+
+        Http.NetworkError ->
+            "Network Error: something went awry in the network stack. I recommend checking the server logs if you can."
+
+
+getWithCredentials :
+    { url : String
+    , expect : Http.Expect msg
+    }
+    -> Cmd msg
+getWithCredentials { url, expect } =
+    Http.riskyRequest
+        { url = url
+        , headers = [ Http.header "Origin" Shared.clientOrigin ]
+        , method = "GET"
+        , timeout = Nothing
+        , tracker = Nothing
+        , body = Http.emptyBody
+        , expect = expect
+        }
+
+
+postWithCredentials :
+    { url : String
+    , body : Http.Body
+    , expect : Http.Expect msg
+    }
+    -> Cmd msg
+postWithCredentials { url, body, expect } =
+    Http.riskyRequest
+        { url = url
+        , headers = [ Http.header "Origin" Shared.clientOrigin ]
+        , method = "POST"
+        , timeout = Nothing
+        , tracker = Nothing
+        , body = body
+        , expect = expect
+        }
+
+
+deleteWithCredentials :
+    { url : String
+    , body : Http.Body
+    , expect : Http.Expect msg
+    }
+    -> Cmd msg
+deleteWithCredentials { url, body, expect } =
+    Http.riskyRequest
+        { url = url
+        , headers = [ Http.header "Origin" Shared.clientOrigin ]
+        , method = "DELETE"
+        , timeout = Nothing
+        , tracker = Nothing
+        , body = body
+        , expect = expect
+        }
+
+putWithCredentials :
+    { url : String
+    , body : Http.Body
+    , expect : Http.Expect msg
+    }
+    -> Cmd msg
+putWithCredentials { url, body, expect } =
+    Http.riskyRequest
+        { url = url
+        , headers = [ Http.header "Origin" Shared.clientOrigin ]
+        , method = "PUT"
+        , timeout = Nothing
+        , tracker = Nothing
+        , body = body
+        , expect = expect
+        }
+
+
+
+formatTime : Time.Posix -> String
+formatTime ts =
+    DateFormat.format
+        [ DateFormat.monthNameFull
+        , DateFormat.text " "
+        , DateFormat.dayOfMonthSuffix
+        , DateFormat.text ", "
+        , DateFormat.yearNumber
+        ]
+        Time.utc
+        ts
diff --git a/users/wpcarro/assessments/tt/data/accounts.csv b/users/wpcarro/assessments/tt/data/accounts.csv
new file mode 100644
index 0000000000..f5fc77b6d7
--- /dev/null
+++ b/users/wpcarro/assessments/tt/data/accounts.csv
@@ -0,0 +1,2 @@
+mimi,$2b$12$LynoGCNbe2RA1WWSiBEMVudJKs5dxnssY16rYmUyiwlSBIhHBOLbu,miriamwright@google.com,user,
+wpcarro,$2b$12$3wbi4xfQmksLsu6GOKTbj.5WHywESATnXB4R8FJ55RSRLy6X9xA7u,wpcarro@google.com,admin,
\ No newline at end of file
diff --git a/users/wpcarro/assessments/tt/data/trips.csv b/users/wpcarro/assessments/tt/data/trips.csv
new file mode 100644
index 0000000000..a583c750f7
--- /dev/null
+++ b/users/wpcarro/assessments/tt/data/trips.csv
@@ -0,0 +1,3 @@
+mimi,Rome,2020-08-10,2020-08-12,Heading home before the upcoming trip with Panarea.
+mimi,Panarea,2020-08-15,2020-08-28,Exciting upcoming trip with Matt and Sarah!
+mimi,London,2020-08-30,2020-09-15,Heading back to London...
\ No newline at end of file
diff --git a/users/wpcarro/assessments/tt/populate.sqlite3 b/users/wpcarro/assessments/tt/populate.sqlite3
new file mode 100644
index 0000000000..e200d2b49c
--- /dev/null
+++ b/users/wpcarro/assessments/tt/populate.sqlite3
@@ -0,0 +1,7 @@
+PRAGMA foreign_keys = on;
+.read src/init.sql
+.mode csv
+.import data/accounts.csv Accounts
+.import data/trips.csv Trips
+.mode column
+.headers on
\ No newline at end of file
diff --git a/users/wpcarro/assessments/tt/shell.nix b/users/wpcarro/assessments/tt/shell.nix
new file mode 100644
index 0000000000..bf8486ba1d
--- /dev/null
+++ b/users/wpcarro/assessments/tt/shell.nix
@@ -0,0 +1,18 @@
+{ pkgs, depot, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    (haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
+      hpkgs.aeson
+      hpkgs.cryptonite
+      hpkgs.envy
+      hpkgs.hailgun
+      hpkgs.resource-pool
+      hpkgs.servant-server
+      hpkgs.sqlite-simple
+      hpkgs.uuid
+      hpkgs.wai-cors
+      hpkgs.warp
+    ]))
+  ];
+}
diff --git a/users/wpcarro/assessments/tt/src/.ghci b/users/wpcarro/assessments/tt/src/.ghci
new file mode 100644
index 0000000000..efc88e630c
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/.ghci
@@ -0,0 +1,2 @@
+:set prompt "> "
+:set -Wall
diff --git a/users/wpcarro/assessments/tt/src/API.hs b/users/wpcarro/assessments/tt/src/API.hs
new file mode 100644
index 0000000000..471fa761e0
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/API.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+--------------------------------------------------------------------------------
+module API where
+--------------------------------------------------------------------------------
+import Data.Text
+import Servant.API
+import Web.Cookie
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+-- | Once authenticated, users receive a SessionCookie.
+type SessionCookie = Header' '[Required] "Cookie" T.SessionCookie
+
+type API =
+      -- accounts: Create
+           "accounts"
+           :> Header "Cookie" T.SessionCookie
+           :> ReqBody '[JSON] T.CreateAccountRequest
+           :> Post '[JSON] NoContent
+      :<|> "verify"
+           :> ReqBody '[JSON] T.VerifyAccountRequest
+           :> Post '[JSON] NoContent
+      -- accounts: Read
+      -- accounts: Update
+      -- accounts: Delete
+      :<|> "accounts"
+           :> SessionCookie
+           :> QueryParam' '[Required] "username" Text
+           :> Delete '[JSON] NoContent
+      -- accounts: List
+      :<|> "accounts"
+           :> SessionCookie
+           :> Get '[JSON] [T.User]
+
+      -- trips: Create
+      :<|> "trips"
+           :> SessionCookie
+           :> ReqBody '[JSON] T.Trip
+           :> Post '[JSON] NoContent
+      -- trips: Read
+      -- trips: Update
+      :<|> "trips"
+           :> SessionCookie
+           :> ReqBody '[JSON] T.UpdateTripRequest
+           :> Put '[JSON] NoContent
+      -- trips: Delete
+      :<|> "trips"
+           :> SessionCookie
+           :> ReqBody '[JSON] T.TripPK
+           :> Delete '[JSON] NoContent
+      -- trips: List
+      :<|> "trips"
+           :> SessionCookie
+           :> Get '[JSON] [T.Trip]
+
+      -- Miscellaneous
+      :<|> "login"
+           :> ReqBody '[JSON] T.AccountCredentials
+           :> Post '[JSON] (Headers '[Header "Set-Cookie" SetCookie] T.Session)
+      :<|> "logout"
+           :> SessionCookie
+           :> Get '[JSON] (Headers '[Header "Set-Cookie" SetCookie] NoContent)
+      :<|> "unfreeze"
+           :> SessionCookie
+           :> ReqBody '[JSON] T.UnfreezeAccountRequest
+           :> Post '[JSON] NoContent
+      :<|> "invite"
+           :> SessionCookie
+           :> ReqBody '[JSON] T.InviteUserRequest
+           :> Post '[JSON] NoContent
+      :<|> "accept-invitation"
+           :> ReqBody '[JSON] T.AcceptInvitationRequest
+           :> Post '[JSON] NoContent
diff --git a/users/wpcarro/assessments/tt/src/Accounts.hs b/users/wpcarro/assessments/tt/src/Accounts.hs
new file mode 100644
index 0000000000..c7ab7a2f13
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Accounts.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedStrings #-}
+--------------------------------------------------------------------------------
+module Accounts where
+--------------------------------------------------------------------------------
+import Database.SQLite.Simple
+
+import qualified PendingAccounts
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+-- | Delete the account in PendingAccounts and create on in Accounts.
+transferFromPending :: FilePath -> T.PendingAccount -> IO ()
+transferFromPending dbFile T.PendingAccount{..} = withConnection dbFile $
+  \conn -> withTransaction conn $ do
+    PendingAccounts.delete dbFile pendingAccountUsername
+    execute conn "INSERT INTO Accounts (username,password,email,role) VALUES (?,?,?,?)"
+      ( pendingAccountUsername
+      , pendingAccountPassword
+      , pendingAccountEmail
+      , pendingAccountRole
+      )
+
+-- | Create a new account in the Accounts table.
+create :: FilePath -> T.Username -> T.ClearTextPassword -> T.Email -> T.Role -> IO ()
+create dbFile username password email role = withConnection dbFile $ \conn -> do
+  hashed <- T.hashPassword password
+  execute conn "INSERT INTO Accounts (username,password,email,role) VALUES (?,?,?,?)"
+    (username, hashed, email, role)
+
+-- | Delete `username` from `dbFile`.
+delete :: FilePath -> T.Username -> IO ()
+delete dbFile username = withConnection dbFile $ \conn -> do
+  execute conn "DELETE FROM Accounts WHERE username = ?"
+    (Only username)
+
+-- | Attempt to find `username` in the Account table of `dbFile`.
+lookup :: FilePath -> T.Username -> IO (Maybe T.Account)
+lookup dbFile username = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT username,password,email,role,profilePicture FROM Accounts WHERE username = ?" (Only username)
+  case res of
+    [x] -> pure (Just x)
+    _ -> pure Nothing
+
+-- | Return a list of accounts with the sensitive data removed.
+list :: FilePath -> IO [T.User]
+list dbFile = withConnection dbFile $ \conn -> do
+  accounts <- query_ conn "SELECT username,password,email,role,profilePicture FROM Accounts"
+  pure $ T.userFromAccount <$> accounts
diff --git a/users/wpcarro/assessments/tt/src/App.hs b/users/wpcarro/assessments/tt/src/App.hs
new file mode 100644
index 0000000000..742bc962dc
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/App.hs
@@ -0,0 +1,270 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeApplications #-}
+--------------------------------------------------------------------------------
+module App where
+--------------------------------------------------------------------------------
+import Control.Monad.IO.Class (liftIO)
+import Data.String.Conversions (cs)
+import Data.Text (Text)
+import Servant
+import API
+import Utils
+import Web.Cookie
+
+import qualified Network.Wai.Handler.Warp as Warp
+import qualified Network.Wai.Middleware.Cors as Cors
+import qualified System.Random as Random
+import qualified Email as Email
+import qualified Data.UUID as UUID
+import qualified Types as T
+import qualified Accounts as Accounts
+import qualified Auth as Auth
+import qualified Trips as Trips
+import qualified Sessions as Sessions
+import qualified Invitations as Invitations
+import qualified LoginAttempts as LoginAttempts
+import qualified PendingAccounts as PendingAccounts
+--------------------------------------------------------------------------------
+
+err429 :: ServerError
+err429 = ServerError
+  { errHTTPCode = 429
+  , errReasonPhrase = "Too many requests"
+  , errBody = ""
+  , errHeaders = []
+  }
+
+-- | Send an email to recipient, `to`, with a secret code.
+sendVerifyEmail :: T.Config
+                -> T.Username
+                -> T.Email
+                -> T.RegistrationSecret
+                -> IO (Either Email.SendError Email.SendSuccess)
+sendVerifyEmail T.Config{..} (T.Username username) email (T.RegistrationSecret secretUUID) = do
+  Email.send mailgunAPIKey subject (cs body) email
+  where
+    subject = "Please confirm your account"
+    body =
+      let secret = secretUUID |> UUID.toString in
+        "To verify your account: POST /verify username=" ++ cs username ++ " secret=" ++ secret
+
+-- | Send an invitation email to recipient, `to`, with a secret code.
+sendInviteEmail :: T.Config
+                -> T.Email
+                -> T.InvitationSecret
+                -> IO (Either Email.SendError Email.SendSuccess)
+sendInviteEmail T.Config{..} email@(T.Email to) (T.InvitationSecret secretUUID) = do
+  Email.send mailgunAPIKey subject (cs body) email
+  where
+    subject = "You've been invited!"
+    body =
+      let secret = secretUUID |> UUID.toString in
+        "To accept the invitation: POST /accept-invitation username=<username> password=<password> email=" ++ cs to ++ " secret=" ++ secret
+
+server :: T.Config -> Server API
+server config@T.Config{..} = createAccount
+                        :<|> verifyAccount
+                        :<|> deleteAccount
+                        :<|> listAccounts
+                        :<|> createTrip
+                        :<|> updateTrip
+                        :<|> deleteTrip
+                        :<|> listTrips
+                        :<|> login
+                        :<|> logout
+                        :<|> unfreezeAccount
+                        :<|> inviteUser
+                        :<|> acceptInvitation
+  where
+    -- Admit Admins + whatever the predicate `p` passes.
+    adminsAnd cookie p = Auth.assert dbFile cookie (\acct@T.Account{..} -> accountRole == T.Admin || p acct)
+    -- Admit Admins only.
+    adminsOnly cookie = adminsAnd cookie (const True)
+
+    -- TODO(wpcarro): Handle failed CONSTRAINTs instead of sending 500s
+    createAccount :: Maybe T.SessionCookie
+                  -> T.CreateAccountRequest
+                  -> Handler NoContent
+    createAccount mCookie T.CreateAccountRequest{..} =
+      case (mCookie, createAccountRequestRole) of
+        (_, T.RegularUser) ->
+          doCreateAccount
+        (Nothing, T.Manager) ->
+          throwError err401 { errBody = "Only admins can create Manager accounts" }
+        (Nothing, T.Admin) ->
+          throwError err401 { errBody = "Only admins can create Admin accounts" }
+        (Just cookie, _) ->
+          adminsAnd cookie (\T.Account{..} -> accountRole == T.Manager) doCreateAccount
+      where
+        doCreateAccount :: Handler NoContent
+        doCreateAccount = do
+          secretUUID <- liftIO $ T.RegistrationSecret <$> Random.randomIO
+          liftIO $ PendingAccounts.create dbFile
+            secretUUID
+            createAccountRequestUsername
+            createAccountRequestPassword
+            createAccountRequestRole
+            createAccountRequestEmail
+          res <- liftIO $ sendVerifyEmail config
+            createAccountRequestUsername
+            createAccountRequestEmail
+            secretUUID
+          case res of
+            Left _ -> undefined
+            Right _ -> pure NoContent
+
+    verifyAccount :: T.VerifyAccountRequest -> Handler NoContent
+    verifyAccount T.VerifyAccountRequest{..} = do
+      mPendingAccount <- liftIO $ PendingAccounts.get dbFile verifyAccountRequestUsername
+      case mPendingAccount of
+        Nothing ->
+          throwError err401 { errBody = "Either your secret or your username (or both) is invalid" }
+        Just pendingAccount@T.PendingAccount{..} ->
+          if pendingAccountSecret == verifyAccountRequestSecret then do
+            liftIO $ Accounts.transferFromPending dbFile pendingAccount
+            pure NoContent
+          else
+            throwError err401 { errBody = "The secret you provided is invalid" }
+
+    deleteAccount :: T.SessionCookie -> Text -> Handler NoContent
+    deleteAccount cookie username = adminsOnly cookie $ do
+      liftIO $ Accounts.delete dbFile (T.Username username)
+      pure NoContent
+
+    listAccounts :: T.SessionCookie -> Handler [T.User]
+    listAccounts cookie = adminsOnly cookie $ do
+      liftIO $ Accounts.list dbFile
+
+    createTrip :: T.SessionCookie -> T.Trip -> Handler NoContent
+    createTrip cookie trip@T.Trip{..} =
+      adminsAnd cookie (\T.Account{..} -> accountUsername == tripUsername) $ do
+        liftIO $ Trips.create dbFile trip
+        pure NoContent
+
+    updateTrip :: T.SessionCookie -> T.UpdateTripRequest -> Handler NoContent
+    updateTrip cookie updates@T.UpdateTripRequest{..} =
+      adminsAnd cookie (\T.Account{..} -> accountUsername == T.tripPKUsername updateTripRequestTripPK) $ do
+        mTrip <- liftIO $ Trips.get dbFile updateTripRequestTripPK
+        case mTrip of
+          Nothing -> throwError err400 { errBody = "tripKey is invalid" }
+          Just trip@T.Trip{..} -> do
+            -- TODO(wpcarro): Prefer function in Trips module that does this in a
+            -- DB transaction.
+            liftIO $ Trips.delete dbFile updateTripRequestTripPK
+            liftIO $ Trips.create dbFile (T.updateTrip updates trip)
+            pure NoContent
+
+    deleteTrip :: T.SessionCookie -> T.TripPK -> Handler NoContent
+    deleteTrip cookie tripPK@T.TripPK{..} =
+      adminsAnd cookie (\T.Account{..} -> accountUsername == tripPKUsername) $ do
+      liftIO $ Trips.delete dbFile tripPK
+      pure NoContent
+
+    listTrips :: T.SessionCookie -> Handler [T.Trip]
+    listTrips cookie = do
+      mAccount <- liftIO $ Auth.accountFromCookie dbFile cookie
+      case mAccount of
+        Nothing -> throwError err401 { errBody = "Your session cookie is invalid. Try logging out and logging back in." }
+        Just T.Account{..} ->
+          case accountRole of
+            T.Admin -> liftIO $ Trips.listAll dbFile
+            _ -> liftIO $ Trips.list dbFile accountUsername
+
+    login :: T.AccountCredentials
+          -> Handler (Headers '[Header "Set-Cookie" SetCookie] T.Session)
+    login (T.AccountCredentials username password) = do
+      mAccount <- liftIO $ Accounts.lookup dbFile username
+      case mAccount of
+        Just account@T.Account{..} -> do
+          mAttempts <- liftIO $ LoginAttempts.forUsername dbFile accountUsername
+          case mAttempts of
+            Nothing ->
+              if T.passwordsMatch password accountPassword then do
+                uuid <- liftIO $ Sessions.findOrCreate dbFile account
+                pure $ addHeader (Auth.mkCookie uuid)
+                  T.Session{ sessionUsername = accountUsername
+                           , sessionRole = accountRole
+                           }
+              else do
+                liftIO $ LoginAttempts.increment dbFile username
+                throwError err401 { errBody = "Your credentials are invalid" }
+            Just attempts ->
+              if attempts >= 3 then
+                throwError err429
+              else if T.passwordsMatch password accountPassword then do
+                uuid <- liftIO $ Sessions.findOrCreate dbFile account
+                pure $ addHeader (Auth.mkCookie uuid)
+                  T.Session{ sessionUsername = accountUsername
+                           , sessionRole = accountRole
+                           }
+              else do
+                liftIO $ LoginAttempts.increment dbFile username
+                throwError err401 { errBody = "Your credentials are invalid" }
+
+        -- In this branch, the user didn't supply a known username.
+        Nothing -> throwError err401 { errBody = "Your credentials are invalid" }
+
+    logout :: T.SessionCookie
+           -> Handler (Headers '[Header "Set-Cookie" SetCookie] NoContent)
+    logout cookie = do
+      case Auth.uuidFromCookie cookie of
+        Nothing ->
+          pure $ addHeader Auth.emptyCookie NoContent
+        Just uuid -> do
+          liftIO $ Sessions.delete dbFile uuid
+          pure $ addHeader Auth.emptyCookie NoContent
+
+    unfreezeAccount :: T.SessionCookie
+                    -> T.UnfreezeAccountRequest
+                    -> Handler NoContent
+    unfreezeAccount cookie T.UnfreezeAccountRequest{..} =
+      adminsAnd cookie (\T.Account{..} -> accountRole == T.Manager) $ do
+        liftIO $ LoginAttempts.reset dbFile unfreezeAccountRequestUsername
+        pure NoContent
+
+    inviteUser :: T.SessionCookie
+               -> T.InviteUserRequest
+               -> Handler NoContent
+    inviteUser cookie T.InviteUserRequest{..} = adminsOnly cookie $ do
+      secretUUID <- liftIO $ T.InvitationSecret <$> Random.randomIO
+      liftIO $ Invitations.create dbFile
+        secretUUID
+        inviteUserRequestEmail
+        inviteUserRequestRole
+      res <- liftIO $ sendInviteEmail config inviteUserRequestEmail secretUUID
+      case res of
+        Left _ -> undefined
+        Right _ -> pure NoContent
+
+    acceptInvitation :: T.AcceptInvitationRequest -> Handler NoContent
+    acceptInvitation T.AcceptInvitationRequest{..} = do
+      mInvitation <- liftIO $ Invitations.get dbFile acceptInvitationRequestEmail
+      case mInvitation of
+        Nothing -> throwError err404 { errBody = "No invitation for email" }
+        Just T.Invitation{..} ->
+          if invitationSecret == acceptInvitationRequestSecret then do
+            liftIO $ Accounts.create dbFile
+              acceptInvitationRequestUsername
+              acceptInvitationRequestPassword
+              invitationEmail
+              invitationRole
+            pure NoContent
+          else
+            throwError err401 { errBody = "You are not providing a valid secret" }
+
+run :: T.Config -> IO ()
+run config@T.Config{..} =
+  Warp.run 3000 (enforceCors $ serve (Proxy @ API) $ server config)
+  where
+    enforceCors = Cors.cors (const $ Just corsPolicy)
+    corsPolicy :: Cors.CorsResourcePolicy
+    corsPolicy =
+      Cors.simpleCorsResourcePolicy
+        { Cors.corsOrigins = Just ([cs configClient], True)
+        , Cors.corsMethods = Cors.simpleMethods ++ ["PUT", "PATCH", "DELETE", "OPTIONS"]
+        , Cors.corsRequestHeaders = Cors.simpleHeaders ++ ["Content-Type", "Authorization"]
+        }
diff --git a/users/wpcarro/assessments/tt/src/Auth.hs b/users/wpcarro/assessments/tt/src/Auth.hs
new file mode 100644
index 0000000000..f1bff23257
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Auth.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Auth where
+--------------------------------------------------------------------------------
+import Control.Monad.IO.Class (liftIO)
+import Web.Cookie
+import Servant
+
+import qualified Data.UUID as UUID
+import qualified Sessions as Sessions
+import qualified Accounts as Accounts
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+-- | Return the UUID from a Session cookie.
+uuidFromCookie :: T.SessionCookie -> Maybe T.SessionUUID
+uuidFromCookie (T.SessionCookie cookies) = do
+  auth <- lookup "auth" cookies
+  uuid <- UUID.fromASCIIBytes auth
+  pure $ T.SessionUUID uuid
+
+-- | Attempt to return the account associated with `cookie`.
+accountFromCookie :: FilePath -> T.SessionCookie -> IO (Maybe T.Account)
+accountFromCookie dbFile cookie =
+  case uuidFromCookie cookie of
+    Nothing -> pure Nothing
+    Just uuid -> do
+      mSession <- Sessions.get dbFile uuid
+      case mSession of
+        Nothing -> pure Nothing
+        Just T.StoredSession{..} -> do
+          mAccount <- Accounts.lookup dbFile storedSessionUsername
+          case mAccount of
+            Nothing -> pure Nothing
+            Just x -> pure (Just x)
+
+-- | Create a new session cookie.
+mkCookie :: T.SessionUUID -> SetCookie
+mkCookie (T.SessionUUID uuid) =
+  defaultSetCookie
+    { setCookieName = "auth"
+    , setCookieValue = UUID.toASCIIBytes uuid
+    }
+
+-- | Use this to clear out the session cookie.
+emptyCookie :: SetCookie
+emptyCookie =
+  defaultSetCookie
+    { setCookieName = "auth"
+    , setCookieValue = ""
+    }
+
+-- | Throw a 401 error if the `predicate` fails.
+assert :: FilePath -> T.SessionCookie -> (T.Account -> Bool) -> Handler a -> Handler a
+assert dbFile cookie predicate handler = do
+  mRole <- liftIO $ accountFromCookie dbFile cookie
+  case mRole of
+    Nothing -> throwError err401 { errBody = "Missing valid session cookie" }
+    Just account ->
+      if predicate account then
+        handler
+      else
+        throwError err401 { errBody = "You are not authorized to access this resource" }
diff --git a/users/wpcarro/assessments/tt/src/Email.hs b/users/wpcarro/assessments/tt/src/Email.hs
new file mode 100644
index 0000000000..2dac0973ba
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Email.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedStrings #-}
+--------------------------------------------------------------------------------
+module Email where
+--------------------------------------------------------------------------------
+import Data.Text
+import Data.String.Conversions (cs)
+import Utils
+
+import qualified Mail.Hailgun as MG
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+newtype SendSuccess = SendSuccess MG.HailgunSendResponse
+
+data SendError
+  = MessageError MG.HailgunErrorMessage
+  | ResponseError MG.HailgunErrorResponse
+
+-- | Attempt to send an email with `subject` and with message, `body`.
+send :: Text
+     -> Text
+     -> Text
+     -> T.Email
+     -> IO (Either SendError SendSuccess)
+send apiKey subject body (T.Email to) = do
+  case mkMsg of
+    Left e -> pure $ Left (MessageError e)
+    Right x -> do
+      res <- MG.sendEmail ctx x
+      case res of
+        Left e -> pure $ Left (ResponseError e)
+        Right y -> pure $ Right (SendSuccess y)
+  where
+    ctx = MG.HailgunContext { MG.hailgunDomain = "sandboxda5038873f924b50af2f82a0f05cffdf.mailgun.org"
+                            , MG.hailgunApiKey = cs apiKey
+                            , MG.hailgunProxy = Nothing
+                            }
+    mkMsg = MG.hailgunMessage
+            subject
+            (body |> cs |> MG.TextOnly)
+            "mailgun@sandboxda5038873f924b50af2f82a0f05cffdf.mailgun.org"
+            (MG.MessageRecipients { MG.recipientsTo = [cs to]
+                                  , MG.recipientsCC = []
+                                  , MG.recipientsBCC = []
+                                  })
+            []
diff --git a/users/wpcarro/assessments/tt/src/Invitations.hs b/users/wpcarro/assessments/tt/src/Invitations.hs
new file mode 100644
index 0000000000..0c700470f3
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Invitations.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module Invitations where
+--------------------------------------------------------------------------------
+import Database.SQLite.Simple
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+create :: FilePath -> T.InvitationSecret -> T.Email -> T.Role -> IO ()
+create dbFile secret email role = withConnection dbFile $ \conn -> do
+  execute conn "INSERT INTO Invitations (email,role,secret) VALUES (?,?,?)"
+    (email, role, secret)
+
+get :: FilePath -> T.Email -> IO (Maybe T.Invitation)
+get dbFile email = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT email,role,secret FROM Invitations WHERE email = ?" (Only email)
+  case res of
+    [x] -> pure (Just x)
+    _ -> pure Nothing
diff --git a/users/wpcarro/assessments/tt/src/LoginAttempts.hs b/users/wpcarro/assessments/tt/src/LoginAttempts.hs
new file mode 100644
index 0000000000..d78e12e3fd
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/LoginAttempts.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module LoginAttempts where
+--------------------------------------------------------------------------------
+import Database.SQLite.Simple
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+reset :: FilePath -> T.Username -> IO ()
+reset dbFile username = withConnection dbFile $ \conn ->
+  execute conn "UPDATE LoginAttempts SET numAttempts = 0 WHERE username = ?"
+    (Only username)
+
+-- | Attempt to return the number of failed login attempts for
+-- `username`. Returns a Maybe in case `username` doesn't exist.
+forUsername :: FilePath -> T.Username -> IO (Maybe Integer)
+forUsername dbFile username = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT username,numAttempts FROM LoginAttempts WHERE username = ?"
+    (Only username)
+  case res of
+    [T.LoginAttempt{..}] -> pure (Just loginAttemptNumAttempts)
+    _  -> pure Nothing
+
+-- | INSERT a failed login attempt for `username` or UPDATE an existing entry.
+increment :: FilePath -> T.Username -> IO ()
+increment dbFile username = withConnection dbFile $ \conn ->
+  execute conn "INSERT INTO LoginAttempts (username,numAttempts) VALUES (?,?) ON CONFLICT (username) DO UPDATE SET numAttempts = numAttempts + 1"
+    (username, 1 :: Integer)
diff --git a/users/wpcarro/assessments/tt/src/Main.hs b/users/wpcarro/assessments/tt/src/Main.hs
new file mode 100644
index 0000000000..9df4232066
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Main.hs
@@ -0,0 +1,13 @@
+--------------------------------------------------------------------------------
+module Main where
+--------------------------------------------------------------------------------
+import qualified App
+import qualified System.Envy as Envy
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = do
+  mEnv <- Envy.decodeEnv
+  case mEnv of
+    Left err -> putStrLn err
+    Right env -> App.run env
diff --git a/users/wpcarro/assessments/tt/src/PendingAccounts.hs b/users/wpcarro/assessments/tt/src/PendingAccounts.hs
new file mode 100644
index 0000000000..a555185fa7
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/PendingAccounts.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+--------------------------------------------------------------------------------
+module PendingAccounts where
+--------------------------------------------------------------------------------
+import Database.SQLite.Simple
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+create :: FilePath
+       -> T.RegistrationSecret
+       -> T.Username
+       -> T.ClearTextPassword
+       -> T.Role
+       -> T.Email
+       -> IO ()
+create dbFile secret username password role email = withConnection dbFile $ \conn -> do
+  hashed <- T.hashPassword password
+  execute conn "INSERT INTO PendingAccounts (secret,username,password,role,email) VALUES (?,?,?,?,?)"
+    (secret, username, hashed, role, email)
+
+get :: FilePath -> T.Username -> IO (Maybe T.PendingAccount)
+get dbFile username = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT secret,username,password,role,email FROM PendingAccounts WHERE username = ?" (Only username)
+  case res of
+    [x] -> pure (Just x)
+    _ -> pure Nothing
+
+delete :: FilePath -> T.Username -> IO ()
+delete dbFile username = withConnection dbFile $ \conn ->
+  execute conn "DELETE FROM PendingAccounts WHERE username = ?" (Only username)
diff --git a/users/wpcarro/assessments/tt/src/Sessions.hs b/users/wpcarro/assessments/tt/src/Sessions.hs
new file mode 100644
index 0000000000..713059a383
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Sessions.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+--------------------------------------------------------------------------------
+module Sessions where
+--------------------------------------------------------------------------------
+import Database.SQLite.Simple
+
+import qualified Data.Time.Clock as Clock
+import qualified Types as T
+import qualified System.Random as Random
+--------------------------------------------------------------------------------
+
+-- | Return True if `session` was created at most three hours ago.
+isValid :: T.StoredSession -> IO Bool
+isValid session = do
+  t1 <- Clock.getCurrentTime
+  let t0 = T.storedSessionTsCreated session in
+    pure $ Clock.diffUTCTime t1 t0 <= 3 * 60 * 60
+
+-- | Lookup the session by UUID.
+get :: FilePath -> T.SessionUUID -> IO (Maybe T.StoredSession)
+get dbFile uuid = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT uuid,username,tsCreated FROM Sessions WHERE uuid = ?" (Only uuid)
+  case res of
+    [x] -> pure (Just x)
+    _ -> pure Nothing
+
+-- | Lookup the session stored under `username` in `dbFile`.
+find :: FilePath -> T.Username -> IO (Maybe T.StoredSession)
+find dbFile username = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT uuid,username,tsCreated FROM Sessions WHERE username = ?" (Only username)
+  case res of
+    [x] -> pure (Just x)
+    _ -> pure Nothing
+
+-- | Create a session under the `username` key in `dbFile`.
+create :: FilePath -> T.Username -> IO T.SessionUUID
+create dbFile username = withConnection dbFile $ \conn -> do
+  now <- Clock.getCurrentTime
+  uuid <- Random.randomIO
+  execute conn "INSERT INTO Sessions (uuid,username,tsCreated) VALUES (?,?,?)"
+    (T.SessionUUID uuid, username, now)
+  pure (T.SessionUUID uuid)
+
+-- | Reset the tsCreated field to the current time to ensure the token is valid.
+refresh :: FilePath -> T.SessionUUID -> IO ()
+refresh dbFile uuid = withConnection dbFile $ \conn -> do
+  now <- Clock.getCurrentTime
+  execute conn "UPDATE Sessions SET tsCreated = ? WHERE uuid = ?"
+    (now, uuid)
+  pure ()
+
+-- | Delete the session under `username` from `dbFile`.
+delete :: FilePath -> T.SessionUUID -> IO ()
+delete dbFile uuid = withConnection dbFile $ \conn ->
+  execute conn "DELETE FROM Sessions WHERE uuid = ?" (Only uuid)
+
+-- | Find or create a session in the Sessions table. If a session exists,
+-- refresh the token's validity.
+findOrCreate :: FilePath -> T.Account -> IO T.SessionUUID
+findOrCreate dbFile account =
+  let username = T.accountUsername account in do
+    mSession <- find dbFile username
+    case mSession of
+      Nothing -> create dbFile username
+      Just session ->
+        let uuid = T.storedSessionUUID session in do
+          refresh dbFile uuid
+          pure uuid
+
+-- | Return a list of all sessions in the Sessions table.
+list :: FilePath -> IO [T.StoredSession]
+list dbFile = withConnection dbFile $ \conn ->
+  query_ conn "SELECT uuid,username,tsCreated FROM Sessions"
diff --git a/users/wpcarro/assessments/tt/src/Trips.hs b/users/wpcarro/assessments/tt/src/Trips.hs
new file mode 100644
index 0000000000..f90740363c
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Trips.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE OverloadedStrings #-}
+--------------------------------------------------------------------------------
+module Trips where
+--------------------------------------------------------------------------------
+import Database.SQLite.Simple
+import Utils
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+-- | Create a new `trip` in `dbFile`.
+create :: FilePath -> T.Trip -> IO ()
+create dbFile trip = withConnection dbFile $ \conn ->
+  execute conn "INSERT INTO Trips (username,destination,startDate,endDate,comment) VALUES (?,?,?,?,?)"
+    (trip |> T.tripFields)
+
+-- | Attempt to get the trip record from `dbFile` under `tripKey`.
+get :: FilePath -> T.TripPK -> IO (Maybe T.Trip)
+get dbFile tripKey = withConnection dbFile $ \conn -> do
+  res <- query conn "SELECT username,destination,startDate,endDate,comment FROM Trips WHERE username = ? AND destination = ? AND startDate = ? LIMIT 1"
+    (T.tripPKFields tripKey)
+  case res of
+    [x] -> pure (Just x)
+    _ -> pure Nothing
+
+-- | Delete a trip from `dbFile` using its `tripKey` Primary Key.
+delete :: FilePath -> T.TripPK -> IO ()
+delete dbFile tripKey =
+  withConnection dbFile $ \conn -> do
+    execute conn "DELETE FROM Trips WHERE username = ? AND destination = ? and startDate = ?"
+      (T.tripPKFields tripKey)
+
+-- | Return a list of all of the trips in `dbFile`.
+listAll :: FilePath -> IO [T.Trip]
+listAll dbFile = withConnection dbFile $ \conn ->
+  query_ conn "SELECT username,destination,startDate,endDate,comment FROM Trips ORDER BY date(startDate) ASC"
+
+-- | Return a list of all of the trips in `dbFile`.
+list :: FilePath -> T.Username -> IO [T.Trip]
+list dbFile username = withConnection dbFile $ \conn ->
+  query conn "SELECT username,destination,startDate,endDate,comment FROM Trips WHERE username = ? ORDER BY date(startDate) ASC"
+    (Only username)
diff --git a/users/wpcarro/assessments/tt/src/Types.hs b/users/wpcarro/assessments/tt/src/Types.hs
new file mode 100644
index 0000000000..6b06a39694
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Types.hs
@@ -0,0 +1,544 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE NamedFieldPuns #-}
+--------------------------------------------------------------------------------
+module Types where
+--------------------------------------------------------------------------------
+import Data.Aeson
+import Utils
+import Data.Text
+import Data.Typeable
+import Database.SQLite.Simple
+import Database.SQLite.Simple.Ok
+import Database.SQLite.Simple.FromField
+import Database.SQLite.Simple.ToField
+import GHC.Generics
+import Web.Cookie
+import Servant.API
+import System.Envy (FromEnv, fromEnv, env)
+import Crypto.Random.Types (MonadRandom)
+
+import qualified Data.Time.Calendar as Calendar
+import qualified Crypto.KDF.BCrypt as BC
+import qualified Data.Time.Clock as Clock
+import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString as BS
+import qualified Data.Text.Encoding as TE
+import qualified Data.Maybe as M
+import qualified Data.UUID as UUID
+--------------------------------------------------------------------------------
+
+-- | Top-level application configuration.
+data Config = Config
+  { mailgunAPIKey :: Text
+  , dbFile :: FilePath
+  , configClient :: Text
+  , configServer :: Text
+  } deriving (Eq, Show)
+
+instance FromEnv Config where
+  fromEnv _ = do
+    mailgunAPIKey <- env "MAILGUN_API_KEY"
+    dbFile <- env "DB_FILE"
+    configClient <- env "CLIENT"
+    configServer <- env "SERVER"
+    pure Config {..}
+
+-- TODO(wpcarro): Properly handle NULL for columns like profilePicture.
+forNewtype :: (Typeable b) => (Text -> b) -> FieldParser b
+forNewtype wrapper y =
+  case fieldData y of
+    (SQLText x) -> Ok (wrapper x)
+    x -> returnError ConversionFailed y ("We expected SQLText, but we received: " ++ show x)
+
+newtype Username = Username Text
+  deriving (Eq, Show, Generic)
+
+instance ToJSON Username
+instance FromJSON Username
+
+instance ToField Username where
+  toField (Username x) = SQLText x
+
+instance FromField Username where
+  fromField = forNewtype Username
+
+newtype HashedPassword = HashedPassword BS.ByteString
+  deriving (Eq, Show, Generic)
+
+instance ToField HashedPassword where
+  toField (HashedPassword x) = SQLText (TE.decodeUtf8 x)
+
+instance FromField HashedPassword where
+  fromField y =
+    case fieldData y of
+      (SQLText x) -> x |> TE.encodeUtf8 |> HashedPassword |> Ok
+      x -> returnError ConversionFailed y ("We expected SQLText, but we received: " ++ show x)
+
+newtype ClearTextPassword = ClearTextPassword Text
+  deriving (Eq, Show, Generic)
+
+instance ToJSON ClearTextPassword
+instance FromJSON ClearTextPassword
+
+instance ToField ClearTextPassword where
+  toField (ClearTextPassword x) = SQLText x
+
+instance FromField ClearTextPassword where
+  fromField = forNewtype ClearTextPassword
+
+newtype Email = Email Text
+  deriving (Eq, Show, Generic)
+
+instance ToJSON Email
+instance FromJSON Email
+
+instance ToField Email where
+  toField (Email x) = SQLText x
+
+instance FromField Email where
+  fromField = forNewtype Email
+
+data Role = RegularUser | Manager | Admin
+  deriving (Eq, Show, Generic)
+
+instance ToJSON Role where
+  toJSON RegularUser = "user"
+  toJSON Manager = "manager"
+  toJSON Admin = "admin"
+
+instance FromJSON Role where
+  parseJSON = withText "Role" $ \x ->
+    case x of
+      "user" -> pure RegularUser
+      "manager" -> pure Manager
+      "admin" -> pure Admin
+      _ -> fail "Expected \"user\" or \"manager\" or \"admin\""
+
+instance ToField Role where
+  toField RegularUser = SQLText "user"
+  toField Manager = SQLText "manager"
+  toField Admin = SQLText "admin"
+
+instance FromField Role where
+  fromField y =
+    case fieldData y of
+      (SQLText "user") -> Ok RegularUser
+      (SQLText "manager") -> Ok Manager
+      (SQLText "admin") -> Ok Admin
+      x -> returnError ConversionFailed y ("We expected user, manager, admin, but we received: " ++ show x)
+
+-- TODO(wpcarro): Prefer Data.ByteString instead of Text
+newtype ProfilePicture = ProfilePicture Text
+  deriving (Eq, Show, Generic)
+
+instance ToJSON ProfilePicture
+instance FromJSON ProfilePicture
+
+instance ToField ProfilePicture where
+  toField (ProfilePicture x) = SQLText x
+
+instance FromField ProfilePicture where
+  fromField = forNewtype ProfilePicture
+
+data Account = Account
+  { accountUsername :: Username
+  , accountPassword :: HashedPassword
+  , accountEmail :: Email
+  , accountRole :: Role
+  , accountProfilePicture :: Maybe ProfilePicture
+  } deriving (Eq, Show, Generic)
+
+-- | Return a tuple with all of the fields for an Account record to use for SQL.
+accountFields :: Account -> (Username, HashedPassword, Email, Role, Maybe ProfilePicture)
+accountFields (Account {..})
+  = ( accountUsername
+    , accountPassword
+    , accountEmail
+    , accountRole
+    , accountProfilePicture
+    )
+
+instance FromRow Account where
+  fromRow = do
+    accountUsername <- field
+    accountPassword <- field
+    accountEmail <- field
+    accountRole <- field
+    accountProfilePicture <- field
+    pure Account{..}
+
+data Session = Session
+  { sessionUsername :: Username
+  , sessionRole :: Role
+  } deriving (Eq, Show)
+
+instance ToJSON Session where
+  toJSON (Session username role) =
+    object [ "username" .= username
+           , "role" .= role
+           ]
+
+newtype Comment = Comment Text
+  deriving (Eq, Show, Generic)
+
+instance ToJSON Comment
+instance FromJSON Comment
+
+instance ToField Comment where
+  toField (Comment x) = SQLText x
+
+instance FromField Comment where
+  fromField = forNewtype Comment
+
+newtype Destination = Destination Text
+  deriving (Eq, Show, Generic)
+
+instance ToJSON Destination
+instance FromJSON Destination
+
+instance ToField Destination where
+  toField (Destination x) = SQLText x
+
+instance FromField Destination where
+  fromField = forNewtype Destination
+
+newtype Year = Year Integer deriving (Eq, Show)
+newtype Month = Month Integer deriving (Eq, Show)
+newtype Day = Day Integer deriving (Eq, Show)
+data Date = Date
+  { dateYear :: Year
+  , dateMonth :: Month
+  , dateDay :: Day
+  } deriving (Eq, Show)
+
+data Trip = Trip
+  { tripUsername :: Username
+  , tripDestination :: Destination
+  , tripStartDate :: Calendar.Day
+  , tripEndDate :: Calendar.Day
+  , tripComment :: Comment
+  } deriving (Eq, Show, Generic)
+
+instance FromRow Trip where
+  fromRow = do
+    tripUsername <- field
+    tripDestination <- field
+    tripStartDate <- field
+    tripEndDate <- field
+    tripComment <- field
+    pure Trip{..}
+
+-- | The fields used as the Primary Key for a Trip entry.
+data TripPK = TripPK
+  { tripPKUsername :: Username
+  , tripPKDestination :: Destination
+  , tripPKStartDate :: Calendar.Day
+  } deriving (Eq, Show, Generic)
+
+tripPKFields :: TripPK -> (Username, Destination, Calendar.Day)
+tripPKFields (TripPK{..})
+  = (tripPKUsername, tripPKDestination, tripPKStartDate)
+
+instance FromJSON TripPK where
+  parseJSON = withObject "TripPK" $ \x -> do
+    tripPKUsername    <- x .: "username"
+    tripPKDestination <- x .: "destination"
+    tripPKStartDate   <- x .: "startDate"
+    pure TripPK{..}
+
+-- | Return the tuple representation of a Trip record for SQL.
+tripFields :: Trip
+           -> (Username, Destination, Calendar.Day, Calendar.Day, Comment)
+tripFields (Trip{..})
+  = ( tripUsername
+    , tripDestination
+    , tripStartDate
+    , tripEndDate
+    , tripComment
+    )
+
+instance ToJSON Trip where
+  toJSON (Trip username destination startDate endDate comment) =
+    object [ "username" .= username
+           , "destination" .= destination
+           , "startDate" .= startDate
+           , "endDate" .= endDate
+           , "comment" .= comment
+           ]
+
+instance FromJSON Trip where
+  parseJSON = withObject "Trip" $ \x -> do
+    tripUsername    <- x .: "username"
+    tripDestination <- x .: "destination"
+    tripStartDate   <- x .: "startDate"
+    tripEndDate     <- x .: "endDate"
+    tripComment     <- x .: "comment"
+    pure Trip{..}
+
+-- | Users and Accounts both refer to the same underlying entities; however,
+-- Users model the user-facing Account details, hiding sensitive details like
+-- passwords and emails.
+data User = User
+  { userUsername :: Username
+  , userProfilePicture :: Maybe ProfilePicture
+  , userRole :: Role
+  } deriving (Eq, Show, Generic)
+
+instance ToJSON User where
+  toJSON (User username profilePicture role) =
+    object [ "username" .= username
+           , "profilePicture" .= profilePicture
+           , "role" .= role
+           ]
+
+userFromAccount :: Account -> User
+userFromAccount account =
+  User { userUsername = accountUsername account
+       , userProfilePicture = accountProfilePicture account
+       , userRole = accountRole account
+       }
+
+-- | This is the data that a user needs to supply to authenticate with the
+-- application.
+data AccountCredentials = AccountCredentials
+  { accountCredentialsUsername :: Username
+  , accountCredentialsPassword :: ClearTextPassword
+  } deriving (Eq, Show, Generic)
+
+instance FromJSON AccountCredentials where
+  parseJSON = withObject "AccountCredentials" $ \x -> do
+    accountCredentialsUsername <- x.: "username"
+    accountCredentialsPassword <- x.: "password"
+    pure AccountCredentials{..}
+
+
+-- | Hash password `x`.
+hashPassword :: (MonadRandom m) => ClearTextPassword -> m HashedPassword
+hashPassword (ClearTextPassword x) = do
+  hashed <- BC.hashPassword 12 (x |> unpack |> B.pack)
+  pure $ HashedPassword hashed
+
+-- | Return True if the cleartext password matches the hashed password.
+passwordsMatch :: ClearTextPassword -> HashedPassword -> Bool
+passwordsMatch (ClearTextPassword clear) (HashedPassword hashed) =
+  BC.validatePassword (clear |> unpack |> B.pack) hashed
+
+data CreateAccountRequest = CreateAccountRequest
+  { createAccountRequestUsername :: Username
+  , createAccountRequestPassword :: ClearTextPassword
+  , createAccountRequestEmail :: Email
+  , createAccountRequestRole :: Role
+  } deriving (Eq, Show)
+
+instance FromJSON CreateAccountRequest where
+  parseJSON = withObject "CreateAccountRequest" $ \x -> do
+    createAccountRequestUsername <- x .: "username"
+    createAccountRequestPassword <- x .: "password"
+    createAccountRequestEmail <- x .: "email"
+    createAccountRequestRole <- x .: "role"
+    pure $ CreateAccountRequest{..}
+
+createAccountRequestFields :: CreateAccountRequest
+                           -> (Username, ClearTextPassword, Email, Role)
+createAccountRequestFields CreateAccountRequest{..} =
+  ( createAccountRequestUsername
+  , createAccountRequestPassword
+  , createAccountRequestEmail
+  , createAccountRequestRole
+  )
+
+newtype SessionUUID = SessionUUID UUID.UUID
+  deriving (Eq, Show, Generic)
+
+instance FromField SessionUUID where
+  fromField y =
+    case fieldData y of
+      (SQLText x) ->
+        case UUID.fromText x of
+          Nothing -> returnError ConversionFailed y ("Could not convert to UUID: " ++ show x)
+          Just uuid -> Ok $ SessionUUID uuid
+      _ -> returnError ConversionFailed y "Expected SQLText for SessionUUID, but we received"
+
+instance ToField SessionUUID where
+  toField (SessionUUID uuid) =
+    uuid |> UUID.toText |> SQLText
+
+data StoredSession = StoredSession
+  { storedSessionUUID :: SessionUUID
+  , storedSessionUsername :: Username
+  , storedSessionTsCreated :: Clock.UTCTime
+  } deriving (Eq, Show, Generic)
+
+instance FromRow StoredSession where
+  fromRow = do
+    storedSessionUUID <- field
+    storedSessionUsername <- field
+    storedSessionTsCreated <- field
+    pure StoredSession {..}
+
+data LoginAttempt = LoginAttempt
+  { loginAttemptUsername :: Username
+  , loginAttemptNumAttempts :: Integer
+  } deriving (Eq, Show)
+
+instance FromRow LoginAttempt where
+  fromRow = do
+    loginAttemptUsername <- field
+    loginAttemptNumAttempts <- field
+    pure LoginAttempt {..}
+
+newtype SessionCookie = SessionCookie Cookies
+
+instance FromHttpApiData SessionCookie where
+  parseHeader x =
+    x |> parseCookies |> SessionCookie |> pure
+  parseQueryParam x =
+    x |> TE.encodeUtf8 |> parseCookies |> SessionCookie |> pure
+
+newtype RegistrationSecret = RegistrationSecret UUID.UUID
+  deriving (Eq, Show, Generic)
+
+instance FromHttpApiData RegistrationSecret where
+  parseQueryParam x =
+    case UUID.fromText x of
+      Nothing -> Left x
+      Just uuid -> Right (RegistrationSecret uuid)
+
+instance FromField RegistrationSecret where
+  fromField y =
+    case fieldData y of
+      (SQLText x) ->
+        case UUID.fromText x of
+          Nothing -> returnError ConversionFailed y ("Could not convert text to UUID: " ++ show x)
+          Just uuid -> Ok $ RegistrationSecret uuid
+      _ -> returnError ConversionFailed y "Field data is not SQLText, which is what we expect"
+
+instance ToField RegistrationSecret where
+  toField (RegistrationSecret secretUUID) =
+    secretUUID |> UUID.toText |> SQLText
+
+instance FromJSON RegistrationSecret
+
+data VerifyAccountRequest = VerifyAccountRequest
+  { verifyAccountRequestUsername :: Username
+  , verifyAccountRequestSecret :: RegistrationSecret
+  } deriving (Eq, Show)
+
+instance FromJSON VerifyAccountRequest where
+  parseJSON = withObject "VerifyAccountRequest" $ \x -> do
+    verifyAccountRequestUsername <- x .: "username"
+    verifyAccountRequestSecret   <- x .: "secret"
+    pure VerifyAccountRequest{..}
+
+data PendingAccount = PendingAccount
+  { pendingAccountSecret :: RegistrationSecret
+  , pendingAccountUsername :: Username
+  , pendingAccountPassword :: HashedPassword
+  , pendingAccountRole :: Role
+  , pendingAccountEmail :: Email
+  } deriving (Eq, Show)
+
+instance FromRow PendingAccount where
+  fromRow = do
+    pendingAccountSecret <- field
+    pendingAccountUsername <- field
+    pendingAccountPassword <- field
+    pendingAccountRole <- field
+    pendingAccountEmail <- field
+    pure PendingAccount {..}
+
+data UpdateTripRequest = UpdateTripRequest
+  { updateTripRequestTripPK :: TripPK
+  , updateTripRequestDestination :: Maybe Destination
+  , updateTripRequestStartDate :: Maybe Calendar.Day
+  , updateTripRequestEndDate :: Maybe Calendar.Day
+  , updateTripRequestComment :: Maybe Comment
+  } deriving (Eq, Show)
+
+instance FromJSON UpdateTripRequest where
+  parseJSON = withObject "UpdateTripRequest" $ \x -> do
+    updateTripRequestTripPK <- x .: "tripKey"
+    -- the following four fields might not be present
+    updateTripRequestDestination <- x .:? "destination"
+    updateTripRequestStartDate   <- x .:? "startDate"
+    updateTripRequestEndDate     <- x .:? "endDate"
+    updateTripRequestComment     <- x .:? "comment"
+    pure UpdateTripRequest{..}
+
+-- | Apply the updates in the UpdateTripRequest to Trip.
+updateTrip :: UpdateTripRequest -> Trip -> Trip
+updateTrip UpdateTripRequest{..} Trip{..} = Trip
+  { tripUsername    = tripUsername
+  , tripDestination = M.fromMaybe tripDestination updateTripRequestDestination
+  , tripStartDate   = M.fromMaybe tripStartDate updateTripRequestStartDate
+  , tripEndDate     = M.fromMaybe tripEndDate updateTripRequestEndDate
+  , tripComment     = M.fromMaybe tripComment updateTripRequestComment
+  }
+
+data UnfreezeAccountRequest = UnfreezeAccountRequest
+  { unfreezeAccountRequestUsername :: Username
+  } deriving (Eq, Show)
+
+instance FromJSON UnfreezeAccountRequest where
+  parseJSON = withObject "UnfreezeAccountRequest" $ \x -> do
+    unfreezeAccountRequestUsername <- x .: "username"
+    pure UnfreezeAccountRequest{..}
+
+data InviteUserRequest = InviteUserRequest
+  { inviteUserRequestEmail :: Email
+  , inviteUserRequestRole :: Role
+  } deriving (Eq, Show)
+
+instance FromJSON InviteUserRequest where
+  parseJSON = withObject "InviteUserRequest" $ \x -> do
+    inviteUserRequestEmail <- x .: "email"
+    inviteUserRequestRole <- x .: "role"
+    pure InviteUserRequest{..}
+
+newtype InvitationSecret = InvitationSecret UUID.UUID
+  deriving (Eq, Show, Generic)
+
+instance ToJSON InvitationSecret
+instance FromJSON InvitationSecret
+
+instance ToField InvitationSecret where
+  toField (InvitationSecret secretUUID) =
+    secretUUID |> UUID.toText |> SQLText
+
+instance FromField InvitationSecret where
+  fromField y =
+    case fieldData y of
+      (SQLText x) ->
+        case UUID.fromText x of
+          Nothing -> returnError ConversionFailed y ("Could not convert text to UUID: " ++ show x)
+          Just z -> Ok $ InvitationSecret z
+      _ -> returnError ConversionFailed y "Field data is not SQLText, which is what we expect"
+
+data Invitation = Invitation
+  { invitationEmail :: Email
+  , invitationRole :: Role
+  , invitationSecret :: InvitationSecret
+  } deriving (Eq, Show)
+
+instance FromRow Invitation where
+  fromRow = Invitation <$> field
+                       <*> field
+                       <*> field
+
+data AcceptInvitationRequest = AcceptInvitationRequest
+  { acceptInvitationRequestUsername :: Username
+  , acceptInvitationRequestPassword :: ClearTextPassword
+  , acceptInvitationRequestEmail :: Email
+  , acceptInvitationRequestSecret :: InvitationSecret
+  } deriving (Eq, Show)
+
+instance FromJSON AcceptInvitationRequest where
+  parseJSON = withObject "AcceptInvitationRequest" $ \x -> do
+    acceptInvitationRequestUsername <- x .: "username"
+    acceptInvitationRequestPassword <- x .: "password"
+    acceptInvitationRequestEmail <- x .: "email"
+    acceptInvitationRequestSecret <- x .: "secret"
+    pure AcceptInvitationRequest{..}
diff --git a/users/wpcarro/assessments/tt/src/Utils.hs b/users/wpcarro/assessments/tt/src/Utils.hs
new file mode 100644
index 0000000000..48c33af079
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/Utils.hs
@@ -0,0 +1,9 @@
+--------------------------------------------------------------------------------
+module Utils where
+--------------------------------------------------------------------------------
+import Data.Function ((&))
+--------------------------------------------------------------------------------
+
+-- | Prefer this operator to the ampersand for stylistic reasons.
+(|>) :: a -> (a -> b) -> b
+(|>) = (&)
diff --git a/users/wpcarro/assessments/tt/src/init.sql b/users/wpcarro/assessments/tt/src/init.sql
new file mode 100644
index 0000000000..b42753ae5d
--- /dev/null
+++ b/users/wpcarro/assessments/tt/src/init.sql
@@ -0,0 +1,67 @@
+-- Run `.read init.sql` from within a SQLite3 REPL to initialize the tables we
+-- need for this application. This will erase all current entries, so use with
+-- caution.
+-- Make sure to set `PRAGMA foreign_keys = on;` when transacting with the
+-- database.
+
+BEGIN TRANSACTION;
+
+DROP TABLE IF EXISTS Accounts;
+DROP TABLE IF EXISTS Trips;
+DROP TABLE IF EXISTS Sessions;
+DROP TABLE IF EXISTS LoginAttempts;
+DROP TABLE IF EXISTS PendingAccounts;
+DROP TABLE IF EXISTS Invitations;
+
+CREATE TABLE Accounts (
+  username TEXT CHECK(LENGTH(username) > 0) NOT NULL,
+  password TEXT CHECK(LENGTH(password) > 0) NOT NULL,
+  email TEXT CHECK(LENGTH(email) > 0) NOT NULL UNIQUE,
+  role TEXT CHECK(role IN ('user', 'manager', 'admin')) NOT NULL,
+  profilePicture BLOB,
+  PRIMARY KEY (username)
+);
+
+CREATE TABLE Trips (
+  username TEXT NOT NULL,
+  destination TEXT CHECK(LENGTH(destination) > 0) NOT NULL,
+  startDate TEXT CHECK(LENGTH(startDate) == 10) NOT NULL, -- 'YYYY-MM-DD'
+  endDate TEXT CHECK(LENGTH(endDate) == 10) NOT NULL, -- 'YYYY-MM-DD'
+  comment TEXT NOT NULL,
+  PRIMARY KEY (username, destination, startDate),
+  FOREIGN KEY (username) REFERENCES Accounts ON DELETE CASCADE
+);
+
+CREATE TABLE Sessions (
+  uuid TEXT CHECK(LENGTH(uuid) == 36) NOT NULL,
+  username TEXT NOT NULL UNIQUE,
+  -- TODO(wpcarro): Add a LENGTH CHECK here
+  tsCreated TEXT NOT NULL, -- 'YYYY-MM-DD HH:MM:SS'
+  PRIMARY KEY (uuid),
+  FOREIGN KEY (username) REFERENCES Accounts ON DELETE CASCADE
+);
+
+CREATE TABLE LoginAttempts (
+  username TEXT NOT NULL UNIQUE,
+  numAttempts INTEGER NOT NULL,
+  PRIMARY KEY (username),
+  FOREIGN KEY (username) REFERENCES Accounts ON DELETE CASCADE
+);
+
+CREATE TABLE PendingAccounts (
+  secret TEXT CHECK(LENGTH(secret) == 36) NOT NULL,
+  username TEXT CHECK(LENGTH(username) > 0) NOT NULL,
+  password TEXT CHECK(LENGTH(password) > 0) NOT NULL,
+  role TEXT CHECK(role IN ('user', 'manager', 'admin')) NOT NULL,
+  email TEXT CHECK(LENGTH(email) > 0) NOT NULL UNIQUE,
+  PRIMARY KEY (username)
+);
+
+CREATE TABLE Invitations (
+  email TEXT CHECK(LENGTH(email) > 0) NOT NULL UNIQUE,
+  role TEXT CHECK(role IN ('user', 'manager', 'admin')) NOT NULL,
+  secret TEXT CHECK(LENGTH(secret) == 36) NOT NULL,
+  PRIMARY KEY (email)
+);
+
+COMMIT;
diff --git a/users/wpcarro/assessments/tt/tests/create-accounts.sh b/users/wpcarro/assessments/tt/tests/create-accounts.sh
new file mode 100755
index 0000000000..8c2a66bc8b
--- /dev/null
+++ b/users/wpcarro/assessments/tt/tests/create-accounts.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env sh
+
+# This script populates the Accounts table over HTTP.
+
+http POST :3000/accounts \
+  username=mimi \
+  password=testing \
+  email=miriamwright@google.com \
+  role=user
+
+http POST :3000/accounts \
+  username=bill \
+  password=testing \
+  email=wpcarro@gmail.com \
+  role=manager
+
+http POST :3000/accounts \
+  username=wpcarro \
+  password=testing \
+  email=wpcarro@google.com \
+  role=admin
diff --git a/users/wpcarro/assessments/tt/todo.org b/users/wpcarro/assessments/tt/todo.org
new file mode 100644
index 0000000000..39592d0482
--- /dev/null
+++ b/users/wpcarro/assessments/tt/todo.org
@@ -0,0 +1,18 @@
+* TODO Users must be able to create an account
+* TODO Users must verify their account by email
+* TODO Support federated login with Google
+* TODO Users must be able to authenticate and login
+* TODO Define three roles: user, manager, admin
+* TODO Users can add trips
+* TODO Users can edit trips
+* TODO Users can delete trips
+* TODO Users can filter trips
+* TODO Support all actions via the REST API
+* TODO Block users after three failed authentication attempts
+* TODO Only admins and managers can unblock blocked login attempts
+* TODO Add unit tests
+* TODO Add E2E tests
+* TODO Pull user profile pictures using Gravatar
+* TODO Allow users to change their profile picture
+* TODO Admins should be allowed to invite new users via email
+* TODO Allow users to print their travel itineraries
diff --git a/users/wpcarro/bin/__dispatch.sh b/users/wpcarro/bin/__dispatch.sh
new file mode 100755
index 0000000000..6da9a1c416
--- /dev/null
+++ b/users/wpcarro/bin/__dispatch.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# This script dispatches invocations transparently to programs instantiated from
+# Nix.
+#
+# To add a new tool, insert it into the case statement below by setting `attr`
+# to the key in nixpkgs which represents the program you want to run.
+set -ueo pipefail
+
+TARGET_TOOL=$(basename "$0")
+
+case "${TARGET_TOOL}" in
+  deploy-diogenes)
+    attr="users.wpcarro.nixos.deploy-diogenes"
+    ;;
+  rebuild-diogenes)
+    attr="users.wpcarro.nixos.rebuild-diogenes"
+    ;;
+  import-gpg)
+    attr="users.wpcarro.configs.import-gpg"
+    ;;
+  export-gpg)
+    attr="users.wpcarro.configs.export-gpg"
+    ;;
+  *)
+    echo "The tool '${TARGET_TOOL}' is currently not installed in this repository."
+    exit 1
+    ;;
+esac
+
+result=$(nix-build --no-out-link --attr "${attr}" "${REPO_ROOT}")
+PATH="${result}/bin:$PATH"
+
+exec "${TARGET_TOOL}" "${@}"
diff --git a/users/wpcarro/bin/deploy-diogenes b/users/wpcarro/bin/deploy-diogenes
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/users/wpcarro/bin/deploy-diogenes
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/users/wpcarro/bin/export-gpg b/users/wpcarro/bin/export-gpg
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/users/wpcarro/bin/export-gpg
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/users/wpcarro/bin/import-gpg b/users/wpcarro/bin/import-gpg
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/users/wpcarro/bin/import-gpg
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/users/wpcarro/bin/rebuild-diogenes b/users/wpcarro/bin/rebuild-diogenes
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/users/wpcarro/bin/rebuild-diogenes
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/users/wpcarro/boilerplate/README.md b/users/wpcarro/boilerplate/README.md
new file mode 100644
index 0000000000..aa72266a33
--- /dev/null
+++ b/users/wpcarro/boilerplate/README.md
@@ -0,0 +1,21 @@
+# Boilerplate
+
+Storing some boilerplate code to help me reduce the time it takes me to develop
+and deploy applications.
+
+## Usage
+
+Let's say that you would like to create a game for
+`sandbox.wpcarro.dev/game`. We will create a new TypeScript project with the
+following:
+
+```shell
+$ cp -r typescript path/to/new-project
+```
+
+This initializes the project. To start developing, run:
+
+```shell
+$ nix-shell
+$ yarn run dev
+```
diff --git a/users/wpcarro/boilerplate/clojure/.envrc b/users/wpcarro/boilerplate/clojure/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/boilerplate/clojure/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/boilerplate/clojure/.gitignore b/users/wpcarro/boilerplate/clojure/.gitignore
new file mode 100644
index 0000000000..f24c5e393a
--- /dev/null
+++ b/users/wpcarro/boilerplate/clojure/.gitignore
@@ -0,0 +1,4 @@
+/.lein-repl-history
+/target
+/?
+/.nrepl-port
\ No newline at end of file
diff --git a/users/wpcarro/boilerplate/clojure/README.md b/users/wpcarro/boilerplate/clojure/README.md
new file mode 100644
index 0000000000..53590850bc
--- /dev/null
+++ b/users/wpcarro/boilerplate/clojure/README.md
@@ -0,0 +1,33 @@
+# Clojure Boilerplate
+
+This boilerplate uses `lein` to manage the project.
+
+## Files to change
+
+To use this boilerplate, run the following in a shell:
+
+```shell
+$ cp -r . path/to/new-project
+```
+
+After running the above command, change the following files to remove the
+placeholder values:
+
+- `README.md`: Change the title; change the description; drop "Files to change";
+  keep "Getting started"
+- `project.clj`: Change title
+- `src/main.clj`: Change `:doc`; drop `main/foo`
+
+## Getting started
+
+From a shell, run:
+
+```shell
+$ lein repl
+```
+
+From Emacs, navigate to a source code buffer and run:
+
+```
+M-x cider-jack-in
+```
diff --git a/users/wpcarro/boilerplate/clojure/project.clj b/users/wpcarro/boilerplate/clojure/project.clj
new file mode 100644
index 0000000000..54e34eab7a
--- /dev/null
+++ b/users/wpcarro/boilerplate/clojure/project.clj
@@ -0,0 +1,2 @@
+(defproject boilerplate "0.0.1"
+  :dependencies [[org.clojure/clojure "1.8.0"]])
diff --git a/users/wpcarro/boilerplate/clojure/shell.nix b/users/wpcarro/boilerplate/clojure/shell.nix
new file mode 100644
index 0000000000..8b92b592e1
--- /dev/null
+++ b/users/wpcarro/boilerplate/clojure/shell.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    leiningen
+  ];
+}
diff --git a/users/wpcarro/boilerplate/clojure/src/main.clj b/users/wpcarro/boilerplate/clojure/src/main.clj
new file mode 100644
index 0000000000..f6b60dba40
--- /dev/null
+++ b/users/wpcarro/boilerplate/clojure/src/main.clj
@@ -0,0 +1,8 @@
+(ns ^{:doc "Top-level module."
+      :author "William Carroll"}
+    main)
+
+(declare main)
+
+(defn foo [a b]
+  (+ a b))
diff --git a/users/wpcarro/boilerplate/elm/.envrc b/users/wpcarro/boilerplate/elm/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/boilerplate/elm/.gitignore b/users/wpcarro/boilerplate/elm/.gitignore
new file mode 100644
index 0000000000..1cb4f3034c
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/.gitignore
@@ -0,0 +1,3 @@
+/elm-stuff
+/Main.min.js
+/output.css
diff --git a/users/wpcarro/boilerplate/elm/README.md b/users/wpcarro/boilerplate/elm/README.md
new file mode 100644
index 0000000000..04804ad94f
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/README.md
@@ -0,0 +1,18 @@
+# Elm
+
+Elm has one of the best developer experiences that I'm aware of. The error
+messages are helpful and the entire experience is optimized to improve the ease
+of writing web applications.
+
+## Developing
+
+If you're interested in contributing, the following will create an environment
+in which you can develop:
+
+```shell
+$ nix-shell
+$ npx tailwindcss build index.css -o output.css
+$ elm-live -- src/Main.elm --output=Main.min.js
+```
+
+You can now view your web client at `http://localhost:8000`!
diff --git a/users/wpcarro/boilerplate/elm/elm.json b/users/wpcarro/boilerplate/elm/elm.json
new file mode 100644
index 0000000000..a95f80408e
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/elm.json
@@ -0,0 +1,30 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/random": "1.0.0",
+            "elm/svg": "1.0.1",
+            "elm/time": "1.0.0",
+            "elm-community/list-extra": "8.2.3",
+            "elm-community/maybe-extra": "5.2.0",
+            "elm-community/random-extra": "3.1.0"
+        },
+        "indirect": {
+            "elm/json": "1.1.3",
+            "elm/url": "1.0.0",
+            "elm/virtual-dom": "1.0.2",
+            "owanturist/elm-union-find": "1.0.0"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}
diff --git a/users/wpcarro/boilerplate/elm/index.css b/users/wpcarro/boilerplate/elm/index.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/users/wpcarro/boilerplate/elm/index.html b/users/wpcarro/boilerplate/elm/index.html
new file mode 100644
index 0000000000..ce8f727b6f
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Elm SPA</title>
+    <link rel="stylesheet" href="./output.css" />
+    <script src="./Main.min.js"></script>
+  </head>
+  <body class="font-serif">
+    <div id="mount"></div>
+    <script>
+     Elm.Main.init({node: document.getElementById("mount")});
+    </script>
+  </body>
+</html>
diff --git a/users/wpcarro/boilerplate/elm/shell.nix b/users/wpcarro/boilerplate/elm/shell.nix
new file mode 100644
index 0000000000..afcc0f4d36
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs.elmPackages; [
+    elm
+    elm-format
+    elm-live
+  ];
+}
diff --git a/users/wpcarro/boilerplate/elm/src/Landing.elm b/users/wpcarro/boilerplate/elm/src/Landing.elm
new file mode 100644
index 0000000000..00bb9e281a
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/src/Landing.elm
@@ -0,0 +1,13 @@
+module Landing exposing (render)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import State
+
+
+render : State.Model -> Html State.Msg
+render model =
+    div [ class "pt-10 pb-20 px-10" ]
+        [ p [] [ text "Welcome to the landing page!" ]
+        ]
diff --git a/users/wpcarro/boilerplate/elm/src/Login.elm b/users/wpcarro/boilerplate/elm/src/Login.elm
new file mode 100644
index 0000000000..27f1d811a8
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/src/Login.elm
@@ -0,0 +1,13 @@
+module Login exposing (render)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import State
+
+
+render : State.Model -> Html State.Msg
+render model =
+    div [ class "pt-10 pb-20 px-10" ]
+        [ p [] [ text "Please authenticate" ]
+        ]
diff --git a/users/wpcarro/boilerplate/elm/src/Main.elm b/users/wpcarro/boilerplate/elm/src/Main.elm
new file mode 100644
index 0000000000..30006460cd
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/src/Main.elm
@@ -0,0 +1,31 @@
+module Main exposing (main)
+
+import Browser
+import Html exposing (..)
+import Landing
+import Login
+import State
+
+
+subscriptions : State.Model -> Sub State.Msg
+subscriptions model =
+    Sub.none
+
+
+view : State.Model -> Html State.Msg
+view model =
+    case model.view of
+        State.Landing ->
+            Landing.render model
+
+        State.Login ->
+            Login.render model
+
+
+main =
+    Browser.element
+        { init = \() -> ( State.init, Cmd.none )
+        , subscriptions = subscriptions
+        , update = State.update
+        , view = view
+        }
diff --git a/users/wpcarro/boilerplate/elm/src/State.elm b/users/wpcarro/boilerplate/elm/src/State.elm
new file mode 100644
index 0000000000..c1edae8bb6
--- /dev/null
+++ b/users/wpcarro/boilerplate/elm/src/State.elm
@@ -0,0 +1,43 @@
+module State exposing (..)
+
+
+type Msg
+    = DoNothing
+    | SetView View
+
+
+type View
+    = Landing
+    | Login
+
+
+type alias Model =
+    { isLoading : Bool
+    , view : View
+    }
+
+
+{-| The initial state for the application.
+-}
+init : Model
+init =
+    { isLoading = False
+    , view = Landing
+    }
+
+
+{-| Now that we have state, we need a function to change the state.
+-}
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        DoNothing ->
+            ( model, Cmd.none )
+
+        SetView x ->
+            ( { model
+                | view = x
+                , isLoading = True
+              }
+            , Cmd.none
+            )
diff --git a/users/wpcarro/boilerplate/typescript/.envrc b/users/wpcarro/boilerplate/typescript/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/boilerplate/typescript/.gitignore b/users/wpcarro/boilerplate/typescript/.gitignore
new file mode 100644
index 0000000000..ebea22e071
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/.gitignore
@@ -0,0 +1,3 @@
+/.cache
+/dist
+/node_modules
\ No newline at end of file
diff --git a/users/wpcarro/boilerplate/typescript/README.md b/users/wpcarro/boilerplate/typescript/README.md
new file mode 100644
index 0000000000..a54186a9f2
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/README.md
@@ -0,0 +1,26 @@
+# Frontend Boilerplate
+
+While many times I prefer using alt-languages like ReasonML, ClojureScript, or
+Elm, sometimes I prefer to write an application using TypeScript. This directory
+contains the necessary starter code to create these applications.
+
+- React: Maps application state to UI
+- React-Router: Stateful routing for SPAs
+- Redux: Application state management
+- TypeScript: Type-safety
+- TailwindCSS: Styling library using utility classes
+- Prettier: Source code formatting
+- Jest: Test runner
+
+## Developing
+
+```shell
+$ nix-shell
+$ yarn run dev
+```
+
+## Building
+
+```shell
+$ nix-build
+```
diff --git a/users/wpcarro/boilerplate/typescript/default.nix b/users/wpcarro/boilerplate/typescript/default.nix
new file mode 100644
index 0000000000..84949cae7f
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/default.nix
@@ -0,0 +1,23 @@
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "typescript";
+  srcs = builtins.path { path = ./.; name = "typescript"; };
+  buildInputs = with pkgs; [
+    nodejs
+    # Exposes lscpu for parcel.js
+    utillinux
+  ];
+  # parcel.js needs number of CPUs
+  PARCEL_WORKERS = "1";
+  buildPhase = ''
+    export HOME="."
+    npx parcel build src/index.html --public-url ./
+  '';
+  installPhase = ''
+    mv dist $out
+  '';
+
+  # TODO(wpcarro): This doesn't build at all.
+  meta.ci.skip = true;
+}
diff --git a/users/wpcarro/boilerplate/typescript/package.json b/users/wpcarro/boilerplate/typescript/package.json
new file mode 100644
index 0000000000..104e7272da
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/package.json
@@ -0,0 +1,27 @@
+{
+  "name": "tailwindcss",
+  "version": "1.0.0",
+  "main": "index.js",
+  "license": "MIT",
+  "scripts": {
+    "dev": "parcel src/index.html & npx tsc --watch --noEmit",
+    "prettier": "prettier --ignore-path .gitignore --write \"**/*.{js,ts,jsx,tsx,html,css.json}\""
+  },
+  "devDependencies": {
+    "@types/node": "^13.9.3",
+    "parcel-bundler": "^1.12.4",
+    "prettier": "^2.0.2",
+    "tailwindcss": "^1.2.0",
+    "typescript": "^3.8.3"
+  },
+  "dependencies": {
+    "@reduxjs/toolkit": "^1.2.5",
+    "@types/react-dom": "^16.9.5",
+    "@types/react-redux": "^7.1.7",
+    "@types/react-router-dom": "^5.1.3",
+    "react": "^16.13.1",
+    "react-dom": "^16.13.1",
+    "react-redux": "^7.2.0",
+    "react-router-dom": "^5.1.2"
+  }
+}
diff --git a/users/wpcarro/boilerplate/typescript/postcss.config.js b/users/wpcarro/boilerplate/typescript/postcss.config.js
new file mode 100644
index 0000000000..d68fa61866
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/postcss.config.js
@@ -0,0 +1,7 @@
+const tailwindcss = require('tailwindcss')
+
+module.exports = {
+  plugins: [
+    tailwindcss('./tailwind.config.js')
+  ]
+}
diff --git a/users/wpcarro/boilerplate/typescript/shell.nix b/users/wpcarro/boilerplate/typescript/shell.nix
new file mode 100644
index 0000000000..a3ae929ef4
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/shell.nix
@@ -0,0 +1,8 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    nodejs
+    yarn
+  ];
+}
diff --git a/users/wpcarro/boilerplate/typescript/src/App.tsx b/users/wpcarro/boilerplate/typescript/src/App.tsx
new file mode 100644
index 0000000000..4fae1b36ac
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/src/App.tsx
@@ -0,0 +1,52 @@
+import React, { useEffect } from "react";
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { actions, useTypedSelector } from "./store";
+import { Link } from "react-router-dom";
+
+const App: React.FC = () => {
+  const dispatch = useDispatch();
+  const { isLoading } = useTypedSelector(state => ({
+    isLoading: state.isLoading,
+  }));
+
+  return (
+    <Router>
+      <nav className="bg-blue-400">
+        <ul className="container mx-auto justify-between flex py-6 text-white">
+          <li>
+            <Link to="/">Home</Link>
+          </li>
+          <li>
+            <Link to="/about">About</Link>
+          </li>
+          <li>
+            <Link to="/contact">Contact</Link>
+          </li>
+        </ul>
+      </nav>
+      <Switch>
+        <Route exact path="/">
+          <div className="container mx-auto">
+            <h1>Welcome to the home page. Loading: {isLoading ? "true" : "false"}</h1>
+            <button
+              className="bg-gray-300 py-4 px-6"
+              onClick={() => dispatch(actions.toggleIsLoading())}>isLoading</button>
+          </div>
+        </Route>
+        <Route exact path="/about">
+          <div className="container mx-auto">
+            <h1>Here is the about page.</h1>
+          </div>
+        </Route>
+        <Route exact path="/contact">
+          <div className="container mx-auto">
+            <h1>Here is the contact page.</h1>
+          </div>
+        </Route>
+      </Switch>
+    </Router>
+  );
+};
+
+export default App;
diff --git a/users/wpcarro/boilerplate/typescript/src/index.css b/users/wpcarro/boilerplate/typescript/src/index.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/src/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/users/wpcarro/boilerplate/typescript/src/index.html b/users/wpcarro/boilerplate/typescript/src/index.html
new file mode 100644
index 0000000000..91752af916
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/src/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="stylesheet" href="./index.css" />
+  </head>
+  <body>
+    <div id="mount"></div>
+    <script src="./index.tsx"></script>
+  </body>
+</html>
diff --git a/users/wpcarro/boilerplate/typescript/src/index.tsx b/users/wpcarro/boilerplate/typescript/src/index.tsx
new file mode 100644
index 0000000000..dc28dc4a9c
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/src/index.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+import { Provider } from "react-redux";
+import store from "./store";
+
+ReactDOM.render(
+  <Provider store={store}>
+    <App />
+  </Provider>,
+  document.getElementById("mount")
+);
diff --git a/users/wpcarro/boilerplate/typescript/src/store.ts b/users/wpcarro/boilerplate/typescript/src/store.ts
new file mode 100644
index 0000000000..03e980a491
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/src/store.ts
@@ -0,0 +1,26 @@
+import { createSlice, configureStore, PayloadAction } from "@reduxjs/toolkit";
+import { useSelector, TypedUseSelectorHook } from "react-redux";
+
+export interface State {
+  isLoading: boolean;
+}
+
+const initialState: State = {
+  isLoading: true,
+};
+
+export const { actions, reducer } = createSlice({
+  name: "application",
+  initialState,
+  reducers: {
+    toggleIsLoading: state => ({ ...state, isLoading: !state.isLoading }),
+  }
+});
+
+/**
+ * Defining and consuming this allows us to avoid annotating State in all of our
+ * selectors.
+ */
+export const useTypedSelector: TypedUseSelectorHook<State> = useSelector;
+
+export default configureStore({ reducer });
diff --git a/users/wpcarro/boilerplate/typescript/tailwind.config.js b/users/wpcarro/boilerplate/typescript/tailwind.config.js
new file mode 100644
index 0000000000..af829e20f9
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/tailwind.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+  theme: {
+    extend: {},
+  },
+  variants: {},
+  plugins: [],
+}
diff --git a/users/wpcarro/boilerplate/typescript/tsconfig.json b/users/wpcarro/boilerplate/typescript/tsconfig.json
new file mode 100644
index 0000000000..013f34fdf0
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react"
+  },
+  "include": [
+    "src/**/*"
+  ]
+}
diff --git a/users/wpcarro/boilerplate/typescript/yarn.lock b/users/wpcarro/boilerplate/typescript/yarn.lock
new file mode 100644
index 0000000000..0e16fe80a4
--- /dev/null
+++ b/users/wpcarro/boilerplate/typescript/yarn.lock
@@ -0,0 +1,5670 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
+  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
+  dependencies:
+    "@babel/highlight" "^7.8.3"
+
+"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c"
+  integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==
+  dependencies:
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    semver "^5.5.0"
+
+"@babel/core@^7.4.4":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
+  integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helpers" "^7.9.0"
+    "@babel/parser" "^7.9.0"
+    "@babel/template" "^7.8.6"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
+"@babel/generator@^7.4.4", "@babel/generator@^7.9.0":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.3.tgz#7c8b2956c6f68b3ab732bd16305916fbba521d94"
+  integrity sha512-RpxM252EYsz9qLUIq6F7YJyK1sv0wWDBFuztfDGWaQKzHjqDHysxSiRUpA/X9jmfqo+WzkAVKFaUily5h+gDCQ==
+  dependencies:
+    "@babel/types" "^7.9.0"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+
+"@babel/helper-annotate-as-pure@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
+  integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503"
+  integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==
+  dependencies:
+    "@babel/helper-explode-assignable-expression" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-builder-react-jsx-experimental@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.0.tgz#066d80262ade488f9c1b1823ce5db88a4cedaa43"
+  integrity sha512-3xJEiyuYU4Q/Ar9BsHisgdxZsRlsShMe90URZ0e6przL26CCs8NJbDoxH94kKT17PcxlMhsCAwZd90evCo26VQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/types" "^7.9.0"
+
+"@babel/helper-builder-react-jsx@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz#16bf391990b57732700a3278d4d9a81231ea8d32"
+  integrity sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/types" "^7.9.0"
+
+"@babel/helper-compilation-targets@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde"
+  integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==
+  dependencies:
+    "@babel/compat-data" "^7.8.6"
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087"
+  integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-regex" "^7.8.3"
+    regexpu-core "^4.7.0"
+
+"@babel/helper-define-map@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15"
+  integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==
+  dependencies:
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/types" "^7.8.3"
+    lodash "^4.17.13"
+
+"@babel/helper-explode-assignable-expression@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982"
+  integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==
+  dependencies:
+    "@babel/traverse" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-function-name@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca"
+  integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-get-function-arity@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
+  integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-hoist-variables@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134"
+  integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-member-expression-to-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
+  integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-module-imports@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
+  integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-module-transforms@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
+  integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-simple-access" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/template" "^7.8.6"
+    "@babel/types" "^7.9.0"
+    lodash "^4.17.13"
+
+"@babel/helper-optimise-call-expression@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
+  integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
+  integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
+
+"@babel/helper-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965"
+  integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==
+  dependencies:
+    lodash "^4.17.13"
+
+"@babel/helper-remap-async-to-generator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86"
+  integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-wrap-function" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
+  integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/traverse" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
+"@babel/helper-simple-access@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
+  integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-split-export-declaration@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
+  integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-validator-identifier@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed"
+  integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==
+
+"@babel/helper-wrap-function@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610"
+  integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==
+  dependencies:
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helpers@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
+  integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+
+"@babel/highlight@^7.8.3":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
+  integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.4.4", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.3.tgz#043a5fc2ad8b7ea9facddc4e802a1f0f25da7255"
+  integrity sha512-E6SpIDJZ0cZAKoCNk+qSDd0ChfTnpiJN9FfNf3RZ20dzwA2vL2oq5IX1XTVT+4vDmRlta2nGk5HGMMskJAR+4A==
+
+"@babel/plugin-proposal-async-generator-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
+  integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-remap-async-to-generator" "^7.8.3"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+
+"@babel/plugin-proposal-dynamic-import@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054"
+  integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+
+"@babel/plugin-proposal-json-strings@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b"
+  integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2"
+  integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8"
+  integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+
+"@babel/plugin-proposal-object-rest-spread@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f"
+  integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9"
+  integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58"
+  integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d"
+  integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.8"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-async-generators@^7.8.0":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+  integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-dynamic-import@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
+  integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-flow@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.8.3.tgz#f2c883bd61a6316f2c89380ae5122f923ba4527f"
+  integrity sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-json-strings@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-jsx@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94"
+  integrity sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f"
+  integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-object-rest-spread@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+  integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391"
+  integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-arrow-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
+  integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-async-to-generator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086"
+  integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-remap-async-to-generator" "^7.8.3"
+
+"@babel/plugin-transform-block-scoped-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3"
+  integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-block-scoping@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a"
+  integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    lodash "^4.17.13"
+
+"@babel/plugin-transform-classes@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d"
+  integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-define-map" "^7.8.3"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b"
+  integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-destructuring@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b"
+  integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e"
+  integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-duplicate-keys@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1"
+  integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-exponentiation-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7"
+  integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==
+  dependencies:
+    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-flow-strip-types@^7.4.4":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.9.0.tgz#8a3538aa40434e000b8f44a3c5c9ac7229bd2392"
+  integrity sha512-7Qfg0lKQhEHs93FChxVLAvhBshOPQDtJUTVHr/ZwQNRccCm4O9D79r9tVSoV8iNwjP1YgfD+e/fgHcPkN1qEQg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-flow" "^7.8.3"
+
+"@babel/plugin-transform-for-of@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e"
+  integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-function-name@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b"
+  integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==
+  dependencies:
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1"
+  integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-member-expression-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410"
+  integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-modules-amd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4"
+  integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.4.4", "@babel/plugin-transform-modules-commonjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940"
+  integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-simple-access" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90"
+  integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==
+  dependencies:
+    "@babel/helper-hoist-variables" "^7.8.3"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-umd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697"
+  integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c"
+  integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+
+"@babel/plugin-transform-new-target@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43"
+  integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-object-super@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725"
+  integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.3"
+
+"@babel/plugin-transform-parameters@^7.8.7":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a"
+  integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-property-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263"
+  integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-react-jsx@^7.0.0":
+  version "7.9.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.1.tgz#d03af29396a6dc51bfa24eefd8005a9fd381152a"
+  integrity sha512-+xIZ6fPoix7h57CNO/ZeYADchg1tFyX9NDsnmNFFua8e1JNPln156mzS+8AQe1On2X2GLlANHJWHIXbMCqWDkQ==
+  dependencies:
+    "@babel/helper-builder-react-jsx" "^7.9.0"
+    "@babel/helper-builder-react-jsx-experimental" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-jsx" "^7.8.3"
+
+"@babel/plugin-transform-regenerator@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8"
+  integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==
+  dependencies:
+    regenerator-transform "^0.14.2"
+
+"@babel/plugin-transform-reserved-words@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5"
+  integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-shorthand-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8"
+  integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-spread@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8"
+  integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-sticky-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100"
+  integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-regex" "^7.8.3"
+
+"@babel/plugin-transform-template-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80"
+  integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-typeof-symbol@^7.8.4":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412"
+  integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-unicode-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad"
+  integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/preset-env@^7.4.4":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
+  integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==
+  dependencies:
+    "@babel/compat-data" "^7.9.0"
+    "@babel/helper-compilation-targets" "^7.8.7"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-proposal-async-generator-functions" "^7.8.3"
+    "@babel/plugin-proposal-dynamic-import" "^7.8.3"
+    "@babel/plugin-proposal-json-strings" "^7.8.3"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-proposal-numeric-separator" "^7.8.3"
+    "@babel/plugin-proposal-object-rest-spread" "^7.9.0"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-proposal-optional-chaining" "^7.9.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.8.3"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.8.3"
+    "@babel/plugin-transform-arrow-functions" "^7.8.3"
+    "@babel/plugin-transform-async-to-generator" "^7.8.3"
+    "@babel/plugin-transform-block-scoped-functions" "^7.8.3"
+    "@babel/plugin-transform-block-scoping" "^7.8.3"
+    "@babel/plugin-transform-classes" "^7.9.0"
+    "@babel/plugin-transform-computed-properties" "^7.8.3"
+    "@babel/plugin-transform-destructuring" "^7.8.3"
+    "@babel/plugin-transform-dotall-regex" "^7.8.3"
+    "@babel/plugin-transform-duplicate-keys" "^7.8.3"
+    "@babel/plugin-transform-exponentiation-operator" "^7.8.3"
+    "@babel/plugin-transform-for-of" "^7.9.0"
+    "@babel/plugin-transform-function-name" "^7.8.3"
+    "@babel/plugin-transform-literals" "^7.8.3"
+    "@babel/plugin-transform-member-expression-literals" "^7.8.3"
+    "@babel/plugin-transform-modules-amd" "^7.9.0"
+    "@babel/plugin-transform-modules-commonjs" "^7.9.0"
+    "@babel/plugin-transform-modules-systemjs" "^7.9.0"
+    "@babel/plugin-transform-modules-umd" "^7.9.0"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3"
+    "@babel/plugin-transform-new-target" "^7.8.3"
+    "@babel/plugin-transform-object-super" "^7.8.3"
+    "@babel/plugin-transform-parameters" "^7.8.7"
+    "@babel/plugin-transform-property-literals" "^7.8.3"
+    "@babel/plugin-transform-regenerator" "^7.8.7"
+    "@babel/plugin-transform-reserved-words" "^7.8.3"
+    "@babel/plugin-transform-shorthand-properties" "^7.8.3"
+    "@babel/plugin-transform-spread" "^7.8.3"
+    "@babel/plugin-transform-sticky-regex" "^7.8.3"
+    "@babel/plugin-transform-template-literals" "^7.8.3"
+    "@babel/plugin-transform-typeof-symbol" "^7.8.4"
+    "@babel/plugin-transform-unicode-regex" "^7.8.3"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.9.0"
+    browserslist "^4.9.1"
+    core-js-compat "^3.6.2"
+    invariant "^2.2.2"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/preset-modules@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
+  integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
+
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
+  integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/template@^7.4.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
+  integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/parser" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
+"@babel/traverse@^7.4.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892"
+  integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/parser" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.13"
+
+"@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5"
+  integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
+"@iarna/toml@^2.2.0":
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.3.tgz#f060bf6eaafae4d56a7dac618980838b0696e2ab"
+  integrity sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+  integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+  dependencies:
+    call-me-maybe "^1.0.1"
+    glob-to-regexp "^0.3.0"
+
+"@nodelib/fs.stat@^1.1.2":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+  integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+
+"@parcel/fs@^1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-1.11.0.tgz#fb8a2be038c454ad46a50dc0554c1805f13535cd"
+  integrity sha512-86RyEqULbbVoeo8OLcv+LQ1Vq2PKBAvWTU9fCgALxuCTbbs5Ppcvll4Vr+Ko1AnmMzja/k++SzNAwJfeQXVlpA==
+  dependencies:
+    "@parcel/utils" "^1.11.0"
+    mkdirp "^0.5.1"
+    rimraf "^2.6.2"
+
+"@parcel/logger@^1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-1.11.1.tgz#c55b0744bcbe84ebc291155627f0ec406a23e2e6"
+  integrity sha512-9NF3M6UVeP2udOBDILuoEHd8VrF4vQqoWHEafymO1pfSoOMfxrSJZw1MfyAAIUN/IFp9qjcpDCUbDZB+ioVevA==
+  dependencies:
+    "@parcel/workers" "^1.11.0"
+    chalk "^2.1.0"
+    grapheme-breaker "^0.3.2"
+    ora "^2.1.0"
+    strip-ansi "^4.0.0"
+
+"@parcel/utils@^1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-1.11.0.tgz#539e08fff8af3b26eca11302be80b522674b51ea"
+  integrity sha512-cA3p4jTlaMeOtAKR/6AadanOPvKeg8VwgnHhOyfi0yClD0TZS/hi9xu12w4EzA/8NtHu0g6o4RDfcNjqN8l1AQ==
+
+"@parcel/watcher@^1.12.1":
+  version "1.12.1"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-1.12.1.tgz#b98b3df309fcab93451b5583fc38e40826696dad"
+  integrity sha512-od+uCtCxC/KoNQAIE1vWx1YTyKYY+7CTrxBJPRh3cDWw/C0tCtlBMVlrbplscGoEpt6B27KhJDCv82PBxOERNA==
+  dependencies:
+    "@parcel/utils" "^1.11.0"
+    chokidar "^2.1.5"
+
+"@parcel/workers@^1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-1.11.0.tgz#7b8dcf992806f4ad2b6cecf629839c41c2336c59"
+  integrity sha512-USSjRAAQYsZFlv43FUPdD+jEGML5/8oLF0rUzPQTtK4q9kvaXr49F5ZplyLz5lox78cLZ0TxN2bIDQ1xhOkulQ==
+  dependencies:
+    "@parcel/utils" "^1.11.0"
+    physical-cpu-count "^2.0.0"
+
+"@reduxjs/toolkit@^1.2.5":
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.2.5.tgz#149aa62da12a18a67a30495cb63fd897003f2272"
+  integrity sha512-/OWoW5mniUXAomw4+3ZhhWodcs1/SRvK2HKyxLXdW6vKgmJhiBiSHe/huHARlKWujEmGaJrkafx548GE494bCQ==
+  dependencies:
+    immer "^4.0.1"
+    redux "^4.0.0"
+    redux-devtools-extension "^2.13.8"
+    redux-immutable-state-invariant "^2.1.0"
+    redux-thunk "^2.3.0"
+    reselect "^4.0.0"
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+"@types/history@*":
+  version "4.7.5"
+  resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
+  integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==
+
+"@types/hoist-non-react-statics@^3.3.0":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+  integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+  dependencies:
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+
+"@types/node@^13.9.3":
+  version "13.9.3"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.3.tgz#6356df2647de9eac569f9a52eda3480fa9e70b4d"
+  integrity sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==
+
+"@types/prop-types@*":
+  version "15.7.3"
+  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
+  integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
+
+"@types/q@^1.5.1":
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
+  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
+
+"@types/react-dom@^16.9.5":
+  version "16.9.5"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7"
+  integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg==
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-redux@^7.1.7":
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.7.tgz#12a0c529aba660696947384a059c5c6e08185c7a"
+  integrity sha512-U+WrzeFfI83+evZE2dkZ/oF/1vjIYgqrb5dGgedkqVV8HEfDFujNgWCwHL89TDuWKb47U0nTBT6PLGq4IIogWg==
+  dependencies:
+    "@types/hoist-non-react-statics" "^3.3.0"
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+    redux "^4.0.0"
+
+"@types/react-router-dom@^5.1.3":
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196"
+  integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==
+  dependencies:
+    "@types/history" "*"
+    "@types/react" "*"
+    "@types/react-router" "*"
+
+"@types/react-router@*":
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6"
+  integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ==
+  dependencies:
+    "@types/history" "*"
+    "@types/react" "*"
+
+"@types/react@*":
+  version "16.9.25"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.25.tgz#6ae2159b40138c792058a23c3c04fd3db49e929e"
+  integrity sha512-Dlj2V72cfYLPNscIG3/SMUOzhzj7GK3bpSrfefwt2YT9GLynvLCCZjbhyF6VsT0q0+aRACRX03TDJGb7cA0cqg==
+  dependencies:
+    "@types/prop-types" "*"
+    csstype "^2.2.0"
+
+abab@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
+  integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
+
+acorn-globals@^4.3.0:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+  integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+  dependencies:
+    acorn "^6.0.1"
+    acorn-walk "^6.0.1"
+
+acorn-node@^1.6.1:
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+  integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
+  dependencies:
+    acorn "^7.0.0"
+    acorn-walk "^7.0.0"
+    xtend "^4.0.2"
+
+acorn-walk@^6.0.1:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
+  integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+
+acorn-walk@^7.0.0:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
+  integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
+
+acorn@^6.0.1, acorn@^6.0.4:
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
+  integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+
+acorn@^7.0.0, acorn@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
+  integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
+
+ajv@^6.5.5:
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
+  integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+alphanum-sort@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-styles@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+  integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+ansi-to-html@^0.6.4:
+  version "0.6.14"
+  resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8"
+  integrity sha512-7ZslfB1+EnFSDO5Ju+ue5Y6It19DRnZXWv8jrGHgIlPna5Mh4jz7BV5jCbQneXNFurQcKoolaaAjHtgSBfOIuA==
+  dependencies:
+    entities "^1.1.2"
+
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+  integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+  integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+  integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-equal@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+  integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-unique@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+asn1.js@^4.0.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+  integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
+  dependencies:
+    bn.js "^4.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert@^1.1.1:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
+  integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
+  dependencies:
+    object-assign "^4.1.1"
+    util "0.10.3"
+
+assign-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+  integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+async-each@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
+  integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
+
+async-limiter@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+autoprefixer@^9.4.5:
+  version "9.7.4"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378"
+  integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g==
+  dependencies:
+    browserslist "^4.8.3"
+    caniuse-lite "^1.0.30001020"
+    chalk "^2.4.2"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^7.0.26"
+    postcss-value-parser "^4.0.2"
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+
+babel-plugin-dynamic-import-node@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
+  integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
+  dependencies:
+    object.assign "^4.1.0"
+
+babel-runtime@^6.11.6, babel-runtime@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+  integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+  dependencies:
+    core-js "^2.4.0"
+    regenerator-runtime "^0.11.0"
+
+babel-types@^6.15.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+  integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
+  dependencies:
+    babel-runtime "^6.26.0"
+    esutils "^2.0.2"
+    lodash "^4.17.4"
+    to-fast-properties "^1.0.3"
+
+babylon-walk@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/babylon-walk/-/babylon-walk-1.0.2.tgz#3b15a5ddbb482a78b4ce9c01c8ba181702d9d6ce"
+  integrity sha1-OxWl3btIKni0zpwByLoYFwLZ1s4=
+  dependencies:
+    babel-runtime "^6.11.6"
+    babel-types "^6.15.0"
+    lodash.clone "^4.5.0"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base64-js@^1.0.2:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
+  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+
+base@^0.11.1:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+  integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+  dependencies:
+    cache-base "^1.0.1"
+    class-utils "^0.3.5"
+    component-emitter "^1.2.1"
+    define-property "^1.0.0"
+    isobject "^3.0.1"
+    mixin-deep "^1.2.0"
+    pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+  dependencies:
+    tweetnacl "^0.14.3"
+
+binary-extensions@^1.0.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
+  integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+  integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+boolbase@^1.0.0, boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^2.3.1, braces@^2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+  integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+  dependencies:
+    arr-flatten "^1.1.0"
+    array-unique "^0.3.2"
+    extend-shallow "^2.0.1"
+    fill-range "^4.0.0"
+    isobject "^3.0.1"
+    repeat-element "^1.1.2"
+    snapdragon "^0.8.1"
+    snapdragon-node "^2.0.1"
+    split-string "^3.0.2"
+    to-regex "^3.0.1"
+
+brfs@^1.2.0:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3"
+  integrity sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==
+  dependencies:
+    quote-stream "^1.0.1"
+    resolve "^1.1.5"
+    static-module "^2.2.0"
+    through2 "^2.0.0"
+
+brorand@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+  integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browser-process-hrtime@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+  integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+  integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+  dependencies:
+    buffer-xor "^1.0.3"
+    cipher-base "^1.0.0"
+    create-hash "^1.1.0"
+    evp_bytestokey "^1.0.3"
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+  integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+  dependencies:
+    browserify-aes "^1.0.4"
+    browserify-des "^1.0.0"
+    evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
+  integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+  dependencies:
+    cipher-base "^1.0.1"
+    des.js "^1.0.0"
+    inherits "^2.0.1"
+    safe-buffer "^5.1.2"
+
+browserify-rsa@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+  integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+  dependencies:
+    bn.js "^4.1.0"
+    randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+  integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=
+  dependencies:
+    bn.js "^4.1.1"
+    browserify-rsa "^4.0.0"
+    create-hash "^1.1.0"
+    create-hmac "^1.1.2"
+    elliptic "^6.0.0"
+    inherits "^2.0.1"
+    parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+  integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+  dependencies:
+    pako "~1.0.5"
+
+browserslist@^4.0.0, browserslist@^4.1.0, browserslist@^4.8.3, browserslist@^4.9.1:
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad"
+  integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A==
+  dependencies:
+    caniuse-lite "^1.0.30001035"
+    electron-to-chromium "^1.3.380"
+    node-releases "^1.1.52"
+    pkg-up "^3.1.0"
+
+buffer-equal@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
+  integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=
+
+buffer-from@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-xor@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+  integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
+builtin-status-codes@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+  integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bytes@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+cache-base@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+  integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+  dependencies:
+    collection-visit "^1.0.0"
+    component-emitter "^1.2.1"
+    get-value "^2.0.6"
+    has-value "^1.0.0"
+    isobject "^3.0.1"
+    set-value "^2.0.0"
+    to-object-path "^0.3.0"
+    union-value "^1.0.0"
+    unset-value "^1.0.0"
+
+call-me-maybe@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+  integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
+caller-callsite@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
+  integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+  dependencies:
+    callsites "^2.0.0"
+
+caller-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
+  integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+  dependencies:
+    caller-callsite "^2.0.0"
+
+callsites@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+  integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
+camelcase-css@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+  integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035:
+  version "1.0.30001036"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0"
+  integrity sha512-jU8CIFIj2oR7r4W+5AKcsvWNVIb6Q6OZE3UsrXrZBHFtreT4YgTeOJtTucp+zSedEpTi3L5wASSP0LYIE3if6w==
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+  integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+  dependencies:
+    ansi-styles "^2.2.1"
+    escape-string-regexp "^1.0.2"
+    has-ansi "^2.0.0"
+    strip-ansi "^3.0.0"
+    supports-color "^2.0.0"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+  integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chokidar@^2.1.5:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
+  integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
+  dependencies:
+    anymatch "^2.0.0"
+    async-each "^1.0.1"
+    braces "^2.3.2"
+    glob-parent "^3.1.0"
+    inherits "^2.0.3"
+    is-binary-path "^1.0.0"
+    is-glob "^4.0.0"
+    normalize-path "^3.0.0"
+    path-is-absolute "^1.0.0"
+    readdirp "^2.2.1"
+    upath "^1.1.1"
+  optionalDependencies:
+    fsevents "^1.2.7"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+  integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+class-utils@^0.3.5:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+  integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+  dependencies:
+    arr-union "^3.1.0"
+    define-property "^0.2.5"
+    isobject "^3.0.0"
+    static-extend "^0.1.1"
+
+cli-cursor@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+  integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+  dependencies:
+    restore-cursor "^2.0.0"
+
+cli-spinners@^1.1.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a"
+  integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==
+
+cliui@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+  dependencies:
+    string-width "^3.1.0"
+    strip-ansi "^5.2.0"
+    wrap-ansi "^5.1.0"
+
+clone@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
+
+clone@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
+coa@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
+  integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+  dependencies:
+    "@types/q" "^1.5.1"
+    chalk "^2.4.1"
+    q "^1.1.2"
+
+collection-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+  integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+  dependencies:
+    map-visit "^1.0.0"
+    object-visit "^1.0.0"
+
+color-convert@^1.9.0, color-convert@^1.9.1:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.5.2:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
+  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
+color@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
+  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  dependencies:
+    color-convert "^1.9.1"
+    color-string "^1.5.2"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+command-exists@^1.2.6:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.8.tgz#715acefdd1223b9c9b37110a149c6392c2852291"
+  integrity sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw==
+
+commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+component-emitter@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@~1.6.0:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+console-browserify@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
+  integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
+
+constants-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+  integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+convert-source-map@^1.5.1, convert-source-map@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+  dependencies:
+    safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+  integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js-compat@^3.6.2:
+  version "3.6.4"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17"
+  integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==
+  dependencies:
+    browserslist "^4.8.3"
+    semver "7.0.0"
+
+core-js@^2.4.0, core-js@^2.6.5:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
+  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cosmiconfig@^5.0.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
+  integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
+  dependencies:
+    import-fresh "^2.0.0"
+    is-directory "^0.3.1"
+    js-yaml "^3.13.1"
+    parse-json "^4.0.0"
+
+create-ecdh@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
+  integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==
+  dependencies:
+    bn.js "^4.1.0"
+    elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+  integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+  dependencies:
+    cipher-base "^1.0.1"
+    inherits "^2.0.1"
+    md5.js "^1.3.4"
+    ripemd160 "^2.0.1"
+    sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+  integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+  dependencies:
+    cipher-base "^1.0.3"
+    create-hash "^1.1.0"
+    inherits "^2.0.1"
+    ripemd160 "^2.0.0"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
+cross-spawn@^6.0.4:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+crypto-browserify@^3.11.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+  integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+  dependencies:
+    browserify-cipher "^1.0.0"
+    browserify-sign "^4.0.0"
+    create-ecdh "^4.0.0"
+    create-hash "^1.1.0"
+    create-hmac "^1.1.0"
+    diffie-hellman "^5.0.0"
+    inherits "^2.0.1"
+    pbkdf2 "^3.0.3"
+    public-encrypt "^4.0.0"
+    randombytes "^2.0.0"
+    randomfill "^1.0.3"
+
+css-color-names@0.0.4, css-color-names@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+  integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
+
+css-declaration-sorter@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22"
+  integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==
+  dependencies:
+    postcss "^7.0.1"
+    timsort "^0.3.0"
+
+css-modules-loader-core@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz#5908668294a1becd261ae0a4ce21b0b551f21d16"
+  integrity sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=
+  dependencies:
+    icss-replace-symbols "1.1.0"
+    postcss "6.0.1"
+    postcss-modules-extract-imports "1.1.0"
+    postcss-modules-local-by-default "1.2.0"
+    postcss-modules-scope "1.1.0"
+    postcss-modules-values "1.3.0"
+
+css-select-base-adapter@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
+  integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+
+css-select@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
+  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^3.2.1"
+    domutils "^1.7.0"
+    nth-check "^1.0.2"
+
+css-selector-tokenizer@^0.7.0:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz#11e5e27c9a48d90284f22d45061c303d7a25ad87"
+  integrity sha512-yj856NGuAymN6r8bn8/Jl46pR+OC3eEvAhfGYDUe7YPtTPAYrSSw4oAniZ9Y8T5B92hjhwTBLUen0/vKPxf6pw==
+  dependencies:
+    cssesc "^3.0.0"
+    fastparse "^1.1.2"
+    regexpu-core "^4.6.0"
+
+css-tree@1.0.0-alpha.37:
+  version "1.0.0-alpha.37"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
+  integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
+  dependencies:
+    mdn-data "2.0.4"
+    source-map "^0.6.1"
+
+css-unit-converter@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
+  integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
+
+css-what@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1"
+  integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
+  integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+  dependencies:
+    css-declaration-sorter "^4.0.1"
+    cssnano-util-raw-cache "^4.0.1"
+    postcss "^7.0.0"
+    postcss-calc "^7.0.1"
+    postcss-colormin "^4.0.3"
+    postcss-convert-values "^4.0.1"
+    postcss-discard-comments "^4.0.2"
+    postcss-discard-duplicates "^4.0.2"
+    postcss-discard-empty "^4.0.1"
+    postcss-discard-overridden "^4.0.1"
+    postcss-merge-longhand "^4.0.11"
+    postcss-merge-rules "^4.0.3"
+    postcss-minify-font-values "^4.0.2"
+    postcss-minify-gradients "^4.0.2"
+    postcss-minify-params "^4.0.2"
+    postcss-minify-selectors "^4.0.2"
+    postcss-normalize-charset "^4.0.1"
+    postcss-normalize-display-values "^4.0.2"
+    postcss-normalize-positions "^4.0.2"
+    postcss-normalize-repeat-style "^4.0.2"
+    postcss-normalize-string "^4.0.2"
+    postcss-normalize-timing-functions "^4.0.2"
+    postcss-normalize-unicode "^4.0.1"
+    postcss-normalize-url "^4.0.1"
+    postcss-normalize-whitespace "^4.0.2"
+    postcss-ordered-values "^4.1.2"
+    postcss-reduce-initial "^4.0.3"
+    postcss-reduce-transforms "^4.0.2"
+    postcss-svgo "^4.0.2"
+    postcss-unique-selectors "^4.0.1"
+
+cssnano-util-get-arguments@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f"
+  integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=
+
+cssnano-util-get-match@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d"
+  integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=
+
+cssnano-util-raw-cache@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282"
+  integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==
+  dependencies:
+    postcss "^7.0.0"
+
+cssnano-util-same-parent@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
+  integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
+
+cssnano@^4.0.0, cssnano@^4.1.10:
+  version "4.1.10"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
+  integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+  dependencies:
+    cosmiconfig "^5.0.0"
+    cssnano-preset-default "^4.0.7"
+    is-resolvable "^1.0.0"
+    postcss "^7.0.0"
+
+csso@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d"
+  integrity sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==
+  dependencies:
+    css-tree "1.0.0-alpha.37"
+
+cssom@0.3.x, cssom@^0.3.4:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+  integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^1.1.1:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
+  integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==
+  dependencies:
+    cssom "0.3.x"
+
+csstype@^2.2.0:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098"
+  integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+  dependencies:
+    assert-plus "^1.0.0"
+
+data-urls@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+  integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+  dependencies:
+    abab "^2.0.0"
+    whatwg-mimetype "^2.2.0"
+    whatwg-url "^7.0.0"
+
+deasync@^0.1.14:
+  version "0.1.19"
+  resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.19.tgz#e7ea89fcc9ad483367e8a48fe78f508ca86286e8"
+  integrity sha512-oh3MRktfnPlLysCPpBpKZZzb4cUC/p0aA3SyRGp15lN30juJBTo/CiD0d4fR+f1kBtUQoJj1NE9RPNWQ7BQ9Mg==
+  dependencies:
+    bindings "^1.5.0"
+    node-addon-api "^1.7.1"
+
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+  dependencies:
+    ms "^2.1.1"
+
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+defaults@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+  integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
+  dependencies:
+    clone "^1.0.2"
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+define-property@^0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+  integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+  dependencies:
+    is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+  integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+  dependencies:
+    is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+  integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+  dependencies:
+    is-descriptor "^1.0.2"
+    isobject "^3.0.1"
+
+defined@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+  integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+depd@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+des.js@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
+  integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
+  dependencies:
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+  integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detective@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
+  integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==
+  dependencies:
+    acorn-node "^1.6.1"
+    defined "^1.0.0"
+    minimist "^1.1.1"
+
+diffie-hellman@^5.0.0:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+  integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+  dependencies:
+    bn.js "^4.1.0"
+    miller-rabin "^4.0.0"
+    randombytes "^2.0.0"
+
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+domain-browser@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+  integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+domelementtype@1, domelementtype@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
+  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+
+domexception@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+  integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+  dependencies:
+    webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+  dependencies:
+    domelementtype "1"
+
+domutils@^1.5.1, domutils@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+dot-prop@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
+  integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
+  dependencies:
+    is-obj "^2.0.0"
+
+dotenv-expand@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
+  integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+
+dotenv@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
+  integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
+
+duplexer2@~0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
+  integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
+  dependencies:
+    readable-stream "^2.0.2"
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.380:
+  version "1.3.381"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.381.tgz#952678ff91a5f36175a3832358a6dd2de3bf62b7"
+  integrity sha512-JQBpVUr83l+QOqPQpj2SbOve1bBE4ACpmwcMNqWlZmfib7jccxJ02qFNichDpZ5LS4Zsqc985NIPKegBIZjK8Q==
+
+elliptic@^6.0.0:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
+  integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
+  dependencies:
+    bn.js "^4.4.0"
+    brorand "^1.0.1"
+    hash.js "^1.0.0"
+    hmac-drbg "^1.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.0"
+
+emoji-regex@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+entities@^1.1.1, entities@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
+  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+
+envinfo@^7.3.1:
+  version "7.5.0"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.0.tgz#91410bb6db262fb4f1409bd506e9ff57e91023f4"
+  integrity sha512-jDgnJaF/Btomk+m3PZDTTCb5XIIIX3zYItnCRfF73zVgvinLoRomuhi75Y4su0PtQxWz4v66XnLLckyvyJTOIQ==
+
+error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.2:
+  version "1.17.5"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
+  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^1.11.0, escodegen@^1.11.1:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457"
+  integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+escodegen@~1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
+  integrity sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==
+  dependencies:
+    esprima "^3.1.3"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+esprima@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+  integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
+
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+  integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+events@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
+  integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+  integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+  dependencies:
+    md5.js "^1.3.4"
+    safe-buffer "^5.1.1"
+
+expand-brackets@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+  integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+  dependencies:
+    debug "^2.3.3"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    posix-character-classes "^0.1.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extend-shallow@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+  dependencies:
+    is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+  integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+  dependencies:
+    assign-symbols "^1.0.0"
+    is-extendable "^1.0.1"
+
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+  integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+  dependencies:
+    array-unique "^0.3.2"
+    define-property "^1.0.0"
+    expand-brackets "^2.1.4"
+    extend-shallow "^2.0.1"
+    fragment-cache "^0.2.1"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+falafel@^2.1.0:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.2.4.tgz#b5d86c060c2412a43166243cb1bce44d1abd2819"
+  integrity sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ==
+  dependencies:
+    acorn "^7.1.1"
+    foreach "^2.0.5"
+    isarray "^2.0.1"
+    object-keys "^1.0.6"
+
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+
+fast-glob@^2.2.2:
+  version "2.2.7"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
+  integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
+  dependencies:
+    "@mrmlnc/readdir-enhanced" "^2.2.1"
+    "@nodelib/fs.stat" "^1.1.2"
+    glob-parent "^3.1.0"
+    is-glob "^4.0.0"
+    merge2 "^1.2.3"
+    micromatch "^3.1.10"
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fastparse@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
+  integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
+
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+filesize@^3.6.0:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+  integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+
+fill-range@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+  integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+    to-regex-range "^2.1.0"
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+for-in@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+foreach@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+  integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+  integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+  dependencies:
+    map-cache "^0.2.2"
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+fs-extra@^8.0.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+  version "1.2.12"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c"
+  integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==
+  dependencies:
+    bindings "^1.5.0"
+    nan "^2.12.1"
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+gensync@^1.0.0-beta.1:
+  version "1.0.0-beta.1"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+
+get-caller-file@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-port@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
+  integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=
+
+get-value@^2.0.3, get-value@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+  integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+  dependencies:
+    assert-plus "^1.0.0"
+
+glob-parent@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+  integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+  dependencies:
+    is-glob "^3.1.0"
+    path-dirname "^1.0.0"
+
+glob-to-regexp@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+  integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+
+glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    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"
+
+globals@^11.1.0:
+  version "11.12.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
+  integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+
+grapheme-breaker@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/grapheme-breaker/-/grapheme-breaker-0.3.2.tgz#5b9e6b78c3832452d2ba2bb1cb830f96276410ac"
+  integrity sha1-W55reMODJFLSuiuxy4MPlidkEKw=
+  dependencies:
+    brfs "^1.2.0"
+    unicode-trie "^0.3.1"
+
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+  integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+has-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+  integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-value@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+  integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+  dependencies:
+    get-value "^2.0.3"
+    has-values "^0.1.4"
+    isobject "^2.0.0"
+
+has-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+  integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+  dependencies:
+    get-value "^2.0.6"
+    has-values "^1.0.0"
+    isobject "^3.0.0"
+
+has-values@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+  integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+  integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+has@^1.0.0, has@^1.0.1, has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+hash-base@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+  integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+  integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+  dependencies:
+    inherits "^2.0.3"
+    minimalistic-assert "^1.0.1"
+
+hex-color-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
+  integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+
+history@^4.9.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
+  integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    loose-envify "^1.2.0"
+    resolve-pathname "^3.0.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+    value-equal "^1.0.1"
+
+hmac-drbg@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+  integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+  dependencies:
+    hash.js "^1.0.3"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.1"
+
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+  integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+  dependencies:
+    react-is "^16.7.0"
+
+hsl-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
+  integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=
+
+hsla-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
+  integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+
+html-comment-regex@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
+  integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+
+html-encoding-sniffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+  integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+  dependencies:
+    whatwg-encoding "^1.0.1"
+
+html-tags@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.2.0.tgz#c78de65b5663aa597989dd2b7ab49200d7e4db98"
+  integrity sha1-x43mW1Zjqll5id0rerSSANfk25g=
+
+htmlnano@^0.2.2:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-0.2.5.tgz#134fd9548c7cbe51c8508ce434a3f9488cff1b0b"
+  integrity sha512-X1iPSwXG/iF9bVs+/obt2n6F64uH0ETkA8zp7qFDmLW9/+A6ueHGeb/+qD67T21qUY22owZPMdawljN50ajkqA==
+  dependencies:
+    cssnano "^4.1.10"
+    normalize-html-whitespace "^1.0.0"
+    posthtml "^0.12.0"
+    posthtml-render "^1.1.5"
+    purgecss "^1.4.0"
+    svgo "^1.3.2"
+    terser "^4.3.9"
+    uncss "^0.17.2"
+
+htmlparser2@^3.9.2:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+  dependencies:
+    domelementtype "^1.3.1"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^3.1.1"
+
+http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+  integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+iconv-lite@0.4.24:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+  integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
+
+ieee754@^1.1.4:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+
+immer@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/immer/-/immer-4.0.2.tgz#9ff0fcdf88e06f92618a5978ceecb5884e633559"
+  integrity sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w==
+
+import-fresh@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
+  integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+  dependencies:
+    caller-path "^2.0.0"
+    resolve-from "^3.0.0"
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+  integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+inherits@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+invariant@^2.1.0, invariant@^2.2.2, invariant@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+  integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+  dependencies:
+    loose-envify "^1.0.0"
+
+is-absolute-url@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+  integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+
+is-absolute-url@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-accessor-descriptor@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+  integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+  integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+  dependencies:
+    kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-binary-path@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+  integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+  dependencies:
+    binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.1.4, is-callable@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+
+is-color-stop@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
+  integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=
+  dependencies:
+    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-data-descriptor@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+  integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+  integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+  dependencies:
+    kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-descriptor@^0.1.0:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+  integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+  dependencies:
+    is-accessor-descriptor "^0.1.6"
+    is-data-descriptor "^0.1.4"
+    kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+  integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+  dependencies:
+    is-accessor-descriptor "^1.0.0"
+    is-data-descriptor "^1.0.0"
+    kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+  integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+  dependencies:
+    is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-glob@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+  integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+  dependencies:
+    is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-html@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-html/-/is-html-1.1.0.tgz#e04f1c18d39485111396f9a0273eab51af218464"
+  integrity sha1-4E8cGNOUhRETlvmgJz6rUa8hhGQ=
+  dependencies:
+    html-tags "^1.0.0"
+
+is-number@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+  integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-obj@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
+  integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-regex@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+  dependencies:
+    has "^1.0.3"
+
+is-resolvable@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-svg@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
+  integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
+  dependencies:
+    html-comment-regex "^1.1.0"
+
+is-symbol@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+  dependencies:
+    has-symbols "^1.0.1"
+
+is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-url@^1.2.2:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
+  integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+
+is-windows@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+  integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+isarray@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isarray@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+  dependencies:
+    isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.10.0, js-yaml@^3.13.1:
+  version "3.13.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+  integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^14.1.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-14.1.0.tgz#916463b6094956b0a6c1782c94e380cd30e1981b"
+  integrity sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng==
+  dependencies:
+    abab "^2.0.0"
+    acorn "^6.0.4"
+    acorn-globals "^4.3.0"
+    array-equal "^1.0.0"
+    cssom "^0.3.4"
+    cssstyle "^1.1.1"
+    data-urls "^1.1.0"
+    domexception "^1.0.1"
+    escodegen "^1.11.0"
+    html-encoding-sniffer "^1.0.2"
+    nwsapi "^2.1.3"
+    parse5 "5.1.0"
+    pn "^1.1.0"
+    request "^2.88.0"
+    request-promise-native "^1.0.5"
+    saxes "^3.1.9"
+    symbol-tree "^3.2.2"
+    tough-cookie "^2.5.0"
+    w3c-hr-time "^1.0.1"
+    w3c-xmlserializer "^1.1.2"
+    webidl-conversions "^4.0.2"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^7.0.0"
+    ws "^6.1.2"
+    xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+  integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
+  integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+  integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+  integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levenary@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
+  integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
+  dependencies:
+    leven "^3.1.0"
+
+levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+lodash.clone@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
+  integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.sortby@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash.toarray@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
+  integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+log-symbols@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+  integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
+  dependencies:
+    chalk "^2.0.1"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+magic-string@^0.22.4:
+  version "0.22.5"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
+  integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==
+  dependencies:
+    vlq "^0.2.2"
+
+map-cache@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+  integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+  integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+  dependencies:
+    object-visit "^1.0.0"
+
+md5.js@^1.3.4:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+  integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+    safe-buffer "^5.1.2"
+
+mdn-data@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
+  integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+
+merge-source-map@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f"
+  integrity sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=
+  dependencies:
+    source-map "^0.5.6"
+
+merge2@^1.2.3:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
+  integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
+
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+  integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    braces "^2.3.1"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    extglob "^2.0.4"
+    fragment-cache "^0.2.1"
+    kind-of "^6.0.2"
+    nanomatch "^1.2.9"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.2"
+
+miller-rabin@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+  integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+  dependencies:
+    bn.js "^4.0.0"
+    brorand "^1.0.1"
+
+mime-db@1.43.0:
+  version "1.43.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
+  integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+  version "2.1.26"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
+  integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
+  dependencies:
+    mime-db "1.43.0"
+
+mime@1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mimic-fn@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+  integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+mini-create-react-context@^0.3.0:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
+  integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
+  dependencies:
+    "@babel/runtime" "^7.4.0"
+    gud "^1.0.0"
+    tiny-warning "^1.0.2"
+
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+  integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+mixin-deep@^1.2.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+  dependencies:
+    for-in "^1.0.2"
+    is-extendable "^1.0.1"
+
+mkdirp@^0.5.1, mkdirp@~0.5.1:
+  version "0.5.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512"
+  integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==
+  dependencies:
+    minimist "^1.2.5"
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+  integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+ms@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nan@^2.12.1:
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
+  integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+
+nanomatch@^1.2.9:
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+  integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    fragment-cache "^0.2.1"
+    is-windows "^1.0.2"
+    kind-of "^6.0.2"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-addon-api@^1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.1.tgz#cf813cd69bb8d9100f6bdca6755fc268f54ac492"
+  integrity sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==
+
+node-emoji@^1.8.1:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
+  integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==
+  dependencies:
+    lodash.toarray "^4.4.0"
+
+node-forge@^0.7.1:
+  version "0.7.6"
+  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
+  integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
+
+node-libs-browser@^2.0.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
+  integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
+  dependencies:
+    assert "^1.1.1"
+    browserify-zlib "^0.2.0"
+    buffer "^4.3.0"
+    console-browserify "^1.1.0"
+    constants-browserify "^1.0.0"
+    crypto-browserify "^3.11.0"
+    domain-browser "^1.1.1"
+    events "^3.0.0"
+    https-browserify "^1.0.0"
+    os-browserify "^0.3.0"
+    path-browserify "0.0.1"
+    process "^0.11.10"
+    punycode "^1.2.4"
+    querystring-es3 "^0.2.0"
+    readable-stream "^2.3.3"
+    stream-browserify "^2.0.1"
+    stream-http "^2.7.2"
+    string_decoder "^1.0.0"
+    timers-browserify "^2.0.4"
+    tty-browserify "0.0.0"
+    url "^0.11.0"
+    util "^0.11.0"
+    vm-browserify "^1.0.1"
+
+node-releases@^1.1.52:
+  version "1.1.52"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9"
+  integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ==
+  dependencies:
+    semver "^6.3.0"
+
+normalize-html-whitespace@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-html-whitespace/-/normalize-html-whitespace-1.0.0.tgz#5e3c8e192f1b06c3b9eee4b7e7f28854c7601e34"
+  integrity sha512-9ui7CGtOOlehQu0t/OhhlmDyc71mKVlv+4vF+me4iZLPrNtRL2xoquEdfZxasC/bdQi/Hr3iTrpyRKIG+ocabA==
+
+normalize-path@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+  dependencies:
+    remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+  integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+normalize-url@^3.0.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
+  integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
+
+normalize.css@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+  integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
+
+nth-check@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+  dependencies:
+    boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+  integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
+nwsapi@^2.1.3:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+  integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+  dependencies:
+    copy-descriptor "^0.1.0"
+    define-property "^0.2.5"
+    kind-of "^3.0.3"
+
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-inspect@~1.4.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.4.1.tgz#37ffb10e71adaf3748d05f713b4c9452f402cbc4"
+  integrity sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-visit@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+  integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+  dependencies:
+    isobject "^3.0.0"
+
+object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+object.pick@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+  integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+  dependencies:
+    isobject "^3.0.1"
+
+object.values@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+on-finished@~2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+  dependencies:
+    ee-first "1.1.1"
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+  integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+  dependencies:
+    mimic-fn "^1.0.0"
+
+opn@^5.1.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+  integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+  dependencies:
+    is-wsl "^1.1.0"
+
+optionator@^0.8.1:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+ora@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ora/-/ora-2.1.0.tgz#6caf2830eb924941861ec53a173799e008b51e5b"
+  integrity sha512-hNNlAd3gfv/iPmsNxYoAPLvxg7HuPozww7fFonMZvL84tP6Ox5igfk5j/+a9rtJJwqMgKK+JgWsAQik5o0HTLA==
+  dependencies:
+    chalk "^2.3.1"
+    cli-cursor "^2.1.0"
+    cli-spinners "^1.1.0"
+    log-symbols "^2.2.0"
+    strip-ansi "^4.0.0"
+    wcwidth "^1.0.1"
+
+os-browserify@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+  integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+p-limit@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
+  integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+pako@^0.2.5:
+  version "0.2.9"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+  integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=
+
+pako@~1.0.5:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+parcel-bundler@^1.12.4:
+  version "1.12.4"
+  resolved "https://registry.yarnpkg.com/parcel-bundler/-/parcel-bundler-1.12.4.tgz#31223f4ab4d00323a109fce28d5e46775409a9ee"
+  integrity sha512-G+iZGGiPEXcRzw0fiRxWYCKxdt/F7l9a0xkiU4XbcVRJCSlBnioWEwJMutOCCpoQmaQtjB4RBHDGIHN85AIhLQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/core" "^7.4.4"
+    "@babel/generator" "^7.4.4"
+    "@babel/parser" "^7.4.4"
+    "@babel/plugin-transform-flow-strip-types" "^7.4.4"
+    "@babel/plugin-transform-modules-commonjs" "^7.4.4"
+    "@babel/plugin-transform-react-jsx" "^7.0.0"
+    "@babel/preset-env" "^7.4.4"
+    "@babel/runtime" "^7.4.4"
+    "@babel/template" "^7.4.4"
+    "@babel/traverse" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    "@iarna/toml" "^2.2.0"
+    "@parcel/fs" "^1.11.0"
+    "@parcel/logger" "^1.11.1"
+    "@parcel/utils" "^1.11.0"
+    "@parcel/watcher" "^1.12.1"
+    "@parcel/workers" "^1.11.0"
+    ansi-to-html "^0.6.4"
+    babylon-walk "^1.0.2"
+    browserslist "^4.1.0"
+    chalk "^2.1.0"
+    clone "^2.1.1"
+    command-exists "^1.2.6"
+    commander "^2.11.0"
+    core-js "^2.6.5"
+    cross-spawn "^6.0.4"
+    css-modules-loader-core "^1.1.0"
+    cssnano "^4.0.0"
+    deasync "^0.1.14"
+    dotenv "^5.0.0"
+    dotenv-expand "^5.1.0"
+    envinfo "^7.3.1"
+    fast-glob "^2.2.2"
+    filesize "^3.6.0"
+    get-port "^3.2.0"
+    htmlnano "^0.2.2"
+    is-glob "^4.0.0"
+    is-url "^1.2.2"
+    js-yaml "^3.10.0"
+    json5 "^1.0.1"
+    micromatch "^3.0.4"
+    mkdirp "^0.5.1"
+    node-forge "^0.7.1"
+    node-libs-browser "^2.0.0"
+    opn "^5.1.0"
+    postcss "^7.0.11"
+    postcss-value-parser "^3.3.1"
+    posthtml "^0.11.2"
+    posthtml-parser "^0.4.0"
+    posthtml-render "^1.1.3"
+    resolve "^1.4.0"
+    semver "^5.4.1"
+    serialize-to-js "^3.0.0"
+    serve-static "^1.12.4"
+    source-map "0.6.1"
+    terser "^3.7.3"
+    v8-compile-cache "^2.0.0"
+    ws "^5.1.1"
+
+parse-asn1@^5.0.0:
+  version "5.1.5"
+  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e"
+  integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==
+  dependencies:
+    asn1.js "^4.0.0"
+    browserify-aes "^1.0.0"
+    create-hash "^1.1.0"
+    evp_bytestokey "^1.0.0"
+    pbkdf2 "^3.0.3"
+    safe-buffer "^5.1.1"
+
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
+parse5@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+  integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+
+parseurl@~1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+  integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
+  integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
+
+path-dirname@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+  integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@^1.7.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+  integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+  dependencies:
+    isarray "0.0.1"
+
+pbkdf2@^3.0.3:
+  version "3.0.17"
+  resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
+  integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==
+  dependencies:
+    create-hash "^1.1.2"
+    create-hmac "^1.1.4"
+    ripemd160 "^2.0.1"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+physical-cpu-count@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz#18de2f97e4bf7a9551ad7511942b5496f7aba660"
+  integrity sha1-GN4vl+S/epVRrXURlCtUlverpmA=
+
+pkg-up@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
+  integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
+  dependencies:
+    find-up "^3.0.0"
+
+pn@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+  integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+posix-character-classes@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss-calc@^7.0.1:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.2.tgz#504efcd008ca0273120568b0792b16cdcde8aac1"
+  integrity sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==
+  dependencies:
+    postcss "^7.0.27"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381"
+  integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==
+  dependencies:
+    browserslist "^4.0.0"
+    color "^3.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-convert-values@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
+  integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-discard-comments@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033"
+  integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-duplicates@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb"
+  integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-empty@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765"
+  integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-overridden@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57"
+  integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-functions@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-functions/-/postcss-functions-3.0.0.tgz#0e94d01444700a481de20de4d55fb2640564250e"
+  integrity sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=
+  dependencies:
+    glob "^7.1.2"
+    object-assign "^4.1.1"
+    postcss "^6.0.9"
+    postcss-value-parser "^3.3.0"
+
+postcss-js@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9"
+  integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==
+  dependencies:
+    camelcase-css "^2.0.1"
+    postcss "^7.0.18"
+
+postcss-merge-longhand@^4.0.11:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24"
+  integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==
+  dependencies:
+    css-color-names "0.0.4"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    stylehacks "^4.0.0"
+
+postcss-merge-rules@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650"
+  integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-api "^3.0.0"
+    cssnano-util-same-parent "^4.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+    vendors "^1.0.0"
+
+postcss-minify-font-values@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6"
+  integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-minify-gradients@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471"
+  integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    is-color-stop "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-minify-params@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874"
+  integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    browserslist "^4.0.0"
+    cssnano-util-get-arguments "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8"
+  integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+
+postcss-modules-extract-imports@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz#b614c9720be6816eaee35fb3a5faa1dba6a05ddb"
+  integrity sha1-thTJcgvmgW6u41+zpfqh26agXds=
+  dependencies:
+    postcss "^6.0.1"
+
+postcss-modules-local-by-default@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
+  integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=
+  dependencies:
+    css-selector-tokenizer "^0.7.0"
+    postcss "^6.0.1"
+
+postcss-modules-scope@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
+  integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A=
+  dependencies:
+    css-selector-tokenizer "^0.7.0"
+    postcss "^6.0.1"
+
+postcss-modules-values@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
+  integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=
+  dependencies:
+    icss-replace-symbols "^1.1.0"
+    postcss "^6.0.1"
+
+postcss-nested@^4.1.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.1.tgz#4bc2e5b35e3b1e481ff81e23b700da7f82a8b248"
+  integrity sha512-AMayXX8tS0HCp4O4lolp4ygj9wBn32DJWXvG6gCv+ZvJrEa00GUxJcJEEzMh87BIe6FrWdYkpR2cuyqHKrxmXw==
+  dependencies:
+    postcss "^7.0.21"
+    postcss-selector-parser "^6.0.2"
+
+postcss-normalize-charset@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4"
+  integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-normalize-display-values@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a"
+  integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-positions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f"
+  integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-repeat-style@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c"
+  integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-string@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c"
+  integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==
+  dependencies:
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-timing-functions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9"
+  integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-unicode@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb"
+  integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==
+  dependencies:
+    browserslist "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-url@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1"
+  integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==
+  dependencies:
+    is-absolute-url "^2.0.0"
+    normalize-url "^3.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-whitespace@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
+  integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-ordered-values@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee"
+  integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-reduce-initial@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df"
+  integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-api "^3.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+
+postcss-reduce-transforms@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29"
+  integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-selector-parser@6.0.2, postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-selector-parser@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270"
+  integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==
+  dependencies:
+    dot-prop "^5.2.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-svgo@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
+  integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+  dependencies:
+    is-svg "^3.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    svgo "^1.0.0"
+
+postcss-unique-selectors@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac"
+  integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    postcss "^7.0.0"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+  integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
+  integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
+
+postcss@6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.1.tgz#000dbd1f8eef217aa368b9a212c5fc40b2a8f3f2"
+  integrity sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=
+  dependencies:
+    chalk "^1.1.3"
+    source-map "^0.5.6"
+    supports-color "^3.2.3"
+
+postcss@^6.0.1, postcss@^6.0.9:
+  version "6.0.23"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
+  integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
+  dependencies:
+    chalk "^2.4.1"
+    source-map "^0.6.1"
+    supports-color "^5.4.0"
+
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27:
+  version "7.0.27"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
+  integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
+posthtml-parser@^0.4.0, posthtml-parser@^0.4.1:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.4.2.tgz#a132bbdf0cd4bc199d34f322f5c1599385d7c6c1"
+  integrity sha512-BUIorsYJTvS9UhXxPTzupIztOMVNPa/HtAm9KHni9z6qEfiJ1bpOBL5DfUOL9XAc3XkLIEzBzpph+Zbm4AdRAg==
+  dependencies:
+    htmlparser2 "^3.9.2"
+
+posthtml-render@^1.1.3, posthtml-render@^1.1.5:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.0.tgz#3df0c800a8bbb95af583a94748520469477addf4"
+  integrity sha512-dQB+hoAKDtnI94RZm/wxBUH9My8OJcXd0uhWmGh2c7tVtQ85A+OS3yCN3LNbFtPz3bViwBJXAeoi+CBGMXM0DA==
+
+posthtml@^0.11.2:
+  version "0.11.6"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.11.6.tgz#e349d51af7929d0683b9d8c3abd8166beecc90a8"
+  integrity sha512-C2hrAPzmRdpuL3iH0TDdQ6XCc9M7Dcc3zEW5BLerY65G4tWWszwv6nG/ksi6ul5i2mx22ubdljgktXCtNkydkw==
+  dependencies:
+    posthtml-parser "^0.4.1"
+    posthtml-render "^1.1.5"
+
+posthtml@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.12.0.tgz#6e2a2fcd774eaed1a419a95c5cc3a92b676a40a6"
+  integrity sha512-aNUEP/SfKUXAt+ghG51LC5MmafChBZeslVe/SSdfKIgLGUVRE68mrMF4V8XbH07ZifM91tCSuxY3eHIFLlecQw==
+  dependencies:
+    posthtml-parser "^0.4.1"
+    posthtml-render "^1.1.5"
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+prettier@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.2.tgz#1ba8f3eb92231e769b7fcd7cb73ae1b6b74ade08"
+  integrity sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==
+
+pretty-hrtime@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
+  integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
+
+private@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+  integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+prop-types@^15.6.2, prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
+psl@^1.1.28:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
+  integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
+
+public-encrypt@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
+  integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+  dependencies:
+    bn.js "^4.1.0"
+    browserify-rsa "^4.0.0"
+    create-hash "^1.1.0"
+    parse-asn1 "^5.0.0"
+    randombytes "^2.0.1"
+    safe-buffer "^5.1.2"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+purgecss@^1.4.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-1.4.2.tgz#67ab50cb4f5c163fcefde56002467c974e577f41"
+  integrity sha512-hkOreFTgiyMHMmC2BxzdIw5DuC6kxAbP/gGOGd3MEsF3+5m69rIvUEPaxrnoUtfODTFKe9hcXjGwC6jcjoyhOw==
+  dependencies:
+    glob "^7.1.3"
+    postcss "^7.0.14"
+    postcss-selector-parser "^6.0.0"
+    yargs "^14.0.0"
+
+q@^1.1.2:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qs@~6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring-es3@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+  integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+quote-stream@^1.0.1, quote-stream@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2"
+  integrity sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=
+  dependencies:
+    buffer-equal "0.0.1"
+    minimist "^1.1.3"
+    through2 "^2.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+  integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+  dependencies:
+    randombytes "^2.0.5"
+    safe-buffer "^5.1.0"
+
+range-parser@~1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+react-dom@^16.13.1:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
+  integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    scheduler "^0.19.1"
+
+react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-redux@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
+  integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    hoist-non-react-statics "^3.3.0"
+    loose-envify "^1.4.0"
+    prop-types "^15.7.2"
+    react-is "^16.9.0"
+
+react-router-dom@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
+  integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    history "^4.9.0"
+    loose-envify "^1.3.1"
+    prop-types "^15.6.2"
+    react-router "5.1.2"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+
+react-router@5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
+  integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    history "^4.9.0"
+    hoist-non-react-statics "^3.1.0"
+    loose-envify "^1.3.1"
+    mini-create-react-context "^0.3.0"
+    path-to-regexp "^1.7.0"
+    prop-types "^15.6.2"
+    react-is "^16.6.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+
+react@^16.13.1:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
+  integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+
+readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@^3.1.1:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+readdirp@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+  integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+  dependencies:
+    graceful-fs "^4.1.11"
+    micromatch "^3.1.10"
+    readable-stream "^2.0.2"
+
+reduce-css-calc@^2.1.6:
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
+  integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==
+  dependencies:
+    css-unit-converter "^1.1.1"
+    postcss-value-parser "^3.3.0"
+
+redux-devtools-extension@^2.13.8:
+  version "2.13.8"
+  resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
+  integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
+
+redux-immutable-state-invariant@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1"
+  integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==
+  dependencies:
+    invariant "^2.1.0"
+    json-stringify-safe "^5.0.1"
+
+redux-thunk@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
+  integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
+
+redux@^4.0.0:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+  integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+  dependencies:
+    loose-envify "^1.4.0"
+    symbol-observable "^1.2.0"
+
+regenerate-unicode-properties@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
+  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
+  dependencies:
+    regenerate "^1.4.0"
+
+regenerate@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+  integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+
+regenerator-runtime@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+  integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
+regenerator-runtime@^0.13.4:
+  version "0.13.5"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
+  integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+
+regenerator-transform@^0.14.2:
+  version "0.14.4"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
+  integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==
+  dependencies:
+    "@babel/runtime" "^7.8.4"
+    private "^0.1.8"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+  integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+  dependencies:
+    extend-shallow "^3.0.2"
+    safe-regex "^1.1.0"
+
+regexpu-core@^4.6.0, regexpu-core@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
+  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+  dependencies:
+    regenerate "^1.4.0"
+    regenerate-unicode-properties "^8.2.0"
+    regjsgen "^0.5.1"
+    regjsparser "^0.6.4"
+    unicode-match-property-ecmascript "^1.0.4"
+    unicode-match-property-value-ecmascript "^1.2.0"
+
+regjsgen@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
+  integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
+
+regjsparser@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
+  integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
+  dependencies:
+    jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+  integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+  integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+  dependencies:
+    lodash "^4.17.15"
+
+request-promise-native@^1.0.5:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
+  integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+  dependencies:
+    request-promise-core "1.1.3"
+    stealthy-require "^1.1.1"
+    tough-cookie "^2.3.3"
+
+request@^2.88.0:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+reselect@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+  integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
+resolve-from@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+  integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-pathname@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
+  integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
+
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@^1.1.5, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.4.0:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
+  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+  dependencies:
+    path-parse "^1.0.6"
+
+restore-cursor@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+  integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+  dependencies:
+    onetime "^2.0.0"
+    signal-exit "^3.0.2"
+
+ret@~0.1.10:
+  version "0.1.15"
+  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+  integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rgb-regex@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
+  integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE=
+
+rgba-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
+  integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+
+rimraf@^2.6.2:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+  integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+  integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+  dependencies:
+    ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@~1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+saxes@^3.1.9:
+  version "3.1.11"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
+  integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==
+  dependencies:
+    xmlchars "^2.1.1"
+
+scheduler@^0.19.1:
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
+  integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
+semver@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
+  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
+
+semver@^5.4.1, semver@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+send@0.17.1:
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
+  integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+  dependencies:
+    debug "2.6.9"
+    depd "~1.1.2"
+    destroy "~1.0.4"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "~1.7.2"
+    mime "1.6.0"
+    ms "2.1.1"
+    on-finished "~2.3.0"
+    range-parser "~1.2.1"
+    statuses "~1.5.0"
+
+serialize-to-js@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/serialize-to-js/-/serialize-to-js-3.1.1.tgz#b3e77d0568ee4a60bfe66287f991e104d3a1a4ac"
+  integrity sha512-F+NGU0UHMBO4Q965tjw7rvieNVjlH6Lqi2emq/Lc9LUURYJbiCzmpi4Cy1OOjjVPtxu0c+NE85LU6968Wko5ZA==
+
+serve-static@^1.12.4:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
+  integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.17.1"
+
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^2.0.0, set-value@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+  integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+shallow-copy@~0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
+  integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+signal-exit@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+  dependencies:
+    is-arrayish "^0.3.1"
+
+snapdragon-node@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+  integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+  dependencies:
+    define-property "^1.0.0"
+    isobject "^3.0.0"
+    snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+  integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+  dependencies:
+    kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+  integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+  dependencies:
+    base "^0.11.1"
+    debug "^2.2.0"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    map-cache "^0.2.2"
+    source-map "^0.5.6"
+    source-map-resolve "^0.5.0"
+    use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+  dependencies:
+    atob "^2.1.2"
+    decode-uri-component "^0.2.0"
+    resolve-url "^0.2.1"
+    source-map-url "^0.4.0"
+    urix "^0.1.0"
+
+source-map-support@~0.5.10, source-map-support@~0.5.12:
+  version "0.5.16"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
+  integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+  integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@^0.5.0, source-map@^0.5.6:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+  integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+split-string@^3.0.1, split-string@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+  integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+  dependencies:
+    extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
+    ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
+    jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
+    tweetnacl "~0.14.0"
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+static-eval@^2.0.0:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.5.tgz#f0782e66999c4b3651cda99d9ce59c507d188f71"
+  integrity sha512-nNbV6LbGtMBgv7e9LFkt5JV8RVlRsyJrphfAt9tOtBBW/SfnzZDf2KnS72an8e434A+9e/BmJuTxeGPvrAK7KA==
+  dependencies:
+    escodegen "^1.11.1"
+
+static-extend@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+  integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+  dependencies:
+    define-property "^0.2.5"
+    object-copy "^0.1.0"
+
+static-module@^2.2.0:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/static-module/-/static-module-2.2.5.tgz#bd40abceae33da6b7afb84a0e4329ff8852bfbbf"
+  integrity sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==
+  dependencies:
+    concat-stream "~1.6.0"
+    convert-source-map "^1.5.1"
+    duplexer2 "~0.1.4"
+    escodegen "~1.9.0"
+    falafel "^2.1.0"
+    has "^1.0.1"
+    magic-string "^0.22.4"
+    merge-source-map "1.0.4"
+    object-inspect "~1.4.0"
+    quote-stream "~1.0.2"
+    readable-stream "~2.3.3"
+    shallow-copy "~0.0.1"
+    static-eval "^2.0.0"
+    through2 "~2.0.3"
+
+"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+stream-browserify@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
+  integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
+  dependencies:
+    inherits "~2.0.1"
+    readable-stream "^2.0.2"
+
+stream-http@^2.7.2:
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
+  integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+  dependencies:
+    builtin-status-codes "^3.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.3.6"
+    to-arraybuffer "^1.0.0"
+    xtend "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+  dependencies:
+    emoji-regex "^7.0.1"
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^5.1.0"
+
+string.prototype.trimleft@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
+  integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==
+  dependencies:
+    define-properties "^1.1.3"
+    function-bind "^1.1.1"
+
+string.prototype.trimright@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
+  integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==
+  dependencies:
+    define-properties "^1.1.3"
+    function-bind "^1.1.1"
+
+string_decoder@^1.0.0, string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+  dependencies:
+    ansi-regex "^4.1.0"
+
+stylehacks@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
+  integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==
+  dependencies:
+    browserslist "^4.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+
+supports-color@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+  integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+
+supports-color@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+  integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=
+  dependencies:
+    has-flag "^1.0.0"
+
+supports-color@^5.3.0, supports-color@^5.4.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+  integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^1.0.0, svgo@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
+  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
+  dependencies:
+    chalk "^2.4.1"
+    coa "^2.0.2"
+    css-select "^2.0.0"
+    css-select-base-adapter "^0.1.1"
+    css-tree "1.0.0-alpha.37"
+    csso "^4.0.2"
+    js-yaml "^3.13.1"
+    mkdirp "~0.5.1"
+    object.values "^1.1.0"
+    sax "~1.2.4"
+    stable "^0.1.8"
+    unquote "~1.1.1"
+    util.promisify "~1.0.0"
+
+symbol-observable@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+  integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
+symbol-tree@^3.2.2:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+  integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+tailwindcss@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.2.0.tgz#5df317cebac4f3131f275d258a39da1ba3a0f291"
+  integrity sha512-CKvY0ytB3ze5qvynG7qv4XSpQtFNGPbu9pUn8qFdkqgD8Yo/vGss8mhzbqls44YCXTl4G62p3qVZBj45qrd6FQ==
+  dependencies:
+    autoprefixer "^9.4.5"
+    bytes "^3.0.0"
+    chalk "^3.0.0"
+    detective "^5.2.0"
+    fs-extra "^8.0.0"
+    lodash "^4.17.15"
+    node-emoji "^1.8.1"
+    normalize.css "^8.0.1"
+    postcss "^7.0.11"
+    postcss-functions "^3.0.0"
+    postcss-js "^2.0.0"
+    postcss-nested "^4.1.1"
+    postcss-selector-parser "^6.0.0"
+    pretty-hrtime "^1.0.3"
+    reduce-css-calc "^2.1.6"
+    resolve "^1.14.2"
+
+terser@^3.7.3:
+  version "3.17.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
+  integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==
+  dependencies:
+    commander "^2.19.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.10"
+
+terser@^4.3.9:
+  version "4.6.7"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72"
+  integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.12"
+
+through2@^2.0.0, through2@~2.0.3:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+  integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+  dependencies:
+    readable-stream "~2.3.6"
+    xtend "~4.0.1"
+
+timers-browserify@^2.0.4:
+  version "2.0.11"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
+  integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+  dependencies:
+    setimmediate "^1.0.4"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+tiny-inflate@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
+  integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
+tiny-invariant@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
+  integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+
+tiny-warning@^1.0.0, tiny-warning@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
+to-arraybuffer@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+  integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+to-fast-properties@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+  integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
+
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+  integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+  dependencies:
+    kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+  integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+  dependencies:
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+  integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+  dependencies:
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    regex-not "^1.0.2"
+    safe-regex "^1.1.0"
+
+toidentifier@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tr46@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+  integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+  dependencies:
+    punycode "^2.1.0"
+
+tty-browserify@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+  integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+typescript@^3.8.3:
+  version "3.8.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
+  integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
+
+uncss@^0.17.2:
+  version "0.17.3"
+  resolved "https://registry.yarnpkg.com/uncss/-/uncss-0.17.3.tgz#50fc1eb4ed573ffff763458d801cd86e4d69ea11"
+  integrity sha512-ksdDWl81YWvF/X14fOSw4iu8tESDHFIeyKIeDrK6GEVTQvqJc1WlOEXqostNwOCi3qAj++4EaLsdAgPmUbEyog==
+  dependencies:
+    commander "^2.20.0"
+    glob "^7.1.4"
+    is-absolute-url "^3.0.1"
+    is-html "^1.1.0"
+    jsdom "^14.1.0"
+    lodash "^4.17.15"
+    postcss "^7.0.17"
+    postcss-selector-parser "6.0.2"
+    request "^2.88.0"
+
+unicode-canonical-property-names-ecmascript@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
+  integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+
+unicode-match-property-ecmascript@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
+  integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+  dependencies:
+    unicode-canonical-property-names-ecmascript "^1.0.4"
+    unicode-property-aliases-ecmascript "^1.0.4"
+
+unicode-match-property-value-ecmascript@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
+  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
+
+unicode-property-aliases-ecmascript@^1.0.4:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
+  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
+
+unicode-trie@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-0.3.1.tgz#d671dddd89101a08bac37b6a5161010602052085"
+  integrity sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=
+  dependencies:
+    pako "^0.2.5"
+    tiny-inflate "^1.0.0"
+
+union-value@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+  dependencies:
+    arr-union "^3.1.0"
+    get-value "^2.0.6"
+    is-extendable "^0.1.1"
+    set-value "^2.0.1"
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unquote@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
+  integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+
+unset-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+  integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+  dependencies:
+    has-value "^0.3.1"
+    isobject "^3.0.0"
+
+upath@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
+  integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
+
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  dependencies:
+    punycode "^2.1.0"
+
+urix@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+  integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+  integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+use@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
+
+util@0.10.3:
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+  integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+  dependencies:
+    inherits "2.0.1"
+
+util@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
+  integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
+  dependencies:
+    inherits "2.0.3"
+
+uuid@^3.3.2:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+v8-compile-cache@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
+  integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
+
+value-equal@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
+  integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
+
+vendors@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+vlq@^0.2.2:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
+  integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
+
+vm-browserify@^1.0.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+  integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+
+w3c-hr-time@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+  integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+  dependencies:
+    browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
+  integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
+  dependencies:
+    domexception "^1.0.1"
+    webidl-conversions "^4.0.2"
+    xml-name-validator "^3.0.0"
+
+wcwidth@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+  integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
+  dependencies:
+    defaults "^1.0.3"
+
+webidl-conversions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+  integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+  dependencies:
+    iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+  integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wrap-ansi@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+  dependencies:
+    ansi-styles "^3.2.0"
+    string-width "^3.0.0"
+    strip-ansi "^5.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+ws@^5.1.1:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+  integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+  dependencies:
+    async-limiter "~1.0.0"
+
+ws@^6.1.2:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
+  integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+  dependencies:
+    async-limiter "~1.0.0"
+
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+  integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xmlchars@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
+xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+  integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yargs-parser@^15.0.1:
+  version "15.0.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3"
+  integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs@^14.0.0:
+  version "14.2.3"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414"
+  integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==
+  dependencies:
+    cliui "^5.0.0"
+    decamelize "^1.2.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^15.0.1"
diff --git a/users/wpcarro/buildHaskell/default.nix b/users/wpcarro/buildHaskell/default.nix
new file mode 100644
index 0000000000..2f0fd9e1c2
--- /dev/null
+++ b/users/wpcarro/buildHaskell/default.nix
@@ -0,0 +1,35 @@
+{ pkgs, ... }:
+
+{
+  # Create a nix-shell for Haskell development.
+  shell = { deps }:
+    let
+      ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: deps hpkgs);
+    in
+    pkgs.mkShell {
+      buildInputs = [ ghc ];
+    };
+
+  # Build a Haskell executable. This assumes a project directory with a
+  # top-level Main.hs.
+  # - `name`: You can find the result at ./result/$name
+  # - `srcs`: Will be passed to `srcs` field of `pkgs.stdenv.mkDerivation`.
+  # - `deps`: A function that accepts `hpkgs` and returns a list of Haskell
+  # - `ghcExtensions`: A list of strings representing the language extensions to
+  #   use.
+  program = { name, srcs, deps, ghcExtensions }:
+    let
+      ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: deps hpkgs);
+    in
+    pkgs.stdenv.mkDerivation {
+      name = name;
+      buildInputs = [ ];
+      srcs = srcs;
+      buildPhase = ''
+        ${ghc}/bin/ghc -Wall Main.hs ${pkgs.lib.concatMapStrings (x: "-X${x} ") ghcExtensions}
+      '';
+      installPhase = ''
+        mkdir -p $out && mv Main $out/${name}
+      '';
+    };
+}
diff --git a/users/wpcarro/ci/pipelines/post-receive.nix b/users/wpcarro/ci/pipelines/post-receive.nix
new file mode 100644
index 0000000000..09b8990e13
--- /dev/null
+++ b/users/wpcarro/ci/pipelines/post-receive.nix
@@ -0,0 +1,14 @@
+{ pkgs, depot, ... }:
+
+let
+  inherit (builtins) path toJSON;
+
+  pipeline.steps = [
+    {
+      key = "lint-secrets";
+      command = "${pkgs.git-secrets}/bin/git-secrets --scan-history";
+      label = ":broom: lint secrets";
+    }
+  ];
+in
+pkgs.writeText "pipeline.yaml" (toJSON pipeline)
diff --git a/users/wpcarro/ci/secret-patterns.txt b/users/wpcarro/ci/secret-patterns.txt
new file mode 100644
index 0000000000..cbf58a1e74
--- /dev/null
+++ b/users/wpcarro/ci/secret-patterns.txt
@@ -0,0 +1,9 @@
+(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}
+("|')?(AWS|aws|Aws)?_?(SECRET|secret|Secret)?_?(ACCESS|access|Access)?_?(KEY|key|Key)("|')?\s*(:|=>|=)\s*("|')?[A-Za-z0-9/\+=]{40}("|')?
+("|')?(AWS|aws|Aws)?_?(ACCOUNT|account|Account)_?(ID|id|Id)?("|')?\s*(:|=>|=)\s*("|')?[0-9]{4}\-?[0-9]{4}\-?[0-9]{4}("|')?
+AIza[0-9A-Za-z_-]{35}
+[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com
+(^|[^0-9A-Za-z/+])1/[0-9A-Za-z_-]{43}
+(^|[^0-9A-Za-z/+])1/[0-9A-Za-z_-]{64}
+ya29\.[0-9A-Za-z_-]+
+(sk|pk)_(test|live)_[a-zA-Z0-9]{99}
diff --git a/users/wpcarro/common.nix b/users/wpcarro/common.nix
new file mode 100644
index 0000000000..9a8dd344d8
--- /dev/null
+++ b/users/wpcarro/common.nix
@@ -0,0 +1,71 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.users) wpcarro;
+in
+{
+  programs = {
+    fish.enable = true;
+
+    gnupg.agent.enable = true;
+
+    ssh = {
+      startAgent = true;
+      extraConfig = ''
+        AddKeysToAgent yes
+      '';
+    };
+
+    git = {
+      enable = true;
+      config = {
+        user.name = "William Carroll";
+        user.email = "wpcarro@gmail.com";
+      };
+    };
+  };
+
+  services = {
+    locate.enable = true;
+
+    depot.automatic-gc = {
+      enable = true;
+      interval = "1 hour";
+      diskThreshold = 16; # GiB
+      maxFreed = 10; # GiB
+      preserveGenerations = "14d";
+    };
+  };
+
+  # Command-line tools I commonly used and want available on most (or all) of my
+  # machines.
+  shell-utils = with pkgs; [
+    bat
+    dig
+    direnv
+    diskus
+    emacs
+    exa
+    fd
+    fzf
+    git
+    gnupg
+    htop
+    jq
+    mkpasswd
+    nmap
+    pass
+    python3
+    rink
+    ripgrep
+    tldr
+    tokei
+    tree
+    vim
+    whois
+    # TODO(wpcarro): Debug this failing build.
+    # wpcarro.tools.simple_vim
+    xclip
+    zip
+  ];
+}
diff --git a/users/wpcarro/configs/.config/nixpkgs/config.nix b/users/wpcarro/configs/.config/nixpkgs/config.nix
new file mode 100644
index 0000000000..1dd1750ae0
--- /dev/null
+++ b/users/wpcarro/configs/.config/nixpkgs/config.nix
@@ -0,0 +1,3 @@
+{
+  allowUnfree = true;
+}
diff --git a/users/wpcarro/configs/.config/nvim/init.vim b/users/wpcarro/configs/.config/nvim/init.vim
new file mode 100644
index 0000000000..57cfe7ea6a
--- /dev/null
+++ b/users/wpcarro/configs/.config/nvim/init.vim
@@ -0,0 +1,668 @@
+" -- BEGIN: Vundle config --
+set nocompatible              " be iMproved, required
+filetype off                  " required
+
+" set the runtime path to include Vundle and initialize
+" share Vundle between vim and neovim
+set rtp+=~/.vim/bundle/Vundle.vim
+set rtp+=~/.config/nvim/bundle/Vundle.vim
+call vundle#begin()
+" alternatively, pass a path where Vundle should install plugins
+"call vundle#begin('~/some/path/here')
+
+" let Vundle manage Vundle, required
+Plugin 'VundleVim/Vundle.vim'
+
+" Rust IDE features
+Plugin 'racer-rust/vim-racer'
+
+set hidden
+let g:racer_experimental_completer = 1
+autocmd FileType rust nmap         gd <Plug>(rust-def)
+autocmd FileType rust nmap         gs <Plug>(rust-def-split)
+autocmd FileType rust nmap         gx <Plug>(rust-def-vertical)
+autocmd FileType rust nmap <leader>gd <Plug>(rust-doc)
+
+Plugin 'xolox/vim-misc'
+
+" The following are examples of different formats supported.
+" Keep Plugin commands between vundle#begin/end.
+
+" Displays git information in airline.
+Plugin 'tpope/vim-fugitive'
+
+" easier file navigation
+Plugin 'tpope/vim-vinegar'
+
+" Displays git-tracked C*UD ops within gutter.
+Plugin 'airblade/vim-gitgutter'
+
+" Fuzzy-finder
+Plugin 'kien/ctrlp.vim'
+
+" Grep file contents
+Plugin 'mileszs/ack.vim'
+
+" Syntax and other light-weight suppor for a variety of languages
+Plugin 'sheerun/vim-polyglot'
+
+" Themes
+Plugin 'deviantfero/wpgtk.vim'
+Plugin 'rainglow/vim'
+
+
+" Executes shell commands and pipes output into new Vim buffer.
+Plugin 'sjl/clam.vim'
+
+" Multiple cursors for simultaneous edits.
+" NOTE: use <C-n> to run miltiple cursors not <C-d>
+Plugin 'terryma/vim-multiple-cursors'
+
+" Visualize buffers
+Plugin 'vim-airline/vim-airline'
+Plugin 'vim-airline/vim-airline-themes'
+
+" Visually align assignments
+Plugin 'godlygeek/tabular'
+
+" Visually Highlight and comment code.
+Plugin 'tpope/vim-commentary'
+
+" Macros for quotes, parens, etc.
+Plugin 'tpope/vim-surround'
+
+" Allows Plugins to be repeated with `.` character
+Plugin 'tpope/vim-repeat'
+
+" Pairs of mappings
+Plugin 'tpope/vim-unimpaired'
+
+" LISPs support
+Plugin 'guns/vim-sexp'
+Plugin 'tpope/vim-sexp-mappings-for-regular-people'
+let g:sexp_enable_insert_mode_mappings = 0
+let g:sexp_filetypes = ''
+
+" Seamlessly navigate Vim and Tmux with similar bindings.
+Plugin 'christoomey/vim-tmux-navigator'
+
+" Async `:make` for code linting etc.
+Plugin 'neomake/neomake'
+
+" Better buffer mgt than CtrlP
+Plugin 'yegappan/mru'
+
+Plugin 'zanglg/nova.vim'
+
+" Emulates Emacs's Helm Swoop search
+Plugin 'pelodelfuego/vim-swoop'
+
+" Transparent encryption + decryption
+Plugin 'jamessan/vim-gnupg'
+
+" Javascript auto-formatting
+" Plugin 'prettier/vim-prettier', {
+"   \ 'do': 'yarn install',
+  " \ 'for': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'graphql', 'markdown'] }
+
+" Support Org mode
+Plugin 'jceb/vim-orgmode'
+
+" Autocompletion
+Plugin 'junegunn/fzf'
+
+" Text objects made easy
+Plugin 'kana/vim-textobj-user'
+
+" Elixir text objects
+Plugin 'andyl/vim-textobj-elixir'
+
+" Making HTML editing faster
+Plugin 'mattn/emmet-vim'
+
+" Snippets for all languages
+Plugin 'honza/vim-snippets'
+
+" Automatic bracket insertion
+Plugin 'jiangmiao/auto-pairs'
+
+" Linting & error warnings
+Plugin 'vim-syntastic/syntastic'
+
+" Angular.js support
+Plugin 'burnettk/vim-angular'
+
+" Asynchronous Linting Engine
+Plugin 'w0rp/ale'
+
+call vundle#end()            " required
+filetype plugin indent on    " required
+" Put your non-Plugin stuff after this line
+" -- END: Vundle config --
+
+" Changes <leader> to <space> character.
+let mapleader = " "
+
+
+" Highlight column width
+set textwidth=80
+set colorcolumn=+0
+
+" autoreload a file when it changes on disk
+set autoread
+
+" default to case-insensitive searching
+set ignorecase
+
+" JSX configuration
+let g:jsx_ext_required = 0
+
+
+autocmd FileType reason nnoremap <buffer> gd :call LanguageClient_textDocument_definition()<CR>
+autocmd FileType reason nnoremap <buffer> gf :call LanguageClient_textDocument_formatting()<CR>
+autocmd FileType reason nnoremap <buffer> gh :call LanguageClient_textDocument_hover()<CR>
+autocmd FileType reason nnoremap <buffer> gr :call LanguageClient_textDocument_rename()<CR>
+
+" Replace <CR> with G for faster navigation
+nnoremap <CR> G
+onoremap <CR> G
+vnoremap <CR> G
+
+" Mirror ZLE KBD
+inoremap <M-'> :echo "Working"<CR>
+
+" Syntastic configuration
+set statusline+=%#warningmsg#
+set statusline+=%{SyntasticStatuslineFlag()}
+set statusline+=%*
+
+let g:syntastic_always_populate_loc_list = 1
+let g:syntastic_auto_loc_list = 1
+let g:syntastic_check_on_open = 1
+let g:syntastic_check_on_wq = 0
+" let g:syntastic_javascript_checkers = ['eslint']
+let g:syntastic_javascript_eslint_generic = 1
+" this is a hack to prevent a false negative
+" https://github.com/vim-syntastic/syntastic/issues/1692
+" let g:syntastic_javascript_eslint_exec = '/bin/ls'
+" let g:syntastic_javascript_eslint_exe = 'npx eslint'
+" let g:syntastic_javascript_eslint_args = '-f compact'
+
+" javascript autocompletion
+" autocmd FileType javascript setlocal omnifunc=javascriptcomplete#CompleteJS
+" autocmd FileType javascript nnoremap <buffer> gf :Prettier<CR>
+
+" Maximize the current window
+" Similar to Tmux mapping alt-z in my tmux.conf
+nnoremap t% :tab sp<CR>
+
+" Allow C-g to act like C-c the way it does in Emacs
+cnoremap <C-g> <C-c>
+
+" Prettier configuration
+" let g:prettier#exec_cmd_async = 1
+" force Prettier to run on files even without the @format pragma
+" let g:prettier#autoformat = 0
+
+
+" Basic settings
+" Thin cursor on INSERT mode
+if has('nvim')
+  let $NVIM_TUI_ENABLE_CURSOR_SHAPE = 1
+endif
+
+set number
+set nowrap
+set tabstop=2
+set expandtab
+set shiftwidth=2
+set background=dark
+
+syntax enable
+colorscheme peacock
+
+" Vim in terminal cannot have a different font from the one set within your
+" terminal. However, this setting will set the font for the GUI version.
+if has('gui_running')
+  set guifont=Operator\ Mono:h12
+endif
+
+if has('termguicolors')
+  set termguicolors
+endif
+
+if &term =~# '^screen'
+  let &t_8f = "\<Esc>[38;2;%lu;%lu;%lum"
+  let &t_8b = "\<Esc>[48;2;%lu;%lu;%lum"
+endif
+
+set history=1000
+set undolevels=1000
+
+set t_Co=255
+
+" Support italics
+highlight Comment cterm=italic
+
+
+" quickly edit popular configuration files
+nnoremap <leader>ev :vsplit $MYVIMRC<CR>
+nnoremap <leader>ee :vsplit ~/.emacs.d/init.el<CR>
+nnoremap <leader>ez :vsplit ~/.zshrc<CR>
+nnoremap <leader>ea :vsplit ~/aliases.zsh<CR>
+nnoremap <leader>ef :vsplit ~/functions.zsh<CR>
+nnoremap <leader>el :vsplit ~/variables.zsh<CR>
+nnoremap <leader>ex :vsplit ~/.Xresources<CR>
+
+" quickly source your vimrc
+nnoremap <leader>sv :source $MYVIMRC<CR>
+
+" quickly edit your snippets
+nnoremap <leader>es :vsplit<CR>:edit ~/.vim/bundle/vim-snippets/snippets/reason.snippets<CR>
+
+
+" Auto resize window splits
+autocmd VimResized * wincmd =
+
+
+" Neomake Settings
+autocmd! BufWritePost * Neomake
+
+" Elixir linting
+let g:neomake_elixir_credo_maker = {
+      \ 'exe': 'mix',
+      \ 'args': ['credo', 'list', '%:p', '--format=oneline'],
+      \ 'errorformat':
+      \   '%W[F] %. %f:%l:%c %m,' .
+      \   '%W[F] %. %f:%l %m,' .
+      \   '%W[R] %. %f:%l:%c %m,' .
+      \   '%W[R] %. %f:%l %m,' .
+      \   '%I[C] %. %f:%l:%c %m,' .
+      \   '%I[C] %. %f:%l %m,' .
+      \   '%-Z%.%#'
+      \ }
+
+
+let g:neomake_elixir_enabled_makers = ['mix', 'credo']
+
+augroup my_error_signs
+  au!
+  autocmd ColorScheme * hi NeomakeErrorSign ctermfg=203 guifg=#ff5f5f
+  autocmd ColorScheme * hi NeomakeWarningSign ctermfg=209 guifg=#ffaf00
+  autocmd ColorScheme * hi NeomakeInfoSign ctermfg=183 guifg=#dfafff
+  autocmd ColorScheme * hi NeomakeMessageSign ctermfg=27 guifg=#0087ff
+augroup END
+
+
+" templates
+if has("autocmd")
+  autocmd BufNewFile *.c  0r ~/.config/nvim/templates/boilerplate.c
+  autocmd BufNewFile *.rs 0r ~/.config/nvim/templates/boilerplate.rs
+endif
+
+let g:neomake_error_sign = {
+            \ 'text': '>>',
+            \ 'texthl': 'NeoMakeErrorSign',
+            \ }
+
+let g:neomake_warning_sign = {
+            \ 'text': '>>',
+            \ 'texthl': 'NeoMakeWarningSign',
+            \ }
+
+let g:neomake_info_sign = {
+            \ 'text': '>>',
+            \ 'texthl': 'NeoMakeInfoSign',
+            \ }
+
+let g:neomake_message_sign = {
+            \ 'text': '>>',
+            \ 'texthl': 'NeoMakeMessageSign',
+            \ }
+
+function! <SID>LocationPrevious()
+  try
+    lprev
+  catch /^Vim\%((\a\+)\)\=:E553/
+    llast
+  endtry
+endfunction
+
+function! <SID>LocationNext()
+  try
+    lnext
+  catch /^Vim\%((\a\+)\)\=:E553/
+    lfirst
+  endtry
+endfunction
+
+nnoremap <Leader>[ :call <SID>LocationPrevious()<CR>
+nnoremap <Leader>] :call <SID>LocationNext()<CR>
+
+
+" Alchemist settings
+let g:alchemist#elixir_erlang_src = '/usr/local/share/src'
+
+
+" Airline Settings
+" Enables the list of buffers.
+let g:airline#extensions#tabline#enabled = 0
+
+" Buffer numbers alongside files
+let g:airline#extensions#tabline#buffer_nr_show = 0
+
+" Shows the filename only.
+let g:airline#extensions#tabline#fnamemod = ':t'
+
+" Allow glyphs in airline
+let g:airline_powerline_fonts = 1
+
+" Change Airline theme
+let g:airline_theme = 'hybrid'
+
+
+" Vim-Swoop Settings
+" Edits colorscheme
+let g:swoopHighlight = ["hi! link SwoopBufferLineHi Warning", "hi! link SwoopPatternHi Error"]
+
+
+" Jump to buffers.
+nmap <F1> :1b<CR>
+nmap <F2> :2b<CR>
+nmap <F3> :3b<CR>
+nmap <F4> :4b<CR>
+nmap <F5> :5b<CR>
+nmap <F6> :6b<CR>
+nmap <F7> :7b<CR>
+nmap <F8> :8b<CR>
+nmap <F9> :9b<CR>
+
+
+" It's the twenty-first century...no swaps.
+set noswapfile
+
+
+" Allow visual tab completion in command mode
+set wildmenu
+
+
+" Show Vim commands as they're being input.
+set showcmd
+
+
+" Code folding
+" set foldmethod=indent
+" set foldnestmax=10
+" set nofoldenable
+" set foldlevel=4
+
+
+" emulate ci" and ci' behavior
+nnoremap ci( f(%ci(
+nnoremap ci[ f[%ci[
+
+
+" extend functionality of <C-e> & <C-y> scrolling
+nnoremap <C-e> <C-e>j
+vnoremap <C-e> <C-e>j
+nnoremap <C-y> <C-y>k
+vnoremap <C-y> <C-y>k
+
+
+" Opens all folds within the buffer
+" nnoremap ZZ zR
+
+" Closes all folds within the buffer
+" nnoremap zz zM
+
+" Opens all folds beneath the cursor
+" NOTE: j is the character to go down
+" nnoremap zJ zO
+
+" Opens single fold beneath the cursor
+" NOTE: j is the character to go down
+" nnoremap zj zo
+
+" Opens single fold beneath the cursor
+" NOTE: k is the character to go down
+" nnoremap zK zC
+
+" Opens single fold beneath the cursor
+" NOTE: k is the character to go down
+" nnoremap zk zc
+
+
+" Save shortcut
+nnoremap <C-s> :w<CR>
+
+
+" Switch to MRU'd buffer
+nnoremap <leader><leader> <C-^>
+
+
+" Alternative MRU to CtrlP MRU
+nnoremap <leader>b :MRU<CR>
+
+
+" Supports mouse interaction.
+set mouse=a
+
+
+" Highlights matches during a search.
+set hlsearch
+
+" Clear highlight
+noremap <silent> <leader>h :nohlsearch<bar>:echo<CR>
+
+
+" backspace settings
+set backspace=2
+set backspace=indent,eol,start
+
+
+" Javascript specific variables
+let g:javascript_plugin_jsdoc = 1
+
+" GlobalListchars
+set list
+set listchars=tab:··,trail:·,nbsp:·
+
+
+" Keeps everything concealed at all times. Even when cursor is on the word.
+set conceallevel=1
+set concealcursor=nvic
+
+
+" map jk to <Esc>
+inoremap jk <Esc>
+
+
+" Hybrid mode for Vim
+inoremap <C-a> <Esc>I
+inoremap <C-e> <Esc>A
+
+inoremap <M-b> <S-Left>
+inoremap <M-f> <S-Right>
+
+inoremap <C-b> <Left>
+inoremap <C-f> <Right>
+inoremap <C-p> <Up>
+inoremap <C-n> <Down>
+
+" temporarily disable <C-p> in normal mode so it doesn't attempt to index all of
+" Google3.
+nnoremap <C-p> :echo "You are attempting to index all of Google3. Aborting..."<CR>
+
+" tab maintenence
+nnoremap <C-t> :tabnew<CR>
+nnoremap <C-w> :tabclose<CR>
+nnoremap <Tab> :tabnext<CR>
+nnoremap <S-Tab> :tabprevious<CR>
+
+" Manage Vertical and Horizontal splits
+nnoremap sl <Esc>:vs<CR><C-w>l
+nnoremap sh <Esc>:vs<CR>
+nnoremap sj <Esc>:sp<CR><C-w>j
+nnoremap sk <Esc>:sp<CR>
+
+
+" Delete (i.e. "close") the currently opened buffer
+" TODO: unless it's a split window, which should be :q
+nnoremap <leader>q :bdelete<CR>
+
+
+" Set CtrlP runtime path
+set runtimepath^=~/.vim/bundle/ctrlp.vim
+
+
+" Pane movement
+let g:tmux_navigator_no_mappings = 1
+
+nnoremap <silent> <M-h> :TmuxNavigateLeft<CR>
+nnoremap <silent> <M-j> :TmuxNavigateDown<CR>
+nnoremap <silent> <M-k> :TmuxNavigateUp<CR>
+nnoremap <silent> <M-l> :TmuxNavigateRight<CR>
+nnoremap <silent> <M-q> :q<CR>
+
+" make Y do what is intuitive given:
+"   D: deletes until EOL
+"   C: changes until EOL
+"   Y: (should) yank until EOL
+nnoremap Y y$
+
+
+" scrolling and maintaing mouse position
+" nnoremap <C-j> j<C-e>
+" nnoremap <C-k> k<C-y>
+
+
+" remap redo key that is eclipsed by `rotate` currently
+nnoremap U :redo<CR>
+
+
+" Define highlighting groups
+" NOTE: The ANSII aliases for colors will change when iTerm2 settings are
+" changed.
+highlight InterestingWord1 ctermbg=Magenta ctermfg=Black
+highlight InterestingWord2 ctermbg=Blue ctermfg=Black
+
+" h1 highlighting
+nnoremap <silent> <leader>1 :execute '2match InterestingWord1 /\<<c-r><c-w>\>/'<CR>
+nnoremap <silent> <leader>x1 :execute '2match none'<CR>
+vnoremap <silent> <leader>1 :execute '2match InterestingWord1 /\<<c-r><c-w>\>/'<CR>
+
+" h2 highlighting
+nnoremap <silent> <leader>2 :execute '3match InterestingWord2 /\<<c-r><c-w>\>/'<CR>
+nnoremap <silent> <leader>x2 :execute '3match none'<CR>
+
+"clear all highlighted groups
+nnoremap <silent> <leader>xx :execute '2match none'<CR> :execute '3match none'<CR> hh
+
+
+" pasteboard copy & paste
+set clipboard+=unnamedplus
+
+
+" Manage 80 char line limits
+highlight OverLength1 ctermbg=Magenta ctermfg=Black
+highlight OverLength2 ctermbg=LightMagenta ctermfg=Black
+highlight OverLength3 ctermbg=White ctermfg=Black
+" match OverLength3 /\%81v.\+/
+match OverLength2 /\%91v.\+/
+" match OverLength3 /\%101v.\+/
+
+nnoremap <leader>w :w<CR>
+
+
+" Resize split to 10,20,...,100 chars
+" Uncomment the next lines for support at those sizes.
+" These bindings interfere with the highlight groups, however.
+" Increases the width of a vertical split.
+" nnoremap <leader>1 :vertical resize 10<CR>
+" nnoremap <leader>2 :vertical resize 20<CR>
+nnoremap <leader>3 :vertical resize 30<CR>
+nnoremap <leader>4 :vertical resize 40<CR>
+nnoremap <leader>5 :vertical resize 50<CR>
+nnoremap <leader>6 :vertical resize 60<CR>
+nnoremap <leader>7 :vertical resize 70<CR>
+nnoremap <leader>8 :vertical resize 80<CR>
+nnoremap <leader>9 :vertical resize 90<CR>
+nnoremap <leader>0 :vertical resize 100<CR>
+
+
+" Increases the height of a horizontal split.
+nnoremap <leader>v1 :resize 5<CR>
+nnoremap <leader>v2 :resize 10<CR>
+nnoremap <leader>v3 :resize 15<CR>
+nnoremap <leader>v4 :resize 20<CR>
+nnoremap <leader>v5 :resize 25<CR>
+nnoremap <leader>v6 :resize 30<CR>
+nnoremap <leader>v7 :resize 35<CR>
+nnoremap <leader>v8 :resize 40<CR>
+nnoremap <leader>v9 :resize 45<CR>
+nnoremap <leader>v0 :resize 50<CR>
+
+
+" BOL and EOL
+nnoremap H ^
+vnoremap H ^
+nnoremap L $
+vnoremap L $
+
+
+" Search for visually selected text
+vnoremap // y/<C-r>"<CR>N
+
+
+" trim trailing whitespace on save
+" Are there any file type where I wouldn't want this?
+autocmd BufWritePre *.{js,py,tpl,less,html,ex,exs,txt,hs,java,rs,ml} :%s/\s\+$//e
+
+
+" Use .gitignore file to populate Ctrl-P
+let g:ctrlp_user_command = ['.git', 'cd %s && git ls-files . -co --exclude-standard', 'find %s -type f']
+
+
+" Ignores dirs and files
+let g:ctrlp_custom_ignore = {
+  \ 'dir':  'node_modules',
+  \ 'file': '\v\.(exe|dll|png|jpg|jpeg)$'
+\}
+
+
+" WIP: Run elixir tests on that line
+" TODO: only register binding in *.exs? file extensions
+nnoremap <leader>t :call ExTestToggle()<CR>
+
+
+" Jumps from an Elixir module file to an Elixir test file.
+fun! ExTestToggle()
+  if expand('%:e') == "ex"
+
+    let test_file_name = expand('%:t:r') . "_test.exs"
+    let test_file_dir = substitute(expand('%:p:h'), "/lib/", "/test/", "")
+    let full_test_path = join([test_file_dir, test_file_name], "/")
+
+    e `=full_test_path`
+
+  elseif match(expand('%:t'), "_test.exs") != -1
+
+    let test_file_name = expand('%:t:r')
+    let offset_amt = strlen(test_file_name) - strlen("_test")
+    let module_file_name = strpart(test_file_name, 0, offset_amt) . ".ex"
+    let module_file_dir = substitute(expand('%:p:h'), "/test/", "/lib/", "")
+    let full_module_path = join([module_file_dir, module_file_name], "/")
+
+    e `=full_module_path`
+
+  endif
+endfun
+
+
+" Creates intermediate directories and file to match current buffer's filepath
+fun! CreateNonExistingDirsAndFile()
+  ! echo "Creating directory..." && mkdir -p %:p:h && echo "Created directory." && echo "Creating file..." && touch %:t:p && echo "Created file."
+
+  " Write the buffer to the recently created file.
+  w
+endfun
diff --git a/users/wpcarro/configs/.config/nvim/templates/boilerplate.c b/users/wpcarro/configs/.config/nvim/templates/boilerplate.c
new file mode 100644
index 0000000000..949743d725
--- /dev/null
+++ b/users/wpcarro/configs/.config/nvim/templates/boilerplate.c
@@ -0,0 +1,6 @@
+#include <stdio.h>
+
+int main() {
+  printf("Hello, world!");
+  return 0;
+}
diff --git a/users/wpcarro/configs/.config/nvim/templates/boilerplate.rs b/users/wpcarro/configs/.config/nvim/templates/boilerplate.rs
new file mode 100644
index 0000000000..c83adbc69f
--- /dev/null
+++ b/users/wpcarro/configs/.config/nvim/templates/boilerplate.rs
@@ -0,0 +1,5 @@
+fn main() {
+    // The statements here will be executed when the compiled binary is called.
+
+    println!("Hello, world!");
+}
diff --git a/users/wpcarro/configs/.config/systemd/user/clipmenud.service b/users/wpcarro/configs/.config/systemd/user/clipmenud.service
new file mode 100644
index 0000000000..fac317f3f0
--- /dev/null
+++ b/users/wpcarro/configs/.config/systemd/user/clipmenud.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=Clipmenu daemon
+
+[Service]
+ExecStart=clipmenud
+Restart=always
+RestartSec=500ms
+Environment=DISPLAY=:0
+
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+ProtectControlGroups=yes
+ProtectKernelTunables=yes
+RestrictAddressFamilies=
+RestrictRealtime=yes
+
+[Install]
+WantedBy=default.target
diff --git a/users/wpcarro/configs/.config/systemd/user/default.target.wants/clipmenud.service b/users/wpcarro/configs/.config/systemd/user/default.target.wants/clipmenud.service
new file mode 120000
index 0000000000..387f2023d2
--- /dev/null
+++ b/users/wpcarro/configs/.config/systemd/user/default.target.wants/clipmenud.service
@@ -0,0 +1 @@
+/usr/local/google/home/wpcarro/.config/systemd/user/clipmenud.service
\ No newline at end of file
diff --git a/users/wpcarro/configs/.config/systemd/user/lieer-google.service b/users/wpcarro/configs/.config/systemd/user/lieer-google.service
new file mode 100644
index 0000000000..2f79ed6cca
--- /dev/null
+++ b/users/wpcarro/configs/.config/systemd/user/lieer-google.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Lieer sync for account 'google'
+
+[Service]
+Type=oneshot
+ExecStart=/nix/store/n6c4pr4fyrsjfksspkapb7yqc6fzl166-corp-lieer/bin/gmi sync
+WorkingDirectory=%h/mail/account.google
diff --git a/users/wpcarro/configs/.config/systemd/user/lieer-google.timer b/users/wpcarro/configs/.config/systemd/user/lieer-google.timer
new file mode 100644
index 0000000000..a073da25ea
--- /dev/null
+++ b/users/wpcarro/configs/.config/systemd/user/lieer-google.timer
@@ -0,0 +1,9 @@
+[Unit]
+Description=Run lieer sync for account 'google'
+
+[Timer]
+OnActiveSec=1
+OnUnitActiveSec=120
+
+[Install]
+WantedBy=timers.target
diff --git a/users/wpcarro/configs/.config/systemd/user/timers.target.wants/lieer-google.timer b/users/wpcarro/configs/.config/systemd/user/timers.target.wants/lieer-google.timer
new file mode 120000
index 0000000000..e9f2cab3bc
--- /dev/null
+++ b/users/wpcarro/configs/.config/systemd/user/timers.target.wants/lieer-google.timer
@@ -0,0 +1 @@
+/usr/local/google/home/wpcarro/.config/systemd/user/lieer-google.timer
\ No newline at end of file
diff --git a/users/wpcarro/configs/.gitconfig b/users/wpcarro/configs/.gitconfig
new file mode 100644
index 0000000000..a036b13081
--- /dev/null
+++ b/users/wpcarro/configs/.gitconfig
@@ -0,0 +1,3 @@
+[user]
+	name = "William Carroll"
+	email = "wpcarro@gmail.com"
diff --git a/users/wpcarro/configs/.gnupg/crls.d/DIR.txt b/users/wpcarro/configs/.gnupg/crls.d/DIR.txt
new file mode 100644
index 0000000000..2a29a47b8d
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/crls.d/DIR.txt
@@ -0,0 +1 @@
+v:1:
diff --git a/users/wpcarro/configs/.gnupg/export.sh b/users/wpcarro/configs/.gnupg/export.sh
new file mode 100755
index 0000000000..31def2beb1
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/export.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Run this script to export all the information required to transport your GPG
+# information.
+# Usage: ./export.sh
+# TODO: run this periodically as a job.
+
+output="$(pwd)/export.zip"
+destination="$(mktemp -d)"
+
+function cleanup() {
+  rm -rf "${destination}"
+}
+trap cleanup EXIT
+
+gpg --armor --export >"${destination}/public.asc"
+gpg --armor --export-secret-keys >"${destination}/secret.asc"
+gpg --armor --export-ownertrust >"${destination}/ownertrust.txt"
+
+# Strangely enough this appears to be the only way to create a zip of a
+# directory that doesn't contain the (noisy) full paths of each item from the
+# source filesystem. (i.e. -j doesn't cooperate with -r)
+pushd "${destination}"
+zip -r "${output}" ./*
+popd
+
+echo "$(realpath ${output})"
diff --git a/users/wpcarro/configs/.gnupg/exported/ownertrust.txt b/users/wpcarro/configs/.gnupg/exported/ownertrust.txt
new file mode 100644
index 0000000000..79b727914f
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/exported/ownertrust.txt
@@ -0,0 +1,3 @@
+# List of assigned trustvalues, created Mon 29 Jul 2019 15:01:24 BST
+# (Use "gpg --import-ownertrust" to restore them)
+7E87921AAC9C514E9341C4F1C7A53CC58D3B1F8C:6:
diff --git a/users/wpcarro/configs/.gnupg/exported/public.asc b/users/wpcarro/configs/.gnupg/exported/public.asc
new file mode 100644
index 0000000000..8b5547f4c3
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/exported/public.asc
@@ -0,0 +1,225 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBFk52cEBEADW2uF8AjpGxbd/yrtCguVzl7fWCCo/vZYGTomoHy7K3ru7bQEN
+upIBj1ElcsLGxbNLqdEqb17blTOUpaLLxWhEUw38rTpRyepBH0y2u5INDiw9GlpU
+uXKnkvaAF2f7DJH24jQA2mLI5Jcgc2M0Kzmuh1Q1foAy3frORBnYlrd9TlSPU7Og
+Jj0T20jtZIsIORov2TFC2cEpwa+9jHkNaBK2Bdg5c0SyI2r3TSJq+L7X8Vkf3Hmb
+NEWJj286+ElcFP/FyVgRCtSJPjBg/MF0ucukm96cel5qYfK5RkMA/HCyv6xI8iNn
+eZj8sJnozDY4rMxFwNkTxIjwH9cCTW0CR9FMsc1wlIe6Zx0ic8Fu7PZCS5MjM8cQ
+LnruOunVnb0YodQ+cLde6FlKu7kNUlLJrH5NnuFxjWPxzC63u+/K6CcRV9ilWe5r
+so/ImtNfGO1JiCvisYeOqlTYBKceQgjvu5tZtLJoGxH0UzoJARLCLRwyHN8dGqgp
+STRd0Ze6LtzYLG0uuedyPNXDKci7GyrVdAmxVIo+eLA1a7n3YCcluGKZlM0IBWx8
+fTKJ16ASTXpK7Hqr3XSf5V7tUcwxiFFtxh5C7kXglyd4QI6Jk6Xp8HlPLvYXNSNj
+VYRMHi/ueFI92jlt3kCodD26btgIEfD3e3JxKfHhOtwSoA2i1Hr43qdvtQARAQAB
+tCNXaWxsaWFtIENhcnJvbGwgPHdwY2Fycm9AZ21haWwuY29tPokCVAQTAQgAPhYh
+BH6HkhqsnFFOk0HE8celPMWNOx+MBQJZOdnBAhsDBQkJZgGABQsJCAcCBhUICQoL
+AgQWAgMBAh4BAheAAAoJEMelPMWNOx+MGm8P/RYqv5mnneRbyJ6CgisYn2iIBQvz
++rmpdGDfkFqsd2YqDoGjzEJLVkan+I1oLnKSv5QJqPw1gG7fSv6X7Trov9J+Cma3
+h1bSn2BBiq8L9paWTILYmsrBe7kU9bQjNKFu0qjfvPqkGX6HXO6c81N00Qgie574
+MCByWgPtJTcbPLJodIxu6+aibwNBc3XInL/d0ZbXLs8Fc0+z2/dO7cmzAdE77d5Q
+QaG9fGyztiYlZoUS9g3xT4ZulpPqs9zFa04fPvOXWVl+RQjZOYVYW/T8aVRnXohz
+3y7tnxOWs8cmCFd91DDR099DZXstAesWllPsdSld18aMjeM2XrzuaWVDYktaraiY
+RUdz6ZRPcaCpsIA2RHn++xEg+3q6QRz1J4bsbqEOKys+KlQO9uIgPMIkiMaLFoe5
+nu63XI4EMezrti86ETUxFPFL107P9KZ0gitjUXP0KSbnGQ7jt5FmuZgSAkbCSlis
+Ulm8PZ/cmKj9OZysezDKkXFGtyskAELkpToIy48GtyEVIMk+CXcgNydUXDiLnWI8
+VwgmR1Q+hClLYMPvrk7OR6zK8txXsglJItCRUF5fmAn4Q7loh6i/BCfQpHdylO7G
+nn7BOEJ0CJ8Hrr4Y785dtswAX8hWMIuzS4mxAHCjqkkfsOObBfLi+XpkZ6lDkfrQ
+jAt2KuAjQR58dHDSiQEzBBABCAAdFiEEDxGpiYeei7v9weI2RO9bXoYcCacFAlmu
+vnAACgkQRO9bXoYcCacQ4AgAmjDO+8Sd8d+cezwIjZgq1nPPb+/K0KTGsALe7jdF
+MDOKwwPKd75mKbAVyJRu8CMEfgFW04YKbkeVp9bLeD2lpMYsIgpNYy5bU6DNCgi5
+QO501sTqeaWc/rlm7Ng5AlF8GIK6FagrPS31eUexxJ62VFozi3EiibKYepgeIUHR
+3ukw6+PWBkvOYSQjZ0Uc08nci8UsewDQaQvuDABR+6WbLDYX6PuyUEzV7MPbyzME
+QOvGuYpgcAq5gGB6NNe9zFQK0xAQob1UhDlaa1p8VSZyH/RLnyYCdlq0Bmf95PNq
+eE8YkmqFCPKNoWwzOa6pJOk/Y2mXhJm/eD4Avmlo122LN4kCMwQQAQgAHRYhBBP5
+Ly1Y78tmaShOtNywW0z4mvxmBQJZvs4vAAoJENywW0z4mvxmIAoQAMM6QLexK9fK
+88EZxYC6x+qYkb3rzjaGyO3dzGhfRQTFJ8HtFrWTR5s/m+1ACKFnbf1xo7AWbsYq
+jVIxXsqUt9a4jserpaczlzDQLojFCKSGFmfCVV40FwQHL3W+C40xLHLOq93bpHLD
+knp38daRzSryW11ev9y0J3to0qX03WpgFKKwT3fQMT5+V4wZNuzOFmWGaPUsuCQh
+SQj5VU+p9Q4soIIzu2gaal4vW3/qZgIlkAmkg0FV5iW7mwScWPcF/kPlkHFMj9pG
+aTNVgegxtUQBJEXx+VF0vDiOtRnBjE2woVLq1FWGkn7feX4Jvnajqpf6A8dKeNcv
+Egfm00HHhhR5f7LTOYTSXWCqhCmIBKGpYlZo/PDGHdlVoRSR5+qN/1kyP7WZg35u
++7XR0paCTR71RwO+oHZHiv97u6P/iPvVWE7aqCJe6kBW6Q8hB4zjLfH6AbjcIN/9
+Vy0k3ALBfNxatkJZAyl/PQpgSpfpAkbk5upBPvOKVGCWsYA5sYRH4MMiwrOuSgCh
+nibnUT4jJjgU7hK7iOTJB1mbNEvyMSksgxtbdA4XWee4iv5qS3ZCX+1RUspZk3IS
+8NePnaycg+OlI5gMbSEVEmLCat2V3l5KNZRFWpbmNTCo+Mi4t1i8kgJAHRtn0r6t
+sIS9beklhQz1p7KZAphYWjl6kO9p80cnuQINBFk52cEBEAC8e3b6SN4t5I/RRmRt
+/YbPFyC81yElaPzBM+OFsbRDr9MrtfeDUp/wgcihQIw01HUDlm4F2WjbGwth/8Zs
+tEML3CFwtv4V+sYhKqfg+sS5YqzFrFWfZYod8ppFKNMaw9Pcjl70td2egcBDt1SR
+51ni4SdhyMt/KOm4mym/Lf4UNyzlYwykbjtb3nsmvxYI/uVdceDv+7vZoW1rapSw
+Zj4ZS8+jhgHrO2p5B5TCHdsDJEYQ2SOYeIm8tfqb4oQlTWwjG0frl5eOp/+9HfK1
+q7R049FMAzmd6fbm0jpzxDveDe58qWWq2aj+7HwTZhmvr9l7uFyR+3TL6s9hEALl
+B3RGpOy5hGmvwLIAi4qylZuRJzW0tMveDcaDEdMhtyEKF9DYpk1Ug/01uG+PzK8e
+TiFyTCN6OAawWIe7pIxRhlk4+CIqRPEprMTfDJxKEUS7RnUYZ76E94FSV9nEqIJj
+UWFLq3aTMqSiSN5LAgT6AJKHrR2+cJVHM1cEILvCIugeuw1GUC+Bxs1qaxIpp4SS
+xi9Givx8PkKuCukp1J4B8Alx0ZpRELsBEuhZdEN2LP+Hpz7uyD875r8BvEJ+hU20
+Qfnj2wHmKVF+6jBPwrpzZ/gbXuQzfstmrSJCY6p6izfcQSJmbSNpaTsrOvgmaUev
+Wuss4bjuG4loSRwFb/7fsPsX9wARAQABiQI8BBgBCAAmFiEEfoeSGqycUU6TQcTx
+x6U8xY07H4wFAlk52cECGwwFCQlmAYAACgkQx6U8xY07H4xvTg/+MbWyGFmLe9b8
+AMJtqwX+3EyP44Mo2CeafvmbPSqxoXh1NdOezlEESZU9fHMDY50IA2hpariO/Le3
+Ck8py5NAznCc4avS+gnahcPyhvUaMCskmN4UaRsohMvxKrdAGyRfXZcQqE5Tu6zM
+6TxycQkT00qe+PTSQnV2dvGXE4iBFV5kj3NHV7RBC+7sDZ7cYuLHrw1gOaMWeCcF
+oQ0l+DW3CNh3klFds/PIfPALjV25+niwOYcenxqp8GVjooWj7xkASkFyZAqusFTa
++/XY2y7+jvdSmm36gfWiPXWdpPiesekPK1NqPGdAtyv1/EKJd+7cYCbkSH6qPJpb
+FnpfK/ItVm39OIe7OVUuZYd4lGeFvKK/nDqSQ+9STVar2+n8Wths4C8KJbLdZxGE
+QYWKw5aL09tpQRy/skReAt9hVDq3qflODyuWPqS/oDbSGERA2NndkV3LIkU+ZCXI
+xsin4IP8XzC8yJjv11PAzM8wmhlXWOdKDIZXMV/2wO+cyM/t9WtfeXUOuk2NyWO4
+nQ+gD3cDPTJS+t8ReKb/bEXSeHiRfgiYuZXcwT5vGmx4HiYgJ7d9i+8Ikcew75zw
+EvR+OLXYVICsI9PiQPzqVBfp+2u5saEvmWORrNLLUBQt24R2CF4Y63pLumsCB3gv
+n+cUG+sEsct2sSKhQEEU7Yra9tppHlO5Ag0EWT7OXAEQANUBV3/GFcAtsM9XiSGL
+GuWs+S8np+A7WRIISsR5BU22u9XqF7P8/5o/ZJIvoNu9EwTkFZYP9pAxx7F+I/62
+x8YbXCU5byiOG0X/RKRafW1j9zJdZHK2jdga2pRUiCexpk43knTMyYUxyNlOFM+r
+UKJlMErTaN8PJldD9f7qq+assxFN+rLW+tWwxtcL9WxdCIBBMKE7ldyaIRzKNMbZ
+b+mckdP412Ht05sn2BijgHj/co07M0zw+MLWNc9c50wI+/CYBZXerDp7TZoB2HxD
+JH9FqQ+ypjZQkNArieMj8IB6o8nZRbOqs+FO3LJf4A2WuHSLb187LpTLGoL/WaGG
+YK5ZE/0DuR7lSZmVYt2qaKqzDUJbbETLNJlhEykKI0f4bkhjI9UPjxlFgarDuFOe
+V7KkZXJoapda6lwy+W8GDAcrtMasMIgN5lMQdYJllIOJVs0wTJyMbLFN1hcr8oGc
+09tLEPr0FO5lxygMHqcNiia1SO34IJF6HaRHZQeX6Hv2M+FHWpZ3fcu9dkY9i9JK
+RM0W3x0OLFBNfCAvBVFxat9xmYYJuRqomPAFIlt3hik/Dl3dWOkPLdVoE97T/r1/
+5DDNBpaO/g7XU24bH6ja9/WO8T9g0L4nTLCtdKSaGFxwT0No7jmgTJxJQan7hHDZ
++0CHj8jCUsMNSK587HTWiZFZABEBAAGJBHIEGAEIACYWIQR+h5IarJxRTpNBxPHH
+pTzFjTsfjAUCWT7OXAIbAgUJCWYBgAJACRDHpTzFjTsfjMF0IAQZAQgAHRYhBOpX
+PxAcncyJjCSg9h7cm2rmkT65BQJZPs5cAAoJEB7cm2rmkT65H2sQAMlGuoA6pKlA
+W9L+Mdn3aHaGHzxiLU0mxJZHLPxLru9YNkEF2uDiSzHRMSSbJCujF8O2Z8jg08f2
++r/ZVHK1wWd/J7zikh/1pMVj9KVNG0JdyqiHuQ02i5vWdBg6lZZku4uUvU616Ynh
+PMDVoEQ6QXQ24BhrSBH9B1tgSnIRc9EHzzW5lTF3+qttA1tJETJODzEZGmSlBene
+kUxBZVuH7daEa1pRPGNVSa4TmCTkxgYZUIdnIDC7CeDREldAftCvEll37Ewy8QTi
+goAPYYZQd9jJ7ywq1qWFiTt6n6IYvjfVm2ttO/3PBtla5FdCW3U0MZyxOiIIpHjh
+IgVFWknvOEsKQhaN4rbq7YSMLLZW5Y0ukHk8c3OsRpO5Clc/yl0hHURGtjhvHgSg
+kL8z80WhnP+XiMa/vaWIsats1/LGc+uvstmohI4np9+jF/6Byk9Y85ki1ilQszVP
+/JEIj2IvH6/OsjcXlgAs8In7EBeixlERLneK9F0D+rb99m21rhkXmy/EQ75yeSV/
+z7sy9suK3SuULqYSuTKuw+mbAY3KOF3JLjQdW1NXCZhqRaxYF+5nMq1f6VDwGcpO
+pPSF+9fY6/R5/cyEk8LOY4kS9O9rqc4PIi1ieYoyxezTBdX1aWpZEsAzVDWuJRCd
+pwaKM8YF7ofZxEo5QAnfOO/gc1opPVIUMzAP/2md27adc9Qn0AkWf4Gbr5HSbArq
+urGjqUa8HCUcishzW/TrWbDtuy0ZplvWv4z1KgqoAAr+U2rZwck8mk02UCmjpaDL
+KliTiBAuRei6wgW0y7rLE0S1q9wjoJZemhVrI5C0hu7jIPzl1r/ZRNEqcKtsl6UG
++2zlo0loGiLt+G+p5KpcwuscYGEU0Ekasu+68sual5UUdRXpu8J9ePnqSIZUHanj
+mrARW5iv17NGG3ZtGeX+rDZ2G23Ymnc/IOK1Qeyz07GE3OcxaikTk4EvNDJcl33E
+uI7Dcauz+1msBxGwkib/BUx686ZI8e3Qa9cxzmzGBAwJGqtuOv0BrKbmT6dJ1FG2
+XtRCKsYDGttvoEM8fnhAeVXLIEidiMg3ri8cE/uhIKQlTCoel/hr6yM0BztxQRIk
+PIkFDGSpOq6pknv0KgAxhymJlHmC002e3FAl+B+q71FUthwjs7h110CrI6yZTwbC
+La9FZEODAFWkWLghV3iLP0D8HD+rsBxDttpJOC0lndsONIMkb2Xf4ue8pceUehEN
+zf+mkS6B5ilfHOhrcY3vkfV/cuF2Zv1kBpjayCbanweeyEIok720eP4RoTu8NJGZ
+gD0qLo2iJBW65FRr557CWET+X22k2vDO92PeB6MXdZ+PGNgPWw+SZfY5nUN6hsbm
+73dNlPTad6zfMtHRuQINBFk+zuMBEADd0yUpELWiFKezO/GLaqBs5iI5fRvO97pk
+5yhROIjaM4xz2tmZMvenO9AdVSchgh6w1CCNMvhbE9MkEakh8qN5hWl0XeN+UarF
+XvIx1ARfzmI+Xwz40wNRNHkGMwZipvHQ0oFW2NI+qQaxu1QzX7eGIF1uQPhyw5wg
+dQeO3fbAKR7G1YNOiBM5KyEPSFj29fSyPVjhqM5orHyrD3rtHir+978hA0W4yFY3
+0lY7OqaHs2crU4txy30bc2oYL93J5uMDeXmDg+K7NEGeih1vJnh3lF73yh3i6d+t
+MxrMyzkhLf8GBD+38QqJ4npkcd4E66g0kB3Q1EYfh4R2eMkYCCcTuRuQVrtIyC81
+r490Kx3WFB2ioIOARk/0NUrnqO0tze4KuLKc4wVpKFCeRkBpN5ZxoF+NJ2DLodg8
+w5G5xkMluLpcUWU+D/LEf52Nu8JBro4BVOv8h+D3MwC8icaQaL4xTVqCBtOvhA3/
+0gxrEJ98g5jpR/Puspa/zQVpY2MP632m6Ctfw6mdHtcq3PKX2sJIF8UKLGhJsAYg
+9bUSD4ka+IV1BLXY71b2DFZoTA9WaprG4GDTV4x3PcERq1/LSUBIKl9kHw9DvGzW
+uBcQQOwUR564ghGPt4Lxq8fc5G03h2Oou6LZx1lfYHuwQs8iJtiX6EtkVhZxxQX5
+TClQMdVgEwARAQABiQI8BBgBCAAmFiEEfoeSGqycUU6TQcTxx6U8xY07H4wFAlk+
+zuMCGwwFCQlmAYAACgkQx6U8xY07H4yNohAApKlliONc+s6PMtwAOJ3j3NzOCPDy
+MOiA24kKHMg4yCUiJDJ+xX2tQs6Jf99IcCIF625nnsUqyRDgdHyDeu4ZTneo1aFB
+YMf4fgxqUkEiV7VNxvw2idDfW2Wy0fmyGCdS8UOw9UOjSMURNjfvY/pQlFNG+cWx
+ZUfrU2HgXzdAchTlYQnpPwaDaMQE7xsV/Q+VVtWNe9gAuycrGNgPhh4zNAmjqiGN
+a+YS0vW4v+TSaA7Y/jMTEJYEz9+60dYy4I64Wv1NWODT03KExQoINrROLwhn/wD8
+AZhyJKBNuAbSZpMXNMD+2QKtqeNxE7HbTQY7Bqx5feBvDkDgr7ox+KyzR3NuXOHQ
+CSbmSEQPN3miiGglHGASctp3Fd93PhXzVtiiRAnqfBw7zGDSgdpaNC8z9DAG6iUY
+ZtcNz5UoiCHSOqE1vxV38poWDtZLkKuQRXvXy/uNyPcPx2efaDNf/FxH3gM6L7+6
+gfJ9vDMKUI9Xa9A5u3BMR/1Xiehx/GL7kZ16fZDWhJH1iCUcPwu/wSPDCmzGaX3B
+O/FT+H1m/Fql8oKWOy82K1zBVMx7cx+b+3/Qlkbx3wGPGtNLPH1m5QFKoPV7z2zP
+tM9VziUSLPPlIEbsT3I4lXYdhFsHbGBk++ZbY9kRUTXkNRciqX2NFcFtNSo9RH8F
+KOmVBBI+ZQ28HDGJAh8EKAEIAAkFAlm/5kACHQMACgkQx6U8xY07H4yS/xAAiHYW
+T+wgYgFR0e0DYNOlz3KeYZwhORc5/ED07qCxUMFkChpBnXbKLzGViiKK3H9FyaFy
+fOhuIqb0GgXX4TTYdHShvceBtMfTfeYMLXQC+WaJgIydbjRK978mDBgIDs96ylEj
+ErtgP3J/GXTk616nv9VYYjGGNKQVJNxDGCRzfZks3m/gWH/whbctFQXaBOshstra
+nGpoR6EEZERpDkDMdLqd1JEhCPK8YUSPwT0LKm2yQpeR3ly5phZiJC7uZVmq518P
+f8UoHYhtb6P18kJeMVbrNpEzDTdGCZ6eKC3B+1tfoft8MXF3fEINFzZKqAXKqsHD
+sQtVWPshg49J8HNpc0NbQi90+8ph4oVrWDHoDulhGg9xUTlY1fXUye1uDXhVn8gL
+rAeLqz6WP2i7jPnNCJgTXw5+e2kAye0rCvKH0pw8a1Aq2iaxvxr0L1MzAgtKaTh/
+AU3K5j8r7YRUdOMUHMGS5CwwdhwNkABM2Sm7FmlZL/BNwmgxekhJSivLL3M6qPY3
+LjcxxJBfe4gk9RRX9/YCgSkKTwvx1Ko9368G4WcxYOSTP3eVol6o0yBqd0rV/P+l
+CCgiAZ/ZoVOvi5jmTy2I9flafPzGp51EdH+RS/rGwW1feP5JMg+NULwc1y4kTru6
+pkHTUu3Ol+M2616HU32p8XJi4mDV5qMRWmLn+UCZAQ0EVKyAeQEIALyGS95q8aCp
+8rjM35kpabNOhr9hAcdq0DrxwjOWZd5u569X1sS81VjPqoj1jpA+/GgheWeYrNxm
+RbMT1fdtd22W5yiNd5TNXF+RMhZYvnT4Mxm3NNggZoriHsnrG4XbtLZZmMTXwF/6
+a/CsaCXYHp4J3YvYnDc/B0fssj37OXQH0SjpBQnU7U1m7mvLXfm7Mh+zi7VTSz45
+WcFyr7Lg1HRN6OzDtbBjn8kuWWMzYIlg+EUZPuHLHoCkjhN6g7AM5eDhQqXvzOcy
+lSIk/TIPy7n8EeKrrgfijGQOJF6c6d5n0Hq+6lejT6uL3iHUOKtv8PYGr8vF01ao
+vP/EOVTkYQEAEQEAAbQrR3JpZmZpbiBTbWl0aCA8Z3NtaXRoQHNlY3VyaXR5c2Nv
+cmVjYXJkLmlvPokBNwQTAQgAIQUCWCuGZQIbAwULCQgHAgYVCAkKCwIEFgIDAQIe
+AQIXgAAKCRBE71tehhwJp2lCCACiTsLoGbq44A11+k24oWItbJTrg5pISwKUwfwt
+hvik0oQPWfQoz/sr0w/Ie0rUnCuSyOVUXuJZSgzFOjEcwmw1dDv0hsanyt+NZ3SC
+r/hSasAOMIeXS7+hyL894E6NKIGDi25+Yhpj1AFneCu9cOoxlEXqynVaiBJbpHIw
+atwB5i7ZvUz+krTBjf6wwgLzBi1EHw7IJYhgS1Ye9A/+h+iur7d/4/C6cC31IgBd
+r8d8iNbMhqyk66+fhdZ5Vd2QO4DUq4CUgFoakO9X383Jf8azR0zXIPPphJ2QpQzD
+sfriUT0J18bP546tknwOsNYlt1XPYwlLvXKljXr1YkRyTdPTtCdHcmlmZmluIFNt
+aXRoIDx3aWxkZ3JpZmZpbjQ1QGdtYWlsLmNvbT6JATcEEwEIACEFAlSsgHkCGwMF
+CwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQRO9bXoYcCaed+Qf+JSDZ3odMwlnr
+bb2kwslduAt9VhRm+dfdIm25nAgxJUxIju8uIgE9v+8dRdGFwrV8pKBYYOCMi8MF
+NYuu9zS66wXS4opd/DeYDj2yaN0wBYEfeXMCwVLVDHU7AHrsxQWRSxbcUOi2Mm2s
+ig70ZSq2iNicX2f6eUSr/4CjocTP6jOqcHd6Di4odEy/hK6ukCCW8ia1Uujh7JYC
+U7quHnuE1N184W2Jf6hUieFC2kE+Nmhix0LsYYe6c1InembHRZ85BpOsWWuE9cS6
+IuVO/jbNZcgS7NkuCHkG7CubPnSZX/EDwmyr9Pd57tr9BANuDNvTGgcbaXhJj5nl
+Ix/usDsrRLQxR3JpZmZpbiBTbWl0aCAoS2V5YmFzZSkgPGdsaXR0ZXJzaGFya0Br
+ZXliYXNlLmlvPokBNwQTAQgAIQUCWCuHlgIbAwULCQgHAgYVCAkKCwIEFgIDAQIe
+AQIXgAAKCRBE71tehhwJpxWTB/0ehZJ1Bjkf7AvtWYn7PEwr1y9aAWHLhAxNNXOE
+M5IhXjnpL5o3Pic8DonUrzDVxRsNxaGU8jvAvbQpWgtQXJFi0qgDxS6b1hf5CSlS
+kcjqtkcMMqyi7XAydSyCXr5s0sZ2ZBn0tri0AKN7JW4Wd0aXJrP/RmmXNeTTARI/
+LGy5Em/PBFogDPTHRWwJQ5uCaddwev3pcOzNvrSvR0m1JXG+ZtP/Z+c4QQA3YGdT
+TSxanK2w9NXTQVToJKO8Lig3ivYNgpbscE0ywrbXVfu3pzB1+9uTa3zd9MmQ0QL9
+mX3RiJeExNE+Vxj5jG+kE8GhcRxXKefXkg+UweaYfkcX8vEEuQENBFSsgHkBCADI
+E/6vQg1OW9aGffzp3atrHtCjEHU6ZONE6unlez4CGHZXIZYTAbA0Nmgd3d3JA7wd
+d0p48whI/tREFHlBD4lxQBN3wrpmDFVq0OiSLuMSAZaTXrX5ctY4CiHJVOIJUK16
+6zsoQFqvTBW7hYTsmFml1frOZrnyeYD9Hyj1Kkk1kaUkf+JrtnZzcftqD0hFzYHe
+645YsLS2ub/ZoXrlV1hznDdIH64TYwlvabvBcZR6Exn6+hByMSbem1nNqB4PN2GV
+/dO2OrkolThctGaxVoChDoauA+vfUQRWbpxzMJQHAJ3/PtKMKyMjv0+TTSIO1zsp
+i2mayI7XUyXLu5fcTfQxABEBAAGJAR8EGAEIAAkFAlSsgHkCGwwACgkQRO9bXoYc
+Cae2+Qf/QWJ+sVhFHNHUjPWSL1o+dSUMIv6qseCGyojGLZxAl9z6IKUng638XMrV
+kgAy1aoy91N+HY0IPg45huTvU36uFoD2Hr9dd+ZVftO38jfviiowqu+iPt16sfZq
+f9VUTDTJpsLzoxiwq+x5FbJYt2iqDqK30JyQD2EMn5Li0qtR1ohunxR2CE5byNRA
+1ymk1BKMDb0tDHl87fCY5+bHZBrG0svqyDsxxK0T4bqRl0gSUfhVA67xcL1C7wG1
+MmtNZM+Ks9AepFlxmRDJnX0XNdaw7P6QtK4igLw5hSiFpVYmdfEyL9W0yn4No4Pj
+rJnQvrCvIJqJ+1ANxY9H9ArCl3iF/5kCDQRZvrPlARAA1DjXoVu6jU9Rfojm7iFu
+XJm2Suq7W3v4HjsycExn3ZBh2Lh5Jc7EdncPbP3UWnNBI5MlerHS5VMfC1OFzG/Z
+IRXZyWIVOu1ajRH39i/8pIxOfcCQ1Y4msN0QntL79Z1qAtOdUGqppViiywgTA7XC
+yGU8lvVEx7TlgXdmviRSI2Mm2McbeD/YLdTgqD8+8J00sIW5DUk1n/gUkyU2z4mk
+4rwvGLJR2bGv1KIndZzfg7c+fNd2UsXjcJV8+eMRJCjE+xlrviBnJHnrNj75Ps9v
+xX4WDX2PQZycS4NEictsFOmWmyiAHWcW8ZOrqsrDbd2QGUxjKrh6kdrZvVhCogsO
+StwdVm39TfBykLt3K5jdhU5QK4AyNBbVoikoBtZFyVg2G6e0vbVAYF5NDoy+uDyM
+jK9cHeJfMHRPCW+rBkaNh4+i0uj97SML9F1G1s3+dlPSNIofvJvZ69VxJi5w0OCT
+vwdgmR/na7DQTBikZK+F0hoZRJAmKgxh0yUMzExYUq9rvdgNeEyIWs3nh6GVPs15
+iZfcRITiGFZsi/BWeSH/ce96qYG+c5UkN1QOvFqMZdneF/uUfUI/qOc3KGmhjW+f
+lcO/qqtROO3UisckjvUZL1/80YQISMn96iJpG8mOUrDHiHndSMwErMyyxz3XXYzQ
+ys7W2oihWL5iKU+SHmisNA0AEQEAAbQqSnVzdGluIERlTWFyaXMgKGh0dHA0MDIp
+IDxqdXN0aW5AZGVtYXIuaXM+iQI+BBMBAgAoBQJZvrPlAhsDBQkDwmcABgsJCAcD
+AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDcsFtM+Jr8ZiuBD/9dOkCFWDZWAj1LKBc6
+nKci16H8tazePpvjYeZFxJw2w5NEGgwg1iGsdhKm60EOA8Okh8cEmmfq+AriFPEH
+nAbnSePGNeXTRFy7njaApxnRQGaVTV+++B/J/zQTA+2iJXh4gWR2/ip6gmyAGQJK
+u63jA/fwoeWqcQDI1FnHqpGEHb6BLeeyRA7iXd3TYTOYpuFJGx57yhZflFssbmwY
+3MW3NyWdOYXkWiH1OfujYHuh5du5txiEMvN78q5F18byIaLSkoa3eOM6osGOn8kj
+X3iJEAOk+HzMONQd2O59OWmozzyxHicr4rv7LIOeAvL9gi+gpEflT65/AJbgLa5N
+JLcvQwAD/iRRv9fs7CSsOlwLyZEVGy0huZ2iyxSguBwkDsHd6yFAr90F8eehV4z6
+DlW3g5UREVQEcUIKW4FEg+E0XMe7tILOcqTzhsMrd3PwMmC/RDPoyOOhJLCLFAg9
+hN+xaFEr4cDUPT1PwudHqQ9u9uqyeH47O3Qi0KJ2IsrWgcjTy3z0setCZDh3APlv
+Y9o+Go4ykvMNV/iHwnui/CS/sIX64VKrBq5L+0cq3PnbJfeqxi/Q7CRBko4Zf2h1
+A5SkSM3lwSuLk+zLNu+erS13EjwfainK4eOgFism7lN5CD4Z7VrKxtOTKjozidG0
+N3Ez7edUYQ28NGWJBIMEb6qn/7kCDQRZvrPlARAAoWI61RD8wjDINkiXAtX0jcoG
+dvO9oMXvVFWqsGEGivqciifdA5VvB/9jK0YfFQbLvQtkfvcqITuGflBExCK47CDg
+lv4AxI0xNkj1jKwgvm/tU6y+Oe1mrw+b64Z/V5naptNnIU4VgVSNsWvZkH2EKxgq
+6k+fAoCCwxlctw2JMmbnmUNOiu2miwoiq/Agl8Jfd4xSrAGZn77ZHM+XNLgabKiJ
+782E3alCFOXbIftOXIcxgOQWbiiPEUjzCJ5llMdjVnOkn7uP+ZXm3/h7IsrC9/GP
+DqSsebGPbNuxgNrDj9HigYPNK6jjZ95bLImaADfd2h/OXA9FYz+HwJ2kBZRNGtDj
+FxsmLvqNolu8WZKSjiw7SNK0Ya+55y8KU2iO/G3T5ilAB6nRPliP9aE6IIEzNWgK
+9nNM92S34CZQMhOnVjRqH4WEi9i3j/rlSAX4mJLbe2pi6cueSBc53qisBs6H8p5Z
+hqPQJfUlVeRxF4ZNKF7wt6dhQcEbi3/IxoABBizIt5DSBybOMLOAB65A5GQkPlJ7
+VyHhzlIoS5RqzwOqg6TCQ4UUtifQqtFXuwVHlHAPhi56U10IiCWJd4hy635Eei6C
+WDmGr4+eXTK14h93f79JxIqqve5y7cZgcQ+dQPSBVl19FZnvQUA4/5E8UPI1X3cQ
+o8I1542PpL74CcXBZ1kAEQEAAYkCJQQYAQIADwUCWb6z5QIbDAUJA8JnAAAKCRDc
+sFtM+Jr8ZqSQEACFW1xrxu8mmXGXpLlXpGx9CFWBfJSFtWBYNjbLZ8Rcdu8FweBt
+HVIAmdwYbBHYgT/xQd9Gxg+Z+JmnaAZFi88pN5Pmh2dy53nysLsjYZS8G7p2lKdu
+alXrM90PxGpwugNNPVEr//+Bb6ahQgnJQLDY6wz3DuA3L1vk+sBN+00iuGbaW992
+kPRcz2KSDXY0jR3lh3938qBXJR6jbYN2YxHMhfCeK8y9hpNSP5UVUYlLeEjyEIT7
+HgbMwsX8WX8OvL+uacwSzwC1JE8Vn98pIEQgMveZn9ylwuJZp1zv5eSulDsDRWA8
+S8Agjb/fjdQGsck4REiahw3DIPqcIvUFr3yDybB8dTLp509UqK+HLw/bf8QMmpc8
+YazIquk0/HVm0tdijDCgAIw+Dh8LEP1gmCVxynlrHs9ItCjlipuTao8LopjwOiGE
+9LnYy19ESF7kUbtyFenmp+FX0WAvlUfhrSfeeM+vR1yD8dJSa1XDI/WafkxBQRuV
+sd3cfQIxDn7JGlhwytRkxl7oabxHokc3wCSzu5Sb4ok6A91HPbSNqJ2nKSkbAwQE
+u6d5vx6SSO02stvyHPUFLM7zTTNa2B5Vrz9e7eItKEm6taXqzx1e7A8w+kOlP/0p
+4oLlBDWgklpqVZcPWtFDsldyNZlxwo5xw9czlTZ+hVuaHdSqwP2NNQegYw==
+=5XsB
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/users/wpcarro/configs/.gnupg/import.sh b/users/wpcarro/configs/.gnupg/import.sh
new file mode 100755
index 0000000000..bb449267ce
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/import.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Run this script to import all of the information exported by `export.sh`.
+# Usage: ./import.sh path/to/export.zip
+
+if [ -z "${1+x}" ]; then
+  echo "You must specify the path to export.zip. Exiting..."
+  exit 1
+fi
+
+destination="$(mktemp -d)"
+
+function cleanup() {
+  rm -rf "${destination}"
+}
+trap cleanup EXIT
+
+unzip "${1}" -d "${destination}" >/dev/null
+
+gpg --import "${destination}/public.asc"
+gpg --import "${destination}/secret.asc"
+gpg --import-ownertrust "${destination}/ownertrust.txt"
+
+# Run this at the end to output some verification
+gpg --list-keys
+gpg --list-secret-keys
diff --git a/users/wpcarro/configs/.gnupg/pubring.kbx b/users/wpcarro/configs/.gnupg/pubring.kbx
new file mode 100644
index 0000000000..208fad71b7
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/pubring.kbx
Binary files differdiff --git a/users/wpcarro/configs/.gnupg/trustdb.gpg b/users/wpcarro/configs/.gnupg/trustdb.gpg
new file mode 100644
index 0000000000..8781b2ad9b
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/trustdb.gpg
Binary files differdiff --git a/users/wpcarro/configs/.sqliterc b/users/wpcarro/configs/.sqliterc
new file mode 100644
index 0000000000..7e8b3e3fb4
--- /dev/null
+++ b/users/wpcarro/configs/.sqliterc
@@ -0,0 +1,2 @@
+.mode column
+.headers on
\ No newline at end of file
diff --git a/users/wpcarro/configs/.xsecurelockrc b/users/wpcarro/configs/.xsecurelockrc
new file mode 100644
index 0000000000..101495c3ef
--- /dev/null
+++ b/users/wpcarro/configs/.xsecurelockrc
@@ -0,0 +1,5 @@
+# Replace the gLinux penguin with a custom image.
+XSECURELOCK_LOGO_IMAGE=~/.local/share/static/pickle-rick.jpg
+
+# Turn this off on laptop (not on desktop).
+XSECURELOCK_BLANK_DPMS_STATE=on
diff --git a/users/wpcarro/configs/default.nix b/users/wpcarro/configs/default.nix
new file mode 100644
index 0000000000..681f976052
--- /dev/null
+++ b/users/wpcarro/configs/default.nix
@@ -0,0 +1,73 @@
+{ pkgs, ... }:
+
+let
+  inherit (pkgs) writeShellScript;
+  inherit (pkgs.lib.strings) makeBinPath;
+in
+{
+  install = writeShellScript "install-configs" ''
+    cd "$WPCARRO/configs" && ${pkgs.stow}/bin/stow --target="$HOME" .
+  '';
+
+  uninstall = writeShellScript "uninstall-configs" ''
+    cd "$WPCARRO/configs" && ${pkgs.stow}/bin/stow --delete --target="$HOME" .
+  '';
+
+  # Run this script to import all of the information exported by `export.sh`.
+  # Usage: import-gpg path/to/export.zip
+  import-gpg = writeShellScript "import-gpg" ''
+    set -euo pipefail
+
+    if [ -z "''${1+x}" ]; then
+      echo "You must specify the path to export.zip. Exiting..."
+      exit 1
+    fi
+
+    PATH="${makeBinPath (with pkgs; [ busybox gnupg ])}"
+    destination="$(mktemp -d)"
+
+    function cleanup() {
+      rm -rf "$destination"
+    }
+    trap cleanup EXIT
+
+    unzip "$1" -d "$destination" >/dev/null
+
+    gpg --import "$destination/public.asc"
+    gpg --import "$destination/secret.asc"
+    gpg --import-ownertrust "$destination/ownertrust.txt"
+
+    # Run this at the end to output some verification
+    gpg --list-keys
+    gpg --list-secret-keys
+  '';
+
+  # Run this script to export all the information required to transport your GPG
+  # information to a zip file.
+  # Usage: export-gpg
+  export-gpg = writeShellScript "export-gpg" ''
+    set -euo pipefail
+
+    PATH="${makeBinPath (with pkgs; [ busybox gnupg zip ])}"
+    output="$(pwd)/export.zip"
+    destination="$(mktemp -d)"
+
+    function cleanup() {
+      rm -rf "$destination"
+    }
+    trap cleanup EXIT
+
+    gpg --armor --export >"$destination/public.asc"
+    gpg --armor --export-secret-keys >"$destination/secret.asc"
+    gpg --armor --export-ownertrust >"$destination/ownertrust.txt"
+
+    # Strangely enough this appears to be the only way to create a zip of a
+    # directory that doesn't contain the (noisy) full paths of each item from
+    # the source filesystem. (i.e. -j doesn't cooperate with -r).
+    pushd "$destination"
+    zip -r "$output" ./*
+    popd
+
+    echo "$(realpath $output)"
+  '';
+}
diff --git a/users/wpcarro/configs/install b/users/wpcarro/configs/install
new file mode 100755
index 0000000000..a3f3ec328e
--- /dev/null
+++ b/users/wpcarro/configs/install
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+configs="$WPCARRO/configs"
+
+(cd "$configs" && stow --target="$HOME" .)
diff --git a/users/wpcarro/configs/uninstall b/users/wpcarro/configs/uninstall
new file mode 100755
index 0000000000..9650479c42
--- /dev/null
+++ b/users/wpcarro/configs/uninstall
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+configs="$WPCARRO/configs"
+
+(cd "$configs" && stow --delete --target="$HOME" .)
diff --git a/users/wpcarro/dotfiles/config.fish b/users/wpcarro/dotfiles/config.fish
new file mode 100644
index 0000000000..9d91b0086f
--- /dev/null
+++ b/users/wpcarro/dotfiles/config.fish
@@ -0,0 +1,34 @@
+alias c 'xclip -selection clipboard -i'
+alias p 'xclip -selection clipboard -o'
+alias cat 'bat --theme="Monokai Extended Light"'
+alias rgh 'rg --hidden'
+alias fdh 'fd --hidden'
+alias tpr 'tput reset'
+alias ls 'exa --sort=type'
+alias ll 'exa --long --sort=type'
+alias la 'exa --long --all --sort=type'
+alias gd 'git diff'
+alias glp 'git log --pretty --oneline --graph'
+alias gpf 'git push --force-with-lease'
+alias gsh 'git show HEAD'
+alias gst 'git status'
+alias edit 'emacsclient -n'
+# fs navigation
+alias d 'cd /depot'
+alias w 'cd /depot/users/wpcarro'
+# This allows me to call rebuild-system from any directory.
+alias rebuild-system 'sudo /depot/bin/rebuild-system'
+
+# environment variables
+set -gx EDITOR "emacsclient"
+set -gx ALTERNATE_EDITOR "emacs -q -nw"
+set -gx VISUAL "emacsclient"
+
+# Use my custom fish prompt
+source /depot/users/wpcarro/dotfiles/prompt.fish
+
+# Configure fuzzy history, file, directory searching
+source (fzf-share)/key-bindings.fish && fzf_key_bindings
+
+# Install direnv
+eval (direnv hook fish)
diff --git a/users/wpcarro/dotfiles/default.nix b/users/wpcarro/dotfiles/default.nix
new file mode 100644
index 0000000000..8150f53706
--- /dev/null
+++ b/users/wpcarro/dotfiles/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  dunstrc = ./dunstrc;
+}
diff --git a/users/wpcarro/dotfiles/dunstrc b/users/wpcarro/dotfiles/dunstrc
new file mode 100644
index 0000000000..a17533f073
--- /dev/null
+++ b/users/wpcarro/dotfiles/dunstrc
@@ -0,0 +1,53 @@
+[global]
+font = JetBrains Mono
+origin = top-right
+markup = yes
+plain_text = no
+format = "<b>%s</b>\n%b"
+sort = no
+indicate_hidden = yes
+alignment = center
+bounce_freq = 0
+show_age_threshold = -1
+word_wrap = yes
+ignore_newline = no
+stack_duplicates = yes
+hide_duplicate_count = yes
+geometry = "300x50-15+49"
+shrink = no
+transparency = 5
+idle_threshold = 0
+monitor = 0
+follow = keyboard
+sticky_history = yes
+history_length = 15
+show_indicators = no
+line_height = 3
+separator_height = 2
+padding = 6
+horizontal_padding = 6
+separator_color = frame
+startup_notification = false
+browser = xdg-open
+icon_position = off
+max_icon_size = 80
+frame_width = 3
+frame_color = "#8EC07C"
+
+[urgency_low]
+frame_color = "#3B7C87"
+foreground = "#3B7C87"
+background = "#191311"
+timeout = 4
+
+[urgency_normal]
+frame_color = "#5B8234"
+foreground = "#5B8234"
+background = "#191311"
+timeout = 6
+
+[urgency_critical]
+frame_color = "#B7472A"
+foreground = "#B7472A"
+background = "#191311"
+timeout = 8
\ No newline at end of file
diff --git a/users/wpcarro/dotfiles/prompt.fish b/users/wpcarro/dotfiles/prompt.fish
new file mode 100644
index 0000000000..58d22dab5e
--- /dev/null
+++ b/users/wpcarro/dotfiles/prompt.fish
@@ -0,0 +1,87 @@
+# When the Emacs SSH client, Tramp, connects to a remote host that uses Fish,
+# it's important to keep the shell prompt simple so that Tramp can parse it.
+if test "$TERM" = "dumb"
+    function fish_prompt
+        echo "\$ "
+    end
+    function fish_right_prompt; end
+    function fish_greeting; end
+    function fish_title; end
+else
+    function fish_prompt
+        # My custom prompt.
+        #
+        # Design objectives:
+        # - max-length <= 80 characters
+        # - minimal
+        # - no dependencies (well, you know what I mean)
+        #
+        # Components
+        # - ssh connection
+        # - user
+        # - host
+        # - git repo
+        # - git branch
+        # - lambda character as prompt
+
+        # Cache status before we overwrite it.
+        set -l last_status $status
+
+        # Colors
+        set -l color_inactive (set_color red --bold)
+        set -l color_active (set_color green --bold)
+        set -l color_normal (set_color normal)
+
+        # SSH information
+        if set -q SSH_CLIENT; or set -q SSH_TTY
+            echo -en "$color_active \bssh ✓ [$color_normal$USER@"(hostname)"$color_active]$color_normal"
+        else
+            echo -en "$color_inactive \bssh ✗ [$color_normal$USER@"(hostname)"$color_inactive]$color_normal"
+        end
+
+        # Separator
+        echo -n " "
+
+        # Git information
+        set -l git_repo (git rev-parse --show-toplevel 2>/dev/null)
+        set -l git_status $status
+
+        if [ (realpath .) = "/" ]
+            set -g dir_path (realpath .)
+        else if [ (realpath ..) = "/" ]
+            set -g dir_path (realpath .)
+        else
+            set -g dir_path (echo (basename (realpath ..))"/"(basename (realpath .)))
+        end
+
+        if test $git_status -eq 0
+            set -l git_repo_name (basename (git rev-parse --show-toplevel))
+            set -l git_branch (git branch 2>/dev/null | grep '^\*' | cut -d' ' -f2-)
+            echo -en "$color_active \bgit ✓ [$color_normal$git_branch$color_active|$color_normal$git_repo_name$color_active|$color_normal$dir_path$color_active]$color_normal"
+        else
+            echo -en "$color_inactive \bgit ✗ [$color_normal$dir_path$color_inactive]$color_normal"
+        end
+
+        # Newline
+        echo
+
+        # Handle root vs non-root
+        if [ "$USER" = "root" ]
+            set -g prompt_sigil "#"
+        else
+            set -g prompt_sigil "λ"
+        end
+
+        set -l time (date +"%T")
+        if test $last_status -eq 0
+            set -l color_prompt (set_color white --bold)
+            echo -n "$time$color_prompt $prompt_sigil$color_normal "
+        else
+            set -l color_prompt (set_color red --bold)
+            echo -n "$time$color_prompt $prompt_sigil$color_normal "
+        end
+    end
+    function fish_right_prompt; end
+    function fish_greeting; end
+    function fish_title; end
+end
diff --git a/users/wpcarro/emacs/.emacs.d/init.el b/users/wpcarro/emacs/.emacs.d/init.el
new file mode 100644
index 0000000000..9554147fe7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/init.el
@@ -0,0 +1,16 @@
+;; load order is intentional
+(setq-default debug-on-error t)
+(require 'wpc-package)
+(require 'wpc-misc)
+(require 'ssh)
+(require 'keyboard)
+(require 'irc)
+(require 'email)
+(require 'keybindings)
+(require 'window-manager)
+(require 'wpc-ui)
+(require 'wpc-dired)
+(require 'wpc-org)
+(require 'wpc-company)
+(require 'wpc-shell)
+(require 'wpc-language-support)
diff --git a/users/wpcarro/emacs/.emacs.d/opam-user-setup.el b/users/wpcarro/emacs/.emacs.d/opam-user-setup.el
new file mode 100644
index 0000000000..a23addefaf
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/opam-user-setup.el
@@ -0,0 +1,145 @@
+;; ## added by OPAM user-setup for emacs / base ## cfd3c9b7837c85cffd0c59de521990f0 ## you can edit, but keep this line
+(provide 'opam-user-setup)
+
+;; Base configuration for OPAM
+
+(defun opam-shell-command-to-string (command)
+  "Similar to shell-command-to-string, but returns nil unless the process
+  returned 0, and ignores stderr (shell-command-to-string ignores return value)"
+  (let* ((return-value 0)
+         (return-string
+          (with-output-to-string
+            (setq return-value
+                  (with-current-buffer standard-output
+                    (process-file shell-file-name nil '(t nil) nil
+                                  shell-command-switch command))))))
+    (if (= return-value 0) return-string nil)))
+
+(defun opam-update-env (switch)
+  "Update the environment to follow current OPAM switch configuration"
+  (interactive
+   (list
+    (let ((default
+            (car (split-string (opam-shell-command-to-string "opam switch show --safe")))))
+      (completing-read
+       (concat "opam switch (" default "): ")
+       (split-string (opam-shell-command-to-string "opam switch list -s --safe") "\n")
+       nil t nil nil default))))
+  (let* ((switch-arg (if (= 0 (length switch)) "" (concat "--switch " switch)))
+         (command (concat "opam config env --safe --sexp " switch-arg))
+         (env (opam-shell-command-to-string command)))
+    (when (and env (not (string= env "")))
+      (dolist (var (car (read-from-string env)))
+        (setenv (car var) (cadr var))
+        (when (string= (car var) "PATH")
+          (setq exec-path (split-string (cadr var) path-separator)))))))
+
+(opam-update-env nil)
+
+(defvar opam-share
+  (let ((reply (opam-shell-command-to-string "opam config var share --safe")))
+    (when reply (substring reply 0 -1))))
+
+(add-to-list 'load-path (concat opam-share "/emacs/site-lisp"))
+;; OPAM-installed tools automated detection and initialisation
+
+(defun opam-setup-tuareg ()
+  (add-to-list 'load-path (concat opam-share "/tuareg") t)
+  (load "tuareg-site-file"))
+
+(defun opam-setup-add-ocaml-hook (h)
+  (add-hook 'tuareg-mode-hook h t)
+  (add-hook 'caml-mode-hook h t))
+
+(defun opam-setup-complete ()
+  (if (require 'company nil t)
+    (opam-setup-add-ocaml-hook
+      (lambda ()
+         (company-mode)
+         (defalias 'auto-complete 'company-complete)))
+    (require 'auto-complete nil t)))
+
+(defun opam-setup-ocp-indent ()
+  (opam-setup-complete)
+  (autoload 'ocp-setup-indent "ocp-indent" "Improved indentation for Tuareg mode")
+  (autoload 'ocp-indent-caml-mode-setup "ocp-indent" "Improved indentation for Caml mode")
+  (add-hook 'tuareg-mode-hook 'ocp-setup-indent t)
+  (add-hook 'caml-mode-hook 'ocp-indent-caml-mode-setup  t))
+
+(defun opam-setup-ocp-index ()
+  (autoload 'ocp-index-mode "ocp-index" "OCaml code browsing, documentation and completion based on build artefacts")
+  (opam-setup-add-ocaml-hook 'ocp-index-mode))
+
+(defun opam-setup-merlin ()
+  (opam-setup-complete)
+  (require 'merlin)
+  (opam-setup-add-ocaml-hook 'merlin-mode)
+
+  (defcustom ocp-index-use-auto-complete nil
+    "Use auto-complete with ocp-index (disabled by default by opam-user-setup because merlin is in use)"
+    :group 'ocp_index)
+  (defcustom merlin-ac-setup 'easy
+    "Use auto-complete with merlin (enabled by default by opam-user-setup)"
+    :group 'merlin-ac)
+
+  ;; So you can do it on a mac, where `C-<up>` and `C-<down>` are used
+  ;; by spaces.
+  (define-key merlin-mode-map
+    (kbd "C-c <up>") 'merlin-type-enclosing-go-up)
+  (define-key merlin-mode-map
+    (kbd "C-c <down>") 'merlin-type-enclosing-go-down)
+  (set-face-background 'merlin-type-face "skyblue"))
+
+(defun opam-setup-utop ()
+  (autoload 'utop "utop" "Toplevel for OCaml" t)
+  (autoload 'utop-minor-mode "utop" "Minor mode for utop" t)
+  (add-hook 'tuareg-mode-hook 'utop-minor-mode))
+
+(defvar opam-tools
+  '(("tuareg" . opam-setup-tuareg)
+    ("ocp-indent" . opam-setup-ocp-indent)
+    ("ocp-index" . opam-setup-ocp-index)
+    ("merlin" . opam-setup-merlin)
+    ("utop" . opam-setup-utop)))
+
+(defun opam-detect-installed-tools ()
+  (let*
+      ((command "opam list --installed --short --safe --color=never")
+       (names (mapcar 'car opam-tools))
+       (command-string (mapconcat 'identity (cons command names) " "))
+       (reply (opam-shell-command-to-string command-string)))
+    (when reply (split-string reply))))
+
+(defvar opam-tools-installed (opam-detect-installed-tools))
+
+(defun opam-auto-tools-setup ()
+  (interactive)
+  (dolist (tool opam-tools)
+    (when (member (car tool) opam-tools-installed)
+     (funcall (symbol-function (cdr tool))))))
+
+(opam-auto-tools-setup)
+;; ## end of OPAM user-setup addition for emacs / base ## keep this line
+;; ## added by OPAM user-setup for emacs / tuareg ## b10f42abebd2259b784b70d1a7f7e426 ## you can edit, but keep this line
+;; Set to autoload tuareg from its original switch when not found in current
+;; switch (don't load tuareg-site-file as it adds unwanted load-paths)
+(defun opam-tuareg-autoload (fct file doc args)
+  (let ((load-path (cons "/home/wpcarro/.opam/default/share/emacs/site-lisp" load-path)))
+    (load file))
+  (apply fct args))
+(when (not (member "tuareg" opam-tools-installed))
+  (defun tuareg-mode (&rest args)
+    (opam-tuareg-autoload 'tuareg-mode "tuareg" "Major mode for editing OCaml code" args))
+  (defun tuareg-run-ocaml (&rest args)
+    (opam-tuareg-autoload 'tuareg-run-ocaml "tuareg" "Run an OCaml toplevel process" args))
+  (defun ocamldebug (&rest args)
+    (opam-tuareg-autoload 'ocamldebug "ocamldebug" "Run the OCaml debugger" args))
+  (defalias 'run-ocaml 'tuareg-run-ocaml)
+  (defalias 'camldebug 'ocamldebug)
+  (add-to-list 'auto-mode-alist '("\\.ml[iylp]?\\'" . tuareg-mode))
+  (add-to-list 'auto-mode-alist '("\\.eliomi?\\'" . tuareg-mode))
+  (add-to-list 'interpreter-mode-alist '("ocamlrun" . tuareg-mode))
+  (add-to-list 'interpreter-mode-alist '("ocaml" . tuareg-mode))
+  (dolist (ext '(".cmo" ".cmx" ".cma" ".cmxa" ".cmxs" ".cmt" ".cmti" ".cmi" ".annot"))
+    (add-to-list 'completion-ignored-extensions ext)))
+;; ## end of OPAM user-setup addition for emacs / tuareg ## keep this line
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/c-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdio b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdio
new file mode 100644
index 0000000000..52bc717e47
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdio
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: <stdio.h>
+# key: sio
+# --
+#include <stdio.h>
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdlib b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdlib
new file mode 100644
index 0000000000..5d44e8ed79
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/stdlib
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: <stdlib.h>
+# key: slb
+# --
+#include <stdlib.h>
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/c-mode/struct b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/struct
new file mode 100644
index 0000000000..6e9282f83c
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/c-mode/struct
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: struct
+# key: struct
+# --
+typedef struct $1 {
+  $2
+} $1_t;
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/elisp-module-docs b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/elisp-module-docs
new file mode 100644
index 0000000000..8ea7b8f077
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/elisp-module-docs
@@ -0,0 +1,11 @@
+# -*- mode: snippet -*-
+# name: Elisp module docs
+# key: emd
+# --
+;;; `(-> (buffer-file-name) f-filename)` --- $2 -*- lexical-binding: t -*-
+;; Author: William Carroll <wpcarro@gmail.com>
+
+;;; Commentary:
+;; $3
+
+;;; Code:
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/function b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/function
new file mode 100644
index 0000000000..bfa888d526
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/function
@@ -0,0 +1,8 @@
+# -*- mode: snippet -*-
+# name: Function
+# key: fn
+# expand-env: ((yas-indent-line 'fixed))
+# --
+(defun $1 ($2)
+  "$3"
+  $4)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/generic-header b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/generic-header
new file mode 100644
index 0000000000..bf6e525f8c
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/generic-header
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Header
+# key: hdr
+# --
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; $1
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/library-header b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/library-header
new file mode 100644
index 0000000000..0f0ad5c4fc
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/library-header
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Library header
+# key: lib
+# --
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/provide-footer b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/provide-footer
new file mode 100644
index 0000000000..2a0bcc33f7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/emacs-lisp-mode/provide-footer
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: Provide footer
+# key: elf
+# --
+(provide '`(-> (buffer-file-name) f-filename f-no-ext)`)
+;;; `(-> (buffer-file-name) f-filename)` ends here
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/derive-safe-copy b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/derive-safe-copy
new file mode 100644
index 0000000000..95f7d9deec
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/derive-safe-copy
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Derive Safe Copy
+# key: dsc
+# --
+deriveSafeCopy 0 'base ''$1
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/import-qualified b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/import-qualified
new file mode 100644
index 0000000000..4c4db62a8a
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/import-qualified
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Import qualified
+# key: iq
+# --
+import qualified $1 as $2
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/instance-defn b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/instance-defn
new file mode 100644
index 0000000000..10d194ce41
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/instance-defn
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: Instance
+# key: inst
+# --
+instance $1 where
+  $2 = $3
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/language-extension b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/language-extension
new file mode 100644
index 0000000000..9d6084acb4
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/language-extension
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: language extension
+# key: lang
+# --
+{-# LANGUAGE $1 #-}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/separator b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/separator
new file mode 100644
index 0000000000..1ab0d762b6
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/separator
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Separator
+# key: -
+# --
+--------------------------------------------------------------------------------
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/undefined b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/undefined
new file mode 100644
index 0000000000..7609f801f2
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/haskell-mode/undefined
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Undefiend
+# key: nd
+# --
+undefined
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/html-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/html-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/html-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/html-mode/index-boilerplate b/users/wpcarro/emacs/.emacs.d/snippets/html-mode/index-boilerplate
new file mode 100644
index 0000000000..3cea6ce003
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/html-mode/index-boilerplate
@@ -0,0 +1,18 @@
+# -*- mode: snippet -*-
+# name: HTML index.html starter
+# key: html
+# --
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>$1</title>
+  <meta name="description" content="$2">
+  <meta name="author" content="William Carroll">
+  <link rel="stylesheet" href="index.css">
+</head>
+<body>
+  <script src="index.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/java-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/java-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/java-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/java-mode/public-static-void-main b/users/wpcarro/emacs/.emacs.d/snippets/java-mode/public-static-void-main
new file mode 100644
index 0000000000..1839a27eb5
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/java-mode/public-static-void-main
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: public static void main
+# key: psvm
+# --
+public static void main(String[] args) {
+    $1
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/defpackage b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/defpackage
new file mode 100644
index 0000000000..7f110a9718
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/defpackage
@@ -0,0 +1,9 @@
+# -*- mode: snippet -*-
+# name: Define package
+# key: defp
+# --
+(in-package #:cl-user)
+(defpackage #:$1
+  (:documentation "$2")
+  (:use #:cl))
+(in-package #:$1)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/function b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/function
new file mode 100644
index 0000000000..b1769cd3d1
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/function
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Function
+# key: fn
+# --
+(defun $1 ($2)
+  "$3"
+  $4)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/typed-function b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/typed-function
new file mode 100644
index 0000000000..a3c236821e
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/lisp-mode/typed-function
@@ -0,0 +1,8 @@
+# -*- mode: snippet -*-
+# name: Typed function
+# key: tfn
+# --
+(type $1 ($3) $4)
+(defun $1 ($2)
+  "$5"
+  $6)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/nix-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/nix-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/nix-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/nix-mode/shell-nix b/users/wpcarro/emacs/.emacs.d/snippets/nix-mode/shell-nix
new file mode 100644
index 0000000000..b5eb5a2447
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/nix-mode/shell-nix
@@ -0,0 +1,12 @@
+# -*- mode: snippet -*-
+# name: shell.nix boilerplate
+# key: import
+# --
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "$1";
+  buildInputs = [
+    $2
+  ];
+}
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/org-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/org-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/org-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/org-mode/code-snippet b/users/wpcarro/emacs/.emacs.d/snippets/org-mode/code-snippet
new file mode 100644
index 0000000000..4215b15992
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/org-mode/code-snippet
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Code Snippet
+# key: src
+# --
+#+BEGIN_SRC $1
+$2
+#+END_SRC
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/org-mode/href b/users/wpcarro/emacs/.emacs.d/snippets/org-mode/href
new file mode 100644
index 0000000000..ac65ea2e49
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/org-mode/href
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Org mode URL
+# key: href
+# --
+[[$1][$2]]
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/dunder-main b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/dunder-main
new file mode 100644
index 0000000000..4dd22dc0b2
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/dunder-main
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: Dunder main (__main__)
+# key: mn
+# --
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/function b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/function
new file mode 100644
index 0000000000..379ceda1a3
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/function
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: Function
+# key: fn
+# --
+def $1($2):
+    $3
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/header b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/header
new file mode 100644
index 0000000000..db48adfec7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/header
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Header
+# key: hdr
+# --
+################################################################################
+# $1
+################################################################################
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/init b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/init
new file mode 100644
index 0000000000..5c407495f5
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/init
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: dunder init
+# key: ctor
+# --
+def __init__(self$1):
+    $2
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/shebang b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/shebang
new file mode 100644
index 0000000000..0f45ae782d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/shebang
@@ -0,0 +1,6 @@
+# -*- mode: snippet -*-
+# name: shebang
+# key: shb
+# --
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/python-mode/utf-8 b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/utf-8
new file mode 100644
index 0000000000..3babc73030
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/python-mode/utf-8
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: utf-8
+# key: utf
+# --
+# -*- coding: utf-8 -*-
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/function b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/function
new file mode 100644
index 0000000000..882c48ded3
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/function
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Function
+# key: fn
+# --
+(define ($1) $2)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda
new file mode 100644
index 0000000000..b9a684588b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Lambda function
+# key: ld
+# --
+(λ ($1) $2)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda-symbol b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda-symbol
new file mode 100644
index 0000000000..254b9fd96b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/racket-mode/lambda-symbol
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Lambda symbol
+# key: l
+# --
+λ
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/function b/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/function
new file mode 100644
index 0000000000..6b4b6a5db2
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/function
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Function
+# key: fn
+# --
+let $1 = (~$2:$3) => {
+  $4
+};
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/switch b/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/switch
new file mode 100644
index 0000000000..40f34ff8d1
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/reason-mode/switch
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Switch statement
+# key: sw
+# --
+switch ($1) {
+| $2 =>
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/action-extractor b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/action-extractor
new file mode 100644
index 0000000000..62834a29ab
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/action-extractor
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: exactness
+# key: $x
+# --
+$Exact<$Call<typeof $1>>
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/console-log b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/console-log
new file mode 100644
index 0000000000..82ec3fd8e3
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/console-log
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Console.log helper
+# key: clg
+# --
+console.log($1)
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-defn b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-defn
new file mode 100644
index 0000000000..8e35e61fc2
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-defn
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: const definition
+# key: cn
+# --
+const $1 = '$2'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-function b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-function
new file mode 100644
index 0000000000..13f2018f22
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/const-function
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: const function
+# key: cfn
+# --
+const $1 = ($2) => {
+  $3
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/destructure-const b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/destructure-const
new file mode 100644
index 0000000000..2a52c57c75
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/destructure-const
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Destructuring a const
+# key: cds
+# --
+const { $1 } = $2
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow
new file mode 100644
index 0000000000..187a2efc5a
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Fat arrow function
+# key: fa
+# --
+=>
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow-function b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow-function
new file mode 100644
index 0000000000..694914a83c
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/fat-arrow-function
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Fat arrow function
+# key: faf
+# --
+() => {
+  $1
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-destructured b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-destructured
new file mode 100644
index 0000000000..ded3ce163a
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-destructured
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Import destructured
+# key: ids
+# --
+import { $1 } from '$2'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-react b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-react
new file mode 100644
index 0000000000..0463f5cd55
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-react
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Import React dependency (ES6)
+# key: ir
+# --
+import React from 'react'
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-type b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-type
new file mode 100644
index 0000000000..fcd51f687b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-type
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: import type
+# key: ixt
+# --
+import type { $1 } from '$2'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-x-from-y b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-x-from-y
new file mode 100644
index 0000000000..09fa6df505
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-x-from-y
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: import x from y
+# key: ix
+# --
+import $1 from '$2'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-y b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-y
new file mode 100644
index 0000000000..9f550e300d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/import-y
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: import y
+# key: iy
+# --
+import '$1'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-describe-test b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-describe-test
new file mode 100644
index 0000000000..ed382d4f74
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-describe-test
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: Jest describe/test block
+# key: dsc
+# --
+describe('$1', () => {
+  test('$2', () => {
+
+    expect($3).toEqual($4)
+  })
+})
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-test b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-test
new file mode 100644
index 0000000000..12ca2e786d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/jest-test
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Jest / Jasmine test
+# key: tst
+# --
+test('$1', () => {
+  expect($2).toBe($3)
+})
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/react-class-component b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/react-class-component
new file mode 100644
index 0000000000..f2a93a31d9
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/react-class-component
@@ -0,0 +1,11 @@
+# -*- mode: snippet -*-
+# name: React class extends
+# key: clz
+# --
+class $1 extends React.Component {
+  render() {
+    $2
+  }
+}
+
+export default $1
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/redux-action b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/redux-action
new file mode 100644
index 0000000000..681c5d0dfd
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/redux-action
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: redux-action
+# key: rax
+# --
+export const ${1:$$(string-lower->caps yas-text)} = '`(downcase (functions-buffer-dirname))`/${1:$(string-caps->kebab yas-text)}'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/typed-redux-action b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/typed-redux-action
new file mode 100644
index 0000000000..53c6e5fc5a
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rjsx-mode/typed-redux-action
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: typed-redux-action
+# key: trax
+# --
+export const ${1:$$(string-lower->caps yas-text)}: '`(downcase (functions-buffer-dirname))`/${1:$(string-caps->kebab yas-text)}' = '`(downcase (buffer-dirname))`/${1:$(string-caps->kebab yas-text)}'
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/for-loop b/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/for-loop
new file mode 100644
index 0000000000..4d8e0e3bbd
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/for-loop
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: for-loop
+# key: for
+# --
+for $1 in $2 {
+    $3
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/match b/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/match
new file mode 100644
index 0000000000..bf0e876e2b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/rust-mode/match
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: match
+# key: match
+# --
+match $1 {
+    $2 => $3,
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/sh-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/sh-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/sh-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/sh-mode/function b/users/wpcarro/emacs/.emacs.d/snippets/sh-mode/function
new file mode 100644
index 0000000000..efa946bb27
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/sh-mode/function
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Create function
+# key: fn
+# --
+$1() {
+  $2
+}
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/text-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/text-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/text-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/text-mode/check-mark b/users/wpcarro/emacs/.emacs.d/snippets/text-mode/check-mark
new file mode 100644
index 0000000000..7977819688
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/text-mode/check-mark
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Unicode checkmark
+# key: uck
+# --
+✓
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/text-mode/x-mark b/users/wpcarro/emacs/.emacs.d/snippets/text-mode/x-mark
new file mode 100644
index 0000000000..bc3c356a61
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/text-mode/x-mark
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Unicode ex-mark
+# key: ux
+# --
+✗
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/web-mode/.yas-parents b/users/wpcarro/emacs/.emacs.d/snippets/web-mode/.yas-parents
new file mode 100644
index 0000000000..d58dacb7a0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/web-mode/.yas-parents
@@ -0,0 +1 @@
+text-mode
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/web-mode/header b/users/wpcarro/emacs/.emacs.d/snippets/web-mode/header
new file mode 100644
index 0000000000..ae59c7a50f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/web-mode/header
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: Header
+# key: hdr
+# --
+/*******************************************************************************
+ * $1
+ ******************************************************************************/
\ No newline at end of file
diff --git a/users/wpcarro/emacs/.emacs.d/snippets/web-mode/index-boilerplate b/users/wpcarro/emacs/.emacs.d/snippets/web-mode/index-boilerplate
new file mode 100644
index 0000000000..b791cdf86f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/snippets/web-mode/index-boilerplate
@@ -0,0 +1,18 @@
+# -*- mode: snippet -*-
+# name: HTML index.html starter
+# key: html
+# --
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>$1</title>
+  <meta name="description" content="$2">
+  <meta name="author" content="William Carroll">
+  <link rel="stylesheet" href="index.css">
+</head>
+<body>
+  <script src="index.js"></script>
+</body>
+</html>
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/>.el b/users/wpcarro/emacs/.emacs.d/wpc/>.el
new file mode 100644
index 0000000000..6d5f86f8b4
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/>.el
@@ -0,0 +1,28 @@
+;;; >.el --- Small utility functions -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Originally I stored the `>>` macro in macros.el, but after setting up linting
+;; for my Elisp in CI, `>>` failed because it didn't have the `macros-`
+;; namespace.  I created this module to establish a `>-` namespace under which I
+;; can store some utilities that would be best kept without a cumbersome
+;; namespace.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmacro >-> (&rest forms)
+  "Compose a new, point-free function by composing FORMS together."
+  (let ((sym (gensym)))
+    `(lambda (,sym)
+       (->> ,sym ,@forms))))
+
+
+(provide '>)
+;;; >.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/al.el b/users/wpcarro/emacs/.emacs.d/wpc/al.el
new file mode 100644
index 0000000000..3cf98fee29
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/al.el
@@ -0,0 +1,254 @@
+;;; al.el --- Interface for working with associative lists -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Firstly, a rant:
+;; In most cases, I find Elisp's APIs to be confusing.  There's a mixture of
+;; overloaded functions that leak the implementation details (TODO: provide an
+;; example of this.) of the abstract data type, which I find privileges those
+;; "insiders" who spend disproportionately large amounts of time in Elisp land,
+;; and other functions with little-to-no pattern about the order in which
+;; arguments should be applied.  In theory, however, most of these APIs could
+;; and should be much simpler.  This module represents a step in that direction.
+;;
+;; I'm modelling these APIs after Elixir's APIs.
+;;
+;; On my wishlist is to create protocols that will allow generic interfaces like
+;; Enum protocols, etc.  Would be nice to abstract over...
+;; - associative lists (i.e. alists)
+;; - property lists (i.e. plists)
+;; - hash tables
+;; ...with some dictionary or map-like interface.  This will probably end up
+;; being quite similar to the kv.el project but with differences at the API
+;; layer.
+;;
+;; Similar libraries:
+;; - map.el: Comes bundled with recent versions of Emacs.
+;; - asoc.el: Helpers for working with alists.  asoc.el is similar to alist.el
+;;   because it uses the "!" convention for signalling that a function mutates
+;;   the underlying data structure.
+;; - ht.el: Hash table library.
+;; - kv.el: Library for dealing with key-value collections.  Note that map.el
+;;   has a similar typeclass because it works with lists, hash-tables, or
+;;   arrays.
+;; - a.el: Clojure-inspired way of working with key-value data structures in
+;; Elisp.  Works with alists, hash-tables, and sometimes vectors.
+;;
+;; Some API design principles:
+;; - The "noun" (i.e. alist) of the "verb" (i.e. function) comes last to improve
+;; composability with the threading macro (i.e. `->>') and to improve consumers'
+;; intuition with the APIs.  Learn this once, know it always.
+;;
+;; - Every function avoids mutating the alist unless it ends with !.
+;;
+;; - CRUD operations will be named according to the following table:
+;;   - "create" *and* "set"
+;;   - "read"   *and* "get"
+;;   - "update"
+;;   - "delete" *and* "remove"
+;;
+;; For better or worse, all of this code expects alists in the form of:
+;; ((first-name . "William") (last-name . "Carroll"))
+;;
+;; Special thanks to github.com/alphapapa/emacs-package-dev-handbook for some of
+;; the idiomatic ways to update alists.
+;;
+;; TODO: Include a section that compares alist.el to a.el from
+;; github.com/plexus/a.el.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies:
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'macros)
+(require 'dash)
+(require 'tuple)
+(require 'maybe)
+
+;; TODO: Support function aliases for:
+;; - create/set
+;; - read/get
+;; - update
+;; - delete/remove
+
+;; Support mutative variants of functions with an ! appendage to their name.
+
+;; Ensure that the same message about only updating the first occurrence of a
+;; key is consistent throughout documentation using string interpolation or some
+;; other mechanism.
+
+;; TODO: Consider wrapping all of this with `(cl-defstruct alist xs)'.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst al-enable-tests? t
+  "When t, run the test suite.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Support a variadic version of this to easily construct alists.
+(defun al-new ()
+  "Return a new, empty alist."
+  '())
+
+;; Create
+;; TODO: See if this mutates.
+(defun al-set (k v xs)
+  "Set K to V in XS."
+  (if (al-has-key? k xs)
+      (progn
+        ;; Note: this is intentional `alist-get' and not `al-get'.
+        (setf (alist-get k xs) v)
+        xs)
+    (list-cons `(,k . ,v) xs)))
+
+(defun al-set! (k v xs)
+  "Set K to V in XS mutatively.
+Note that this doesn't append to the alist in the way that most alists handle
+  writing.  If the k already exists in XS, it is overwritten."
+  (map-delete xs k)
+  (map-put! xs k v))
+
+;; Read
+(defun al-get (k xs)
+  "Return the value at K in XS; otherwise, return nil.
+Returns the first occurrence of K in XS since alists support multiple entries."
+  (cdr (assoc k xs)))
+
+(defun al-get-entry (k xs)
+  "Return the first key-value pair at K in XS."
+  (assoc k xs))
+
+;; Update
+;; TODO: Add warning about only the first occurrence being updated in the
+;; documentation.
+(defun al-update (k f xs)
+  "Apply F to the value stored at K in XS.
+If `K' is not in `XS', this function errors.  Use `al-upsert' if you're
+interested in inserting a value when a key doesn't already exist."
+  (if (not (al-has-key? k xs))
+      (error "Refusing to update: key does not exist in alist")
+    (al-set k (funcall f (al-get k xs)) xs)))
+
+(defun al-update! (k f xs)
+  "Call F on the entry at K in XS.
+Mutative variant of `al-update'."
+  (al-set! k (funcall f (al-get k xs))xs))
+
+;; TODO: Support this.
+(defun al-upsert (k v f xs)
+  "If K exists in `XS' call `F' on the value otherwise insert `V'."
+  (if (al-has-key? k xs)
+      (al-update k f xs)
+    (al-set k v xs)))
+
+;; Delete
+;; TODO: Make sure `delete' and `remove' behave as advertised in the Elisp docs.
+(defun al-delete (k xs)
+  "Deletes the entry of K from XS.
+This only removes the first occurrence of K, since alists support multiple
+  key-value entries.  See `al-delete-all' and `al-dedupe'."
+  (remove (assoc k xs) xs))
+
+(defun al-delete! (k xs)
+  "Delete the entry of K from XS.
+Mutative variant of `al-delete'."
+  (delete (assoc k xs) xs))
+
+;; Additions to the CRUD API
+;; TODO: Implement this function.
+(defun al-dedupe-keys (xs)
+  "Remove the entries in XS where the keys are `equal'.")
+
+(defun al-dedupe-entries (xs)
+  "Remove the entries in XS where the key-value pair are `equal'."
+  (delete-dups xs))
+
+(defun al-keys (xs)
+  "Return a list of the keys in XS."
+  (mapcar 'car xs))
+
+(defun al-values (xs)
+  "Return a list of the values in XS."
+  (mapcar 'cdr xs))
+
+(defun al-has-key? (k xs)
+  "Return t if XS has a key `equal' to K."
+  (maybe-some? (assoc k xs)))
+
+(defun al-has-value? (v xs)
+  "Return t if XS has a value of V."
+  (maybe-some? (rassoc v xs)))
+
+(defun al-count (xs)
+  "Return the number of entries in XS."
+  (length xs))
+
+;; TODO: Should I support `al-find-key' and `al-find-value' variants?
+(defun al-find (p xs)
+  "Find an element in XS.
+
+Apply a predicate fn, P, to each key and value in XS and return the key of the
+first element that returns t."
+  (let ((result (list-find (lambda (x) (funcall p (car x) (cdr x))) xs)))
+    (if result
+        (car result)
+      nil)))
+
+(defun al-map-keys (f xs)
+  "Call F on the values in XS, returning a new alist."
+  (list-map (lambda (x)
+              `(,(funcall f (car x)) . ,(cdr x)))
+            xs))
+
+(defun al-map-values (f xs)
+  "Call F on the values in XS, returning a new alist."
+  (list-map (lambda (x)
+              `(,(car x) . ,(funcall f (cdr x))))
+            xs))
+
+(defun al-reduce (acc f xs)
+  "Return a new alist by calling F on k v and ACC from XS.
+F should return a tuple.  See tuple.el for more information."
+  (->> (al-keys xs)
+       (list-reduce acc
+                    (lambda (k acc)
+                      (funcall f k (al-get k xs) acc)))))
+
+(defun al-merge (a b)
+  "Return a new alist with a merge of alists, A and B.
+In this case, the last writer wins, which is B."
+  (al-reduce a #'al-set b))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(when al-enable-tests?
+  (prelude-assert
+   (equal '((2 . one)
+            (3 . two))
+          (al-map-keys #'1+
+                          '((1 . one)
+                            (2 . two)))))
+  (prelude-assert
+   (equal '((one . 2)
+            (two . 3))
+          (al-map-values #'1+
+                            '((one . 1)
+                              (two . 2))))))
+
+
+;; TODO: Support test cases for the entire API.
+
+(provide 'al)
+;;; al.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/bag.el b/users/wpcarro/emacs/.emacs.d/wpc/bag.el
new file mode 100644
index 0000000000..467e25fceb
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/bag.el
@@ -0,0 +1,70 @@
+;;; bag.el --- Working with bags (aka multi-sets) -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; What is a bag?  A bag should be thought of as a frequency table.  It's a way
+;; to convert a list of something into a set that allows duplicates.  Isn't
+;; allowing duplicates the whole thing with Sets?  Kind of.  But the interface
+;; of Sets is something that bags resemble, so multi-set isn't as bag of a name
+;; as it may first seem.
+;;
+;; If you've used Python's collections.Counter, the concept of a bag should be
+;; familiar already.
+;;
+;; Interface:
+;; - add        :: x -> Bag(x) -> Bag(x)
+;; - remove     :: x -> Bag(x) -> Bag(x)
+;; - union      :: Bag(x) -> Bag(x) -> Bag(x)
+;; - difference :: Bag(x) -> Bag(x) -> Bag(x)
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'al)
+(require 'number)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct bag xs)
+
+(defun bag-update (f xs)
+  "Call F on alist in XS."
+  (let ((ys (bag-xs xs)))
+    (setf (bag-xs xs) (funcall f ys))))
+
+(defun bag-new ()
+  "Create an empty bag."
+  (make-bag :xs (al-new)))
+
+(defun bag-contains? (x xs)
+  "Return t if XS has X."
+  (al-has-key? x (bag-xs xs)))
+
+;; TODO: Tabling this for now since working with structs seems to be
+;; disappointingly difficult.  Where is `struct-update'?
+;; (defun bag-add (x xs)
+;;   "Add X to XS.")
+
+;; TODO: What do we name delete vs. remove?
+;; (defun bag-remove (x xs)
+;;   "Remove X from XS.
+;; This is a no-op is X doesn't exist in XS.")
+
+(defun bag-from-list (xs)
+  "Map a list of `XS' into a bag."
+  (->> xs
+       (list-reduce
+        (bag-new)
+        (lambda (x acc)
+          (bag-add x 1 #'number-inc acc)))))
+
+(provide 'bag)
+;;; bag.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/bookmark.el b/users/wpcarro/emacs/.emacs.d/wpc/bookmark.el
new file mode 100644
index 0000000000..ab9169a078
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/bookmark.el
@@ -0,0 +1,50 @@
+;;; bookmark.el --- Saved files and directories on my filesystem -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; A more opinionated version of Emacs's builtin `jump-to-register'.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'project)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct bookmark label path kbd)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; API
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun bookmark-open (b)
+  "Open bookmark, B, as either a project directory or a regular directory."
+  (with-temp-buffer
+    (cd (bookmark-path b))
+    (call-interactively #'project-find-file)))
+
+(defun bookmark-install-kbd (b)
+  "Define two functions to explore B and assign them to keybindings."
+  (eval `(defun ,(intern (format "bookmark-visit-%s" (bookmark-label b))) ()
+           (interactive)
+           (find-file ,(bookmark-path b))))
+  (eval `(defun ,(intern (format "bookmark-browse-%s" (bookmark-label b))) ()
+           (interactive)
+           (bookmark-open ,b)))
+  (general-define-key
+   :prefix "<SPC>"
+   :states '(motion)
+   (format "J%s" (bookmark-kbd b)) `,(intern (format "bookmark-visit-%s" (bookmark-label b)))
+   (format "j%s" (bookmark-kbd b)) `,(intern (format "bookmark-browse-%s" (bookmark-label b)))))
+
+(provide 'bookmark)
+;;; bookmark.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/buffer.el b/users/wpcarro/emacs/.emacs.d/wpc/buffer.el
new file mode 100644
index 0000000000..fa98393df8
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/buffer.el
@@ -0,0 +1,173 @@
+;;; buffer.el --- Working with buffers -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Utilities for CRUDing buffers in Emacs.
+;;
+;; Many of these functions may seem unnecessary especially when you consider
+;; there implementations.  In general I believe that Elisp suffers from a
+;; library disorganization problem.  Providing simple wrapper functions that
+;; rename functions or reorder parameters is worth the effort in my opinion if
+;; it improves discoverability (via intuition) and improve composability.
+;;
+;; I support three ways for switching between what I'm calling "source code
+;; buffers":
+;; 1. Toggling previous: <SPC><SPC>
+;; 2. Using `ivy-read': <SPC>b
+;; TODO: These obscure evil KBDs.  Maybe a hydra definition would be best?
+;; 3. Cycling (forwards/backwards): C-f, C-b
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'maybe)
+(require 'set)
+(require 'cycle)
+(require 'struct)
+(require 'ts)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst buffer-source-code-blacklist
+  (set-new 'dired-mode
+           'erc-mode
+           'vterm-mode
+           'magit-status-mode
+           'magit-process-mode
+           'magit-log-mode
+           'magit-diff-mode
+           'org-mode
+           'fundamental-mode)
+  "A blacklist of major-modes to ignore for listing source code buffers.")
+
+(defconst buffer-ivy-source-code-whitelist '("*scratch*" "*Messages*")
+  "A whitelist of buffers to include when listing source code buffers.")
+
+(defconst buffer-source-code-timeout 2
+  "Number of seconds to wait before invalidating the cycle.")
+
+(cl-defstruct source-code-cycle cycle last-called)
+
+(defun buffer-emacs-generated? (name)
+  "Return t if buffer, NAME, is an Emacs-generated buffer.
+Some buffers are Emacs-generated but are surrounded by whitespace."
+  (let ((trimmed (s-trim name)))
+    (and (s-starts-with? "*" trimmed))))
+
+(defun buffer-find (buffer-or-name)
+  "Find a buffer by its BUFFER-OR-NAME."
+  (get-buffer buffer-or-name))
+
+(defun buffer-major-mode (name)
+  "Return the active `major-mode' in buffer, NAME."
+  (with-current-buffer (buffer-find name)
+    major-mode))
+
+(defun buffer-source-code-buffers ()
+  "Return a list of source code buffers.
+This will ignore Emacs-generated buffers, like *Messages*.  It will also ignore
+  any buffer whose major mode is defined in `buffer-source-code-blacklist'."
+  (->> (buffer-list)
+       (list-map #'buffer-name)
+       (list-reject #'buffer-emacs-generated?)
+       (list-reject (lambda (name)
+                      (set-contains? (buffer-major-mode name)
+                                     buffer-source-code-blacklist)))))
+
+(defvar buffer-source-code-cycle-state
+  (make-source-code-cycle
+   :cycle (cycle-from-list (buffer-source-code-buffers))
+   :last-called (ts-now))
+  "State used to manage cycling between source code buffers.")
+
+(defun buffer-exists? (name)
+  "Return t if buffer, NAME, exists."
+  (maybe-some? (buffer-find name)))
+
+(defun buffer-new (name)
+  "Return a newly created buffer NAME."
+  (generate-new-buffer name))
+
+(defun buffer-find-or-create (name)
+  "Find or create buffer, NAME.
+Return a reference to that buffer."
+  (let ((x (buffer-find name)))
+    (if (maybe-some? x)
+        x
+      (buffer-new name))))
+
+;; TODO: Should this consume: `display-buffer' or `switch-to-buffer'?
+(defun buffer-show (buffer-or-name)
+  "Display the BUFFER-OR-NAME, which is either a buffer reference or its name."
+  (display-buffer buffer-or-name))
+
+;; TODO: Move this and `buffer-cycle-prev' into a separate module that
+;; encapsulates all of this behavior.
+
+(defun buffer-cycle (cycle-fn)
+  "Using CYCLE-FN, move through `buffer-source-code-buffers'."
+  (let ((last-called (source-code-cycle-last-called
+                      buffer-source-code-cycle-state))
+        (cycle (source-code-cycle-cycle
+                buffer-source-code-cycle-state)))
+    (if (> (ts-diff (ts-now) last-called)
+           buffer-source-code-timeout)
+        (progn
+          (struct-set! source-code-cycle
+                       cycle
+                       (cycle-from-list (buffer-source-code-buffers))
+                       buffer-source-code-cycle-state)
+          (let ((cycle (source-code-cycle-cycle
+                        buffer-source-code-cycle-state)))
+            (funcall cycle-fn cycle)
+            (switch-to-buffer (cycle-current cycle)))
+          (struct-set! source-code-cycle
+                       last-called
+                       (ts-now)
+                       buffer-source-code-cycle-state))
+      (progn
+        (funcall cycle-fn cycle)
+        (switch-to-buffer (cycle-current cycle))))))
+
+(defun buffer-cycle-next ()
+  "Cycle forward through the `buffer-source-code-buffers'."
+  (interactive)
+  (buffer-cycle #'cycle-next))
+
+(defun buffer-cycle-prev ()
+  "Cycle backward through the `buffer-source-code-buffers'."
+  (interactive)
+  (buffer-cycle #'cycle-prev))
+
+(defun buffer-ivy-source-code ()
+  "Use `ivy-read' to choose among all open source code buffers."
+  (interactive)
+  (ivy-read "Source code buffer: "
+            (-concat buffer-ivy-source-code-whitelist
+                     (-drop 1 (buffer-source-code-buffers)))
+            :sort nil
+            :action #'switch-to-buffer))
+
+(defun buffer-show-previous ()
+  "Call `switch-to-buffer' on the previously visited buffer.
+This function ignores Emacs-generated buffers, i.e. the ones that look like
+  this: *Buffer*.  It also ignores buffers that are `dired-mode' or `erc-mode'.
+  This blacklist can easily be changed."
+  (interactive)
+  (let* ((xs (buffer-source-code-buffers))
+         (candidate (list-get 1 xs)))
+    (prelude-assert (maybe-some? candidate))
+    (switch-to-buffer candidate)))
+
+(provide 'buffer)
+;;; buffer.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/bytes.el b/users/wpcarro/emacs/.emacs.d/wpc/bytes.el
new file mode 100644
index 0000000000..b76921d3c7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/bytes.el
@@ -0,0 +1,112 @@
+;;; bytes.el --- Working with byte values -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Functions to help with human-readable representations of byte values.
+;;
+;; Usage:
+;; See the test cases for example usage.  Or better yet, I should use a type of
+;; structured documentation that would allow me to expose a view into the test
+;; suite here.  Is this currently possible in Elisp?
+;;
+;; API:
+;; - serialize :: Integer -> String
+;;
+;; Wish list:
+;; - Rounding: e.g. (bytes (* 1024 1.7)) => "2KB"
+
+;;; Code:
+
+;; TODO: Support -ibabyte variants like Gibibyte (GiB).
+
+;; Ranges:
+;;  B: [   0,  1e3)
+;; KB: [ 1e3,  1e6)
+;; MB: [ 1e6,  1e6)
+;; GB: [ 1e9, 1e12)
+;; TB: [1e12, 1e15)
+;; PB: [1e15, 1e18)
+;;
+;; Note: I'm currently not support exabytes because that causes the integer to
+;;  overflow.  I imagine a larger integer type may exist, but for now, I'll
+;;  treat this as a YAGNI.
+
+(require 'prelude)
+(require 'tuple)
+(require 'math)
+(require 'number)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst bytes-kb (math-exp 2 10)
+  "Number of bytes in a kilobyte.")
+
+(defconst bytes-mb (math-exp 2 20)
+  "Number of bytes in a megabytes.")
+
+(defconst bytes-gb (math-exp 2 30)
+  "Number of bytes in a gigabyte.")
+
+(defconst bytes-tb (math-exp 2 40)
+  "Number of bytes in a terabyte.")
+
+(defconst bytes-pb (math-exp 2 50)
+  "Number of bytes in a petabyte.")
+
+(defconst bytes-eb (math-exp 2 60)
+  "Number of bytes in an exabyte.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun bytes-classify (x)
+  "Return unit that closest fits byte count, X."
+  (prelude-assert (number-whole? x))
+  (cond
+   ((and (>= x 0)        (< x bytes-kb))     'byte)
+   ((and (>= x bytes-kb) (< x bytes-mb)) 'kilobyte)
+   ((and (>= x bytes-mb) (< x bytes-gb)) 'megabyte)
+   ((and (>= x bytes-gb) (< x bytes-tb)) 'gigabyte)
+   ((and (>= x bytes-tb) (< x bytes-pb)) 'terabyte)
+   ((and (>= x bytes-pb) (< x bytes-eb)) 'petabyte)))
+
+(defun bytes-to-string (x)
+  "Convert integer X into a human-readable string."
+  (let ((base-and-unit
+         (pcase (bytes-classify x)
+           ('byte     (tuple/from        1 "B"))
+           ('kilobyte (tuple/from bytes-kb "KB"))
+           ('megabyte (tuple/from bytes-mb "MB"))
+           ('gigabyte (tuple/from bytes-gb "GB"))
+           ('terabyte (tuple/from bytes-tb "TB"))
+           ('petabyte (tuple/from bytes-pb "PB")))))
+    (string-format "%d%s"
+                   (round x (tuple/first base-and-unit))
+                   (tuple/second base-and-unit))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(progn
+  (prelude-assert
+   (equal "1000B" (bytes-to-string 1000)))
+  (prelude-assert
+   (equal "2KB" (bytes-to-string (* 2 bytes-kb))))
+  (prelude-assert
+   (equal "17MB" (bytes-to-string (* 17 bytes-mb))))
+  (prelude-assert
+   (equal "419GB" (bytes-to-string (* 419 bytes-gb))))
+  (prelude-assert
+   (equal "999TB" (bytes-to-string (* 999 bytes-tb))))
+  (prelude-assert
+   (equal "2PB" (bytes-to-string (* 2 bytes-pb)))))
+
+(provide 'bytes)
+;;; bytes.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/cache.el b/users/wpcarro/emacs/.emacs.d/wpc/cache.el
new file mode 100644
index 0000000000..70ebdb71ef
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/cache.el
@@ -0,0 +1,88 @@
+;;; cache.el --- Caching things -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; An immutable cache data structure.
+;;
+;; This is like a sideways stack, that you can pull values out from and re-push
+;; to the top.  It'd be like a stack supporting push, pop, pull.
+;;
+;; This isn't a key-value data-structure like you might expect from a
+;; traditional cache.  The name is subject to change, but the underlying idea of
+;; a cache remains the same.
+;;
+;; Think about prescient.el, which uses essentially an LRU cache integrated into
+;; counsel to help create a "clairovoyant", self-organizing list.
+;;
+;; Use-cases:
+;; - Keeps an cache of workspaces sorted as MRU with an LRU eviction strategy.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'struct)
+(require '>)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct cache xs)
+
+;; TODO: Prefer another KBD for yasnippet form completion than company-mode's
+;; current KBD.
+
+(defun cache-from-list (xs)
+  "Turn list, XS, into a cache."
+  (make-cache :xs xs))
+
+(defun cache-contains? (x xs)
+  "Return t if X in XS."
+  (->> xs
+       cache-xs
+       (list-contains? x)))
+
+(defun cache-touch (x xs)
+  "Ensure value X in cache, XS, is front of the list.
+If X isn't in XS (using `equal'), insert it at the front."
+  (struct-update
+   cache
+   xs
+   (>-> (list-reject (lambda (y) (equal x y)))
+       (list-cons x))
+   xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(progn
+  (let ((cache (cache-from-list '("chicken" "nugget"))))
+    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+    ;; contains?/2
+    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+    (prelude-refute
+     (cache-contains? "turkey" cache))
+    (prelude-assert
+     (cache-contains? "chicken" cache))
+    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+    ;; touch/2
+    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+    (prelude-assert
+     (equal
+      (cache-touch "nugget" cache)
+      (cache-from-list '("nugget" "chicken"))))
+    (prelude-assert
+     (equal
+      (cache-touch "spicy" cache)
+      (cache-from-list '("spicy" "chicken" "nugget"))))))
+
+(provide 'cache)
+;;; cache.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/clipboard.el b/users/wpcarro/emacs/.emacs.d/wpc/clipboard.el
new file mode 100644
index 0000000000..ec2a46f540
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/clipboard.el
@@ -0,0 +1,40 @@
+;;; clipboard.el --- Working with X11's pasteboard -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Simple functions for copying and pasting.
+;;
+;; Integrate with bburns/clipmon so that System Clipboard can integrate with
+;; Emacs's kill-ring.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defun clipboard-copy (x &key (message "[clipboard.el] Copied!"))
+  "Copy string, X, to X11's clipboard and `message' MESSAGE."
+  (kill-new x)
+  (message message))
+
+(cl-defun clipboard-paste (&key (message "[clipboard.el] Pasted!"))
+  "Paste contents of X11 clipboard and `message' MESSAGE."
+  (yank)
+  (message message))
+
+(defun clipboard-contents ()
+  "Return the contents of the clipboard as a string."
+  (substring-no-properties (current-kill 0)))
+
+(provide 'clipboard)
+;;; clipboard.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/colorscheme.el b/users/wpcarro/emacs/.emacs.d/wpc/colorscheme.el
new file mode 100644
index 0000000000..cc2afd6c57
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/colorscheme.el
@@ -0,0 +1,85 @@
+;;; colorscheme.el --- Syntax highlight and friends -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;;
+;; TODO: Clarify this.
+;; Since I have my own definition of "theme", which couples wallpaper, font,
+;; with Emacs's traditional notion of the word "theme", I'm choosing to use
+;; "colorscheme" to refer to *just* the notion of syntax highlight etc.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'cycle)
+(require '>)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defcustom colorscheme-whitelist
+  (cycle-from-list
+   (->> (custom-available-themes)
+        (list-map #'symbol-name)
+        (list-filter (>-> (s-starts-with? "doom-")))
+        (list-map #'intern)))
+  "The whitelist of colorschemes through which to cycle.")
+
+(defun colorscheme-current ()
+  "Return the currently enabled colorscheme."
+  (cycle-current colorscheme-whitelist))
+
+(defun colorscheme-disable-all ()
+  "Disable all currently enabled colorschemes."
+  (interactive)
+  (->> custom-enabled-themes
+       (list-map #'disable-theme)))
+
+(defun colorscheme-set (theme)
+    "Call `load-theme' with `THEME', ensuring that the line numbers are bright.
+There is no hook that I'm aware of to handle this more elegantly."
+    (load-theme theme t)
+    (prelude-set-line-number-color "#da5468"))
+
+(defun colorscheme-whitelist-set (colorscheme)
+  "Focus the COLORSCHEME in the `colorscheme-whitelist' cycle."
+  (cycle-focus (lambda (x) (equal x colorscheme)) colorscheme-whitelist)
+  (colorscheme-set (colorscheme-current)))
+
+(defun colorscheme-ivy-select ()
+  "Load a colorscheme using ivy."
+  (interactive)
+  (let ((theme (ivy-read "Theme: " (cycle-to-list colorscheme-whitelist))))
+    (colorscheme-disable-all)
+    (colorscheme-set (intern theme))))
+
+(cl-defun colorscheme-cycle (&key forward?)
+  "Cycle next if `FORWARD?' is non-nil.
+Cycle prev otherwise."
+  (disable-theme (cycle-current colorscheme-whitelist))
+  (let ((theme (if forward?
+                   (cycle-next colorscheme-whitelist)
+                 (cycle-prev colorscheme-whitelist))))
+    (colorscheme-set theme)
+    (message (s-concat "Active theme: " (symbol-to-string theme)))))
+
+(defun colorscheme-next ()
+  "Disable the currently active theme and load the next theme."
+  (interactive)
+  (colorscheme-cycle :forward? t))
+
+(defun colorscheme-prev ()
+  "Disable the currently active theme and load the previous theme."
+  (interactive)
+  (colorscheme-cycle :forward? nil))
+
+(provide 'colorscheme)
+;;; colorscheme.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/constants.el b/users/wpcarro/emacs/.emacs.d/wpc/constants.el
new file mode 100644
index 0000000000..69003f5955
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/constants.el
@@ -0,0 +1,26 @@
+;;; constants.el --- Constants for organizing my Elisp -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; This file contains constants that are shared across my configuration.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'maybe)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst constants-ci? (maybe-some? (getenv "CI"))
+  "Encoded as t when Emacs is running in CI.")
+
+(provide 'constants)
+;;; constants.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/cycle.el b/users/wpcarro/emacs/.emacs.d/wpc/cycle.el
new file mode 100644
index 0000000000..a1853ece14
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/cycle.el
@@ -0,0 +1,224 @@
+;;; cycle.el --- Simple module for working with cycles -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Something like this may already exist, but I'm having trouble finding it, and
+;; I think writing my own is a nice exercise for learning more Elisp.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'math)
+(require 'maybe)
+(require 'struct)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Wish list
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; - TODO: Provide immutable variant.
+;; - TODO: Replace mutable consumption with immutable variant.
+;; - TODO: Replace indexing with (math-mod current cycle).
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; `current-index' tracks the current index
+;; `xs' is the original list
+(cl-defstruct cycle current-index previous-index xs)
+
+(defconst cycle-enable-tests? t
+  "When t, run the tests defined herein.")
+
+(defun cycle-from-list (xs)
+  "Create a cycle from a list of `XS'."
+  (if (= 0 (length xs))
+      (make-cycle :current-index nil
+                  :previous-index nil
+                  :xs xs)
+    (make-cycle :current-index 0
+                :previous-index nil
+                :xs xs)))
+
+(defun cycle-new (&rest xs)
+  "Create a cycle with XS as the values."
+  (cycle-from-list xs))
+
+(defun cycle-to-list (xs)
+  "Return the list representation of a cycle, XS."
+  (cycle-xs xs))
+
+(defun cycle--next-index<- (lo hi x)
+  "Return the next index in a cycle when moving downwards.
+- `LO' is the lower bound.
+- `HI' is the upper bound.
+- `X' is the current index."
+  (if (< (- x 1) lo)
+      (- hi 1)
+    (- x 1)))
+
+(defun cycle--next-index-> (lo hi x)
+  "Return the next index in a cycle when moving upwards.
+- `LO' is the lower bound.
+- `HI' is the upper bound.
+- `X' is the current index."
+  (if (>= (+ 1 x) hi)
+      lo
+    (+ 1 x)))
+
+(defun cycle-previous-focus (cycle)
+  "Return the previously focused entry in CYCLE."
+  (let ((i (cycle-previous-index cycle)))
+    (if (maybe-some? i)
+        (nth i (cycle-xs cycle))
+      nil)))
+
+;; TODO: Consider adding "!" to the function name herein since many of them
+;; mutate the collection, and the APIs are beginning to confuse me.
+(defun cycle-focus-previous! (xs)
+  "Jump to the item in XS that was most recently focused; return the cycle.
+This will error when previous-index is nil.  This function mutates the
+underlying struct."
+  (let ((i (cycle-previous-index xs)))
+    (if (maybe-some? i)
+        (progn
+          (cycle-jump i xs)
+          (cycle-current xs))
+      (error "Cannot focus the previous element since cycle-previous-index is nil"))))
+
+(defun cycle-next (xs)
+  "Return the next value in `XS' and update `current-index'."
+  (let* ((current-index (cycle-current-index xs))
+         (next-index (cycle--next-index-> 0 (cycle-count xs) current-index)))
+    (struct-set! cycle previous-index current-index xs)
+    (struct-set! cycle current-index next-index xs)
+    (nth next-index (cycle-xs xs))))
+
+(defun cycle-prev (xs)
+  "Return the previous value in `XS' and update `current-index'."
+  (let* ((current-index (cycle-current-index xs))
+         (next-index (cycle--next-index<- 0 (cycle-count xs) current-index)))
+    (struct-set! cycle previous-index current-index xs)
+    (struct-set! cycle current-index next-index xs)
+    (nth next-index (cycle-xs xs))))
+
+(defun cycle-current (cycle)
+  "Return the current value in `CYCLE'."
+  (nth (cycle-current-index cycle) (cycle-xs cycle)))
+
+(defun cycle-count (cycle)
+  "Return the length of `xs' in `CYCLE'."
+  (length (cycle-xs cycle)))
+
+(defun cycle-jump (i xs)
+  "Jump to the I index of XS."
+  (let ((current-index (cycle-current-index xs))
+        (next-index (math-mod i (cycle-count xs))))
+    (struct-set! cycle previous-index current-index xs)
+    (struct-set! cycle current-index next-index xs))
+  xs)
+
+(defun cycle-focus (p cycle)
+  "Focus the element in CYCLE for which predicate, P, is t."
+  (let ((i (->> cycle
+                cycle-xs
+                (-find-index p))))
+    (if i
+        (cycle-jump i cycle)
+      (error "No element in cycle matches predicate"))))
+
+(defun cycle-focus-item (x xs)
+  "Focus item, X, in cycle XS.
+ITEM is the first item in XS that t for `equal'."
+  (cycle-focus (lambda (y) (equal x y)) xs))
+
+(defun cycle-contains? (x xs)
+  "Return t if cycle, XS, has member X."
+  (->> xs
+       cycle-xs
+       (list-contains? x)))
+
+(defun cycle-empty? (xs)
+  "Return t if cycle XS has no elements."
+  (= 0 (length (cycle-xs xs))))
+
+(defun cycle-focused? (xs)
+  "Return t if cycle XS has a non-nil value for current-index."
+  (maybe-some? (cycle-current-index xs)))
+
+(defun cycle-append (x xs)
+  "Add X to the left of the focused element in XS.
+If there is no currently focused item, add X to the beginning of XS."
+  (if (cycle-empty? xs)
+      (progn
+        (struct-set! cycle xs (list x) xs)
+        (struct-set! cycle current-index 0 xs)
+        (struct-set! cycle previous-index nil xs))
+    (let ((curr-i (cycle-current-index xs))
+          (prev-i (cycle-previous-index xs)))
+      (if curr-i
+          (progn
+            (struct-set! cycle xs (-insert-at curr-i x (cycle-xs xs)) xs)
+            (when (and prev-i (>= prev-i curr-i))
+              (struct-set! cycle previous-index (1+ prev-i) xs))
+            (when curr-i (struct-set! cycle current-index (1+ curr-i) xs)))
+        (progn
+          (struct-set! cycle xs (cons x (cycle-xs xs)) xs)
+          (when prev-i (struct-set! cycle previous-index (1+ prev-i) xs))))
+      xs)))
+
+(defun cycle-remove (x xs)
+  "Attempt to remove X from XS.
+
+X is found using `equal'.
+
+If X is the currently focused value, after it's deleted, current-index will be
+  nil.  If X is the previously value, after it's deleted, previous-index will be
+  nil."
+  (let ((curr-i (cycle-current-index xs))
+        (prev-i (cycle-previous-index xs))
+        (rm-i (-elem-index x (cycle-xs xs))))
+    (struct-set! cycle xs (-remove-at rm-i (cycle-xs xs)) xs)
+    (when prev-i
+      (when (> prev-i rm-i) (struct-set! cycle previous-index (1- prev-i) xs))
+      (when (= prev-i rm-i) (struct-set! cycle previous-index nil xs)))
+    (when curr-i
+      (when (> curr-i rm-i) (struct-set! cycle current-index (1- curr-i) xs))
+      (when (= curr-i rm-i) (struct-set! cycle current-index nil xs)))
+    xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(when cycle-enable-tests?
+  (let ((xs (cycle-new 1 2 3)))
+    (prelude-assert (maybe-nil? (cycle-previous-focus xs)))
+    (prelude-assert (= 1 (cycle-current xs)))
+    (prelude-assert (= 2 (cycle-next xs)))
+    (prelude-assert (= 1 (cycle-previous-focus xs)))
+    (prelude-assert (= 1 (->> xs (cycle-jump 0) cycle-current)))
+    (prelude-assert (= 2 (->> xs (cycle-jump 1) cycle-current)))
+    (prelude-assert (= 3 (->> xs (cycle-jump 2) cycle-current)))
+    (prelude-assert (= 2 (cycle-previous-focus xs)))
+    (prelude-assert (= 2 (cycle-focus-previous! xs)))
+    (prelude-assert (equal '(1 4 2 3) (cycle-xs (cycle-append 4 xs))))
+    (prelude-assert (equal '(1 2 3) (cycle-xs (cycle-remove 4 xs))))
+    (progn
+      (cycle-focus-item 3 xs)
+      (cycle-focus-item 2 xs)
+      (cycle-remove 1 xs)
+      (prelude-assert (= 2 (cycle-current xs)))
+      (prelude-assert (= 3 (cycle-previous-focus xs))))))
+
+(provide 'cycle)
+;;; cycle.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/device.el b/users/wpcarro/emacs/.emacs.d/wpc/device.el
new file mode 100644
index 0000000000..09819ad748
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/device.el
@@ -0,0 +1,62 @@
+;;; device.el --- Physical device information -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Functions for querying device information.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'dash)
+(require 'al)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO(wpcarro): Consider integrating this with Nix and depot instead of
+;; denormalizing the state.
+(defconst device-hostname->device
+  '(("zeno.lon.corp.google.com" . work-desktop-lon)
+    ("wpcarro.svl.corp.google.com" . work-desktop-svl)
+    ("seneca" . work-laptop)
+    ("marcus" . personal-laptop)
+    ("diogenes" . personal-vm))
+  "Mapping hostname to a device symbol.")
+
+;; TODO: Should I generate these predicates?
+
+(defun device-classify ()
+  "Return the device symbol for the current host or nil if not supported."
+  (al-get system-name device-hostname->device))
+
+(defun device-work-laptop? ()
+  "Return t if current device is work laptop."
+  (equal 'work-laptop
+         (device-classify)))
+
+(defun device-work-desktop? ()
+  "Return t if current device is work desktop."
+  (-contains? '(work-desktop-lon
+                work-desktop-svl)
+              (device-classify)))
+
+(defun device-corporate? ()
+  "Return t if the current device is owned by my company."
+  (-contains? '(work-desktop-lon
+                work-desktop-svl
+                work-laptop)
+              (device-classify)))
+
+(defun device-laptop? ()
+  "Return t if the current device is a laptop."
+  (-contains? '(work-laptop personal-laptop) (device-classify)))
+
+(provide 'device)
+;;; device.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/display.el b/users/wpcarro/emacs/.emacs.d/wpc/display.el
new file mode 100644
index 0000000000..69dae6939e
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/display.el
@@ -0,0 +1,103 @@
+;;; display.el --- Working with single or multiple displays -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Mostly wrappers around xrandr.
+;;
+;; Troubleshooting:
+;; The following commands help me when I (infrequently) interact with xrandr.
+;; - xrandr --listmonitors
+;; - xrandr --query
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'dash)
+(require 's)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defmacro display-register (name &key
+                                    output
+                                    primary
+                                    coords
+                                    size
+                                    rate
+                                    dpi
+                                    rotate)
+  "Macro to define constants and two functions for {en,dis}abling a display.
+
+NAME    - the human-readable identifier for the display
+OUTPUT  - the xrandr identifier for the display
+PRIMARY - if true, send --primary flag to xrandr
+COORDS  - X and Y offsets
+SIZE    - the pixel resolution of the display (width height)
+RATE    - the refresh rate
+DPI     - the pixel density in dots per square inch
+rotate  - one of {normal,left,right,inverted}
+
+See the man-page for xrandr for more details."
+  `(progn
+     (defconst ,(intern (format "display-%s" name)) ,output
+       ,(format "The xrandr identifier for %s" name))
+     (defconst ,(intern (format "display-%s-args" name))
+       ,(replace-regexp-in-string
+         "\s+" " "
+         (s-format "--output ${output} ${primary-flag} --auto \
+                    --size ${size-x}x${size-y} --rate ${rate} --dpi ${dpi} \
+                    --rotate ${rotate} ${pos-flag}"
+                   #'aget
+                   `(("output" . ,output)
+                     ("primary-flag" . ,(if primary "--primary" "--noprimary"))
+                     ("pos-flag" . ,(if coords
+                                        (format "--pos %dx%d"
+                                                (car coords)
+                                                (cadr coords))
+                                      ""))
+                     ("size-x" . ,(car size))
+                     ("size-y" . ,(cadr size))
+                     ("rate" . ,rate)
+                     ("dpi" . ,dpi)
+                     ("rotate" . ,rotate))))
+       ,(format "The arguments we pass to xrandr for display-%s." name))
+     (defconst ,(intern (format "display-%s-command" name))
+       (format "xrandr %s" ,(intern (format "display-%s-args" name)))
+       ,(format "The command we run to configure %s" name))
+     (defun ,(intern (format "display-enable-%s" name)) ()
+       ,(format "Attempt to enable my %s monitor" name)
+       (interactive)
+       (prelude-start-process
+        :name ,(format "display-enable-%s" name)
+        :command ,(intern (format "display-%s-command" name))))
+     (defun ,(intern (format "display-disable-%s" name)) ()
+       ,(format "Attempt to disable my %s monitor." name)
+       (interactive)
+       (prelude-start-process
+        :name ,(format "display-disable-%s" name)
+        :command ,(format
+                   "xrandr --output %s --off"
+                   output)))))
+
+(defmacro display-arrangement (name &key displays)
+  "Create a function, display-arrange-<NAME>, to enable all your DISPLAYS."
+  `(defun ,(intern (format "display-arrange-%s" name)) ()
+     (interactive)
+     (prelude-start-process
+      :name ,(format "display-configure-%s" name)
+      :command ,(format "xrandr %s"
+                        (->> displays
+                             (-map (lambda (x)
+                                     (eval (intern (format "display-%s-args" x)))))
+                             (s-join " "))))))
+
+(provide 'display)
+;;; display.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/dotted.el b/users/wpcarro/emacs/.emacs.d/wpc/dotted.el
new file mode 100644
index 0000000000..b824ddbda7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/dotted.el
@@ -0,0 +1,57 @@
+;;; dotted.el --- Working with dotted pairs in Elisp -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Part of my primitives library extensions in Elisp.  Contrast my primitives
+;; with the wrapper extensions that I provide, which expose immutable variants
+;; of data structures like an list, alist, tuple, as well as quasi-typeclasses
+;; like sequence, etc.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'macros)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defun dotted-new (&optional a b)
+  "Create a new dotted pair of A and B."
+  (cons a b))
+
+(defun dotted-instance? (x)
+  "Return t if X is a dotted pair."
+  (let ((b (cdr x)))
+    (and b (atom b))))
+
+(defun dotted-first (x)
+  "Return the first element of X."
+  (car x))
+
+(defun dotted-second (x)
+  "Return the second element of X."
+  (cdr x))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(progn
+  (prelude-assert
+   (equal '(fname . "Bob") (dotted-new 'fname "Bob")))
+  (prelude-assert
+   (dotted-instance? '(one . two)))
+  (prelude-refute
+   (dotted-instance? '(1 2 3))))
+
+(provide 'dotted)
+;;; dotted.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/email.el b/users/wpcarro/emacs/.emacs.d/wpc/email.el
new file mode 100644
index 0000000000..a83ca25e6c
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/email.el
@@ -0,0 +1,76 @@
+;;; email.el --- My email settings -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Attempting to configure to `notmuch' for my personal use.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'notmuch)
+(require 'list)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(setq notmuch-saved-searches
+      '((:name "inbox" :query "tag:inbox" :key "i")
+        (:name "direct"
+         :query "tag:direct and tag:unread and not tag:sent"
+         :key "d")
+        (:name "action" :query "tag:action" :key "a")
+        (:name "review" :query "tag:review" :key "r")
+        (:name "waiting" :query "tag:waiting" :key "w")
+        (:name "broadcast" :query "tag:/broadcast\/.+/ and tag:unread" :key "b")
+        (:name "systems" :query "tag:/systems\/.+/ and tag:unread" :key "s")
+        (:name "sent" :query "tag:sent" :key "t")
+        (:name "drafts" :query "tag:draft" :key "D")))
+
+;; Sort results from newest-to-oldest.
+(setq notmuch-search-oldest-first nil)
+
+;; Discard noisy email signatures.
+(setq notmuch-mua-cite-function #'message-cite-original-without-signature)
+
+;; By default, this is just '("-inbox")
+(setq notmuch-archive-tags '("-inbox" "-unread" "+archive"))
+
+;; Show saved searches even when they're empty.
+(setq notmuch-show-empty-saved-searches t)
+
+;; Currently the sendmail executable on my system is symlinked to msmtp.
+(setq send-mail-function #'sendmail-send-it)
+
+;; I'm not sure if I need this or not. Copying it from tazjin@'s monorepo.
+(setq notmuch-always-prompt-for-sender nil)
+
+;; Add the "User-Agent" header to my emails and ensure that it includes Emacs
+;; and notmuch information.
+(setq notmuch-mua-user-agent-function
+      (lambda ()
+        (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version)))
+
+;; I was informed that Gmail does this server-side
+(setq notmuch-fcc-dirs nil)
+
+;; Ensure buffers are closed after sending mail.
+(setq message-kill-buffer-on-exit t)
+
+;; Ensure sender is correctly passed to msmtp.
+(setq mail-specify-envelope-from t
+      message-sendmail-envelope-from 'header
+      mail-envelope-from 'header)
+
+;; Assert that no two saved searches share share a KBD
+(prelude-assert
+ (list-xs-distinct-by? (lambda (x) (plist-get x :key)) notmuch-saved-searches))
+
+(provide 'email)
+;;; email.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/fonts.el b/users/wpcarro/emacs/.emacs.d/wpc/fonts.el
new file mode 100644
index 0000000000..196b882862
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/fonts.el
@@ -0,0 +1,167 @@
+;;; fonts.el --- Font preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Control my font preferences with ELisp.
+
+;;; Code:
+
+;; TODO: `defcustom' font-size.
+;; TODO: `defcustom' fonts.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'cycle)
+(require 'maybe)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Troubleshoot why "8" appears so large on my desktop.
+
+;; TODO: Consider having a different font size when I'm using my 4K monitor.
+
+(defconst fonts-size "10"
+  "My preferred default font-size.")
+
+(defconst fonts-size-step 10
+  "The amount (%) by which to increase or decrease a font.")
+
+(defconst fonts-hacker-news-recommendations
+  '("APL385 Unicode"
+    "Go Mono"
+    "Sudo"
+    "Monoid"
+    "Input Mono Medium" ;; NOTE: Also "Input Mono Thin" is nice.
+    )
+  "List of fonts optimized for programming I found in a HN article.")
+
+(defconst fonts-whitelist
+  (cycle-from-list
+   (list-concat
+    fonts-hacker-news-recommendations
+    '("JetBrainsMono"
+      "Mononoki Medium"
+      "Monospace"
+      "Operator Mono Light"
+      "Courier"
+      "Andale Mono"
+      "Source Code Pro"
+      "Terminus")))
+  "This is a list of my preferred fonts.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: fonts and fonts-whitelist make it difficult to name functions like
+;; fonts-set as a generic Emacs function vs choosing a font from the whitelist.
+
+(cl-defun fonts-cycle (&key forward?)
+  "Cycle forwards when `FORWARD?' non-nil."
+  (let ((font (if forward?
+                  (cycle-next fonts-whitelist)
+                (cycle-prev fonts-whitelist))))
+    (message (s-concat "Active font: " font))
+    (fonts-set font)))
+
+(defun fonts-next ()
+  "Quickly cycle through preferred fonts."
+  (interactive)
+  (fonts-cycle :forward? t))
+
+(defun fonts-prev ()
+  "Quickly cycle through preferred fonts."
+  (interactive)
+  (fonts-cycle :forward? nil))
+
+(defun fonts-set (font &optional size)
+  "Change the font to `FONT' with option integer, SIZE, in pixels."
+  (if (maybe-some? size)
+      (set-frame-font (string-format "%s %s" font size) nil t)
+    (set-frame-font font nil t)))
+
+(defun fonts-whitelist-set (font)
+  "Focuses the FONT in the `fonts-whitelist' cycle.
+The size of the font is determined by `fonts-size'."
+  (prelude-assert (cycle-contains? font fonts-whitelist))
+  (cycle-focus (lambda (x) (equal x font)) fonts-whitelist)
+  (fonts-set (fonts-current) fonts-size))
+
+(defun fonts-ivy-select ()
+  "Select a font from an ivy prompt."
+  (interactive)
+  (fonts-whitelist-set
+   (ivy-read "Font: " (cycle-to-list fonts-whitelist))))
+
+(defun fonts-print-current ()
+  "Message the currently enabled font."
+  (interactive)
+  (message
+   (string-format "[fonts] Current font: \"%s\""
+                  (fonts-current))))
+
+(defun fonts-current ()
+  "Return the currently enabled font."
+  (cycle-current fonts-whitelist))
+
+(defun fonts-increase-size ()
+  "Increase font size."
+  (interactive)
+  (->> (face-attribute 'default :height)
+       (+ fonts-size-step)
+       (set-face-attribute 'default (selected-frame) :height)))
+
+(defun fonts-decrease-size ()
+  "Decrease font size."
+  (interactive)
+  (->> (face-attribute 'default :height)
+       (+ (- fonts-size-step))
+       (set-face-attribute 'default (selected-frame) :height)))
+
+(defun fonts-reset-size ()
+  "Restore font size to its default value."
+  (interactive)
+  (fonts-whitelist-set (fonts-current)))
+
+(defun fonts-enable-ligatures ()
+  "Call this function to enable ligatures."
+  (interactive)
+  (let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
+                 (35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)") ;;
+                 (36 . ".\\(?:>\\)")
+                 (37 . ".\\(?:\\(?:%%\\)\\|%\\)")
+                 (38 . ".\\(?:\\(?:&&\\)\\|&\\)")
+                 (42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)") ;;
+                 (43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
+                 (45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
+                 (46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)") ;;
+                 (47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
+                 (48 . ".\\(?:x[a-zA-Z]\\)")
+                 (58 . ".\\(?:::\\|[:=]\\)")
+                 (59 . ".\\(?:;;\\|;\\)")
+                 (60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
+                 (61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
+                 (62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
+                 (63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
+                 (91 . ".\\(?:]\\)")
+                 (92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
+                 (94 . ".\\(?:=\\)")
+                 (119 . ".\\(?:ww\\)")
+                 (123 . ".\\(?:-\\)")
+                 (124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
+                 (126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)"))))
+    (dolist (char-regexp alist)
+      (set-char-table-range composition-function-table (car char-regexp)
+                            `([,(cdr char-regexp) 0 font-shape-gstring])))))
+
+(provide 'fonts)
+;;; fonts.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/fs.el b/users/wpcarro/emacs/.emacs.d/wpc/fs.el
new file mode 100644
index 0000000000..c303b23539
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/fs.el
@@ -0,0 +1,69 @@
+;;; fs.el --- Make working with the filesystem easier -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.1"))
+
+;;; Commentary:
+;; Ergonomic alternatives for working with the filesystem.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'dash)
+(require 'f)
+(require 's)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun fs-ensure-file (path)
+  "Ensure that a file and its directories in `PATH' exist.
+Will error for inputs with a trailing slash."
+  (when (s-ends-with? "/" path)
+    (error (format "Input path has trailing slash: %s" path)))
+  (->> path
+       f-dirname
+       fs-ensure-dir)
+  (f-touch path))
+
+(f-dirname "/tmp/a/b/file.txt")
+
+(defun fs-ensure-dir (path)
+  "Ensure that a directory and its ancestor directories in `PATH' exist."
+  (->> path
+       f-split
+       (apply #'f-mkdir)))
+
+(defun fs-ls (dir &optional full-path?)
+  "List the files in `DIR' one-level deep.
+Should behave similarly in spirit to the Unix command, ls.
+If `FULL-PATH?' is set, return the full-path of the files."
+  (-drop 2 (directory-files dir full-path?)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest fs-test-ensure-file ()
+  (let ((file "/tmp/file/a/b/c/file.txt"))
+    ;; Ensure this file doesn't exist first to prevent false-positives.
+    (f-delete file t)
+    (fs-ensure-file file)
+    (should (and (f-exists? file)
+                 (f-file? file)))))
+
+(ert-deftest fs-test-ensure-dir ()
+  (let ((dir "/tmp/dir/a/b/c"))
+    ;; Ensure the directory doesn't exist.
+    (f-delete dir t)
+    (fs-ensure-dir dir)
+    (should (and (f-exists? dir)
+                 (f-dir? dir)))))
+
+(provide 'fs)
+;;; fs.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/functions.el b/users/wpcarro/emacs/.emacs.d/wpc/functions.el
new file mode 100644
index 0000000000..936e25eb2d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/functions.el
@@ -0,0 +1,46 @@
+;;; functions.el --- Helper functions -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; This file hopefully contains friendly APIs that making ELisp development more
+;; enjoyable.
+
+;; TODO: Break these out into separate modules.
+
+;;; Code:
+(defun functions-evil-window-vsplit-right ()
+  "Split the window vertically and focus the right half."
+  (interactive)
+  (evil-window-vsplit)
+  (windmove-right))
+
+(defun functions-evil-window-split-down ()
+  "Split the window horizontal and focus the bottom half."
+  (interactive)
+  (evil-window-split)
+  (windmove-down))
+
+(defun functions-create-snippet ()
+  "Create a window split and then opens the Yasnippet editor."
+  (interactive)
+  (evil-window-vsplit)
+  (call-interactively #'yas-new-snippet))
+
+(defun functions-evil-replace-under-point ()
+  "Faster than typing %s//thing/g."
+  (interactive)
+  (let ((term (s-replace "/" "\\/" (symbol-to-string (symbol-at-point)))))
+    (save-excursion
+      (evil-ex (concat "%s/\\b" term "\\b/")))))
+
+(defun functions-buffer-dirname ()
+  "Return the directory name of the current buffer as a string."
+  (->> buffer-file-name
+       f-dirname
+       f-filename))
+
+(provide 'functions)
+;;; functions.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/graph.el b/users/wpcarro/emacs/.emacs.d/wpc/graph.el
new file mode 100644
index 0000000000..1d2f67a4dd
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/graph.el
@@ -0,0 +1,94 @@
+;;; graph.el --- Working with in-memory graphs -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;;
+;; Remember that there are optimal three ways to model a graph:
+;; 1. Edge List
+;; 2. Vertex Table (a.k.a. Neighbors Table)
+;; 3. Adjacency Matrix
+;;
+;; I may call these "Edges", "Neighbors", "Adjacencies" to avoid verbose naming.
+;; For now, I'm avoiding dealing with Adjacency Matrices as I don't have an
+;; immediate use-case for them.  This is subject to change.
+;;
+;; There are also hybrid representations of graphs that combine the three
+;; aforementioned models.  I believe Erlang's digraph module models graphs in
+;; Erlang Term Storage (i.e. ETS) this way.
+;; TODO: Verify this claim.
+;;
+;; Graphs can be weighted or unweighted.  They can also be directed or
+;; undirected.
+;; TODO: Create a table explaining all graph variants.
+;;
+;; TODO: Figure out the relationship of this module and tree.el, which should in
+;; principle overlap.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; For now, I'll support storing *either* neighbors or edges in the graph struct
+;; as long as both aren't set, since that introduces consistency issues.  I may
+;; want to handle that use-case in the future, but not now.
+(cl-defstruct graph neighbors edges)
+
+;; TODO: How do you find the starting point for a topo sort?
+(defun graph-sort (xs)
+  "Return a topological sort of XS.")
+
+(defun graph-from-edges (xs)
+  "Create a graph struct from the Edge List, XS.
+The user must pass in a valid Edge List since asserting on the shape of XS might
+  be expensive."
+  (make-graph :edges xs))
+
+(defun graph-from-neighbors (xs)
+  "Create a graph struct from a Neighbors Table, XS.
+The user must pass in a valid Neighbors Table since asserting on the shape of
+  XS might be expensive."
+  (make-graph :neighbors xs))
+
+(defun graph-instance? (xs)
+  "Return t if XS is a graph struct."
+  (graph-p xs))
+
+;; TODO: Model each of the mapping functions into an isomorphism.
+(defun graph-edges->neighbors (xs)
+  "Map Edge List, XS, into a Neighbors Table."
+  (prelude-assert (graph-instance? xs)))
+
+(defun graph-neighbors->edges (xs)
+  "Map Neighbors Table, XS, into an Edge List."
+  (prelude-assert (graph-instance? xs)))
+
+;; Below are three different models of the same unweighted, directed graph.
+
+(defvar graph-edges
+  '((a . b) (a . c) (a . e)
+    (b . c) (b . d)
+    (c . e)
+    (d . f)
+    (e . d) (e . f)))
+
+(defvar graph-neighbors
+  ((a b c e)
+   (b c d)
+   (c e)
+   (d f)
+   (e d g)
+   (f)))
+
+(provide 'graph)
+;;; graph.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/irc.el b/users/wpcarro/emacs/.emacs.d/wpc/irc.el
new file mode 100644
index 0000000000..9103bd38fe
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/irc.el
@@ -0,0 +1,170 @@
+;;; irc.el --- Configuration for IRC chat -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Need to decide which client I will use for IRC.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'erc)
+(require 'cycle)
+(require 'string)
+(require 'prelude)
+(require 'al)
+(require 'set)
+(require 'maybe)
+(require 'macros)
+(require '>)
+(require 'password-store)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defcustom irc-install-kbds? t
+  "When t, install the keybindings defined herein.")
+
+(setq erc-rename-buffers t)
+
+;; Setting `erc-join-buffer' to 'bury prevents erc from stealing focus of the
+;; current buffer when it connects to IRC servers.
+(setq erc-join-buffer 'bury)
+
+;; TODO: Find a way to avoid putting "freenode" and "#freenode" as channels
+;; here.  I'm doing it because when erc first connects, it's `(buffer-name)' is
+;; "freenode", so when `irc-next-channel' is called, it 404s on the
+;; `cycle-contains?' call in `irc-channel->cycle" unless "freenode" is there. To
+;; make matters even uglier, when `erc-join-channel' is called with "freenode"
+;; as the value, it connects to the "#freenode" channel, so unless "#freenode"
+;; exists in this cycle also, `irc-next-channel' breaks again.
+(defconst irc-server->channels
+  `(("irc.freenode.net"    . ,(cycle-new "freenode" "#freenode" "#nixos" "#emacs" "#pass"))
+    ("irc.corp.google.com" . ,(cycle-new "#drive-prod")))
+  "Mapping of IRC servers to a cycle of my preferred channels.")
+
+;; TODO: Here is another horrible hack that should be revisted.
+(setq erc-autojoin-channels-alist
+      (->> irc-server->channels
+           (al-map-values #'cycle-to-list)
+           (al-map-keys (>-> (s-chop-prefix "irc.")
+                             (s-chop-suffix ".net")))))
+
+;; TODO: Assert that no two servers have a channel with the same name. We need
+;; this because that's the assumption that underpins the `irc-channel->server'
+;; function. This will probably be an O(n^2) operation.
+(prelude-assert
+ (set-distinct? (set-from-list
+                 (cycle-to-list
+                  (al-get "irc.freenode.net"
+                          irc-server->channels)))
+                (set-from-list
+                 (cycle-to-list
+                  (al-get "irc.corp.google.com"
+                          irc-server->channels)))))
+
+(defun irc-channel->server (server->channels channel)
+  "Using SERVER->CHANNELS, resolve an IRC server from a given CHANNEL."
+  (let ((result (al-find (lambda (k v) (cycle-contains? channel v))
+                         server->channels)))
+    (prelude-assert (maybe-some? result))
+    result))
+
+(defun irc-channel->cycle (server->channels channel)
+  "Using SERVER->CHANNELS, resolve an IRC's channels cycle from CHANNEL."
+  (al-get (irc-channel->server server->channels channel)
+          server->channels))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun irc-message (x)
+  "Print message X in a structured way."
+  (message (string-format "[irc.el] %s" x)))
+
+;; TODO: Integrate Google setup with Freenode setup.
+
+;; TODO: Support function or KBD for switching to an ERC buffer.
+
+(defun irc-kill-all-erc-processes ()
+  "Kill all ERC buffers and processes."
+  (interactive)
+  (->> (erc-buffer-list)
+       (-map #'kill-buffer)))
+
+(defun irc-switch-to-erc-buffer ()
+  "Switch to an ERC buffer."
+  (interactive)
+  (let ((buffers (erc-buffer-list)))
+    (if (list-empty? buffers)
+        (error "[irc.el] No ERC buffers available")
+      (switch-to-buffer (list-head (erc-buffer-list))))))
+
+(defun irc-connect-to-freenode ()
+  "Connect to Freenode IRC."
+  (interactive)
+  (erc-ssl :server "irc.freenode.net"
+           :port 6697
+           :nick "wpcarro"
+           :password (password-store-get "programming/irc/freenode")
+           :full-name "William Carroll"))
+
+;; TODO: Handle failed connections.
+(defun irc-connect-to-google ()
+  "Connect to Google's Corp IRC using ERC."
+  (interactive)
+  (erc-ssl :server "irc.corp.google.com"
+           :port 6697
+           :nick "wpcarro"
+           :full-name "William Carroll"))
+
+;; TODO: Prefer defining these with a less homespun solution. There is a
+;; function call `erc-buffer-filter' that would be more appropriate for the
+;; implementation of `irc-next-channel' and `irc-prev-channel'.
+(defun irc-next-channel ()
+  "Join the next channel for the active server."
+  (interactive)
+  (with-current-buffer (current-buffer)
+    (let ((cycle (irc-channel->cycle irc-server->channels (buffer-name))))
+      (erc-join-channel
+       (cycle-next cycle))
+      (irc-message
+       (string-format "Current IRC channel: %s" (cycle-current cycle))))))
+
+(defun irc-prev-channel ()
+  "Join the previous channel for the active server."
+  (interactive)
+  (with-current-buffer (current-buffer)
+    (let ((cycle (irc-channel->cycle irc-server->channels (buffer-name))))
+      (erc-join-channel
+       (cycle-prev cycle))
+      (irc-message
+       (string-format "Current IRC channel: %s" (cycle-current cycle))))))
+
+(add-hook 'erc-mode-hook (macros-disable auto-fill-mode))
+(add-hook 'erc-mode-hook (macros-disable company-mode))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Keybindings
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(when irc-install-kbds?
+  (general-define-key
+   :keymaps 'erc-mode-map
+   "<C-tab>" #'irc-next-channel
+   "<C-S-iso-lefttab>" #'irc-prev-channel))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(provide 'irc)
+;;; irc.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/ivy-helpers.el b/users/wpcarro/emacs/.emacs.d/wpc/ivy-helpers.el
new file mode 100644
index 0000000000..3303237d52
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/ivy-helpers.el
@@ -0,0 +1,67 @@
+;;; ivy-helpers.el --- More interfaces to ivy -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Hopefully to improve my workflows.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'tuple)
+(require 'string)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defun ivy-helpers-kv (prompt kv f)
+  "PROMPT users with the keys in KV and return its corresponding value.
+
+Apply key and value from KV to F."
+  (ivy-read
+   prompt
+   kv
+   :require-match t
+   :action (lambda (entry)
+             (funcall f (car entry) (cdr entry)))))
+
+(defun ivy-helpers-do-run-external-command (cmd)
+  "Execute the specified CMD and notify the user when it finishes."
+  (message "Starting %s..." cmd)
+  (set-process-sentinel
+   (start-process-shell-command cmd nil cmd)
+   (lambda (process event)
+     (when (string= event "finished\n")
+       (message "%s process finished." process)))))
+
+(defun ivy-helpers-list-external-commands ()
+  "Create a list of all external commands available on $PATH."
+  (cl-loop
+   for dir in (split-string (getenv "PATH") path-separator)
+   when (and (file-exists-p dir) (file-accessible-directory-p dir))
+   for lsdir = (cl-loop for i in (directory-files dir t)
+                        for bn = (file-name-nondirectory i)
+                        when (and (not (s-contains? "-wrapped" i))
+                                  (not (member bn completions))
+                                  (not (file-directory-p i))
+                                  (file-executable-p i))
+                        collect bn)
+   append lsdir into completions
+   finally return (sort completions 'string-lessp)))
+
+(defun ivy-helpers-run-external-command ()
+  "Prompts the user with a list of all installed applications to launch."
+  (interactive)
+  (let ((external-commands-list (ivy-helpers-list-external-commands)))
+    (ivy-read "Command:" external-commands-list
+              :require-match t
+              :action #'ivy-helpers-do-run-external-command)))
+
+;;; Code:
+(provide 'ivy-helpers)
+;;; ivy-helpers.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/kbd.el b/users/wpcarro/emacs/.emacs.d/wpc/kbd.el
new file mode 100644
index 0000000000..7defc3d08f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/kbd.el
@@ -0,0 +1,85 @@
+;;; kbd.el --- Elisp keybinding -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; In order to stay organized, I'm attempting to dedicate KBD prefixes to
+;; specific functions.  I'm hoping I can be more deliberate with my keybinding
+;; choices this way.
+;;
+;; Terminology:
+;; For a more thorough overview of the terminology refer to `keybindings.md'
+;; file.  Here's a brief overview:
+;; - workspace: Anything concerning EXWM workspaces.
+;; - x11: Anything concerning X11 applications.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'al)
+(require 'set)
+(require 'string)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst kbd-prefixes
+  '((workspace . "s")
+    (x11 . "C-s"))
+  "Mapping of functions to designated keybinding prefixes to stay organized.")
+
+;; Assert that no keybindings are colliding.
+(prelude-assert
+ (= (al-count kbd-prefixes)
+    (->> kbd-prefixes
+         al-values
+         set-from-list
+         set-count)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun kbd-raw (f x)
+  "Return the string keybinding for function F and appendage X.
+Values for F include:
+- workspace
+- x11"
+  (prelude-assert (al-has-key? f kbd-prefixes))
+  (string-format
+   "%s-%s"
+   (al-get f kbd-prefixes)
+   x))
+
+(defun kbd-for (f x)
+  "Return the `kbd' for function F and appendage X.
+Values for F include:
+- workspace
+- x11"
+  (kbd (kbd-raw f x)))
+
+;; TODO: Prefer copying human-readable versions to the clipboard.  Right now
+;; this isn't too useful.
+(defun kbd-copy-keycode ()
+  "Copy the pressed key to the system clipboard."
+  (interactive)
+  (message "[kbd] Awaiting keypress...")
+  (let ((key (read-key)))
+    (clipboard-copy (string-format "%s" key))
+    (message (string-format "[kbd] \"%s\" copied!" key))))
+
+(defun kbd-print-keycode ()
+  "Prints the pressed keybinding."
+  (interactive)
+  (message "[kbd] Awaiting keypress...")
+  (message (string-format "[kbd] keycode: %s" (read-key))))
+
+(provide 'kbd)
+;;; kbd.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/keybindings.el b/users/wpcarro/emacs/.emacs.d/wpc/keybindings.el
new file mode 100644
index 0000000000..ca9ba16271
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/keybindings.el
@@ -0,0 +1,437 @@
+;;; keybindings.el --- Centralizing my keybindings -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Attempting to centralize my keybindings to simplify my configuration.
+;;
+;; I have some expectations about my keybindings.  Here are some of those
+;; defined:
+;; - In insert mode:
+;;   - C-a: beginning-of-line
+;;   - C-e: end-of-line
+;;   - C-b: backwards-char
+;;   - C-f: forwards-char
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'functions)
+(require 'screen-brightness)
+(require 'pulse-audio)
+(require 'scrot)
+(require 'ivy)
+(require 'ivy-clipmenu)
+(require 'ivy-helpers)
+(require 'general)
+(require 'exwm)
+(require 'vterm-mgt)
+(require 'buffer)
+(require 'device)
+(require 'fonts)
+(require 'bookmark)
+(require 'tvl)
+(require 'window-manager)
+(require 'wpc-misc)
+
+;; Note: The following lines must be sorted this way.
+(setq evil-want-integration t)
+(setq evil-want-keybinding nil)
+(general-evil-setup)
+(require 'evil)
+(require 'evil-collection)
+(require 'evil-commentary)
+(require 'evil-surround)
+(require 'key-chord)
+(require 'edebug)
+(require 'avy)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; General Keybindings
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Ensure that evil's command mode behaves with readline bindings.
+(general-define-key
+ :keymaps 'evil-ex-completion-map
+ "C-a" #'move-beginning-of-line
+ "C-e" #'move-end-of-line
+ "C-k" #'kill-line
+ "C-u" #'evil-delete-whole-line
+ "C-v" #'evil-paste-after
+ "C-d" #'delete-char
+ "C-f" #'forward-char
+ "M-b" #'backward-word
+ "M-f" #'forward-word
+ "M-d" #'kill-word
+ "M-DEL" #'backward-kill-word
+ "C-b" #'backward-char)
+
+(general-mmap
+  :keymaps 'override
+  "RET" #'evil-goto-line
+  "H"   #'evil-first-non-blank
+  "L"   #'evil-end-of-line
+  "_"   #'ranger
+  "-"   #'dired-jump
+  "sl"  #'functions-evil-window-vsplit-right
+  "sh"  #'evil-window-vsplit
+  "sk"  #'evil-window-split
+  "sj"  #'functions-evil-window-split-down)
+
+(general-nmap
+  :keymaps 'override
+  "gu" #'browse-url-at-point
+  "gd" #'xref-find-definitions
+  ;; Wrapping `xref-find-references' in the `let' binding to prevent xref from
+  ;; prompting.  There are other ways to handle this variable, such as setting
+  ;; it globally with `setq' or buffer-locally with `setq-local'.  For now, I
+  ;; prefer setting it with `let', which should bind it in the dynamic scope
+  ;; for the duration of the `xref-find-references' function call.
+  "gx" (lambda ()
+         (interactive)
+         (let ((xref-prompt-for-identifier nil))
+           (call-interactively #'xref-find-references))))
+
+(general-unbind 'motion "M-." "C-p" "<SPC>")
+(general-unbind 'normal "s"   "M-." "C-p" "C-n")
+(general-unbind 'insert "C-v" "C-d" "C-a" "C-e" "C-n" "C-p" "C-k")
+
+(customize-set-variable 'evil-symbol-word-search t)
+(evil-mode 1)
+(evil-collection-init)
+(evil-commentary-mode)
+(global-evil-surround-mode 1)
+
+;; Ensure the Evil search results get centered vertically.
+;; When Emacs is run from a terminal, this forces Emacs to redraw itself, which
+;; is visually disruptive.
+(when window-system
+  (progn
+    (defadvice isearch-update
+        (before advice-for-isearch-update activate)
+      (evil-scroll-line-to-center (line-number-at-pos)))
+    (defadvice evil-search-next
+        (after advice-for-evil-search-next activate)
+      (evil-scroll-line-to-center (line-number-at-pos)))
+    (defadvice evil-search-previous
+        (after advice-for-evil-search-previous activate)
+      (evil-scroll-line-to-center (line-number-at-pos)))))
+
+(key-chord-mode 1)
+(key-chord-define evil-insert-state-map "jk" 'evil-normal-state)
+
+;; This may be contraversial, but I never use the prefix key, and I'd prefer to
+;; have to bound to the readline function that deletes the entire line.
+(general-unbind "C-u")
+
+(defmacro keybindings-exwm (c fn)
+  "Bind C to FN using `exwm-input-set-key' with `kbd' applied to C."
+  `(exwm-input-set-key (kbd ,c) ,fn))
+
+(keybindings-exwm "C-M-v" #'ivy-clipmenu-copy)
+(keybindings-exwm "<XF86MonBrightnessUp>" #'screen-brightness-increase)
+(keybindings-exwm "<XF86MonBrightnessDown>" #'screen-brightness-decrease)
+(keybindings-exwm "<XF86AudioMute>" #'pulse-audio-toggle-mute)
+(keybindings-exwm "<XF86AudioLowerVolume>" #'pulse-audio-decrease-volume)
+(keybindings-exwm "<XF86AudioRaiseVolume>" #'pulse-audio-increase-volume)
+(keybindings-exwm "<XF86AudioMicMute>" #'pulse-audio-toggle-microphone)
+(keybindings-exwm (kbd-raw 'x11 "s") #'scrot-select)
+(keybindings-exwm "<C-M-tab>" #'window-manager-switch-to-exwm-buffer)
+(keybindings-exwm (kbd-raw 'workspace "k") #'fonts-increase-size)
+(keybindings-exwm (kbd-raw 'workspace "j") #'fonts-decrease-size)
+(keybindings-exwm (kbd-raw 'workspace "0") #'fonts-reset-size)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Window sizing
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(keybindings-exwm "C-M-=" #'balance-windows)
+(keybindings-exwm "C-M-j" #'shrink-window)
+(keybindings-exwm "C-M-k" #'enlarge-window)
+(keybindings-exwm "C-M-h" #'shrink-window-horizontally)
+(keybindings-exwm "C-M-l" #'enlarge-window-horizontally)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Window Management
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(keybindings-exwm "M-h" #'windmove-left)
+(keybindings-exwm "M-j" #'windmove-down)
+(keybindings-exwm "M-k" #'windmove-up)
+(keybindings-exwm "M-l" #'windmove-right)
+(keybindings-exwm "M-\\" #'evil-window-vsplit)
+(keybindings-exwm "M--" #'evil-window-split)
+(keybindings-exwm "M-q" #'delete-window)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Miscellaneous
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(keybindings-exwm "M-:" #'eval-expression)
+(keybindings-exwm "M-SPC" #'ivy-helpers-run-external-command)
+(keybindings-exwm "M-x" #'counsel-M-x)
+(keybindings-exwm "<M-tab>" #'window-manager-next-workspace)
+(keybindings-exwm "<M-S-iso-lefttab>" #'window-manager-prev-workspace)
+(keybindings-exwm "C-S-f" #'window-manager-toggle-previous)
+(keybindings-exwm "C-M-\\" #'ivy-pass)
+(keybindings-exwm "s-e" #'wpc-misc-copy-emoji)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Workspaces
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(keybindings-exwm (kbd-raw 'workspace "l")
+                  (lambda ()
+                    (interactive)
+                    (shell-command window-manager-screenlocker)))
+
+(general-define-key
+ :keymaps 'override
+ "M-q" #'delete-window
+ "<s-return>" #'toggle-frame-fullscreen
+ "M-h" #'windmove-left
+ "M-l" #'windmove-right
+ "M-k" #'windmove-up
+ "M-j" #'windmove-down
+ "M-q" #'delete-window)
+
+;; Support pasting in M-:.
+(general-define-key
+ :keymaps 'read-expression-map
+ "C-v"   #'clipboard-yank
+ "C-S-v" #'clipboard-yank)
+
+(general-define-key
+ :prefix "<SPC>"
+ :states '(normal)
+ "." #'ffap
+ "gn" #'notmuch
+ "i" #'counsel-semantic-or-imenu
+ "I" #'ibuffer
+ "hk" #'helpful-callable
+ "hf" #'helpful-function
+ "hm" #'helpful-macro
+ "hc" #'helpful-command
+ "hk" #'helpful-key
+ "hv" #'helpful-variable
+ "hp" #'helpful-at-point
+ "hi" #'info-apropos
+ "s" #'flyspell-mode
+ "S" #'sort-lines
+ "=" #'align
+ "p" #'flycheck-previous-error
+ "f" #'project-find-file
+ "n" #'flycheck-next-error
+ "N" #'smerge-next
+ "W" #'balance-windows
+ "gss" #'magit-status
+ "gsd" #'tvl-depot-status
+ "E" #'refine
+ "es" #'functions-create-snippet
+ "l" #'linum-mode
+ "B" #'magit-blame
+ "w" #'save-buffer
+ "r" #'functions-evil-replace-under-point
+ "R" #'deadgrep)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Vterm
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Show or hide a vterm buffer.  I'm intentionally not defining this in
+;; vterm-mgt.el because it consumes `buffer-show-previous', and I'd like to
+;; avoid bloating vterm-mgt.el with dependencies that others may not want.
+(general-define-key (kbd-raw 'x11 "t")
+                    (lambda ()
+                      (interactive)
+                      (if (vterm-mgt--instance? (current-buffer))
+                          (switch-to-buffer (first (buffer-source-code-buffers)))
+                        (call-interactively #'vterm-mgt-find-or-create))))
+
+(general-define-key
+ :keymaps '(vterm-mode-map)
+ ;; For some reason vterm captures this KBD instead of EXWM
+ "C-S-f" nil
+ "s-x" #'vterm-mgt-select
+ "C-S-n" #'vterm-mgt-instantiate
+ "C-S-w" #'vterm-mgt-kill
+ "<C-tab>" #'vterm-mgt-next
+ "<C-S-iso-lefttab>" #'vterm-mgt-prev
+ "<s-backspace>" #'vterm-mgt-rename-buffer
+ ;; Without this, typing "+" is effectively no-op. Try for yourself:
+ ;; (vterm-send-key "<kp-add>")
+ "<kp-add>" "+")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; notmuch
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; evil-collection adds many KBDs to notmuch modes. Some of these I find
+;; disruptive.
+(general-define-key
+ :states '(normal)
+ :keymaps '(notmuch-show-mode-map)
+ "M-j" nil
+ "M-k" nil
+ "<C-S-iso-lefttab>" #'notmuch-show-previous-thread-show
+ "<C-tab>" #'notmuch-show-next-thread-show
+ "e" #'notmuch-show-archive-message-then-next-or-next-thread)
+
+;; TODO(wpcarro): Consider moving this to a separate module
+(defun keybindings--evil-ex-define-cmd-local (cmd f)
+  "Define CMD to F locally to a buffer."
+  (unless (local-variable-p 'evil-ex-commands)
+    (setq-local evil-ex-commands (copy-alist evil-ex-commands)))
+  (evil-ex-define-cmd cmd f))
+
+;; TODO(wpcarro): Support a macro that can easily define evil-ex commands for a
+;; particular mode.
+;; Consumption:
+;; (evil-ex-for-mode 'notmuch-message-mode
+;;                   "x" #'notmuch-mua-send-and-exit)
+
+(add-hook 'notmuch-message-mode-hook
+          (lambda ()
+            (keybindings--evil-ex-define-cmd-local "x" #'notmuch-mua-send-and-exit)))
+
+;; For now, I'm mimmicking Gmail KBDs that I have memorized and enjoy
+(general-define-key
+ :states '(normal visual)
+ :keymaps '(notmuch-search-mode-map)
+ "M"  (lambda ()
+        (interactive)
+        (notmuch-search-tag '("-inbox" "+muted")))
+ "mi" (lambda ()
+        (interactive)
+        (notmuch-search-tag '("+inbox" "-action" "-review" "-waiting" "-muted")))
+ "ma" (lambda ()
+        (interactive)
+        (notmuch-search-tag '("-inbox" "+action" "-review" "-waiting")))
+ "mr" (lambda ()
+        (interactive)
+        (notmuch-search-tag '("-inbox" "-action" "+review" "-waiting")))
+ "mw" (lambda ()
+        (interactive)
+        (notmuch-search-tag '("-inbox" "-action" "-review" "+waiting")))
+ "e" #'notmuch-search-archive-thread)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; magit
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :states '(normal)
+ :keymaps '(magit-status-mode-map
+            magit-log-mode-map
+            magit-revision-mode-map)
+ "l" #'evil-forward-char
+ "L" #'magit-log)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Info-mode
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; NOTE: I find some of the following, existing KBDs useful:
+;;   M-x info-apropos
+;;   u   Info-up
+;;   M-n clone-buffer
+(general-define-key
+ :states '(normal)
+ :keymaps '(Info-mode-map)
+ "SPC" nil
+ "g SPC" #'Info-scroll-up
+ "RET" #'Info-follow-nearest-node
+ "<C-tab>" #'Info-next
+ "<C-S-iso-lefttab>" #'Info-prev
+ "g l" #'Info-history-back
+ "g t" #'Info-toc)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; ibuffer
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :states '(normal)
+ :keymaps '(ibuffer-mode-map)
+ "M-j" nil
+ "K" #'ibuffer-do-delete)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; buffers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :states '(normal)
+ "C-f" #'buffer-cycle-next
+ "C-b" #'buffer-cycle-prev)
+
+(general-define-key
+ :prefix "<SPC>"
+ :states '(normal)
+ "b" #'buffer-ivy-source-code
+ "<SPC>" #'buffer-show-previous
+ "k" #'kill-buffer)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; edebug
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :states '(normal)
+ :keymaps '(edebug-mode-map)
+ ;; this restores my ability to move-left while debugging
+ "h" nil)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; deadgrep
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :states '(normal)
+ :keymaps '(deadgrep-mode-map)
+ "<tab>" #'deadgrep-forward
+ "<backtab>" #'deadgrep-backward)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; bookmarks
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(bookmark-install-kbd
+ (make-bookmark :label "wpcarro"
+                :path (f-join tvl-depot-path "users/wpcarro")
+                :kbd "w"))
+
+(bookmark-install-kbd
+ (make-bookmark :label "depot"
+                :path tvl-depot-path
+                :kbd "d"))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; refine
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :keymaps '(refine-mode-map)
+ :states '(normal)
+ "K" #'refine-delete
+ "q" #'kill-this-buffer)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; avy
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(global-set-key (kbd "C-;") #'avy-goto-char)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; ivy
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; restore the ability to paste in ivy
+(general-define-key
+ :keymaps '(ivy-minibuffer-map)
+ "C-v" #'clipboard-yank
+ "C-S-v" #'clipboard-yank)
+
+(provide 'keybindings)
+;;; keybindings.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/keyboard.el b/users/wpcarro/emacs/.emacs.d/wpc/keyboard.el
new file mode 100644
index 0000000000..03fb9e3f35
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/keyboard.el
@@ -0,0 +1,139 @@
+;;; keyboard.el --- Managing keyboard preferences with Elisp -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Setting key repeat and other values.
+;;
+;; Be wary of suspiciously round numbers.  Especially those divisible by ten!
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'string)
+(require 'number)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Support clamping functions for repeat-{rate,delay} to ensure only valid
+;; values are sent to xset.
+(defcustom keyboard-repeat-rate 80
+  "The number of key repeat signals sent per second.")
+
+(defcustom keyboard-repeat-delay 170
+  "The number of milliseconds before autorepeat starts.")
+
+(defconst keyboard-repeat-rate-copy keyboard-repeat-rate
+  "Copy of `keyboard-repeat-rate' to support `keyboard-reset-key-repeat'.")
+
+(defconst keyboard-repeat-delay-copy keyboard-repeat-delay
+  "Copy of `keyboard-repeat-delay' to support `keyboard-reset-key-repeat'.")
+
+(defcustom keyboard-install-preferences? t
+  "When t, install keyboard preferences.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun keyboard-message (x)
+  "Message X in a structured way."
+  (message (string-format "[keyboard.el] %s" x)))
+
+(cl-defun keyboard-set-key-repeat (&key
+                                   (rate keyboard-repeat-rate)
+                                   (delay keyboard-repeat-delay))
+  "Use xset to set the key-repeat RATE and DELAY."
+  (prelude-start-process
+   :name "keyboard-set-key-repeat"
+   :command (string-format "xset r rate %s %s" delay rate)))
+
+;; NOTE: Settings like this are machine-dependent. For instance I only need to
+;; do this on my laptop and other devices where I don't have access to my split
+;; keyboard.
+;; NOTE: Running keysym Caps_Lock is not idempotent.  If this is called more
+;; than once, xmodmap will start to error about non-existent Caps_Lock symbol.
+;; For more information see here:
+;; https://unix.stackexchange.com/questions/108207/how-to-map-caps-lock-as-the-compose-key-using-xmodmap-portably-and-idempotently
+(defun keyboard-swap-caps-lock-and-escape ()
+  "Swaps the caps lock and escape keys using xmodmap."
+  (interactive)
+  ;; TODO: Ensure these work once the tokenizing in prelude-start-process works
+  ;; as expected.
+  (start-process "keyboard-swap-caps-lock-and-escape"
+                 nil "/usr/bin/xmodmap" "-e" "remove Lock = Caps_Lock")
+  (start-process "keyboard-swap-caps-lock-and-escape"
+                 nil "/usr/bin/xmodmap" "-e" "keysym Caps_Lock = Escape"))
+
+(defun keyboard-inc-repeat-rate ()
+  "Increment `keyboard-repeat-rate'."
+  (interactive)
+  (setq keyboard-repeat-rate (number-inc keyboard-repeat-rate))
+  (keyboard-set-key-repeat :rate keyboard-repeat-rate)
+  (keyboard-message
+   (string-format "Rate: %s" keyboard-repeat-rate)))
+
+(defun keyboard-dec-repeat-rate ()
+  "Decrement `keyboard-repeat-rate'."
+  (interactive)
+  (setq keyboard-repeat-rate (number-dec keyboard-repeat-rate))
+  (keyboard-set-key-repeat :rate keyboard-repeat-rate)
+  (keyboard-message
+   (string-format "Rate: %s" keyboard-repeat-rate)))
+
+(defun keyboard-inc-repeat-delay ()
+  "Increment `keyboard-repeat-delay'."
+  (interactive)
+  (setq keyboard-repeat-delay (number-inc keyboard-repeat-delay))
+  (keyboard-set-key-repeat :delay keyboard-repeat-delay)
+  (keyboard-message
+   (string-format "Delay: %s" keyboard-repeat-delay)))
+
+(defun keyboard-dec-repeat-delay ()
+  "Decrement `keyboard-repeat-delay'."
+  (interactive)
+  (setq keyboard-repeat-delay (number-dec keyboard-repeat-delay))
+  (keyboard-set-key-repeat :delay keyboard-repeat-delay)
+  (keyboard-message
+   (string-format "Delay: %s" keyboard-repeat-delay)))
+
+(defun keyboard-print-key-repeat ()
+  "Print the currently set values for key repeat."
+  (interactive)
+  (keyboard-message
+   (string-format "Rate: %s. Delay: %s"
+                  keyboard-repeat-rate
+                  keyboard-repeat-delay)))
+
+(defun keyboard-set-preferences ()
+  "Reset the keyboard preferences to their default values.
+NOTE: This function exists because occasionally I unplug and re-plug in a
+  keyboard and all of the preferences that I set using xset disappear."
+  (interactive)
+  (keyboard-swap-caps-lock-and-escape)
+  (keyboard-set-key-repeat :rate keyboard-repeat-rate
+                           :delay keyboard-repeat-delay)
+  ;; TODO: Implement this message function as a macro that pulls the current
+  ;; file name.
+  (keyboard-message "Keyboard preferences set!"))
+
+(defun keyboard-reset-key-repeat ()
+  "Set key repeat rate and delay to original values."
+  (interactive)
+  (keyboard-set-key-repeat :rate keyboard-repeat-rate-copy
+                           :delay keyboard-repeat-delay-copy)
+  (keyboard-message "Key repeat preferences reset."))
+
+(when keyboard-install-preferences?
+  (keyboard-set-preferences))
+
+(provide 'keyboard)
+;;; keyboard.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/laptop-battery.el b/users/wpcarro/emacs/.emacs.d/wpc/laptop-battery.el
new file mode 100644
index 0000000000..80dc96ebeb
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/laptop-battery.el
@@ -0,0 +1,63 @@
+;;; laptop-battery.el --- Display laptop battery information -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Some wrappers to obtain battery information.
+;;
+;; To troubleshoot battery consumpton look into the CLI `powertop`.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Roadmap
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Support functions that work with reporting battery stats.
+;; TODO: low-battery-reporting-threshold
+;; TODO: charged-battery-reporting-threshold
+;; TODO: Format modeline battery information.
+;; TODO: Provide better time information in the modeline.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'battery)
+(require 'al)
+(require 'maybe)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun laptop-battery-available? ()
+  "Return t if battery information is available."
+  (maybe-some? battery-status-function))
+
+(defun laptop-battery-percentage ()
+  "Return the current percentage of the battery."
+  (->> battery-status-function
+       funcall
+       (al-get 112)))
+
+(defun laptop-battery-print-percentage ()
+  "Return the current percentage of the battery."
+  (interactive)
+  (->> (laptop-battery-percentage)
+       message))
+
+(defun laptop-battery-display ()
+  "Display laptop battery percentage in the modeline."
+  (interactive)
+  (display-battery-mode 1))
+
+(defun laptop-battery-hide ()
+  "Hide laptop battery percentage in the modeline."
+  (interactive)
+  (display-battery-mode -1))
+
+(provide 'laptop-battery)
+;;; laptop-battery.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/list.el b/users/wpcarro/emacs/.emacs.d/wpc/list.el
new file mode 100644
index 0000000000..2f1509eeb4
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/list.el
@@ -0,0 +1,221 @@
+;;; list.el --- Functions for working with lists -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Since I prefer having the `list-' namespace, I wrote this module to wrap many
+;; of the functions that are defined in the the global namespace in ELisp.  I
+;; sometimes forget the names of these functions, so it's nice for them to be
+;; organized like this.
+;;
+;; Motivation:
+;; Here are some examples of function names that I cannot tolerate:
+;; - `car': Return the first element (i.e. "head") of a linked list
+;; - `cdr': Return the tail of a linked list
+
+;; As are most APIs for standard libraries that I write, this is heavily
+;; influenced by Elixir's standard library.
+;;
+;; Elixir's List library:
+;; - ++/2
+;; - --/2
+;; - hd/1
+;; - tl/1
+;; - in/2
+;; - length/1
+;;
+;; Similar libraries:
+;; - dash.el: Functional library that mimmicks Clojure.  It is consumed herein.
+;; - list-utils.el: Utility library that covers things that dash.el may not
+;;   cover.
+;;   stream.el: Elisp implementation of streams, "implemented as delayed
+;;   evaluation of cons cells."
+
+;; TODO: Consider naming this file linked-list.el.
+
+;; TODO: Support module-like macro that auto-namespaces functions.
+
+;; TODO: Consider wrapping most data structures like linked-lists,
+;; associative-lists, etc in a `cl-defstruct', so that the dispatching by type
+;; can be nominal instead of duck-typing.  I'm not sure if this is a good idea
+;; or not.  If I do this, I should provide isomorphisms to map between idiomatic
+;; ways of working with Elisp data structures and my wrapped variants.
+
+;; TODO: Are function aliases/synonyms even a good idea?  Or do they just
+;; bloat the API unnecessarily?
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Move `prelude-assert' elsewhere so that I can require it without
+;; introducing the circular dependency of list.el -> prelude.el -> list.el.
+;;(require 'prelude)
+(require 'dash)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst list-tests? t
+  "When t, run the test suite.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list-new ()
+  "Return a new, empty list."
+  '())
+
+(defun list-concat (&rest lists)
+  "Joins `LISTS' into on list."
+  (apply #'-concat lists))
+
+(defun list-join (joint xs)
+  "Join a list of strings, XS, with JOINT."
+  (if (list-empty? xs)
+      ""
+    (list-reduce (list-first xs)
+                 (lambda (x acc)
+                   (string-concat acc joint x))
+                 (list-tail xs))))
+
+(defun list-length (xs)
+  "Return the number of elements in `XS'."
+  (length xs))
+
+(defun list-get (i xs)
+  "Return the value in `XS' at `I', or nil."
+  (nth i xs))
+
+(defun list-head (xs)
+  "Return the head of `XS'."
+  (car xs))
+
+;; TODO: Learn how to write proper function aliases.
+(defun list-first (xs)
+  "Alias for `list-head' for `XS'."
+  (list-head xs))
+
+(defun list-tail (xs)
+  "Return the tail of `XS'."
+  (cdr xs))
+
+(defun list-reverse (xs)
+  "Reverses `XS'."
+  (reverse xs))
+
+(defun list-cons (x xs)
+  "Add `X' to the head of `XS'."
+  (cons x xs))
+
+;; map, filter, reduce
+
+;; TODO: Create function adapters like swap.
+;; (defun adapter/swap (f)
+;;   "Return a new function that wraps `F' and swaps the arguments."
+;;   (lambda (a b)
+;;     (funcall f b a)))
+
+;; TODO: Make this function work.
+(defun list-reduce (acc f xs)
+  "Return over `XS' calling `F' on an element in `XS'and `ACC'."
+  (-reduce-from (lambda (acc x) (funcall f x acc)) acc xs))
+
+(defun list-map (f xs)
+  "Call `F' on each element of `XS'."
+  (-map f xs))
+
+(defun list-map-indexed (f xs)
+  "Call `F' on each element of `XS' along with its index."
+  (-map-indexed (lambda (i x) (funcall f x i)) xs))
+
+(defun list-filter (p xs)
+  "Return a subset of XS where predicate P returned t."
+  (list-reverse
+   (list-reduce
+    '()
+    (lambda (x acc)
+      (if (funcall p x)
+          (list-cons x acc)
+        acc))
+    xs)))
+
+(defun list-reject (p xs)
+  "Return a subset of XS where predicate of P return nil."
+  (list-filter (lambda (x) (not (funcall p x))) xs))
+
+(defun list-find (p xs)
+  "Return the first x in XS that passes P or nil."
+  (-find p xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list-instance? (xs)
+  "Return t if `XS' is a list.
+Be leery of using this with things like alists.  Many data structures in Elisp
+  are implemented using linked lists."
+  (listp xs))
+
+(defun list-empty? (xs)
+  "Return t if XS are empty."
+  (= 0 (list-length xs)))
+
+(defun list-all? (p xs)
+  "Return t if all `XS' pass the predicate, `P'."
+  (-all? p xs))
+
+(defun list-any? (p xs)
+  "Return t if any `XS' pass the predicate, `P'."
+  (-any? p xs))
+
+(defun list-contains? (x xs)
+  "Return t if X is in XS using `equal'."
+  (-contains? xs x))
+
+(defun list-xs-distinct-by? (f xs)
+  "Return t if all elements in XS are distinct after applying F to each."
+  (= (length xs)
+     (->> xs (-map f) set-from-list set-count)))
+
+;; TODO: Support dedupe.
+;; TODO: Should we call this unique? Or distinct?
+
+;; TODO: Add tests.
+(defun list-dedupe-adjacent (xs)
+  "Return XS without adjacent duplicates."
+  (prelude-assert (not (list-empty? xs)))
+  (list-reduce (list (list-first xs))
+    (lambda (x acc)
+      (if (equal x (list-first acc))
+          acc
+        (list-cons x acc)))
+    xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; (when list-tests?
+;;   (prelude-assert
+;;    (= 0
+;;       (list-length '())))
+;;   (prelude-assert
+;;    (= 5
+;;       (list-length '(1 2 3 4 5))))
+;;   (prelude-assert
+;;    (= 16
+;;       (list-reduce 1 (lambda (x acc) (+ x acc)) '(1 2 3 4 5))))
+;;   (prelude-assert
+;;    (equal '(2 4 6 8 10)
+;;           (list-map (lambda (x) (* x 2)) '(1 2 3 4 5)))))
+
+(provide 'list)
+;;; list.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/macros.el b/users/wpcarro/emacs/.emacs.d/wpc/macros.el
new file mode 100644
index 0000000000..32c9b59dcd
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/macros.el
@@ -0,0 +1,63 @@
+;;; macros.el --- Helpful variables for making my ELisp life more enjoyable -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; This file contains helpful variables that I use in my ELisp development.
+
+;; TODO: Consider a macro solution for mimmicking OCaml's auto resolution of
+;; dependencies using `load-path' and friends.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'f)
+(require 'string)
+(require 'symbol)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmacro macros-enable (mode)
+  "Helper for enabling `MODE'.
+Useful in `add-hook' calls.  Some modes, like `linum-mode' need to be called as
+`(linum-mode 1)', so `(add-hook mode #'linum-mode)' won't work."
+  `#'(lambda nil (,mode 1)))
+
+(defmacro macros-disable (mode)
+  "Helper for disabling `MODE'.
+Useful in `add-hook' calls."
+  `#'(lambda nil (,mode -1)))
+
+(defmacro macros-add-hook-before-save (mode f)
+  "Register a hook, `F', for a mode, `MODE' more conveniently.
+Usage: (macros-add-hook-before-save 'reason-mode-hook #'refmt-before-save)"
+  `(add-hook ,mode
+             (lambda ()
+               (add-hook 'before-save-hook ,f))))
+
+;; TODO: Privatize?
+(defun macros--namespace ()
+  "Return the namespace for a function based on the filename."
+  (->> (buffer-file-name)
+       f-filename
+       f-base))
+
+(defmacro macros-comment (&rest _)
+  "Empty comment s-expresion where `BODY' is ignored."
+  `nil)
+
+(defmacro macros-support-file-extension (ext mode)
+  "Register MODE to automatically load with files ending with EXT extension.
+Usage: (macros-support-file-extension \"pb\" protobuf-mode)"
+  (let ((extension (string-format "\\.%s\\'" ext)))
+    `(add-to-list 'auto-mode-alist '(,extension . ,mode))))
+
+(provide 'macros)
+;;; macros.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/math.el b/users/wpcarro/emacs/.emacs.d/wpc/math.el
new file mode 100644
index 0000000000..4013ce3be2
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/math.el
@@ -0,0 +1,62 @@
+;;; math.el --- Math stuffs -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Containing some useful mathematical functions.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'maybe)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst math-pi pi
+  "The number pi.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Support all three arguments.
+;; Int -> Int -> Int -> Boolean
+(cl-defun math-triangle-of-power (&key base power result)
+  (cond
+   ((maybe-somes? base power result)
+    (error "All three arguments should not be set"))
+   ((maybe-somes? power result)
+    (message "power and result"))
+   ((maybe-somes? base result)
+    (log result base))
+   ((maybe-somes? base power)
+    (expt base power))
+   (t
+    (error "Two of the three arguments must be set"))))
+
+(defun math-mod (x y)
+  "Return X mod Y."
+  (mod x y))
+
+(defun math-exp (x y)
+  "Return X raised to the Y."
+  (expt x y))
+
+(defun math-round (x)
+  "Round X to nearest ones digit."
+  (round x))
+
+(defun math-floor (x)
+  "Floor value X."
+  (floor x))
+
+(provide 'math)
+;;; math.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/maybe.el b/users/wpcarro/emacs/.emacs.d/wpc/maybe.el
new file mode 100644
index 0000000000..ef92e5a4c1
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/maybe.el
@@ -0,0 +1,78 @@
+;;; maybe.el --- Library for dealing with nil values -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Inspired by Elm's Maybe library.
+;;
+;; For now, a Nothing value will be defined exclusively as a nil value.  I'm
+;; uninterested in supported falsiness in this module even at risk of going
+;; against the LISP grain.
+;;
+;; I'm avoiding introducing a struct to handle the creation of Just and Nothing
+;; variants of Maybe.  Perhaps this is a mistake in which case this file would
+;; be more aptly named nil.el.  I may change that.  Because of this limitation,
+;; functions in Elm's Maybe library like andThen, which is the monadic bind for
+;; the Maybe type, doesn't have a home here since we cannot compose multiple
+;; Nothing or Just values without a struct or some other construct.
+;;
+;; Possible names for the variants of a Maybe.
+;; None    | Some
+;; Nothing | Something
+;; None    | Just
+;; Nil     | Set
+;;
+;; NOTE: In Elisp, values like '() (i.e. the empty list) are aliases for nil.
+;; What else in Elisp is an alias in this way?
+;; Examples:
+;; TODO: Provide examples of other nil types in Elisp.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'list)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar maybe--run-tests? t
+  "When t, run the test suite defined herein.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun maybe-nil? (x)
+  "Return t if X is nil."
+  (eq nil x))
+
+(defun maybe-some? (x)
+  "Return t when X is non-nil."
+  (not (maybe-nil? x)))
+
+(defun maybe-nils? (&rest xs)
+  "Return t if all XS are nil."
+  (list-all? #'maybe-nil? xs))
+
+(defun maybe-somes? (&rest xs)
+  "Return t if all XS are non-nil."
+  (list-all? #'maybe-some? xs))
+
+(defun maybe-default (default x)
+  "Return DEFAULT when X is nil."
+  (if (maybe-nil? x) default x))
+
+(defun maybe-map (f x)
+  "Apply F to X if X is not nil."
+  (if (maybe-some? x)
+      (funcall f x)
+    x))
+
+(provide 'maybe)
+;;; maybe.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/modeline.el b/users/wpcarro/emacs/.emacs.d/wpc/modeline.el
new file mode 100644
index 0000000000..df1cddec9d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/modeline.el
@@ -0,0 +1,68 @@
+;;; modeline.el --- Customize my mode-line -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Because I use EXWM, I treat my Emacs mode-line like my system bar: I need to
+;; quickly check the system time, and I expect it to be at the bottom-right of
+;; my Emacs frame.  I used doom-modeline for awhile, which is an impressive
+;; package, but it conditionally colorizes on the modeline for the active
+;; buffer.  So if my bottom-right window is inactive, I cannot see the time.
+;;
+;; My friend, @tazjin, has a modeline setup that I think is more compatible with
+;; EXWM, so I'm going to base my setup off of his.
+
+;;; Code:
+
+(use-package telephone-line)
+
+(defun modeline-bottom-right-window? ()
+  "Determines whether the last (i.e.
+bottom-right) window of the
+active frame is showing the buffer in which this function is
+  executed."
+  (let* ((frame (selected-frame))
+         (right-windows (window-at-side-list frame 'right))
+         (bottom-windows (window-at-side-list frame 'bottom))
+         (last-window (car (seq-intersection right-windows bottom-windows))))
+    (eq (current-buffer) (window-buffer last-window))))
+
+(defun modeline-maybe-render-time ()
+  "Conditionally renders the `mode-line-misc-info' string.
+
+  The idea is to not display information like the current time,
+  load, battery levels on all buffers."
+  (when (modeline-bottom-right-window?)
+    (telephone-line-raw mode-line-misc-info t)))
+
+(defun modeline-setup ()
+  "Render my custom modeline."
+  (telephone-line-defsegment telephone-line-last-window-segment ()
+    (modeline-maybe-render-time))
+  ;; Display the current EXWM workspace index in the mode-line
+  (telephone-line-defsegment telephone-line-exwm-workspace-index ()
+    (when (modeline-bottom-right-window?)
+      (format "[%s]" exwm-workspace-current-index)))
+  ;; Define a highlight font for ~ important ~ information in the last
+  ;; window.
+  (defface special-highlight
+    '((t (:foreground "white" :background "#5f627f"))) "")
+  (add-to-list 'telephone-line-faces
+               '(highlight . (special-highlight . special-highlight)))
+  (setq telephone-line-lhs
+        '((nil . (telephone-line-position-segment))
+          (accent . (telephone-line-buffer-segment))))
+  (setq telephone-line-rhs
+        '((accent . (telephone-line-major-mode-segment))
+          (nil . (telephone-line-last-window-segment
+                  telephone-line-exwm-workspace-index))))
+  (setq telephone-line-primary-left-separator 'telephone-line-tan-left
+        telephone-line-primary-right-separator 'telephone-line-tan-right
+        telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left
+        telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right)
+  (telephone-line-mode 1))
+
+(provide 'modeline)
+;;; modeline.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/number.el b/users/wpcarro/emacs/.emacs.d/wpc/number.el
new file mode 100644
index 0000000000..c8ed665b30
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/number.el
@@ -0,0 +1,142 @@
+;;; number.el --- Functions for working with numbers -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+;; Homepage: https://user.git.corp.google.com/wpcarro/briefcase
+
+;;; Commentary:
+;;
+;; Classifications of numbers:
+;; - Natural: (a.k.a positive integers, counting numbers); {1, 2, 3, ... }
+;;
+;; - Whole: Natural Numbers, plus zero; {0, 1, 2, 3, ...}
+;;
+;; - Integers: Whole numbers plus all the negatives of the natural numbers;
+;;   {... , -2, -1, 0, 1, 2, ...}
+;;
+;; - Rational numbers: (a.k.a. fractions) where the top and bottom numbers are
+;;   integers; e.g., 1/2, 3/4, 7/2, ⁻4/3, 4/1.  Note: The denominator cannot be
+;;   0, but the numerator can be.
+;;
+;; - Real numbers: All numbers that can be written as a decimal.  This includes
+;;   fractions written in decimal form e.g., 0.5, 0.75 2.35, ⁻0.073, 0.3333, or
+;;   2.142857. It also includes all the irrational numbers such as π, √2 etc.
+;;   Every real number corresponds to a point on the number line.
+;;
+;; The functions defined herein attempt to capture the mathematical definitions
+;; of numbers and their classifications as defined above.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'dash)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst number-test? t
+  "When t, run the test suite defined herein.")
+
+;; TODO: What about int.el?
+
+;; TODO: How do we handle a number typeclass?
+
+(defun number-positive? (x)
+  "Return t if `X' is a positive number."
+  (> x 0))
+
+(defun number-negative? (x)
+  "Return t if `X' is a positive number."
+  (< x 0))
+
+;; TODO: Don't rely on this. Need to have 10.0 and 10 behave similarly.
+(defun number-float? (x)
+  "Return t if `X' is a floating point number."
+  (floatp x))
+
+(defun number-natural? (x)
+  "Return t if `X' is a natural number."
+  (and (number-positive? x)
+       (not (number-float? x))))
+
+(defun number-whole? (x)
+  "Return t if `X' is a whole number."
+  (or (= 0 x)
+      (number-natural? x)))
+
+(defun number-integer? (x)
+  "Return t if `X' is an integer."
+  (or (number-whole? x)
+      (number-natural? (- x))))
+
+;; TODO: How defensive should these guards be?  Should we assert that the inputs
+;; are integers before checking evenness or oddness?
+
+;; TODO: Look up Runar (from Unison) definition of handling zero as even or odd.
+
+;; TODO: How should rational numbers be handled? Lisp is supposedly famous for
+;; its handling of rational numbers.
+;; TODO: `calc-mode' supports rational numbers as "1:2" meaning "1/2"
+;; (defun number-rational? (x))
+
+;; TODO: Can or should I support real numbers?
+;; (defun number-real? (x))
+
+(defun number-even? (x)
+  "Return t if `X' is an even number."
+  (or (= 0 x)
+      (= 0 (mod x 2))))
+
+(defun number-odd? (x)
+  "Return t if `X' is an odd number."
+  (not (number-even? x)))
+
+(defun number-dec (x)
+  "Subtract one from `X'.
+While this function is undeniably trivial, I have unintentionally done (- 1 x)
+  when in fact I meant to do (- x 1) that I figure it's better for this function
+  to exist, and for me to train myself to reach for it and its inc counterpart."
+  (- x 1))
+
+(defun number-inc (x)
+  "Add one to `X'."
+  (+ x 1))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(when number-test?
+  (prelude-assert
+   (number-positive? 10))
+  (prelude-assert
+   (number-natural? 10))
+  (prelude-assert
+   (number-whole? 10))
+  (prelude-assert
+   (number-whole? 0))
+  (prelude-assert
+   (number-integer? 10))
+  ;; (prelude-assert
+  ;;  (= 120 (number-factorial 5)))
+  (prelude-assert
+   (number-even? 6))
+  (prelude-refute
+   (number-odd? 6))
+  (prelude-refute
+   (number-positive? -10))
+  (prelude-refute
+   (number-natural? 10.0))
+  (prelude-refute
+   (number-natural? -10))
+  (prelude-refute
+   (number-natural? -10.0)))
+
+(provide 'number)
+;;; number.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/prelude.el b/users/wpcarro/emacs/.emacs.d/wpc/prelude.el
new file mode 100644
index 0000000000..4a332cb8ca
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/prelude.el
@@ -0,0 +1,144 @@
+;;; prelude.el --- My attempt at augmenting Elisp stdlib -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Some of these ideas are scattered across other modules like `fs',
+;; `string-functions', etc.  I'd like to keep everything modular.  I still don't
+;; have an answer for which items belond in `misc'; I don't want that to become
+;; a dumping grounds.  Ideally this file will `require' all other modules and
+;; define just a handful of functions.
+
+;; TODO: Consider removing all dependencies from prelude.el.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'dash)
+(require 's)
+(require 'f)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Utilities
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun prelude-to-string (x)
+  "Convert X to a string."
+  (format "%s" x))
+
+(defun prelude-inspect (&rest args)
+  "Message ARGS where ARGS are any type."
+  (->> args
+       (-map #'prelude-to-string)
+       (apply #'s-concat)
+       message))
+
+(defmacro prelude-call-process-to-string (cmd &rest args)
+  "Return the string output of CMD called with ARGS."
+  `(with-temp-buffer
+     (call-process ,cmd nil (current-buffer) nil ,@args)
+     (buffer-string)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Assertions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Should I `throw' instead of `error' here?
+(defmacro prelude-assert (x)
+  "Errors unless X is t.
+These are strict assertions and purposely do not rely on truthiness."
+  (let ((as-string (prelude-to-string x)))
+    `(unless (equal t ,x)
+       (error (s-concat "Assertion failed: " ,as-string)))))
+
+(defmacro prelude-refute (x)
+  "Errors unless X is nil."
+  (let ((as-string (prelude-to-string x)))
+    `(unless (equal nil ,x)
+       (error (s-concat "Refutation failed: " ,as-string)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Adapter functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun prelude-identity (x)
+  "Return X unchanged."
+  x)
+
+(defun prelude-const (x)
+  "Return a variadic lambda that will return X."
+  (lambda (&rest _) x))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Miscellaneous
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Consider packaging these into a linum-color.el package.
+;; TODO: Generate the color used here from the theme.
+(defvar prelude--linum-safe? nil
+  "Flag indicating whether it is safe to work with function `linum-mode'.")
+
+(defvar prelude--linum-mru-color nil
+  "Stores the color most recently attempted to be applied.")
+
+(add-hook 'linum-mode-hook
+          (lambda ()
+            (setq prelude--linum-safe? t)
+            (when (maybe-some? prelude--linum-mru-color)
+              (set-face-foreground 'linum prelude--linum-mru-color))))
+
+(defun prelude-set-line-number-color (color)
+  "Safely set linum color to `COLOR'.
+
+If this is called before Emacs initializes, the color will be stored in
+`prelude--linum-mru-color' and applied once initialization completes.
+
+Why is this safe?
+If `(set-face-foreground 'linum)' is called before initialization completes,
+Emacs will silently fail.  Without this function, it is easy to introduce
+difficult to troubleshoot bugs in your init files."
+  (if prelude--linum-safe?
+      (set-face-foreground 'linum color)
+    (setq prelude--linum-mru-color color)))
+
+(defun prelude-prompt (prompt)
+  "Read input from user with PROMPT."
+  (read-string prompt))
+
+(cl-defun prelude-start-process (&key name command)
+  "Pass command string, COMMAND, and the function name, NAME.
+This is a wrapper around `start-process' that has an API that resembles
+`shell-command'."
+  ;; TODO: Fix the bug with tokenizing here, since it will split any whitespace
+  ;; character, even though it shouldn't in the case of quoted string in shell.
+  ;; e.g. - "xmodmap -e 'one two three'" => '("xmodmap" "-e" "'one two three'")
+  (prelude-refute (s-contains? "'" command))
+  (let* ((tokens (s-split " " command))
+         (program-name (nth 0 tokens))
+         (program-args (cdr tokens)))
+    (apply #'start-process
+           `(,(format "*%s<%s>*" program-name name)
+             ,nil
+             ,program-name
+             ,@program-args))))
+
+(defun prelude-executable-exists? (name)
+  "Return t if CLI tool NAME exists according to the variable `exec-path'."
+  (let ((file (locate-file name exec-path)))
+    (require 'maybe)
+    (if (maybe-some? file)
+        (f-exists? file)
+      nil)))
+
+(defmacro prelude-time (x)
+  "Print the time it takes to evaluate X."
+  `(benchmark 1 ',x))
+
+(provide 'prelude)
+;;; prelude.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/pulse-audio.el b/users/wpcarro/emacs/.emacs.d/wpc/pulse-audio.el
new file mode 100644
index 0000000000..eaa6106590
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/pulse-audio.el
@@ -0,0 +1,69 @@
+;;; pulse-audio.el --- Control audio with Elisp -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Because everything in my configuration is turning into Elisp these days.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'string)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst pulse-audio--step-size 5
+  "The size by which to increase or decrease the volume.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun pulse-audio--message (x)
+  "Output X to *Messages*."
+  (message (string-format "[pulse-audio.el] %s" x)))
+
+(defun pulse-audio-toggle-mute ()
+  "Mute the default sink."
+  (interactive)
+  (prelude-start-process
+   :name "pulse-audio-toggle-mute"
+   :command "pactl set-sink-mute @DEFAULT_SINK@ toggle")
+  (pulse-audio--message "Mute toggled."))
+
+(defun pulse-audio-toggle-microphone ()
+  "Mute the default sink."
+  (interactive)
+  (prelude-start-process
+   :name "pulse-audio-toggle-microphone"
+   :command "pactl set-source-mute @DEFAULT_SOURCE@ toggle")
+  (pulse-audio--message "Microphone toggled."))
+
+(defun pulse-audio-decrease-volume ()
+  "Low the volume output of the default sink."
+  (interactive)
+  (prelude-start-process
+   :name "pulse-audio-decrease-volume"
+   :command (string-format "pactl set-sink-volume @DEFAULT_SINK@ -%s%%"
+                           pulse-audio--step-size))
+  (pulse-audio--message "Volume decreased."))
+
+(defun pulse-audio-increase-volume ()
+  "Raise the volume output of the default sink."
+  (interactive)
+  (prelude-start-process
+   :name "pulse-audio-increase-volume"
+   :command (string-format "pactl set-sink-volume @DEFAULT_SINK@ +%s%%"
+                           pulse-audio--step-size))
+  (pulse-audio--message "Volume increased."))
+
+(provide 'pulse-audio)
+;;; pulse-audio.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/random.el b/users/wpcarro/emacs/.emacs.d/wpc/random.el
new file mode 100644
index 0000000000..dfe10b6d47
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/random.el
@@ -0,0 +1,80 @@
+;;; random.el --- Functions for working with randomness -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Functions for working with randomness.  Some of this code is not as
+;; functional as I'd like from.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'number)
+(require 'math)
+(require 'series)
+(require 'list)
+(require 'set)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun random-int (x)
+  "Return a random integer from 0 to `X'."
+  (random x))
+
+;; TODO: Make this work with sequences instead of lists.
+(defun random-choice (xs)
+  "Return a random element of `XS'."
+  (let ((ct (list-length xs)))
+    (list-get
+     (random-int ct)
+     xs)))
+
+(defun random-boolean? ()
+  "Randonly return t or nil."
+  (random-choice (list t nil)))
+
+;; TODO: This may not work if any of these generate numbers like 0, 1, etc.
+(defun random-uuid ()
+  "Return a generated UUID string."
+  (let ((eight  (number-dec (math-triangle-of-power :base 16 :power 8)))
+        (four   (number-dec (math-triangle-of-power :base 16 :power 4)))
+        (twelve (number-dec (math-triangle-of-power :base 16 :power 12))))
+    (format "%x-%x-%x-%x-%x"
+            (random-int eight)
+            (random-int four)
+            (random-int four)
+            (random-int four)
+            (random-int twelve))))
+
+(defun random-token (length)
+  "Return a randomly generated hexadecimal string of LENGTH."
+  (->> (series/range 0 (number-dec length))
+       (list-map (lambda (_) (format "%x" (random-int 15))))
+       (list-join "")))
+
+;; TODO: Support random-sample
+;; (defun random-sample (n xs)
+;;   "Return a randomly sample of list XS of size N."
+;;   (prelude-assert (and (>= n 0) (< n (list-length xs))))
+;;   (cl-labels ((do-sample
+;;                (n xs y ys)
+;;                (if (= n (set-count ys))
+;;                    (->> ys
+;;                         set-to-list
+;;                         (list-map (lambda (i)
+;;                                     (list-get i xs))))
+;;                  (if (set-contains? y ys)
+;;                      (do-sample n xs (random-int (list-length xs)) ys)
+;;                    (do-sample n xs y (set-add y ys))))))
+;;     (do-sample n xs (random-int (list-length xs)) (set-new))))
+
+(provide 'random)
+;;; random.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/region.el b/users/wpcarro/emacs/.emacs.d/wpc/region.el
new file mode 100644
index 0000000000..0b692981f8
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/region.el
@@ -0,0 +1,23 @@
+;;; region.el --- Functions for working with regions -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Sometimes Emacs's function names and argument ordering is great; other times,
+;; it isn't.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun region-to-string ()
+  "Return the string in the active region."
+  (buffer-substring-no-properties (region-beginning)
+                                  (region-end)))
+
+(provide 'region)
+;;; region.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/scope.el b/users/wpcarro/emacs/.emacs.d/wpc/scope.el
new file mode 100644
index 0000000000..99cdbd2b5e
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/scope.el
@@ -0,0 +1,106 @@
+;;; scope.el --- Work with a scope data structure -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Exposing an API for working with a scope data structure in a non-mutative
+;; way.
+;;
+;; What's a scope?  Think of a scope as a stack of key-value bindings.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'al)
+(require 'stack)
+(require 'struct)
+(require '>)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Create
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct scope scopes)
+
+(defun scope-new ()
+  "Return an empty scope."
+  (make-scope :scopes (->> (stack-new)
+                           (stack-push (al-new)))))
+
+(defun scope-flatten (xs)
+  "Return a flattened representation of the scope, XS.
+The newest bindings eclipse the oldest."
+  (->> xs
+       scope-scopes
+       stack-to-list
+       (list-reduce (al-new)
+                    (lambda (scope acc)
+                      (al-merge acc scope)))))
+
+(defun scope-push-new (xs)
+  "Push a new, empty scope onto XS."
+  (struct-update scope
+                 scopes
+                 (>-> (stack-push (al-new)))
+                 xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Read
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun scope-get (k xs)
+  "Return K from XS if it's in scope."
+  (->> xs
+       scope-flatten
+       (al-get k)))
+
+(defun scope-current (xs)
+  "Return the newest scope from XS."
+  (let ((xs-copy (copy-scope xs)))
+    (->> xs-copy
+         scope-scopes
+         stack-peek)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Update
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun scope-set (k v xs)
+  "Set value, V, at key, K, in XS for the current scope."
+  (struct-update scope
+                 scopes
+                 (>-> (stack-map-top (>-> (al-set k v))))
+                 xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Delete
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun scope-pop (xs)
+  "Return a new scope without the top element from XS."
+  (->> xs
+       scope-scopes
+       stack-pop))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun scope-defined? (k xs)
+  "Return t if K is in scope of XS."
+  (->> xs
+       scope-flatten
+       (al-has-key? k)))
+
+;; TODO: Find a faster way to write aliases like this.
+(defun scope-instance? (xs)
+  "Return t if XS is a scope struct."
+  (scope-p xs))
+
+(provide 'scope)
+;;; scope.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/screen-brightness.el b/users/wpcarro/emacs/.emacs.d/wpc/screen-brightness.el
new file mode 100644
index 0000000000..851be9f99f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/screen-brightness.el
@@ -0,0 +1,57 @@
+;;; screen-brightness.el --- Control laptop screen brightness -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Control your laptop's screen brightness.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup screen-brightness nil "Configuration for screen-brightness.")
+
+(defcustom screen-brightness-increase-cmd
+  "light -A 3"
+  "The shell command to run to increase screen brightness."
+  :group 'screen-brightness
+  :type 'string)
+
+(defcustom screen-brightness-decrease-cmd
+  "light -U 3"
+  "The shell command to run to decrease screen brightness."
+  :group 'screen-brightness
+  :type 'string)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun screen-brightness-increase ()
+  "Increase the screen brightness."
+  (interactive)
+  (prelude-start-process
+   :name "screen-brightness-increase"
+   :command screen-brightness-increase-cmd)
+  (message "[screen-brightness.el] Increased screen brightness."))
+
+(defun screen-brightness-decrease ()
+  "Decrease the screen brightness."
+  (interactive)
+  (prelude-start-process
+   :name "screen-brightness-decrease"
+   :command screen-brightness-decrease-cmd)
+  (message "[screen-brightness.el] Decreased screen brightness."))
+
+(provide 'screen-brightness)
+;;; screen-brightness.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/scrot.el b/users/wpcarro/emacs/.emacs.d/wpc/scrot.el
new file mode 100644
index 0000000000..08994fea5f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/scrot.el
@@ -0,0 +1,54 @@
+;;; scrot.el --- Screenshot functions -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; scrot is a Linux utility for taking screenshots.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'f)
+(require 'string)
+(require 'ts)
+(require 'clipboard)
+(require 'kbd)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst scrot-screenshot-directory "~/Downloads"
+  "The default directory for screenshot outputs.")
+
+(defconst scrot-output-format "screenshot_%H:%M:%S_%Y-%m-%d.png"
+  "The format string for the output screenshot file.
+See scrot's man page for more information.")
+
+(defun scrot--copy-image (path)
+  "Use xclip to copy the image at PATH to the clipboard.
+This currently only works for PNG files because that's what I'm outputting"
+  (call-process "xclip" nil nil nil
+                "-selection" "clipboard" "-t" "image/png" path)
+  (message (string-format "[scrot.el] Image copied to clipboard!")))
+
+(defun scrot-select ()
+  "Click-and-drag to screenshot a region.
+The output path is copied to the user's clipboard."
+  (interactive)
+  (let ((screenshot-path (f-join scrot-screenshot-directory
+                                 (ts-format scrot-output-format (ts-now)))))
+    (make-process
+     :name "scrot-select"
+     :command `("scrot" "--select" ,screenshot-path)
+     :sentinel (lambda (proc _err)
+                 (when (= 0 (process-exit-status proc))
+                   (scrot--copy-image screenshot-path))))))
+
+(provide 'scrot)
+;;; scrot.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/sequence.el b/users/wpcarro/emacs/.emacs.d/wpc/sequence.el
new file mode 100644
index 0000000000..204a72c5b0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/sequence.el
@@ -0,0 +1,108 @@
+;;; sequence.el --- Working with the "sequence" types -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Elisp supports a typeclass none as "sequence" which covers the following
+;; types:
+;; - list: '(1 2 3 4 5)
+;; - vector: ["John" 27 :blue]
+;; - string: "To be or not to be..."
+
+;; TODO: Document the difference between a "reduce" and a "fold".  I.e. - reduce
+;; has an initial value whereas fold uses the first element in the sequence as
+;; the initial value.
+;;
+;; Note: This should be an approximation of Elixir's Enum protocol albeit
+;; without streams.
+;;
+;; Elisp has done a lot of this work already and these are mostly wrapper
+;; functions.
+;; See the following list for reference:
+;; - sequencep
+;; - elt
+;; - copy-sequence
+;; - reverse
+;; - nreverse
+;; - sort
+;; - seq-elt
+;; - seq-length
+;; - seqp
+;; - seq-drop
+;; - seq-take
+;; - seq-take-while
+;; - seq-drop-while
+;; - seq-do
+;; - seq-map
+;; - seq-mapn
+;; - seq-filter
+;; - seq-remove
+;; - seq-reduce
+;; - seq-some
+;; - seq-find
+;; - seq-every-p
+;; - seq-empty-p
+;; - seq-count
+;; - seq-sort
+;; - seq-contains
+;; - seq-position
+;; - seq-uniq
+;; - seq-subseq
+;; - seq-concatenate
+;; - seq-mapcat
+;; - seq-partition
+;; - seq-intersection
+;; - seq-difference
+;; - seq-group-by
+;; - seq-into
+;; - seq-min
+;; - seq-max
+;; - seq-doseq
+;; - seq-let
+
+;;; Code:
+
+;; Perhaps we can provide default implementations for `filter' and `map' derived
+;; from the `reduce' implementation.
+;; (defprotocol sequence
+;;   :functions (reduce))
+;; (definstance sequence list
+;;   :reduce #'list-reduce
+;;   :filter #'list-filter
+;;   :map    #'list-map)
+;; (definstance sequence vector
+;;   :reduce #'vector/reduce)
+;; (definstance sequence string
+;;   :reduce #'string)
+
+(defun sequence-classify (xs)
+  "Return the type of `XS'."
+  (cond
+   ((listp xs) 'list)
+   ((vectorp xs) 'vector)
+   ((stringp xs) 'string)))
+
+(defun sequence-reduce (acc f xs)
+  "Reduce of `XS' calling `F' on x and `ACC'."
+  (seq-reduce
+   (lambda (acc x)
+     (funcall f x acc))
+   xs
+   acc))
+
+;; Elixir also turned everything into a list for efficiecy reasons.
+
+(defun sequence-filter (p xs)
+  "Filter `XS' with predicate, `P'.
+Returns a list regardless of the type of `XS'."
+  (seq-filter p xs))
+
+(defun sequence-map (f xs)
+  "Maps `XS' calling `F' on each element.
+Returns a list regardless of the type of `XS'."
+  (seq-map f xs))
+
+(provide 'sequence)
+;;; sequence.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/series.el b/users/wpcarro/emacs/.emacs.d/wpc/series.el
new file mode 100644
index 0000000000..d890038839
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/series.el
@@ -0,0 +1,92 @@
+;;; series.el --- Hosting common series of numbers -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Encoding number series as I learn about them.
+;;
+;; These are the following series I'm interested in supporting:
+;; - Fibonacci
+;; - Catalan numbers
+;; - Figurate number series
+;;   - Triangular
+;;   - Square
+;;   - Pentagonal
+;;   - Hexagonal
+;;   - Lazy-caterer
+;; - Magic square
+;; - Look-and-say
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'number)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun series-range (beg end)
+  "Create a list of numbers from `BEG' to `END'.
+This is an inclusive number range."
+  (if (< end beg)
+      (list-reverse
+       (number-sequence end beg))
+    (number-sequence beg end)))
+
+(defun series-fibonacci-number (i)
+  "Return the number in the fibonacci series at `I'."
+  (cond
+   ((= 0 i) 0)
+   ((= 1 i) 1)
+   (t (+ (series-fibonacci-number (- i 1))
+         (series-fibonacci-number (- i 2))))))
+
+(defun series-fibonacci (n)
+  "Return the first `N' numbers of the fibonaccci series starting at zero."
+  (if (= 0 n)
+      '()
+    (list-reverse
+     (list-cons (series-fibonacci-number (number-dec n))
+                (list-reverse
+                 (series-fibonacci (number-dec n)))))))
+
+;; TODO: Consider memoization.
+(defun series-triangular-number (i)
+  "Return the number in the triangular series at `I'."
+  (if (= 0 i)
+      0
+    (+ i (series-triangular-number (number-dec i)))))
+
+;; TODO: Improve performance.
+;; TODO: Consider creating a stream protocol with `stream/next' and implement
+;; this using that.
+(defun series-triangular (n)
+  "Return the first `N' numbers of a triangular series starting at 0."
+  (if (= 0 n)
+      '()
+    (list-reverse
+     (list-cons (series-triangular-number (number-dec n))
+                (list-reverse
+                 (series-triangular (number-dec n)))))))
+
+(defun series-catalan-number (i)
+  "Return the catalan number in the series at `I'."
+  (if (= 0 i)
+      1
+    (/ (number-factorial (* 2 i))
+       (* (number-factorial (number-inc i))
+          (number-factorial i)))))
+
+(defun series-catalan (n)
+  "Return the first `N' numbers in a catalan series."
+  (->> (series-range 0 (number-dec n))
+       (list-map #'series-catalan-number)))
+
+(provide 'series)
+;;; series.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/set.el b/users/wpcarro/emacs/.emacs.d/wpc/set.el
new file mode 100644
index 0000000000..778b089e15
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/set.el
@@ -0,0 +1,174 @@
+;;; set.el --- Working with mathematical sets -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; The set data structure is a collection that deduplicates its elements.
+
+;;; Code:
+
+(require 'ht) ;; friendlier API for hash-tables
+(require 'dotted)
+(require 'struct)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Wish List
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; - TODO: Support enum protocol for set.
+;; - TODO: Prefer a different hash-table library that doesn't rely on mutative
+;;   code.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct set xs)
+
+(defconst set-enable-testing? t
+  "Run tests when t.")
+
+(defun set-from-list (xs)
+  "Create a new set from the list XS."
+  (make-set :xs (->> xs
+                     (list-map #'dotted-new)
+                     ht-from-alist)))
+
+(defun set-new (&rest args)
+  "Create a new set from ARGS."
+  (set-from-list args))
+
+(defun set-to-list (xs)
+  "Map set XS into a list."
+  (->> xs
+       set-xs
+       ht-keys))
+
+(defun set-add (x xs)
+  "Add X to set XS."
+  (struct-update set
+                 xs
+                 (lambda (table)
+                   (let ((table-copy (ht-copy table)))
+                     (ht-set table-copy x nil)
+                     table-copy))
+                 xs))
+
+;; TODO: Ensure all `*/reduce' functions share the same API.
+(defun set-reduce (acc f xs)
+  "Return a new set by calling F on each element of XS and ACC."
+  (->> xs
+       set-to-list
+       (list-reduce acc f)))
+
+(defun set-intersection (a b)
+  "Return the set intersection between A and B."
+  (set-reduce (set-new)
+              (lambda (x acc)
+                (if (set-contains? x b)
+                    (set-add x acc)
+                  acc))
+              a))
+
+(defun set-count (xs)
+  "Return the number of elements in XS."
+  (->> xs
+       set-xs
+       ht-size))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun set-empty? (xs)
+  "Return t if XS has no elements in it."
+  (= 0 (set-count xs)))
+
+(defun set-contains? (x xs)
+  "Return t if set XS has X."
+  (ht-contains? (set-xs xs) x))
+
+;; TODO: Prefer using `ht.el' functions for this.
+(defun set-equal? (a b)
+  "Return t if A and B share the name members."
+  (ht-equal? (set-xs a)
+             (set-xs b)))
+
+(defun set-distinct? (a b)
+  "Return t if A and B have no shared members."
+  (set-empty? (set-intersection a b)))
+
+(defun set-superset? (a b)
+  "Return t if A has all of the members of B."
+  (->> b
+       set-to-list
+       (list-all? (lambda (x) (set-contains? x a)))))
+
+(defun set-subset? (a b)
+  "Return t if each member of set A is present in set B."
+  (set-superset? b a))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(when set-enable-testing?
+  ;; set-distinct?
+  (prelude-assert
+   (set-distinct? (set-new 'one 'two 'three)
+                  (set-new 'a 'b 'c)))
+  (prelude-refute
+   (set-distinct? (set-new 1 2 3)
+                  (set-new 3 4 5)))
+  (prelude-refute
+   (set-distinct? (set-new 1 2 3)
+                  (set-new 1 2 3)))
+  ;; set-equal?
+  (prelude-refute
+   (set-equal? (set-new 'a 'b 'c)
+               (set-new 'x 'y 'z)))
+  (prelude-refute
+   (set-equal? (set-new 'a 'b 'c)
+               (set-new 'a 'b)))
+  (prelude-assert
+   (set-equal? (set-new 'a 'b 'c)
+               (set-new 'a 'b 'c)))
+  ;; set-intersection
+  (prelude-assert
+   (set-equal? (set-new 2 3)
+               (set-intersection (set-new 1 2 3)
+                                 (set-new 2 3 4))))
+  ;; set-{from,to}-list
+  (prelude-assert (equal '(1 2 3)
+                         (->> '(1 1 2 2 3 3)
+                              set-from-list
+                              set-to-list)))
+  (let ((primary-colors (set-new "red" "green" "blue")))
+    ;; set-subset?
+    (prelude-refute
+     (set-subset? (set-new "black" "grey")
+                  primary-colors))
+    (prelude-assert
+     (set-subset? (set-new "red")
+                  primary-colors))
+    ;; set-superset?
+    (prelude-refute
+     (set-superset? primary-colors
+                    (set-new "black" "grey")))
+    (prelude-assert
+     (set-superset? primary-colors
+                    (set-new "red" "green" "blue")))
+    (prelude-assert
+     (set-superset? primary-colors
+                    (set-new "red" "blue"))))
+  ;; set-empty?
+  (prelude-assert (set-empty? (set-new)))
+  (prelude-refute (set-empty? (set-new 1 2 3)))
+  ;; set-count
+  (prelude-assert (= 0 (set-count (set-new))))
+  (prelude-assert (= 2 (set-count (set-new 1 1 2 2)))))
+
+(provide 'set)
+;;; set.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/ssh.el b/users/wpcarro/emacs/.emacs.d/wpc/ssh.el
new file mode 100644
index 0000000000..1179e90363
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/ssh.el
@@ -0,0 +1,67 @@
+;;; ssh.el --- When working remotely -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Configuration to make remote work easier.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'tramp)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Is "ssh" preferable to "scp"?
+(setq tramp-default-method "ssh")
+
+;; Taken from: https://superuser.com/questions/179313/tramp-waiting-for-prompts-from-remote-shell
+(setq tramp-shell-prompt-pattern "^[^$>\n]*[#$%>] *\\(\[[0-9;]*[a-zA-Z] *\\)*")
+
+;; Sets the value of the TERM variable to "dumb" when logging into the remote
+;; host. This allows me to check for the value of "dumb" in my shell's init file
+;; and control the startup accordingly. You can see in the (shamefully large)
+;; commit, 0b4ef0e, that I added a check like this to my ~/.zshrc. I've since
+;; switched from z-shell to fish. I don't currently have this check in
+;; config.fish, but I may need to add it one day soon.
+(setq tramp-terminal-type "dumb")
+
+;; Maximizes the tramp debugging noisiness while I'm still learning about tramp.
+(setq tramp-verbose 10)
+
+;; As confusing as this may seem, this forces Tramp to use *my* .ssh/config
+;; options, which enable ControlMaster. In other words, disabling this actually
+;; enables ControlMaster.
+(setq tramp-use-ssh-controlmaster-options nil)
+
+(defcustom ssh-hosts '("wpcarro@wpcarro.dev"
+                       "foundation"
+                       "edge")
+  "List of hosts to which I commonly connect.")
+
+(defun ssh-sudo-buffer ()
+  "Open the current buffer with sudo rights."
+  (interactive)
+  (with-current-buffer (current-buffer)
+    (if (s-starts-with? "/ssh:" buffer-file-name)
+        (pcase (s-split ":" buffer-file-name)
+          (`(,one ,two ,three) (find-file (format "/ssh:%s|sudo:%s:%s" two two three))))
+        (find-file
+         (s-join ":" (-insert-at 2 "|sudo" (s-split ":" buffer-file-name))))
+      (find-file (format "/sudo::%s" buffer-file-name)))))
+
+(defun ssh-cd-home ()
+  "Prompt for an SSH host and open a dired buffer for wpcarro on that machine."
+  (interactive)
+  (let ((machine (completing-read "Machine: " ssh-hosts)))
+    (find-file (format "/ssh:%s:~" machine))))
+
+(provide 'ssh)
+;;; ssh.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/stack.el b/users/wpcarro/emacs/.emacs.d/wpc/stack.el
new file mode 100644
index 0000000000..3d1e3e4a16
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/stack.el
@@ -0,0 +1,101 @@
+;;; stack.el --- Working with stacks in Elisp -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; A stack is a LIFO queue.
+;; The design goal here is to expose an intuitive API for working with stacks in
+;; non-mutative way.
+;;
+;; TODO: Consider naming a Functor instance "Mappable."
+;; TODO: Consider naming a Foldable instance "Reduceable."
+;;
+;; TODO: Consider implementing an instance for Mappable.
+;; TODO: Consider implementing an instance for Reduceable.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'list)
+(require '>)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Create
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct stack xs)
+
+(defun stack-new ()
+  "Create an empty stack."
+  (make-stack :xs '()))
+
+(defun stack-from-list (xs)
+  "Create a new stack from the list, `XS'."
+  (list-reduce (stack-new) #'stack-push xs))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Read
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun stack-peek (xs)
+  "Look at the top element of `XS' without popping it off."
+  (->> xs
+       stack-xs
+       list-head))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Update
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun stack-push (x xs)
+  "Push `X' on `XS'."
+  (struct-update stack
+                 xs
+                 (>-> (list-cons x))
+                 xs))
+
+;; TODO: How to return something like {(list-head xs), (list-tail xs)} in Elixir
+;; TODO: How to handle popping from empty stacks?
+(defun stack-pop (xs)
+  "Return the stack, `XS', without the top element.
+Since I cannot figure out a nice way of return tuples in Elisp, if you want to
+look at the first element, use `stack-peek' before running `stack-pop'."
+  (struct-update stack
+                 xs
+                 (>-> list-tail)
+                 xs))
+
+(defun stack-map-top (f xs)
+  "Apply F to the top element of XS."
+  (->> xs
+       stack-pop
+       (stack-push (funcall f (stack-peek xs)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Miscellaneous
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun stack-to-list (xs)
+  "Return XS as a list.
+The round-trip property of `stack-from-list' and `stack-to-list' should hold."
+  (->> xs
+       stack-xs
+       list-reverse))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Create a macro that wraps `cl-defstruct' that automatically creates
+;; things like `new', `instance?'.
+(defun stack-instance? (xs)
+  "Return t if XS is a stack."
+  (stack-p xs))
+
+(provide 'stack)
+;;; stack.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/string.el b/users/wpcarro/emacs/.emacs.d/wpc/string.el
new file mode 100644
index 0000000000..7e3f10c75a
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/string.el
@@ -0,0 +1,110 @@
+;;; string.el --- Library for working with strings -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Library for working with string.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 's)
+(require 'dash)
+;; TODO: Resolve the circular dependency that this introduces.
+;; (require 'prelude)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun string-contains? (c x)
+  "Return t if X is in C."
+  (s-contains? c x))
+
+(defun string-hookify (x)
+  "Append \"-hook\" to X."
+  (s-append "-hook" x))
+
+(defun string-split (y x)
+  "Map string X into a list of strings that were separated by Y."
+  (s-split y x))
+
+(defun string-ensure-hookified (x)
+  "Ensure that X has \"-hook\" appended to it."
+  (if (s-ends-with? "-hook" x)
+      x
+    (string-hookify x)))
+
+(defun string-format (x &rest args)
+  "Format template string X with ARGS."
+  (apply #'format (cons x args)))
+
+(defun string-concat (&rest strings)
+  "Joins `STRINGS' into onto string."
+  (apply #'s-concat strings))
+
+(defun string-->symbol (string)
+  "Maps `STRING' to a symbol."
+  (intern string))
+
+(defun string-<-symbol (symbol)
+  "Maps `SYMBOL' into a string."
+  (symbol-name symbol))
+
+(defun string-prepend (prefix x)
+  "Prepend `PREFIX' onto `X'."
+  (s-concat prefix x))
+
+(defun string-append (postfix x)
+  "Appen `POSTFIX' onto `X'."
+  (s-concat x postfix))
+
+(defun string-surround (s x)
+  "Surrounds `X' one each side with `S'."
+  (->> x
+       (string-prepend s)
+       (string-append s)))
+
+;; TODO: Define a macro for defining a function and a test.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Casing
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun string-caps->kebab (x)
+  "Change the casing of `X' from CAP_CASE to kebab-case."
+  (->> x
+       s-downcase
+       (s-replace "_" "-")))
+
+(defun string-kebab->caps (x)
+  "Change the casing of X from CAP_CASE to kebab-case."
+  (->> x
+       s-upcase
+       (s-replace "-" "_")))
+
+(defun string-lower->caps (x)
+  "Change the casing of X from lowercase to CAPS_CASE."
+  (->> x
+       s-upcase
+       (s-replace " " "_")))
+
+(defun string-lower->kebab (x)
+  "Change the casing of `X' from lowercase to kebab-case."
+  (s-replace " " "-" x))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun string-instance? (x)
+  "Return t if X is a string."
+  (stringp x))
+
+(provide 'string)
+;;; string.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/struct.el b/users/wpcarro/emacs/.emacs.d/wpc/struct.el
new file mode 100644
index 0000000000..eeea04bf26
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/struct.el
@@ -0,0 +1,85 @@
+;;; struct.el --- Helpers for working with structs -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;; Provides new macros for working with structs.  Also provides adapter
+;; interfaces to existing struct macros, that should have more intuitive
+;; interfaces.
+;;
+;; Sometimes `setf' just isn't enough.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'string)
+(require 'dash)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar struct--enable-tests? t
+  "When t, run the test suite defined herein.")
+
+(defmacro struct-update (type field f xs)
+  "Apply F to FIELD in XS, which is a struct of TYPE.
+This is immutable."
+  (let ((copier (->> type
+                     symbol-name
+                     (string-prepend "copy-")
+                     intern))
+        (accessor (->> field
+                       symbol-name
+                       (string-prepend (string-concat (symbol-name type) "-"))
+                       intern)))
+    `(let ((copy (,copier ,xs)))
+       (setf (,accessor copy) (funcall ,f (,accessor copy)))
+       copy)))
+
+(defmacro struct-set (type field x xs)
+  "Immutably set FIELD in XS (struct TYPE) to X."
+  (let ((copier (->> type
+                     symbol-name
+                     (string-prepend "copy-")
+                     intern))
+        (accessor (->> field
+                       symbol-name
+                       (string-prepend (string-concat (symbol-name type) "-"))
+                       intern)))
+    `(let ((copy (,copier ,xs)))
+       (setf (,accessor copy) ,x)
+       copy)))
+
+(defmacro struct-set! (type field x xs)
+  "Set FIELD in XS (struct TYPE) to X mutably.
+This is an adapter interface to `setf'."
+  (let ((accessor (->> field
+                       symbol-name
+                       (string-prepend (string-concat (symbol-name type) "-"))
+                       intern)))
+    `(progn
+       (setf (,accessor ,xs) ,x)
+       ,xs)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(when struct--enable-tests?
+  (cl-defstruct dummy name age)
+  (defvar struct--test-dummy (make-dummy :name "Roofus" :age 19))
+  (struct-set! dummy name "Doofus" struct--test-dummy)
+  (prelude-assert (string= "Doofus" (dummy-name struct--test-dummy)))
+  (let ((result (struct-set dummy name "Shoofus" struct--test-dummy)))
+    ;; Test the immutability of `struct-set'
+    (prelude-assert (string= "Doofus" (dummy-name struct--test-dummy)))
+    (prelude-assert (string= "Shoofus" (dummy-name result)))))
+
+(provide 'struct)
+;;; struct.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/symbol.el b/users/wpcarro/emacs/.emacs.d/wpc/symbol.el
new file mode 100644
index 0000000000..79d665fa20
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/symbol.el
@@ -0,0 +1,48 @@
+;;; symbol.el --- Library for working with symbols -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Library for working with symbols.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'string)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Symbols
+(defun symbol-as-string (callback x)
+  "Treat the symbol, X, as a string while applying CALLBACK to it.
+Coerce back to a symbol on the way out."
+  (->> x
+       #'symbol-name
+       callback
+       #'intern))
+
+(defun symbol-to-string (x)
+  "Map `X' into a string."
+  (string-<-symbol x))
+
+(defun symbol-hookify (x)
+  "Append \"-hook\" to X when X is a symbol."
+  (symbol-as-string #'string-hookify x))
+
+(defun symbol-ensure-hookified (x)
+  "Ensure that X has \"-hook\" appended to it when X is a symbol."
+  (symbol-as-string #'string-ensure-hookified x))
+
+(defun symbol-instance? (x)
+  "Return t if X is a symbol."
+  (symbolp x))
+
+(provide 'symbol)
+;;; symbol.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/timestring.el b/users/wpcarro/emacs/.emacs.d/wpc/timestring.el
new file mode 100644
index 0000000000..245ace49e7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/timestring.el
@@ -0,0 +1,77 @@
+;;; timestring.el --- Quickly access timestamps in different formats -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+
+;; I was making some API calls where a URL needed a `since` parameter that of an
+;; RFC 3339 encoded string.
+;;
+;; Because I didn't know what a RFC 3339 encoded
+;; string was at the time, and because I didn't know what its format was
+;; according to strftime, and because I'm most likely to forget both of these
+;; things by the next time that I need something similar, I decided to write
+;; this package so that I can accumulate a list of common time encodings.
+;;
+;; Thank you, Emacs.
+;;
+;; p.s. - I may turn this into a proper module and publish it.  But not today.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ts)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup timestring nil
+  "Customize group for timestring configuration.")
+
+(defcustom timestring-supported-encodings
+  '(("RFC 3339" . "%Y-%m-%dT%H:%M:%SZ")
+    ;; Does anyone recognize this format?
+    ("IDK" . "%Y-%m-%d %H:%M:%S %z"))
+  "Mapping of encoding names to their format strings."
+  :group 'timestring)
+
+(defcustom timestring-supported-times
+  '(("yesterday" . timestring--yesterday)
+    ("now" . ts-now)
+    ("tomorrow" . timestring--tomorrow))
+  "Mapping of a labels to the functions that create those time objects."
+  :group 'timestring)
+
+(defun timestring--yesterday ()
+  "Return a time object for yesterday."
+  (ts-adjust 'day -1 (ts-now)))
+
+(defun timestring--tomorrow ()
+  "Return a time object for yesterday."
+  (ts-adjust 'day +1 (ts-now)))
+
+(defun timestring--completing-read (label xs)
+  "Call `completing-read' with LABEL over the collection XS."
+  (alist-get (completing-read label xs) xs nil nil #'equal))
+
+(defun timestring-copy-encoded-time ()
+  "Select a common time and an encoding.
+
+The selected time will be encoded using the selected encoding and copied onto
+your clipboard."
+  (interactive)
+  (let ((time (funcall (timestring--completing-read
+                        "Time: " timestring-supported-times)))
+        (fmt (timestring--completing-read
+              "Encoding: " timestring-supported-encodings)))
+    (kill-new (ts-format fmt time))
+    (message "Copied!")))
+
+(provide 'timestring)
+;;; timestring.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/tree.el b/users/wpcarro/emacs/.emacs.d/wpc/tree.el
new file mode 100644
index 0000000000..332e6c8d25
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/tree.el
@@ -0,0 +1,199 @@
+;;; tree.el --- Working with Trees -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Some friendly functions that hopefully will make working with trees cheaper
+;; and therefore more appealing!
+;;
+;; Tree terminology:
+;; - leaf: node with zero children.
+;; - root: node with zero parents.
+;; - depth: measures a node's distance from the root node.  This implies the
+;;   root node has a depth of zero.
+;; - height: measures the longest traversal from a node to a leaf.  This implies
+;;   that a leaf node has a height of zero.
+;; - balanced?
+;;
+;; Tree variants:
+;; - binary: the maximum number of children is two.
+;; - binary search: the maximum number of children is two and left sub-trees are
+;;   lower in value than right sub-trees.
+;; - rose: the number of children is variable.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'list)
+(require 'set)
+(require 'tuple)
+(require 'series)
+(require 'random)
+(require 'maybe)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct tree xs)
+
+(cl-defstruct node value children)
+
+(cl-defun tree-node (value &optional children)
+  "Create a node struct of VALUE with CHILDREN."
+  (make-node :value value
+             :children children))
+
+(defun tree-reduce-breadth (acc f xs)
+  "Reduce over XS breadth-first applying F to each x and ACC (in that order).
+Breadth-first traversals guarantee to find the shortest path in a graph.
+  They're typically more difficult to implement than DFTs and may also incur
+  higher memory costs on average than their depth-first counterparts.")
+
+;; TODO: Support :order as 'pre | 'in | 'post.
+;; TODO: Troubleshoot why I need defensive (nil? node) check.
+(defun tree-reduce-depth (acc f node)
+  "Reduce over NODE depth-first applying F to each NODE and ACC.
+F is called with each NODE, ACC, and the current depth.
+Depth-first traversals have the advantage of typically consuming less memory
+  than their breadth-first equivalents would have.  They're also typically
+  easier to implement using recursion.  This comes at the cost of not
+  guaranteeing to be able to find the shortest path in a graph."
+  (cl-labels ((do-reduce-depth
+               (acc f node depth)
+               (let ((acc-new (funcall f node acc depth)))
+                 (if (or (maybe-nil? node)
+                         (tree-leaf? node))
+                     acc-new
+                   (list-reduce
+                    acc-new
+                    (lambda (node acc)
+                      (tree-do-reduce-depth
+                       acc
+                       f
+                       node
+                       (number-inc depth)))
+                    (node-children node))))))
+    (do-reduce-depth acc f node 0)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helpers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun tree-height (xs)
+  "Return the height of tree XS.")
+
+;; TODO: Troubleshoot why need for (nil? node).  Similar misgiving
+;; above.
+(defun tree-leaf-depths (xs)
+  "Return a list of all of the depths of the leaf nodes in XS."
+  (list-reverse
+   (tree-reduce-depth
+    '()
+    (lambda (node acc depth)
+      (if (or (maybe-nil? node)
+              (tree-leaf? node))
+          (list-cons depth acc)
+        acc))
+    xs)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Generators
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Consider parameterizing height, forced min-max branching, random
+;; distributions, etc.
+
+;; TODO: Bail out before stack overflowing by consider branching, current-depth.
+
+(cl-defun tree-random (&optional (value-fn (lambda (_) nil))
+                                 (branching-factor 2))
+  "Randomly generate a tree with BRANCHING-FACTOR.
+
+This uses VALUE-FN to compute the node values.  VALUE-FN is called with the
+current-depth of the node.  Useful for generating test data.  Warning this
+function can overflow the stack."
+  (cl-labels ((do-random
+               (d vf bf)
+               (make-node
+                :value (funcall vf d)
+                :children (->> (series/range 0 (number-dec bf))
+                               (list-map
+                                (lambda (_)
+                                  (when (random-boolean?)
+                                    (do-random d vf bf))))))))
+    (do-random 0 value-fn branching-factor)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun tree-instance? (tree)
+  "Return t if TREE is a tree struct."
+  (node-p tree))
+
+(defun tree-leaf? (node)
+  "Return t if NODE has no children."
+  (maybe-nil? (node-children node)))
+
+(defun tree-balanced? (n xs)
+  "Return t if the tree, XS, is balanced.
+A tree is balanced if none of the differences between any two depths of two leaf
+  nodes in XS is greater than N."
+  (> n (->> xs
+            tree-leaf-depths
+            set-from-list
+            set-count
+            number-dec)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst tree-enable-testing? t
+  "When t, test suite runs.")
+
+;; TODO: Create set of macros for a proper test suite including:
+;; - describe (arbitrarily nestable)
+;; - it (arbitrarily nestable)
+;; - line numbers for errors
+;; - accumulated output for synopsis
+;; - do we want describe *and* it? Why not a generic label that works for both?
+(when tree-enable-testing?
+  (let ((tree-a (tree-node 1
+                           (list (tree-node 2
+                                            (list (tree-node 5)
+                                                  (tree-node 6)))
+                                 (tree-node 3
+                                            (list (tree-node 7)
+                                                  (tree-node 8)))
+                                 (tree-node 4
+                                            (list (tree-node 9)
+                                                  (tree-node 10))))))
+        (tree-b (tree-node 1
+                           (list (tree-node 2
+                                            (list (tree-node 5)
+                                                  (tree-node 6)))
+                                 (tree-node 3)
+                                 (tree-node 4
+                                            (list (tree-node 9)
+                                                  (tree-node 10)))))))
+    ;; instance?
+    (prelude-assert (tree-instance? tree-a))
+    (prelude-assert (tree-instance? tree-b))
+    (prelude-refute (tree-instance? '(1 2 3)))
+    (prelude-refute (tree-instance? "oak"))
+    ;; balanced?
+    (prelude-assert (tree-balanced? 1 tree-a))
+    (prelude-refute (tree-balanced? 1 tree-b))
+    (message "Tests pass!")))
+
+(provide 'tree)
+;;; tree.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/tuple.el b/users/wpcarro/emacs/.emacs.d/wpc/tuple.el
new file mode 100644
index 0000000000..848c6fa48b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/tuple.el
@@ -0,0 +1,93 @@
+;;; tuple.el --- Tuple API for Elisp -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Work with cons cells with two elements with a familiar API for those who have
+;; worked with tuples before.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct tuple first second)
+
+;; Create
+(defun tuple-new ()
+  "Return an empty tuple."
+  (make-tuple :first nil
+              :second nil))
+
+(defun tuple-from (a b)
+  "Return a new tuple from A and B."
+  (make-tuple :first a
+              :second b))
+
+(defun tuple-from-dotted (dp)
+  "Convert dotted pair, DP, into a tuple."
+  (tuple-from (car dp) (cdr dp)))
+
+;; Read
+(defun tuple-first (pair)
+  "Return the first element of PAIR."
+  (tuple-first pair))
+
+(defun tuple-second (pair)
+  "Return the second element of PAIR."
+  (tuple-second pair))
+
+;; Update
+(defun tuple-map-each (f g pair)
+  "Apply F to first, G to second in PAIR."
+  (->> pair
+       (tuple-map-first f)
+       (tuple-map-second g)))
+
+(defun tuple-map (f pair)
+  "Apply F to PAIR."
+  (let ((pair-copy (copy-tuple pair)))
+    (funcall f pair-copy)))
+
+(defun tuple-map-first (f pair)
+  "Apply function F to the first element of PAIR."
+  (let ((pair-copy (copy-tuple pair)))
+    (setf (tuple-first pair-copy) (funcall f (tuple-first pair-copy)))
+    pair-copy))
+
+(defun tuple-map-second (f pair)
+  "Apply function F to the second element of PAIR."
+  (let ((pair-copy (copy-tuple pair)))
+    (setf (tuple-second pair-copy) (funcall f (tuple-second pair-copy)))
+    pair-copy))
+
+(defun tuple-set-first (a pair)
+  "Return a new tuple with the first element set as A in PAIR."
+  (tuple-map-first (lambda (_) a) pair))
+
+(defun tuple-set-second (b pair)
+  "Return a new tuple with the second element set as B in PAIR."
+  (tuple-map-second (lambda (_) b) pair))
+
+;; Delete
+(defun tuple-delete-first (pair)
+  "Return PAIR with the first element set to nil."
+  (tuple-set-first nil pair))
+
+(defun tuple-delete-second (pair)
+  "Return PAIR with the second element set to nil."
+  (tuple-set-second nil pair))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun tuple-instance? (x)
+  "Return t if X is a tuple."
+  (tuple-p x))
+
+(provide 'tuple)
+;;; tuple.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/vector.el b/users/wpcarro/emacs/.emacs.d/wpc/vector.el
new file mode 100644
index 0000000000..6b89708cef
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/vector.el
@@ -0,0 +1,84 @@
+;;; vector.el --- Working with Elisp's Vector data type -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; It might be best to think of Elisp vectors as tuples in languages like
+;; Haskell or Erlang.
+;;
+;; Not surprisingly, this API is modelled after Elixir's Tuple API.
+;;
+;; Some Elisp trivia:
+;; - "Array": Usually means vector or string.
+;; - "Sequence": Usually means list or "array" (see above).
+;;
+;; It might be a good idea to think of Array and Sequence as typeclasses in
+;; Elisp.  This is perhaps more similar to Elixir's notion of the Enum protocol.
+;;
+;; Intentionally not supporting a to-list function, because tuples can contain
+;; heterogenous types whereas lists should contain homogenous types.
+
+;;; Code:
+
+;; TODO: Consider supporting an alias named tuple for vector.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst vector-enable-tests? t
+  "When t, run the tests defined herein.")
+
+;; TODO: Consider labelling variadic functions like `vector-concat*'
+;; vs. `vector-concat'.
+(defun vector-concat (&rest args)
+  "Return a new vector composed of all vectors in `ARGS'."
+  (apply #'vconcat args))
+
+;; TODO: Here's a sketch of a protocol macro being consumed.
+;; (definstance monoid vector
+;;   :empty (lambda () []))
+
+(defun vector-prepend (x xs)
+  "Add `X' to the beginning of `XS'."
+  (vector-concat `[,x] xs))
+
+(defun vector-append (x xs)
+  "Add `X' to the end of `XS'."
+  (vector-concat xs `[,x]))
+
+(defun vector-get (i xs)
+  "Return the value in `XS' at index, `I'."
+  (aref xs i))
+
+(defun vector-set (i v xs)
+  "Set index `I' to value `V' in `XS'.
+Returns a copy of `XS' with the updates."
+  (let ((copy (vconcat [] xs)))
+    (aset copy i v)
+    copy))
+
+(defun vector-set! (i v xs)
+  "Set index `I' to value `V' in `XS'.
+This function mutates XS."
+  (aset xs i v))
+
+(when vector-enable-tests?
+  (let ((xs [1 2 3])
+        (ys [1 2 3]))
+    (prelude-assert (= 1 (vector-get 0 ys)))
+    (vector-set 0 4 ys)
+    (prelude-assert (= 1 (vector-get 0 ys)))
+    (prelude-assert (= 1 (vector-get 0 xs)))
+    (vector-set! 0 4 xs)
+    (prelude-assert (= 4 (vector-get 0 xs)))))
+
+;; TODO: Decide between "remove" and "delete" as the appropriate verbs.
+;; TODO: Implement this.
+;; (defun vector/delete (i xs)
+;;   "Remove the element at `I' in `XS'.")
+
+(provide 'vector)
+;;; vector.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/vterm-mgt.el b/users/wpcarro/emacs/.emacs.d/wpc/vterm-mgt.el
new file mode 100644
index 0000000000..ec9a04d1c8
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/vterm-mgt.el
@@ -0,0 +1,142 @@
+;;; vterm-mgt.el --- Help me manage my vterm instances -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Supporting functions to instantiate vterm buffers, kill existing vterm
+;; buffers, rename vterm buffers, cycle forwards and backwards through vterm
+;; buffers.
+;;
+;; Many of the functions defined herein are intended to be bound to
+;; `vterm-mode-map'.  Some assertions are made to guard against calling
+;; functions that are intended to be called from outside of a vterm buffer.
+;; These assertions shouldn't error when the functions are bound to
+;; `vterm-mode-map'.  If for some reason, you'd like to bind these functions to
+;; a separate keymap, caveat emptor.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'dash)
+(require 'cycle)
+(require 'vterm)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup vterm-mgt nil
+  "Customization options for `vterm-mgt'.")
+
+(defcustom vterm-mgt-scroll-on-focus nil
+  "When t, call `end-of-buffer' after focusing a vterm instance."
+  :type '(boolean)
+  :group 'vterm-mgt)
+
+(defconst vterm-mgt--instances (cycle-new)
+  "A cycle tracking all of my vterm instances.")
+
+(defun vterm-mgt--instance? (b)
+  "Return t if the buffer B is a vterm instance."
+  (equal 'vterm-mode (buffer-local-value 'major-mode b)))
+
+(defmacro vterm-mgt--assert-vterm-buffer ()
+  "Error when the `current-buffer' is not a vterm buffer."
+  '(prelude-assert (vterm-mgt--instance? (current-buffer))))
+
+(defun vterm-mgt-next ()
+  "Replace the current buffer with the next item in `vterm-mgt--instances'.
+This function should be called from a buffer running vterm."
+  (interactive)
+  (vterm-mgt--assert-vterm-buffer)
+  (vterm-mgt-reconcile-state)
+  (cycle-focus-item (current-buffer) vterm-mgt--instances)
+  (switch-to-buffer (cycle-next vterm-mgt--instances))
+  (when vterm-mgt-scroll-on-focus (end-of-buffer)))
+
+(defun vterm-mgt-prev ()
+  "Replace the current buffer with the previous item in `vterm-mgt--instances'.
+This function should be called from a buffer running vterm."
+  (interactive)
+  (vterm-mgt--assert-vterm-buffer)
+  (vterm-mgt-reconcile-state)
+  (cycle-focus-item (current-buffer) vterm-mgt--instances)
+  (switch-to-buffer (cycle-prev vterm-mgt--instances))
+  (when vterm-mgt-scroll-on-focus (end-of-buffer)))
+
+(defun vterm-mgt-instantiate ()
+  "Create a new vterm instance.
+
+Prefer calling this function instead of `vterm'.  This function ensures that the
+  newly created instance is added to `vterm-mgt--instances'.
+
+If however you must call `vterm', if you'd like to cycle through vterm
+  instances, make sure you call `vterm-mgt-reconcile-state' to allow vterm-mgt
+  to collect any untracked vterm instances."
+  (interactive)
+  (vterm-mgt-reconcile-state)
+  (let ((buffer (vterm t)))
+    (cycle-append buffer vterm-mgt--instances)
+    (cycle-focus-item buffer vterm-mgt--instances)))
+
+(defun vterm-mgt-kill ()
+  "Kill the current buffer and remove it from `vterm-mgt--instances'.
+This function should be called from a buffer running vterm."
+  (interactive)
+  (vterm-mgt--assert-vterm-buffer)
+  (let* ((buffer (current-buffer)))
+    (when (kill-buffer buffer)
+      (vterm-mgt-reconcile-state))))
+
+(defun vterm-mgt-find-or-create ()
+  "Call `switch-to-buffer' on a focused vterm instance if there is one.
+
+When `cycle-focused?' returns nil, focus the first item in the cycle.  When
+there are no items in the cycle, call `vterm-mgt-instantiate' to create a vterm
+instance."
+  (interactive)
+  (vterm-mgt-reconcile-state)
+  (if (cycle-empty? vterm-mgt--instances)
+      (vterm-mgt-instantiate)
+    (if (cycle-focused? vterm-mgt--instances)
+        (switch-to-buffer (cycle-current vterm-mgt--instances))
+      (progn
+        (cycle-jump 0 vterm-mgt--instances)
+        (switch-to-buffer (cycle-current vterm-mgt--instances))))))
+
+(defun vterm-mgt-rename-buffer (name)
+  "Rename the current buffer ensuring that its NAME is wrapped in *vterm*<...>.
+This function should be called from a buffer running vterm."
+  (interactive "SRename vterm buffer: ")
+  (vterm-mgt--assert-vterm-buffer)
+  (rename-buffer (format "*vterm*<%s>" name)))
+
+(defun vterm-mgt-reconcile-state ()
+  "Fill `vterm-mgt--instances' with the existing vterm buffers.
+
+If for whatever reason, the state of `vterm-mgt--instances' is corrupted and
+  misaligns with the state of vterm buffers in Emacs, use this function to
+  restore the state."
+  (interactive)
+  (setq vterm-mgt--instances
+        (->> (buffer-list)
+             (-filter #'vterm-mgt--instance?)
+             cycle-from-list)))
+
+(defun vterm-mgt-select ()
+  "Select a vterm instance by name from the list in `vterm-mgt--instances'."
+  (interactive)
+  (vterm-mgt-reconcile-state)
+  (switch-to-buffer
+   (completing-read "Switch to vterm: "
+                    (->> vterm-mgt--instances
+                         cycle-to-list
+                         (-map #'buffer-name)))))
+
+(provide 'vterm-mgt)
+;;; vterm-mgt.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/window-manager.el b/users/wpcarro/emacs/.emacs.d/wpc/window-manager.el
new file mode 100644
index 0000000000..4c61138f94
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/window-manager.el
@@ -0,0 +1,228 @@
+;;; window-manager.el --- Functions augmenting my usage of EXWM -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; I switched to EXWM from i3, and I haven't looked back.  One day I may write a
+;; poem declaring my love for Emacs and EXWM.  For now, I haven't the time.
+
+;; Wist List:
+;; - TODO: Consider supporting MRU cache of worksapces.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'alert)
+(require 'cycle)
+(require 'dash)
+(require 'kbd)
+(require 's)
+(require 'exwm)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Variables
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup window-manager nil
+  "Customization options for `window-manager'.")
+
+(cl-defstruct window-manager-named-workspace
+  label kbd display)
+
+(defcustom window-manager-named-workspaces nil
+  "List of `window-manager-named-workspace' structs."
+  :group 'window-manager
+  :type (list 'window-manager-named-workspace))
+
+(defcustom window-manager-screenlocker "xsecurelock"
+  "Reference to a screen-locking executable."
+  :group 'window-manager
+  :type 'string)
+
+(defvar window-manager--workspaces nil
+  "Cycle of the my EXWM workspaces.")
+
+(defconst window-manager--modes
+  (cycle-from-list (list #'window-manager--char-mode
+                         #'window-manager--line-mode))
+  "Functions to switch exwm modes.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun window-manager--alert (x)
+  "Message X with a structured format."
+  (alert (s-concat "[exwm] " x)))
+
+(cl-defun window-manager-init (&key init-hook)
+  "Call `exwm-enable' alongside other bootstrapping functions."
+  (require 'exwm-config)
+  (require 'exwm-randr)
+  (setq exwm-randr-workspace-monitor-plist
+        (->> window-manager-named-workspaces
+             (-map-indexed (lambda (i x)
+                             (list i (window-manager-named-workspace-display x))))
+             -flatten))
+  (setq exwm-workspace-number (length window-manager-named-workspaces))
+  (setq exwm-input-simulation-keys
+        '(([?\C-b] . [left])
+          ([?\M-b] . [C-left])
+          ([?\C-f] . [right])
+          ([?\M-f] . [C-right])
+          ([?\C-p] . [up])
+          ([?\C-n] . [down])
+          ([?\C-a] . [home])
+          ([?\C-e] . [end])
+          ([?\C-d] . [delete])
+          ([?\C-c] . [C-c])))
+  ;; Install workspace KBDs
+  (progn
+    (->> window-manager-named-workspaces
+         (list-map #'window-manager--register-kbd))
+    (window-manager--alert "Registered workspace KBDs!"))
+  ;; Ensure exwm apps open in char-mode.
+  (add-hook 'exwm-manage-finish-hook #'window-manager--char-mode)
+  (add-hook 'exwm-init-hook init-hook)
+  (setq window-manager--workspaces
+        (cycle-from-list window-manager-named-workspaces))
+  (exwm-randr-enable)
+  (exwm-enable))
+
+(defun window-manager-next-workspace ()
+  "Cycle forwards to the next workspace."
+  (interactive)
+  (window-manager--change-workspace (cycle-next window-manager--workspaces)))
+
+(defun window-manager-prev-workspace ()
+  "Cycle backwards to the previous workspace."
+  (interactive)
+  (window-manager--change-workspace (cycle-prev window-manager--workspaces)))
+
+;; Here is the code required to toggle EXWM's modes.
+(defun window-manager--line-mode ()
+  "Switch exwm to line-mode."
+  (call-interactively #'exwm-input-grab-keyboard)
+  (window-manager--alert "Switched to line-mode"))
+
+(defun window-manager--char-mode ()
+  "Switch exwm to char-mode."
+  (call-interactively #'exwm-input-release-keyboard)
+  (window-manager--alert "Switched to char-mode"))
+
+(defun window-manager-toggle-mode ()
+  "Switch between line- and char- mode."
+  (interactive)
+  (with-current-buffer (window-buffer)
+    (when (eq major-mode 'exwm-mode)
+      (funcall (cycle-next window-manager--modes)))))
+
+(defun window-manager--label->index (label workspaces)
+  "Return the index of the workspace in WORKSPACES named LABEL."
+  (let ((index (-elem-index label (-map #'window-manager-named-workspace-label
+                                        workspaces))))
+    (if index index (error (format "No workspace found for label: %s" label)))))
+
+(defun window-manager--register-kbd (workspace)
+  "Registers a keybinding for WORKSPACE struct.
+Currently using super- as the prefix for switching workspaces."
+  (let ((handler (lambda ()
+                   (interactive)
+                   (window-manager--switch
+                    (window-manager-named-workspace-label workspace))))
+        (key (window-manager-named-workspace-kbd workspace)))
+    (exwm-input-set-key
+     (kbd-for 'workspace key)
+     handler)))
+
+(defun window-manager--change-workspace (workspace)
+  "Switch EXWM workspaces to the WORKSPACE struct."
+  (exwm-workspace-switch
+   (window-manager--label->index
+    (window-manager-named-workspace-label workspace)
+    window-manager-named-workspaces))
+  (window-manager--alert
+   (format "Switched to: %s"
+           (window-manager-named-workspace-label workspace))))
+
+(defun window-manager--switch (label)
+  "Switch to a named workspaces using LABEL."
+  (cycle-focus (lambda (x)
+                 (equal label
+                        (window-manager-named-workspace-label x)))
+               window-manager--workspaces)
+  (window-manager--change-workspace (cycle-current window-manager--workspaces)))
+
+(defun window-manager-toggle-previous ()
+  "Focus the previously active EXWM workspace."
+  (interactive)
+  (window-manager--change-workspace
+   (cycle-focus-previous! window-manager--workspaces)))
+
+(defun window-manager--exwm-buffer? (x)
+  "Return t if buffer X is an EXWM buffer."
+  (equal 'exwm-mode (buffer-local-value 'major-mode x)))
+
+(defun window-manager--application-name (buffer)
+  "Return the name of the application running in the EXWM BUFFER.
+This function asssumes that BUFFER passes the `window-manager--exwm-buffer?'
+predicate."
+  (with-current-buffer buffer exwm-class-name))
+
+;; TODO: Support disambiguating between two or more instances of the same
+;; application. For instance if two `exwm-class-name' values are
+;; "Google-chrome", find a encode this information in the `buffer-alist'.
+(defun window-manager-switch-to-exwm-buffer ()
+  "Use `completing-read' to focus an EXWM buffer."
+  (interactive)
+  (let* ((buffer-alist (->> (buffer-list)
+                            (-filter #'window-manager--exwm-buffer?)
+                            (-map
+                             (lambda (buffer)
+                               (cons (window-manager--application-name buffer)
+                                     buffer)))))
+         (label (completing-read "Switch to EXWM buffer: " buffer-alist)))
+    (exwm-workspace-switch-to-buffer
+     (al-get label buffer-alist))))
+
+(defun window-manager-current-workspace ()
+  "Output the label of the currently active workspace."
+  (->> window-manager--workspaces
+       cycle-current
+       window-manager-named-workspace-label))
+
+(defun window-manager-workspace-move ()
+  "Prompt the user to move the current workspace to another."
+  (interactive)
+  (exwm-workspace-move
+   exwm-workspace--current
+   (window-manager--label->index
+    (completing-read "Move current workspace to: "
+                     (->> window-manager-named-workspaces
+                          (-map #'window-manager-named-workspace-label))
+                     nil
+                     t)
+    window-manager-named-workspaces)))
+
+(defun window-manager-move-window ()
+  "Prompt the user to move the current window to another workspace."
+  (interactive)
+  (let ((window (get-buffer-window))
+        (dest (completing-read "Move current window to: "
+                               (->> window-manager-named-workspaces
+                                    (-map #'window-manager-named-workspace-label))
+                               nil
+                               t)))
+    (exwm-workspace-move-window
+     (exwm-workspace--workspace-from-frame-or-index
+      (window-manager--label->index dest window-manager-named-workspaces))
+     (exwm--buffer->id window))
+    (window-manager--switch dest)))
+
+(provide 'window-manager)
+;;; window-manager.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/window.el b/users/wpcarro/emacs/.emacs.d/wpc/window.el
new file mode 100644
index 0000000000..aec3c7012f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/window.el
@@ -0,0 +1,40 @@
+;;; window.el --- Working with windows -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Utilities to make CRUDing windows in Emacs easier.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'macros)
+(require 'maybe)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun window-find (name)
+  "Find a window by the NAME of the buffer it's hosting."
+  (let ((buffer (get-buffer name)))
+    (if (maybe-some? buffer)
+        (get-buffer-window buffer)
+      nil)))
+
+;; TODO: Find a way to incorporate these into function documentation.
+(macros-comment
+ (window-find "*scratch*"))
+
+(defun window-delete (window)
+  "Delete the WINDOW reference."
+  (delete-window window))
+
+(provide 'window)
+;;; window.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-clojure.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-clojure.el
new file mode 100644
index 0000000000..5582641b3f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-clojure.el
@@ -0,0 +1,71 @@
+;;; wpc-clojure.el --- My Clojure preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Hosting my Clojure tooling preferences
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package clojure-mode
+  :config
+  ;; from Ryan Schmukler:
+  (setq cljr-magic-require-namespaces
+        '(("io" . "clojure.java.io")
+          ("sh" . "clojure.java.shell")
+          ("jdbc" . "clojure.java.jdbc")
+          ("set" . "clojure.set")
+          ("time" . "java-time")
+          ("str" . "cuerdas.core")
+          ("path" . "pathetic.core")
+          ("walk" . "clojure.walk")
+          ("zip" . "clojure.zip")
+          ("async" . "clojure.core.async")
+          ("component" . "com.stuartsierra.component")
+          ("http" . "clj-http.client")
+          ("url" . "cemerick.url")
+          ("sql" . "honeysql.core")
+          ("csv" . "clojure.data.csv")
+          ("json" . "cheshire.core")
+          ("s" . "clojure.spec.alpha")
+          ("fs" . "me.raynes.fs")
+          ("ig" . "integrant.core")
+          ("cp" . "com.climate.claypoole")
+          ("re-frame" . "re-frame.core")
+          ("rf" . "re-frame.core")
+          ("re" . "reagent.core")
+          ("reagent" . "reagent.core")
+          ("u.core" . "utopia.core")
+          ("gen" . "clojure.spec.gen.alpha"))))
+
+(use-package cider
+  :config
+  (general-define-key
+    :keymaps 'cider-repl-mode-map
+    "C-l"    #'cider-repl-clear-buffer
+    "C-u"    #'kill-whole-line
+    "<up>"   #'cider-repl-previous-input
+    "<down>" #'cider-repl-next-input)
+  (general-define-key
+   :keymaps 'clojure-mode-map
+   :states '(normal)
+   :prefix "<SPC>"
+   "x" #'cider-eval-defun-at-point
+   "X" #'cider-eval-buffer
+   "d" #'cider-symbol-at-point)
+  (setq cider-prompt-for-symbol nil))
+
+(provide 'wpc-clojure)
+;;; wpc-clojure.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-company.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-company.el
new file mode 100644
index 0000000000..89fde4b6a1
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-company.el
@@ -0,0 +1,41 @@
+;;; wpc-company.el --- Autocompletion package, company, preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Hosts my company mode preferences
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; autocompletion client
+(use-package company
+  :config
+  (general-define-key
+    :keymaps 'company-active-map
+    "C-j" #'company-select-next
+    "C-n" #'company-select-next
+    "C-k" #'company-select-previous
+    "C-p" #'company-select-previous
+    "C-d" #'company-show-doc-buffer)
+  (setq company-tooltip-align-annotations t)
+  (setq company-idle-delay 0)
+  (setq company-show-numbers t)
+  (setq company-minimum-prefix-length 2)
+  (setq company-dabbrev-downcase nil
+        company-dabbrev-ignore-case t)
+  (global-company-mode))
+
+(provide 'wpc-company)
+;;; wpc-company.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-dired.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-dired.el
new file mode 100644
index 0000000000..6df7ba4106
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-dired.el
@@ -0,0 +1,52 @@
+;;; wpc-dired.el --- My dired preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; File management in Emacs, if learned and configured properly, should be
+;; capable to reduce my dependency on the terminal.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'macros)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(progn
+  (require 'dired)
+  (setq dired-recursive-copies 'always
+        dired-recursive-deletes 'top
+        dired-dwim-target t)
+  (setq dired-listing-switches "-la --group-directories-first")
+  (general-define-key
+   :keymaps 'dired-mode-map
+   :states '(normal)
+   ;; Overriding some KBDs defined in the evil-collection module.
+   "o" #'dired-find-file-other-window
+   "<SPC>" nil ;; This unblocks some of my leader-prefixed KBDs.
+   "s" nil ;; This unblocks my window-splitting KBDs.
+   "c" #'find-file
+   "f" #'project-find-file
+   "-" (lambda () (interactive) (find-alternate-file "..")))
+  (general-add-hook 'dired-mode-hook
+                    (list (macros-enable dired-hide-details-mode)
+                          #'auto-revert-mode)))
+
+(progn
+  (require 'locate)
+  (general-define-key
+   :keymaps 'locate-mode-map
+   :states 'normal
+   "o" #'dired-find-file-other-window))
+
+(provide 'wpc-dired)
+;;; wpc-dired.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-elixir.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-elixir.el
new file mode 100644
index 0000000000..69259274c8
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-elixir.el
@@ -0,0 +1,27 @@
+;;; wpc-elixir.el --- Elixir / Erland configuration -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; My preferences for working with Elixir / Erlang projects
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'macros)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package elixir-mode
+  :config
+  (macros-add-hook-before-save 'elixir-mode-hook #'elixir-format))
+
+(provide 'wpc-elixir)
+;;; wpc-elixir.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-flycheck.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-flycheck.el
new file mode 100644
index 0000000000..c32c5daeff
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-flycheck.el
@@ -0,0 +1,17 @@
+;;; wpc-flycheck.el --- My flycheck configuration -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Hosts my Flycheck preferences
+
+;;; Code:
+
+(use-package flycheck
+  :config
+  (global-flycheck-mode))
+
+(provide 'wpc-flycheck)
+;;; wpc-flycheck.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-golang.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-golang.el
new file mode 100644
index 0000000000..47198c8e02
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-golang.el
@@ -0,0 +1,42 @@
+;;; wpc-golang.el --- Tooling preferences for Go -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Tooling support for golang development.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'macros)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO: Support jumping to go source code for fmt.Println, etc.
+
+(use-package go-mode
+  :config
+  (setq gofmt-command "goimports")
+  ;; TODO: Consider configuring `xref-find-definitions' to use `godef-jump'
+  ;; instead of shadowing the KBD here.
+  (general-define-key
+   :states '(normal)
+   :keymaps '(go-mode-map)
+   "M-." #'godef-jump)
+  ;; Support calling M-x `compile'.
+  (add-hook 'go-mode-hook (lambda ()
+                            (setq-local tab-width 2)
+                            (setq-local compile-command "go build -v")))
+  (macros-add-hook-before-save 'go-mode-hook #'gofmt-before-save))
+
+(provide 'wpc-golang)
+;;; wpc-golang.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-haskell.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-haskell.el
new file mode 100644
index 0000000000..536790e36d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-haskell.el
@@ -0,0 +1,53 @@
+;;; wpc-haskell.el --- My Haskell preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Hosts my Haskell development preferences
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'macros)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; font-locking, glyph support, etc
+(use-package haskell-mode
+  :config
+  (macros-add-hook-before-save 'haskell-mode #'haskell-align-imports))
+
+;; Test toggling
+(defun wpc-haskell-module->test ()
+  "Jump from a module to a test."
+  (let ((filename (->> buffer-file-name
+                       (s-replace "/src/" "/test/")
+                       (s-replace ".hs" "Test.hs")
+                       find-file)))
+    (make-directory (f-dirname filename) t)
+    (find-file filename)))
+
+(defun wpc-haskell-test->module ()
+  "Jump from a test to a module."
+  (let ((filename (->> buffer-file-name
+                       (s-replace "/test/" "/src/")
+                       (s-replace "Test.hs" ".hs"))))
+    (make-directory (f-dirname filename) t)
+    (find-file filename)))
+
+(defun wpc-haskell-test<->module ()
+  "Toggle between test and module in Haskell."
+  (interactive)
+  (if (s-contains? "/src/" buffer-file-name)
+      (wpc-haskell-module->test)
+    (wpc-haskell-test->module)))
+
+(provide 'wpc-haskell)
+;;; wpc-haskell.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-javascript.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-javascript.el
new file mode 100644
index 0000000000..7c1816c561
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-javascript.el
@@ -0,0 +1,98 @@
+;;; wpc-javascript.el --- My Javascript preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; This module hosts my Javascript tooling preferences.  This also includes
+;; tooling for TypeScript and other frontend tooling.  Perhaps this module will
+;; change names to more accurately reflect that.
+;;
+;; Depends
+;; - yarn global add prettier
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Constants
+(defconst wpc-javascript--js-hooks
+  '(js-mode-hook
+    web-mode-hook
+    typescript-mode-hook
+    js2-mode-hook
+    rjsx-mode-hook)
+  "All of the commonly used hooks for Javascript buffers.")
+
+(defconst wpc-javascript--frontend-hooks
+  (-insert-at 0 'css-mode-hook wpc-javascript--js-hooks)
+  "All of the commonly user hooks for frontend development.")
+
+;; frontend indentation settings
+(setq typescript-indent-level 2
+      js-indent-level 2
+      css-indent-offset 2)
+
+;; Flow for Javascript
+(use-package add-node-modules-path
+  :config
+  (general-add-hook wpc-javascript--js-hooks #'add-node-modules-path))
+
+(use-package web-mode
+  :mode "\\.html\\'"
+  :config
+  (setq web-mode-css-indent-offset 2)
+  (setq web-mode-code-indent-offset 2)
+  (setq web-mode-markup-indent-offset 2))
+
+;; JSX highlighting
+(use-package rjsx-mode
+  :config
+  (general-unbind rjsx-mode-map "<" ">" "C-d")
+  (general-nmap
+    :keymaps 'rjsx-mode-map
+    "K" #'flow-minor-type-at-pos)
+  (setq js2-mode-show-parse-errors nil
+        js2-mode-show-strict-warnings nil))
+
+(progn
+  (defun wpc-javascript-tide-setup ()
+    (interactive)
+    (tide-setup)
+    (flycheck-mode 1)
+    (setq flycheck-check-syntax-automatically '(save mode-enabled))
+    (eldoc-mode 1)
+    (tide-hl-identifier-mode 1)
+    (company-mode 1))
+  (use-package tide
+    :config
+    (add-hook 'typescript-mode-hook #'wpc-javascript-tide-setup))
+  (require 'web-mode)
+  (add-to-list 'auto-mode-alist '("\\.tsx\\'" . web-mode))
+  (add-hook 'web-mode-hook
+            (lambda ()
+              (when (string-equal "tsx" (f-ext buffer-file-name))
+                (wpc-javascript-tide-setup))))
+  (flycheck-add-mode 'typescript-tslint 'web-mode))
+
+;; JS autoformatting
+(use-package prettier-js
+  :config
+  (general-add-hook wpc-javascript--frontend-hooks #'prettier-js-mode))
+
+;; Support Elm
+(use-package elm-mode
+  :config
+  (add-hook 'elm-mode-hook #'elm-format-on-save-mode))
+
+(provide 'wpc-javascript)
+;;; wpc-javascript.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-language-support.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-language-support.el
new file mode 100644
index 0000000000..1fe3509559
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-language-support.el
@@ -0,0 +1,36 @@
+;;; wpc-language-support.el --- Support for miscellaneous programming languages -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; I defined this module to declutter my init.el.
+;;
+;; When a particular programming-language's configuration gets too complicated,
+;; I break it out into a dedicated module. Everything else gets dumped in
+;; "Miscellaneous Configuration".
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dedicated Modules
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'wpc-lisp)
+(require 'wpc-haskell)
+(require 'wpc-elixir)
+(require 'wpc-nix)
+(require 'wpc-rust)
+(require 'wpc-clojure)
+(require 'wpc-prolog)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Miscellaneous Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package csharp-mode)
+(use-package terraform-mode)
+
+(provide 'wpc-language-support)
+;;; wpc-language-support.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-lisp.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-lisp.el
new file mode 100644
index 0000000000..599d426204
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-lisp.el
@@ -0,0 +1,123 @@
+;;; wpc-lisp.el --- Generic LISP preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; parent (up)
+;; child (down)
+;; prev-sibling (left)
+;; next-sibling (right)
+
+;;; Code:
+
+;; TODO: Consider having a separate module for each LISP dialect.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst wpc-lisp--hooks
+  '(lisp-mode-hook
+    emacs-lisp-mode-hook
+    clojure-mode-hook
+    clojurescript-mode-hook
+    racket-mode-hook)
+  "List of LISP modes.")
+
+(use-package sly
+  :config
+  (setq inferior-lisp-program "sbcl")
+  (general-define-key
+   :keymaps 'sly-mode-map
+   :states '(normal)
+   :prefix "<SPC>"
+   "x" #'sly-eval-defun
+   "X" #'sly-eval-buffer
+   "d" #'sly-describe-symbol))
+
+(use-package rainbow-delimiters
+  :config
+  (general-add-hook wpc-lisp--hooks #'rainbow-delimiters-mode))
+
+(use-package racket-mode
+  :config
+  (general-define-key
+   :keymaps 'racket-mode-map
+   :states 'normal
+   :prefix "<SPC>"
+   "x" #'racket-send-definition
+   "X" #'racket-run
+   "d" #'racket-describe)
+  (setq racket-program "~/.nix-profile/bin/racket"))
+
+(use-package lispyville
+  :init
+  (defconst wpc-lisp--lispyville-key-themes
+    '(c-w
+      operators
+      text-objects
+      prettify
+      commentary
+      slurp/barf-cp
+      wrap
+      additional
+      additional-insert
+      additional-wrap
+      escape)
+    "All available key-themes in Lispyville.")
+  :config
+  (general-add-hook wpc-lisp--hooks #'lispyville-mode)
+  (lispyville-set-key-theme wpc-lisp--lispyville-key-themes)
+  (progn
+    (general-define-key
+     :keymaps 'lispyville-mode-map
+     :states 'motion
+     ;; first unbind
+     "M-h" nil
+     "M-l" nil)
+    (general-define-key
+     :keymaps 'lispyville-mode-map
+     :states 'normal
+     ;; first unbind
+     "M-j" nil
+     "M-k" nil
+     ;; second rebind
+     "C-s-h" #'lispyville-drag-backward
+     "C-s-l" #'lispyville-drag-forward
+     "C-s-e" #'lispyville-end-of-defun
+     "C-s-a" #'lispyville-beginning-of-defun)))
+
+;; Elisp
+(use-package elisp-slime-nav
+  :config
+  (general-add-hook 'emacs-lisp-mode #'ielm-mode))
+
+(defun wpc-lisp-copy-elisp-eval-output ()
+  "Copy the output of the elisp evaluation"
+  (interactive)
+  (call-interactively 'eval-last-sexp)
+  (clipboard-copy (current-message)
+                  :message (format "%s - copied!" (current-message))))
+
+(general-define-key
+ :keymaps 'emacs-lisp-mode-map
+ :prefix "<SPC>"
+ :states 'normal
+ "c" #'wpc-lisp-copy-elisp-eval-output
+ "x" #'eval-defun
+ "X" #'eval-buffer
+ "d" (lambda ()
+       (interactive)
+       (with-current-buffer (current-buffer)
+         (helpful-function (symbol-at-point)))))
+
+(provide 'wpc-lisp)
+;;; wpc-lisp.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-misc.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-misc.el
new file mode 100644
index 0000000000..f075776bfc
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-misc.el
@@ -0,0 +1,340 @@
+;;; wpc-misc.el --- Hosting miscellaneous configuration -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; This is the home of any configuration that couldn't find a better home.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'project)
+(require 'f)
+(require 'dash)
+(require 'tvl)
+(require 'region)
+(require 'general)
+(require 'constants)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(setq display-time-string-forms
+      '((format-time-string "%H:%M %a %b %d")))
+(display-time-mode 1)
+
+;; Remove the boilerplate in the *scratch* buffer
+(setq initial-scratch-message "")
+
+;; disable custom variable entries from being written to ~/.emacs.d/init.el
+(setq custom-file (f-join user-emacs-directory "custom.el"))
+(load custom-file 'noerror)
+
+;; integrate Emacs with X11 clipboard
+(customize-set-variable 'select-enable-primary t)
+(customize-set-variable 'select-enable-clipboard t)
+(customize-set-variable 'evil-visual-update-x-selection-p nil)
+(general-def 'insert
+  "s-v" #'clipboard-yank
+  "C-S-v" #'clipboard-yank)
+
+;; transparently edit compressed files
+(auto-compression-mode t)
+
+;; autowrap when over the fill-column
+(setq-default auto-fill-function #'do-auto-fill)
+
+;; link to Emacs source code
+;; TODO: Update this link.
+(setq find-function-C-source-directory
+      "~/Dropbox/programming/emacs/src")
+
+;; change emacs prompts from "yes or no" -> "y or n"
+(fset 'yes-or-no-p 'y-or-n-p)
+
+;; open photos in Emacs
+(auto-image-file-mode 1)
+
+;; disable line-wrapping
+(setq-default truncate-lines 1)
+
+;; shell file indentation
+(setq sh-basic-offset 2)
+(setq sh-indentation 2)
+
+;; Emacs library that interfaces with my Linux password manager.
+(use-package password-store)
+
+(use-package vterm
+  :config
+  (general-define-key
+   :keymaps '(vterm-mode-map)
+   :states '(insert)
+   "C-S-v" #'vterm-yank)
+  (general-define-key
+   :keymaps '(vterm-mode-map)
+   :states '(normal)
+   "K" #'evil-scroll-line-up
+   "J" #'evil-scroll-line-down
+   "C-b" #'evil-scroll-page-up
+   "C-f" #'evil-scroll-page-down))
+
+;; Use en Emacs buffer as a REST client.
+;; For more information: http://emacsrocks.com/e15.html
+(use-package restclient)
+
+;; Run `package-lint' before publishing to MELPA.
+(use-package package-lint)
+
+;; Parser combinators in Elisp.
+(use-package parsec)
+
+;; disable company mode when editing markdown
+;; TODO: move this out of wpc-misc.el and into a later file to call
+;; `(disable company-mode)'
+(use-package markdown-mode
+  :config
+  ;; TODO: Add assertion that pandoc is installed and it is accessible from
+  ;; Emacs.
+  (setq markdown-command "pandoc")
+  (setq markdown-split-window-direction 'right)
+  ;; (add-hook 'markdown-mode-hook #'markdown-live-preview-mode)
+  ;; Use mode-specific syntax highlighting for code blocks.
+  (setq markdown-fontify-code-blocks-natively t)
+  ;; Prevent Emacs from adding a space after the leading 3x-backticks.
+  (setq markdown-spaces-after-code-fence 0))
+
+(use-package alert)
+
+(use-package refine)
+
+;; Required by some google-emacs package commands.
+(use-package deferred)
+
+;; git integration
+(use-package magit
+  :config
+  (add-hook 'git-commit-setup-hook
+            (lambda ()
+              (company-mode -1)
+              (flyspell-mode 1)))
+  (setq magit-display-buffer-function
+        #'magit-display-buffer-same-window-except-diff-v1))
+
+(use-package magit-popup)
+
+;; http
+(use-package request)
+
+;; TVL depot stuff
+(use-package tvl)
+
+;; perl-compatible regular expressions
+(use-package pcre2el)
+
+;; alternative to help
+(use-package helpful)
+
+;; If called from an existing helpful-mode buffer, reuse that buffer; otherwise,
+;; call `pop-to-buffer'.
+(setq helpful-switch-buffer-function
+      (lambda (buffer-or-name)
+        (if (eq major-mode 'helpful-mode)
+            (switch-to-buffer buffer-or-name)
+          (pop-to-buffer buffer-or-name))))
+
+;; Emacs integration with direnv
+(use-package direnv
+  :config
+  (direnv-mode))
+
+;; Superior Elisp library for working with dates and times.
+;; TODO: Put this where my other installations for dash.el, s.el, a.el, and
+;; other utility Elisp libraries are located.
+(use-package ts)
+
+;; persist history etc b/w Emacs sessions
+(setq desktop-save 'if-exists)
+(desktop-save-mode 1)
+(setq desktop-globals-to-save
+      (append '((extended-command-history . 30)
+                (file-name-history        . 100)
+                (grep-history             . 30)
+                (compile-history          . 30)
+                (minibuffer-history       . 50)
+                (query-replace-history    . 60)
+                (read-expression-history  . 60)
+                (regexp-history           . 60)
+                (regexp-search-ring       . 20)
+                (search-ring              . 20)
+                (shell-command-history    . 50)
+                tags-file-name
+                register-alist)))
+
+;; configure ibuffer
+(setq ibuffer-default-sorting-mode 'major-mode)
+
+;; config Emacs to use $PATH values
+(use-package exec-path-from-shell
+  :if (memq window-system '(mac ns))
+  :config
+  (exec-path-from-shell-initialize))
+
+;; Emacs autosave, backup, interlocking files
+(setq auto-save-default nil
+      make-backup-files nil
+      create-lockfiles nil)
+
+;; ensure code wraps at 80 characters by default
+(setq-default fill-column 80)
+
+;; render tabs 2x-chars wide
+(setq tab-width 2)
+
+(put 'narrow-to-region 'disabled nil)
+
+;; trim whitespace on save
+(add-hook 'before-save-hook #'delete-trailing-whitespace)
+
+;; call `git secret hide` after saving secrets.json
+(add-hook 'after-save-hook
+          (lambda ()
+            (when (f-equal? (buffer-file-name)
+                            (f-join tvl-depot-path
+                                    "users"
+                                    "wpcarro"
+                                    "secrets.json"))
+              (shell-command "git secret hide"))))
+
+;; use tabs instead of spaces
+(setq-default indent-tabs-mode nil)
+
+;; prefer shorter tab-widths (e.g. writing Go code)
+(setq-default tab-width 2)
+
+;; automatically follow symlinks
+(setq vc-follow-symlinks t)
+
+;; fullscreen settings
+(setq ns-use-native-fullscreen nil)
+
+(use-package yasnippet
+  :config
+  (unless constants-ci?
+    (setq yas-snippet-dirs (list (f-join user-emacs-directory "snippets")))
+    (yas-global-mode 1)))
+
+(use-package projectile
+  :config
+  (projectile-mode t))
+
+;; TODO(wpcarro): Consider replacing this with a TVL version if it exists.
+(defun wpc-misc--depot-find (dir)
+  "Find the default.nix nearest to DIR."
+  ;; I use 'vc only at the root of my monorepo because 'transient doesn't use my
+  ;; .gitignore, which slows things down. Ideally, I could write a version that
+  ;; behaves like 'transient but also respects my monorepo's .gitignore and any
+  ;; ancestor .gitignore files.
+  (if (f-equal? tvl-depot-path dir)
+      (cons 'vc dir)
+    (when (f-ancestor-of? tvl-depot-path dir)
+      (if (f-exists? (f-join dir "default.nix"))
+          (cons 'transient dir)
+        (wpc-misc--depot-find (f-parent dir))))))
+
+(add-to-list 'project-find-functions #'wpc-misc--depot-find)
+
+(defun wpc-misc-pkill (name)
+  "Call the pkill executable using NAME as its argument."
+  (interactive "sProcess name: ")
+  (call-process "pkill" nil nil nil name))
+
+(use-package deadgrep
+  :config
+  (general-define-key
+   :keymaps 'deadgrep-mode-map
+   :states 'normal
+   "o" #'deadgrep-visit-result-other-window)
+  (setq-default deadgrep--context '(0 . 3))
+  (defun wpc-misc-deadgrep-region ()
+    "Run a ripgrep search on the active region."
+    (interactive)
+    (deadgrep (region-to-string)))
+  (defun wpc-misc-deadgrep-dwim ()
+    "If a region is active, use that as the search, otherwise don't."
+    (interactive)
+    (with-current-buffer (current-buffer)
+      (if (region-active-p)
+          (setq deadgrep--additional-flags '("--multiline"))
+          (wpc-misc-deadgrep-region)
+        (call-interactively #'deadgrep))))
+  (advice-add 'deadgrep--arguments
+              :filter-return
+              (lambda (args)
+                (push "--hidden" args)
+                (push "--follow" args))))
+
+;; TODO: Do I need this when I have swiper?
+(use-package counsel)
+
+(use-package counsel-projectile)
+
+;; search Google, Stackoverflow from within Emacs
+(use-package engine-mode
+  :config
+  (defengine google
+    "http://www.google.com/search?ie=utf-8&oe=utf-8&q=%s"
+    :keybinding "g")
+  (defengine stack-overflow
+    "https://stackoverflow.com/search?q=%s"
+    :keybinding "s"))
+
+;; EGlot (another LSP client)
+(use-package eglot)
+
+;; Microsoft's Debug Adapter Protocol (DAP)
+(use-package dap-mode
+  :after lsp-mode
+  :config
+  (dap-mode 1)
+  (dap-ui-mode 1))
+
+;; Microsoft's Language Server Protocol (LSP)
+(use-package lsp-ui
+  :config
+  (add-hook 'lsp-mode-hook #'lsp-ui-mode))
+
+;; Wilfred/suggest.el - Tool for discovering functions basesd on declaring your
+;; desired inputs and outputs.
+(use-package suggest)
+
+;; Malabarba/paradox - Enhances the `list-packages' view.
+(use-package paradox
+  :config
+  (paradox-enable))
+
+;; render emojis in Emacs 🕺
+(use-package emojify
+  :config
+  (add-hook 'after-init-hook #'global-emojify-mode)
+  ;; Disable the default styles of:
+  ;; - ascii  :P (When this is enabled, the vim command, :x, renders as 😶)
+  ;; - github :smile:
+  (setq emojify-emoji-styles '(unicode))
+  (defun wpc-misc-copy-emoji ()
+    "Select an emoji from the completing-read menu."
+    (interactive)
+    (clipboard-copy (emojify-completing-read "Copy: "))))
+
+;; Start the Emacs server
+(when (not (server-running-p))
+  (server-start))
+
+(provide 'wpc-misc)
+;;; wpc-misc.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-nix.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-nix.el
new file mode 100644
index 0000000000..e9dc203691
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-nix.el
@@ -0,0 +1,37 @@
+;;; wpc-nix.el --- Nix support -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "25.1"))
+
+;;; Commentary:
+;; Configuration to support working with Nix.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'tvl)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package nix-mode
+  :mode "\\.nix\\'")
+
+(defun wpc-nix-rebuild-emacs ()
+  "Use nix-env to rebuild wpcarros-emacs."
+  (interactive)
+  (let* ((pname (format "nix-env -iA users.wpcarro.emacs.nixos"))
+         (bname (format "*%s*" pname)))
+    (start-process pname bname
+                   "nix-env"
+                   "-f" tvl-depot-path
+                   "-iA" "users.wpcarro.emacs.nixos")
+    (display-buffer bname)))
+
+(provide 'wpc-nix)
+;;; wpc-nix.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-org.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-org.el
new file mode 100644
index 0000000000..229177220b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-org.el
@@ -0,0 +1,39 @@
+;;; wpc-org.el --- My org preferences -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.1"))
+
+;;; Commentary:
+;; Hosts my org mode preferences
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'f)
+(require 'macros)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package org
+  :config
+  (evil-set-initial-state 'org-mode 'normal)
+  (general-add-hook 'org-mode-hook
+                    (list (macros-disable linum-mode)
+                          (macros-disable company-mode)))
+  (setq org-startup-folded nil)
+  (setq org-todo-keywords '((sequence "TODO" "BLOCKED" "DONE")))
+  (general-unbind 'normal org-mode-map "M-h" "M-j" "M-k" "M-l"))
+
+(use-package org-bullets
+  :config
+  (general-add-hook 'org-mode-hook (macros-enable org-bullets-mode)))
+
+(provide 'wpc-org)
+;;; wpc-org.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-package.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-package.el
new file mode 100644
index 0000000000..9c57bb4270
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-package.el
@@ -0,0 +1,32 @@
+;;; wpc-package.el --- My package configuration -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.1"))
+
+;;; Commentary:
+;; This module hosts all of the settings required to work with ELPA,
+;; MELPA, QUELPA, and co.
+
+;;; Code:
+
+(require 'package)
+
+;; Even though we're packaging our Emacs with Nix, having MELPA registered is
+;; helpful to ad-hoc test out packages before declaratively adding them to
+;; emacs/default.nix.
+(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/"))
+(package-initialize)
+
+(unless (package-installed-p 'use-package)
+  ;; TODO: Consider removing this to improve initialization speed.
+  (package-refresh-contents)
+  (package-install 'use-package))
+(eval-when-compile
+  (require 'use-package))
+;; TODO: Consider removing this, since I'm requiring general.el in individual
+;; modules.
+(use-package general)
+
+(provide 'wpc-package)
+;;; wpc-package.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-prolog.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-prolog.el
new file mode 100644
index 0000000000..6779431c12
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-prolog.el
@@ -0,0 +1,19 @@
+;;; wpc-prolog.el --- For Prologging things -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Code configuring my Prolog work.
+
+;;; Code:
+
+(require 'macros)
+
+;; TODO: Notice that the .pl extension conflicts with Perl files. This may
+;; become a problem should I start working with Perl.
+(macros-support-file-extension "pl" prolog-mode)
+
+(provide 'wpc-prolog)
+;;; wpc-prolog.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-python.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-python.el
new file mode 100644
index 0000000000..9ffb7c4c8a
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-python.el
@@ -0,0 +1,24 @@
+;;; wpc-python.el --- Python configuration -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; My Python configuration settings
+;;
+;; Depends
+;; - `apti yapf`
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package py-yapf
+  :config
+  (add-hook 'python-mode-hook #'py-yapf-enable-on-save))
+
+(provide 'wpc-python)
+;;; wpc-python.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-rust.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-rust.el
new file mode 100644
index 0000000000..dfee2fc86b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-rust.el
@@ -0,0 +1,47 @@
+;;; wpc-rust.el --- Support Rust language -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Supports my Rust work.
+;;
+;; Dependencies:
+;; - `rustup`
+;; - `rustup component add rust-src`
+;; - `rustup toolchain add nightly && cargo +nightly install racer`
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'macros)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package racer
+  :config
+  (setq rust-sysroot (->> "~/.cargo/bin/rustc --print sysroot"
+                          shell-command-to-string
+                          s-trim-right))
+  (setq racer-rust-src-path (f-join rust-sysroot "lib/rustlib/src/rust/src"))
+  (add-hook 'racer-mode-hook #'eldoc-mode))
+
+(use-package rust-mode
+  :config
+  (add-hook 'rust-mode-hook #'racer-mode)
+  (macros-add-hook-before-save 'rust-mode-hook #'rust-format-buffer)
+  (define-key rust-mode-map
+    (kbd "TAB")
+    #'company-indent-or-complete-common)
+  (define-key rust-mode-map
+    (kbd "M-d")
+    #'racer-describe))
+
+(provide 'wpc-rust)
+;;; wpc-rust.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-shell.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-shell.el
new file mode 100644
index 0000000000..f4229ed328
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-shell.el
@@ -0,0 +1,31 @@
+;;; wpc-shell.el --- POSIX Shell scripting support -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Helpers for my shell scripting.  Includes bash, zsh, etc.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'zle)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Code
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package flymake-shellcheck
+  :commands flymake-shellcheck-load
+  :init
+  (add-hook 'sh-mode-hook #'flymake-shellcheck-load)
+  (add-hook 'sh-mode-hook #'zle-minor-mode))
+
+(use-package fish-mode)
+
+(provide 'wpc-shell)
+;;; wpc-shell.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/wpc-ui.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-ui.el
new file mode 100644
index 0000000000..f4ed1dd9ad
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-ui.el
@@ -0,0 +1,182 @@
+;;; wpc-ui.el --- Any related to the UI/UX goes here -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Hosts font settings, scrolling, color schemes.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'constants)
+(require 'tvl)
+(require 'prelude)
+(require 'al)
+(require 'fonts)
+(require 'colorscheme)
+(require 'device)
+(require 'laptop-battery)
+(require 'modeline)
+(require 'general)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; line height
+(setq-default line-spacing 0)
+
+(when window-system
+  (setq frame-title-format '(buffer-file-name "%f" ("%b"))))
+
+;; Ensure that buffers update when their contents change on disk.
+(global-auto-revert-mode t)
+
+;; smooth scrolling settings
+(setq scroll-step 1
+      scroll-conservatively 10000)
+
+;; clean up modeline
+(use-package diminish
+  :config
+  (diminish 'emacs-lisp-mode "elisp")
+  (diminish 'evil-commentary-mode)
+  (diminish 'flycheck-mode)
+  (diminish 'auto-revert-mode)
+  (diminish 'which-key-mode)
+  (diminish 'yas-minor-mode)
+  (diminish 'lispyville-mode)
+  (diminish 'undo-tree-mode)
+  (diminish 'company-mode)
+  (diminish 'projectile-mode)
+  (diminish 'eldoc-mode)
+  ;; This is how to diminish `auto-fill-mode'.
+  (diminish 'auto-fill-function)
+  (diminish 'counsel-mode)
+  (diminish 'ivy-mode))
+
+;; TODO: Further customize `mode-line-format' variable.
+(delete 'mode-line-modes mode-line-format)
+(delete '(vc-mode vc-mode) mode-line-format)
+
+;; disable startup screen
+(setq inhibit-startup-screen t)
+
+;; disable toolbar
+(tool-bar-mode -1)
+
+;; set default buffer for Emacs
+(setq initial-buffer-choice tvl-depot-path)
+
+;; premium Emacs themes
+(use-package doom-themes
+  :config
+  (setq doom-themes-enable-bold t
+        doom-themes-enable-italic t)
+  (doom-themes-visual-bell-config)
+  (doom-themes-org-config))
+
+;; kbd discovery
+(use-package which-key
+  :config
+  (setq which-key-idle-delay 0.25)
+  (which-key-mode))
+
+;; completion framework
+(use-package ivy
+  :config
+  (counsel-mode t)
+  (ivy-mode t)
+  ;; Remove preceding "^" from ivy prompts
+  (setq ivy-initial-inputs-alist nil)
+  ;; prefer using `helpful' variants
+  (progn
+    (setq counsel-describe-function-function #'helpful-callable)
+    (setq counsel-describe-variable-function #'helpful-variable))
+  (general-define-key
+   :keymaps '(ivy-minibuffer-map ivy-switch-buffer-map)
+   ;; prev
+   "C-k" #'ivy-previous-line
+   "<backtab>" #'ivy-previous-line
+   ;; next
+   "C-j" #'ivy-next-line
+   "<tab>" #'ivy-next-line))
+
+(use-package ivy-prescient
+  :config
+  (ivy-prescient-mode 1)
+  (unless constants-ci?
+    (prescient-persist-mode 1)))
+
+(use-package ivy-pass)
+
+;; all-the-icons
+(use-package all-the-icons
+  :config
+  (unless (or constants-ci?
+              (f-exists? "~/.local/share/fonts/all-the-icons.ttf"))
+    (all-the-icons-install-fonts t)))
+
+;; icons for Ivy
+(use-package all-the-icons-ivy
+  :after (ivy all-the-icons)
+  :config
+  (all-the-icons-ivy-setup))
+
+;; disable menubar
+(menu-bar-mode -1)
+
+;; reduce noisiness of auto-revert-mode
+(setq auto-revert-verbose nil)
+
+;; highlight lines that are over 80 characters long
+(use-package whitespace
+  :config
+  ;; TODO: This should change depending on the language and project. For
+  ;; example, Google Java projects prefer 100 character width instead of 80
+  ;; character width.
+  (setq whitespace-line-column 80)
+  (setq whitespace-style '(face lines-tail))
+  (add-hook 'prog-mode-hook #'whitespace-mode))
+
+;; dirname/filename instead of filename<dirname>
+(setq uniquify-buffer-name-style 'forward)
+
+;; highlight matching parens, brackets, etc
+(show-paren-mode 1)
+
+;; hide the scroll-bars in the GUI
+(scroll-bar-mode -1)
+
+;; TODO: Learn how to properly integrate this with dunst or another system-level
+;; notification program.
+;; GUI alerts in emacs
+(use-package alert
+  :commands (alert)
+  :config
+  (setq alert-default-style 'notifier))
+
+;; TODO: Should `device-laptop?' be a function or a constant that gets set
+;; during initialization?
+(when (device-laptop?) (laptop-battery-display))
+
+(colorscheme-whitelist-set 'doom-peacock)
+
+(when window-system
+  (let ((font "Monospace"))
+    (fonts-whitelist-set font)
+    ;; Some themes (e.g. doom-acario-*) change the font for comments. This
+    ;; should prevent that.
+    (set-face-attribute font-lock-comment-face nil
+                        :family font
+                        :slant 'normal)))
+
+(modeline-setup)
+
+(provide 'wpc-ui)
+;;; wpc-ui.el ends here
diff --git a/users/wpcarro/emacs/.emacs.d/wpc/zle.el b/users/wpcarro/emacs/.emacs.d/wpc/zle.el
new file mode 100644
index 0000000000..d4aa88258f
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/zle.el
@@ -0,0 +1,91 @@
+;;; zle.el --- Functions to mimmick my ZLE KBDs -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; This is primarily for personal use.  The keybindings that I choose are those
+;; that feel slightly mnemonic while also not shadowing important bindings.
+;; It's quite possible that our tastes will differ here.
+;;
+;; All of these keybindings are intended to shave off milliseconds off your
+;; typing.  I don't expect these numbers to sum up to a meaningful amount.  The
+;; primary reason that I wrote this, is that it introduces a small amount of
+;; structural editing to my workflow.  I've been using these exact keybindings
+;; on the command line, and I find them subtely delightful to use.  So much so
+;; that I decided to bring them to my Emacs configuration.
+;;
+;; ZLE is the Z-shell line editor.  I have some KBDs and functions that I often
+;; want in Emacs.
+;;
+;; Usage:
+;; Consider running `(zle-minor-mode)' to run this globally.  Depending on your
+;; configuration, it could be non-disruptive, disruptive, or extremely
+;; disruptive.
+
+;;; Code:
+
+;; subshell (C-j)
+(defun zle-subshell ()
+  "Insert the characters necessary to create a subshell."
+  (interactive)
+  (insert-char ?$)
+  (insert-char ?\()
+  (save-excursion
+    (insert-char ?\))))
+
+;; variable (C-v)
+(defun zle-variable ()
+  "Insert the characters to reference a variable."
+  (interactive)
+  (insert-char ?$)
+  (insert-char ?{)
+  (save-excursion
+    (insert-char ?})))
+
+;; 2x dash (C-M--)
+(defun zle-dash-dash ()
+  "Insert the characters for flags with 2x dashes."
+  (interactive)
+  (insert-char ? )
+  (insert-char ?-)
+  (insert-char ?-))
+
+;; 1x quotes (M-')
+(defun zle-single-quote ()
+  "Insert the characters to quickly create single quotes."
+  (interactive)
+  (insert-char ? )
+  (insert-char ?')
+  (save-excursion
+    (insert-char ?')))
+
+;; 2x quotes (M-")
+(defun zle-double-quote ()
+  "Insert the characters to quickly create double quotes."
+  (interactive)
+  (insert-char ? )
+  (insert-char ?\")
+  (save-excursion
+    (insert-char ?\")))
+
+(defvar zle-kbds
+  (let ((map (make-sparse-keymap)))
+    (bind-keys :map map
+               ("C-j"   . zle-subshell)
+               ("C-v"   . zle-variable)
+               ("C-M--" . zle-dash-dash)
+               ("M-'"   . zle-single-quote)
+               ("M-\""  . zle-double-quote))
+    map)
+  "Keybindings shaving milliseconds off of typing.")
+
+(define-minor-mode zle-minor-mode
+  "A minor mode mirroring my ZLE keybindings."
+  :init-value nil
+  :lighter " zle"
+  :keymap zle-kbds)
+
+(provide 'zle)
+;;; zle.el ends here
diff --git a/users/wpcarro/emacs/README.md b/users/wpcarro/emacs/README.md
new file mode 100644
index 0000000000..16f4fc31f9
--- /dev/null
+++ b/users/wpcarro/emacs/README.md
@@ -0,0 +1,15 @@
+# Emacs
+
+Emacs is one of a handful software projects that I highly value. I consider it
+as central to my workflow as `git` and `nix`.
+
+## Installing
+
+If you already have `depot` on your local file system, run the following from
+the top-level `depot` directory:
+
+```shell
+$ nix-env -iA users.wpcarro.emacs.nixos
+```
+
+Test edit (from depot).
diff --git a/users/wpcarro/emacs/ci.el b/users/wpcarro/emacs/ci.el
new file mode 100644
index 0000000000..9dfaf3056f
--- /dev/null
+++ b/users/wpcarro/emacs/ci.el
@@ -0,0 +1,44 @@
+;; This script initializes Emacs and exits with either a zero or non-zero status
+;; depending on whether or not Emacs initialized without logging warnings or
+;; encountering errors.
+;;
+;; This script reads the location of init.el as the last argument in `argv'.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'prelude)
+(require 'f)
+(require 'dash)
+(require 'buffer)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Script
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar init-el-path (-last-item argv)
+  "Path to the init.el file that this script attempts to load.")
+
+(prelude-assert (f-exists? init-el-path))
+
+(condition-case err
+    (load init-el-path)
+  (error
+   (message "Encountered an error while attempting to load init.el: %s" err)
+   (kill-emacs 1)))
+
+(when (buffer-exists? "*Errors*")
+  (progn
+    (with-current-buffer "*Errors*"
+      (message "Encountered errors in *Errors* buffer: %s" (buffer-string)))
+    (kill-emacs 1)))
+
+(when (buffer-exists? "*Warnings*")
+  (progn
+    (with-current-buffer "*Warnings*"
+      (message "Encountered warnings in *Warnings* buffer: %s" (buffer-string)))
+    (kill-emacs 1)))
+
+(message "Successfully initialized Emacs without errors or warnings!")
+(kill-emacs 0)
diff --git a/users/wpcarro/emacs/default.nix b/users/wpcarro/emacs/default.nix
new file mode 100644
index 0000000000..c6e6e913a0
--- /dev/null
+++ b/users/wpcarro/emacs/default.nix
@@ -0,0 +1,179 @@
+{ depot, pkgs, lib, ... }:
+
+# TODO(wpcarro): See if it's possible to expose emacsclient on PATH, so that I
+# don't need to depend on wpcarros-emacs and emacs in my NixOS configurations.
+let
+  inherit (depot.third_party.nixpkgs) emacsPackagesFor emacs28;
+  inherit (depot.users) wpcarro;
+  inherit (lib) mapAttrsToList;
+  inherit (lib.strings) concatStringsSep makeBinPath;
+  inherit (pkgs) runCommand writeShellScriptBin;
+
+  emacsBinPath = makeBinPath (
+    wpcarro.common.shell-utils ++
+    (with pkgs; [
+      clipmenu
+      ispell
+      nix
+      pass
+      scrot
+      xorg.xset
+    ])
+  );
+
+  emacsWithPackages = (emacsPackagesFor emacs28).emacsWithPackages;
+
+  wpcarrosEmacs = emacsWithPackages (epkgs:
+    (with epkgs.tvlPackages; [
+      tvl
+    ]) ++
+
+    (with epkgs.elpaPackages; [
+      exwm
+    ]) ++
+
+    (with epkgs.melpaPackages; [
+      avy
+      org-bullets
+      sly
+      notmuch
+      elm-mode
+      ts
+      vterm
+      base16-theme
+      password-store
+      # TODO(wpcarro): Prefer an Emacs client for clipmenud.
+      clipmon
+      csharp-mode
+      dockerfile-mode
+      evil
+      evil-collection
+      evil-commentary
+      evil-surround
+      key-chord
+      # TODO(wpcarro): Assess whether or not I need this with Nix.
+      add-node-modules-path
+      web-mode
+      rjsx-mode
+      tide
+      prettier-js
+      flycheck
+      diminish
+      doom-themes
+      telephone-line
+      which-key
+      all-the-icons
+      all-the-icons-ivy
+      ivy
+      ivy-clipmenu
+      ivy-pass
+      ivy-prescient
+      restclient
+      package-lint
+      parsec
+      magit-popup
+      direnv
+      alert
+      nix-mode
+      racer
+      rust-mode
+      rainbow-delimiters
+      racket-mode
+      lispyville
+      elisp-slime-nav
+      py-yapf
+      reason-mode
+      terraform-mode
+      elixir-mode
+      go-mode
+      company
+      markdown-mode
+      refine
+      deferred
+      magit
+      request
+      pcre2el
+      helpful
+      # TODO(wpcarro): Determine if Nix solves this problem.
+      exec-path-from-shell
+      yasnippet
+      projectile
+      deadgrep
+      counsel
+      counsel-projectile
+      # TODO(wpcarro): Learn what this is.
+      engine-mode
+      eglot
+      dap-mode
+      lsp-ui
+      suggest
+      paradox
+      flymake-shellcheck
+      fish-mode
+      tuareg
+      haskell-mode
+      use-package
+      general
+      clojure-mode
+      cider
+      f
+      dash
+      company
+      counsel
+      flycheck
+      emojify
+      yaml-mode
+    ]));
+
+  loadPath = concatStringsSep ":" [
+    ./.emacs.d/wpc
+    # TODO(wpcarro): Explain why the trailing ":" is needed.
+    "${wpcarrosEmacs.deps}/share/emacs/site-lisp:"
+  ];
+
+  # Transform an attrset into "export k=v" statements.
+  makeEnvVars = env: concatStringsSep "\n"
+    (mapAttrsToList (k: v: "export ${k}=\"${v}\"") env);
+
+  withEmacsPath = { emacsBin, env ? { }, load ? [ ] }:
+    writeShellScriptBin "wpcarros-emacs" ''
+      export XMODIFIERS=emacs
+      export PATH="${emacsBinPath}:$PATH"
+      export EMACSLOADPATH="${loadPath}"
+      ${makeEnvVars env}
+      exec ${emacsBin} \
+        --debug-init \
+        --no-init-file \
+        --no-site-file \
+        --no-site-lisp \
+        --load ${./.emacs.d/init.el} \
+        ${concatStringsSep "\n  " (map (el: "--load ${el} \\") load)}
+        "$@"
+    '';
+in
+{
+  inherit withEmacsPath;
+
+  nixos = { load ? [ ] }: withEmacsPath {
+    inherit load;
+    emacsBin = "${wpcarrosEmacs}/bin/emacs";
+  };
+
+  # Script that asserts my Emacs can initialize without warnings or errors.
+  check = runCommand "check-emacs" { } ''
+    # Even though Buildkite defines this, I'd still like still be able to test
+    # this locally without depending on my ability to remember to set CI=true.
+    export CI=true
+    export PATH="${emacsBinPath}:$PATH"
+    export EMACSLOADPATH="${loadPath}"
+    ${wpcarrosEmacs}/bin/emacs \
+      --no-site-file \
+      --no-site-lisp \
+      --no-init-file \
+      --script ${./ci.el} \
+      ${./.emacs.d/init.el} && \
+    touch $out
+  '';
+
+  meta.ci.targets = [ "check" ];
+}
diff --git a/users/wpcarro/emacs/elisp-conventions.md b/users/wpcarro/emacs/elisp-conventions.md
new file mode 100644
index 0000000000..0e39c3069d
--- /dev/null
+++ b/users/wpcarro/emacs/elisp-conventions.md
@@ -0,0 +1,20 @@
+# Elisp Conventions
+
+Some of this aligns with existing style guides. Some of it does not.
+
+In general, prefer functions with fixed arities instead of variadic
+alternatives.
+
+- Namespace functions with `namespace/function-name`
+- Use `ensure`, `assert`, `refute` whenever possible.
+- When talking about encoding and decoding, let's use the words "encoding" and
+  "decoding" rather than the myriad of other variants that appear like:
+  - `marshalling` and `unmarshalling`
+  - `parse` and `deparse`, `serialize`, `stringify`
+  - `unpickle` and `pickle` (Python)
+  - `from-string` and `to-string`
+  - TODO: Add more examples of these; there should be close to a dozen.
+- Annotate assertions with `!` endings.
+- Prefer the Scheme style of `predicate?`
+- Variadic functions *should* encode this by appending * onto their
+  name. E.g. `maybe/nil?*`
diff --git a/users/wpcarro/emacs/keybindings.md b/users/wpcarro/emacs/keybindings.md
new file mode 100644
index 0000000000..96ba7c9645
--- /dev/null
+++ b/users/wpcarro/emacs/keybindings.md
@@ -0,0 +1,47 @@
+# Keybindings
+
+Since I'm using Emacs to manage most of my workflow, all of the keybindings
+should be defined herein and -- in order to scale -- order must be imposed. This
+can help avoid KBD collisions and improve my ability to remember each KBD.
+
+See `kbd.el` for the programmatic encoding of these principles.
+
+## Troubleshooting
+
+When in doubt, use Emacs's `read-key` and `read-event` to learn what signal
+you're sending Emacs.
+
+### Super-
+
+- EXWM X11 windows are not processing `s-`.
+- EXWM X11 windows are not processing `<M-ESC>`.
+
+### Super-Ctrl-
+
+I'm reserving `C-s-` for opening X11 applications.
+
+- `terminator`: `t`
+- `google-chrome`: `c`
+
+## Emacs nouns
+
+Most of my keybindings should be organized according to their function, which in
+turn should be related to the following Emacs nouns.
+
+- `workspace`: As defined by EXWM.
+- `frame`: What non-Emacs users would call a "window". Currently my workflow
+  doesn't use or rely on Emacs frames.
+- `window`: A vertical or horizontal split within an Emacs frame.
+- `buffer`: Anything storing text in memory.
+
+## Prefixes and their meanings
+
+TODO: Have a system for leader-prefixed KBDs, chords, and prefixed chords.
+
+- `s-`: Switching between named workspaces. Right now, super is too overloaded
+  and would benefit from having more deliberate keybindings.
+- `C-M-`: Window sizing
+- `M-{h,j,k,l}`: Window traversing
+- `M-{\,-}`: Window splitting
+- `M-q`: Window deletion
+- `<leader>-q`: Window deletion
diff --git a/users/wpcarro/emacs/snippets.md b/users/wpcarro/emacs/snippets.md
new file mode 100644
index 0000000000..2081b56171
--- /dev/null
+++ b/users/wpcarro/emacs/snippets.md
@@ -0,0 +1,22 @@
+# Snippets
+
+Specifying snippets that I plan on defining for most of the programming
+languages with which I work. I hope this will serve as a checklist of language
+constructs I should support when adopting a new language.
+
+## Shared language features
+
+These are language features that should be available across most of the
+languages that I'm hoping to support.
+
+- `ld`: anonymous functions (i.e. lambdas)
+- `fn`: named function definition
+- `var`: variable definition
+
+## Miscellaneous other language KBDs
+
+Some of this is related to language tool must-haves, which may need to be a
+separate document.
+
+- `<leader>d`: Show documentation
+- `<leader>x`: Evaluate expression (works mostly for LISPs)
diff --git a/users/wpcarro/emacs/workspace.josh b/users/wpcarro/emacs/workspace.josh
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/wpcarro/emacs/workspace.josh
diff --git a/users/wpcarro/go/.envrc b/users/wpcarro/go/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/go/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/go/actors.go b/users/wpcarro/go/actors.go
new file mode 100644
index 0000000000..1409db185e
--- /dev/null
+++ b/users/wpcarro/go/actors.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+// Call function `f` in a go-routine, passing a reference to a newly created
+// channel, `c`, as its only argument. Return a reference to `c` to the caller
+// of `act`. When `f` halts, close the channel.
+func act(f func(chan interface{})) chan interface{} {
+	c := make(chan interface{})
+
+	go func() {
+		defer close(c)
+		f(c)
+	}()
+
+	return c
+}
+
+func prompt(msg string) string {
+	reader := bufio.NewReader(os.Stdin)
+	fmt.Print(msg)
+	text, _ := reader.ReadString('\n')
+	// TODO: Trim trailing newline from the rhs of text.
+	return text
+}
+
+func main() {
+	c := act(func(c chan interface{}) {
+		for {
+			x := <-c
+			fmt.Printf("[A] Received value: %v\n", x)
+
+		}
+	})
+
+	for {
+		x := prompt("[B] Enter a value: ")
+		c <- x
+	}
+	os.Exit(0)
+}
diff --git a/users/wpcarro/go/atomic-counters.go b/users/wpcarro/go/atomic-counters.go
new file mode 100644
index 0000000000..6cbcd2ee4e
--- /dev/null
+++ b/users/wpcarro/go/atomic-counters.go
@@ -0,0 +1,26 @@
+// Attempting to apply some of the lessons I learned here:
+// https://gobyexample.com/atomic-counters
+package main
+
+import (
+	"fmt"
+	"sync"
+	"sync/atomic"
+)
+
+func main() {
+	var count uint64
+	var wg sync.WaitGroup
+
+	for i := 0; i < 50; i += 1 {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			for j := 0; j < 1000; j += 1 {
+				atomic.AddUint64(&count, 1)
+			}
+		}()
+	}
+	wg.Wait()
+	fmt.Println("Count: ", count)
+}
diff --git a/users/wpcarro/go/channels.go b/users/wpcarro/go/channels.go
new file mode 100644
index 0000000000..cba8abfc96
--- /dev/null
+++ b/users/wpcarro/go/channels.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+	"fmt"
+	"math/rand"
+	"sync"
+	"sync/atomic"
+)
+
+type readMsg struct {
+	key    int
+	sender chan int
+}
+
+type writeMsg struct {
+	key    int
+	value  int
+	sender chan bool
+}
+
+func main() {
+	fmt.Println("Hello, go.")
+
+	var readOps uint64
+	var writeOps uint64
+	var wg sync.WaitGroup
+
+	reads := make(chan readMsg)
+	writes := make(chan writeMsg)
+
+	go func() {
+		state := make(map[int]int)
+		for {
+			select {
+			case msg := <-reads:
+				msg.sender <- state[msg.key]
+			case msg := <-writes:
+				state[msg.key] = msg.value
+				msg.sender <- true
+			}
+		}
+	}()
+
+	// Reads
+	for i := 0; i < 100; i += 1 {
+		go func() {
+			wg.Add(1)
+			defer wg.Done()
+			for j := 0; j < 100; j += 1 {
+				msg := readMsg{
+					key:    rand.Intn(5),
+					sender: make(chan int)}
+				reads <- msg
+				val := <-msg.sender
+				fmt.Printf("Received %d.\n", val)
+				atomic.AddUint64(&readOps, 1)
+			}
+		}()
+	}
+
+	// Writes
+	for i := 0; i < 100; i += 1 {
+		go func() {
+			wg.Add(1)
+			defer wg.Done()
+			for j := 0; j < 100; j += 1 {
+				msg := writeMsg{
+					key:    rand.Intn(5),
+					value:  rand.Intn(10),
+					sender: make(chan bool)}
+				writes <- msg
+				<-msg.sender
+				fmt.Printf("Set %d as %d in state\n", msg.key, msg.value)
+				atomic.AddUint64(&writeOps, 1)
+			}
+		}()
+	}
+
+	wg.Wait()
+	fmt.Printf("Read ops: %d\tWrite ops: %d\n", atomic.LoadUint64(&readOps), atomic.LoadUint64(&writeOps))
+}
diff --git a/users/wpcarro/go/mutex.go b/users/wpcarro/go/mutex.go
new file mode 100644
index 0000000000..5cea20754b
--- /dev/null
+++ b/users/wpcarro/go/mutex.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+	"fmt"
+	"math/rand"
+	"sync"
+	"sync/atomic"
+	"time"
+)
+
+func main() {
+	state := make(map[int]int)
+	mux := &sync.Mutex{}
+
+	var readOps uint64
+	var writeOps uint64
+
+	// Read from state
+	for i := 0; i < 1000; i += 1 {
+		for j := 0; j < 100; j += 1 {
+			go func() {
+				key := rand.Intn(5)
+				mux.Lock()
+				fmt.Printf("state[%d] = %d\n", key, state[key])
+				mux.Unlock()
+				atomic.AddUint64(&readOps, 1)
+				time.Sleep(time.Millisecond)
+			}()
+		}
+	}
+
+	// Write to state
+	for i := 0; i < 10; i += 1 {
+		for j := 0; j < 100; j += 1 {
+			go func() {
+				key := rand.Intn(5)
+				mux.Lock()
+				state[key] += 1
+				mux.Unlock()
+				fmt.Printf("Wrote to state[%d].\n", key)
+				atomic.AddUint64(&writeOps, 1)
+				time.Sleep(time.Millisecond)
+			}()
+		}
+	}
+
+	time.Sleep(time.Millisecond)
+
+	mux.Lock()
+	fmt.Printf("State: %v\n", state)
+	mux.Unlock()
+	fmt.Printf("Reads: %d\tWrites: %d\n", atomic.LoadUint64(&readOps), atomic.LoadUint64(&writeOps))
+}
diff --git a/users/wpcarro/go/shell.nix b/users/wpcarro/go/shell.nix
new file mode 100644
index 0000000000..f777c13fef
--- /dev/null
+++ b/users/wpcarro/go/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    go
+    goimports
+    godef
+  ];
+}
diff --git a/users/wpcarro/go/waitgroups.go b/users/wpcarro/go/waitgroups.go
new file mode 100644
index 0000000000..816321b877
--- /dev/null
+++ b/users/wpcarro/go/waitgroups.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+	"fmt"
+	"sync"
+	"time"
+)
+
+func saySomething(x string, wg *sync.WaitGroup) {
+	defer wg.Done()
+	fmt.Println(x)
+	time.Sleep(time.Second)
+	fmt.Printf("Finished saying \"%s\"\n", x)
+}
+
+func main() {
+	var wg sync.WaitGroup
+	var things = [5]string{"chicken", "panini", "cheeseburger", "rice", "bread"}
+	for i := 0; i < 5; i += 1 {
+		wg.Add(1)
+		go saySomething(things[i], &wg)
+	}
+	wg.Wait()
+}
diff --git a/users/wpcarro/gopkgs/kv/default.nix b/users/wpcarro/gopkgs/kv/default.nix
new file mode 100644
index 0000000000..72aae7827b
--- /dev/null
+++ b/users/wpcarro/gopkgs/kv/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.nix.buildGo.package {
+  name = "kv";
+  srcs = [
+    ./kv.go
+  ];
+}
diff --git a/users/wpcarro/gopkgs/kv/kv.go b/users/wpcarro/gopkgs/kv/kv.go
new file mode 100644
index 0000000000..040cc63e0e
--- /dev/null
+++ b/users/wpcarro/gopkgs/kv/kv.go
@@ -0,0 +1,39 @@
+// Supporting reading and writing key-value pairs to disk.
+package kv
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"log"
+	"path"
+)
+
+// Return the decoded store from disk.
+func getStore(storePath string) map[string]interface{} {
+	b, err := ioutil.ReadFile(path.Join(storePath, "kv.json"))
+	if err != nil {
+		log.Fatal("Could not read store: ", err)
+	}
+	var state map[string]interface{}
+	err = json.Unmarshal(b, &state)
+	if err != nil {
+		log.Fatal("Could not decode store as JSON: ", err)
+	}
+	return state
+}
+
+// Set `key` to `value` in the store.
+func Set(storePath string, key string, value interface{}) error {
+	state := getStore(storePath)
+	state[key] = value
+	b, err := json.Marshal(state)
+	if err != nil {
+		log.Fatal("Could not encode state as JSON: ", err)
+	}
+	return ioutil.WriteFile(path.Join(storePath, "kv.json"), b, 0644)
+}
+
+// Get `key` from the store.
+func Get(storePath string, key string) interface{} {
+	return getStore(path.Join(storePath, "kv.json"))[key]
+}
diff --git a/users/wpcarro/gopkgs/utils/default.nix b/users/wpcarro/gopkgs/utils/default.nix
new file mode 100644
index 0000000000..25321f50a0
--- /dev/null
+++ b/users/wpcarro/gopkgs/utils/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.nix.buildGo.package {
+  name = "utils";
+  srcs = [
+    ./utils.go
+  ];
+}
diff --git a/users/wpcarro/gopkgs/utils/utils.go b/users/wpcarro/gopkgs/utils/utils.go
new file mode 100644
index 0000000000..7d662d0866
--- /dev/null
+++ b/users/wpcarro/gopkgs/utils/utils.go
@@ -0,0 +1,131 @@
+// Some utility functions to tidy up my Golang.
+package utils
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httputil"
+	"os"
+	"os/user"
+	"path/filepath"
+)
+
+// Return the absolute path to the current uesr's home directory.
+func HomeDir() string {
+	user, err := user.Current()
+	if err != nil {
+		log.Fatal(err)
+	}
+	return user.HomeDir
+}
+
+// Returns true if `info` is a symlink.
+func IsSymlink(info os.FileMode) bool {
+	return info&os.ModeSymlink != 0
+}
+
+// Return true if `path` exists and false otherwise.
+func FileExists(path string) bool {
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		return false
+	} else {
+		return true
+	}
+}
+
+// Return the absolute file path of `file` using the following resolution
+// strategy:
+// - Traverse and search upwards until you reach the user's home directory
+// - Return the first path in `backupPaths` that exists
+// - Fail
+func Resolve(fileName string, backupPaths []string) string {
+	// TODO(wpcarro): Drop hardcoding when whoami behaves as expected.
+	boundary := "/home"
+	cwd := "."
+	files, _ := ioutil.ReadDir(cwd)
+
+	for {
+		fullCwd, _ := filepath.Abs(cwd)
+		if fullCwd == boundary {
+			break
+		}
+		for _, file := range files {
+			if file.Name() == fileName {
+				path, _ := filepath.Abs(cwd + "/" + file.Name())
+				return path
+			}
+		}
+		cwd += "/.."
+		files, _ = ioutil.ReadDir(cwd)
+	}
+
+	// TODO(wpcarro): Support expanding these paths to allow the consumer to
+	// pass in relative paths, and paths with "~" in them.
+	for _, backup := range backupPaths {
+		if FileExists(backup) {
+			return backup
+		}
+	}
+	log.Fatal("Cannot find a run.json to use.")
+	// This code should be unreachable.
+	return ""
+}
+
+// Call log.Fatal with `err` when it's not nil.
+func FailOn(err error) {
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+// Prints the verbose form of an HTTP request.
+func DebugRequest(req *http.Request) {
+	bytes, _ := httputil.DumpRequest(req, true)
+	fmt.Println(string(bytes))
+}
+
+// Prints out the verbose form of an HTTP response.
+func DebugResponse(res *http.Response) {
+	bytes, _ := httputil.DumpResponse(res, true)
+	fmt.Println(string(bytes))
+}
+
+// Make a simple GET request to `url`. Fail if anything returns an error. I'd
+// like to accumulate a library of these, so that I can write scrappy Go
+// quickly. For now, this function just returns the body of the response back as
+// a string.
+func SimpleGet(url string, headers map[string]string, debug bool) string {
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+	for k, v := range headers {
+		req.Header.Add(k, v)
+	}
+
+	res, err := client.Do(req)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer res.Body.Close()
+
+	if debug {
+		DebugRequest(req)
+		DebugResponse(res)
+	}
+
+	if res.StatusCode == http.StatusOK {
+		bytes, err := ioutil.ReadAll(res.Body)
+		if err != nil {
+			log.Fatal(err)
+		}
+		return string(bytes)
+	} else {
+		log.Println(res)
+		log.Fatalf("HTTP status code of response not OK: %v\n", res.StatusCode)
+		return ""
+	}
+}
diff --git a/users/wpcarro/haskell-file/.envrc b/users/wpcarro/haskell-file/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/haskell-file/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/haskell-file/README.md b/users/wpcarro/haskell-file/README.md
new file mode 100644
index 0000000000..3f3ac1474b
--- /dev/null
+++ b/users/wpcarro/haskell-file/README.md
@@ -0,0 +1,7 @@
+# haskell-file
+
+This is a half-baked project. I'd like to write a library whose API closely
+resembles some of the more modern filesystem APIs to which I am accustomed:
+notably f.el for Elisp.
+
+I expect more development to come.
diff --git a/users/wpcarro/haskell-file/f-todo.org b/users/wpcarro/haskell-file/f-todo.org
new file mode 100644
index 0000000000..6dd43a9629
--- /dev/null
+++ b/users/wpcarro/haskell-file/f-todo.org
@@ -0,0 +1,67 @@
+* Paths
+** TODO f-join (&rest args)
+** TODO f-split (path)
+** TODO f-expand (path &optional dir)
+** TODO f-filename (path)
+** TODO f-dirname (path)
+** TODO f-common-parent (paths)
+** TODO f-ext (path)
+** TODO f-no-ext (path)
+** TODO f-swap-ext (path ext)
+** TODO f-base (path)
+** TODO f-relative (path &optional dir)
+** TODO f-short (path)
+** TODO f-long (path)
+** TODO f-canonical (path)
+** TODO f-slash (path)
+** TODO f-full (path)
+** TODO f-uniquify (paths)
+** TODO f-uniquify-alist (paths)
+* I/O
+** TODO f-read-bytes (path)
+** TODO f-write-bytes (data path)
+** TODO f-read-text (path &optional coding)
+** TODO f-write-text(text coding path)
+** TODO f-append-text(text coding path)
+** TODO f-append-bytes(text coding path)
+** TODO Destructive
+** TODO f-mkdir (&rest dirs)
+** TODO f-delete (path &optional force)
+** TODO f-symlink (source path)
+** TODO f-move (from to)
+** TODO f-copy (from to)
+** TODO f-copy-contenst (from to)
+** TODO f-touch (path)
+** TODO Predicates
+** TODO f-exists? (path)
+** TODO f-directory? (path)
+** TODO f-file? (path)
+** TODO f-symlink? (path)
+** TODO f-readable? (path)
+** TODO f-writable? (path)
+** TODO f-executable? (path)
+** TODO f-absolute? (path)
+** TODO f-relative? (path)
+** TODO f-root? (path)
+** TODO f-ext? (path ext)
+** TODO f-same? (path-a path-b)
+** TODO f-parent-of? (path-a path-b)
+** TODO f-child-of? (path-a path-b)
+** TODO f-ancestor-of? (path-a path-b)
+** TODO f-descendant-of? (path-a path-b)
+** TODO f-hidden? (path)
+** TODO f-empty? (path)
+** TODO Stats
+** TODO f-size (path)
+** f-depth (path)
+
+* Misc
+** TODO f-this-file ()
+** TODO f-path-separator ()
+** TODO f-glob (pattern &optional path)
+** TODO f-entries (path &optional fn recursive)
+** TODO f-directories (path &optional fn recursive)
+** TODO f-files (path &optional fn recursive)
+** TODO f-root ()
+** TODO f-traverse-upwards (fn &optional path)
+** TODO f-with-sandbox (path-or-paths &rest body)
diff --git a/users/wpcarro/haskell-file/f.hs b/users/wpcarro/haskell-file/f.hs
new file mode 100644
index 0000000000..295575f3f4
--- /dev/null
+++ b/users/wpcarro/haskell-file/f.hs
@@ -0,0 +1,64 @@
+module F
+  ( join
+  , split
+  ) where
+
+--------------------------------------------------------------------------------
+-- Dependencies
+--------------------------------------------------------------------------------
+
+import Data.List (span)
+import System.FilePath (FilePath, pathSeparator)
+import System.FilePath.Posix (FilePath)
+import qualified System.FilePath.Posix as F
+
+-- TODO: Move this to a misc.hs, prelude.hs, operators.hs; somewhere.
+(|>) :: a -> (a -> b) -> b
+(|>) a f = f a
+infixl 1 |>
+
+-- TODO: Move this to a test_utils.hs or elsewhere.
+simpleAssert :: (Eq a) => a -> a -> ()
+simpleAssert x y =
+  if x == y then
+    ()
+  else
+    error "Assertion error"
+
+--------------------------------------------------------------------------------
+-- Library
+--------------------------------------------------------------------------------
+
+join :: [FilePath] -> FilePath
+join = F.joinPath
+
+-- | Split path and return  list containing parts.
+split :: FilePath -> [String]
+split = splitJoin . span (/= pathSeparator)
+  where
+    splitJoin :: (String, String) -> [String]
+    splitJoin ([], []) = []
+    splitJoin (a, []) = [a]
+    splitJoin (a, [_]) = [a]
+    splitJoin (a, _:b) = a : split b
+
+--------------------------------------------------------------------------------
+-- Tests
+--------------------------------------------------------------------------------
+
+expected :: [([FilePath], FilePath)]
+expected = [ (["path"], "path")
+           , (["/path"], "/path")
+           , (["path", "to", "file"], "path/to/file")
+           , (["/path", "to", "file"], "/path/to/file")
+           , (["/"], "/")
+           ]
+
+runTests :: [()]
+runTests =
+  fmap (\(input, expected) -> simpleAssert (join input) expected) expected
+
+main :: IO ()
+main = do
+  print runTests
+  pure ()
diff --git a/users/wpcarro/haskell-file/shell.nix b/users/wpcarro/haskell-file/shell.nix
new file mode 100644
index 0000000000..0c6a298bf2
--- /dev/null
+++ b/users/wpcarro/haskell-file/shell.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: [ ];
+}
diff --git a/users/wpcarro/haskell-file/tests.hs b/users/wpcarro/haskell-file/tests.hs
new file mode 100644
index 0000000000..e3967b77de
--- /dev/null
+++ b/users/wpcarro/haskell-file/tests.hs
@@ -0,0 +1,39 @@
+module FTest where
+--------------------------------------------------------------------------------
+import Test.Tasty
+import Test.Tasty.Hedgehog
+import Hedgehog
+--------------------------------------------------------------------------------
+import qualified Hedgehog as H
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+--------------------------------------------------------------------------------
+import Data.List (intercalate)
+import System.FilePath (pathSeparator)
+--------------------------------------------------------------------------------
+import F
+--------------------------------------------------------------------------------
+main :: IO ()
+main
+  = defaultMain
+  . localOption (HedgehogTestLimit $ Just 50)
+  $ testGroup "f functions"
+  [ test_split
+  ]
+--------------------------------------------------------------------------------
+test_split :: TestTree
+test_split
+  = testGroup "split function"
+  [ testProperty "splits parts properly" splitSuccess
+  ]
+splitSuccess :: Property
+splitSuccess = property $ do
+  -- separator
+  --   <- H.forAll
+  --   $ Gen.element ['/', '\\']
+  parts
+    <- H.forAll
+    . Gen.list (Range.linear 0 10)
+    $ Gen.list (Range.linear 1 10) Gen.alphaNum
+  let path = intercalate [pathSeparator] parts
+  F.split path === parts
diff --git a/users/wpcarro/keys.nix b/users/wpcarro/keys.nix
new file mode 100644
index 0000000000..001ca30ce8
--- /dev/null
+++ b/users/wpcarro/keys.nix
@@ -0,0 +1,18 @@
+# wpcarro's public SSH keys
+{ ... }:
+
+rec {
+  ava = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB/5Fuo7wi8rNXVXgNaCK2X6ePCh9LQs/9h7Tj6UeXrl wpcarro@ava";
+  diogenes = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILFDRfpNXDxQuTJAqVg8+Mm/hOfE5VAJP+Lpw9kA5cDG wpcarro@gmail.com";
+  marcus = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJkNQJBXekuSzZJ8+gxT+V1+eXTm3hYsfigllr/ARXkf wpcarro@gmail.com";
+  nathan = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP2NjuP722VUgpSu5bVUPTfdVNPO8fSW0Jlas8L4up13 bill@nathan";
+  tarasco = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh+wG4f7tI0IwGyF2sLi5mPlh3JKE7KqV2ab0tlcL36 wpcarro@tarasco";
+
+  all = [
+    ava
+    diogenes
+    marcus
+    nathan
+    tarasco
+  ];
+}
diff --git a/users/wpcarro/lisp/README.md b/users/wpcarro/lisp/README.md
new file mode 100644
index 0000000000..9f8693fa6a
--- /dev/null
+++ b/users/wpcarro/lisp/README.md
@@ -0,0 +1,16 @@
+# Common Lisp
+
+Things that I like about Common Lisp:
+- It's an S-expression based language.
+- It has a powerful macro system
+- It has a unique way of handling-errors
+- It is highly introspectible
+- The tooling integration with Emacs is the best I have ever seen for any language
+
+Things that I don't like about Common Lisp:
+- I find its standard libraries difficult to use and -- compared to modern
+  libraries -- like Golang's or Elixir's standard libraries, Common Lisp's
+  libraries are clunky
+
+As such, I would like to modernize CL's libraries to resemble other libraries
+with which I am more familiar and, therefore, productive.
diff --git a/users/wpcarro/lisp/prelude.lisp b/users/wpcarro/lisp/prelude.lisp
new file mode 100644
index 0000000000..3522567ea0
--- /dev/null
+++ b/users/wpcarro/lisp/prelude.lisp
@@ -0,0 +1,14 @@
+(in-package #:cl-user)
+(defpackage #:prelude
+  (:documentation "Supporting miscellaneous utility functions and macros.")
+  (:use #:cl)
+  (:shadow #:type)
+  (:export #:type #:comment))
+(in-package #:prelude)
+
+;; TODO: Add documentation to these macros.
+
+(defmacro type (name in out)
+  `(declaim (ftype (function ,in ,out) ,name)))
+
+(defmacro comment (&rest _forms) nil)
diff --git a/users/wpcarro/lisp/prelude.nix b/users/wpcarro/lisp/prelude.nix
new file mode 100644
index 0000000000..5fe5d628e0
--- /dev/null
+++ b/users/wpcarro/lisp/prelude.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.nix.buildLisp.library {
+  name = "prelude";
+  srcs = [
+    ./prelude.lisp
+  ];
+}
diff --git a/users/wpcarro/nixos/ava/ava.el b/users/wpcarro/nixos/ava/ava.el
new file mode 100644
index 0000000000..e4537c4c17
--- /dev/null
+++ b/users/wpcarro/nixos/ava/ava.el
@@ -0,0 +1,57 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'display)
+(require 'window-manager)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Monitor Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(display-register primary
+                  :output "HDMI-1"
+                  :primary t
+                  :coords (0 0)
+                  :size (2560 1440)
+                  :rate 30.0
+                  :dpi 96
+                  :rotate normal)
+
+(display-register secondary
+                  :output "HDMI-2"
+                  :primary nil
+                  :coords (2561 0)
+                  :size (2560 1440)
+                  :rate 30.0
+                  :dpi 96
+                  :rotate normal)
+
+(display-arrangement main :displays (primary secondary))
+
+(setq window-manager-named-workspaces
+      (list (make-window-manager-named-workspace
+             :label "Web Browsing"
+             :kbd "c"
+             :display display-secondary)
+            (make-window-manager-named-workspace
+             :label "Coding I"
+             :kbd "1"
+             :display display-primary)
+            (make-window-manager-named-workspace
+             :label "Coding II"
+             :kbd "2"
+             :display display-primary)
+            (make-window-manager-named-workspace
+             :label "Chatting"
+             :kbd "h"
+             :display display-secondary)))
+
+(window-manager-init :init-hook #'display-arrange-main)
+
+(bookmark-install-kbd
+ (make-bookmark :label "hadrian"
+                :path (f-join tvl-depot-path "/hadrian")
+                :kbd "h"))
+
+(add-to-list 'ssh-hosts "wpcarro@tarasco")
diff --git a/users/wpcarro/nixos/ava/default.nix b/users/wpcarro/nixos/ava/default.nix
new file mode 100644
index 0000000000..11dae8d66a
--- /dev/null
+++ b/users/wpcarro/nixos/ava/default.nix
@@ -0,0 +1,136 @@
+{ depot, pkgs, lib, ... }:
+{ ... }:
+
+let
+  inherit (depot.users) wpcarro;
+
+  wpcarrosEmacs = wpcarro.emacs.nixos {
+    load = [ ./ava.el ];
+  };
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+{
+  imports = [ ./hardware.nix ];
+
+  # Use the TVL binary cache
+  tvl.cache.enable = true;
+
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  # Support IP forwarding to use this device as a Tailscale exit node.
+  boot.kernel.sysctl."net.ipv4.ip_forward" = true;
+  boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
+
+  time.timeZone = "America/Los_Angeles";
+
+  networking = {
+    # The global useDHCP flag is deprecated, therefore explicitly set to false
+    # here.  Per-interface useDHCP will be mandatory in the future, so this
+    # generated config replicates the default behaviour.
+    useDHCP = false;
+    hostName = "ava";
+    networkmanager.enable = true;
+    interfaces.enp1s0.useDHCP = true;
+    interfaces.enp3s0.useDHCP = true;
+    interfaces.wlp2s0.useDHCP = true;
+  };
+
+  services = wpcarro.common.services // {
+    # Check the amount of available memory and free swap a few times per second
+    # and kill the largest process if both are below 10%.
+    earlyoom.enable = true;
+
+    tailscale.enable = true;
+
+    openssh.enable = true;
+
+    xserver = {
+      enable = true;
+      layout = "us";
+      xkbOptions = "caps:escape";
+      displayManager = {
+        # Give EXWM permission to control the session (from tazjin's setup).
+        sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localhost:$USER";
+        lightdm.enable = true;
+      };
+      windowManager.session = lib.singleton {
+        name = "exwm";
+        start = "${wpcarrosEmacs}/bin/wpcarros-emacs";
+      };
+    };
+  };
+
+  # Enable sound.
+  sound.enable = true;
+  hardware.pulseaudio.enable = true;
+
+  users.mutableUsers = true;
+  users.users.root.openssh.authorizedKeys.keys = [
+    wpcarro.keys.nathan
+    wpcarro.keys.tarasco
+  ];
+  users.users.wpcarro = {
+    isNormalUser = true;
+    extraGroups = [
+      "networkmanager"
+      "wheel"
+      "docker"
+    ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = [
+      wpcarro.keys.nathan
+      wpcarro.keys.tarasco
+    ];
+  };
+  users.extraGroups.vboxusers.members = [ "wpcarro" ];
+
+  security.sudo.wheelNeedsPassword = false;
+
+  fonts = {
+    fonts = with pkgs; [
+      jetbrains-mono
+    ];
+
+    fontconfig = {
+      defaultFonts = {
+        monospace = [ "JetBrains Mono" ];
+      };
+    };
+  };
+
+  programs = wpcarro.common.programs // {
+    mosh.enable = true;
+  };
+
+  virtualisation.docker.enable = true;
+  virtualisation.virtualbox.host.enable = true;
+
+  environment.variables = {
+    EDITOR = "emacsclient";
+    ALTERNATE_EDITOR = "emacs -q -nw";
+    VISUAL = "emacsclient";
+  };
+
+  environment.systemPackages =
+    wpcarro.common.shell-utils ++
+    (with pkgs; [
+      alacritty
+      firefox
+      google-chrome
+      httpie
+      pavucontrol
+      quasselClient
+      remmina
+      tdesktop
+      wpcarrosEmacs
+      xsecurelock
+    ]);
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/wpcarro/nixos/ava/hardware.nix b/users/wpcarro/nixos/ava/hardware.nix
new file mode 100644
index 0000000000..4bfdca1cc5
--- /dev/null
+++ b/users/wpcarro/nixos/ava/hardware.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports =
+    [
+      (modulesPath + "/installer/scan/not-detected.nix")
+    ];
+
+  boot.initrd.availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
+  boot.initrd.kernelModules = [ ];
+  boot.kernelModules = [ "kvm-intel" ];
+  boot.extraModulePackages = [ ];
+
+  fileSystems."/" =
+    {
+      device = "/dev/disk/by-label/NIXROOT";
+      fsType = "ext4";
+    };
+
+  fileSystems."/boot" =
+    {
+      device = "/dev/disk/by-label/NIXBOOT";
+      fsType = "vfat";
+    };
+
+  swapDevices = [ ];
+
+  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+  # high-resolution display
+  hardware.video.hidpi.enable = lib.mkDefault true;
+}
diff --git a/users/wpcarro/nixos/default.nix b/users/wpcarro/nixos/default.nix
new file mode 100644
index 0000000000..b1623d4975
--- /dev/null
+++ b/users/wpcarro/nixos/default.nix
@@ -0,0 +1,59 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.users.wpcarro.nixos)
+    ava
+    diogenes
+    marcus
+    tarasco;
+
+  systemFor = sys: (depot.ops.nixos.nixosFor sys).system;
+in
+{
+  avaSystem = systemFor ava;
+  marcusSystem = systemFor marcus;
+  tarascoSystem = systemFor ava;
+
+  # Apply terraform updates and rebuild NixOS for diogenes.
+  deploy-diogenes = pkgs.writeShellScriptBin "deploy-diogenes" ''
+    set -euo pipefail
+    readonly TF_STATE_DIR=/depot/users/wpcarro/terraform
+    rm -f $TF_STATE_DIR/*.json
+    readonly STORE_PATH="${diogenes.json}"
+    # We can't use the result symlink because terraform looks for a *.json file
+    # in the current working directory.
+    cp $STORE_PATH $TF_STATE_DIR
+
+    if [ ! -d $TF_STATE_DIR/.terraform ]; then
+      ${pkgs.terraform}/bin/terraform -chdir="$TF_STATE_DIR" init
+    fi
+
+    function cleanup() {
+      rm -f "$TF_STATE_DIR/$(basename $STORE_PATH)"
+    }
+    trap cleanup EXIT
+
+    ${pkgs.terraform}/bin/terraform -chdir="$TF_STATE_DIR" apply
+  '';
+
+  # Rebuild NixOS for diogenes without applying terraform updates.
+  rebuild-diogenes = pkgs.writeShellScriptBin "rebuild-diogenes" ''
+    set -euo pipefail
+    readonly target="root@billandhiscomputer.com"
+
+    # We need to call nix-build here on the drvPath because it may not be in
+    # /nix/store yet.
+    readonly STORE_PATH="$(nix-build ${diogenes.drvPath} --no-out-link --show-trace)"
+    nix-copy-closure --to $target ${diogenes.osPath} \
+      --gzip --use-substitutes $STORE_PATH
+
+    ssh $target 'nix-env --profile /nix/var/nix/profiles/system --set ${diogenes.osPath}'
+    ssh $target '${diogenes.osPath}/bin/switch-to-configuration switch'
+  '';
+
+  meta.ci.targets = [
+    "avaSystem"
+    "marcusSystem"
+    "tarascoSystem"
+  ];
+}
diff --git a/users/wpcarro/nixos/diogenes/README.md b/users/wpcarro/nixos/diogenes/README.md
new file mode 100644
index 0000000000..f77c01d2d4
--- /dev/null
+++ b/users/wpcarro/nixos/diogenes/README.md
@@ -0,0 +1,13 @@
+# diogenes
+
+diogenes is a NixOS machine deployed on a Google VM. It hosts
+https://billandhiscomputer.com.
+
+## Deployment
+
+I manage diogenes's deployment with Terraform. My current workflow looks like
+this:
+
+```shell
+deploy-diogenes
+```
diff --git a/users/wpcarro/nixos/diogenes/default.nix b/users/wpcarro/nixos/diogenes/default.nix
new file mode 100644
index 0000000000..022570e3d3
--- /dev/null
+++ b/users/wpcarro/nixos/diogenes/default.nix
@@ -0,0 +1,160 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.users) wpcarro;
+  name = "diogenes";
+  domainName = "billandhiscomputer.com";
+in
+wpcarro.terraform.googleCloudVM {
+  project = "wpcarros-infrastructure";
+  name = "diogenes";
+  region = "us-central1";
+  zone = "us-central1-a";
+
+  # DNS configuration
+  extraConfig = {
+    # billandhiscomputer.com
+    resource.google_dns_managed_zone."${name}" = {
+      inherit name;
+      dns_name = "${domainName}.";
+    };
+
+    resource.google_dns_record_set."${name}" = {
+      name = "${domainName}.";
+      type = "A";
+      ttl = 300; # 5m
+      managed_zone = "\${google_dns_managed_zone.${name}.name}";
+      rrdatas = [ "\${google_compute_instance.${name}.network_interface[0].access_config[0].nat_ip}" ];
+    };
+
+    resource.google_compute_instance."${name}" = {
+      network_interface.access_config = {
+        public_ptr_domain_name = "${domainName}.";
+      };
+    };
+  };
+
+  configuration = {
+    imports = [
+      (depot.path.origSrc + "/ops/modules/quassel.nix")
+    ];
+
+    networking = {
+      firewall.allowedTCPPorts = [
+        22 # ssh
+        80 # http
+        443 # https
+        6698 # quassel
+      ];
+      firewall.allowedUDPPortRanges = [
+        { from = 60000; to = 61000; } # mosh
+      ];
+    };
+
+    # Use the TVL binary cache
+    tvl.cache.enable = true;
+
+    users = {
+      mutableUsers = true;
+      users = {
+        root = {
+          openssh.authorizedKeys.keys = wpcarro.keys.all;
+        };
+        wpcarro = {
+          isNormalUser = true;
+          extraGroups = [ "wheel" "quassel" ];
+          openssh.authorizedKeys.keys = wpcarro.keys.all;
+          shell = pkgs.fish;
+        };
+        # This is required so that quasselcore can read the ACME cert in
+        # /var/lib/acme, which is only available to user=acme or group=nginx.
+        quassel.extraGroups = [ "nginx" ];
+      };
+    };
+
+    security = {
+      acme = {
+        acceptTerms = true;
+        defaults.email = "wpcarro@gmail.com";
+      };
+
+      sudo.wheelNeedsPassword = false;
+    };
+
+    programs = wpcarro.common.programs // {
+      mosh.enable = true;
+    };
+
+    # I won't have an Emacs server running on diogenes, and I'll likely be in an
+    # SSH session from within vterm. As such, Vim is one of the few editors that
+    # I tolerably navigate this way.
+    environment.variables = {
+      EDITOR = "vim";
+    };
+
+    environment.systemPackages = wpcarro.common.shell-utils;
+
+    services = wpcarro.common.services // {
+      # TODO(wpcarro): Re-enable this when rebuild-system better supports
+      # terraform deployments.
+      # depot.auto-deploy = {
+      #   enable = true;
+      #   interval = "1h";
+      # };
+
+      # TODO(wpcarro): Re-enable this after debugging ACME and NXDOMAIN.
+      depot.quassel = {
+        enable = true;
+        acmeHost = domainName;
+        bindAddresses = [
+          "0.0.0.0"
+        ];
+      };
+
+      journaldriver = {
+        enable = true;
+        logStream = "home";
+        googleCloudProject = "wpcarros-infrastructure";
+        applicationCredentials = "/etc/gcp/key.json";
+      };
+
+      nginx = {
+        enable = true;
+        enableReload = true;
+
+        recommendedTlsSettings = true;
+        recommendedGzipSettings = true;
+        recommendedProxySettings = true;
+
+        # for journaldriver
+        commonHttpConfig = ''
+          log_format json_combined escape=json
+          '{'
+              '"remote_addr":"$remote_addr",'
+              '"method":"$request_method",'
+              '"host":"$host",'
+              '"uri":"$request_uri",'
+              '"status":$status,'
+              '"request_size":$request_length,'
+              '"response_size":$body_bytes_sent,'
+              '"response_time":$request_time,'
+              '"referrer":"$http_referer",'
+              '"user_agent":"$http_user_agent"'
+          '}';
+
+          access_log syslog:server=unix:/dev/log,nohostname json_combined;
+        '';
+
+        virtualHosts = {
+          "${domainName}" = {
+            addSSL = true;
+            enableACME = true;
+            root = wpcarro.website.root;
+          };
+        };
+      };
+    };
+
+    system.stateVersion = "21.11";
+  };
+}
diff --git a/users/wpcarro/nixos/iso.nix b/users/wpcarro/nixos/iso.nix
new file mode 100644
index 0000000000..8102c98fb8
--- /dev/null
+++ b/users/wpcarro/nixos/iso.nix
@@ -0,0 +1,17 @@
+# TODO(wpcarro): Support the workflow outlined in these docs.
+#
+# Usage:
+#   $ lsblk  # get your USB dev path (e.g. /dev/sdb)
+#   $ create-installer --dev=/dev/sdb //users/wpcarro/nixos/marcus
+
+{ pkgs, ... }:
+
+{
+  imports = [
+    "${pkgs.nixos}/modules/installer/cd-graphical-gnome.nix"
+  ];
+
+  config = {
+    networking.wireless.enable = true;
+  };
+}
diff --git a/users/wpcarro/nixos/marcus/default.nix b/users/wpcarro/nixos/marcus/default.nix
new file mode 100644
index 0000000000..0dc4fc57f5
--- /dev/null
+++ b/users/wpcarro/nixos/marcus/default.nix
@@ -0,0 +1,172 @@
+{ depot, pkgs, lib, ... }:
+{ ... }:
+
+let
+  inherit (depot.users) wpcarro;
+
+  wpcarrosEmacs = wpcarro.emacs.nixos {
+    load = [ ./marcus.el ];
+  };
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+{
+  imports = [
+    (depot.path.origSrc + "/users/wpcarro/nixos/marcus/hardware.nix")
+    (pkgs.home-manager.src + "/nixos")
+  ];
+
+  # Use the TVL binary cache
+  tvl.cache.enable = true;
+
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  networking = {
+    # The global useDHCP flag is deprecated, therefore explicitly set to false
+    # here.  Per-interface useDHCP will be mandatory in the future, so this
+    # generated config replicates the default behaviour.
+    useDHCP = false;
+    hostName = "marcus";
+    networkmanager.enable = true;
+    interfaces.enp0s31f6.useDHCP = true;
+    interfaces.wlp0s20f3.useDHCP = true;
+  };
+
+  # Schedule daily reboots.
+  systemd.timers.auto-reboot = {
+    wantedBy = [ "timers.target" ];
+    timerConfig = {
+      OnCalendar = "*-*-* 03:00:00";
+      Unit = "reboot.target";
+    };
+  };
+
+  services = wpcarro.common.services // {
+    tzupdate.enable = true;
+
+    depot.auto-deploy = {
+      enable = true;
+      interval = "1d";
+    };
+
+    xserver = {
+      enable = true;
+      libinput = {
+        enable = true;
+        touchpad.naturalScrolling = false;
+        touchpad.tapping = false;
+      };
+      layout = "us";
+      xkbOptions = "caps:escape";
+      displayManager = {
+        # Give EXWM permission to control the session (from tazjin's setup).
+        sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localhost:$USER";
+        lightdm.enable = true;
+      };
+      extraConfig = ''
+        Section "InputClass"
+            Identifier "Touchscreen catchall"
+            MatchIsTouchscreen "on"
+            Option "Ignore" "on"
+        EndSection
+      '';
+      windowManager.session = lib.singleton {
+        name = "exwm";
+        start = "${wpcarrosEmacs}/bin/wpcarros-emacs";
+      };
+    };
+  };
+
+  # Enable sound.
+  sound.enable = true;
+  hardware.pulseaudio.enable = true;
+
+  users.mutableUsers = true;
+  users.users.wpcarro = {
+    isNormalUser = true;
+    extraGroups = [
+      "networkmanager"
+      "wheel"
+      "video" # needed to control the screen brightness
+    ];
+    shell = pkgs.fish;
+  };
+
+  security.sudo.wheelNeedsPassword = false;
+
+  fonts = {
+    fonts = with pkgs; [
+      jetbrains-mono
+    ];
+
+    fontconfig = {
+      defaultFonts = {
+        monospace = [ "JetBrains Mono" ];
+      };
+    };
+  };
+
+  programs = wpcarro.common.programs // {
+    light.enable = true;
+  };
+
+  environment.variables = {
+    EDITOR = "emacsclient";
+    ALTERNATE_EDITOR = "emacs -q -nw";
+    VISUAL = "emacsclient";
+  };
+
+  home-manager.useGlobalPkgs = true;
+  home-manager.users.wpcarro = { config, lib, ... }: {
+    programs.git = {
+      enable = true;
+      userName = "William Carroll";
+      userEmail = "wpcarro@gmail.com";
+      extraConfig = {
+        pull.rebase = true;
+      };
+    };
+
+    services.picom = {
+      enable = true;
+      vSync = true;
+      backend = "glx";
+    };
+
+    services.redshift = {
+      enable = true;
+      latitude = 37.4223931;
+      longitude = -122.0864016;
+    };
+
+    services.dunst.enable = true;
+    xdg.configFile."dunst/dunstrc" = {
+      source = wpcarro.dotfiles.dunstrc;
+      onChange = ''
+        ${pkgs.procps}/bin/pkill -u "$USER" ''${VERBOSE+-e} dunst || true
+      '';
+    };
+
+    systemd.user.startServices = true;
+  };
+
+  environment.systemPackages =
+    wpcarro.common.shell-utils ++
+    (with pkgs; [
+      alacritty
+      firefox
+      pavucontrol
+      quasselClient
+      tdesktop
+      weechat
+      wpcarrosEmacs
+      xsecurelock
+    ]);
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/wpcarro/nixos/marcus/hardware.nix b/users/wpcarro/nixos/marcus/hardware.nix
new file mode 100644
index 0000000000..329926269a
--- /dev/null
+++ b/users/wpcarro/nixos/marcus/hardware.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports = [
+    (modulesPath + "/installer/scan/not-detected.nix")
+  ];
+
+  boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "sd_mod" ];
+  boot.initrd.kernelModules = [ ];
+  boot.kernelModules = [ "kvm-intel" ];
+  boot.extraModulePackages = [ ];
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-label/NIXROOT";
+    fsType = "ext4";
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-label/NIXBOOT";
+    fsType = "vfat";
+  };
+
+  swapDevices = [
+    { device = "/dev/disk/by-uuid/b87e2b8f-c835-4179-a428-fe466a846df0"; }
+  ];
+
+  powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
+  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+}
diff --git a/users/wpcarro/nixos/marcus/marcus.el b/users/wpcarro/nixos/marcus/marcus.el
new file mode 100644
index 0000000000..94dd164a12
--- /dev/null
+++ b/users/wpcarro/nixos/marcus/marcus.el
@@ -0,0 +1,37 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'display)
+(require 'window-manager)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Monitor Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(display-register laptop
+                  :output "eDP-1"
+                  :primary t
+                  :coords (0 0)
+                  :size (1920 1080)
+                  :rate 30.0
+                  :dpi 96
+                  :rotate normal)
+
+(display-arrangement primary :displays (laptop))
+
+(setq window-manager-named-workspaces
+      (list (make-window-manager-named-workspace
+             :label "Web Browsing"
+             :kbd "c"
+             :display display-laptop)
+            (make-window-manager-named-workspace
+             :label "Coding"
+             :kbd "d"
+             :display display-laptop)
+            (make-window-manager-named-workspace
+             :label "Chatting"
+             :kbd "h"
+             :display display-laptop)))
+
+(window-manager-init :init-hook #'display-arrange-primary)
diff --git a/users/wpcarro/nixos/tarasco/default.nix b/users/wpcarro/nixos/tarasco/default.nix
new file mode 100644
index 0000000000..17dd4bc856
--- /dev/null
+++ b/users/wpcarro/nixos/tarasco/default.nix
@@ -0,0 +1,158 @@
+{ depot, pkgs, lib, ... }:
+{ ... }:
+
+let
+  inherit (depot.users) wpcarro;
+
+  wpcarrosEmacs = wpcarro.emacs.nixos {
+    load = [ ./tarasco.el ];
+  };
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+{
+  imports = [ ./hardware.nix ];
+
+  # Use the TVL binary cache
+  tvl.cache.enable = true;
+
+  boot = {
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+
+    # Support IP forwarding to use this device as a Tailscale exit node.
+    kernel.sysctl."net.ipv4.ip_forward" = true;
+    kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
+    kernelModules = [
+      "kvm-intel"
+    ];
+
+    # Can verify these settings with:
+    # $ lsmod
+    # ...or:
+    # $ cat /etc/modprobe.d/nixos.conf
+    blacklistedKernelModules = [
+      # Disabling this buggy network driver (and preferring ethernet) to prevent
+      # my machine from becoming unresponsive.
+      # TODO(wpcarro): Consider replacing this module with this fork (if NixOS
+      # isn't already): https://github.com/tomaspinho/rtl8821ce
+      "rtw88_8821ce"
+    ];
+
+    initrd.availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
+    initrd.kernelModules = [ ];
+    extraModulePackages = [ ];
+  };
+
+
+  time.timeZone = "America/Los_Angeles";
+
+  networking = {
+    useDHCP = false;
+    hostName = "tarasco";
+    networkmanager.enable = true;
+    interfaces.enp1s0.useDHCP = true;
+    interfaces.enp3s0.useDHCP = true;
+    firewall.checkReversePath = "loose";
+    # Disabling wifi because the Realtek network card drivers crash. For more
+    # context, see the boot.blacklistedKernelModules configuration.
+    # interfaces.wlp2s0.useDHCP = true;
+  };
+
+  services = wpcarro.common.services // {
+    # Check the amount of available memory and free swap a few times per second
+    # and kill the largest process if both are below 10%.
+    earlyoom.enable = true;
+
+    tailscale.enable = true;
+
+    openssh.enable = true;
+
+    xserver = {
+      enable = true;
+      layout = "us";
+      xkbOptions = "caps:escape";
+      displayManager = {
+        # Give EXWM permission to control the session (from tazjin's setup).
+        sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localhost:$USER";
+        lightdm.enable = true;
+      };
+      windowManager.session = lib.singleton {
+        name = "exwm";
+        start = "${wpcarrosEmacs}/bin/wpcarros-emacs";
+      };
+    };
+  };
+
+  # Enable sound.
+  sound.enable = true;
+  hardware.pulseaudio.enable = true;
+
+  users.mutableUsers = true;
+  users.users.root.openssh.authorizedKeys.keys = [
+    wpcarro.keys.nathan
+    wpcarro.keys.ava
+  ];
+  users.users.wpcarro = {
+    isNormalUser = true;
+    extraGroups = [
+      "networkmanager"
+      "wheel"
+      "docker"
+    ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = [
+      wpcarro.keys.nathan
+      wpcarro.keys.ava
+    ];
+  };
+  users.extraGroups.vboxusers.members = [ "wpcarro" ];
+
+  security.sudo.wheelNeedsPassword = false;
+
+  fonts = {
+    fonts = with pkgs; [
+      jetbrains-mono
+    ];
+
+    fontconfig = {
+      defaultFonts = {
+        monospace = [ "JetBrains Mono" ];
+      };
+    };
+  };
+
+  programs = wpcarro.common.programs // {
+    mosh.enable = true;
+  };
+
+  virtualisation.docker.enable = true;
+  virtualisation.virtualbox.host.enable = true;
+
+  environment.variables = {
+    EDITOR = "emacsclient";
+    ALTERNATE_EDITOR = "emacs -q -nw";
+    VISUAL = "emacsclient";
+  };
+
+  environment.systemPackages =
+    wpcarro.common.shell-utils ++
+    (with pkgs; [
+      alacritty
+      firefox
+      google-chrome
+      httpie
+      pavucontrol
+      quasselClient
+      remmina
+      tdesktop
+      wpcarrosEmacs
+      xsecurelock
+    ]);
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/wpcarro/nixos/tarasco/hardware.nix b/users/wpcarro/nixos/tarasco/hardware.nix
new file mode 100644
index 0000000000..3d09b9edd8
--- /dev/null
+++ b/users/wpcarro/nixos/tarasco/hardware.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports =
+    [
+      (modulesPath + "/installer/scan/not-detected.nix")
+    ];
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-label/NIXROOT";
+    fsType = "ext4";
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-label/NIXBOOT";
+    fsType = "vfat";
+  };
+
+  swapDevices = [ ];
+
+  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+  # high-resolution display
+  hardware.video.hidpi.enable = lib.mkDefault true;
+}
diff --git a/users/wpcarro/nixos/tarasco/tarasco.el b/users/wpcarro/nixos/tarasco/tarasco.el
new file mode 100644
index 0000000000..c1ecd4409c
--- /dev/null
+++ b/users/wpcarro/nixos/tarasco/tarasco.el
@@ -0,0 +1,57 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'display)
+(require 'window-manager)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Monitor Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(display-register primary
+                  :output "HDMI-1"
+                  :primary t
+                  :coords (0 0)
+                  :size (2560 1440)
+                  :rate 30.0
+                  :dpi 96
+                  :rotate normal)
+
+(display-register secondary
+                  :output "HDMI-2"
+                  :primary nil
+                  :coords (2561 0)
+                  :size (2560 1440)
+                  :rate 30.0
+                  :dpi 96
+                  :rotate normal)
+
+(display-arrangement main :displays (primary secondary))
+
+(setq window-manager-named-workspaces
+      (list (make-window-manager-named-workspace
+             :label "Web Browsing"
+             :kbd "c"
+             :display display-secondary)
+            (make-window-manager-named-workspace
+             :label "Coding I"
+             :kbd "1"
+             :display display-primary)
+            (make-window-manager-named-workspace
+             :label "Coding II"
+             :kbd "2"
+             :display display-primary)
+            (make-window-manager-named-workspace
+             :label "Chatting"
+             :kbd "h"
+             :display display-secondary)))
+
+(window-manager-init :init-hook #'display-arrange-main)
+
+(bookmark-install-kbd
+ (make-bookmark :label "hadrian"
+                :path (f-join tvl-depot-path "/hadrian")
+                :kbd "h"))
+
+(add-to-list 'ssh-hosts "wpcarro@ava")
diff --git a/users/wpcarro/playbooks/README.md b/users/wpcarro/playbooks/README.md
new file mode 100644
index 0000000000..70a26c8e89
--- /dev/null
+++ b/users/wpcarro/playbooks/README.md
@@ -0,0 +1,3 @@
+# playbooks
+
+Here's the vision: playbooks for everything - not just software.
diff --git a/users/wpcarro/playbooks/first-of-the-month.org b/users/wpcarro/playbooks/first-of-the-month.org
new file mode 100644
index 0000000000..7bce39ca76
--- /dev/null
+++ b/users/wpcarro/playbooks/first-of-the-month.org
@@ -0,0 +1,12 @@
+# In total this should take one hour to complete. This is a substantial amount
+# of time, which may disincentivize me from completing it. This time is
+# amortized over the length of its usefulness (i.e. an entire month), so it
+# should be thought of instead as two-minutes worth of work per day that is all
+# being completed upfront.
+* Tasks
+** TODO [10m] create habit template in journal
+** TODO [30m] assess previous month's performance
+** TODO [10m] book massage for the month
+** TODO [10m] create go/hallpass entries (BJJ, VHP)
+** TODO [10m] expense home internet
+** TODO [10m] buy TSLA through tdameritrade.com
diff --git a/users/wpcarro/playbooks/habits.org b/users/wpcarro/playbooks/habits.org
new file mode 100644
index 0000000000..aac63735d9
--- /dev/null
+++ b/users/wpcarro/playbooks/habits.org
@@ -0,0 +1,49 @@
+* First of the year
+** [1hr] Write a post mortem for the previous year
+* First of the month
+** see ./first-of-the-month.org
+* Payday
+** [10m] Audit Monzo expenses
+** [05m] Review "finances_2020" spreadsheet
+** [05m] Transfer GBP to USD account
+** [10m] Withdraw cash from ATM
+* Morning
+** [00m] Wake up at 7:00
+** [15m] Read
+** [02m] Brush teeth
+** [01m] Make bed
+** [01m] Water plants
+** [10m] 12 rounds of forward folds
+** [05m] 12 rounds Pranayama
+** [30m] Transcendental meditation
+** [10m] Shower
+** [05m] Put on clothes
+* Evening
+** [01m] Layout tomorrow's outfit
+** [01m] Floss
+** [02m] Brush teeth
+** [01m] Mouth wash
+** [30m] Read
+** [01m] Journal daily progress
+* Monday
+** [1hr] Jiu Jitsu
+* Tuesday
+** Work from 6PS
+** [1hr] Jiu Jitsu
+* Wednesday
+** [1hr] Hot Yoga
+** [10m] Shave
+** [15m] Clean apartment sinks
+* Thursday
+* Friday
+** [1hr] Hot Yoga
+* Saturday
+** [10m] Vacuum
+** [30m] Nap
+* Sunday
+** [1hr] Jiu Jitsu
+** [30m] Nap
+** [10m] Shave
+** [05m] Trim nails
+** [05m] Take out trash
+** [05m] Laundry
diff --git a/users/wpcarro/playbooks/hip_opening_challenge/poses.pdf b/users/wpcarro/playbooks/hip_opening_challenge/poses.pdf
new file mode 100644
index 0000000000..d292ef832c
--- /dev/null
+++ b/users/wpcarro/playbooks/hip_opening_challenge/poses.pdf
Binary files differdiff --git a/users/wpcarro/playbooks/hip_opening_challenge/progress.org b/users/wpcarro/playbooks/hip_opening_challenge/progress.org
new file mode 100644
index 0000000000..80749a3c6b
--- /dev/null
+++ b/users/wpcarro/playbooks/hip_opening_challenge/progress.org
@@ -0,0 +1,65 @@
+# From Lucas Rockwood's 21-day hip challenge from yogabody.com
+* DONE day 1
+** pigeon
+** butterfly
+* DONE day 2
+** blaster
+** squat
+* DONE day 3
+** happy baby
+** thread the needle (supine)
+* DONE day 4
+** frog
+** jackknife blaster
+* DONE day 5
+** lightning bolt
+** scissors
+* DONE day 6
+** zorro
+** supine butterfly (w/ strap)
+* TODO day 7
+** thread the needle (wall)
+** prone butterfly
+* TODO day 8
+** ninja squat
+** chair scissors
+** lateral chain stretch
+* TODO day 9
+** psoas blaster (chair)
+** reclined scissors
+* DONE day 10
+** twisted blaster
+** twisted squat
+* TODO day 11
+** double pigeon
+** bound butterfly
+* TODO day 12
+** eagle fold
+** cross-thread
+* DONE day 13
+** swiss army knife
+** saddle
+* TODO day 14
+** butterfly squat
+** half lightning bolt
+* DONE day 15
+** fallen blaster
+** asymmetric baby
+* DONE day 16
+** standing psoas
+** standing pigeon
+* TODO day 17
+** marichi B
+** long butterfly
+* TODO day 18
+** eagle legs
+** chair squat
+* DONE day 19
+** twisted pigeon
+** bound baby
+* DONE day 20
+** seated pigeon
+** railroad squat
+* DONE day 21
+** thunderbolt
+** yogi squat
diff --git a/users/wpcarro/playbooks/nix_gcr/README.md b/users/wpcarro/playbooks/nix_gcr/README.md
new file mode 100644
index 0000000000..9d111cf6bb
--- /dev/null
+++ b/users/wpcarro/playbooks/nix_gcr/README.md
@@ -0,0 +1,62 @@
+# Nix + Google Cloud Run (i.e. GCR)
+
+I'm documenting how I currently deploy projects that I package with Nix on
+Google Cloud Run.
+
+I'd like to automate this workflow as much as possible, and I intend to do just
+that. For now, I'm running things manually until I can design an generalization
+that appeals to me.
+
+## Dependencies
+- `nix-build`
+- `docker`
+- `gcloud`
+
+## Step-by-step
+
+1. Use `nix-build` to create our Docker image for Cloud Run.
+
+```shell
+> nix-build ./cloud_run.nix
+```
+
+This outputs a Docker image at `./result`.
+
+1. Load the built image (i.e. `./result`) into `docker` so that we can tag it
+   and push it to the Google Container Registry (i.e. GCR).
+
+```shell
+> sudo docker load <./result
+```
+
+1. (Optionally) Run the image locally to verify its integrity.
+
+```shell
+> sudo docker run -d -p 8080:4242 <name>:<tag>
+```
+
+1. Tag and push the image to GCR.
+
+```shell
+> sudo docker tag <name>:<label> gcr.io/<google-cloud-project-id>/<name>:<latest>
+```
+
+1. Visit Google Cloud Run; create a new service with "Create Service"; select
+   the uploaded Docker image from the "Container Image URL" field; click
+   "Create" to deploy.
+
+## Notes
+
+You may need to authorize `gcloud` by running the following:
+
+```shell
+> sudo gcloud auth login --no-launch-browser
+```
+
+You must use `sudo` here since the `docker` invocations are prefixed with `sudo`
+as well.
+
+## Todos
+
+- If possible, prefer using a command line tool like `gcloud` to create the
+  Cloud Run service.
diff --git a/users/wpcarro/playbooks/nix_gcr/cloud_run.nix b/users/wpcarro/playbooks/nix_gcr/cloud_run.nix
new file mode 100644
index 0000000000..1f473b5f59
--- /dev/null
+++ b/users/wpcarro/playbooks/nix_gcr/cloud_run.nix
@@ -0,0 +1,14 @@
+{ pkgs, depot, ... }:
+
+pkgs.dockerTools.buildLayeredImage {
+  name = "gemma";
+  tag = "latest";
+  config.ExposedPorts = {
+    "4242" = { };
+  };
+  config.Env = [
+    "GEMMA_CONFIG=${./config.lisp}"
+  ];
+  config.Cmd = [ "${depot.fun.gemma}/bin/gemma" ];
+  maxLayers = 120;
+}
diff --git a/users/wpcarro/playbooks/nix_gcr/config.lisp b/users/wpcarro/playbooks/nix_gcr/config.lisp
new file mode 100644
index 0000000000..54f8e5f344
--- /dev/null
+++ b/users/wpcarro/playbooks/nix_gcr/config.lisp
@@ -0,0 +1,21 @@
+;; Example configuration file for Gemma
+
+(config :port 4242
+        :data-dir "/tmp/gemma/")
+
+(deftask bathroom/wipe-mirror 7)
+(deftask bathroom/wipe-counter 7)
+
+;; Bedroom tasks
+(deftask bedroom/change-sheets 7)
+(deftask bedroom/vacuum 10)
+
+;; Kitchen tasks
+(deftask kitchen/normal-trash 3)
+(deftask kitchen/green-trash 5)
+(deftask kitchen/blue-trash 5)
+(deftask kitchen/wipe-counters 3)
+(deftask kitchen/vacuum 5 "Kitchen has more crumbs and such!")
+
+;; Entire place
+(deftask clean-windows 60)
diff --git a/users/wpcarro/playbooks/shell.md b/users/wpcarro/playbooks/shell.md
new file mode 100644
index 0000000000..5eda417f48
--- /dev/null
+++ b/users/wpcarro/playbooks/shell.md
@@ -0,0 +1,12 @@
+# Shell
+
+I'm making this as an offline reference for some of the commands that I use
+often enough to need to remember but not often enough to *actually* remember.
+
+## Reference
+
+- To kill a process by its port number:
+
+```shell
+$ fuser 8080/tcp
+```
diff --git a/users/wpcarro/playbooks/sqlite3.md b/users/wpcarro/playbooks/sqlite3.md
new file mode 100644
index 0000000000..aec87f0b59
--- /dev/null
+++ b/users/wpcarro/playbooks/sqlite3.md
@@ -0,0 +1,115 @@
+# SQLite3
+
+Creating a reference for SQLite that I can access when I'm offline
+(e.g. traveling in an airplane).
+
+## Benefits
+
+I enjoy using SQLite because it's lightweight and simple. Instead of networking
+microservices, I can oftentimes just create a simple `db.sqlite3` file and get
+significant mileage without much tooling overhead.
+
+## Limitations
+
+SQLite has some limitations; here are some of the limitations that I have encountered.
+
+- SQLite **disables** support for `FOREIGN KEY` by default. Enable it with:
+
+```
+sqlite> PRAGMA foreign_keys = ON;
+```
+
+- SQLite has no `BOOLEAN` type; it uses 0 and 1 instead.
+
+```
+sqlite> SELECT TRUE;
+TRUE
+----------
+1
+sqlite> SELECT FALSE;
+FALSE
+----------
+0
+```
+
+- SQLite has no `DATETIME` type; it uses `TEXT` instead.
+
+```
+sqlite> SELECT datetime('now');
+datetime('now')
+-------------------
+2020-07-26 09:52:32
+```
+
+## Reference
+
+The following should serve as a useful reference for working with SQLite.
+
+### Schema
+
+```sql
+CREATE TABLE IF NOT EXISTS Movies (
+  title TEXT NOT NULL,
+  year INTEGER,
+  PRIMARY KEY (title)
+);
+
+ALTER TABLE Movies ADD COLUMN rating DEFAULT 0.0;
+
+DROP TABLE Movies;
+```
+
+### Queries
+
+The following queries should come in handy as a reference:
+
+```
+sqlite> -- I'm using an intentionally incorrect date here for the subsequent UPDATE.
+sqlite> INSERT INTO Movies (title, year) VALUES ('Toy Story 3', 2100);
+sqlite> SELECT * FROM Movies WHERE year IS NULL;
+sqlite> UPDATE Movies SET year = 2010 WHERE title = 'Toy Story 3';
+sqlite> -- % is like .* in a regex
+sqlite> DELETE FROM Movies WHERE title LIKE 'Toy Story%';
+```
+
+## Command Line
+
+- Create a `~/.sqliterc` file with the following contents:
+
+```
+.mode column
+.headers on
+```
+
+- To start an interactive session:
+
+```shell
+$ sqlite3 db.sqlite3
+```
+
+- To create a SQLite database from a `.sql` file:
+
+```shell
+$ sqlite3 db.sqlite3 <db.sql
+```
+
+- To reload changes to a `.sql` file while in an interactive session:
+
+```
+sqlite> .read db.sql
+```
+
+## Miscellaneous
+
+- For a web-browser-based SQLite viewer, run the following:
+
+```shell
+$ sqlite_web db.sqlite3
+```
+
+- To import a CSV:
+
+```
+sqlite> .mode csv <table-name>
+sqlite> .import path/to/file.csv <table-name>
+```
diff --git a/users/wpcarro/scratch/README.md b/users/wpcarro/scratch/README.md
new file mode 100644
index 0000000000..8259ac70d9
--- /dev/null
+++ b/users/wpcarro/scratch/README.md
@@ -0,0 +1,6 @@
+# Scratch
+
+The purpose of the `scratch` directory is to host practice exercises. Practice
+encompasses things like working on data structures and algorithms problems for
+upcoming coding interviews or general aptitude as well as writing code snippets
+to help me learn a new programming language or understand an unfamiliar concept.
diff --git a/users/wpcarro/scratch/advent-of-code-2019/README.md b/users/wpcarro/scratch/advent-of-code-2019/README.md
new file mode 100644
index 0000000000..e7c105a7f6
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/README.md
@@ -0,0 +1,4 @@
+# 2019 Advent of Code
+
+Here are my attempts at the 2019 Advent of Code challenge before my dedication
+to the effort plummeted.
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_1.py b/users/wpcarro/scratch/advent-of-code-2019/day_1.py
new file mode 100644
index 0000000000..bd4024e3ec
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_1.py
@@ -0,0 +1,119 @@
+from math import floor
+
+xs = [
+    102473,
+    84495,
+    98490,
+    68860,
+    62204,
+    72810,
+    65185,
+    145951,
+    77892,
+    108861,
+    70764,
+    67286,
+    74002,
+    80773,
+    52442,
+    131505,
+    107162,
+    126993,
+    59784,
+    64231,
+    91564,
+    68585,
+    98735,
+    69020,
+    77332,
+    60445,
+    65826,
+    111506,
+    95431,
+    146687,
+    135119,
+    86804,
+    95915,
+    85434,
+    111303,
+    148127,
+    132921,
+    136213,
+    89004,
+    143137,
+    144853,
+    143017,
+    104386,
+    100612,
+    54760,
+    63813,
+    144191,
+    84481,
+    69718,
+    84936,
+    98621,
+    124993,
+    92736,
+    60369,
+    137284,
+    101902,
+    112726,
+    51784,
+    126496,
+    85005,
+    101661,
+    137278,
+    136637,
+    90340,
+    100209,
+    53683,
+    50222,
+    132060,
+    98797,
+    139054,
+    135638,
+    100632,
+    137849,
+    125333,
+    103981,
+    76954,
+    134352,
+    74229,
+    93402,
+    62552,
+    50286,
+    57066,
+    98439,
+    120708,
+    117827,
+    107884,
+    72837,
+    148663,
+    125645,
+    61460,
+    120555,
+    142473,
+    106668,
+    58612,
+    58576,
+    143366,
+    90058,
+    121087,
+    89546,
+    126161,
+]
+
+
+def fuel_for_mass(x):
+    """Return the amount of fuel (in mass) required for a mass of X. The total
+    amount of fuel includes the amount of fuel required for the fuel itself,
+    since fuel also has a mass weights."""
+    mass_fuel = floor(x / 3) - 2
+    if mass_fuel < 0:
+        return 0
+    else:
+        fuel_fuel = fuel_for_mass(mass_fuel)
+        return mass_fuel + fuel_fuel
+
+
+print(sum(fuel_for_mass(x) for x in xs))
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_2.py b/users/wpcarro/scratch/advent-of-code-2019/day_2.py
new file mode 100644
index 0000000000..77774c1bb5
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_2.py
@@ -0,0 +1,32 @@
+from itertools import product
+
+x = [
+    1, 0, 0, 3, 1, 1, 2, 3, 1, 3, 4, 3, 1, 5, 0, 3, 2, 1, 10, 19, 1, 6, 19, 23,
+    2, 23, 6, 27, 2, 6, 27, 31, 2, 13, 31, 35, 1, 10, 35, 39, 2, 39, 13, 43, 1,
+    43, 13, 47, 1, 6, 47, 51, 1, 10, 51, 55, 2, 55, 6, 59, 1, 5, 59, 63, 2, 9,
+    63, 67, 1, 6, 67, 71, 2, 9, 71, 75, 1, 6, 75, 79, 2, 79, 13, 83, 1, 83, 10,
+    87, 1, 13, 87, 91, 1, 91, 10, 95, 2, 9, 95, 99, 1, 5, 99, 103, 2, 10, 103,
+    107, 1, 107, 2, 111, 1, 111, 5, 0, 99, 2, 14, 0, 0
+]
+
+
+def interpret(i, x):
+    op, a, b, out = x[i + 0], x[i + 1], x[i + 2], x[i + 3]
+    if op == 1:
+        x[out] = x[a] + x[b]
+        return interpret(i + 4, x)
+    elif op == 2:
+        x[out] = x[a] * x[b]
+        return interpret(i + 4, x)
+    elif op == 99:
+        return x
+    else:
+        raise Exception('Unsupported opcode: {}. {}, {}'.format(op, a, b))
+
+
+for a, b in product(range(100), range(100)):
+    y = x[:]
+    y[1] = a
+    y[2] = b
+    if interpret(0, y)[0] == 19690720:
+        print(100 * a + b)
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_3.py b/users/wpcarro/scratch/advent-of-code-2019/day_3.py
new file mode 100644
index 0000000000..6dd863528c
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_3.py
@@ -0,0 +1,137 @@
+from math import floor
+from heapq import heappush, heappop
+
+xs = [
+    "R1009", "U993", "L383", "D725", "R163", "D312", "R339", "U650", "R558",
+    "U384", "R329", "D61", "L172", "D555", "R160", "D972", "L550", "D801",
+    "L965", "U818", "L123", "D530", "R176", "D353", "L25", "U694", "L339",
+    "U600", "L681", "D37", "R149", "D742", "R762", "U869", "R826", "U300",
+    "L949", "U978", "L303", "U361", "R136", "D343", "L909", "U551", "R745",
+    "U913", "L566", "D292", "R820", "U886", "R205", "D431", "L93", "D71",
+    "R577", "U872", "L705", "U510", "L698", "U963", "R607", "U527", "L669",
+    "D543", "R690", "U954", "L929", "D218", "R490", "U500", "L589", "D332",
+    "R949", "D538", "R696", "U659", "L188", "U468", "L939", "U833", "L445",
+    "D430", "R78", "D303", "R130", "D649", "R849", "D712", "L511", "U745",
+    "R51", "U973", "R799", "U829", "R605", "D771", "L837", "U204", "L414",
+    "D427", "R538", "U116", "R540", "D168", "R493", "U900", "L679", "U431",
+    "L521", "D500", "L428", "U332", "L954", "U717", "L853", "D339", "L88",
+    "U807", "L607", "D496", "L163", "U468", "L25", "U267", "L759", "D898",
+    "L591", "U445", "L469", "U531", "R596", "D486", "L728", "D677", "R350",
+    "D429", "R39", "U568", "R92", "D875", "L835", "D841", "R877", "U178",
+    "L221", "U88", "R592", "U692", "R455", "U693", "L419", "U90", "R609",
+    "U672", "L293", "U168", "R175", "D456", "R319", "D570", "R504", "D165",
+    "L232", "D624", "L604", "D68", "R807", "D59", "R320", "D281", "L371",
+    "U956", "L788", "D897", "L231", "D829", "R287", "D798", "L443", "U194",
+    "R513", "D925", "L232", "U225", "L919", "U563", "R448", "D889", "R661",
+    "U852", "L950", "D558", "L269", "U186", "L625", "U673", "L995", "U732",
+    "R435", "U849", "L413", "D690", "L158", "D234", "R361", "D458", "L271",
+    "U90", "L781", "U754", "R256", "U162", "L842", "U927", "L144", "D62",
+    "R928", "D238", "R473", "U97", "L745", "U303", "L487", "D349", "L520",
+    "D31", "L825", "U385", "L133", "D948", "L39", "U62", "R801", "D664",
+    "L333", "U134", "R692", "U385", "L658", "U202", "L279", "D374", "R489",
+    "D686", "L182", "U222", "R733", "U177", "R94", "D603", "L376", "U901",
+    "R216", "D851", "L155", "D214", "L460", "U758", "R121", "D746", "L180",
+    "U175", "L943", "U146", "L166", "D251", "L238", "U168", "L642", "D341",
+    "R281", "U182", "R539", "D416", "R553", "D67", "L748", "U272", "R257",
+    "D869", "L340", "U180", "R791", "U138", "L755", "D976", "R731", "U713",
+    "R602", "D284", "L258", "U176", "R509", "U46", "R935", "U576", "R96",
+    "U89", "L913", "U703", "R833"
+]
+ys = [
+    "L1006", "D998", "R94", "D841", "R911", "D381", "R532", "U836", "L299",
+    "U237", "R781", "D597", "L399", "D800", "L775", "D405", "L485", "U636",
+    "R589", "D942", "L878", "D779", "L751", "U711", "L973", "U410", "L151",
+    "U15", "L685", "U417", "L106", "D648", "L105", "D461", "R448", "D743",
+    "L589", "D430", "R883", "U37", "R155", "U350", "L421", "U23", "R337",
+    "U816", "R384", "D671", "R615", "D410", "L910", "U914", "L579", "U385",
+    "R916", "U13", "R268", "D519", "R289", "U410", "L389", "D885", "L894",
+    "U734", "L474", "U707", "L72", "U155", "L237", "U760", "L127", "U806",
+    "L15", "U381", "L557", "D727", "L569", "U320", "L985", "D452", "L8",
+    "D884", "R356", "U732", "L672", "D458", "L485", "U402", "L238", "D30",
+    "R644", "U125", "R753", "U183", "L773", "U487", "R849", "U210", "L164",
+    "D808", "L595", "D668", "L340", "U785", "R313", "D72", "L76", "D263",
+    "R689", "U604", "R471", "U688", "R462", "D915", "R106", "D335", "R869",
+    "U499", "R190", "D916", "R468", "D882", "R56", "D858", "L143", "D741",
+    "L386", "U856", "R50", "U853", "R151", "D114", "L773", "U854", "L290",
+    "D344", "L23", "U796", "L531", "D932", "R314", "U960", "R643", "D303",
+    "L661", "D493", "L82", "D491", "L722", "U848", "L686", "U4", "L985",
+    "D509", "L135", "D452", "R500", "U105", "L326", "D101", "R222", "D944",
+    "L645", "D362", "L628", "U305", "L965", "U356", "L358", "D137", "R787",
+    "U728", "R967", "U404", "R18", "D928", "L695", "D965", "R281", "D597",
+    "L791", "U731", "R746", "U163", "L780", "U41", "L255", "U81", "L530",
+    "D964", "R921", "D297", "R475", "U663", "L226", "U623", "L984", "U943",
+    "L143", "U201", "R926", "U572", "R343", "U839", "R764", "U751", "R128",
+    "U939", "R987", "D108", "R474", "U599", "R412", "D248", "R125", "U797",
+    "L91", "D761", "L840", "U290", "L281", "U779", "R650", "D797", "R185",
+    "D320", "L25", "U378", "L696", "U332", "R75", "D620", "L213", "D667",
+    "R558", "U267", "L846", "U306", "R939", "D220", "R311", "U827", "R345",
+    "U534", "R56", "D679", "R48", "D845", "R898", "U8", "R862", "D960", "R753",
+    "U319", "L886", "D795", "R805", "D265", "R876", "U729", "R894", "D368",
+    "R858", "U744", "R506", "D327", "L903", "U919", "L721", "U507", "L463",
+    "U753", "R775", "D719", "R315", "U128", "R17", "D376", "R999", "D386",
+    "L259", "U181", "L162", "U605", "L265", "D430", "R35", "D968", "R207",
+    "U466", "R796", "D667", "R93", "U749", "L315", "D410", "R312", "U929",
+    "L923", "U260", "R638"
+]
+
+
+def to_coords(xs):
+    row, col = 0, 0
+    coords = []
+    for x in xs:
+        d, amt = x[0], int(x[1:])
+        if d == 'U':
+            for i in range(1, amt + 1):
+                coords.append((row + i, col))
+            row += amt
+        elif d == 'D':
+            for i in range(1, amt + 1):
+                coords.append((row - i, col))
+            row -= amt
+        elif d == 'L':
+            for i in range(1, amt + 1):
+                coords.append((row, col - i))
+            col -= amt
+        elif d == 'R':
+            for i in range(1, amt + 1):
+                coords.append((row, col + i))
+            col += i
+    return coords
+
+
+def contains(row, col, d):
+    if row not in d:
+        return False
+    return col in d[row]
+
+
+def intersections(xs, ys):
+    d = {}
+    ints = set()
+    for row, col in to_coords(xs):
+        if row in d:
+            d[row].add(col)
+        else:
+            d[row] = {col}
+    for row, col in to_coords(ys):
+        if contains(row, col, d):
+            ints.add((row, col))
+    return ints
+
+
+def trace_to(coord, xs):
+    count = 0
+    for coord_x in to_coords(xs):
+        count += 1
+        if coord_x == coord:
+            return count
+    raise Exception("Intersection doesn't exist")
+
+
+answer = []
+for coord in intersections(xs, ys):
+    x = trace_to(coord, xs)
+    y = trace_to(coord, ys)
+    heappush(answer, x + y)
+
+print(heappop(answer))
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_4.py b/users/wpcarro/scratch/advent-of-code-2019/day_4.py
new file mode 100644
index 0000000000..adef73b452
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_4.py
@@ -0,0 +1,35 @@
+import re
+
+start = 134792
+end = 675810
+
+
+def satisfies(x):
+    x = str(x)
+    result = False
+    double, not_decreasing = False, False
+
+    # double and *only* double exists
+    for i in range(len(x) - 1):
+        # double and left-of-a  is BOL or !x
+        #        and right-of-b is EOL or !x
+        a, b = x[i], x[i + 1]
+        bol = i - 1 < 0
+        eol = i + 2 >= len(x)
+        if a == b and (bol or x[i - 1] != a) and (eol or x[i + 2] != a):
+            double = True
+            break
+
+    # not_decreasing
+    prev = int(x[0])
+    for a in x[1:]:
+        a = int(a)
+        if prev > a:
+            return False
+        prev = a
+    not_decreasing = True
+
+    return double and not_decreasing
+
+
+print(len([x for x in range(start, end + 1) if satisfies(x)]))
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_5.py b/users/wpcarro/scratch/advent-of-code-2019/day_5.py
new file mode 100644
index 0000000000..3d82846e61
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_5.py
@@ -0,0 +1,170 @@
+x = [
+    3, 225, 1, 225, 6, 6, 1100, 1, 238, 225, 104, 0, 1102, 31, 68, 225, 1001,
+    13, 87, 224, 1001, 224, -118, 224, 4, 224, 102, 8, 223, 223, 1001, 224, 7,
+    224, 1, 223, 224, 223, 1, 174, 110, 224, 1001, 224, -46, 224, 4, 224, 102,
+    8, 223, 223, 101, 2, 224, 224, 1, 223, 224, 223, 1101, 13, 60, 224, 101,
+    -73, 224, 224, 4, 224, 102, 8, 223, 223, 101, 6, 224, 224, 1, 224, 223,
+    223, 1101, 87, 72, 225, 101, 47, 84, 224, 101, -119, 224, 224, 4, 224,
+    1002, 223, 8, 223, 1001, 224, 6, 224, 1, 223, 224, 223, 1101, 76, 31, 225,
+    1102, 60, 43, 225, 1102, 45, 31, 225, 1102, 63, 9, 225, 2, 170, 122, 224,
+    1001, 224, -486, 224, 4, 224, 102, 8, 223, 223, 101, 2, 224, 224, 1, 223,
+    224, 223, 1102, 29, 17, 224, 101, -493, 224, 224, 4, 224, 102, 8, 223, 223,
+    101, 1, 224, 224, 1, 223, 224, 223, 1102, 52, 54, 225, 1102, 27, 15, 225,
+    102, 26, 113, 224, 1001, 224, -1560, 224, 4, 224, 102, 8, 223, 223, 101, 7,
+    224, 224, 1, 223, 224, 223, 1002, 117, 81, 224, 101, -3645, 224, 224, 4,
+    224, 1002, 223, 8, 223, 101, 6, 224, 224, 1, 223, 224, 223, 4, 223, 99, 0,
+    0, 0, 677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1105, 0, 99999, 1105, 227, 247,
+    1105, 1, 99999, 1005, 227, 99999, 1005, 0, 256, 1105, 1, 99999, 1106, 227,
+    99999, 1106, 0, 265, 1105, 1, 99999, 1006, 0, 99999, 1006, 227, 274, 1105,
+    1, 99999, 1105, 1, 280, 1105, 1, 99999, 1, 225, 225, 225, 1101, 294, 0, 0,
+    105, 1, 0, 1105, 1, 99999, 1106, 0, 300, 1105, 1, 99999, 1, 225, 225, 225,
+    1101, 314, 0, 0, 106, 0, 0, 1105, 1, 99999, 8, 226, 677, 224, 102, 2, 223,
+    223, 1005, 224, 329, 1001, 223, 1, 223, 1108, 677, 226, 224, 102, 2, 223,
+    223, 1006, 224, 344, 101, 1, 223, 223, 108, 677, 226, 224, 102, 2, 223,
+    223, 1006, 224, 359, 101, 1, 223, 223, 7, 677, 226, 224, 102, 2, 223, 223,
+    1005, 224, 374, 101, 1, 223, 223, 1007, 226, 677, 224, 102, 2, 223, 223,
+    1005, 224, 389, 101, 1, 223, 223, 8, 677, 677, 224, 102, 2, 223, 223, 1006,
+    224, 404, 1001, 223, 1, 223, 1007, 677, 677, 224, 1002, 223, 2, 223, 1006,
+    224, 419, 101, 1, 223, 223, 1108, 677, 677, 224, 1002, 223, 2, 223, 1005,
+    224, 434, 1001, 223, 1, 223, 1107, 226, 677, 224, 102, 2, 223, 223, 1005,
+    224, 449, 101, 1, 223, 223, 107, 226, 226, 224, 102, 2, 223, 223, 1006,
+    224, 464, 101, 1, 223, 223, 1108, 226, 677, 224, 1002, 223, 2, 223, 1005,
+    224, 479, 1001, 223, 1, 223, 7, 677, 677, 224, 102, 2, 223, 223, 1006, 224,
+    494, 1001, 223, 1, 223, 1107, 677, 226, 224, 102, 2, 223, 223, 1005, 224,
+    509, 101, 1, 223, 223, 107, 677, 677, 224, 1002, 223, 2, 223, 1006, 224,
+    524, 101, 1, 223, 223, 1008, 677, 677, 224, 1002, 223, 2, 223, 1006, 224,
+    539, 101, 1, 223, 223, 7, 226, 677, 224, 1002, 223, 2, 223, 1005, 224, 554,
+    101, 1, 223, 223, 108, 226, 226, 224, 1002, 223, 2, 223, 1006, 224, 569,
+    101, 1, 223, 223, 1008, 226, 677, 224, 102, 2, 223, 223, 1005, 224, 584,
+    101, 1, 223, 223, 8, 677, 226, 224, 1002, 223, 2, 223, 1005, 224, 599, 101,
+    1, 223, 223, 1007, 226, 226, 224, 1002, 223, 2, 223, 1005, 224, 614, 101,
+    1, 223, 223, 1107, 226, 226, 224, 1002, 223, 2, 223, 1006, 224, 629, 101,
+    1, 223, 223, 107, 677, 226, 224, 1002, 223, 2, 223, 1005, 224, 644, 1001,
+    223, 1, 223, 1008, 226, 226, 224, 1002, 223, 2, 223, 1006, 224, 659, 101,
+    1, 223, 223, 108, 677, 677, 224, 1002, 223, 2, 223, 1005, 224, 674, 1001,
+    223, 1, 223, 4, 223, 99, 226
+]
+
+# Interpretter spec:
+# Op-code width: 2
+# ABCDE
+# A:  Mode of 3rd parameter
+# B:  Mode of 2rd parameter
+# C:  Mode of 1st parameter
+# DE: 2-digit op-code
+#
+# Not every op-code has the same arity.
+#
+# Parameter modes:
+# - positional: index of memory. 0
+# - immediate: raw value. 1
+# Assert that you never attempt to write to an "immediate value"
+
+# Parameter modes
+POS = '0'  # positional parameter mode
+VAL = '1'  # immediate parameter mode
+
+
+# Pasted from day-2.py
+# interpretter :: Int -> [Int] -> [Int] -> IO ()
+def interpret(i, x, argv=[], outs=[]):
+    """Values in `argv` will be applied to any `input` fields."""
+    # The widest op-code we'll see is 3 + 2 = 5 for either addition or
+    # multiplication since each of those is a 3-arity function with a two-digit
+    # op-code.
+    instruction = '{:05d}'.format(x[i])
+    op = instruction[-2:]
+
+    if op == '01':
+        a, b, out = x[i + 1], x[i + 2], x[i + 3]
+        mode_a, mode_b, mode_out = instruction[2], instruction[1], instruction[
+            0]
+        a = a if mode_a == VAL else x[a]
+        b = b if mode_b == VAL else x[b]
+        assert mode_out == POS
+        x[out] = a + b
+        return interpret(i + 4, x, argv=argv, outs=outs)
+    elif op == '02':
+        a, b, out = x[i + 1], x[i + 2], x[i + 3]
+        mode_a, mode_b, mode_out = instruction[2], instruction[1], instruction[
+            0]
+        a = a if mode_a == VAL else x[a]
+        b = b if mode_b == VAL else x[b]
+        assert mode_out == POS
+        x[out] = a * b
+        return interpret(i + 4, x, argv=argv, outs=outs)
+    # input
+    elif op == '03':
+        a = x[i + 1]
+        mode_a = instruction[2]
+        assert mode_a == POS
+        # What's the pythonic way to defensively get this value?
+        if len(argv) and argv[0] is not None:
+            x[a] = argv[0]
+            return interpret(i + 2, x, argv=argv[1:], outs=outs)
+        elif len(outs) and outs[-1] is not None:
+            x[a] = outs[-1]
+            return interpret(i + 2, x, argv=argv, outs=outs)
+        else:
+            # Here we want to block until the user applies input. This could be
+            # done easily with message passing for something similar.
+            x[a] = int(input('Enter: '))
+            return interpret(i + 2, x, argv=argv)
+    # output
+    elif op == '04':
+        a = x[i + 1]
+        mode_a = instruction[2]
+        a = a if mode_a == VAL else x[a]
+        outs.append(a)
+        return interpret(i + 2, x, argv=argv, outs=outs)
+    # jump-if-true
+    elif op == '05':
+        a, b = x[i + 1], x[i + 2]
+        mode_a, mode_b = instruction[2], instruction[1]
+        a = a if mode_a == VAL else x[a]
+        b = b if mode_b == VAL else x[b]
+        if a != 0:
+            return interpret(b, x, argv=argv, outs=outs)
+        else:
+            return interpret(i + 3, x, argv=argv, outs=outs)
+    # jump-if-false
+    elif op == '06':
+        a, b = x[i + 1], x[i + 2]
+        mode_a, mode_b = instruction[2], instruction[1]
+        a = a if mode_a == VAL else x[a]
+        b = b if mode_b == VAL else x[b]
+        if a == 0:
+            return interpret(b, x, argv=argv, outs=outs)
+        else:
+            return interpret(i + 3, x, argv=argv, outs=outs)
+        pass
+    # less than
+    elif op == '07':
+        a, b, out = x[i + 1], x[i + 2], x[i + 3]
+        mode_a, mode_b, mode_out = instruction[2], instruction[1], instruction[
+            0]
+        a = a if mode_a == VAL else x[a]
+        b = b if mode_b == VAL else x[b]
+        assert mode_out == POS
+        if a < b:
+            x[out] = 1
+        else:
+            x[out] = 0
+        return interpret(i + 4, x, argv=argv, outs=outs)
+    # equals
+    elif op == '08':
+        a, b, out = x[i + 1], x[i + 2], x[i + 3]
+        mode_a, mode_b, mode_out = instruction[2], instruction[1], instruction[
+            0]
+        a = a if mode_a == VAL else x[a]
+        b = b if mode_b == VAL else x[b]
+        assert mode_out == POS
+        if a == b:
+            x[out] = 1
+        else:
+            x[out] = 0
+        return interpret(i + 4, x, argv=argv, outs=outs)
+    elif op == '99':
+        return x[0]
+    else:
+        raise Exception('Unsupported opcode: {}.'.format(op))
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_6.py b/users/wpcarro/scratch/advent-of-code-2019/day_6.py
new file mode 100644
index 0000000000..aba99b8239
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_6.py
@@ -0,0 +1,155 @@
+from graphviz import Digraph
+
+data = """6WF)DRK 2PT)PSM H42)FN8 1XR)LQD HRK)9KL TD6)H8W 98Z)BJM RCQ)LVG
+RWQ)Q7H 2PS)X94 NHB)25X PXC)W57 L8L)MVX CFK)D8K R1B)43T PDY)QKX FQK)82K JJ6)MQJ
+FB6)6V1 R28)5MZ BN2)5HN 6BQ)JVC W57)22C MQJ)DL2 MTC)84R RH8)CRN Y27)3GN CKQ)31C
+R7V)9BK ZDY)PDY X2Q)Y6S Q8B)SAN 1Z3)PVT R87)57R KCJ)44X PWQ)9CB HLC)VYW HFP)9XS
+X33)MC3 RYS)R7R JRF)VHW 79R)FXZ YQQ)STV 8J6)JWX Q6D)RV6 LL9)B4D 6R1)T1Z VK9)42M
+PQP)17N K6C)HMK GLY)N47 KDW)CDC DQ4)RY5 SND)FDR 7YF)1VN MDT)B3S D3F)98Z 5VH)MR7
+KNR)2L8 CJW)QDL FWY)14X SJD)79R COM)BXW T2B)FPB B2Q)BRJ Z21)HYC VHW)5XR WZ4)2JM
+8HF)342 PYR)X9Y RKF)P43 S1S)9WT 2PB)BSB QF7)M9T HML)HMC 7J9)7Q6 8F1)29K DH1)NDM
+1YC)PXC P32)HR7 PMX)7Y9 STV)SLW NYY)NF1 TG9)998 DMB)DLW XGL)1Z3 GK8)WCS YHR)HQC
+9Q5)B6D R2T)CM5 6KC)J5G ZM9)L8L J8T)F89 3LN)YOU T2T)Z8F SCY)FKG 9W4)195 QLM)DD7
+4QY)JCB WKM)3JF 693)YM8 61M)B6Y DSP)X2M YZ5)DPL BC9)3B1 BDB)JTG 3TJ)TW1 W5M)SF6
+K4Q)X56 5HT)YHX YJG)DM5 68N)X2Q 2YP)DS5 BLK)MY3 6WV)VZ4 2JQ)ZT8 G93)V2W WN1)SBD
+SS7)DY9 X56)8HP JY1)VS4 XQ6)L94 98Z)DMC V6S)NWT D9L)Y44 V6G)GVS JDW)FZW FJT)S38
+L2Z)VPL 7ZX)DKK X2M)8WM YVZ)XWS HMK)P87 47M)TD6 TDZ)21T 19R)95B GD9)Q1L 9QX)DFR
+Y64)XGN CRG)6VY V3L)61D RJ4)C9Z XXG)P53 VJ8)QTF CPQ)2M9 JRN)8V1 KMH)K94 DLW)VQ4
+91W)2QQ G4B)RWQ 4P1)MKS K6G)DZ7 WCS)JR9 LXM)7RY 6ZB)K6G HMC)622 Z21)BLK Q6N)48V
+66S)MK4 PDK)6WV Y6S)GY1 2L8)ZMG 42W)ZN6 6MS)8TZ JBY)STQ NSF)3ZM 5CV)X9N K4V)WFL
+J6R)DT8 N3N)CX4 PTD)YXT F74)4T5 C51)3FW KRW)DS1 NWT)CKQ 195)6G6 HVQ)S18 Q7H)BKM
+SKN)4D4 GK2)MLX MVX)TG9 YPK)RHQ Y9F)Z8W 42M)WNL 84R)6JP KNC)NHF FZW)PGM 3FW)HGX
+DBK)FB6 45T)HLT L11)JVN HB5)K6C QH5)888 BTJ)J55 8BT)8ZS FR1)XGL S87)PS9 C4K)BN2
+N2Q)18C KTF)ZM9 TN2)B2Q DF3)CFK 9T3)TMR P29)3P1 P1W)7SQ 4D4)1DJ LML)ZJ3 Q4L)RKF
+MW2)79T LVG)CPQ BDC)JH5 DNZ)232 998)GTM YGS)4WH GY1)C51 J55)QBT B8Z)34W FJ2)H42
+58J)326 T1Z)DCJ 1ZH)GLV 1YC)JG6 14K)22B RY5)QRY 7V2)2WT 4GQ)XHV ZJ3)TQ8 2G8)SN3
+FPB)HMN SC4)57D 5LQ)R2T LXM)R8Z JQ6)G4B WNL)GK2 42M)P75 LM3)YPK ZN6)753 PN4)835
+C4H)JY1 LR4)VD5 PSM)P1W VWL)C6C G2V)WBC 85M)R24 B1V)QW7 175)2PM Y1V)1ZH 34W)3MJ
+WN7)TTB 3PV)CQD N7Y)9T3 223)8D4 RV6)LJ9 HFP)JRF VMT)DNB GJP)D3F J5G)KMS 7Q6)ZW2
+YCB)JBY XGN)MNL 888)DSP X61)Q6N WT5)X12 SDN)FD1 2QC)54W V98)964 T7S)YVZ MLX)9VZ
+FR8)QH5 TVQ)2PS 2PV)FHY F4S)MPT 3J9)JNB J6M)GDC Q4C)MJN 9VZ)BZK P2P)B69 WBC)M1W
+D97)HPF JKB)9L4 593)6YJ RMB)4Q5 QZB)38C H12)6R1 MKY)DDD HGX)CRG P53)WY7 22B)GMM
+44X)2D8 DT8)L7H 3Y2)D3S FB8)68N 3BC)1XR 4XF)TVQ VPL)R7V Z4V)JSK B3S)FW5 49Z)YQQ
+99V)D13 54Q)SS7 CYC)TXH PQ3)78W X4M)G9H WFL)M99 ZYY)3Y2 12Y)PSW W38)P29 H8W)JJ6
+P66)VPH GK2)45T H5F)FJT JDJ)SNV 14F)96Q JG6)TQ4 2L6)52Q SCY)CBJ 3GN)KNC KLM)XPR
+DH1)QZB DMB)X7G DPL)7SX D97)N3N GNS)T95 53P)GW2 BHR)HNB YHX)XQV 2CR)Y1V C9D)Z7P
+FN8)2PT 6LF)FCQ JNL)LQR SPV)YCB HGX)N83 VS4)8BT 5RH)FTX HYC)X2J 69V)J6S 9XS)PN4
+SD7)5Q3 2RN)82D QRY)FFY K2Y)3X2 79Z)S2Z YN2)Y64 JKB)MDT KJ8)NDH N57)5VH 3XK)1Q1
+SCH)FJ6 17N)GMP QR4)7V2 GLV)GLY NHF)ZDY QDL)S14 QF1)BMC ZLF)DHN 3JF)7TR MKS)GCY
+964)91R 9L4)L5G RRX)6ZB CD7)73M 3X2)PGC HNB)S9Z L94)KLM 8MQ)SCR 18C)3TJ M4Y)BTJ
+BC9)5YR TV5)SCY 2NX)8CC C9Z)MTC B69)3QP HR7)CHJ 8ZS)JRN 31C)TJW D43)4NH 93Q)X9X
+T95)DNZ LQ5)BC9 9T5)S2C RP8)DH1 GCY)SD7 Y44)9B5 VG5)ZYY 7RY)V3L PWV)Q4L NF1)7YF
+DRK)Y8V D13)GYG TW1)2PB ZVZ)2VV BRJ)V2V 9CB)Y7B MK4)9CJ TMR)6XS HWF)GK8 QTF)S1S
+DFW)6LF N3S)WN1 N2Q)MSW CZ5)X61 FXZ)C4H SCQ)MF7 9LY)3LN 5MZ)PMX CN9)WF9 FHY)PR8
+S38)NWH M29)G5S 4NH)GZJ 5YR)54H CLX)MNY TJD)HQL RRZ)4GQ YHB)CZ5 P37)93Q YJG)3Q3
+95B)QMF CMQ)BLZ QD9)45M JSK)R28 YCW)CLX 8K3)JGB N8M)PQW P75)1HL XBS)T2T 22C)PVW
+689)6MS FFY)RWX YHL)2G8 Y8V)4P1 Y7B)62Z YKJ)JDJ 1HL)5LQ PZ3)B1C 52Q)7HB 3Q2)ZV7
+YBF)Z4V J95)SDH NM6)YBF 8YN)J3M J6S)KNR PVT)N4X SDH)RFW RFW)7Y1 JCB)52B 3MJ)H58
+4QF)XHZ F62)DFW 7LJ)KDW JHL)C9D B4D)Q8B 342)YGS PFR)ZQT Z9K)TNS 8F8)WLB 94N)DMB
+QBT)RYS 3VR)KRR 8D4)ST6 X9N)2PV 632)8K3 MX5)XNP 57D)Y27 18D)PQP D3F)RJ4 PLS)PBL
+1JP)YDC 79V)BG2 S14)2NX 4Q5)NCQ FTX)555 2PM)KMH HQC)RMB 9Z9)BNZ XHV)Y94 7ZP)YHR
+BNZ)49Z W6D)LX6 SLS)JL3 PVW)P9W Z1L)HB5 DS5)G2V Z9Q)RV8 DFR)LPJ 836)693 K94)VWL
+HRG)836 J3V)593 52N)LPK 9KL)Y7M LX6)F7D JL3)511 L4G)D97 1RH)Y9F NJ2)LML GW2)9WV
+8KZ)NRC XQV)G6D R8Z)QF7 326)HML R7R)8PM 622)YCW WQY)LGS NF1)FF3 5LQ)QF1 5XR)PTD
+V2V)PFR 9T5)JQ6 CBQ)8KZ VZ4)HVQ TJW)DQT 9WT)5M6 CFK)YHL JR9)1JP Y1K)CF4 8WS)JPY
+VYC)1D6 GKK)7J9 JTG)RRX 6V1)F74 1H5)QR4 SN3)NMG MF7)GQ1 RYK)SCH BNZ)9LY 1DJ)9LP
+L6W)5BK FCQ)BFL DCJ)3RD MXD)8MQ RWX)1RH NBF)WKM K6C)WNH H58)L6W Y7B)BJH PGC)NBF
+96Q)Q2W F7D)BSN 223)Z9K K94)VYC X9X)7M3 Q1M)3J9 QXF)XQ6 DD7)3Q2 Q1L)NHB 79T)LXQ
+8TZ)M29 21T)Q4C B1C)NSF 8D8)FJ2 LJH)HGJ QS2)PS1 5KX)Z2L C6C)6BQ VQ2)2YP P87)N8M
+ST5)L4G 8SP)W5M T4H)69V 9WF)GHS FF3)SND C5G)GKK VQ2)X4M P43)8J6 TD6)384 66V)CN9
+CX4)T9T NCQ)2JQ 29K)K8K RY5)K4Q GQ3)T4H FNH)P32 3BC)PRQ 5HN)4QY M1W)BGT 84R)ST5
+S45)CJW CK4)W7G SGX)19R S2C)7ZX DHN)W5Y 8D9)HM2 BSB)SPV D8K)DFV JHL)2L6 KYP)12Y
+KDN)6X7 Y44)SQZ 6G6)SJD N7D)QGF Q84)8WJ F89)LL9 LYJ)2RN 25X)Q84 HM3)53P JNB)QD9
+SLW)1DQ 384)3BC PR8)NGV 49N)7ZP 65H)LHJ 6XS)S45 ZMG)FR1 X2M)Y86 QD3)QLM P4R)PQ3
+RTK)4M3 4YW)N7D R7V)M4M 73M)CBF DFV)64R Z7P)LMK HRG)Y1K 3ZM)BCZ WY7)QXP DMC)9Q5
+PSW)1H5 8CC)TV5 TTB)S88 BZK)K2Y T2B)CBQ HJB)Y19 DQW)KML Z8W)8ZL PBL)5TK 1D6)MX5
+3MJ)4YW MDT)HJB 62Z)X33 DZ7)BDC 9CJ)FRD 82D)KDN LK7)18D 9QQ)61M Y34)DZG J4T)6KC
+971)QD3 511)GQ3 MJN)F62 RNM)NKG BGW)KJ8 DL2)1YH ZQT)RYZ 1YH)ZJ6 2WT)YYQ 7HB)DYQ
+3BN)WQY 2M9)62D TSK)YR1 N7Y)VJ8 WZ4)FWT MNY)YN2 DYQ)RRZ 3RG)YT3 2SM)VK9 JH5)ZXH
+GYG)K2M PKF)V6G JGB)S87 X94)N57 MSW)L2Z X4N)25G BLZ)4QF JPY)GD9 WLB)V6S KML)2SM
+TXH)9X1 48V)KTR 8PM)WZ4 ZW2)967 PS9)3BN 4WH)9T5 8M1)R6V N7M)VWK S88)978 N4X)8KH
+6VY)PLS NRC)874 QGF)QWJ NMG)J3V B8Z)WPF 45M)2QC KDW)VQ2 FZW)223 BXW)QXF FRD)PWV
+8HP)4G7 KDN)YYL LHJ)SDN P6P)XMC W5Y)RYK HX8)KW3 Z2L)H12 WPF)T2B L7H)BGW MNL)17B
+GHS)66V QKX)XWV FW5)W38 PDK)Y34 FKG)Q6D DQT)YJG 15G)79V 4VK)51Y BJH)LR4 48V)6GC
+DM5)Y1F CM5)VG5 KB8)HRK 5HN)RCQ 6JP)SDQ LGH)NJ2 L94)N7Y 4Y2)ZLF 25G)C4K K8K)SLS
+232)ZVZ GQ1)58J RV8)H5F 78W)565 YCF)8D9 DZG)99V N83)CKR TN2)ZCX NGV)8SP BSN)FTN
+LPJ)94N 3Q3)Q1M JVX)971 54W)LGH 67Y)P66 R24)P37 3QP)QTY YHR)FLT GMP)NM6 NDH)632
+PWV)8D8 LMK)3PV ZWJ)KB8 967)4VK 3B1)WN7 XWS)5CV YR1)FNH 565)4PH 5BK)V98 W5Y)FR8
+PS1)HX8 38C)XXG XWV)1YC M4M)LQ5 S9Z)49N XMC)R1B YYL)VC9 GMM)SCQ LXQ)J95 51Y)RP8
+HLT)XBS 82K)B8Z NR5)7K3 K2M)67Y SF6)W6D CF4)85M MC3)LXM HMN)RNM BFL)4XF MT2)PM4
+VWK)JKB 3JF)ZTZ QWJ)9QQ KRR)TJD VYW)Z9Q CK4)QS2 8NQ)NR5 57R)BHR 8WM)YHB Y86)GNS
+2Y2)Z21 X12)9QX LJ9)YKJ 3RD)8F1 7SQ)CK4 ZXH)3XK DDD)5KX ZCX)PYR GZJ)KXL KC5)52N
+PM4)RYP 14X)ZWJ FJ6)175 17B)689 HQL)14F LQR)DBK LGS)4Y2 2QQ)SGR 2VV)8F8 J6S)LM3
+RTP)YZ5 XDD)14K VQ4)MT2 KMH)KYC CKR)RTP VD5)MRM CM5)KRW BG3)XDD PGM)J4T MY3)JVX
+Z8F)WNP BKM)WT5 FLT)KTF N7D)8M1 Y19)CMQ HPF)WDL 65H)JJP 2MQ)66S 4Q5)54Q Q2W)ZL4
+QTY)659 MRM)9Z9 X2J)SC4 YWH)RB3 FTN)LYJ LMK)N7M SGX)15G KW3)FQK 3VV)JNL JWX)R8R
+9Z3)9MB BMC)N3S W7G)Z1L SD7)MW2 376)RH8 NWT)JHL 7CD)N2Z KTR)HM3 1Q1)TDZ DY9)2CR
+6YJ)14G FWT)JDW C2S)C5G SNV)J6M 5TK)YWH J3M)8HF HM2)GJP P9W)7CD 1VN)SGX KMS)RBK
+64R)B1V 62D)3VV 61D)F4S XPR)SKN FJT)N3P 9WV)D43 TQ8)BDB 46H)K4V 8WJ)MXD NDM)9WF
+8ZL)1QJ SCR)2MQ 7Y9)LJH VPH)MKY YDC)PDK 4G7)65H 2JM)NYY T9T)VMT 8M1)TSK G5S)X4N
+6FH)KYP D98)DQW G6D)C2S 6X7)N2Q 1QJ)T7S ZL4)J8T 5BT)3VR 835)KCJ YM8)3RG Y7M)PWQ
+54W)9W4 CBF)7LJ 4T5)8WS RHQ)HBK CQD)D98 HGJ)J6R JVC)79Z FD1)PKF VC9)5BT C4H)6WF
+D3S)P6P MR7)BG3 R6V)DF3 9X1)NQ5 ZTZ)2Y2 8WM)HFP CDC)376 TQ4)M4Y 9MB)N1R HBK)DQ4
+1DQ)CYC WNP)DM8 CBJ)LK7 ZT8)FWY LQD)PNN 555)9Z3 TNS)D9L QMF)L11 FR8)5RH WF9)R87
+NKG)5HT L5G)91W N2Z)YV9 9B5)CD7 ZV7)8NQ ST6)74T ZJ6)CQV S18)47M 74T)8YN WNH)TN2
+874)46H 3VV)PZ3 Y1F)42W MPT)2LP FDR)HWF X7G)RTK 52B)P4R RYP)G93 NWH)YCF 7TR)FB8
+RWQ)6FH 8F8)HLC CRN)P2P B6D)KC5 PNN)HRG""".split()
+
+# COM is the root in this tree
+
+
+# parent :: Vertex -> [Edge] -> Maybe(Vertex)
+def parent(x, xs):
+    for a, b in xs:
+        if b == x:
+            return a
+    return None
+
+
+# parents :: Vertex -> [Edge] -> [Vertex]
+def parents(x, xs):
+    parents = []
+    p = parent(x, xs)
+    while p:
+        parents.append(p)
+        p = parent(p, xs)
+    return parents
+
+
+# alias Vertex :: String
+# alias Edge :: (String, String)
+# to_edge_list :: [String] -> [(String, String)]
+def to_edge_list(xs):
+    """Returns a list of tuples where (A, B) represents a directed edge from
+    vertex A to vertex B."""
+    return [(x[0:3], x[4:]) for x in xs]
+
+
+# to_graphviz :: [Edge] -> String
+def to_graphviz(xs):
+    d = Digraph()
+    for a, b in xs:
+        d.node(a, label=a)
+        d.edge(a, b)
+    return d.source
+
+
+graph = to_edge_list(data)
+you = parents('YOU', graph)
+san = parents('SAN', graph)
+
+# Distance from YOU to shared point with SAN
+yd = 1
+for i in range(len(you)):
+    if you[i] in san:
+        break
+    yd += 1
+
+# Distance from SAN to shared point with YOU
+sd = 1
+for i in range(len(san)):
+    if san[i] in you:
+        break
+    sd += 1
+
+print('Number of orbital transfers required: {}'.format(yd - 1 + sd - 1))
diff --git a/users/wpcarro/scratch/advent-of-code-2019/day_7.py b/users/wpcarro/scratch/advent-of-code-2019/day_7.py
new file mode 100644
index 0000000000..14597d5104
--- /dev/null
+++ b/users/wpcarro/scratch/advent-of-code-2019/day_7.py
@@ -0,0 +1,49 @@
+from day_5 import interpret
+from itertools import permutations
+
+# TODO: I may need to re-write this in Elixir modelling each amplifier as a
+# `Process` and `Process.send`ing each amplifier the signals.
+
+data = [
+    3, 8, 1001, 8, 10, 8, 105, 1, 0, 0, 21, 38, 59, 76, 89, 106, 187, 268, 349,
+    430, 99999, 3, 9, 1002, 9, 3, 9, 101, 2, 9, 9, 1002, 9, 4, 9, 4, 9, 99, 3,
+    9, 1001, 9, 5, 9, 1002, 9, 5, 9, 1001, 9, 2, 9, 1002, 9, 3, 9, 4, 9, 99, 3,
+    9, 1001, 9, 4, 9, 102, 4, 9, 9, 1001, 9, 3, 9, 4, 9, 99, 3, 9, 101, 4, 9,
+    9, 1002, 9, 5, 9, 4, 9, 99, 3, 9, 1002, 9, 3, 9, 101, 5, 9, 9, 1002, 9, 3,
+    9, 4, 9, 99, 3, 9, 102, 2, 9, 9, 4, 9, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9,
+    1002, 9, 2, 9, 4, 9, 3, 9, 101, 2, 9, 9, 4, 9, 3, 9, 1002, 9, 2, 9, 4, 9,
+    3, 9, 102, 2, 9, 9, 4, 9, 3, 9, 101, 1, 9, 9, 4, 9, 3, 9, 1001, 9, 1, 9, 4,
+    9, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9, 101, 2, 9, 9, 4, 9, 99, 3, 9, 1002, 9,
+    2, 9, 4, 9, 3, 9, 101, 2, 9, 9, 4, 9, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9, 101,
+    1, 9, 9, 4, 9, 3, 9, 102, 2, 9, 9, 4, 9, 3, 9, 102, 2, 9, 9, 4, 9, 3, 9,
+    101, 2, 9, 9, 4, 9, 3, 9, 101, 2, 9, 9, 4, 9, 3, 9, 102, 2, 9, 9, 4, 9, 3,
+    9, 1001, 9, 2, 9, 4, 9, 99, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9, 1001, 9, 2, 9,
+    4, 9, 3, 9, 101, 1, 9, 9, 4, 9, 3, 9, 101, 2, 9, 9, 4, 9, 3, 9, 101, 2, 9,
+    9, 4, 9, 3, 9, 102, 2, 9, 9, 4, 9, 3, 9, 1001, 9, 2, 9, 4, 9, 3, 9, 102, 2,
+    9, 9, 4, 9, 3, 9, 1001, 9, 1, 9, 4, 9, 3, 9, 1001, 9, 2, 9, 4, 9, 99, 3, 9,
+    1001, 9, 2, 9, 4, 9, 3, 9, 102, 2, 9, 9, 4, 9, 3, 9, 1001, 9, 2, 9, 4, 9,
+    3, 9, 102, 2, 9, 9, 4, 9, 3, 9, 101, 2, 9, 9, 4, 9, 3, 9, 1002, 9, 2, 9, 4,
+    9, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9, 101, 1, 9,
+    9, 4, 9, 3, 9, 101, 1, 9, 9, 4, 9, 99, 3, 9, 101, 2, 9, 9, 4, 9, 3, 9, 102,
+    2, 9, 9, 4, 9, 3, 9, 1002, 9, 2, 9, 4, 9, 3, 9, 1001, 9, 2, 9, 4, 9, 3, 9,
+    1001, 9, 2, 9, 4, 9, 3, 9, 1001, 9, 2, 9, 4, 9, 3, 9, 1001, 9, 1, 9, 4, 9,
+    3, 9, 1001, 9, 2, 9, 4, 9, 3, 9, 1001, 9, 2, 9, 4, 9, 3, 9, 102, 2, 9, 9,
+    4, 9, 99
+]
+
+data_a, data_b, data_c, data_d, data_e = data[:], data[:], data[:], data[:], data[:]
+
+# m = 0
+# for a, b, c, d, e in permutations(range(5, 10)):
+#     answer = None
+#     z = 0
+#     while z is not None:
+#         print(a, b, c, d, e)
+#         print('---')
+#         v = interpret(0, data_a, argv=[a, z])
+#         print(v)
+#         w = interpret(0, data_b, argv=[b, v])
+#         x = interpret(0, data_c, argv=[c, w])
+#         y = interpret(0, data_d, argv=[d, x])
+#         z = interpret(0, data_e, argv=[e, y])
+#         m = max(m, z)
diff --git a/users/wpcarro/scratch/blockchain/default.nix b/users/wpcarro/scratch/blockchain/default.nix
new file mode 100644
index 0000000000..c02f9a9c81
--- /dev/null
+++ b/users/wpcarro/scratch/blockchain/default.nix
@@ -0,0 +1,14 @@
+{ pkgs, ... }:
+
+let
+  pypkgs = pkgs.python3Packages;
+in
+pkgs.python3Packages.buildPythonApplication {
+  pname = "main";
+  src = ./.;
+  version = "0.0.1";
+  propagatedBuildInputs = with pypkgs; [
+    flask
+    requests
+  ];
+}
diff --git a/users/wpcarro/scratch/blockchain/main.py b/users/wpcarro/scratch/blockchain/main.py
new file mode 100644
index 0000000000..e7b6276133
--- /dev/null
+++ b/users/wpcarro/scratch/blockchain/main.py
@@ -0,0 +1,263 @@
+from flask import Flask, jsonify, request
+from hashlib import sha256
+from datetime import datetime
+from urllib.parse import urlparse
+
+import json
+import requests
+import uuid
+
+################################################################################
+# Helper Functions
+################################################################################
+
+def hash(x):
+  return sha256(x).hexdigest()
+
+def is_pow_valid(guess, prev_proof):
+  """
+  Return true if the hash of `guess` + `prev_proof` has 4x leading zeros.
+  """
+  return hash(str(guess + prev_proof).encode("utf8"))[:4] == "0000"
+
+################################################################################
+# Classes
+################################################################################
+
+class Node(object):
+  def __init__(self, host="0.0.0.0", port=8000):
+    self.app = Flask(__name__)
+    self.define_api()
+    self.identifier = str(uuid.uuid4())
+    self.blockchain = Blockchain()
+    self.neighbors = set()
+
+  def add_neighbors(self, urls=None):
+    for url in urls:
+      parsed = urlparse(url)
+      if not parsed.netloc:
+        raise ValueError("Must pass valid URLs for neighbors")
+      self.neighbors.add(parsed.netloc)
+
+  def decode_chain(chain_json):
+    return Blockchain(
+        blocks=[
+            Block(
+                index=block["index"],
+                ts=block["ts"],
+                transactions=[
+                    Transaction(
+                        origin=tx["origin"],
+                        target=tx["target"],
+                        amount=tx["amount"])
+                        for tx in block["ts"]
+                ],
+                proof=block["proof"],
+                prev_hash=block["prev_hash"])
+                for block in chain_json["blocks"]
+        ],
+        transactions=[
+            Transaction(
+                origin=tx["origin"],
+                target=tx["target"],
+                amount=tx["amount"])
+                for tx in chain_json["transactions"]
+        ])
+
+  def resolve_conflicts(self):
+    auth_chain, auth_length = self.blockchain, len(self.blockchain)
+
+    for neighbor in self.neighbors:
+      res = requests.get(f"http://{neighbor}/chain")
+      if res.status_code == 200 and res.json()["length"] > auth_length:
+         decoded_chain = decode_chain(res.json()["chain"])
+         if Blockchain.is_valid(decoded_chain):
+           auth_length = res.json()["length"]
+           auth_chain = decoded_chain
+
+      self.blockchain = auth_chain
+
+  def define_api(self):
+    def msg(x):
+      return jsonify({"message": x})
+
+    ############################################################################
+    # /
+    ############################################################################
+
+    @self.app.route("/healthz", methods={"GET"})
+    def healthz():
+      return "ok"
+
+    @self.app.route("/reset", methods={"GET"})
+    def reset():
+      self.blockchain = Blockchain()
+      return msg("Success")
+
+    @self.app.route("/mine", methods={"GET"})
+    def mine():
+      # calculate POW
+      proof = self.blockchain.prove_work()
+
+      # reward miner
+      self.blockchain.add_transaction(
+          origin="0", # zero signifies that this is a newly minted coin
+          target=self.identifier,
+          amount=1)
+
+      # publish new block
+      self.blockchain.add_block(proof=proof)
+      return msg("Success")
+
+    ############################################################################
+    # /transactions
+    ############################################################################
+
+    @self.app.route("/transactions/new", methods={"POST"})
+    def new_transaction():
+      payload = request.get_json()
+
+      self.blockchain.add_transaction(
+          origin=payload["origin"],
+          target=payload["target"],
+          amount=payload["amount"])
+      return msg("Success")
+
+    ############################################################################
+    # /blocks
+    ############################################################################
+
+    @self.app.route("/chain", methods={"GET"})
+    def view_blocks():
+      return jsonify({
+          "length": len(self.blockchain),
+          "chain": self.blockchain.dictify(),
+      })
+
+    ############################################################################
+    # /nodes
+    ############################################################################
+    @self.app.route("/node/neighbors", methods={"GET"})
+    def view_neighbors():
+      return jsonify({"neighbors": list(self.neighbors)})
+
+    @self.app.route("/node/register", methods={"POST"})
+    def register_nodes():
+      payload = request.get_json()["neighbors"]
+      payload = set(payload) if payload else set()
+      self.add_neighbors(payload)
+      return msg("Success")
+
+    @self.app.route("/node/resolve", methods={"GET"})
+    def resolve_nodes():
+      self.resolve_conflicts()
+      return msg("Success")
+
+  def run(self):
+    self.app.run(host="0.0.0.0", port=8000)
+
+
+class Blockchain(object):
+  def __init__(self, blocks=None, transactions=None):
+    self.blocks = blocks or []
+    self.transactions = transactions or []
+    self.add_block()
+
+  def __len__(self):
+    return len(self.blocks)
+
+  def __iter__(self):
+    for block in self.blocks:
+      yield block
+
+  def prove_work(self):
+    guess, prev_proof = 0, self.blocks[-1].proof or 0
+    while not is_pow_valid(guess, prev_proof):
+      guess += 1
+    return guess
+
+  def add_block(self, prev_hash=None, proof=None):
+    b = Block(
+        index=len(self),
+        transactions=self.transactions,
+        prev_hash=self.blocks[-1].hash() if self.blocks else None,
+        proof=proof)
+    self.blocks.append(b)
+    return b
+
+  def adopt_blocks(self, json_blocks):
+    pass
+
+  def add_transaction(self, origin=None, target=None, amount=None):
+    tx = Transaction(origin=origin, target=target, amount=amount)
+    self.transactions.append(tx)
+
+  @staticmethod
+  def is_valid(chain):
+    prev_block = next(chain)
+
+    for block in chain:
+      if block.prev_hash != prev_block.hash() or not is_pow_valid(prev_block.proof, block.proof):
+        return False
+      prev_block = block
+
+    return True
+
+  def dictify(self):
+    return {
+        "blocks": [block.dictify() for block in self.blocks],
+        "transactions": [tx.dictify() for tx in self.transactions],
+    }
+
+
+class Block(object):
+  def __init__(self, index=None, ts=None, transactions=None, proof=None, prev_hash=None):
+    self.index = index
+    self.ts = ts or str(datetime.now())
+    self.transactions = transactions
+    self.proof = proof
+    self.prev_hash = prev_hash
+
+  def hash(self):
+    return sha256(self.jsonify().encode()).hexdigest()
+
+  def dictify(self):
+    return {
+        "index": self.index,
+        "ts": self.ts,
+        "transactions": [tx.dictify() for tx in self.transactions],
+        "proof": self.proof,
+        "prev_hash": self.prev_hash,
+    }
+
+  def jsonify(self):
+    return json.dumps(self.dictify(), sort_keys=True)
+
+class Transaction(object):
+  def __init__(self, origin=None, target=None, amount=None):
+    if None in {origin, target, amount}:
+      raise ValueError("To create a Transaction, you must provide origin, target, and amount")
+
+    self.origin = origin
+    self.target = target
+    self.amount = amount
+
+  def dictify(self):
+    return {
+        "origin": self.origin,
+        "target": self.target,
+        "amount": self.amount,
+    }
+
+  def jsonify(self):
+    return json.dumps(self.dictify(), sort_keys=True)
+
+################################################################################
+# Main
+################################################################################
+
+def run():
+  Node(host="0.0.0.0", port=8000).run()
+
+if __name__ == "__main__":
+  run()
diff --git a/users/wpcarro/scratch/blockchain/setup.py b/users/wpcarro/scratch/blockchain/setup.py
new file mode 100644
index 0000000000..e5310565db
--- /dev/null
+++ b/users/wpcarro/scratch/blockchain/setup.py
@@ -0,0 +1,10 @@
+from setuptools import setup
+
+setup(
+    name='main',
+    version='0.0.1',
+    py_modules=['main'],
+    entry_points={
+      'console_scripts': ['main = main:run']
+    },
+)
diff --git a/users/wpcarro/scratch/crack_the_coding_interview/11_1.py b/users/wpcarro/scratch/crack_the_coding_interview/11_1.py
new file mode 100644
index 0000000000..ec7b65dae0
--- /dev/null
+++ b/users/wpcarro/scratch/crack_the_coding_interview/11_1.py
@@ -0,0 +1,40 @@
+# Implementation for a problem from "Crack the Coding Interview".
+#
+# Dependencies:
+# - python 2.7.16
+# - entr 4.1
+#
+# To run the tests, run: `python 11_1.py`
+# For a tight development loop, run: `echo 11_1.py | entr python /_`
+#
+# Author: William Carroll <wpcarro@gmail.com>
+
+################################################################################
+# Implementation
+################################################################################
+def insert_sorted(xs, ys):
+    """
+    Merges `ys` into `xs` and ensures that the result is sorted.
+
+    Assumptions:
+    - `xs` and `ys` are both sorted.
+    - `xs` has enough unused space to accommodate each element in `ys`.
+    """
+    for y in ys:
+        xi = xs.index(None) - 1
+        yi = xs.index(None)
+        xs[yi] = y
+        while xi != -1 and y < xs[xi]:
+            xs[xi], xs[yi] = xs[yi], xs[xi]
+            xi, yi = xi - 1, yi - 1
+    return xs
+
+################################################################################
+# Tests
+################################################################################
+assert insert_sorted([1, 3, 5, None, None], [2, 4]) == [1, 2, 3, 4, 5]
+assert insert_sorted([None, None], [2, 4]) == [2, 4]
+assert insert_sorted([None, None], [2, 4]) == [2, 4]
+assert insert_sorted([1, 1, None, None], [0, 0]) == [0, 0, 1, 1]
+assert insert_sorted([1, 1, None, None], [1, 1]) == [1, 1, 1, 1]
+print('All tests pass!')
diff --git a/users/wpcarro/scratch/crack_the_coding_interview/to_tree.hs b/users/wpcarro/scratch/crack_the_coding_interview/to_tree.hs
new file mode 100644
index 0000000000..8496d88c0c
--- /dev/null
+++ b/users/wpcarro/scratch/crack_the_coding_interview/to_tree.hs
@@ -0,0 +1,11 @@
+data Tree a = Node a [Tree a] deriving (Show)
+
+withRoot :: [a] -> [Tree a]
+withRoot xs = xs |> toThing |> fmap buildTree
+
+buildTree :: (a, [a])
+
+
+toTree :: [a] -> Tree a
+toTree [x]      = Node x []
+toTree [x | xs] = Node x (toTree xs)
diff --git a/users/wpcarro/scratch/cryptopals/.gitignore b/users/wpcarro/scratch/cryptopals/.gitignore
new file mode 100644
index 0000000000..7aa03e126b
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/.gitignore
@@ -0,0 +1 @@
+alice.txt
\ No newline at end of file
diff --git a/users/wpcarro/scratch/cryptopals/README.md b/users/wpcarro/scratch/cryptopals/README.md
new file mode 100644
index 0000000000..f4f5719f9f
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/README.md
@@ -0,0 +1,3 @@
+# cryptopals
+
+My solutions for some of the questions at https://cryptopals.com.
diff --git a/users/wpcarro/scratch/cryptopals/set1/4.txt b/users/wpcarro/scratch/cryptopals/set1/4.txt
new file mode 100644
index 0000000000..d172b6cff7
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/set1/4.txt
@@ -0,0 +1,327 @@
+0e3647e8592d35514a081243582536ed3de6734059001e3f535ce6271032
+334b041de124f73c18011a50e608097ac308ecee501337ec3e100854201d
+40e127f51c10031d0133590b1e490f3514e05a54143d08222c2a4071e351
+45440b171d5c1b21342e021c3a0eee7373215c4024f0eb733cf006e2040c
+22015e420b07ef21164d5935e82338452f42282c1836e42536284c450de3
+043b452e0268e7eb005a080b360f0642e6e342005217ef04a42f3e43113d
+581e0829214202063d70030845e5301f5a5212ed0818e22f120b211b171b
+ea0b342957394717132307133f143a1357e9ed1f5023034147465c052616
+0c300b355c2051373a051851ee154a023723414c023a08171e1b4f17595e
+550c3e13e80246320b0bec09362542243be42d1d5d060e203e1a0c66ef48
+e159464a582a6a0c50471310084f6b1703221d2e7a54502b2b205c433afa
+ec58ea200e3005090e1725005739eda7342aed311001383fff7c58ef1f11
+01305424231c0d2c41f105057f74510d335440332f1038ec17275f5814e1
+05f12f380720ea2b19e24a07e53c142128354e2827f25a08fb401c3126a6
+0d17272f53063954163d050a541b1f1144305ae37d4932431b1f33140b1b
+0b4f070f071fe92c200e1fa05e4b272e50201b5d493110e429482c100730
+100a3148080f227fe60a132f0c10174fe3f63d1a5d38eb414ca8e82f2b05
+0a19e83c58400a023b13234572e6e4272bf67434331631e63b5e0f00175c
+54520c2ceb45530e0f78111d0b0707e01e4bf43b0606073854324421e6f9
+09e7585353ee4a34190de1354e481c373a1b2b0a136127383e271212191f
+0f060d09fb4f2d5024022c5ff6463c390c2b5f1a5532071a31f33503fcea
+371d39121605584f48217235ee1e0602445c162e4942254c071954321d29
+4a0900e63e5f161e15554045f3594c2a6a77e4e52711602beaf53ae53bed
+29011616565d2a372a605bee39eced31183fe068185c3b445b391fe53232
+e4102337000303452a1e2f2b29493f54ed5a037b3e08311b625cfd005009
+2d560d4b0618203249312a310d5f541f295c3f0f25235c2b20037d1600f3
+2c245155e8253708391a7ceb0d05005c3e080f3f0f0e5a16583b111f4448
+493804044d262eec3759594f212d562420105d6a39e70a0f3957f347070c
+e72d1d1f103807590f4339575e00381074485d2d580249f744052605e11d
+e131570ae95307143a71131729552d001057a4540a1f425b190b572dee34
+2c1655342f02581c202b0a5c17a358291e1506f325550f05365e165c1c5f
+e318164df80b043e5406296e5359271d152f552e155a43eda81f23231d1c
+001de0413e174e18192c061e4b3d1b5626f90e3e1429544a20ee150d0c20
+32e902193219033c58191302441a5c1b584825ea140c290927aaea53e23c
+3a36363a732e32ea3f0e430508204b332c382a19292d5b291122e123446a
+1804115614031f5f571f2b143c5d3c1b257a4b37350f18445a3e08341c3d
+21f2fb250b2e55151e77253a3f0e5f4b2030370a4155e720e73914e35a4a
+510a55583a3c491221397c123a2b14a8305b3b09e71b241d0e51202e1a32
+1b51202f4917232b512a141d6812f03c455df05e5a1c2cee14390b3b593a
+5f5731e5203116ee131a4a4b24112cef5d0822f035e6547d3a0014462f26
+0028fb522104f771501a555d3f581e30e9ec3e49e3e63123432f07794145
+1459f6312f000e5a1373e346e40f211e1b0b0e17000f391f170552150500
+7e301e18325717e3412e022f087be30e5641080151357714e0e0eee15e11
+533258e9360f513b083aa51d2824222f40200a470537ecec392d31070b38
+07e32c180dfa56496a461627542115132a4c284050495b23e2245b093159
+2d3c230a1e5a300f6c3e26ed0d1709434950fd6f1e121335054129e4e4ec
+ef22fa2112311b11584ce43434f46f521a215433f9514fe33d313a3e0838
+34e7f336270c08010f2f544f0f1c1e235c0222644c2632efec061de2115f
+121a42395d4c560d213b0c0a26a7e4f4382718153d5e511158a10b2c021e
+e05d414dfa40222f0c382a03235f4d0d04372d4b7855105e26e44f2e0555
+7f3a4f1351f85b0344223e1177e14707190c0e311f4ca633f5f3e9352372
+01424d5d1a322a0d381717130e181d07240c2c19ecee750b1a37085d014c
+16012c5de55a0314a8260e2759e439123ca0c81c321d454e4e0ee14f4c1d
+0b1415512f38580e4e2a227def242643183c224f0ea146443403022fe9fd
+43eb2b1078322a02192d5b5e0c360d584d0b5e2c13072912ee32f03f4155
+002a52553e08361b0be0074b573e201c164c093a5c0f0159333b59770d5b
+38e63c1c5244301a5a01f26930321256143e1ae05e1120a9eaf20a192d58
+7d54140a152ef4035f09083ded531ee04df55848020656a1342e502649eb
+0c211dfe101702015516341136252f3f06f73247133113f5642d083a3417
+015e3d51433f3c003e5e28030b1d413eee186824504b241e0f0d32373e2b
+2d465040ec130c5c0e2704aa17010c40095207223669110f22f45ea155f7
+14552e2b341e5ce0195351066a23e3283e0ee935444b255a1c5c3cef7614
+372b453d5a357c05142be65b3c17f92d2b134853390a312bf92a531b513d
+5658265f4c0ce4440a20322f591a413034292b312206a01be6453a512d21
+1c585c19f31f785324f8583d1ee02620342b10a236263f105011ee5b0e14
+0f522b550818591a752e5fea0e033322ee5e280a4a1b244f5a2b35341255
+39093c1ced331b264127173f1312e2455fa33b31012c1f4d073c553f5d5e
+18f82d5d07e2430b3b3c1b5b49effb0313173f5d4a2e5c134555ff6b1d1a
+550a20234202726341190311295254f4064205aa515ae0145a23071c4e18
+3f2047024e3ce4555a1b39fa145455012c3afb0f2d11134846182e3c575b
+e3e456571937762828065443153b51152e262f09c937024405284f236432
+012f580c3536ec5c021574541d5c41123a4e661d5f0f5f344a083e3a5e4c
+4216252d01eb0a2a4623621b48360d312c29f33e380650447617124b3e71
+54141e59323606390204e95f1206520e5c084510034d30171c5e744f335d
+1e30061401600b342e171059526d1949431a3f412f56594c183711ea4837
+3131254f11e76f550e1e4d26f1391f44363b151c31281ff45259351da0e6
+5def250d0f3505385f22e9f4112633005d272d092e0138275851f943e90e
+0939165718303b445210095c16390cf04f19450e06f4545c0a0c320e3e23
+1e0b0b1f573f3d0fe05d43090fa8482242300819313142325b1f4b19365b
+0d3b2a5d271e463d2203765245065d5d684a051e5815265b52f3171d3004
+6af423303817a43324394af15a5c482e3b16f5a46f1e0b5c1201214b5fe4
+4030544f3f51151e436e04203a5e3b287ee303490a43fb3b28042f36504e
+1a2d5a03fc0e2c04384046242e2b5e1548101825eb2f285f1a210f022141
+122355e90122281deeed3ba05636003826525d5551572d07030d4935201f
+2a3c484a15410d3b16375d4665271b5c4ce7ee37083d3e512b45204f17f6
+03222801255c2c211a7aeb1e042b4e38e8f1293143203139fb202c325f2b
+06542a28041956350e292bf3fe5c32133a2a171b3a3e4e4e3101381529e3
+4a5209ef24e5f3225e503b143d0e5747323fe7ee3d5b1b5110395619e65a
+1fee0a3945563d2b5703701817584b5f5b54702522f5031b561929ea2d1e
+e7271935100e3c31211b23113a3a5524e02241181a251d521ff52f3c5a76
+144a0efee02f0f5f1d353a1c112e1909234f032953ec591e0a58e55d2cf4
+efee0cf00d0955500210015311467543544708eb590d113d30443d080c1e
+1a562c1f7e2b0030094f051c03e30f4d501a0fe22a2817edfc5e470c3843
+1c3df1135321a8e9241a5607f8305d571aa546001e3254555a11511924
+eb1d3f54ec0fea341a097c502ff1111524e24f5b553e49e8576b5b0e1e33
+72413e2f5329e332ec563b5e65185efefd2c3b4e5f0b5133246d214a401d
+352a0ae632183d200a162e5346110552131514e0553e51003e220d47424b
+1d005c58135f3c1b53300c3b49263928f55625454f3be259361ded1f0834
+2d2457524a1e1204255934174d442a1a7d130f350a123c4a075f5be73e30
+0c0518582d131f39575925e0231833370c482b270e183810415d5aec1900
+453b181df1572735380b0446097f00111f1425070b2e1958102ceb592928
+010a4a2d0b0926082d2f1525562d1d070a7a08152f5b4438a4150b132e20
+2b395d0d5d015d41335d21250de33e3d42152d3f557d1e44e4ee22255d2d
+4a1b5c272d0d1c45072639362e402dee2853e51311262b17aa72eb390410
+e7015f0215352030574b4108e44d0e1a204418e62325ff7f34052f234b2d
+1d563c13202346071d39e34055402b0b392c27f552222d3deb3843ee2c16
+29332a521f3c1b0811e33e1a25520e323e75e01c17473f55071226120d3d
+210b35ee1a0a5335222e35033905170c4f3104eb032d425058367d5a2bf2
+1e553809415efb1c460f2f0ffafaec491e4d4e49510452e8245a366a4106
+e1f92cee0e10142514e7ec13155c412fe901092f1f0fa738280c5eee5e04
+3526291e0b2a5f486a3051041f4c16372f5402e6f70b31a03525190b161a
+260e5e1f0c2e4d7528ef11552fefe247201e4752085c1da903563c162a4b
+2a14ff2e3265e604075e523b24455c364a7f284f3a43051d52152f1119e8
+5f02e55a4b1300063640ef10151002565f0b0c010033a1cbef5d3634484a
+1b121c585b495a5e033a09037f2d1754072c2d49084055172a3c220bed4f
+1613400e1632435c0018482aa55b363d26290ae4405ded280f2b0c271536
+4011250ce02119464a1de43113170356342c272d1d3355555e5706245e0a
+16272d5e545953002e10020875e223010719555410f91ce518420e382456
+0d4037320345f945241a1d090a545a310142442131464f4d10562ae4f05a
+07ee4d4ae12e571e313c1636313134233e495459e548317708563c2c1b2f
+e75803294b36565225552c3406304f0201e43323291b5e0e2159025c2f25
+5e63194411490c44494232237e1b323108573d3f391d1f3537e4165a2b35
+51000a3a264c503b5852072a5636f04f5cea58a42838f5fca876415c3521
+3c14130be511275932055a30aa2d03470c51060009f210543002585f5713
+10f0370c5823115200e5015d083e2f1a5df91d68065c1b03f0080855e529
+02ec00f1462d034123151ba6fc07eb3d5e54e85a3f3ee532fb41791a060b
+0c29274232f93efb3d465544e45e491b042ced245100e3f05c14134c254b
+5741235f051e080401a8013c065627e8ee5432205114243d54320e133f2d
+4a4d181635411f5d084e31ed230c16506d5125415e060e4dcd0e5f3708e3
+2d531c3e22065a5eee07310c145305131800063e4a20094b2006ea131240
+e7335c1c4308160be6aa551a0f5a58243e0b10ee470047683c345e1c5b0c
+5434505ee22a18110d20342e4b53062c4d79042a0a02422e225b2523e95a
+3252212407115c07e15eee06391d0519e9271b641330011f383410281f0e
+2cee2b355233292b595d1c69592f483b54584f7154fd4928560752e333a1
+17272b272f110df5e91c560a39104510240b5c4b0c1c570871e422351927
+c32550ec3f132c0c2458503ae5241d3c0d7911480a073826315620403615
+16e11c270d2b010650145de2290b0beb1e120a3a354b2104064f3b533c4e
+505746313d4d2e3455290a281ee81d50007e1148252528025237715a342a
+1c0a13163e404e40242142061d34185421160220fa031f7a423a08f2e01a
+101d303802f51b0c08ef461259315b553823e622a12d565509e23c624139
+0a3d1309e4384c0eed383846545a035a41ee1771513b090a031e15f45159
+2d4944092a1965542507003b23195758403e175a0a450c5c38114de21141
+eb100fe63a031c4b35eb591845e428441c0d5b0037131f5c160a31243619
+c155ef0d19143e24392507a202581a25491b135c27571d5c5b35250f0bef
+0e1d510556485e39557e044e2cf10457523016473f500b1e36370c17591c
+7e5a19250a5e152b46f5130a094cef08e84704ef10197324464b0114017a
+3b56f126390008343d3c400232ed201667211f0b1a1413080202530b08e2
+4912321b61c90a0cf6ef0a0a0c0f17fa62eb385e2616194526701aff5fe6
+2c57114b0400152d4f2aeb18ed41386c2e3a023a281d1a311eefe750ebab
+3a4353282114593b3e36446d2c5e1e582e335337022930331f211604576a
+295f3bfae9271ae8065a3b4417545c3e5b0df11a53351c78530915392d2e
+074a122ee01b17131e4e124e2322a9560ce4120e37582b24e1036fe93f30
+3c08290121090ef72f25e4f220323444532d3fe71f34553c7b2726131009
+12e84a3308590357a719e74c4f2133690a20031a0b045af63551325b1219
+0e3d4fe03f56523cf40f29e4353455120e3a4f2f26f6a30a2b3e0c5b085a
+57f3315c33e41c0f523426232d0651395c1525274e314d0219163b5f181f
+53471622182739e9e25b473d74e1e7023d095a3134e62d1366563004120e
+230a06431935391d5e0b5543223a3bed2b4358f555401e1b3b5c36470d11
+22100330e03b4812e6120f163b1ef6abebe6f602545ef9a459e33d334c2a
+463405faa655563a43532cfe154bec32fe3345eb2c2700340811213e5006
+14241340112b2916017c270a0652732ee8121132385a6c020c040e2be15b
+251119225c573b105d5c0a371c3d421ef23e22377fee334e0228561b2d15
+2e4c2e373b434b0d0b1b340c300e4b195614130ea03c234c292e14530c46
+0d2c3f08560ee32e5a5b6413355215384442563e69ec294a0eef561e3053
+193c100c0b24231c012273e10d2e12552723586120020b02e45632265e5f
+2c175a11553d4b0b16025e2534180964245b125e5d6e595d1d2a0710580b
+213a175ff30855e4001b305000263f5a5c3c5100163cee00114e3518f33a
+10ed33e65b003012e7131e161d5e2e270b4645f358394118330f5a5b241b
+33e80130f45708395457573406422a3b0d03e6e5053d0d2d151c083337a2
+551be2082b1563c4ec2247140400124d4b6508041b5a472256093aea1847
+7b5a4215415d544115415d5015455447414c155c46155f4058455c5b523f
+0864eb4935144c501103a71851370719301bec57093a0929ea3f18060e55
+2d395e57143359e80efffb13330633ea19e323077b4814571e5a3de73a1f
+52e73c1d53330846243c422d3e1b374b5209543903e3195c041c251b7c04
+2f3c2c28273a12520b482f18340d565d1fe84735474f4a012e1a13502523
+23340f39064e306a08194d544647522e1443041d5ee81f5a18415e34a45f
+475a392637565757730a0c4a517b2821040e1709e028071558021f164c54
+100b2135190505264254005618f51152136125370eef27383e45350118ed
+3947452914e0223f1d040943313c193f295b221e573e1b5723391d090d1f
+2c33141859392b04155e3d4e393b322526ee3e581d1b3d6817374d0c085b
+c2ea5821200f1b755b2d13130f04e26625ea3a5b1e37144d3e473c24030d
+ee15025d2019f757305e3f010e2a453a205f1919391e1a04e86d1a350119
+1a5beb4946180fe0002a031a050b41e5164c58795021e1e45c59e2495c20
+1121394f1e381c3647005b7326250514272b55250a49183be5454ba518eb
+1ee55936102a465d5004371f2e382f1d03144f170d2b0eed042ee341eb19
+ec1014ef3ff1272c3408220a41163708140b2e340e505c560c1e4cf82704
+274b341a454a27a0263408292e362c201c0401462049523b2d55e5132d54
+e259032c444b091e2e4920023f1a7ce40908255228e36f0f2424394b3c48
+34130cf8223f23084813e745e006531a1e464b005e0e1ee405413fe22b4e
+4af201080c0928420c2d491f6e5121e451223b070dee54244b3efc470a0e
+771c161f795df81c22101408465ae7ef0c0604733ee03a20560c1512f217
+2f3a142c4155073a200f04166c565634020a59ea04244ff7413c4bc10858
+240d4752e5fa5a4e1ce255505602e55d4c575e2b59f52b4e0c0a0b464019
+21341927f3380232396707232ae424ea123f5b371d4f65e2471dfbede611
+e10e1c3b1d4d28085c091f135b585709332c56134e4844552f45eb41172a
+3f1b5a343f034832193b153c482f1705392f021f5f0953290c4c43312b36
+3810161aea7001fb5d502b285945255d4ef80131572d2c2e59730e2c3035
+4d59052e1f2242403d440a13263e1d2dea0612125e16033b180834030829
+022917180d07474c295f793e42274b0e1e16581036225c1211e41e04042f
+ec2b41054f2a5f56065e5e0e1f56e13e0a702e1b2f2137020e363a2ae2a4
+53085a3b34e75a1caa2e5d031f261f5f044350312f37455d493f131f3746
+0c295f1724e90b001a4e015d27091a0b3256302c303d51a05956e6331531
+e42b315ce21f0def38144d20242845fa3f3b3b0ce8f4fb2d31ed1d54134b
+2957023141335d35372813263b46581af6535a16404d0b4ff12a207648ec
+e4421e301de25c43010c504e0f562f2018421ce137443b41134b5f542047
+0c5600294e085c1d3622292c480d261213e05c1334385108c145f3090612
+062d2e02267404241f4966e6e010052d3224e72856100b1d22f65a30e863
+324950394700e11a01201a0564525706f1013f353319076b4c0d015a2e24
+2a1be80e2013571522483b1e20321a4e03285d211a444d113924e8f41a1f
+27193ae2302208e73010eaa1292001045737013e10e4745aed2c105b25fb
+1b135d46eaef103e1d330a14337a2a4302441c1631ed07e7100c743a0e35
+1a0957115c293b1c0de853245b5b18e2e12d28421b3230245d7b4a55f355
+e7360e2b3846202a2926fa495e3302ed064d127a17343a1f11032b40e8f5
+06e8f90a3118381c5414157d1434050210363e30500511a00a3d56e10438
+30021931f7193e25a0540ef52658350929380974fb035b1a5d2c042959c7
+151b0c24052d0e56025404390e5a3909edec0d03070f040cff710825363e
+2a2328120b2203320810134a0c0a0ef30b25460bec011c1e26e913575a51
+e12d0948ed3c511416151d1c54082b3e385d14f838510bec4e4b5f585321
+1559305c3a49192a010f04ec11001a3d5a5621e5535358353206521f013f
+172c2c155a3a322009505c290516a2c4e4405a1e0a1e353b6e1a5a4e2f09
+552c34e2432b0df1132b130841000d4007232339a2092a593f142b0a0117
+0931432e452d3aea1d02587d3a3e56ed2a3050e2f9363df366331e421947
+0250094823545b20163f1d0a36a92228ed25564d1a304deae8035c32370d
+4314380e264e2359e6a412504a424328e84434ff30236649353315344a00
+25e33540550d3c15135b0eed451cfd1812eaf2063f085d6e214d121c342f
+37513b2d0a4e3e5211372a3a01334c5d51030c46463e3756290c0d0e1222
+132f175e4c4af1120138e1f2085a3804471f5824555d083de6123f533123
+0de11936062d3d2f12193e135f38ff5e1a531d1426523746004e2c063a27
+49241aee1802311611a50de9592009e936270108214a0c4213a01f09545f
+02e14d2babee204a5c4337135821360d021b7831305963ee0737072f0deb
+1512371119050c0c1142245a004f033650481830230a1925085c1a172726
+3be62f230a4b50526ec9345100252aa729eafa59221b3fa517304e500a15
+5e57f231333c3d0c470a47551733511031362a3bed0f334a3f3136104230
+eb24015d051a151f245905061a37ea273d2239fe02463a5e314d565f0457
+23025f415d290a594e3b5940313347a11c5e41531ff15a385a183829780a
+51e0035f2deb3b163eabe8550e2e0414491f573b5419234a28183044e112
+1d54e8390b26585f3aef5f14206672240c4a5e5d31e01b4d406e351401fa
+e555173e242c753b275d4ee50b2f26501402a71b1b5733ec19ee34284aed
+2ee8f023401c09383b084d623ef324ee5a33065a6d5e365b092c5d0d4501
+3f4e024d4b161e144d5e3b140d1e2944465b491d265603a705373c231240
+544f0d4ea6091e00e62d3e130d4f005139f339001a3b480c221b730be75e
+5f1f4f3e0a0dec3b5128e32960e42d0fee02275528154b10e65c36555a2e
+ea3e311b5b0f5f220b1f1b2914f12111f41213e06232224df5ec0114470d
+51203f1e01e5563851284013514a565e53125223052f47100e5011100201
+3f5bee2305217838582be55958a00245265b0308ec56525b5c114c2d5407
+e6e74818e53602160e45372029eb4de72754ec3f49290d2f5901014c0e7f
+08e715e612380a5c1908285a1222073a023c562907384e4f470444483f34
+1110382b5225343ba6092133483e2d683e1e280227084a1e405e3a341513
+415f240f0c53e3f7196e2252fb0105347f345e531f535a344bf439220916
+5722e7f7fa2f4c2e057e2a025e2dec31413439aa12265f5a3458f81a4b15
+135839401856f337a72fec475a060de239a650163a55392a5b303f051415
+56090f18023a2b16e2364407050d48e1541408281d3aa3e84c5b264c1f33
+1725f9540aec5e10ed293e4e5a5a2d2125f053251a55395d1c2044022231
+292d523ff86a180620075f325e02566659f30423525a053a01f0087f4b3b
+17fe493808f25309251e1325596ce32b42311e5d0c2f58652640582a4b17
+67381a5afb7128150a0043e45b173d2111155c49092d2635370a3a201826
+e62d021d36e03b205d5f1f295c094608342a412122583f3bfc34190be62c
+393a055f59060d454a235326e844243a30285c14e316272524f4f0444f51
+352c3c5b2b5845244f55494940194721f80b120f07392b7c2c5a0508111e
+2f1219430151e60f11150b101e295736361b1e053e4d08f83f230e2c383a
+ef5b1d492610e834330f5cf3a2485d324f2822084f41111f582957191b19
+1e3e223704fe1d2e1f592753e5550f15170b231b4234e945301f5605a670
+300d322759ea0337015c662a0e073809543f2741104835512d0624551751
+373727ef1f41084d0b5c0c0137283b1337026aea1c5ae115064ffa183402
+09152b11e1233e5a0e302a521c5a33181e180026463744a82c024b4bf04e
+1df61df1263fee59135c13400950153d3c5c59183b020b1d2d2c492f4968
+e2000c405a01ede30c4c082e2537443c120f38fc57c43651423e5c3beb1d
+1922182420191b293e163d58020b005f454a0621051a38e80b090a463ee9
+39513f2d47042c0fe5134419ec48490f150f323a5ee7a7e0201e193a5e1b
+2037200a2b1013567b35fb4a0f322c2f49435d091920521c302b413f5f35
+775d1a345b483b35a02a4c3e17ee3a3d5a5b57153613264f23041922432f
+35125b3e0a1d2257eb002a26455e1a2f042e1545e92f0b3408032c4f3551
+2d4c392321300a18ed4f3e2c314d20500052aa3917e55d0d29500754282e
+381b2e263758f63c474a1c23110c2d5f1c220412e91043580656080c0427
+081ce1e5350b6a3535f0e6592e5b543432340e38f008e0324102e45a3f25
+30040c181615362e4d1016160a4a5c006eeb1d2422355a3f1028ff192a07
+53f6354d4b5d121974245c14f0225713331f2e381810101428571725e432
+1a2c06372d5b1419742150042d25003c2650512834ef16e51d183f0f0508
+3d191107251100ee2e4125405a44174f061e0e1e5959e606530e06ed245e
+3f592d47512dec5922500e460e1de7183b4c3c2e583942255a0c5d4d2305
+3438001e482a002d56113a1fe13bed542d3508e22f4e22221431121c1539
+ed445a5d28415073eb18022ef836274d573a48090f2a663058194901405d
+215b143954fc313c1e28584b51e729ef31013b232bfb4c52e2322a2d4557
+5244102e1c3d304450ee01761924e62ff2173305e15809102b2125284dfc
+171a3f010f3639056f2be71c2047581de32e05a20833e1221b0e25362459
+2958280de238084f5a1c292e005be71f3b311e1f415809383d3862260238
+361f56ecee120156375862eb3627185c2519545149e2e50b1f3b0c4e3352
+e6115f440634e4005d273611e41c5d383c3814537b3d23362b084024345b
+10370656372e0236eb4f3303e216505f0e465228383729394faa2f205f34
+2e125b2f2c1d0f1f170e0c51331f0c06291610345c0603791f33253f0e0c
+1c2b080526133aeb3e23571d4cfa1e48057a2a010a490a50391b09514f2e
+59383ae11237e5450029162d2e1d3e09221a160e42ea06ea0ca7c7ecf4ea
+3d3024f34d5c07464bea3b185e110d3a10395d3b2632343cf30ca2e6065a
+262f111c0e15441a4825111b185f1e5756243206125f4603e97e79582d27
+2d5801ee2654113e2da00b58e9260d643c10423e1d1f42093b0d0f7d5102
+3649211f210456051e290f1b4c584d0749220c280b2a50531f262901503e
+52053e3e152b5b2b4415580fec57ef5c08e5ed43cc2d2e5b40355d0d2017
+6d3917263f030c4b55f0025d501e57504a122729293c4c5819680d3001ed
+1e313323324e5e177b171cf70c371541395c0e2b7726e42505483014362e
+1910e4f7253f0a012057e03b1e3b4201362b224ff60e0b3a1d115b043957
+200c1e0b242e5e3b4755f61e3be05c040908f1234358e55562711d2efa0f
+0737e0160b1d13132044080d2325f1f0ee2f00354f2106471131020a5d0b
+3f21060de62c052a17576e2ce729242b3e3621300627f01e52580a480050
+1b381a11351f4f5d22040c3c4b3e7d263714e8e61a571d107a34260a4a51
+edf52314e111207c0b23eb482f441d211f306137152407040e08530a783e
+3c054e2d4e2905275e640220f74f1a193f54e1ed5b4e2a290eab27a55147
+33522817335316ea2f3df957e25e02030601514f09f74c2fedee102d3114
+5d05231d03313826164156110c44e4111f4658005e115e300f413b430300
+380bf53a4331f74627492c133fe8eb3141ee39040def040c1a0ae914e3ed
+5b00f0211f0a091e05582e22f05a5d262e0ce352251d25100b102b11e339
+36053935f051f959093252411e2d5af81f360c0fa15d0b373b1d26323b77
+501424184202206215e05944505c4817514540445b0207025de05b050932
+0a5a114515536f553a352c513f0b12f700345fa51d5efb28222676e559ea
+561b0557403f5f534a574638411e2d3b3c133f79555c333215e6f5f9e7ec
+6658f7210218110f00062752e305f21601442c5310162445ed4d175630f3
+0e2154253c4a22f02e1b0933351314071b521513235031250c18120024a1
+e03555453d1e31775f37331823164c341c09e310463438481019fb0b12fa
+37eee654410e4007501f2c0e42faf50125075b2b46164f165a1003097f08
+2a5332145851553926523965582e5b2f530d5d1e292046344feaed461517
+583d2b06251f551d2f5451110911e6034147481a05166e1f241a5817015b
+1f2d3f5c310c315402200010e24135592435f71b4640540a041012ee1b3f
+5b2010060e2f5a4d045e0b36192f79181b0732183b4a261038340032f434
+3a5557340be6f5315c35112912393503320f54065f0e275a3b5853352008
+1c595d183539220eec123478535337110424f90a355af44c267be848173f
+41053f5cef5f6f56e4f5410a5407281600200b2649460a2e3a3c38492a0c
+4c071a57e9356ee415103c5c53e254063f2019340969e30a2e381d5b2555
+32042f46431d2c44607934ed180c1028136a5f2b26092e3b2c4e2930585a
\ No newline at end of file
diff --git a/users/wpcarro/scratch/cryptopals/set1/c1.py b/users/wpcarro/scratch/cryptopals/set1/c1.py
new file mode 100644
index 0000000000..0dfd6bb6d0
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/set1/c1.py
@@ -0,0 +1,19 @@
+from base64 import b64encode
+
+################################################################################
+# Challenge 1
+################################################################################
+
+def hex_to_base64(x):
+    parsed = bytearray.fromhex(x)
+    print(parsed.decode()) # easter egg
+    return b64encode(parsed).decode()
+
+run_tests = False
+if run_tests:
+    actual = hex_to_base64("49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d")
+    expect = "SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t"
+
+    print(actual)
+    assert actual == expect
+    print("Success!")
diff --git a/users/wpcarro/scratch/cryptopals/set1/c2.py b/users/wpcarro/scratch/cryptopals/set1/c2.py
new file mode 100644
index 0000000000..badd60503d
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/set1/c2.py
@@ -0,0 +1,20 @@
+def fixed_xor(x, y, decode_hex=True, encode_hex=True):
+    if decode_hex:
+        x = bytearray.fromhex(x)
+        y = bytearray.fromhex(y)
+
+    result = bytearray(len(x))
+
+    for i in range(len(x)):
+        result[i] = x[i] ^ y[i]
+
+    return result.hex() if encode_hex else result
+
+run_tests = False
+if run_tests:
+    actual = fixed_xor("1c0111001f010100061a024b53535009181c", "686974207468652062756c6c277320657965")
+    expect = "746865206b696420646f6e277420706c6179"
+
+    print(actual)
+    assert actual == expect
+    print("Success!")
diff --git a/users/wpcarro/scratch/cryptopals/set1/c3.py b/users/wpcarro/scratch/cryptopals/set1/c3.py
new file mode 100644
index 0000000000..2d84026a7b
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/set1/c3.py
@@ -0,0 +1,50 @@
+from c2 import fixed_xor
+from collections import Counter
+
+def frequency_table():
+    with open('alice.txt', 'r') as f:
+        chars = {}
+        while True:
+            l = f.readline()
+            if not l: break
+            for c in l:
+                chars[c] = chars.get(c, 0) + 1
+        result = {}
+        for c, n in chars.items():
+            result[c] = n / len(chars)
+        return result
+
+def score(bs, freqs):
+    return sum(freqs.get(b, 0) for b in bs)
+
+def decode_cipher(x):
+    freqs = frequency_table()
+
+    if not freqs:
+        raise Error("Cannot decode cipher without a populated frequency table")
+
+    x = bytearray.fromhex(x)
+    num_bytes = len(x)
+
+    mx, result, key = 0, None, None
+    for b in range(0, 1 << 8):
+        mask = bytearray(b.to_bytes(1, 'big') * num_bytes)
+        try:
+            y = fixed_xor(x, mask, decode_hex=False, encode_hex=False).decode('ascii')
+        except:
+            continue
+        test = score(y, freqs)
+        if test > mx:
+            result = y
+            mx = test
+            key = mask.decode('ascii')
+    return result
+
+run_tests = False
+if run_tests:
+    print(decode_cipher("1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736"))
+
+################################################################################
+# Answer
+################################################################################
+"Cooking MC's like a pound of bacon"
diff --git a/users/wpcarro/scratch/cryptopals/set1/c4.py b/users/wpcarro/scratch/cryptopals/set1/c4.py
new file mode 100644
index 0000000000..c546419a33
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/set1/c4.py
@@ -0,0 +1,23 @@
+import c3
+
+content = None
+with open('4.txt', 'r') as f:
+    content = f.read().splitlines()
+if not content:
+    raise Error("Need content to proceed")
+
+xs = []
+for line in content:
+    try:
+        x = c3.decode_cipher(line)
+        if x: xs.append(x)
+    except:
+        continue
+
+freqs = c3.frequency_table()
+print(max(xs, key=lambda x: c3.score(x, freqs)))
+
+################################################################################
+# Answer
+################################################################################
+"Now that the party is jumping"
diff --git a/users/wpcarro/scratch/cryptopals/set1/c5.py b/users/wpcarro/scratch/cryptopals/set1/c5.py
new file mode 100644
index 0000000000..a098dfe74a
--- /dev/null
+++ b/users/wpcarro/scratch/cryptopals/set1/c5.py
@@ -0,0 +1,16 @@
+def encrypt_repeating_key(x, key):
+    result = b""
+    for i in range(len(x)):
+        b = ord(x[i]) ^ ord(key[i % len(key)])
+        result += b.to_bytes(1, 'big')
+    return result.hex()
+
+cleartext = "Burning 'em, if you ain't quick and nimble\nI go crazy when I hear a cymbal"
+expected = "0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d63343c2a26226324272765272a282b2f20430a652e2c652a3124333a653e2b2027630c692b20283165286326302e27282f"
+
+run_tests = False
+if run_tests:
+    ciphertext = encrypt_repeating_key(cleartext, "ICE")
+    print(ciphertext)
+    assert ciphertext == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/array-traversals.py b/users/wpcarro/scratch/data_structures_and_algorithms/array-traversals.py
new file mode 100644
index 0000000000..35cb439281
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/array-traversals.py
@@ -0,0 +1,87 @@
+# This is practice for various types of list traversals that turn up.
+
+xs = range(10)
+n = len(xs)
+
+print('---')
+# pythonic left-to-right traversal
+result = ''
+for x in xs:
+    result += str(x)
+print(result)
+
+print('---')
+# left-to-right traversal
+result = ''
+for i in range(n):
+    result += str(xs[i])
+print(result)
+
+print('---')
+# right-to-left traversal
+result = ''
+for i in range(n):
+    result += str(xs[n - 1 - i])
+print(result)
+
+print('---')
+# 2x left-to-right traversal
+result = ''
+for i in range(2 * n):
+    result += str(xs[i % n])
+print(result)
+
+print('---')
+# 2x right-to-left traversal
+result = ''
+for i in range(2 * n):
+    result += str(xs[(n - 1 - i) % n])
+print(result)
+
+################################################################################
+# Table traversals
+################################################################################
+
+table = [[row * 10 + i for i in range(10)] for row in range(3)]
+row_ct = len(table)
+col_ct = len(table[0])
+
+print('---')
+# 3x10 table traversal
+result = ''
+for row in table:
+    r = ''
+    for col in row:
+        r += '{:3d}'.format(col)
+    result += r + '\n'
+print(result[0:-1])
+
+print('---')
+# 3x10 table traversal
+result = ''
+for row in range(row_ct):
+    r = ''
+    for col in range(col_ct):
+        r += '{:3d}'.format(table[row][col])
+    result += r + '\n'
+print(result[0:-1])
+
+print('---')
+# 3x10 table traversal (reverse)
+result = ''
+for row in range(row_ct):
+    r = ''
+    for col in range(col_ct):
+        r += '{:3d}'.format(table[row_ct - 1 - row][col_ct - 1 - col])
+    result += r + '\n'
+print(result)
+
+print('---')
+# 3x10 column-row traversal
+result = ''
+for col in range(col_ct):
+    r = ''
+    for row in range(row_ct):
+        r += '{:3d}'.format(table[row][col])
+    result += r + '\n'
+print(result)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/balanced-binary-tree.py b/users/wpcarro/scratch/data_structures_and_algorithms/balanced-binary-tree.py
new file mode 100644
index 0000000000..01fd965fd5
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/balanced-binary-tree.py
@@ -0,0 +1,145 @@
+import unittest
+from itertools import combinations
+
+
+def balanced(xs):
+    """Return True if `xs` contains no two values that differ by more than
+    one."""
+    if len(xs) == 0 or len(xs) == 1:
+        return True
+    if len(xs) == 2:
+        return math.abs(xs[0] - xs[1]) <= 1
+    else:
+        pass
+
+
+def is_leaf(node):
+    return node.left is None and node.right is None
+
+
+def is_balanced(tree_root):
+    """Returns True if the difference between the depths of any two leaf nodes
+    does not exceed 1."""
+    depths = set()
+    populate_depths(tree_root, 0, depths)
+
+    # cartesian product - only the top half
+    for diff in set(abs(a - b) for a, b in combinations(depths, 2)):
+        if diff > 1:
+            return False
+
+    return True
+
+
+def populate_depths(node, depth, depths):
+    if is_leaf(node):
+        depths.add(depth)
+    else:
+        if node.left is not None:
+            populate_depths(node.left, depth + 1, depths)
+        if node.right is not None:
+            populate_depths(node.right, depth + 1, depths)
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class BinaryTreeNode(object):
+        def __init__(self, value):
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def insert_left(self, value):
+            self.left = Test.BinaryTreeNode(value)
+            return self.left
+
+        def insert_right(self, value):
+            self.right = Test.BinaryTreeNode(value)
+            return self.right
+
+    def test_full_tree(self):
+        tree = Test.BinaryTreeNode(5)
+        left = tree.insert_left(8)
+        right = tree.insert_right(6)
+        left.insert_left(1)
+        left.insert_right(2)
+        right.insert_left(3)
+        right.insert_right(4)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_both_leaves_at_the_same_depth(self):
+        tree = Test.BinaryTreeNode(3)
+        left = tree.insert_left(4)
+        right = tree.insert_right(2)
+        left.insert_left(1)
+        right.insert_right(9)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_leaf_heights_differ_by_one(self):
+        tree = Test.BinaryTreeNode(6)
+        left = tree.insert_left(1)
+        right = tree.insert_right(0)
+        right.insert_right(7)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_leaf_heights_differ_by_two(self):
+        tree = Test.BinaryTreeNode(6)
+        left = tree.insert_left(1)
+        right = tree.insert_right(0)
+        right_right = right.insert_right(7)
+        right_right.insert_right(8)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_three_leaves_total(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(5)
+        right = tree.insert_right(9)
+        right.insert_left(8)
+        right.insert_right(5)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_both_subtrees_superbalanced(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(5)
+        right = tree.insert_right(9)
+        right_left = right.insert_left(8)
+        right.insert_right(5)
+        right_left.insert_left(7)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_both_subtrees_superbalanced_two(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(2)
+        right = tree.insert_right(4)
+        left.insert_left(3)
+        left_right = left.insert_right(7)
+        left_right.insert_right(8)
+        right_right = right.insert_right(5)
+        right_right_right = right_right.insert_right(6)
+        right_right_right.insert_right(9)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_only_one_node(self):
+        tree = Test.BinaryTreeNode(1)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_linked_list_tree(self):
+        tree = Test.BinaryTreeNode(1)
+        right = tree.insert_right(2)
+        right_right = right.insert_right(3)
+        right_right.insert_right(4)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/bit-manipulation.py b/users/wpcarro/scratch/data_structures_and_algorithms/bit-manipulation.py
new file mode 100644
index 0000000000..dc30bb5088
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/bit-manipulation.py
@@ -0,0 +1,32 @@
+def test(x, i):
+    return x & (1 << i) != 0
+
+
+def set(x, i):
+    return x | (1 << i)
+
+
+def clear(x, i):
+    return x & ~(1 << i)
+
+
+def toggle(x, i):
+    if test(x, i):
+        return clear(x, i)
+    else:
+        return set(x, i)
+
+
+def test_single(x):
+    if x == 0:
+        return False
+    else:
+        return x & (x - 1) == 0
+
+
+print(test(0b1010, 3))
+print('{0:b}'.format(set(0b1010, 1)))
+print('{0:b}'.format(clear(0b1010, 1)))
+print('{0:b}'.format(toggle(0b1010, 2)))
+print(test_single(0b1010))
+print(test_single(0b1000))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/bracket-validator.py b/users/wpcarro/scratch/data_structures_and_algorithms/bracket-validator.py
new file mode 100644
index 0000000000..a50f8b074e
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/bracket-validator.py
@@ -0,0 +1,63 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# is_valid :: String -> Boolean
+def is_valid(xs):
+    s = []
+    seeking = {
+        '}': '{',
+        ']': '[',
+        ')': '(',
+    }
+    openers = seeking.values()
+    closers = seeking.keys()
+    for c in xs:
+        if c in openers:
+            s.append(c)
+        elif c in closers:
+            if not s:
+                return False
+            elif s[-1] != seeking.get(c):
+                return False
+            else:
+                s.pop()
+    return len(s) == 0
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_valid_short_code(self):
+        result = is_valid('()')
+        self.assertTrue(result)
+
+    def test_valid_longer_code(self):
+        result = is_valid('([]{[]})[]{{}()}')
+        self.assertTrue(result)
+
+    def test_interleaved_openers_and_closers(self):
+        result = is_valid('([)]')
+        self.assertFalse(result)
+
+    def test_mismatched_opener_and_closer(self):
+        result = is_valid('([][]}')
+        self.assertFalse(result)
+
+    def test_missing_closer(self):
+        result = is_valid('[[]()')
+        self.assertFalse(result)
+
+    def test_extra_closer(self):
+        result = is_valid('[[]]())')
+        self.assertFalse(result)
+
+    def test_empty_string(self):
+        result = is_valid('')
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/bst-checker.py b/users/wpcarro/scratch/data_structures_and_algorithms/bst-checker.py
new file mode 100644
index 0000000000..689be97a85
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/bst-checker.py
@@ -0,0 +1,121 @@
+import unittest
+
+
+################################################################################
+# Implementation
+################################################################################
+# is_leaf :: Node(a) -> Boolean
+def is_leaf(node):
+    return not node.left and not node.right
+
+
+# is_binary_search_tree :: Node(Integer) -> Set(Int) -> Set(Int) -> Boolean
+def is_binary_search_tree_a(node, la=set(), ra=set()):
+    """My first solution for this problem."""
+    for x in la:
+        if not node.value < x:
+            return False
+    for x in ra:
+        if not node.value > x:
+            return False
+    if is_leaf(node):
+        return True
+    elif not node.left:
+        return is_binary_search_tree(
+            node.right,
+            la=la,
+            ra=ra ^ {node.value},
+        )
+    elif not node.right:
+        return is_binary_search_tree(node.left, la=la ^ {node.value}, ra=ra)
+    else:
+        return all([
+            is_binary_search_tree(node.left, la=la ^ {node.value}, ra=ra),
+            is_binary_search_tree(node.right, la=la, ra=ra ^ {node.value})
+        ])
+
+
+# is_binary_search_tree :: Node(Int) -> Maybe(Int) -> Maybe(Int) -> Boolean
+def is_binary_search_tree(node, lb=None, ub=None):
+    if lb:
+        if node.value < lb:
+            return False
+    if ub:
+        if node.value > ub:
+            return False
+    if is_leaf(node):
+        return True
+    elif not node.right:
+        return is_binary_search_tree(node.left, lb=lb, ub=node.value)
+    elif not node.left:
+        return is_binary_search_tree(node.right, lb=node.value, ub=ub)
+    else:
+        return is_binary_search_tree(
+            node.left, lb=lb, ub=node.value) and is_binary_search_tree(
+                node.right, lb=node.value, ub=ub)
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class BinaryTreeNode(object):
+        def __init__(self, value):
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def insert_left(self, value):
+            self.left = Test.BinaryTreeNode(value)
+            return self.left
+
+        def insert_right(self, value):
+            self.right = Test.BinaryTreeNode(value)
+            return self.right
+
+    def test_valid_full_tree(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(70)
+        left.insert_left(10)
+        left.insert_right(40)
+        right.insert_left(60)
+        right.insert_right(80)
+        result = is_binary_search_tree(tree)
+        self.assertTrue(result)
+
+    def test_both_subtrees_valid(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(80)
+        left.insert_left(20)
+        left.insert_right(60)
+        right.insert_left(70)
+        right.insert_right(90)
+        result = is_binary_search_tree(tree)
+        self.assertFalse(result)
+
+    def test_descending_linked_list(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(40)
+        left_left = left.insert_left(30)
+        left_left_left = left_left.insert_left(20)
+        left_left_left.insert_left(10)
+        result = is_binary_search_tree(tree)
+        self.assertTrue(result)
+
+    def test_out_of_order_linked_list(self):
+        tree = Test.BinaryTreeNode(50)
+        right = tree.insert_right(70)
+        right_right = right.insert_right(60)
+        right_right.insert_right(80)
+        result = is_binary_search_tree(tree)
+        self.assertFalse(result)
+
+    def test_one_node_tree(self):
+        tree = Test.BinaryTreeNode(50)
+        result = is_binary_search_tree(tree)
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/cafe-order-checker.py b/users/wpcarro/scratch/data_structures_and_algorithms/cafe-order-checker.py
new file mode 100644
index 0000000000..e34a2b136a
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/cafe-order-checker.py
@@ -0,0 +1,91 @@
+import unittest
+
+
+################################################################################
+# Implementation
+################################################################################
+def is_first_come_first_served(to, di, xs):
+    # All the guards, assertions we should need.
+    if to == di == xs == []:
+        return True
+    elif to == di == []:
+        return False
+    elif to == []:
+        return di == xs
+    elif to == []:
+        return di == xs
+    elif di == []:
+        return to == xs
+    elif xs == []:
+        return False
+    elif len(xs) != (len(to) + len(di)):
+        return False
+
+    fst, snd = to, di
+
+    if xs[0] == to[0]:
+        fst, snd = to, di
+    elif xs[0] == di[0]:
+        fst, snd = di, to
+    else:
+        return False
+
+    fst_done, snd_done = False, False
+    fi, si = 1, 0
+
+    for i in range(1, len(xs)):
+        # Short-circuit and avoid index-out-of-bounds without introducing overly
+        # defensive, sloppy code.
+        if fst_done:
+            return snd[si:] == xs[i:]
+        elif snd_done:
+            return fst[fi:] == xs[i:]
+
+        if fst[fi] == xs[i]:
+            fi += 1
+        elif snd[si] == xs[i]:
+            si += 1
+        else:
+            return False
+
+        fst_done, snd_done = fi == len(fst), si == len(snd)
+
+    return True
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_both_registers_have_same_number_of_orders(self):
+        result = is_first_come_first_served([1, 4, 5], [2, 3, 6],
+                                            [1, 2, 3, 4, 5, 6])
+        self.assertTrue(result)
+
+    def test_registers_have_different_lengths(self):
+        result = is_first_come_first_served([1, 5], [2, 3, 6], [1, 2, 6, 3, 5])
+        self.assertFalse(result)
+
+    def test_one_register_is_empty(self):
+        result = is_first_come_first_served([], [2, 3, 6], [2, 3, 6])
+        self.assertTrue(result)
+
+    def test_served_orders_is_missing_orders(self):
+        result = is_first_come_first_served([1, 5], [2, 3, 6], [1, 6, 3, 5])
+        self.assertFalse(result)
+
+    def test_served_orders_has_extra_orders(self):
+        result = is_first_come_first_served([1, 5], [2, 3, 6],
+                                            [1, 2, 3, 5, 6, 8])
+        self.assertFalse(result)
+
+    def test_one_register_has_extra_orders(self):
+        result = is_first_come_first_served([1, 9], [7, 8], [1, 7, 8])
+        self.assertFalse(result)
+
+    def test_one_register_has_unserved_orders(self):
+        result = is_first_come_first_served([55, 9], [7, 8], [1, 7, 8, 9])
+        self.assertFalse(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/cake-thief.py b/users/wpcarro/scratch/data_structures_and_algorithms/cake-thief.py
new file mode 100644
index 0000000000..9eddb34b2d
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/cake-thief.py
@@ -0,0 +1,71 @@
+import unittest
+from math import floor
+
+
+################################################################################
+# Solution
+################################################################################
+def max_duffel_bag_value(xs, cap):
+    ct = (cap + 1)
+    maxes = [0] * ct
+    for c in range(cap + 1):
+        for w, v in xs:
+            if w == 0 and v > 0:
+                return float('inf')
+            if w == c:
+                maxes[c:] = [max(maxes[c], v)] * (ct - c)
+            elif w < c:
+                d = c - w
+                maxes[c:] = [max(maxes[c], v + maxes[d])] * (ct - c)
+            else:
+                continue
+    return maxes[cap]
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_one_cake(self):
+        actual = max_duffel_bag_value([(2, 1)], 9)
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_two_cakes(self):
+        actual = max_duffel_bag_value([(4, 4), (5, 5)], 9)
+        expected = 9
+        self.assertEqual(actual, expected)
+
+    def test_only_take_less_valuable_cake(self):
+        actual = max_duffel_bag_value([(4, 4), (5, 5)], 12)
+        expected = 12
+        self.assertEqual(actual, expected)
+
+    def test_lots_of_cakes(self):
+        actual = max_duffel_bag_value([(2, 3), (3, 6), (5, 1), (6, 1), (7, 1),
+                                       (8, 1)], 7)
+        expected = 12
+        self.assertEqual(actual, expected)
+
+    def test_value_to_weight_ratio_is_not_optimal(self):
+        actual = max_duffel_bag_value([(51, 52), (50, 50)], 100)
+        expected = 100
+        self.assertEqual(actual, expected)
+
+    def test_zero_capacity(self):
+        actual = max_duffel_bag_value([(1, 2)], 0)
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_cake_with_zero_value_and_weight(self):
+        actual = max_duffel_bag_value([(0, 0), (2, 1)], 7)
+        expected = 3
+        self.assertEqual(actual, expected)
+
+    def test_cake_with_non_zero_value_and_zero_weight(self):
+        actual = max_duffel_bag_value([(0, 5)], 5)
+        expected = float('inf')
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/coins.py b/users/wpcarro/scratch/data_structures_and_algorithms/coins.py
new file mode 100644
index 0000000000..eb5754f982
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/coins.py
@@ -0,0 +1,57 @@
+import unittest
+from math import floor
+
+################################################################################
+# Solution
+################################################################################
+
+# change_possibilities :: Int -> [Int] -> Int
+def change_possibilities(n, xs):
+    combinations = [0] * (n + 1)
+    combinations[0] = 1
+
+    for x in xs:
+        for i in range(len(combinations)):
+            if i >= x:
+                combinations[i] += combinations[i - x]
+
+    return combinations[n]
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+
+    def test_sample_input(self):
+        actual = change_possibilities(4, (1, 2, 3))
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_one_way_to_make_zero_cents(self):
+        actual = change_possibilities(0, (1, 2))
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_no_ways_if_no_coins(self):
+        actual = change_possibilities(1, ())
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_big_coin_value(self):
+        actual = change_possibilities(5, (25, 50))
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_big_target_amount(self):
+        actual = change_possibilities(50, (5, 10))
+        expected = 6
+        self.assertEqual(actual, expected)
+
+    def test_change_for_one_dollar(self):
+        actual = change_possibilities(100, (1, 5, 10, 25, 50))
+        expected = 292
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/conways-game-of-life.py b/users/wpcarro/scratch/data_structures_and_algorithms/conways-game-of-life.py
new file mode 100644
index 0000000000..3836bcd0c6
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/conways-game-of-life.py
@@ -0,0 +1,78 @@
+from itertools import product
+from random import choice
+from time import sleep
+from os import system
+from math import floor
+from colorama import Back, Fore, Style
+
+################################################################################
+# Simulation of Conway's Game of Life. The goal here was to write this with a
+# small amount of code as a proof-of-concept that could be run in the terminal.
+#
+# If you'd like to tinker with the rules, see the conditionals defined in the
+# `advance/1` function. For other parameters, like the board size and refresh
+# rate, refer to the while-loop defined at the bottom of this file.
+################################################################################
+
+
+def init_board(n, init_alive_percentage):
+    """Initialize a board of size `n` by `n`. Supply a percentage,
+    `init_alive_percentage`, representing the number of cells in the board that
+    should be alive from the start."""
+    alive_count = floor(n * init_alive_percentage)
+    distribution = [True] * alive_count + [False] * (n - alive_count)
+    return [[choice(distribution) for _ in range(n)] for _ in range(n)]
+
+
+def neighbors(coord, board):
+    """Return the neighbors for a given `coord` on a `board`."""
+    n = len(board)
+    row, col = coord
+    return [
+        board[(row + row_d) % n][(col + col_d) % n]
+        for row_d, col_d in product([-1, 0, 1], [-1, 0, 1])
+        if (row_d, col_d) != (0, 0)
+    ]
+
+
+def advance(board):
+    """Advance the state of the `board` from T[n] to T[n+1]."""
+    n = len(board)
+    new_board = [[False for _ in range(n)] for _ in range(n)]
+    for row in range(n):
+        for col in range(n):
+            alive_count = len([x for x in neighbors((row, col), board) if x])
+            # Loneliness
+            if alive_count == 0:
+                new_board[row][col] = False
+            # Status Quo
+            elif alive_count == 1:
+                new_board[row][col] = board[row][col]
+            # Cooperation
+            elif alive_count == 2:
+                new_board[row][col] = True
+            # Resource starvation
+            elif alive_count >= 3:
+                new_board[row][col] = False
+    return new_board
+
+
+def print_board(board):
+    """Print the game `board` in a human-readable way."""
+    result = ''
+    for row in board:
+        for col in row:
+            if col:
+                result += Back.GREEN + '1 ' + Style.RESET_ALL
+            else:
+                result += Back.RED + '0 ' + Style.RESET_ALL
+        result += '\n'
+    print(result)
+
+
+board = init_board(100, 0.50)
+while True:
+    system('clear')
+    print_board(board)
+    sleep(0.15)
+    board = advance(board)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/delete-node.py b/users/wpcarro/scratch/data_structures_and_algorithms/delete-node.py
new file mode 100644
index 0000000000..7e431e2249
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/delete-node.py
@@ -0,0 +1,60 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def delete_node(x):
+    if not x.next:
+        raise Exception('Cannot delete the last node in a linked list.')
+    else:
+        x.value = x.next.value
+        x.next = x.next.next
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+        def get_values(self):
+            node = self
+            values = []
+            while node is not None:
+                values.append(node.value)
+                node = node.next
+            return values
+
+    def setUp(self):
+        self.fourth = Test.LinkedListNode(4)
+        self.third = Test.LinkedListNode(3, self.fourth)
+        self.second = Test.LinkedListNode(2, self.third)
+        self.first = Test.LinkedListNode(1, self.second)
+
+    def test_node_at_beginning(self):
+        delete_node(self.first)
+        actual = self.first.get_values()
+        expected = [2, 3, 4]
+        self.assertEqual(actual, expected)
+
+    def test_node_in_middle(self):
+        delete_node(self.second)
+        actual = self.first.get_values()
+        expected = [1, 3, 4]
+        self.assertEqual(actual, expected)
+
+    def test_node_at_end(self):
+        with self.assertRaises(Exception):
+            delete_node(self.fourth)
+
+    def test_one_node_in_list(self):
+        unique = Test.LinkedListNode(1)
+        with self.assertRaises(Exception):
+            delete_node(unique)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/dft.py b/users/wpcarro/scratch/data_structures_and_algorithms/dft.py
new file mode 100644
index 0000000000..127d48c186
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/dft.py
@@ -0,0 +1,65 @@
+from random import choice
+
+
+class Node(object):
+    def __init__(self, value=None, left=None, right=None):
+        self.value = value
+        self.left = left
+        self.right = left
+
+
+def p(node, indent=0):
+    print(indent * ' ' + '|-' + str(node.value))
+    if node.left is not None:
+        p(node.left, indent=indent + 2)
+    if node.right is not None:
+        p(node.right, indent=indent + 2)
+
+
+# read trees (i.e. traversing, parsing)
+# write trees (i.e. generating, printing)
+def random(d=0):
+    left = None
+    right = None
+
+    if choice([True, False]):
+        left = random(d + 1)
+
+    if choice([True, False]):
+        right = random(d + 1)
+
+    return Node(
+        value=d,
+        left=left,
+        right=right,
+    )
+
+
+################################################################################
+# DFTs can be:
+# - imperative (mutable)
+# - functional (immutable)
+# - iterative
+# - recursive
+################################################################################
+
+
+# Iterative
+def traverse(node, f):
+    stack = [(node, 0)]
+
+    while len(stack):
+        node, depth = stack.pop()
+        f(node, depth)
+        print(depth)
+
+        if node.left is not None:
+            stack.append((node.left, depth + 1))
+        if node.right is not None:
+            stack.append((node.right, depth + 1))
+
+
+print('----------------------------------------------------------------------')
+for _ in range(10):
+    traverse(random(), lambda _, d: print(d))
+print()
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/dijkstra-shortest-path.py b/users/wpcarro/scratch/data_structures_and_algorithms/dijkstra-shortest-path.py
new file mode 100644
index 0000000000..03907f6040
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/dijkstra-shortest-path.py
@@ -0,0 +1,48 @@
+from collections import deque
+from heapq import heappush, heappop
+from fixtures import weighted_graph
+
+
+def put(t, x, xs):
+    if t == 'stack':
+        return xs.append(x)
+    if t == 'queue':
+        return xs.append(x)
+    if t == 'priority':
+        return heappush(xs, x)
+
+
+def pop(t, xs):
+    if t == 'stack':
+        return xs.pop()
+    if t == 'queue':
+        return xs.popleft()
+    if t == 'priority':
+        return heappop(xs)
+
+
+# shortest_path :: Vertex -> Vertex -> Graph -> [Vertex]
+def shortest_path(a, b, g):
+    """Returns the shortest path from vertex a to vertex b in graph g."""
+    t = 'priority'
+    xs = []
+    seen = set()
+    # Map(Weight, [Vertex])
+    m = {}
+
+    put(t, (0, [a], a), xs)
+
+    while xs:
+        w0, path, v = pop(t, xs)
+
+        seen.add(v)
+        if v == b:
+            m[w0] = path
+        for w1, x in g.get(v):
+            if x not in seen:
+                put(t, (w0 + w1, path + [x], x), xs)
+
+    return m
+
+
+print(shortest_path('a', 'f', graph_a))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space-beast.py b/users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space-beast.py
new file mode 100644
index 0000000000..93fdd9eed2
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space-beast.py
@@ -0,0 +1,56 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def find_duplicate(xs):
+    self_ref_count = 0
+    for i in range(len(xs)):
+        if xs[i] == i + 1:
+            self_ref_count += 1
+    hops = len(xs) - 1 - self_ref_count
+    current = xs[-1]
+    while hops > 0:
+        current = xs[current - 1]
+        hops -= 1
+    return current
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    # TODO: Debug why this fails.
+    def test_darren_from_interview_cake(self):
+        actual = find_duplicate([4, 1, 8, 3, 2, 7, 6, 5, 4])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_just_the_repeated_number(self):
+        actual = find_duplicate([1, 1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_short_list(self):
+        actual = find_duplicate([1, 2, 3, 2])
+        expected = 2
+        self.assertEqual(actual, expected)
+
+    def test_last_cycle(self):
+        actual = find_duplicate([3, 4, 2, 3, 1, 5])
+        expected = 3
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        actual = find_duplicate([1, 2, 5, 5, 5, 5])
+        expected = 5
+        self.assertEqual(actual, expected)
+
+    def test_long_list(self):
+        actual = find_duplicate([4, 1, 4, 8, 3, 2, 7, 6, 5])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space.py b/users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space.py
new file mode 100644
index 0000000000..e2739f0f60
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/find-duplicate-optimize-for-space.py
@@ -0,0 +1,61 @@
+from math import floor
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def bounds(r):
+    ct = len(r)
+    if ct % 2 == 0:
+        h = int(ct / 2)
+        return ct, h
+    else:
+        h = floor(ct / 2)
+        return ct, h
+
+
+def find_repeat(xs):
+    ct, h = bounds(xs)
+    rl = range(1, h + 1)
+    rr = range(h + 1, ct)
+    while True:
+        nl = len([None for x in xs if x in rl])
+        nr = len([None for x in xs if x in rr])
+        branch = rl if nl > nr else rr
+        if len(branch) == 1:
+            return branch[0]
+        ct, h = bounds(branch)
+        rl = range(branch[0], branch[0])
+        rr = range(branch[0] + h, branch[-1] + 1)
+    raise Exception(
+        'We could not find any duplicates in xs. Perhaps xs did not adhere to the usage contract.'
+    )
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_just_the_repeated_number(self):
+        actual = find_repeat([1, 1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_short_list(self):
+        actual = find_repeat([1, 2, 3, 2])
+        expected = 2
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        actual = find_repeat([1, 2, 5, 5, 5, 5])
+        expected = 5
+        self.assertEqual(actual, expected)
+
+    def test_long_list(self):
+        actual = find_repeat([4, 1, 4, 8, 3, 2, 7, 6, 5])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/find-rotation-point.py b/users/wpcarro/scratch/data_structures_and_algorithms/find-rotation-point.py
new file mode 100644
index 0000000000..2103a5b84f
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/find-rotation-point.py
@@ -0,0 +1,59 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def find_rotation_point(xs):
+    """Usage of `visited` here is a hack, but works for the test cases
+    (gulp)."""
+    i = 0
+    j = round(len(xs) / 2)
+    result = None
+    visited = set()
+    while not result:
+        if i in visited:
+            i += 1
+        if j in visited:
+            j -= 1
+        visited.add(i)
+        visited.add(j)
+        if xs[j - 1] > xs[j]:
+            result = j
+        elif xs[i] < xs[j]:
+            i = j
+            j += round((len(xs) - j) / 2)
+        elif xs[i] >= xs[j]:
+            i = j
+            j -= round((j - i) / 2)
+    return result
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_small_list(self):
+        actual = find_rotation_point(['cape', 'cake'])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        actual = find_rotation_point(
+            ['grape', 'orange', 'plum', 'radish', 'apple'])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_large_list(self):
+        actual = find_rotation_point([
+            'ptolemaic', 'retrograde', 'supplant', 'undulate', 'xenoepist',
+            'asymptote', 'babka', 'banoffee', 'engender', 'karpatka',
+            'othellolagkage'
+        ])
+        expected = 5
+        self.assertEqual(actual, expected)
+
+    # Are we missing any edge cases?
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/find-unique-int-among-duplicates.py b/users/wpcarro/scratch/data_structures_and_algorithms/find-unique-int-among-duplicates.py
new file mode 100644
index 0000000000..dfa5de42cc
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/find-unique-int-among-duplicates.py
@@ -0,0 +1,45 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def find_unique_delivery_id(xs):
+    a = 0
+    for x in xs:
+        a ^= x
+    return a
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_one_drone(self):
+        actual = find_unique_delivery_id([1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_unique_id_comes_first(self):
+        actual = find_unique_delivery_id([1, 2, 2])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_unique_id_comes_last(self):
+        actual = find_unique_delivery_id([3, 3, 2, 2, 1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_unique_id_in_middle(self):
+        actual = find_unique_delivery_id([3, 2, 1, 2, 3])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_many_drones(self):
+        actual = find_unique_delivery_id(
+            [2, 5, 4, 8, 6, 3, 1, 4, 2, 3, 6, 5, 1])
+        expected = 8
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/fixtures.py b/users/wpcarro/scratch/data_structures_and_algorithms/fixtures.py
new file mode 100644
index 0000000000..27689ca76d
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/fixtures.py
@@ -0,0 +1,110 @@
+# Using this module to store commonly used, but annoying to create, data
+# structures for my test inputs.
+#
+# Use like:
+# from fixtures import graph_a
+
+################################################################################
+# Constants
+################################################################################
+
+edge_list = [
+    ('a', 'b'),
+    ('a', 'c'),
+    ('a', 'e'),
+    ('b', 'c'),
+    ('b', 'd'),
+    ('c', 'e'),
+    ('d', 'f'),
+    ('e', 'd'),
+    ('e', 'f'),
+]
+
+unweighted_graph = {
+    'a': {'b', 'c', 'e'},
+    'b': {'c', 'd'},
+    'c': {'e'},
+    'd': {'f'},
+    'e': {'d', 'f'},
+    'f': set(),
+}
+
+adjacencies = {
+    'a': {
+        'a': False,
+        'b': False
+    },
+    'a': [],
+    'a': [],
+    'a': [],
+    'a': [],
+    'a': [],
+    'a': [],
+}
+
+weighted_graph = {
+    'a': {(4, 'b'), (2, 'c'), (4, 'e')},
+    'b': {(5, 'c'), (10, 'd')},
+    'c': {(3, 'e')},
+    'd': {(11, 'f')},
+    'e': {(4, 'd'), (5, 'f')},
+    'f': set(),
+}
+
+# This is `weighted_graph` with each of its weighted edges "expanded".
+expanded_weights_graph = {
+    'a': ['b-1', 'c-1', 'e-1'],
+    'b-1': ['b-2'],
+    'b-2': ['b-3'],
+    'b-3': ['b'],
+    'c-1': ['c'],
+    'e-1': ['e-2'],
+    'e-2': ['e-3'],
+    'e-3': ['e'],
+    # and so on...
+}
+
+unweighted_digraph = {
+    '5': {'2', '0'},
+    '4': {'0', '1'},
+    '3': {'1'},
+    '2': {'3'},
+    '1': set(),
+    '0': set(),
+}
+
+################################################################################
+# Functions
+################################################################################
+
+
+def vertices(xs):
+    result = set()
+    for a, b in xs:
+        result.add(a)
+        result.add(b)
+    return result
+
+
+def edges_to_neighbors(xs):
+    result = {v: set() for v in vertices(xs)}
+    for a, b in xs:
+        result[a].add(b)
+    return result
+
+
+def neighbors_to_edges(xs):
+    result = []
+    for k, ys in xs.items():
+        for y in ys:
+            result.append((k, y))
+    return result
+
+
+def edges_to_adjacencies(xs):
+    return xs
+
+
+# Skipping handling adjacencies because I cannot think of a reasonable use-case
+# for it when the vertex labels are items other than integers. I can think of
+# ways of handling this, but none excite me.
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/graph-coloring.py b/users/wpcarro/scratch/data_structures_and_algorithms/graph-coloring.py
new file mode 100644
index 0000000000..bc7f7ceea5
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/graph-coloring.py
@@ -0,0 +1,180 @@
+import unittest
+from collections import deque
+
+
+################################################################################
+# Solution
+################################################################################
+class GraphNode:
+    def __init__(self, label):
+        self.label = label
+        self.neighbors = set()
+        self.color = None
+
+
+# color_graph :: G(V, E) -> Set(Color) -> IO ()
+def color_graph(graph, colors):
+    q = deque()
+    seen = set()
+    q.append(graph[0])
+
+    while q:
+        node = q.popleft()
+
+        illegal = {n.color for n in node.neighbors}
+        for x in colors:
+            if x not in illegal:
+                node.color = x
+
+        seen.add(node)
+
+        for x in node.neighbors:
+            if x not in seen:
+                q.append(x)
+
+        # TODO: Is this the best way to traverse separate graphs?
+        for x in graph:
+            if x not in seen:
+                q.append(x)
+
+    return 0
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def setUp(self):
+        self.colors = frozenset([
+            'red',
+            'green',
+            'blue',
+            'orange',
+            'yellow',
+            'white',
+        ])
+
+    def assertGraphColoring(self, graph, colors):
+        self.assertGraphHasColors(graph, colors)
+        self.assertGraphColorLimit(graph)
+        for node in graph:
+            self.assertNodeUniqueColor(node)
+
+    def assertGraphHasColors(self, graph, colors):
+        for node in graph:
+            msg = 'Node %r color %r not in %r' % (node.label, node.color,
+                                                  colors)
+            self.assertIn(node.color, colors, msg=msg)
+
+    def assertGraphColorLimit(self, graph):
+        max_degree = 0
+        colors_found = set()
+        for node in graph:
+            degree = len(node.neighbors)
+            max_degree = max(degree, max_degree)
+            colors_found.add(node.color)
+        max_colors = max_degree + 1
+        used_colors = len(colors_found)
+        msg = 'Used %d colors and expected %d at most' % (used_colors,
+                                                          max_colors)
+        self.assertLessEqual(used_colors, max_colors, msg=msg)
+
+    def assertNodeUniqueColor(self, node):
+        for adjacent in node.neighbors:
+            msg = 'Adjacent nodes %r and %r have the same color %r' % (
+                node.label,
+                adjacent.label,
+                node.color,
+            )
+            self.assertNotEqual(node.color, adjacent.color, msg=msg)
+
+    def test_line_graph(self):
+        node_a = GraphNode('a')
+        node_b = GraphNode('b')
+        node_c = GraphNode('c')
+        node_d = GraphNode('d')
+
+        node_a.neighbors.add(node_b)
+        node_b.neighbors.add(node_a)
+        node_b.neighbors.add(node_c)
+        node_c.neighbors.add(node_b)
+        node_c.neighbors.add(node_d)
+        node_d.neighbors.add(node_c)
+
+        graph = [node_a, node_b, node_c, node_d]
+        tampered_colors = list(self.colors)
+        color_graph(graph, tampered_colors)
+        self.assertGraphColoring(graph, self.colors)
+
+    def test_separate_graph(self):
+        node_a = GraphNode('a')
+        node_b = GraphNode('b')
+        node_c = GraphNode('c')
+        node_d = GraphNode('d')
+
+        node_a.neighbors.add(node_b)
+        node_b.neighbors.add(node_a)
+        node_c.neighbors.add(node_d)
+        node_d.neighbors.add(node_c)
+
+        graph = [node_a, node_b, node_c, node_d]
+        tampered_colors = list(self.colors)
+        color_graph(graph, tampered_colors)
+        self.assertGraphColoring(graph, self.colors)
+
+    def test_triangle_graph(self):
+        node_a = GraphNode('a')
+        node_b = GraphNode('b')
+        node_c = GraphNode('c')
+
+        node_a.neighbors.add(node_b)
+        node_a.neighbors.add(node_c)
+        node_b.neighbors.add(node_a)
+        node_b.neighbors.add(node_c)
+        node_c.neighbors.add(node_a)
+        node_c.neighbors.add(node_b)
+
+        graph = [node_a, node_b, node_c]
+        tampered_colors = list(self.colors)
+        color_graph(graph, tampered_colors)
+        self.assertGraphColoring(graph, self.colors)
+
+    def test_envelope_graph(self):
+        node_a = GraphNode('a')
+        node_b = GraphNode('b')
+        node_c = GraphNode('c')
+        node_d = GraphNode('d')
+        node_e = GraphNode('e')
+
+        node_a.neighbors.add(node_b)
+        node_a.neighbors.add(node_c)
+        node_b.neighbors.add(node_a)
+        node_b.neighbors.add(node_c)
+        node_b.neighbors.add(node_d)
+        node_b.neighbors.add(node_e)
+        node_c.neighbors.add(node_a)
+        node_c.neighbors.add(node_b)
+        node_c.neighbors.add(node_d)
+        node_c.neighbors.add(node_e)
+        node_d.neighbors.add(node_b)
+        node_d.neighbors.add(node_c)
+        node_d.neighbors.add(node_e)
+        node_e.neighbors.add(node_b)
+        node_e.neighbors.add(node_c)
+        node_e.neighbors.add(node_d)
+
+        graph = [node_a, node_b, node_c, node_d, node_e]
+        tampered_colors = list(self.colors)
+        color_graph(graph, tampered_colors)
+        self.assertGraphColoring(graph, self.colors)
+
+    def test_loop_graph(self):
+        node_a = GraphNode('a')
+        node_a.neighbors.add(node_a)
+        graph = [node_a]
+        tampered_colors = list(self.colors)
+        with self.assertRaises(Exception):
+            color_graph(graph, tampered_colors)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/graph-to-graphviz.py b/users/wpcarro/scratch/data_structures_and_algorithms/graph-to-graphviz.py
new file mode 100644
index 0000000000..0e7e97a20c
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/graph-to-graphviz.py
@@ -0,0 +1,39 @@
+from graphviz import Digraph
+from collections import deque
+from fixtures import weighted_graph
+
+# There are three ways to model a graph:
+# 1. Edge list: [(Vertex, Vertex)]
+# 2. Neighbors table: Map(Vertex, [Vertex])
+# 3. Adjacency matrix: [[Boolean]]
+#
+# The following graph is a neighbors table.
+
+
+# to_graphviz :: Vertex -> Map(Vertex, [(Vertex, Weight)]) -> String
+def to_graphviz(start, g):
+    """Compiles the graph into GraphViz."""
+    d = Digraph()
+    q = deque()
+    seen = set()
+
+    q.append(start)
+
+    while q:
+        v = q.popleft()
+        if v in seen:
+            continue
+        d.node(v, label=v)
+
+        for w, x in g[v]:
+            d.edge(v, x, label=str(w))
+            q.append(x)
+        seen.add(v)
+
+    return d.source
+
+
+with open('/tmp/test.gv', 'w') as f:
+    src = to_graphviz('a', weighted_graph)
+    f.write(src)
+    print('/tmp/test.gv created!')
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/highest-product-of-3.py b/users/wpcarro/scratch/data_structures_and_algorithms/highest-product-of-3.py
new file mode 100644
index 0000000000..889663e058
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/highest-product-of-3.py
@@ -0,0 +1,89 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# f :: [Int] -> Int
+def highest_product_of_3(xs):
+    """Here we're greedily storing:
+    - current max
+    - largest product of two
+    - largest positive number
+    - second largest positive number
+    - largest negative number
+    """
+    if len(xs) < 3:
+        raise Exception
+
+    cm = None
+    ld = xs[0] * xs[1]
+    l2 = min(xs[0], xs[1])
+    if xs[0] < 0 or xs[1] < 0:
+        ln = min(xs[0], xs[1])
+    else:
+        ln = 1
+    l = max(xs[0], xs[1])
+
+    for x in xs[2:]:
+        if not cm:
+            cm = max(x * ln * l, ld * x, x * l * l2)  # beware
+            ld = max(ld, x * ln, x * l)
+            ln = min(ln, x)
+            l = max(l, x)
+            if x < l:
+                l2 = max(l2, x)
+        else:
+            cm = max(cm, x * ln * l, x * ld, x * l * l2)
+            ld = max(ld, x * ln, x * l)
+            ln = min(ln, x)
+            l = max(l, x)
+            if x < l:
+                l2 = max(l2, x)
+
+    return cm
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_short_list(self):
+        actual = highest_product_of_3([1, 2, 3, 4])
+        expected = 24
+        self.assertEqual(actual, expected)
+
+    def test_longer_list(self):
+        actual = highest_product_of_3([6, 1, 3, 5, 7, 8, 2])
+        expected = 336
+        self.assertEqual(actual, expected)
+
+    def test_list_has_one_negative(self):
+        actual = highest_product_of_3([-5, 4, 8, 2, 3])
+        expected = 96
+        self.assertEqual(actual, expected)
+
+    def test_list_has_two_negatives(self):
+        actual = highest_product_of_3([-10, 1, 3, 2, -10])
+        expected = 300
+        self.assertEqual(actual, expected)
+
+    def test_list_is_all_negatives(self):
+        actual = highest_product_of_3([-5, -1, -3, -2])
+        expected = -6
+        self.assertEqual(actual, expected)
+
+    def test_error_with_empty_list(self):
+        with self.assertRaises(Exception):
+            highest_product_of_3([])
+
+    def test_error_with_one_number(self):
+        with self.assertRaises(Exception):
+            highest_product_of_3([1])
+
+    def test_error_with_two_numbers(self):
+        with self.assertRaises(Exception):
+            highest_product_of_3([1, 1])
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/inflight-entertainment.py b/users/wpcarro/scratch/data_structures_and_algorithms/inflight-entertainment.py
new file mode 100644
index 0000000000..6e17baef37
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/inflight-entertainment.py
@@ -0,0 +1,35 @@
+# possible :: Int -> [Int] -> Bool
+def possible(flight_duration, film_durations):
+    seeking = set()
+
+    for x in film_durations:
+        if x in seeking:
+            return True
+        else:
+            seeking.add(flight_duration - x)
+
+    return False
+
+
+should = [
+    (10, [1, 9, 8, 8, 8]),
+    (10, [1, 9]),
+    (10, [1, 9, 5, 5, 6]),
+    (1, [0.5, 0.5]),
+    (1, [0.5, 0.5]),
+]
+
+for a, b in should:
+    print("Testing: %s %s" % (a, b))
+    assert possible(a, b)
+
+shouldnt = [
+    (10, [1, 10, 1, 2, 1, 12]),
+    (1, [0.25, 0.25, 0.25, 0.25]),
+    (5, [1, 2, 2]),
+]
+for a, b in shouldnt:
+    print("Testing: %s %s" % (a, b))
+    assert not possible(a, b)
+
+print("Tests pass")
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/knapsack-0-1.py b/users/wpcarro/scratch/data_structures_and_algorithms/knapsack-0-1.py
new file mode 100644
index 0000000000..c72d19d4ed
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/knapsack-0-1.py
@@ -0,0 +1,38 @@
+import unittest
+from math import floor
+
+
+def knapify(xs, capacity=None):
+    assert capacity is not None
+    n = len(xs)
+    # For 0/1 Knapsack, we must use a table, since this will encode which values
+    # work for which items. This is cleaner than including a separate data
+    # structure to capture it.
+    maxes = [[0 for x in range(capacity + 1)] for x in range(n + 1)]
+
+    # Build table maxes[][] in bottom up manner
+    for row in range(n + 1):
+        for col in range(capacity + 1):
+            if row == 0 or col == 0:
+                maxes[row][col] = 0
+            elif xs[row - 1][0] <= col:
+                maxes[row][col] = max(
+                    xs[row - 1][1] + maxes[row - 1][col - xs[row - 1][0]],
+                    maxes[row - 1][col])
+            else:
+                maxes[row][col] = maxes[row - 1][col]
+
+    return maxes[-1][capacity]
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_one_cake(self):
+        actual = knapify([(3, 10), (2, 15), (7, 2), (12, 20)], capacity=12)
+        expected = None
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/kth-to-last.py b/users/wpcarro/scratch/data_structures_and_algorithms/kth-to-last.py
new file mode 100644
index 0000000000..8291e54533
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/kth-to-last.py
@@ -0,0 +1,82 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def length(x):
+    if not x:
+        return 0
+    else:
+        count = 1
+        while x:
+            x = x.next
+            count += 1
+        return count
+
+
+def kth_to_last_node(k, x):
+    hops = length(x) - 1
+    dest = hops - k
+
+    if k == 0:
+        raise Exception("Our God doesn't support this kind of behavior.")
+
+    if dest < 0:
+        raise Exception('Value k to high for list.')
+
+    while dest > 0:
+        x = x.next
+        dest -= 1
+
+    return x
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+        def get_values(self):
+            node = self
+            values = []
+            while node is not None:
+                values.append(node.value)
+                node = node.next
+            return values
+
+    def setUp(self):
+        self.fourth = Test.LinkedListNode(4)
+        self.third = Test.LinkedListNode(3, self.fourth)
+        self.second = Test.LinkedListNode(2, self.third)
+        self.first = Test.LinkedListNode(1, self.second)
+
+    def test_first_to_last_node(self):
+        actual = kth_to_last_node(1, self.first)
+        expected = self.fourth
+        self.assertEqual(actual, expected)
+
+    def test_second_to_last_node(self):
+        actual = kth_to_last_node(2, self.first)
+        expected = self.third
+        self.assertEqual(actual, expected)
+
+    def test_first_node(self):
+        actual = kth_to_last_node(4, self.first)
+        expected = self.first
+        self.assertEqual(actual, expected)
+
+    def test_k_greater_than_linked_list_length(self):
+        with self.assertRaises(Exception):
+            kth_to_last_node(5, self.first)
+
+    def test_k_is_zero(self):
+        with self.assertRaises(Exception):
+            kth_to_last_node(0, self.first)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/largest-stack.py b/users/wpcarro/scratch/data_structures_and_algorithms/largest-stack.py
new file mode 100644
index 0000000000..aab9671eb6
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/largest-stack.py
@@ -0,0 +1,107 @@
+import unittest
+
+
+class Stack(object):
+    def __init__(self):
+        """Initialize an empty stack"""
+        self.items = []
+
+    def push(self, item):
+        """Push a new item onto the stack"""
+        self.items.append(item)
+
+    def pop(self):
+        """Remove and return the last item"""
+        # If the stack is empty, return None
+        # (it would also be reasonable to throw an exception)
+        if not self.items:
+            return None
+
+        return self.items.pop()
+
+    def peek(self):
+        """Return the last item without removing it"""
+        if not self.items:
+            return None
+        return self.items[-1]
+
+
+class MaxStack(object):
+    # Implement the push, pop, and get_max methods
+    def __init__(self):
+        self.m = Stack()
+        self.stack = Stack()
+
+    def push(self, item):
+        if self.m.peek() is None:
+            self.m.push(item)
+        elif item >= self.m.peek():
+            self.m.push(item)
+        self.stack.push(item)
+
+    def pop(self):
+        x = self.stack.pop()
+        if x == self.m.peek():
+            self.m.pop()
+        return x
+
+    def get_max(self):
+        return self.m.peek()
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_stack_usage(self):
+        max_stack = MaxStack()
+
+        max_stack.push(5)
+
+        actual = max_stack.get_max()
+        expected = 5
+        self.assertEqual(actual, expected)
+
+        max_stack.push(4)
+        max_stack.push(7)
+        max_stack.push(7)
+        max_stack.push(8)
+
+        actual = max_stack.get_max()
+        expected = 8
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.pop()
+        expected = 8
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.get_max()
+        expected = 7
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.pop()
+        expected = 7
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.get_max()
+        expected = 7
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.pop()
+        expected = 7
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.get_max()
+        expected = 5
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.pop()
+        expected = 4
+        self.assertEqual(actual, expected)
+
+        actual = max_stack.get_max()
+        expected = 5
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/linked-list-cycles.py b/users/wpcarro/scratch/data_structures_and_algorithms/linked-list-cycles.py
new file mode 100644
index 0000000000..75a4b99394
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/linked-list-cycles.py
@@ -0,0 +1,88 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def contains_cycle(x):
+    if not x:
+        return False
+    elif not x.next:
+        return False
+
+    a = x
+    b = x.next
+
+    while b.next:
+        if a == b:
+            return True
+
+        a = a.next
+        b = b.next.next
+
+    return False
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+    def test_linked_list_with_no_cycle(self):
+        fourth = Test.LinkedListNode(4)
+        third = Test.LinkedListNode(3, fourth)
+        second = Test.LinkedListNode(2, third)
+        first = Test.LinkedListNode(1, second)
+        result = contains_cycle(first)
+        self.assertFalse(result)
+
+    def test_cycle_loops_to_beginning(self):
+        fourth = Test.LinkedListNode(4)
+        third = Test.LinkedListNode(3, fourth)
+        second = Test.LinkedListNode(2, third)
+        first = Test.LinkedListNode(1, second)
+        fourth.next = first
+        result = contains_cycle(first)
+        self.assertTrue(result)
+
+    def test_cycle_loops_to_middle(self):
+        fifth = Test.LinkedListNode(5)
+        fourth = Test.LinkedListNode(4, fifth)
+        third = Test.LinkedListNode(3, fourth)
+        second = Test.LinkedListNode(2, third)
+        first = Test.LinkedListNode(1, second)
+        fifth.next = third
+        result = contains_cycle(first)
+        self.assertTrue(result)
+
+    def test_two_node_cycle_at_end(self):
+        fifth = Test.LinkedListNode(5)
+        fourth = Test.LinkedListNode(4, fifth)
+        third = Test.LinkedListNode(3, fourth)
+        second = Test.LinkedListNode(2, third)
+        first = Test.LinkedListNode(1, second)
+        fifth.next = fourth
+        result = contains_cycle(first)
+        self.assertTrue(result)
+
+    def test_empty_list(self):
+        result = contains_cycle(None)
+        self.assertFalse(result)
+
+    def test_one_element_linked_list_no_cycle(self):
+        first = Test.LinkedListNode(1)
+        result = contains_cycle(first)
+        self.assertFalse(result)
+
+    def test_one_element_linked_list_cycle(self):
+        first = Test.LinkedListNode(1)
+        first.next = first
+        result = contains_cycle(first)
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/memo.py b/users/wpcarro/scratch/data_structures_and_algorithms/memo.py
new file mode 100644
index 0000000000..44ea93e1bd
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/memo.py
@@ -0,0 +1,60 @@
+import time
+import random
+from heapq import heappush, heappop
+
+
+class Memo(object):
+    def __init__(self, size=1):
+        """
+        Create a key-value data-structure that will never exceed `size`
+        members. Memo evicts the least-recently-accessed elements from itself
+        before adding inserting new key-value pairs.
+        """
+        if size <= 0:
+            raise Exception("We do not support an empty memo")
+        self.xs = {}
+        self.heap = [(0, None)] * size
+
+    def contains(self, k):
+        """
+        Return true if key `k` exists in the Memo.
+        """
+        return k in self.xs
+
+    def get(self, k):
+        """
+        Return the memoized item at key `k`.
+        """
+        # "touch" the element in the heap
+        return self.xs[k]
+
+    def set(self, k, v):
+        """
+        Memoize value `v` at key `k`.
+        """
+        _, to_evict = heappop(self.heap)
+        if to_evict != None:
+            del self.xs[to_evict]
+        heappush(self.heap, (time.time(), k))
+        self.xs[k] = v
+
+
+memo = Memo(size=10)
+
+
+def f(x):
+    """
+    Compute some mysterious, expensive function.
+    """
+    if memo.contains(x):
+        print("Hit.\t\tf({})".format(x))
+        return memo.get(x)
+    else:
+        print("Computing...\tf({})".format(x))
+        time.sleep(0.25)
+        res = random.randint(0, 10)
+        memo.set(x, res)
+        return res
+
+
+[f(random.randint(0, 10)) for _ in range(10)]
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/merge-sort.py b/users/wpcarro/scratch/data_structures_and_algorithms/merge-sort.py
new file mode 100644
index 0000000000..6dbe0fa0f3
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/merge-sort.py
@@ -0,0 +1,28 @@
+
+
+
+# merge :: [a] -> [a] -> [a]
+# merge([], []): []
+# merge(xs, []): xs
+# merge([], ys): ys
+# merge(xs@[x|xs'], ys@[y|ys'])
+#   when y =< x: cons(y, merge(xs, ys'))
+#   when x < y:  cons(x, merge(xs', ys))
+def merge(xs, ys):
+    if xs == [] and ys == []:
+        return []
+    elif ys == []:
+        return xs
+    elif xs == []:
+        return ys
+    else:
+        x = xs[0]
+        y = ys[0]
+
+        if y <= x:
+            return [y] + merge(xs, ys[1:])
+        else:
+            return [x] + merge(xs[1:], ys)
+        
+print(merge([3, 4, 6, 10, 11, 15],
+            [1, 5, 8, 12, 14, 19]))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/merging-ranges.py b/users/wpcarro/scratch/data_structures_and_algorithms/merging-ranges.py
new file mode 100644
index 0000000000..4e3604d5bc
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/merging-ranges.py
@@ -0,0 +1,94 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# do_merge_ranges :: [(Int, Int)] -> [(Int, Int)] -> [(Int, Int)]
+def do_merge_ranges(prev, xs):
+    if len(xs) == 0:
+        return prev
+    elif len(xs) == 1:
+        return prev + xs
+    else:
+        a1, a2 = xs[0]
+        b1, b2 = xs[1]
+        rest = xs[2:]
+        if b1 <= a2:
+            return do_merge_ranges(prev, [(a1, max(a2, b2))] + rest)
+        else:
+            return do_merge_ranges(prev + [(a1, a2)], [(b1, b2)] + rest)
+
+
+# merge_ranges :: [(Int, Int)] -> [(Int, Int)]
+def merge_ranges(xs):
+    xs = xs[:]
+    xs.sort()
+    return do_merge_ranges([], xs)
+
+
+# merge_ranges_b :: [(Int, Int)] -> [(Int, Int)]
+def merge_ranges_b(xs):
+    fi = 0
+    ci = 1
+    result = []
+    xs = xs[:]
+    xs.sort()
+    while ci < len(xs):
+        while ci < len(xs) and xs[ci][0] <= xs[fi][1]:
+            xs[fi] = xs[fi][0], max(xs[ci][1], xs[fi][1])
+            ci += 1
+        result.append(xs[fi])
+        fi = ci
+        ci += 1
+    if fi < len(xs):
+        result.append(xs[fi])
+    return result
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_meetings_overlap(self):
+        actual = merge_ranges([(1, 3), (2, 4)])
+        expected = [(1, 4)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_touch(self):
+        actual = merge_ranges([(5, 6), (6, 8)])
+        expected = [(5, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meeting_contains_other_meeting(self):
+        actual = merge_ranges([(1, 8), (2, 5)])
+        expected = [(1, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_stay_separate(self):
+        actual = merge_ranges([(1, 3), (4, 8)])
+        expected = [(1, 3), (4, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_multiple_merged_meetings(self):
+        actual = merge_ranges([(1, 4), (2, 5), (5, 8)])
+        expected = [(1, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_not_sorted(self):
+        actual = merge_ranges([(5, 8), (1, 4), (6, 8)])
+        expected = [(1, 4), (5, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_one_long_meeting_contains_smaller_meetings(self):
+        actual = merge_ranges([(1, 10), (2, 5), (6, 8), (9, 10), (10, 12)])
+        expected = [(1, 12)]
+        self.assertEqual(actual, expected)
+
+    def test_sample_input(self):
+        actual = merge_ranges([(0, 1), (3, 5), (4, 8), (10, 12), (9, 10)])
+        expected = [(0, 1), (3, 8), (9, 12)]
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.gv b/users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.gv
new file mode 100644
index 0000000000..1e67c3954f
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.gv
@@ -0,0 +1,11 @@
+strict graph {
+    Min -- {William, Jayden, Omar}
+    William -- {Min, Noam}
+    Jayden -- {Min, Amelia, Ren, Noam}
+    Adam -- {Amelia, Miguel, Sofia, Lucas}
+    Ren -- {Jayden, Omar}
+    Amelia -- {Jayden, Adam, Miguel}
+    Miguel -- {Amelia, Adam, Liam, Nathan}
+    Noam -- {Nathan, Jayden, William}
+    Omar -- {Ren, Min, Scott}
+}
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.py b/users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.py
new file mode 100644
index 0000000000..c9d7d9d741
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/mesh-message.py
@@ -0,0 +1,97 @@
+import unittest
+from collections import deque
+
+
+################################################################################
+# Solution
+################################################################################
+# get_path :: G(V, E) -> V -> V -> Maybe([V])
+def get_path(g, src, dst):
+    q = deque()
+    result = None
+    seen = set()
+    q.append(([], src))
+
+    if src not in g or dst not in g:
+        raise Exception
+
+    while q:
+        p, node = q.popleft()
+
+        seen.add(node)
+
+        if node == dst:
+            if not result:
+                result = p + [node]
+            elif len(p + [node]) < len(result):
+                result = p + [node]
+        else:
+            if node not in g:
+                raise Exception
+            for x in g.get(node):
+                if not x in seen:
+                    q.append((p + [node], x))
+
+    return result
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def setUp(self):
+        self.graph = {
+            'a': ['b', 'c', 'd'],
+            'b': ['a', 'd'],
+            'c': ['a', 'e'],
+            'd': ['a', 'b'],
+            'e': ['c'],
+            'f': ['g'],
+            'g': ['f'],
+        }
+
+    def test_two_hop_path_1(self):
+        actual = get_path(self.graph, 'a', 'e')
+        expected = ['a', 'c', 'e']
+        self.assertEqual(actual, expected)
+
+    def test_two_hop_path_2(self):
+        actual = get_path(self.graph, 'd', 'c')
+        expected = ['d', 'a', 'c']
+        self.assertEqual(actual, expected)
+
+    def test_one_hop_path_1(self):
+        actual = get_path(self.graph, 'a', 'c')
+        expected = ['a', 'c']
+        self.assertEqual(actual, expected)
+
+    def test_one_hop_path_2(self):
+        actual = get_path(self.graph, 'f', 'g')
+        expected = ['f', 'g']
+        self.assertEqual(actual, expected)
+
+    def test_one_hop_path_3(self):
+        actual = get_path(self.graph, 'g', 'f')
+        expected = ['g', 'f']
+        self.assertEqual(actual, expected)
+
+    def test_zero_hop_path(self):
+        actual = get_path(self.graph, 'a', 'a')
+        expected = ['a']
+        self.assertEqual(actual, expected)
+
+    def test_no_path(self):
+        actual = get_path(self.graph, 'a', 'f')
+        expected = None
+        self.assertEqual(actual, expected)
+
+    def test_start_node_not_present(self):
+        with self.assertRaises(Exception):
+            get_path(self.graph, 'h', 'a')
+
+    def test_end_node_not_present(self):
+        with self.assertRaises(Exception):
+            get_path(self.graph, 'a', 'h')
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/norman.py b/users/wpcarro/scratch/data_structures_and_algorithms/norman.py
new file mode 100644
index 0000000000..379ba92abb
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/norman.py
@@ -0,0 +1,78 @@
+
+
+
+# Write a function with the following type signature:L
+# equal? :: String -> String -> Bool
+#
+# Determine equality between two inputs with backspace characters encoded as
+# "<".
+
+################################################################################
+# Solution 1
+################################################################################
+
+# from collections import deque
+
+# def equal(a, b):
+#     sa = deque()
+#     sb = deque()
+
+#     for c in a:
+#         if c == '<':
+#             sa.pop()
+#         else:
+#             sa.append(c)
+
+#     for c in b:
+#         if c == '<':
+#             sb.pop()
+#         else:
+#             sb.append(c)
+
+#     return sa == sb
+
+################################################################################
+# Solution 2
+################################################################################
+
+def handle_dels(num_dels, i, xs):
+    if i < 0:
+        return -1
+
+    while xs[i] == '<':
+        num_dels += 1
+        i -= 1
+
+    while num_dels > 0 and xs[i] != '<':
+        num_dels -= 1
+        i -= 1
+
+    if xs[i] == '<':
+        return handle_dels(num_dels, i, xs)
+    else:
+        return i
+
+def update_index(i, xs):
+    # TODO: Indexing into non-available parts of a string.
+    if xs[i] != '<' and xs[i - 1] != '<':
+        return i - 1
+
+    elif xs[i - 1] == '<':
+        return handle_dels(0, i - 1, xs)
+
+def equal(a, b):
+    ia = len(a) - 1
+    ib = len(b) - 1
+
+    while ia >= 0 and ib >= 0:
+        if a[ia] != b[ib]:
+            return False
+        ia = update_index(ia, a)
+        ib = update_index(ib, b)
+
+    if ia != 0:
+        return update_index(ia, a) <= -1
+    if ib != 0:
+        return update_index(ib, b) <= -1
+
+    return True
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/nth-fibonacci.py b/users/wpcarro/scratch/data_structures_and_algorithms/nth-fibonacci.py
new file mode 100644
index 0000000000..cdb2846ea3
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/nth-fibonacci.py
@@ -0,0 +1,59 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+def fib(n):
+    """This should be accomplishable in O(1) space."""
+    if n in {0, 1}:
+        return n
+    a = 0  # i = 0
+    b = 1  # i = 1
+    for x in range(2, n + 1):
+        result = a + b
+        a = b
+        b = result
+    return result
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_zeroth_fibonacci(self):
+        actual = fib(0)
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_first_fibonacci(self):
+        actual = fib(1)
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_second_fibonacci(self):
+        actual = fib(2)
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_third_fibonacci(self):
+        actual = fib(3)
+        expected = 2
+        self.assertEqual(actual, expected)
+
+    def test_fifth_fibonacci(self):
+        actual = fib(5)
+        expected = 5
+        self.assertEqual(actual, expected)
+
+    def test_tenth_fibonacci(self):
+        actual = fib(10)
+        expected = 55
+        self.assertEqual(actual, expected)
+
+    def test_negative_fibonacci(self):
+        with self.assertRaises(Exception):
+            fib(-1)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/optimal-stopping.py b/users/wpcarro/scratch/data_structures_and_algorithms/optimal-stopping.py
new file mode 100644
index 0000000000..af13239941
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/optimal-stopping.py
@@ -0,0 +1,49 @@
+from random import choice
+from math import floor
+
+# Applying Chapter 1 from "Algorithms to Live By", which describes optimal
+# stopping problems. Technically this simulation is invalid because the
+# `candidates` function takes a lower bound and an upper bound, which allows us
+# to know the cardinal number of an individual candidates. The "look then leap"
+# algorithm is ideal for no-information games - i.e. games when upper and lower
+# bounds aren't known. The `look_then_leap/1` function is ignorant of this
+# information, so it behaves as if in a no-information game. Strangely enough,
+# this algorithm will pick the best candidate 37% of the time.
+#
+# Chapter 1 describes two algorithms:
+# 1. Look-then-leap: ordinal numbers - i.e. no-information games. Look-then-leap
+#    finds the best candidate 37% of the time.
+# 2. Threshold: cardinal numbers - i.e. where upper and lower bounds are
+#    known. The Threshold algorithm finds the best candidate ~55% of the time.
+#
+# All of this and more can be studied as "optimal stopping theory". This applies
+# to finding a spouse, parking a car, picking an apartment in a city, and more.
+
+
+# candidates :: Int -> Int -> Int -> [Int]
+def candidates(lb, ub, ct):
+    xs = list(range(lb, ub + 1))
+    return [choice(xs) for _ in range(ct)]
+
+
+# look_then_leap :: [Integer] -> Integer
+def look_then_leap(candidates):
+    best = candidates[0]
+    seen_ct = 1
+    ignore_ct = floor(len(candidates) * 0.37)
+    for x in candidates[1:]:
+        if ignore_ct > 0:
+            ignore_ct -= 1
+            best = max(best, x)
+        else:
+            if x > best:
+                print('Choosing the {} candidate.'.format(seen_ct))
+                return x
+        seen_ct += 1
+    print('You may have waited too long.')
+    return candidates[-1]
+
+
+candidates = candidates(1, 100, 100)
+print(candidates)
+print(look_then_leap(candidates))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/perm-tree.py b/users/wpcarro/scratch/data_structures_and_algorithms/perm-tree.py
new file mode 100644
index 0000000000..0eb389c26b
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/perm-tree.py
@@ -0,0 +1,83 @@
+import unittest
+
+
+################################################################################
+# Answer
+################################################################################
+class Node(object):
+    def __init__(self, value, children=set()):
+        self.value = value
+        self.children = children
+
+
+# treeify :: Char -> Set(Char) -> Node(Char)
+def treeify(x, xs):
+    return Node(x, [treeify(c, xs - {c}) for c in xs])
+
+
+# dft :: Node(Char) -> [String]
+def dft(node):
+    result = []
+    s = []
+
+    s.append(('', node))
+
+    while s:
+        p, n = s.pop()
+        p += str(n.value)
+
+        if not n.children:
+            result.append(p)
+        else:
+            for c in n.children:
+                s.append((p, c))
+
+    return result
+
+
+# main :: String -> Set(String)
+def get_permutations(xs):
+    if xs == '':
+        return set([''])
+
+    ys = set(xs)
+    trees = []
+
+    for y in ys:
+        trees.append(treeify(y, ys - {y}))
+
+    result = set()
+
+    for t in trees:
+        for d in dft(t):
+            result.add(d)
+
+    return result
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_empty_string(self):
+        actual = get_permutations('')
+        expected = set([''])
+        self.assertEqual(actual, expected)
+
+    def test_one_character_string(self):
+        actual = get_permutations('a')
+        expected = set(['a'])
+        self.assertEqual(actual, expected)
+
+    def test_two_character_string(self):
+        actual = get_permutations('ab')
+        expected = set(['ab', 'ba'])
+        self.assertEqual(actual, expected)
+
+    def test_three_character_string(self):
+        actual = get_permutations('abc')
+        expected = set(['abc', 'acb', 'bac', 'bca', 'cab', 'cba'])
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/permutation-palindrome.py b/users/wpcarro/scratch/data_structures_and_algorithms/permutation-palindrome.py
new file mode 100644
index 0000000000..0a2136a408
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/permutation-palindrome.py
@@ -0,0 +1,49 @@
+from collections import Counter
+import unittest
+
+
+################################################################################
+# Impl
+################################################################################
+# palindromifiable :: String -> Boolean
+def has_palindrome_permutation(x):
+    bag = Counter(x)
+    odd_entries_ct = 0
+
+    for _, y in bag.items():
+        if y % 2 != 0:
+            odd_entries_ct += 1
+
+    return odd_entries_ct in {0, 1}
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_permutation_with_odd_number_of_chars(self):
+        result = has_palindrome_permutation('aabcbcd')
+        self.assertTrue(result)
+
+    def test_permutation_with_even_number_of_chars(self):
+        result = has_palindrome_permutation('aabccbdd')
+        self.assertTrue(result)
+
+    def test_no_permutation_with_odd_number_of_chars(self):
+        result = has_palindrome_permutation('aabcd')
+        self.assertFalse(result)
+
+    def test_no_permutation_with_even_number_of_chars(self):
+        result = has_palindrome_permutation('aabbcd')
+        self.assertFalse(result)
+
+    def test_empty_string(self):
+        result = has_palindrome_permutation('')
+        self.assertTrue(result)
+
+    def test_one_character_string(self):
+        result = has_palindrome_permutation('a')
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/permutations.py b/users/wpcarro/scratch/data_structures_and_algorithms/permutations.py
new file mode 100644
index 0000000000..fc2c1ef7ee
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/permutations.py
@@ -0,0 +1,55 @@
+class Node(object):
+    # ctor :: a -> [a] -> Node(a)
+    def __init__(self, value, children=[]):
+        self.value = value
+        self.children = children
+
+
+# is_leaf :: Node(a) -> Boolean
+def is_leaf(node):
+    return len(node.children) == 0
+
+
+# enumerate :: Node(a) -> Set(List(a))
+def enumerate(node):
+    current = []
+    result = []
+    q = []
+
+    q.append(node)
+
+    while q:
+        x = q.pop()
+        print(x.value)
+
+        for c in x.children:
+            q.append(c)
+
+        current.append(x.value)
+        print(current)
+
+        if is_leaf(x):
+            result.append(current)
+            print("Reseting current")
+            current = []
+
+    return result
+
+
+node = Node('root', [
+    Node('a', [
+        Node('b', [Node('c')]),
+        Node('c', [Node('b')]),
+    ]),
+    Node('b', [
+        Node('a', [Node('c')]),
+        Node('c', [Node('a')]),
+    ]),
+    Node('c', [
+        Node('a', [Node('b')]),
+        Node('b', [Node('a')]),
+    ])
+])
+
+print('----------')
+print(enumerate(node))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/plot.py b/users/wpcarro/scratch/data_structures_and_algorithms/plot.py
new file mode 100644
index 0000000000..5601891a0d
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/plot.py
@@ -0,0 +1,9 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+rng = np.random.RandomState(10)  # deterministic random data
+a = np.hstack((rng.normal(size=1000), rng.normal(loc=5, scale=2, size=1000)))
+_ = plt.hist(a, bins='auto')  # arguments are passed to np.histogram
+plt.title("Histogram with 'auto' bins")
+Text(0.5, 1.0, "Histogram with 'auto' bins")
+plt.show()
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/product-of-other-numbers.py b/users/wpcarro/scratch/data_structures_and_algorithms/product-of-other-numbers.py
new file mode 100644
index 0000000000..d05e82d42b
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/product-of-other-numbers.py
@@ -0,0 +1,68 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# f :: [Int] -> [Int]
+def get_products_of_all_ints_except_at_index(xs):
+    if len(xs) in {0, 1}:
+        raise Exception
+
+    ct = len(xs)
+    lefts = [1] * ct
+    rights = [1] * ct
+    result = []
+
+    for i in range(1, ct):
+        lefts[i] = lefts[i - 1] * xs[i - 1]
+    for i in range(ct - 2, -1, -1):
+        rights[i] = rights[i + 1] * xs[i + 1]
+
+    return [lefts[i] * rights[i] for i in range(ct)]
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_small_list(self):
+        actual = get_products_of_all_ints_except_at_index([1, 2, 3])
+        expected = [6, 3, 2]
+        self.assertEqual(actual, expected)
+
+    def test_longer_list(self):
+        actual = get_products_of_all_ints_except_at_index([8, 2, 4, 3, 1, 5])
+        expected = [120, 480, 240, 320, 960, 192]
+        self.assertEqual(actual, expected)
+
+    def test_list_has_one_zero(self):
+        actual = get_products_of_all_ints_except_at_index([6, 2, 0, 3])
+        expected = [0, 0, 36, 0]
+        self.assertEqual(actual, expected)
+
+    def test_list_has_two_zeros(self):
+        actual = get_products_of_all_ints_except_at_index([4, 0, 9, 1, 0])
+        expected = [0, 0, 0, 0, 0]
+        self.assertEqual(actual, expected)
+
+    def test_one_negative_number(self):
+        actual = get_products_of_all_ints_except_at_index([-3, 8, 4])
+        expected = [32, -12, -24]
+        self.assertEqual(actual, expected)
+
+    def test_all_negative_numbers(self):
+        actual = get_products_of_all_ints_except_at_index([-7, -1, -4, -2])
+        expected = [-8, -56, -14, -28]
+        self.assertEqual(actual, expected)
+
+    def test_error_with_empty_list(self):
+        with self.assertRaises(Exception):
+            get_products_of_all_ints_except_at_index([])
+
+    def test_error_with_one_number(self):
+        with self.assertRaises(Exception):
+            get_products_of_all_ints_except_at_index([1])
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/queue-two-stacks.py b/users/wpcarro/scratch/data_structures_and_algorithms/queue-two-stacks.py
new file mode 100644
index 0000000000..63da08ebf7
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/queue-two-stacks.py
@@ -0,0 +1,66 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+class QueueTwoStacks(object):
+    def __init__(self):
+        self.a = []
+        self.b = []
+
+    def enqueue(self, x):
+        self.a.append(x)
+
+    def dequeue(self):
+        if self.b:
+            return self.b.pop()
+        else:
+            while self.a:
+                self.b.append(self.a.pop())
+            return self.dequeue()
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_basic_queue_operations(self):
+        queue = QueueTwoStacks()
+        queue.enqueue(1)
+        queue.enqueue(2)
+        queue.enqueue(3)
+        actual = queue.dequeue()
+        expected = 1
+        self.assertEqual(actual, expected)
+        actual = queue.dequeue()
+        expected = 2
+        self.assertEqual(actual, expected)
+        queue.enqueue(4)
+        actual = queue.dequeue()
+        expected = 3
+        self.assertEqual(actual, expected)
+        actual = queue.dequeue()
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_error_when_dequeue_from_new_queue(self):
+        queue = QueueTwoStacks()
+        with self.assertRaises(Exception):
+            queue.dequeue()
+
+    def test_error_when_dequeue_from_empty_queue(self):
+        queue = QueueTwoStacks()
+        queue.enqueue(1)
+        queue.enqueue(2)
+        actual = queue.dequeue()
+        expected = 1
+        self.assertEqual(actual, expected)
+        actual = queue.dequeue()
+        expected = 2
+        self.assertEqual(actual, expected)
+        with self.assertRaises(Exception):
+            queue.dequeue()
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/rectangular-love.py b/users/wpcarro/scratch/data_structures_and_algorithms/rectangular-love.py
new file mode 100644
index 0000000000..47c0f53979
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/rectangular-love.py
@@ -0,0 +1,246 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# bottom :: Rectangle -> Int
+def bottom(x):
+    return x.get('bottom_y')
+
+
+# top :: Rectangle -> Int
+def top(x):
+    return bottom(x) + x.get('height')
+
+
+# left :: Rectangle -> Int
+def left(x):
+    return x.get('left_x')
+
+
+# right :: Rectangle -> Int
+def right(x):
+    return left(x) + x.get('width')
+
+
+# sort_highest :: Rectangle -> Rectangle -> (Rectangle, Rectangle)
+def sort_highest(x, y):
+    if top(x) >= top(y):
+        return x, y
+    else:
+        return y, x
+
+
+# sort_leftmost :: Rectangle -> Rectangle -> (Rectangle, Rectangle)
+def sort_leftmost(x, y):
+    if left(x) <= left(y):
+        return x, y
+    else:
+        return y, x
+
+
+# rectify :: Int -> Int -> Int -> Int -> Rectify
+def rectify(top=None, bottom=None, left=None, right=None):
+    assert top >= bottom
+    assert left <= right
+    return {
+        'left_x': left,
+        'bottom_y': bottom,
+        'width': right - left,
+        'height': top - bottom,
+    }
+
+
+# empty_rect :: Rectangle
+def empty_rect():
+    return {
+        'left_x': None,
+        'bottom_y': None,
+        'width': None,
+        'height': None,
+    }
+
+
+# find_rectangular_overlap :: Rectangle -> Rectangle -> Maybe(Rectangle)
+def find_rectangular_overlap(x, y):
+    ha, hb = sort_highest(x, y)
+    la, lb = sort_leftmost(x, y)
+
+    if bottom(hb) <= top(hb) <= bottom(ha) <= top(ha):
+        return empty_rect()
+
+    if left(la) <= right(la) <= left(lb) <= right(lb):
+        return empty_rect()
+
+    # We should have an intersection here.
+    verts = [bottom(ha), top(ha), bottom(hb), top(hb)]
+    verts.sort()
+    horzs = [left(la), right(la), left(lb), right(lb)]
+    horzs.sort()
+    return rectify(top=verts[2],
+                   bottom=verts[1],
+                   left=horzs[1],
+                   right=horzs[2])
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_overlap_along_both_axes(self):
+        rect1 = {
+            'left_x': 1,
+            'bottom_y': 1,
+            'width': 6,
+            'height': 3,
+        }
+        rect2 = {
+            'left_x': 5,
+            'bottom_y': 2,
+            'width': 3,
+            'height': 6,
+        }
+        expected = {
+            'left_x': 5,
+            'bottom_y': 2,
+            'width': 2,
+            'height': 2,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+    def test_one_rectangle_inside_another(self):
+        rect1 = {
+            'left_x': 1,
+            'bottom_y': 1,
+            'width': 6,
+            'height': 6,
+        }
+        rect2 = {
+            'left_x': 3,
+            'bottom_y': 3,
+            'width': 2,
+            'height': 2,
+        }
+        expected = {
+            'left_x': 3,
+            'bottom_y': 3,
+            'width': 2,
+            'height': 2,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+    def test_both_rectangles_the_same(self):
+        rect1 = {
+            'left_x': 2,
+            'bottom_y': 2,
+            'width': 4,
+            'height': 4,
+        }
+        rect2 = {
+            'left_x': 2,
+            'bottom_y': 2,
+            'width': 4,
+            'height': 4,
+        }
+        expected = {
+            'left_x': 2,
+            'bottom_y': 2,
+            'width': 4,
+            'height': 4,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+    def test_touch_on_horizontal_edge(self):
+        rect1 = {
+            'left_x': 1,
+            'bottom_y': 2,
+            'width': 3,
+            'height': 4,
+        }
+        rect2 = {
+            'left_x': 2,
+            'bottom_y': 6,
+            'width': 2,
+            'height': 2,
+        }
+        expected = {
+            'left_x': None,
+            'bottom_y': None,
+            'width': None,
+            'height': None,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+    def test_touch_on_vertical_edge(self):
+        rect1 = {
+            'left_x': 1,
+            'bottom_y': 2,
+            'width': 3,
+            'height': 4,
+        }
+        rect2 = {
+            'left_x': 4,
+            'bottom_y': 3,
+            'width': 2,
+            'height': 2,
+        }
+        expected = {
+            'left_x': None,
+            'bottom_y': None,
+            'width': None,
+            'height': None,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+    def test_touch_at_a_corner(self):
+        rect1 = {
+            'left_x': 1,
+            'bottom_y': 1,
+            'width': 2,
+            'height': 2,
+        }
+        rect2 = {
+            'left_x': 3,
+            'bottom_y': 3,
+            'width': 2,
+            'height': 2,
+        }
+        expected = {
+            'left_x': None,
+            'bottom_y': None,
+            'width': None,
+            'height': None,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+    def test_no_overlap(self):
+        rect1 = {
+            'left_x': 1,
+            'bottom_y': 1,
+            'width': 2,
+            'height': 2,
+        }
+        rect2 = {
+            'left_x': 4,
+            'bottom_y': 6,
+            'width': 3,
+            'height': 6,
+        }
+        expected = {
+            'left_x': None,
+            'bottom_y': None,
+            'width': None,
+            'height': None,
+        }
+        actual = find_rectangular_overlap(rect1, rect2)
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/recursive-string-permutations.py b/users/wpcarro/scratch/data_structures_and_algorithms/recursive-string-permutations.py
new file mode 100644
index 0000000000..70461ddf5d
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/recursive-string-permutations.py
@@ -0,0 +1,37 @@
+import unittest
+
+
+################################################################################
+# Implementation
+################################################################################
+# get_permutations :: String -> Set(String)
+def get_permutations(string):
+    pass
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_empty_string(self):
+        actual = get_permutations('')
+        expected = set([''])
+        self.assertEqual(actual, expected)
+
+    def test_one_character_string(self):
+        actual = get_permutations('a')
+        expected = set(['a'])
+        self.assertEqual(actual, expected)
+
+    def test_two_character_string(self):
+        actual = get_permutations('ab')
+        expected = set(['ab', 'ba'])
+        self.assertEqual(actual, expected)
+
+    def test_three_character_string(self):
+        actual = get_permutations('abc')
+        expected = set(['abc', 'acb', 'bac', 'bca', 'cab', 'cba'])
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/reverse-linked-list.py b/users/wpcarro/scratch/data_structures_and_algorithms/reverse-linked-list.py
new file mode 100644
index 0000000000..b7396b20ce
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/reverse-linked-list.py
@@ -0,0 +1,79 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# reverse :: List(a) -> List(a)
+def reverse(node):
+    curr = node
+    prev = None
+    while curr:
+        nxt = curr.next
+        curr.next = prev
+        prev = curr
+        curr = nxt
+    # Make sure to understand the spec! Debugging takes time. Rewriting takes
+    # time.
+    return prev
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+        def get_values(self):
+            node = self
+            values = []
+            while node is not None:
+                values.append(node.value)
+                node = node.next
+            return values
+
+    def test_short_linked_list(self):
+        second = Test.LinkedListNode(2)
+        first = Test.LinkedListNode(1, second)
+
+        result = reverse(first)
+        self.assertIsNotNone(result)
+
+        actual = result.get_values()
+        expected = [2, 1]
+        self.assertEqual(actual, expected)
+
+    def test_long_linked_list(self):
+        sixth = Test.LinkedListNode(6)
+        fifth = Test.LinkedListNode(5, sixth)
+        fourth = Test.LinkedListNode(4, fifth)
+        third = Test.LinkedListNode(3, fourth)
+        second = Test.LinkedListNode(2, third)
+        first = Test.LinkedListNode(1, second)
+
+        result = reverse(first)
+        self.assertIsNotNone(result)
+
+        actual = result.get_values()
+        expected = [6, 5, 4, 3, 2, 1]
+        self.assertEqual(actual, expected)
+
+    def test_one_element_linked_list(self):
+        first = Test.LinkedListNode(1)
+
+        result = reverse(first)
+        self.assertIsNotNone(result)
+
+        actual = result.get_values()
+        expected = [1]
+        self.assertEqual(actual, expected)
+
+    def test_empty_linked_list(self):
+        result = reverse(None)
+        self.assertIsNone(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/reverse-words.py b/users/wpcarro/scratch/data_structures_and_algorithms/reverse-words.py
new file mode 100644
index 0000000000..5df12ebabd
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/reverse-words.py
@@ -0,0 +1,181 @@
+from collections import deque
+import unittest
+
+################################################################################
+# Solution
+################################################################################
+
+
+def rev(xs, i, j):
+    """Reverse xs in place from [i, j]"""
+    while i < j:
+        xs[i], xs[j] = xs[j], xs[i]
+        i += 1
+        j -= 1
+
+
+def rotate(xs, n, i=None, j=None):
+    """Mutably rotates list, xs, n times. Positive n values rotate right while
+    negative n values rotate left. Rotate within window [i, j]."""
+    i = i or 0
+    j = j or len(xs) - 1
+    ct = j - i
+
+    if n < 0:
+        n = abs(n)
+        p = i + n - 1
+        rev(xs, i, p)
+        rev(xs, p + 1, j)
+        rev(xs, i, j)
+    else:
+        p = j - (n - 1)
+        rev(xs, p, j)
+        rev(xs, i, p - 1)
+        rev(xs, i, j)
+    return xs
+
+
+def rev_words(xs, i, j):
+    if j + 1 == len(xs):
+        return 0
+
+    while j + 1 < len(xs):
+        while j + 1 < len(xs) and xs[j + 1] != ' ':
+            j += 1
+
+        rev(xs, i, j)
+        j += 2
+        i = j
+
+    return 0
+
+
+def reverse_words(xs):
+    # first reverse everything
+    rev(xs, 0, len(xs) - 1)
+    return rev_words(xs, 0, 0)
+
+
+def reverse_words_bak(xs, i=None, j=None):
+    i = i or 0
+    j = j or len(xs) - 1
+    w0, w1 = [], []
+
+    if i >= j:
+        return 0
+
+    pi = i
+    while pi < len(xs) and xs[pi] != ' ':
+        w0.append(xs[pi])
+        pi += 1
+
+    if pi == len(xs):
+        return 0
+
+    pj = j
+    while xs[pj] != ' ':
+        w1.append(xs[pj])
+        pj -= 1
+
+    d = len(w0) - len(w1)
+
+    rotate(xs, -1 * d, i, j)
+
+    for k in range(len(w1)):
+        xs[i + k] = w1[len(w1) - 1 - k]
+
+    for k in range(len(w0)):
+        xs[j - k] = w0[len(w0) - 1 - k]
+
+    while i != j and xs[i] != ' ' and xs[j] != ' ':
+        i += 1
+        j -= 1
+
+    if i == j:
+        return 0
+
+    elif xs[i] == ' ':
+        while j > 0 and xs[j] != ' ':
+            j -= 1
+        if j == 0:
+            return 0
+    elif xs[j] == ' ':
+        while i < len(xs) and xs[i] != ' ':
+            i += 1
+        if i == len(xs):
+            return 0
+    return reverse_words(xs, i + 1, j - 1)
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_rev(self):
+        xs = [1, 2, 3, 4, 5]
+        rev(xs, 0, len(xs) - 1)
+        self.assertEqual(xs, [5, 4, 3, 2, 1])
+
+    def test_rotate(self):
+        ys = [1, 2, 3, 4, 5]
+        xs = ys[:]
+        self.assertEqual(rotate(xs, 1, 1, 3), [1, 4, 2, 3, 5])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, -1, 1, 3), [1, 3, 4, 2, 5])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, 1), [5, 1, 2, 3, 4])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, -1), [2, 3, 4, 5, 1])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, -2), [3, 4, 5, 1, 2])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, -5), [1, 2, 3, 4, 5])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, 5), [1, 2, 3, 4, 5])
+        xs = ys[:]
+        self.assertEqual(rotate(xs, 3), [3, 4, 5, 1, 2])
+
+    def test_one_word(self):
+        message = list('vault')
+        reverse_words(message)
+        expected = list('vault')
+        self.assertEqual(message, expected)
+
+    def test_two_words(self):
+        message = list('thief cake')
+        reverse_words(message)
+        expected = list('cake thief')
+        self.assertEqual(message, expected)
+
+    def test_three_words(self):
+        message = list('one another get')
+        reverse_words(message)
+        expected = list('get another one')
+        self.assertEqual(message, expected)
+
+    def test_multiple_words_same_length(self):
+        message = list('rat the ate cat the')
+        reverse_words(message)
+        expected = list('the cat ate the rat')
+        self.assertEqual(message, expected)
+
+    def test_multiple_words_different_lengths(self):
+        message = list('at rat house')
+        reverse_words(message)
+        expected = list('house rat at')
+        self.assertEqual(message, expected)
+
+    def test_multiple_words_different_lengths(self):
+        message = list('yummy is cake bundt chocolate')
+        reverse_words(message)
+        expected = list('chocolate bundt cake is yummy')
+        self.assertEqual(message, expected)
+
+    def test_empty_string(self):
+        message = list('')
+        reverse_words(message)
+        expected = list('')
+        self.assertEqual(message, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/second-largest-item-bst.py b/users/wpcarro/scratch/data_structures_and_algorithms/second-largest-item-bst.py
new file mode 100644
index 0000000000..bc167d975a
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/second-largest-item-bst.py
@@ -0,0 +1,179 @@
+import unittest
+from collections import deque
+
+
+################################################################################
+# Implementation
+################################################################################
+def is_leaf(node):
+    return node.left is None and node.right is None
+
+
+def find_largest(node):
+    current = node
+    while current.right is not None:
+        current = current.right
+    return current.value
+
+
+def find_second_largest(node):
+    history = deque()
+    current = node
+
+    while current.right:
+        history.append(current)
+        current = current.right
+
+    if current.left:
+        return find_largest(current.left)
+    elif history:
+        return history.pop().value
+    else:
+        raise TypeError
+
+
+def find_second_largest_backup(node):
+    history = deque()
+    current = node
+
+    # traverse -> largest
+    while current.right:
+        history.append(current)
+        current = current.right
+
+    if current.left:
+        return find_largest(current.left)
+    elif history:
+        return history.pop().value
+    else:
+        raise ArgumentError
+
+
+# Write a iterative version to avoid consuming memory with the call stack.
+# Commenting out the recursive code for now.
+def find_second_largest_backup(node):
+    if node.left is None and node.right is None:
+        raise ArgumentError
+
+    elif node.right is None and is_leaf(node.left):
+        return node.left.value
+
+    # recursion
+    # elif node.right is None:
+    #     return find_largest(node.left)
+
+    # iterative version
+    elif node.right is None:
+        current = node.left
+        while current.right is not None:
+            current = current.right
+        return current.value
+
+    # recursion
+    # TODO: Remove recursion from here.
+    elif not is_leaf(node.right):
+        return find_second_largest(node.right)
+
+    # could do an else here, but let's be more assertive.
+    elif is_leaf(node.right):
+        return node.value
+
+    else:
+        raise ArgumentError
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    class BinaryTreeNode(object):
+        def __init__(self, value):
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def insert_left(self, value):
+            self.left = Test.BinaryTreeNode(value)
+            return self.left
+
+        def insert_right(self, value):
+            self.right = Test.BinaryTreeNode(value)
+            return self.right
+
+    def test_full_tree(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(70)
+        left.insert_left(10)
+        left.insert_right(40)
+        right.insert_left(60)
+        right.insert_right(80)
+        actual = find_second_largest(tree)
+        expected = 70
+        self.assertEqual(actual, expected)
+
+    def test_largest_has_a_left_child(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(70)
+        left.insert_left(10)
+        left.insert_right(40)
+        right.insert_left(60)
+        actual = find_second_largest(tree)
+        expected = 60
+        self.assertEqual(actual, expected)
+
+    def test_largest_has_a_left_subtree(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(70)
+        left.insert_left(10)
+        left.insert_right(40)
+        right_left = right.insert_left(60)
+        right_left_left = right_left.insert_left(55)
+        right_left.insert_right(65)
+        right_left_left.insert_right(58)
+        actual = find_second_largest(tree)
+        expected = 65
+        self.assertEqual(actual, expected)
+
+    def test_second_largest_is_root_node(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        tree.insert_right(70)
+        left.insert_left(10)
+        left.insert_right(40)
+        actual = find_second_largest(tree)
+        expected = 50
+        self.assertEqual(actual, expected)
+
+    def test_descending_linked_list(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(40)
+        left_left = left.insert_left(30)
+        left_left_left = left_left.insert_left(20)
+        left_left_left.insert_left(10)
+        actual = find_second_largest(tree)
+        expected = 40
+        self.assertEqual(actual, expected)
+
+    def test_ascending_linked_list(self):
+        tree = Test.BinaryTreeNode(50)
+        right = tree.insert_right(60)
+        right_right = right.insert_right(70)
+        right_right.insert_right(80)
+        actual = find_second_largest(tree)
+        expected = 70
+        self.assertEqual(actual, expected)
+
+    def test_error_when_tree_has_one_node(self):
+        tree = Test.BinaryTreeNode(50)
+        with self.assertRaises(Exception):
+            find_second_largest(tree)
+
+    def test_error_when_tree_is_empty(self):
+        with self.assertRaises(Exception):
+            find_second_largest(None)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/shortest-path-inject-vertices.py b/users/wpcarro/scratch/data_structures_and_algorithms/shortest-path-inject-vertices.py
new file mode 100644
index 0000000000..e08ea66b8f
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/shortest-path-inject-vertices.py
@@ -0,0 +1,94 @@
+from heapq import heappush, heappop
+from collections import deque
+from fixtures import weighted_graph, expanded_weights_graph
+
+# UnweightedGraph(a) :: Map(a, Set(a))
+# WeightedGraph(a) :: Map(a, Set(a))
+
+
+# shortest_path_dijkstra :: Vertex -> Vertex -> WeightedGraph(Vertex)
+def shortest_path_dijkstra(a, b, g):
+    q = []
+    seen = set()
+
+    heappush(q, (0, a, [a]))
+
+    while q:
+        w0, v0, path = heappop(q)
+        if v0 in seen:
+            continue
+        elif v0 == b:
+            return w0, path
+        for w1, v1 in g.get(v0):
+            heappush(q, (w0 + w1, v1, path + [v1]))
+        seen.add(v0)
+    return 'weighted', 'pizza'
+
+
+# expand_edge :: Vertex -> (Weight, Vertex) -> Map(Vertex, [Vertex])
+def expand_edge(v0, wv):
+    w, v1 = wv
+    assert w > 1
+
+    result = {v0: ['{}-{}'.format(v1, 1)]}
+    for x in range(w - 2):
+        result['{}-{}'.format(v1, x + 1)] = ['{}-{}'.format(v1, x + 2)]
+    result['{}-{}'.format(v1, w - 1)] = [v1]
+
+    return result
+
+
+# expand_weights :: Vertex -> WeightedGraph(Vertex) -> UnweightedGraph(Vertex)
+def expand_weights(v, g):
+    result = {}
+    q = deque()
+    seen = set()
+
+    q.append(v)
+    while q:
+        v = d.popleft()
+        if v in seen:
+            continue
+        x = expand_edge(v, g.get)
+        for w, v1 in g.get(v):
+            if w > 1:
+                ws = expand_edge(v, (w, v1))
+                result = {**result, **ws}
+            q.append(v)
+        pass
+
+
+# shortest_path_inject :: Vertex -> Vertex -> WeightedGraph(Vertex)
+def shortest_path_inject(a, b, g):
+    q = deque()
+    seen = set()
+
+    q.append((a, [a]))
+
+    while q:
+        v0, path = q.popleft()
+        if v0 == 'dummy':
+            continue
+        elif v0 in seen:
+            continue
+        elif v0 == b:
+            return len(path), path
+        for _, v1 in g.get(v0):
+            q.append((v1, path + [v1]))
+        seen.add(v0)
+        continue
+
+    return None, None
+
+
+print(expand_edge('a', (4, 'b')))
+print(expand_edge('a', (5, 'e')))
+assert expand_weights('a', weighted_graph) == expanded_weights_graph
+# a = 'a'
+# b = 'd'
+# w, x = shortest_path_dijkstra(a, b, weighted_graph)
+# w1, x1 = shortest_path_inject(a, b, weighted_graph)
+# print("[dijkstra]  Shortest path from {} to {} is {} with weight {}".format(
+#     a, b, x, w))
+# print("[injection] Shortest path from {} to {} is {} with weight {}".format(
+#     a, b, x1, w1))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/shuffle.py b/users/wpcarro/scratch/data_structures_and_algorithms/shuffle.py
new file mode 100644
index 0000000000..bdfbad2426
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/shuffle.py
@@ -0,0 +1,34 @@
+import random
+
+
+def get_random(floor, ceiling):
+    return random.randrange(floor, ceiling + 1)
+
+
+# shuffle_in_place :: [a] -> IO ()
+def shuffle_in_place(xs):
+    """Fisher-Yates algorithm. Notice that shuffling here is the same as
+    selecting a random permutation of the input set, `xs`."""
+    n = len(xs) - 1
+    for i in range(len(xs)):
+        r = get_random(i, n)
+        xs[i], xs[r] = xs[r], xs[i]
+    return xs
+
+
+# shuffle :: [a] -> [a]
+def shuffle_not_in_place(xs):
+    result = []
+
+    while xs:
+        i = get_random(0, len(xs) - 1)
+        x = xs.pop(i)
+        result.append(x)
+
+    return result
+
+
+xs = [x for x in range(9)]
+print(xs)
+# print(shuffle_not_in_place(xs))
+print(shuffle_in_place(xs[:]))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/string-reverse.py b/users/wpcarro/scratch/data_structures_and_algorithms/string-reverse.py
new file mode 100644
index 0000000000..8b4cdac1c2
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/string-reverse.py
@@ -0,0 +1,22 @@
+
+# swap :: Int -> Int -> [Char] -> IO ()
+def swap(ia, iz, xs):
+    # handle swap when ia == iz
+    assert ia <= iz
+    xs[ia], xs[iz] = xs[iz], xs[ia]
+    
+
+# reverse :: [Char] -> IO ()
+def reverse(xs):
+    ia = 0
+    iz = len(xs) - 1
+
+    while ia <= iz:
+        swap(ia, iz, xs)
+        ia += 1
+        iz -= 1
+
+x = list("superduperpooper")
+reverse(x)
+print(x)
+print("Tests pass")
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/temperature-tracker.py b/users/wpcarro/scratch/data_structures_and_algorithms/temperature-tracker.py
new file mode 100644
index 0000000000..6b042182f0
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/temperature-tracker.py
@@ -0,0 +1,84 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+class TempTracker(object):
+    def __init__(self):
+        # min / max
+        self.min = None
+        self.max = None
+        # mean
+        self.sum = 0
+        self.num = 0
+        # mode
+        self.nums = [0] * 111
+        self.mode_num = 0
+        self.mode = None
+
+    def insert(self, x):
+        # min / max
+        if not self.min or x < self.min:
+            self.min = x
+        if not self.max or x > self.max:
+            self.max = x
+        # mean
+        self.sum += x
+        self.num += 1
+        # mode
+        self.nums[x] += 1
+        if self.nums[x] >= self.mode_num:
+            self.mode_num = self.nums[x]
+            self.mode = x
+
+    def get_max(self):
+        return self.max
+
+    def get_min(self):
+        return self.min
+
+    def get_mean(self):
+        return self.sum / self.num
+
+    def get_mode(self):
+        return self.mode
+
+
+# Tests
+
+
+class Test(unittest.TestCase):
+    def test_tracker_usage(self):
+        tracker = TempTracker()
+
+        tracker.insert(50)
+        msg = 'failed on first temp recorded'
+        self.assertEqual(tracker.get_max(), 50, msg='max ' + msg)
+        self.assertEqual(tracker.get_min(), 50, msg='min ' + msg)
+        self.assertEqual(tracker.get_mean(), 50.0, msg='mean ' + msg)
+        self.assertEqual(tracker.get_mode(), 50, msg='mode ' + msg)
+
+        tracker.insert(80)
+        msg = 'failed on higher temp recorded'
+        self.assertEqual(tracker.get_max(), 80, msg='max ' + msg)
+        self.assertEqual(tracker.get_min(), 50, msg='min ' + msg)
+        self.assertEqual(tracker.get_mean(), 65.0, msg='mean ' + msg)
+        self.assertIn(tracker.get_mode(), [50, 80], msg='mode ' + msg)
+
+        tracker.insert(80)
+        msg = 'failed on third temp recorded'
+        self.assertEqual(tracker.get_max(), 80, msg='max ' + msg)
+        self.assertEqual(tracker.get_min(), 50, msg='min ' + msg)
+        self.assertEqual(tracker.get_mean(), 70.0, msg='mean ' + msg)
+        self.assertEqual(tracker.get_mode(), 80, msg='mode ' + msg)
+
+        tracker.insert(30)
+        msg = 'failed on lower temp recorded'
+        self.assertEqual(tracker.get_max(), 80, msg='max ' + msg)
+        self.assertEqual(tracker.get_min(), 30, msg='min ' + msg)
+        self.assertEqual(tracker.get_mean(), 60.0, msg='mean ' + msg)
+        self.assertEqual(tracker.get_mode(), 80, msg='mode ' + msg)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/test.txt b/users/wpcarro/scratch/data_structures_and_algorithms/test.txt
new file mode 100644
index 0000000000..ce01362503
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/test.txt
@@ -0,0 +1 @@
+hello
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/top-scores.py b/users/wpcarro/scratch/data_structures_and_algorithms/top-scores.py
new file mode 100644
index 0000000000..8e7b073dd8
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/top-scores.py
@@ -0,0 +1,25 @@
+from collections import deque
+
+# list:
+# array:
+# vector:
+# bit-{array,vector}:
+
+
+def sort(xs, highest):
+    v = [0] * (highest + 1)
+    result = deque()
+
+    for x in xs:
+        v[x] += 1
+
+    for i, x in enumerate(v):
+        if x > 0:
+            result.appendleft(i)
+
+    return list(result)
+
+
+assert sort([37, 89, 41, 100, 65, 91, 53],
+            100) == [100, 91, 89, 65, 53, 41, 37]
+print("Tests pass!")
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/topo-sort.py b/users/wpcarro/scratch/data_structures_and_algorithms/topo-sort.py
new file mode 100644
index 0000000000..fe295b0279
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/topo-sort.py
@@ -0,0 +1,31 @@
+from fixtures import unweighted_digraph
+from collections import deque
+
+# vertices_no_in_edges :: UnweightedDigraph -> Set(Vertex)
+def vertices_no_in_edges(g):
+    """Return the vertices in graph `g` with no in-edges."""
+    result = set()
+    vertices = set(g.keys())
+    for neighbors in g.values():
+        result = result.union(neighbors)
+    return vertices ^ result
+
+# topo_sort :: UnweightedDigraph -> List(Vertex)
+def topo_sort(g):
+    q = deque()
+    seen = set()
+    result = []
+    for x in vertices_no_in_edges(g):
+        q.append(x)
+    while q:
+        vertex = q.popleft()
+        if vertex in seen:
+            continue
+        result.append(vertex)
+        neighbors = g.get(vertex)
+        for x in g.get(vertex):
+            q.append(x)
+        seen.add(vertex)
+    return result
+
+print(topo_sort(unweighted_digraph))
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/trickling-water.py b/users/wpcarro/scratch/data_structures_and_algorithms/trickling-water.py
new file mode 100644
index 0000000000..45621990ec
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/trickling-water.py
@@ -0,0 +1,38 @@
+class Node(object):
+    def __init__(self, value, children=[]):
+        self.value = value
+        self.children = children
+
+
+################################################################################
+# Solution
+################################################################################
+def trip_time(node):
+    s = []
+    result = 0
+    s.append((node.value, node))
+    while s:
+        p, node = s.pop()
+        if not node.children:
+            result = max(result, p)
+        for x in node.children:
+            s.append((p + x.value, x))
+    return result
+
+
+################################################################################
+# Tests
+################################################################################
+tree = Node(
+    0,
+    children=[
+        Node(5, children=[Node(6)]),
+        Node(2, children=[
+            Node(6),
+            Node(10),
+        ]),
+        Node(3, children=[Node(2, children=[Node(11)])]),
+    ])
+
+assert trip_time(tree) == 16
+print("Tests pass!")
diff --git a/users/wpcarro/scratch/data_structures_and_algorithms/which-appears-twice.py b/users/wpcarro/scratch/data_structures_and_algorithms/which-appears-twice.py
new file mode 100644
index 0000000000..e9a4f0eb24
--- /dev/null
+++ b/users/wpcarro/scratch/data_structures_and_algorithms/which-appears-twice.py
@@ -0,0 +1,33 @@
+import unittest
+
+
+################################################################################
+# Solution
+################################################################################
+# find_repeat :: [Int] -> Int
+def find_repeat(xs):
+    n = len(xs) - 1
+    return sum(xs) - ((n**2 + n) / 2)
+
+
+################################################################################
+# Tests
+################################################################################
+class Test(unittest.TestCase):
+    def test_short_list(self):
+        actual = find_repeat([1, 2, 1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        actual = find_repeat([4, 1, 3, 4, 2])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_long_list(self):
+        actual = find_repeat([1, 5, 9, 7, 2, 6, 3, 8, 2, 4])
+        expected = 2
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/balanced-binary-tree.py b/users/wpcarro/scratch/deepmind/part_one/balanced-binary-tree.py
new file mode 100644
index 0000000000..7fc174a2a9
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/balanced-binary-tree.py
@@ -0,0 +1,123 @@
+import unittest
+from collections import deque
+
+
+def is_balanced(node):
+    q, seen, ds = deque(), set(), set()
+    q.append((0, node))
+    while q:
+        d, node = q.popleft()
+        l, r = node.left, node.right
+        seen.add(node)
+        if not l and not r:
+            if d not in ds and len(ds) == 2:
+                return False
+            else:
+                ds.add(d)
+        if l and l not in seen:
+            q.append((d + 1, l))
+        if r and r not in seen:
+            q.append((d + 1, r))
+    return max(ds) - min(ds) <= 1
+
+
+# Tests
+class Test(unittest.TestCase):
+    class BinaryTreeNode(object):
+        def __init__(self, value):
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def insert_left(self, value):
+            self.left = Test.BinaryTreeNode(value)
+            return self.left
+
+        def insert_right(self, value):
+            self.right = Test.BinaryTreeNode(value)
+            return self.right
+
+    def test_full_tree(self):
+        tree = Test.BinaryTreeNode(5)
+        left = tree.insert_left(8)
+        right = tree.insert_right(6)
+        left.insert_left(1)
+        left.insert_right(2)
+        right.insert_left(3)
+        right.insert_right(4)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_both_leaves_at_the_same_depth(self):
+        tree = Test.BinaryTreeNode(3)
+        left = tree.insert_left(4)
+        right = tree.insert_right(2)
+        left.insert_left(1)
+        right.insert_right(9)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_leaf_heights_differ_by_one(self):
+        tree = Test.BinaryTreeNode(6)
+        left = tree.insert_left(1)
+        right = tree.insert_right(0)
+        right.insert_right(7)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_leaf_heights_differ_by_two(self):
+        tree = Test.BinaryTreeNode(6)
+        left = tree.insert_left(1)
+        right = tree.insert_right(0)
+        right_right = right.insert_right(7)
+        right_right.insert_right(8)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_three_leaves_total(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(5)
+        right = tree.insert_right(9)
+        right.insert_left(8)
+        right.insert_right(5)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_both_subtrees_superbalanced(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(5)
+        right = tree.insert_right(9)
+        right_left = right.insert_left(8)
+        right.insert_right(5)
+        right_left.insert_left(7)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_both_subtrees_superbalanced_two(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(2)
+        right = tree.insert_right(4)
+        left.insert_left(3)
+        left_right = left.insert_right(7)
+        left_right.insert_right(8)
+        right_right = right.insert_right(5)
+        right_right_right = right_right.insert_right(6)
+        right_right_right.insert_right(9)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_only_one_node(self):
+        tree = Test.BinaryTreeNode(1)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_linked_list_tree(self):
+        tree = Test.BinaryTreeNode(1)
+        right = tree.insert_right(2)
+        right_right = right.insert_right(3)
+        right_right.insert_right(4)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/dijkstra.py b/users/wpcarro/scratch/deepmind/part_one/dijkstra.py
new file mode 100644
index 0000000000..6975dbe4d1
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/dijkstra.py
@@ -0,0 +1,26 @@
+# Doing a practice implementation of Dijkstra's algorithm: a priority-first
+# search.
+from heapq import heappush, heappop
+
+
+class Node(object):
+    def __init__(self, value, children):
+        self.value = value
+        self.children = children
+
+
+def shortest_path(a, b):
+    """Return the shortest path from `a` to `b`."""
+    q = []
+    seen = set()
+    heappush((a.value, a, [a]), q)
+
+    while q:
+        d, node, path = heappop(q)
+        if node == b:
+            return path
+        seen.add(node)
+        for child in node.children:
+            if child not in seen:
+                heappush((d + child.value, child, path + [child]), q)
+    raise Exception("Path between nodes A and B does not exist.")
diff --git a/users/wpcarro/scratch/deepmind/part_one/efficiency.org b/users/wpcarro/scratch/deepmind/part_one/efficiency.org
new file mode 100644
index 0000000000..89a45c52ad
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/efficiency.org
@@ -0,0 +1,6 @@
+* Sorting
+** Merge:	O(n*log(n))
+** Heap:	O(n*log(n))
+** Insertion:	O(n^2)
+** Quick:	O(n^2)
+** Bubble:	O(n^2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/find-rotation-point.py b/users/wpcarro/scratch/deepmind/part_one/find-rotation-point.py
new file mode 100644
index 0000000000..5c21d5167c
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/find-rotation-point.py
@@ -0,0 +1,55 @@
+import unittest
+from math import floor
+
+
+def midpoint(a, b):
+    return a + floor((b - a) / 2)
+
+
+def do_find_rotation_point(a, b, xs):
+    i = midpoint(a, b)
+    count = b - a + 1
+
+    if count == 2:
+        if xs[a] > xs[b]:
+            return b
+        else:
+            return -1
+
+    if i in {a, b}:
+        return i
+
+    if xs[a] < xs[i]:
+        return do_find_rotation_point(i, b, xs)
+    else:
+        return do_find_rotation_point(a, i, xs)
+
+
+def find_rotation_point(xs):
+    return do_find_rotation_point(0, len(xs) - 1, xs)
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_small_list(self):
+        actual = find_rotation_point(['cape', 'cake'])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        actual = find_rotation_point(
+            ['grape', 'orange', 'plum', 'radish', 'apple'])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_large_list(self):
+        actual = find_rotation_point([
+            'ptolemaic', 'retrograde', 'supplant', 'undulate', 'xenoepist',
+            'asymptote', 'babka', 'banoffee', 'engender', 'karpatka',
+            'othellolagkage'
+        ])
+        expected = 5
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/inflight-entertainment.py b/users/wpcarro/scratch/deepmind/part_one/inflight-entertainment.py
new file mode 100644
index 0000000000..2116b27b0b
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/inflight-entertainment.py
@@ -0,0 +1,51 @@
+import unittest
+
+
+def can_two_movies_fill_flight(xs, t):
+    seeking = set()
+    for x in xs:
+        if x in seeking:
+            return True
+        else:
+            seeking.add(t - x)
+    return False
+
+
+# Tests
+
+
+class Test(unittest.TestCase):
+    def test_short_flight(self):
+        result = can_two_movies_fill_flight([2, 4], 1)
+        self.assertFalse(result)
+
+    def test_long_flight(self):
+        result = can_two_movies_fill_flight([2, 4], 6)
+        self.assertTrue(result)
+
+    def test_one_movie_half_flight_length(self):
+        result = can_two_movies_fill_flight([3, 8], 6)
+        self.assertFalse(result)
+
+    def test_two_movies_half_flight_length(self):
+        result = can_two_movies_fill_flight([3, 8, 3], 6)
+        self.assertTrue(result)
+
+    def test_lots_of_possible_pairs(self):
+        result = can_two_movies_fill_flight([1, 2, 3, 4, 5, 6], 7)
+        self.assertTrue(result)
+
+    def test_not_using_first_movie(self):
+        result = can_two_movies_fill_flight([4, 3, 2], 5)
+        self.assertTrue(result)
+
+    def test_only_one_movie(self):
+        result = can_two_movies_fill_flight([6], 6)
+        self.assertFalse(result)
+
+    def test_no_movies(self):
+        result = can_two_movies_fill_flight([], 2)
+        self.assertFalse(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/kth-to-last.py b/users/wpcarro/scratch/deepmind/part_one/kth-to-last.py
new file mode 100644
index 0000000000..5335e419f7
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/kth-to-last.py
@@ -0,0 +1,64 @@
+import unittest
+
+
+def kth_to_last_node(k, x):
+    a, b = x, x
+
+    if k == 0:
+        raise Exception('Value of 0 for k is not supported')
+
+    for _ in range(k - 1):
+        if not a.next:
+            raise Exception('Value of {} for k is too large'.format(k))
+        a = a.next
+
+    while a.next:
+        a, b = a.next, b.next
+    return b
+
+
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+        def get_values(self):
+            node = self
+            values = []
+            while node is not None:
+                values.append(node.value)
+                node = node.next
+            return values
+
+    def setUp(self):
+        self.fourth = Test.LinkedListNode(4)
+        self.third = Test.LinkedListNode(3, self.fourth)
+        self.second = Test.LinkedListNode(2, self.third)
+        self.first = Test.LinkedListNode(1, self.second)
+
+    def test_first_to_last_node(self):
+        actual = kth_to_last_node(1, self.first)
+        expected = self.fourth
+        self.assertEqual(actual, expected)
+
+    def test_second_to_last_node(self):
+        actual = kth_to_last_node(2, self.first)
+        expected = self.third
+        self.assertEqual(actual, expected)
+
+    def test_first_node(self):
+        actual = kth_to_last_node(4, self.first)
+        expected = self.first
+        self.assertEqual(actual, expected)
+
+    def test_k_greater_than_linked_list_length(self):
+        with self.assertRaises(Exception):
+            kth_to_last_node(5, self.first)
+
+    def test_k_is_zero(self):
+        with self.assertRaises(Exception):
+            kth_to_last_node(0, self.first)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/merging-ranges.py b/users/wpcarro/scratch/deepmind/part_one/merging-ranges.py
new file mode 100644
index 0000000000..23b40793b8
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/merging-ranges.py
@@ -0,0 +1,59 @@
+import unittest
+
+
+def merge_ranges(xs):
+    xs.sort()
+    result = [xs[0]]
+    for curr in xs[1:]:
+        a, z = result[-1]
+        if z >= curr[0]:
+            result[-1] = (a, max(z, curr[1]))
+        else:
+            result.append(curr)
+    return result
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_meetings_overlap(self):
+        actual = merge_ranges([(1, 3), (2, 4)])
+        expected = [(1, 4)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_touch(self):
+        actual = merge_ranges([(5, 6), (6, 8)])
+        expected = [(5, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meeting_contains_other_meeting(self):
+        actual = merge_ranges([(1, 8), (2, 5)])
+        expected = [(1, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_stay_separate(self):
+        actual = merge_ranges([(1, 3), (4, 8)])
+        expected = [(1, 3), (4, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_multiple_merged_meetings(self):
+        actual = merge_ranges([(1, 4), (2, 5), (5, 8)])
+        expected = [(1, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_not_sorted(self):
+        actual = merge_ranges([(5, 8), (1, 4), (6, 8)])
+        expected = [(1, 4), (5, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_one_long_meeting_contains_smaller_meetings(self):
+        actual = merge_ranges([(1, 10), (2, 5), (6, 8), (9, 10), (10, 12)])
+        expected = [(1, 12)]
+        self.assertEqual(actual, expected)
+
+    def test_sample_input(self):
+        actual = merge_ranges([(0, 1), (3, 5), (4, 8), (10, 12), (9, 10)])
+        expected = [(0, 1), (3, 8), (9, 12)]
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/recursive-string-permutations.py b/users/wpcarro/scratch/deepmind/part_one/recursive-string-permutations.py
new file mode 100644
index 0000000000..f50db28387
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/recursive-string-permutations.py
@@ -0,0 +1,56 @@
+import unittest
+from itertools import permutations
+
+
+class Node(object):
+    def __init__(self, x):
+        self.value = x
+        self.children = []
+
+
+def make_tree(c, xs):
+    root = Node(c)
+    for x in xs:
+        root.children.append(make_tree(x, xs - {x}))
+    return root
+
+
+def get_permutations(xs):
+    xs = set(xs)
+    root = make_tree("", xs)
+    q, perms = [], set()
+    q.append(("", root))
+    while q:
+        c, node = q.pop()
+        if not node.children:
+            perms.add(c)
+        else:
+            for child in node.children:
+                q.append((c + child.value, child))
+    return perms
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_empty_string(self):
+        actual = get_permutations('')
+        expected = set([''])
+        self.assertEqual(actual, expected)
+
+    def test_one_character_string(self):
+        actual = get_permutations('a')
+        expected = set(['a'])
+        self.assertEqual(actual, expected)
+
+    def test_two_character_string(self):
+        actual = get_permutations('ab')
+        expected = set(['ab', 'ba'])
+        self.assertEqual(actual, expected)
+
+    def test_three_character_string(self):
+        actual = get_permutations('abc')
+        expected = set(['abc', 'acb', 'bac', 'bca', 'cab', 'cba'])
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/reverse-linked-list.py b/users/wpcarro/scratch/deepmind/part_one/reverse-linked-list.py
new file mode 100644
index 0000000000..82fac171d5
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/reverse-linked-list.py
@@ -0,0 +1,74 @@
+import unittest
+
+
+def reverse(node):
+    prev = None
+    next = None
+    curr = node
+
+    while curr:
+        next = curr.next
+        curr.next = prev
+        prev = curr
+        curr = next
+
+    return prev
+
+
+# Tests
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+        def get_values(self):
+            node = self
+            values = []
+            while node is not None:
+                values.append(node.value)
+                node = node.next
+            return values
+
+    def test_short_linked_list(self):
+        second = Test.LinkedListNode(2)
+        first = Test.LinkedListNode(1, second)
+
+        result = reverse(first)
+        self.assertIsNotNone(result)
+
+        actual = result.get_values()
+        expected = [2, 1]
+        self.assertEqual(actual, expected)
+
+    def test_long_linked_list(self):
+        sixth = Test.LinkedListNode(6)
+        fifth = Test.LinkedListNode(5, sixth)
+        fourth = Test.LinkedListNode(4, fifth)
+        third = Test.LinkedListNode(3, fourth)
+        second = Test.LinkedListNode(2, third)
+        first = Test.LinkedListNode(1, second)
+
+        result = reverse(first)
+        self.assertIsNotNone(result)
+
+        actual = result.get_values()
+        expected = [6, 5, 4, 3, 2, 1]
+        self.assertEqual(actual, expected)
+
+    def test_one_element_linked_list(self):
+        first = Test.LinkedListNode(1)
+
+        result = reverse(first)
+        self.assertIsNotNone(result)
+
+        actual = result.get_values()
+        expected = [1]
+        self.assertEqual(actual, expected)
+
+    def test_empty_linked_list(self):
+        result = reverse(None)
+        self.assertIsNone(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/stock-price.py b/users/wpcarro/scratch/deepmind/part_one/stock-price.py
new file mode 100644
index 0000000000..7055b66af1
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/stock-price.py
@@ -0,0 +1,51 @@
+def get_max_profit(xs):
+    best_profit = xs[1] - xs[0]
+    lowest_buy = xs[0]
+
+    for x in xs[1:]:
+        best_profit = max(best_profit, x - lowest_buy)
+        lowest_buy = min(lowest_buy, x)
+    return best_profit
+
+
+# Tests
+
+import unittest
+
+
+class Test(unittest.TestCase):
+    def test_price_goes_up_then_down(self):
+        actual = get_max_profit([1, 5, 3, 2])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_price_goes_down_then_up(self):
+        actual = get_max_profit([7, 2, 8, 9])
+        expected = 7
+        self.assertEqual(actual, expected)
+
+    def test_price_goes_up_all_day(self):
+        actual = get_max_profit([1, 6, 7, 9])
+        expected = 8
+        self.assertEqual(actual, expected)
+
+    def test_price_goes_down_all_day(self):
+        actual = get_max_profit([9, 7, 4, 1])
+        expected = -2
+        self.assertEqual(actual, expected)
+
+    def test_price_stays_the_same_all_day(self):
+        actual = get_max_profit([1, 1, 1, 1])
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_error_with_empty_prices(self):
+        with self.assertRaises(Exception):
+            get_max_profit([])
+
+    def test_error_with_one_price(self):
+        with self.assertRaises(Exception):
+            get_max_profit([1])
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_one/which-appears-twice.py b/users/wpcarro/scratch/deepmind/part_one/which-appears-twice.py
new file mode 100644
index 0000000000..c01379295d
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_one/which-appears-twice.py
@@ -0,0 +1,29 @@
+import unittest
+
+
+def find_repeat(xs):
+    n = max(xs)
+    expected_sum = (n + 1) * n / 2
+    actual_sum = sum(xs)
+    return actual_sum - expected_sum
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_short_list(self):
+        actual = find_repeat([1, 2, 1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        actual = find_repeat([4, 1, 3, 4, 2])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_long_list(self):
+        actual = find_repeat([1, 5, 9, 7, 2, 6, 3, 8, 2, 4])
+        expected = 2
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/.envrc b/users/wpcarro/scratch/deepmind/part_two/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/scratch/deepmind/part_two/balanced-binary-tree.py b/users/wpcarro/scratch/deepmind/part_two/balanced-binary-tree.py
new file mode 100644
index 0000000000..03de0350d8
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/balanced-binary-tree.py
@@ -0,0 +1,126 @@
+import unittest
+from collections import deque
+
+
+# is_balanced :: Node(a) -> Bool
+def is_balanced(node):
+    q = deque()
+    q.append((0, node))
+    mn, mx = None, None
+
+    while q:
+        depth, node = q.popleft()
+        # Current node is a leaf node
+        if not node.left and not node.right:
+            mx = depth if mx is None else max(mx, depth)
+            mn = depth if mn is None else min(mn, depth)
+            if mx - mn > 1:
+                return False
+        if node.left:
+            q.append((depth + 1, node.left))
+        if node.right:
+            q.append((depth + 1, node.right))
+
+    return mx - mn <= 1
+
+
+# Tests
+class Test(unittest.TestCase):
+    class BinaryTreeNode(object):
+        def __init__(self, value):
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def insert_left(self, value):
+            self.left = Test.BinaryTreeNode(value)
+            return self.left
+
+        def insert_right(self, value):
+            self.right = Test.BinaryTreeNode(value)
+            return self.right
+
+    def test_full_tree(self):
+        tree = Test.BinaryTreeNode(5)
+        left = tree.insert_left(8)
+        right = tree.insert_right(6)
+        left.insert_left(1)
+        left.insert_right(2)
+        right.insert_left(3)
+        right.insert_right(4)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_both_leaves_at_the_same_depth(self):
+        tree = Test.BinaryTreeNode(3)
+        left = tree.insert_left(4)
+        right = tree.insert_right(2)
+        left.insert_left(1)
+        right.insert_right(9)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_leaf_heights_differ_by_one(self):
+        tree = Test.BinaryTreeNode(6)
+        left = tree.insert_left(1)
+        right = tree.insert_right(0)
+        right.insert_right(7)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_leaf_heights_differ_by_two(self):
+        tree = Test.BinaryTreeNode(6)
+        left = tree.insert_left(1)
+        right = tree.insert_right(0)
+        right_right = right.insert_right(7)
+        right_right.insert_right(8)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_three_leaves_total(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(5)
+        right = tree.insert_right(9)
+        right.insert_left(8)
+        right.insert_right(5)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_both_subtrees_superbalanced(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(5)
+        right = tree.insert_right(9)
+        right_left = right.insert_left(8)
+        right.insert_right(5)
+        right_left.insert_left(7)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_both_subtrees_superbalanced_two(self):
+        tree = Test.BinaryTreeNode(1)
+        left = tree.insert_left(2)
+        right = tree.insert_right(4)
+        left.insert_left(3)
+        left_right = left.insert_right(7)
+        left_right.insert_right(8)
+        right_right = right.insert_right(5)
+        right_right_right = right_right.insert_right(6)
+        right_right_right.insert_right(9)
+        result = is_balanced(tree)
+        self.assertFalse(result)
+
+    def test_only_one_node(self):
+        tree = Test.BinaryTreeNode(1)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+    def test_linked_list_tree(self):
+        tree = Test.BinaryTreeNode(1)
+        right = tree.insert_right(2)
+        right_right = right.insert_right(3)
+        right_right.insert_right(4)
+        result = is_balanced(tree)
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/bst-checker.py b/users/wpcarro/scratch/deepmind/part_two/bst-checker.py
new file mode 100644
index 0000000000..fd0374a9ce
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/bst-checker.py
@@ -0,0 +1,110 @@
+import unittest
+from collections import deque
+
+
+# While this function solves the problem, it uses O(n) space since we're storing
+# all of the less-thans and greater-thans.
+def is_binary_search_tree_first_attempt(root):
+    q = deque()
+    q.append((set(), set(), root))
+
+    while q:
+        lts, gts, node = q.popleft()
+
+        if not all([node.value < lt for lt in lts]):
+            return False
+        if not all([node.value > gt for gt in gts]):
+            return False
+
+        if node.left:
+            q.append((lts | {node.value}, gts, node.left))
+        if node.right:
+            q.append((lts, gts | {node.value}, node.right))
+
+    return True
+
+
+# While I did not originally solve this problem this way, when I learned that I
+# could condense the space of my solution's runtime, I wrote this.
+def is_binary_search_tree(root):
+    q = deque()
+    q.append((None, None, root))
+
+    while q:
+        lt, gt, node = q.popleft()
+
+        if not lt is None and node.value >= lt:
+            return False
+        if not gt is None and node.value <= gt:
+            return False
+
+        if node.left:
+            q.append((node.value, gt, node.left))
+        if node.right:
+            q.append((lt, node.value, node.right))
+
+    return True
+
+
+# Tests
+class Test(unittest.TestCase):
+    class BinaryTreeNode(object):
+        def __init__(self, value):
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def insert_left(self, value):
+            self.left = Test.BinaryTreeNode(value)
+            return self.left
+
+        def insert_right(self, value):
+            self.right = Test.BinaryTreeNode(value)
+            return self.right
+
+    def test_valid_full_tree(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(70)
+        left.insert_left(10)
+        left.insert_right(40)
+        right.insert_left(60)
+        right.insert_right(80)
+        result = is_binary_search_tree(tree)
+        self.assertTrue(result)
+
+    def test_both_subtrees_valid(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(30)
+        right = tree.insert_right(80)
+        left.insert_left(20)
+        left.insert_right(60)
+        right.insert_left(70)
+        right.insert_right(90)
+        result = is_binary_search_tree(tree)
+        self.assertFalse(result)
+
+    def test_descending_linked_list(self):
+        tree = Test.BinaryTreeNode(50)
+        left = tree.insert_left(40)
+        left_left = left.insert_left(30)
+        left_left_left = left_left.insert_left(20)
+        left_left_left.insert_left(10)
+        result = is_binary_search_tree(tree)
+        self.assertTrue(result)
+
+    def test_out_of_order_linked_list(self):
+        tree = Test.BinaryTreeNode(50)
+        right = tree.insert_right(70)
+        right_right = right.insert_right(60)
+        right_right.insert_right(80)
+        result = is_binary_search_tree(tree)
+        self.assertFalse(result)
+
+    def test_one_node_tree(self):
+        tree = Test.BinaryTreeNode(50)
+        result = is_binary_search_tree(tree)
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/cafe-order-checker.py b/users/wpcarro/scratch/deepmind/part_two/cafe-order-checker.py
new file mode 100644
index 0000000000..0e31214b83
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/cafe-order-checker.py
@@ -0,0 +1,64 @@
+import unittest
+
+
+# Solution
+def is_first_come_first_served(xs, ys, zs):
+    i, j = 0, 0
+    for z in zs:
+        if i < len(xs) and z == xs[i]:
+            i += 1
+        elif j < len(ys) and z == ys[j]:
+            j += 1
+        else:
+            return False
+    return i == len(xs) and j == len(ys)
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_both_registers_have_same_number_of_orders(self):
+        result = is_first_come_first_served([1, 4, 5], [2, 3, 6],
+                                            [1, 2, 3, 4, 5, 6])
+        self.assertTrue(result)
+
+    def test_registers_have_different_lengths(self):
+        result = is_first_come_first_served([1, 5], [2, 3, 6], [1, 2, 6, 3, 5])
+        self.assertFalse(result)
+
+    def test_one_register_is_empty(self):
+        result = is_first_come_first_served([], [2, 3, 6], [2, 3, 6])
+        self.assertTrue(result)
+
+    def test_served_orders_is_missing_orders(self):
+        result = is_first_come_first_served([1, 5], [2, 3, 6], [1, 6, 3, 5])
+        self.assertFalse(result)
+
+    def test_served_orders_has_extra_orders(self):
+        result = is_first_come_first_served([1, 5], [2, 3, 6],
+                                            [1, 2, 3, 5, 6, 8])
+        self.assertFalse(result)
+
+    def test_one_register_has_extra_orders(self):
+        result = is_first_come_first_served([1, 9], [7, 8], [1, 7, 8])
+        self.assertFalse(result)
+
+    def test_one_register_has_unserved_orders(self):
+        result = is_first_come_first_served([55, 9], [7, 8], [1, 7, 8, 9])
+        self.assertFalse(result)
+
+    # Bonus
+    def test_handles_repeats(self):
+        actual = is_first_come_first_served([1, 2, 1], [3, 4, 5, 5],
+                                            [3, 4, 1, 5, 5, 2, 1])
+        self.assertTrue(actual)
+
+    def test_kitchen_didnt_serve(self):
+        actual = is_first_come_first_served([1, 2], [3, 4], [1, 3, 4])
+        self.assertFalse(actual)
+
+    def test_customer_didnt_pay(self):
+        actual = is_first_come_first_served([2], [3, 4], [1, 3, 4])
+        self.assertFalse(actual)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/coin.ts b/users/wpcarro/scratch/deepmind/part_two/coin.ts
new file mode 100644
index 0000000000..8aa8de8bb8
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/coin.ts
@@ -0,0 +1,102 @@
+// The denomination of a coin.
+type Coin = number;
+
+// The amount of change remaining.
+type Amount = number;
+
+// Mapping of Coin -> Int
+type CoinBag = Map<Coin, number>;
+
+function createCoinBag(coins: Coin[]): CoinBag {
+  const result = new Map();
+
+  for (const coin of coins) {
+    result.set(coin, 0);
+  }
+
+  return result;
+}
+
+// This algorithm should work conceptual, but it does not actually
+// work. JavaScript uses reference equality when constructing a Set<Map<A,B>>,
+// so my result.size returns a higher number than I expect because it contains
+// many duplicate entries.
+//
+// Conceptually, I'm not sure this solution is optimal either -- even after I
+// can dedupe the entries in `result`.
+function changePossibilities(amt: Amount, coins: Coin[]): number {
+  if (amt === 0) {
+    return 1;
+  }
+  const result: Set<CoinBag> = new Set();
+
+  const q: [Coin, Amount, CoinBag][] = [];
+
+  for (const coin of coins) {
+    const bag = createCoinBag(coins);
+    bag.set(coin, 1);
+    q.push([coin, amt - coin, bag]);
+  }
+
+  while (q.length > 0) {
+    const [coin, amt, bag] = q.shift();
+
+    console.log([coin, amt, bag]);
+
+    if (amt === 0) {
+      result.add(bag);
+    } else if (amt < 0) {
+      continue;
+    } else {
+      for (const c of coins) {
+        const bagCopy = new Map(bag);
+        const value = bagCopy.get(c);
+        bagCopy.set(c, value + 1);
+        q.push([c, amt - c, bagCopy]);
+      }
+    }
+  }
+  console.log(result);
+  return result.size;
+}
+
+// Tests
+let desc = "sample input";
+let actual = changePossibilities(4, [1, 2, 3]);
+let expected = 4;
+assertEqual(actual, expected, desc);
+
+desc = "one way to make zero cents";
+actual = changePossibilities(0, [1, 2]);
+expected = 1;
+assertEqual(actual, expected, desc);
+
+desc = "no ways if no coins";
+actual = changePossibilities(1, []);
+expected = 0;
+assertEqual(actual, expected, desc);
+
+desc = "big coin value";
+actual = changePossibilities(5, [25, 50]);
+expected = 0;
+assertEqual(actual, expected, desc);
+
+desc = "big target amount";
+actual = changePossibilities(50, [5, 10]);
+expected = 6;
+assertEqual(actual, expected, desc);
+
+// I think InterviewCake designed this assertion to be computationally
+// expensive.
+desc = "change for one dollar";
+actual = changePossibilities(100, [1, 5, 10, 25, 50]);
+expected = 292;
+assertEqual(actual, expected, desc);
+
+function assertEqual(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/delete-node.py b/users/wpcarro/scratch/deepmind/part_two/delete-node.py
new file mode 100644
index 0000000000..4ed02ec308
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/delete-node.py
@@ -0,0 +1,57 @@
+import unittest
+
+
+def delete_node(node):
+    if node.next:
+        node.value = node.next.value
+        node.next = node.next.next
+    else:
+        raise Exception(
+            "We cannot delete the last node in a linked list using this function"
+        )
+
+
+# Tests
+class Test(unittest.TestCase):
+    class LinkedListNode(object):
+        def __init__(self, value, next=None):
+            self.value = value
+            self.next = next
+
+        def get_values(self):
+            node = self
+            values = []
+            while node is not None:
+                values.append(node.value)
+                node = node.next
+            return values
+
+    def setUp(self):
+        self.fourth = Test.LinkedListNode(4)
+        self.third = Test.LinkedListNode(3, self.fourth)
+        self.second = Test.LinkedListNode(2, self.third)
+        self.first = Test.LinkedListNode(1, self.second)
+
+    def test_node_at_beginning(self):
+        delete_node(self.first)
+        actual = self.first.get_values()
+        expected = [2, 3, 4]
+        self.assertEqual(actual, expected)
+
+    def test_node_in_middle(self):
+        delete_node(self.second)
+        actual = self.first.get_values()
+        expected = [1, 3, 4]
+        self.assertEqual(actual, expected)
+
+    def test_node_at_end(self):
+        with self.assertRaises(Exception):
+            delete_node(self.fourth)
+
+    def test_one_node_in_list(self):
+        unique = Test.LinkedListNode(1)
+        with self.assertRaises(Exception):
+            delete_node(unique)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space-beast-mode.py b/users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space-beast-mode.py
new file mode 100644
index 0000000000..c9edc32c88
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space-beast-mode.py
@@ -0,0 +1,114 @@
+import unittest
+
+
+################################################################################
+# InterviewCake's solution
+################################################################################
+def cycle_len(xs, i):
+    """
+    Returns the length of a cycle that contains no duplicate items.
+    """
+    result = 1
+    checkpt = i
+    current = xs[checkpt - 1]
+
+    while current != checkpt:
+        current = xs[current - 1]
+        result += 1
+
+    return result
+
+
+def theirs(xs):
+    """
+    This is InterviewCake's solution.
+    """
+    i = xs[-1]
+    for _ in range(len(xs) - 1):
+        i = xs[i - 1]
+
+    cycle_length = cycle_len(xs, i)
+
+    p0 = xs[-1]
+    p1 = xs[-1]
+    for _ in range(cycle_length):
+        p1 = xs[p1 - 1]
+
+    while p0 != p1:
+        p0 = xs[p0 - 1]
+        p1 = xs[p1 - 1]
+
+    print(p0, p1)
+
+    return p0
+
+
+################################################################################
+# My solution
+################################################################################
+def mine(xs):
+    """
+    This is the solution that I came up with, which differs from InterviewCake's
+    solution.
+    """
+    i = xs[-1]
+    offset = 1 if len(xs) % 2 == 0 else 2
+
+    for _ in range(len(xs) - offset):
+        i = xs[i - 1]
+
+    return i
+
+
+use_mine = True
+find_duplicate = mine if use_mine else theirs
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_just_the_repeated_number(self):
+        # len(xs) even
+        actual = find_duplicate([1, 1])
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_short_list(self):
+        # len(xs) even
+        actual = find_duplicate([1, 2, 3, 2])
+        expected = 2
+        self.assertEqual(actual, expected)
+
+    def test_medium_list(self):
+        # len(xs) even
+        actual = find_duplicate([1, 2, 5, 5, 5, 5])
+        expected = 5
+        self.assertEqual(actual, expected)
+
+    def test_long_list(self):
+        # len(xs) odd
+        actual = find_duplicate([4, 1, 4, 8, 3, 2, 7, 6, 5])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    ############################################################################
+    # Additional examples from InterviewCake.com
+    ############################################################################
+    def test_example_a(self):
+        # len(xs) even
+        actual = find_duplicate([3, 4, 2, 3, 1, 5])
+        expected = 3
+        self.assertTrue(actual, expected)
+
+    def test_example_b(self):
+        # len(xs) even
+        actual = find_duplicate([3, 1, 2, 2])
+        expected = 2
+        self.assertEqual(actual, expected)
+
+    def test_example_c(self):
+        # len(xs) odd BUT multiple duplicates
+        actual = find_duplicate([4, 3, 1, 1, 4])
+        self.assertTrue(actual in {1, 4})
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space.ts b/users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space.ts
new file mode 100644
index 0000000000..98f5bb144e
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/find-duplicate-optimize-for-space.ts
@@ -0,0 +1,70 @@
+function findRepeatBruteForce(xs: Array<number>): number {
+  // InterviewCake asks us to write a function that optimizes for space. Using
+  // brute force, we can write a function that returns an answer using constant
+  // (i.e. O(1)) space at the cost of a quadratic (i.e. O(n^2)) runtime.
+  //
+  // I did not think of this myself; InterviewCake's "Tell me more" hints
+  // did. Since I think this idea is clever, I wrote a solution from memory to
+  // help me internalize the solution.
+  for (let i = 0; i < xs.length; i += 1) {
+    let seeking = xs[i];
+    for (let j = i + 1; j < xs.length; j += 1) {
+      if (xs[j] === seeking) {
+        return seeking;
+      }
+    }
+  }
+}
+
+function findRepeatSort(xs: Array<number>): number {
+  // This version first sorts xs, which gives the function a time-complexity of
+  // O(n*log(n)), which is better than the quadratic complexity of the
+  // brute-force solution. The space requirement here is constant.
+  //
+  // Since we need to sort xs in-place to avoid paying a O(n) space cost for
+  // storing the newly sorted xs, we're mutating our input. InterviewCake
+  // advises us to not mutate our input.
+  xs.sort();
+  let i = 0;
+  let j = 1;
+  for (; j < xs.length; ) {
+    if (xs[i] === xs[j]) {
+      return xs[i];
+    }
+    i += 1;
+    j += 1;
+  }
+}
+
+function findRepeat(xs: Array<number>): number {
+  return 0;
+}
+
+// Tests
+let desc = "just the repeated number";
+let actual = findRepeat([1, 1]);
+let expected = 1;
+assertEqual(actual, expected, desc);
+
+desc = "short array";
+actual = findRepeat([1, 2, 3, 2]);
+expected = 2;
+assertEqual(actual, expected, desc);
+
+desc = "medium array";
+actual = findRepeat([1, 2, 5, 5, 5, 5]);
+expected = 5;
+assertEqual(actual, expected, desc);
+
+desc = "long array";
+actual = findRepeat([4, 1, 4, 8, 3, 2, 7, 6, 5]);
+expected = 4;
+assertEqual(actual, expected, desc);
+
+function assertEqual(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/find-rotation-point.ts b/users/wpcarro/scratch/deepmind/part_two/find-rotation-point.ts
new file mode 100644
index 0000000000..7bf1a48445
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/find-rotation-point.ts
@@ -0,0 +1,68 @@
+function findRotationPoint(xs: Array<string>): number {
+  // Find the rotation point in the vector.
+  let beg = 0;
+  let end = xs.length - 1;
+
+  while (beg != end) {
+    let mid = beg + Math.floor((end - beg) / 2);
+
+    if (beg === mid) {
+      return xs[beg] < xs[end] ? beg : end;
+    }
+
+    if (xs[end] <= xs[mid]) {
+      beg = mid;
+      end = end;
+    } else {
+      beg = beg;
+      end = mid;
+    }
+  }
+
+  return beg;
+}
+
+// Tests
+let desc;
+let actual;
+let expected;
+
+desc = "small array one";
+actual = findRotationPoint(["cape", "cake"]);
+expected = 1;
+assertEquals(actual, expected, desc);
+
+desc = "small array two";
+actual = findRotationPoint(["cake", "cape"]);
+expected = 0;
+assertEquals(actual, expected, desc);
+
+desc = "medium array";
+actual = findRotationPoint(["grape", "orange", "plum", "radish", "apple"]);
+expected = 4;
+assertEquals(actual, expected, desc);
+
+desc = "large array";
+actual = findRotationPoint([
+  "ptolemaic",
+  "retrograde",
+  "supplant",
+  "undulate",
+  "xenoepist",
+  "asymptote",
+  "babka",
+  "banoffee",
+  "engender",
+  "karpatka",
+  "othellolagkage"
+]);
+expected = 5;
+assertEquals(actual, expected, desc);
+
+function assertEquals(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/graph-coloring.ts b/users/wpcarro/scratch/deepmind/part_two/graph-coloring.ts
new file mode 100644
index 0000000000..a0b6d5dbae
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/graph-coloring.ts
@@ -0,0 +1,232 @@
+type Color = string;
+
+interface GraphNode {
+  label: string;
+  neighbors: Set<GraphNode>;
+  color: string;
+}
+
+class GraphNode {
+  constructor(label: string) {
+    this.label = label;
+    this.neighbors = new Set();
+    this.color = null;
+  }
+}
+
+interface Queue<A> {
+  xs: Array<A>;
+}
+
+class Queue<A> {
+  constructor() {
+    this.xs = [];
+  }
+  isEmpty(): boolean {
+    return this.xs.length === 0;
+  }
+  enqueue(x: A): void {
+    this.xs.push(x);
+  }
+  dequeue(): A {
+    return this.xs.shift();
+  }
+}
+
+type Graph = Array<GraphNode>;
+
+// Return a set of all of the colors from the neighbor nodes of `node`.
+function neighborColors(node: GraphNode): Set<Color> {
+  const result: Set<Color> = new Set();
+
+  for (const x of node.neighbors) {
+    if (typeof x.color === 'string') {
+      result.add(x.color);
+    }
+  }
+
+  return result;
+}
+
+// Returns the set difference between sets `xs`, and `ys`.
+function setDifference<A>(xs: Set<A>, ys: Set<A>): Set<A> {
+  const result: Set<A> = new Set();
+
+  for (const x of xs) {
+    if (!ys.has(x)) {
+      result.add(x);
+    }
+  }
+
+  return result;
+}
+
+// Returns an element from the set, `xs`.
+// Throwns an error if `xs` is an empty set.
+function choose<A>(xs: Set<A>): A {
+  if (xs.size === 0) {
+    throw new Error('Cannot choose an element from an empty set.');
+  } else {
+    return xs.values().next().value;
+  }
+}
+
+// Returns true if `node` is present in `node.neighbors`.
+function isCyclic(node: GraphNode): boolean {
+  for (const x of node.neighbors) {
+    if (x === node) {
+      return true;
+    }
+  }
+}
+
+function colorGraph(graph: Graph, colors: Array<Color>): void {
+  const allColors = new Set(colors);
+
+  for (const node of graph) {
+    if (isCyclic(node)) {
+      throw new Error('InterviewCake would like me to invalidate this');
+    }
+    if (typeof node.color !== 'string') {
+      node.color = choose(setDifference(allColors, neighborColors(node)));
+    }
+  }
+}
+
+
+// Tests
+const colors = ['red', 'green', 'blue', 'orange', 'yellow', 'white'];
+
+let graph = [];
+{
+  const nodeA = new GraphNode('A');
+  const nodeB = new GraphNode('B');
+  const nodeC = new GraphNode('C');
+  const nodeD = new GraphNode('D');
+  nodeA.neighbors.add(nodeB);
+  nodeB.neighbors.add(nodeA);
+  nodeB.neighbors.add(nodeC);
+  nodeC.neighbors.add(nodeB);
+  nodeC.neighbors.add(nodeD);
+  nodeD.neighbors.add(nodeC);
+  graph = [nodeA, nodeB, nodeC, nodeD];
+}
+colorGraph(graph, colors);
+assertEqual(validateGraphColoring(graph), true, 'line graph');
+
+{
+  const nodeA = new GraphNode('A');
+  const nodeB = new GraphNode('B');
+  const nodeC = new GraphNode('C');
+  const nodeD = new GraphNode('D');
+  nodeA.neighbors.add(nodeB);
+  nodeB.neighbors.add(nodeA);
+  nodeC.neighbors.add(nodeD);
+  nodeD.neighbors.add(nodeC);
+  graph = [nodeA, nodeB, nodeC, nodeD];
+}
+colorGraph(graph, colors);
+assertEqual(validateGraphColoring(graph), true, 'separate graph');
+
+{
+  const nodeA = new GraphNode('A');
+  const nodeB = new GraphNode('B');
+  const nodeC = new GraphNode('C');
+  nodeA.neighbors.add(nodeB);
+  nodeA.neighbors.add(nodeC);
+  nodeB.neighbors.add(nodeA);
+  nodeB.neighbors.add(nodeC);
+  nodeC.neighbors.add(nodeA);
+  nodeC.neighbors.add(nodeB);
+  graph = [nodeA, nodeB, nodeC];
+}
+colorGraph(graph, colors);
+assertEqual(validateGraphColoring(graph), true, 'triangle graph');
+
+{
+  const nodeA = new GraphNode('A');
+  const nodeB = new GraphNode('B');
+  const nodeC = new GraphNode('C');
+  const nodeD = new GraphNode('D');
+  const nodeE = new GraphNode('E');
+  nodeA.neighbors.add(nodeB);
+  nodeA.neighbors.add(nodeC);
+  nodeB.neighbors.add(nodeA);
+  nodeB.neighbors.add(nodeC);
+  nodeB.neighbors.add(nodeD);
+  nodeB.neighbors.add(nodeE);
+  nodeC.neighbors.add(nodeA);
+  nodeC.neighbors.add(nodeB);
+  nodeC.neighbors.add(nodeD);
+  nodeC.neighbors.add(nodeE);
+  nodeD.neighbors.add(nodeB);
+  nodeD.neighbors.add(nodeC);
+  nodeD.neighbors.add(nodeE);
+  nodeE.neighbors.add(nodeB);
+  nodeE.neighbors.add(nodeC);
+  nodeE.neighbors.add(nodeD);
+  graph = [nodeA, nodeB, nodeC, nodeD, nodeE];
+}
+colorGraph(graph, colors);
+assertEqual(validateGraphColoring(graph), true, 'envelope graph');
+
+{
+  const nodeA = new GraphNode('A');
+  nodeA.neighbors.add(nodeA);
+  graph = [nodeA];
+}
+assertThrows(() => {
+  colorGraph(graph, colors);
+}, 'loop graph');
+
+function validateGraphColoring(graph) {
+
+  const maxDegree = Math.max(...graph.map(node => node.neighbors.size));
+
+  const colorsUsed = new Set();
+
+  graph.forEach(node => {
+    colorsUsed.add(node.color);
+  });
+
+  if (colorsUsed.has(null)) {
+    return false;
+  }
+
+  if (colorsUsed.size > maxDegree + 1) {
+    return false;
+  }
+
+  let badEdges = 0;
+
+  graph.forEach(node => {
+    node.neighbors.forEach(neighbor => {
+      if (neighbor.color === node.color) {
+        badEdges += 1;
+      }
+    });
+  });
+
+  if (badEdges > 0) {
+    return false;
+  }
+
+  return true;
+}
+
+function assertEqual(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`);
+  }
+}
+
+function assertThrows(func, desc) {
+  try {
+    func();
+    console.log(`${desc} ... FAIL`);
+  } catch (e) {
+    console.log(`${desc} ... PASS`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/highest-product-of-3.py b/users/wpcarro/scratch/deepmind/part_two/highest-product-of-3.py
new file mode 100644
index 0000000000..8ebb5cf29a
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/highest-product-of-3.py
@@ -0,0 +1,81 @@
+import unittest
+import sys
+import trace
+
+
+def highest_product_of_3(xs):
+    if len(xs) < 3:
+        raise Exception("List needs to contain at least three elements.")
+    hp3 = xs[0] * xs[1] * xs[2]
+    hp2 = xs[0] * xs[1]
+    lp2 = xs[0] * xs[1]
+    hn = max(xs[0], xs[1])
+    ln = min(xs[0], xs[1])
+    for x in xs[2:]:
+        hp3 = max(hp3, hp2 * x, lp2 * x)
+        hp2 = max(hp2, hn * x, ln * x)
+        lp2 = min(lp2, hn * x, ln * x)
+        hn = max(hn, x)
+        ln = min(ln, x)
+    return hp3
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_short_list(self):
+        actual = highest_product_of_3([1, 2, 3, 4])
+        expected = 24
+        self.assertEqual(actual, expected)
+
+    def test_longer_list(self):
+        actual = highest_product_of_3([6, 1, 3, 5, 7, 8, 2])
+        expected = 336
+        self.assertEqual(actual, expected)
+
+    def test_list_has_one_negative(self):
+        actual = highest_product_of_3([-5, 4, 8, 2, 3])
+        expected = 96
+        self.assertEqual(actual, expected)
+
+    def test_list_has_two_negatives(self):
+        actual = highest_product_of_3([-10, 1, 3, 2, -10])
+        expected = 300
+        self.assertEqual(actual, expected)
+
+    def test_list_is_all_negatives(self):
+        actual = highest_product_of_3([-5, -1, -3, -2])
+        expected = -6
+        self.assertEqual(actual, expected)
+
+    def test_error_with_empty_list(self):
+        with self.assertRaises(Exception):
+            highest_product_of_3([])
+
+    def test_error_with_one_number(self):
+        with self.assertRaises(Exception):
+            highest_product_of_3([1])
+
+    def test_error_with_two_numbers(self):
+        with self.assertRaises(Exception):
+            highest_product_of_3([1, 1])
+
+    def test_custom(self):
+        actual = highest_product_of_3([9, 5, 2, 1, 7, 3])
+        expected = 9 * 7 * 5
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
+
+
+def main():
+    highest_product_of_3([-5, -1, -3, -2])
+
+
+tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix],
+                     trace=0,
+                     count=1)
+
+tracer.run('main()')
+r = tracer.results()
+r.write_results(show_missing=True, coverdir=".")
diff --git a/users/wpcarro/scratch/deepmind/part_two/inflight-entertainment.ts b/users/wpcarro/scratch/deepmind/part_two/inflight-entertainment.ts
new file mode 100644
index 0000000000..d6da1db3d3
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/inflight-entertainment.ts
@@ -0,0 +1,85 @@
+function canTwoMoviesFillFlightBonus(
+  xs: Array<number>,
+  duration: number
+): boolean {
+  // Returns true if two movies exist that can fill the flight duration +/- 20
+  // minutes.
+  const seeking = {};
+
+  for (let x of xs) {
+    for (let i = 0; i < 40; i += 1) {
+      if (seeking[x + i + 1]) {
+        return true;
+      }
+    }
+    for (let i = 1; i <= 20; i += 1) {
+      seeking[duration - x - i] = true;
+      seeking[duration - x + i] = true;
+    }
+  }
+
+  return false;
+}
+
+function canTwoMoviesFillFlight(xs: Array<number>, duration: number): boolean {
+  const seeking = {};
+
+  for (let x of xs) {
+    if (seeking[x]) {
+      return true;
+    } else {
+      seeking[duration - x] = true;
+    }
+  }
+
+  return false;
+}
+
+// Tests
+let desc = "short flight";
+let actual = canTwoMoviesFillFlight([2, 4], 1);
+let expected = false;
+assertEquals(actual, expected, desc);
+
+desc = "long flight";
+actual = canTwoMoviesFillFlight([2, 4], 6);
+expected = true;
+assertEquals(actual, expected, desc);
+
+desc = "one movie half flight length";
+actual = canTwoMoviesFillFlight([3, 8], 6);
+expected = false;
+assertEquals(actual, expected, desc);
+
+desc = "two movies half flight length";
+actual = canTwoMoviesFillFlight([3, 8, 3], 6);
+expected = true;
+assertEquals(actual, expected, desc);
+
+desc = "lots of possible pairs";
+actual = canTwoMoviesFillFlight([1, 2, 3, 4, 5, 6], 7);
+expected = true;
+assertEquals(actual, expected, desc);
+
+desc = "not using first movie";
+actual = canTwoMoviesFillFlight([4, 3, 2], 5);
+expected = true;
+assertEquals(actual, expected, desc);
+
+desc = "only one movie";
+actual = canTwoMoviesFillFlight([6], 6);
+expected = false;
+assertEquals(actual, expected, desc);
+
+desc = "no movies";
+actual = canTwoMoviesFillFlight([], 2);
+expected = false;
+assertEquals(actual, expected, desc);
+
+function assertEquals(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/merge-sorted-arrays.ts b/users/wpcarro/scratch/deepmind/part_two/merge-sorted-arrays.ts
new file mode 100644
index 0000000000..2d478e0e37
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/merge-sorted-arrays.ts
@@ -0,0 +1,63 @@
+function mergeArrays(xs: Array<number>, ys: Array<number>): Array<number> {
+  let i = 0;
+  let j = 0;
+  const result = [];
+
+  for (let q = 0; q < xs.length + ys.length; q += 1) {
+    if (i === xs.length) {
+      while (j < ys.length) {
+        result.push(ys[j]);
+        j += 1;
+      }
+    } else if (j === ys.length) {
+      while (i < xs.length) {
+        result.push(xs[i]);
+        i += 1;
+      }
+    } else if (xs[i] < ys[j]) {
+      result.push(xs[i]);
+      i += 1;
+    } else {
+      result.push(ys[j]);
+      j += 1;
+    }
+  }
+
+  return result;
+}
+
+// Tests
+let desc = "both arrays are empty";
+let actual = mergeArrays([], []);
+let expected = [];
+assertDeepEqual(actual, expected, desc);
+
+desc = "first array is empty";
+actual = mergeArrays([], [1, 2, 3]);
+expected = [1, 2, 3];
+assertDeepEqual(actual, expected, desc);
+
+desc = "second array is empty";
+actual = mergeArrays([5, 6, 7], []);
+expected = [5, 6, 7];
+assertDeepEqual(actual, expected, desc);
+
+desc = "both arrays have some numbers";
+actual = mergeArrays([2, 4, 6], [1, 3, 7]);
+expected = [1, 2, 3, 4, 6, 7];
+assertDeepEqual(actual, expected, desc);
+
+desc = "arrays are different lengths";
+actual = mergeArrays([2, 4, 6, 8], [1, 7]);
+expected = [1, 2, 4, 6, 7, 8];
+assertDeepEqual(actual, expected, desc);
+
+function assertDeepEqual(a: Array<number>, b: Array<number>, desc: string) {
+  const aStr = JSON.stringify(a);
+  const bStr = JSON.stringify(b);
+  if (aStr !== bStr) {
+    console.log(`${desc} ... FAIL: ${aStr} != ${bStr}`);
+  } else {
+    console.log(`${desc} ... PASS`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/merging-ranges.py b/users/wpcarro/scratch/deepmind/part_two/merging-ranges.py
new file mode 100644
index 0000000000..23d0813d15
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/merging-ranges.py
@@ -0,0 +1,115 @@
+import unittest
+import timeit
+
+
+# Solution that uses O(n) space to store the result.
+def not_in_place(xs):
+    xs.sort()
+    result = [xs[0]]
+    for ca, cb in xs[1:]:
+        pa, pb = result[-1]
+        if ca <= pb:
+            result[-1] = (pa, max(pb, cb))
+        else:
+            result.append((ca, cb))
+    return result
+
+
+# Solution that uses O(1) space to store the result.
+def in_place(xs):
+    xs.sort()
+    i = 0
+    j = i + 1
+    while j < len(xs):
+        pa, pb = xs[i]
+        ca, cb = xs[j]
+        if ca <= pb:
+            xs[i] = (pa, max(pb, cb))
+            del xs[j]
+        else:
+            i = j
+            j += 1
+    return xs
+
+
+def test_nip():
+    inputs = [
+        [(1, 3), (2, 4)],
+        [(5, 6), (6, 8)],
+        [(1, 8), (2, 5)],
+        [(1, 3), (4, 8)],
+        [(1, 4), (2, 5), (5, 8)],
+        [(5, 8), (1, 4), (6, 8)],
+        [(1, 10), (2, 5), (6, 8), (9, 10), (10, 12)],
+        [(0, 1), (3, 5), (4, 8), (10, 12), (9, 10)],
+    ]
+    for x in inputs:
+        not_in_place(x)
+
+
+def test_ip():
+    inputs = [
+        [(1, 3), (2, 4)],
+        [(5, 6), (6, 8)],
+        [(1, 8), (2, 5)],
+        [(1, 3), (4, 8)],
+        [(1, 4), (2, 5), (5, 8)],
+        [(5, 8), (1, 4), (6, 8)],
+        [(1, 10), (2, 5), (6, 8), (9, 10), (10, 12)],
+        [(0, 1), (3, 5), (4, 8), (10, 12), (9, 10)],
+    ]
+    for x in inputs:
+        in_place(x)
+
+
+merge_ranges = in_place
+
+setup = 'from __main__ import test_nip, test_ip'
+print(timeit.timeit('test_nip()', number=10000, setup=setup))
+print(timeit.timeit('test_ip()', number=10000, setup=setup))
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_meetings_overlap(self):
+        actual = merge_ranges([(1, 3), (2, 4)])
+        expected = [(1, 4)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_touch(self):
+        actual = merge_ranges([(5, 6), (6, 8)])
+        expected = [(5, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meeting_contains_other_meeting(self):
+        actual = merge_ranges([(1, 8), (2, 5)])
+        expected = [(1, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_stay_separate(self):
+        actual = merge_ranges([(1, 3), (4, 8)])
+        expected = [(1, 3), (4, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_multiple_merged_meetings(self):
+        actual = merge_ranges([(1, 4), (2, 5), (5, 8)])
+        expected = [(1, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_meetings_not_sorted(self):
+        actual = merge_ranges([(5, 8), (1, 4), (6, 8)])
+        expected = [(1, 4), (5, 8)]
+        self.assertEqual(actual, expected)
+
+    def test_one_long_meeting_contains_smaller_meetings(self):
+        actual = merge_ranges([(1, 10), (2, 5), (6, 8), (9, 10), (10, 12)])
+        expected = [(1, 12)]
+        self.assertEqual(actual, expected)
+
+    def test_sample_input(self):
+        actual = merge_ranges([(0, 1), (3, 5), (4, 8), (10, 12), (9, 10)])
+        expected = [(0, 1), (3, 8), (9, 12)]
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/mesh-message.py b/users/wpcarro/scratch/deepmind/part_two/mesh-message.py
new file mode 100644
index 0000000000..a265296ab0
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/mesh-message.py
@@ -0,0 +1,183 @@
+import unittest
+from collections import deque
+from heapq import heappush, heappop
+
+
+################################################################################
+# InterviewCake.com
+################################################################################
+# construct_path :: Map String String -> String -> String -> [String]
+def construct_path(paths, beg, end):
+    """
+    Reconstruct the path from `beg` to `end`.
+    """
+    result = []
+    current = end
+
+    print(paths)
+    print(beg, end)
+    print('-----')
+    while current:
+        result.append(current)
+        current = paths[current]
+
+    result.reverse()
+    return result
+
+
+def get_path_ic(graph, beg, end):
+    """
+    InterviewCake uses a dictionary and back-tracking to store and reconstruct
+    the path instead of storing the path as state on each node.
+    This reduces the memory costs. See get_path_bft for an example of this less
+    optimal solution.
+    """
+    if beg not in graph:
+        raise Exception('Origin node absent from graph.')
+
+    if end not in graph:
+        raise Exception('Destination node absent from graph.')
+
+    q = deque()
+    q.append(beg)
+    paths = {beg: None}
+
+    while q:
+        node = q.popleft()
+
+        if node == end:
+            print(graph)
+            return construct_path(paths, beg, end)
+
+        for x in graph[node]:
+            if x not in paths:
+                paths[x] = node
+                q.append(x)
+
+    return None
+
+
+################################################################################
+# Per-node state
+################################################################################
+def get_path_bft(graph, beg, end):
+    """
+    Here we find the shortest path from `beg` to `end` in `graph` by doing a BFT
+    from beg to end and storing the path state alongside each node in the queue.
+    """
+    if beg not in graph:
+        raise Exception('Origin node absent from graph.')
+
+    if end not in graph:
+        raise Exception('Destination node absent from graph.')
+
+    q = deque()
+    seen = set()
+    q.append([beg])
+
+    while q:
+        path = q.popleft()
+        node = path[-1]
+        seen.add(node)
+
+        if node == end:
+            return path
+
+        for x in graph[node]:
+            if x not in seen:
+                q.append(path + [x])
+
+
+################################################################################
+# Dijkstra's Algorithm
+################################################################################
+def get_path(graph, beg, end):
+    """
+    Here we find the shortest path using Dijkstra's algorithm, which is my
+    favorite solution.
+    """
+    if beg not in graph:
+        raise Exception(
+            'The origin node, {}, is not present in the graph'.format(beg))
+
+    if end not in graph:
+        raise Exception(
+            'The origin node, {}, is not present in the graph'.format(end))
+
+    q = []
+    seen = set()
+    heappush(q, (1, [beg]))
+
+    while q:
+        weight, path = heappop(q)
+        node = path[-1]
+        seen.add(node)
+
+        if node == end:
+            return path
+
+        for x in graph[node]:
+            if x not in seen:
+                heappush(q, (weight + 1, path + [x]))
+
+    return None
+
+
+# Tests
+class Test(unittest.TestCase):
+    def setUp(self):
+        self.graph = {
+            'a': ['b', 'c', 'd'],
+            'b': ['a', 'd'],
+            'c': ['a', 'e'],
+            'd': ['b', 'a'],
+            'e': ['c'],
+            'f': ['g'],
+            'g': ['f'],
+        }
+
+    def test_two_hop_path_1(self):
+        actual = get_path(self.graph, 'a', 'e')
+        expected = ['a', 'c', 'e']
+        self.assertEqual(actual, expected)
+
+    def test_two_hop_path_2(self):
+        actual = get_path(self.graph, 'd', 'c')
+        expected = ['d', 'a', 'c']
+        self.assertEqual(actual, expected)
+
+    def test_one_hop_path_1(self):
+        actual = get_path(self.graph, 'a', 'c')
+        expected = ['a', 'c']
+        self.assertEqual(actual, expected)
+
+    def test_one_hop_path_2(self):
+        actual = get_path(self.graph, 'f', 'g')
+        expected = ['f', 'g']
+        self.assertEqual(actual, expected)
+
+    def test_one_hop_path_3(self):
+        actual = get_path(self.graph, 'g', 'f')
+        expected = ['g', 'f']
+        self.assertEqual(actual, expected)
+
+    def test_zero_hop_path(self):
+        actual = get_path(self.graph, 'a', 'a')
+        expected = ['a']
+        self.assertEqual(actual, expected)
+
+    def test_no_path(self):
+        actual = get_path(self.graph, 'a', 'f')
+        expected = None
+        self.assertEqual(actual, expected)
+
+    def test_start_node_not_present(self):
+        with self.assertRaises(Exception):
+            get_path(self.graph, 'h', 'a')
+
+    def test_end_node_not_present(self):
+        with self.assertRaises(Exception):
+            get_path(self.graph, 'a', 'h')
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/misc/matrix-traversals.py b/users/wpcarro/scratch/deepmind/part_two/misc/matrix-traversals.py
new file mode 100644
index 0000000000..52354f990e
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/misc/matrix-traversals.py
@@ -0,0 +1,104 @@
+# Herein I'm practicing two-dimensional matrix traversals in all directions of
+# which I can conceive:
+# 0. T -> B; L -> R
+# 1. T -> B; R -> L
+# 2. B -> T; L -> R
+# 3. B -> T; R -> L
+#
+# Commentary:
+# When I think of matrices, I'm reminded of cartesian planes. I think of the
+# cells as (X,Y) coordinates. This has been a pitfall for me because matrices
+# are usually encoded in the opposite way. That is, to access a cell at the
+# coordinates (X,Y) given a matrix M, you index M like this: M[Y][X]. To attempt
+# to avoid this confusion, instead of saying X and Y, I will prefer saying
+# "column" and "row".
+#
+# When traversing a matrix, you typically traverse vertically and then
+# horizontally; in other words, the rows come first followed by the columns. As
+# such, I'd like to refer to traversal orders as "top-to-bottom, left-to-right"
+# rather than "left-to-right, top-to-bottom".
+#
+# These practices are all in an attempt to rewire my thinking.
+
+# This is a list of matrices where the index of a matrix corresponds to the
+# order in which it should be traversed to produce the sequence:
+# [1,2,3,4,5,6,7,8,9].
+boards = [[[1, 2, 3], [4, 5, 6], [7, 8, 9]], [[3, 2, 1], [6, 5, 4], [9, 8, 7]],
+          [[7, 8, 9], [4, 5, 6], [1, 2, 3]], [[9, 8, 7], [6, 5, 4], [3, 2, 1]]]
+
+# T -> B; L -> R
+board = boards[0]
+result = []
+for row in board:
+    for col in row:
+        result.append(col)
+print(result)
+
+# T -> B; R -> L
+board = boards[1]
+result = []
+for row in board:
+    for col in reversed(row):
+        result.append(col)
+print(result)
+
+# B -> T; L -> R
+board = boards[2]
+result = []
+for row in reversed(board):
+    for col in row:
+        result.append(col)
+print(result)
+
+# B -> T; R -> L
+board = boards[3]
+result = []
+for row in reversed(board):
+    for col in reversed(row):
+        result.append(col)
+print(result)
+
+################################################################################
+# Neighbors
+################################################################################
+
+import random
+
+
+# Generate a matrix of size `rows` x `cols` where each cell contains an item
+# randomly selected from `xs`.
+def generate_board(rows, cols, xs):
+    result = []
+    for _ in range(rows):
+        row = []
+        for _ in range(cols):
+            row.append(random.choice(xs))
+        result.append(row)
+    return result
+
+
+# Print the `board` to the screen.
+def print_board(board):
+    print('\n'.join([' '.join(row) for row in board]))
+
+
+board = generate_board(4, 5, ['R', 'G', 'B'])
+print_board(board)
+
+
+# Return all of the cells horizontally and vertically accessible from a starting
+# cell at `row`, `col` in `board`.
+def neighbors(row, col, board):
+    result = {'top': [], 'bottom': [], 'left': [], 'right': []}
+    for i in range(row - 1, -1, -1):
+        result['top'].append(board[i][col])
+    for i in range(row + 1, len(board)):
+        result['bottom'].append(board[i][col])
+    for i in range(col - 1, -1, -1):
+        result['left'].append(board[row][i])
+    for i in range(col + 1, len(board[0])):
+        result['right'].append(board[row][i])
+    return result
+
+
+print(neighbors(1, 2, board))
diff --git a/users/wpcarro/scratch/deepmind/part_two/nth-fibonacci.py b/users/wpcarro/scratch/deepmind/part_two/nth-fibonacci.py
new file mode 100644
index 0000000000..14e176b62a
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/nth-fibonacci.py
@@ -0,0 +1,72 @@
+import unittest
+
+
+# Compute the fibonacci using a bottom-up algorithm.
+def fib(n):
+    if n < 0:
+        raise Error('Cannot call fibonacci with negative values')
+    cache = [0, 1]
+    for i in range(n):
+        cache[0], cache[1] = cache[1], cache[0] + cache[1]
+    return cache[0]
+
+
+# Compute the fibonacci using memoization.
+def fib_memoized(n):
+    cache = {
+        0: 0,
+        1: 1,
+    }
+
+    def do_fib(n):
+        if n < 0:
+            raise Error('The fib function does not support negative inputs')
+
+        if n in cache:
+            return cache[n]
+
+        cache[n - 1] = do_fib(n - 1)
+        cache[n - 2] = do_fib(n - 2)
+        return cache[n - 1] + cache[n - 2]
+
+    return do_fib(n)
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_zeroth_fibonacci(self):
+        actual = fib(0)
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_first_fibonacci(self):
+        actual = fib(1)
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_second_fibonacci(self):
+        actual = fib(2)
+        expected = 1
+        self.assertEqual(actual, expected)
+
+    def test_third_fibonacci(self):
+        actual = fib(3)
+        expected = 2
+        self.assertEqual(actual, expected)
+
+    def test_fifth_fibonacci(self):
+        actual = fib(5)
+        expected = 5
+        self.assertEqual(actual, expected)
+
+    def test_tenth_fibonacci(self):
+        actual = fib(10)
+        expected = 55
+        self.assertEqual(actual, expected)
+
+    def test_negative_fibonacci(self):
+        with self.assertRaises(Exception):
+            fib(-1)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/package-lock.json b/users/wpcarro/scratch/deepmind/part_two/package-lock.json
new file mode 100644
index 0000000000..340aad9f5c
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/package-lock.json
@@ -0,0 +1,79 @@
+{
+  "name": "deepmind-part-two",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "dev": true
+    },
+    "make-error": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+      "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
+      "dev": true
+    },
+    "prettier": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz",
+      "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true
+    },
+    "source-map-support": {
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+      "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "ts-node": {
+      "version": "8.6.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.6.2.tgz",
+      "integrity": "sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg==",
+      "dev": true,
+      "requires": {
+        "arg": "^4.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "source-map-support": "^0.5.6",
+        "yn": "3.1.1"
+      }
+    },
+    "typescript": {
+      "version": "3.7.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
+      "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==",
+      "dev": true
+    },
+    "yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "dev": true
+    }
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/package.json b/users/wpcarro/scratch/deepmind/part_two/package.json
new file mode 100644
index 0000000000..1f10668ec8
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "deepmind-part-two",
+  "version": "1.0.0",
+  "description": "Practicing coding interview questions",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "William Carroll",
+  "license": "MIT",
+  "devDependencies": {
+    "prettier": "^2.0.2",
+    "ts-node": "^8.6.2",
+    "typescript": "^3.7.5"
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/permutation-palindrome.py b/users/wpcarro/scratch/deepmind/part_two/permutation-palindrome.py
new file mode 100644
index 0000000000..730b4bfdc8
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/permutation-palindrome.py
@@ -0,0 +1,37 @@
+import unittest
+from collections import Counter
+
+
+def has_palindrome_permutation(xs):
+    vs = Counter(xs).values()
+    return len([v for v in vs if v % 2 == 1]) in {0, 1}
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_permutation_with_odd_number_of_chars(self):
+        result = has_palindrome_permutation('aabcbcd')
+        self.assertTrue(result)
+
+    def test_permutation_with_even_number_of_chars(self):
+        result = has_palindrome_permutation('aabccbdd')
+        self.assertTrue(result)
+
+    def test_no_permutation_with_odd_number_of_chars(self):
+        result = has_palindrome_permutation('aabcd')
+        self.assertFalse(result)
+
+    def test_no_permutation_with_even_number_of_chars(self):
+        result = has_palindrome_permutation('aabbcd')
+        self.assertFalse(result)
+
+    def test_empty_string(self):
+        result = has_palindrome_permutation('')
+        self.assertTrue(result)
+
+    def test_one_character_string(self):
+        result = has_palindrome_permutation('a')
+        self.assertTrue(result)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/product-of-other-numbers.py b/users/wpcarro/scratch/deepmind/part_two/product-of-other-numbers.py
new file mode 100644
index 0000000000..6f7858ff4e
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/product-of-other-numbers.py
@@ -0,0 +1,68 @@
+import unittest
+
+
+# get_products_of_all_ints_except_at_index :: [Int] -> [Int]
+def get_products_of_all_ints_except_at_index(xs):
+    n = len(xs)
+    if n < 2:
+        raise Exception("Cannot computer without 2 or elements")
+    # lhs
+    befores = [None] * n
+    befores[0] = 1
+    for i in range(1, n):
+        befores[i] = befores[i - 1] * xs[i - 1]
+
+    # rhs
+    afters = [None] * n
+    afters[-1] = 1
+    for i in range(n - 2, -1, -1):
+        afters[i] = afters[i + 1] * xs[i + 1]
+
+    result = [None] * n
+    for i in range(n):
+        result[i] = befores[i] * afters[i]
+    return result
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_small_list(self):
+        actual = get_products_of_all_ints_except_at_index([1, 2, 3])
+        expected = [6, 3, 2]
+        self.assertEqual(actual, expected)
+
+    def test_longer_list(self):
+        actual = get_products_of_all_ints_except_at_index([8, 2, 4, 3, 1, 5])
+        expected = [120, 480, 240, 320, 960, 192]
+        self.assertEqual(actual, expected)
+
+    def test_list_has_one_zero(self):
+        actual = get_products_of_all_ints_except_at_index([6, 2, 0, 3])
+        expected = [0, 0, 36, 0]
+        self.assertEqual(actual, expected)
+
+    def test_list_has_two_zeros(self):
+        actual = get_products_of_all_ints_except_at_index([4, 0, 9, 1, 0])
+        expected = [0, 0, 0, 0, 0]
+        self.assertEqual(actual, expected)
+
+    def test_one_negative_number(self):
+        actual = get_products_of_all_ints_except_at_index([-3, 8, 4])
+        expected = [32, -12, -24]
+        self.assertEqual(actual, expected)
+
+    def test_all_negative_numbers(self):
+        actual = get_products_of_all_ints_except_at_index([-7, -1, -4, -2])
+        expected = [-8, -56, -14, -28]
+        self.assertEqual(actual, expected)
+
+    def test_error_with_empty_list(self):
+        with self.assertRaises(Exception):
+            get_products_of_all_ints_except_at_index([])
+
+    def test_error_with_one_number(self):
+        with self.assertRaises(Exception):
+            get_products_of_all_ints_except_at_index([1])
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/recursive-string-permutations.ts b/users/wpcarro/scratch/deepmind/part_two/recursive-string-permutations.ts
new file mode 100644
index 0000000000..cb930d9ad6
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/recursive-string-permutations.ts
@@ -0,0 +1,85 @@
+// Returns a new string comprised of every characters in `xs` except for the
+// character at `i`.
+function everyOtherChar(xs: string, i: number): string[] {
+  const result = [];
+
+  for (let j = 0; j < xs.length; j += 1) {
+    if (i !== j) {
+      result.push(xs[j]);
+    }
+  }
+
+  return [xs[i], result.join('')];
+}
+
+function getPermutations(xs: string): Set<string> {
+  if (xs === '') {
+    return new Set(['']);
+  }
+
+  const result: Set<string> = new Set;
+
+  for (let i = 0; i < xs.length; i += 1) {
+    const [char, rest] = everyOtherChar(xs, i);
+    const perms = getPermutations(rest);
+
+    for (const perm of perms) {
+      result.add(char + perm);
+    }
+  }
+
+  return result;
+}
+
+// Tests
+let desc = 'empty string';
+let input = '';
+let actual = getPermutations(input);
+let expected = new Set(['']);
+assert(isSetsEqual(actual, expected), desc);
+
+desc = 'one character string';
+input = 'a';
+actual = getPermutations(input);
+expected = new Set(['a']);
+assert(isSetsEqual(actual, expected), desc);
+
+desc = 'two character string';
+input = 'ab';
+actual = getPermutations(input);
+expected = new Set(['ab', 'ba']);
+assert(isSetsEqual(actual, expected), desc);
+
+desc = 'three character string';
+input = 'abc';
+actual = getPermutations(input);
+expected = new Set(['abc', 'acb', 'bac', 'bca', 'cab', 'cba']);
+assert(isSetsEqual(actual, expected), desc);
+
+desc = 'four character string';
+input = 'abca';
+actual = getPermutations(input);
+expected = new Set([
+  'abca', 'abac', 'acba', 'acab', 'aabc', 'aacb', 'baca', 'baac', 'bcaa',
+  'bcaa', 'baac', 'baca', 'caba', 'caab', 'cbaa', 'cbaa', 'caab', 'caba',
+  'aabc', 'aacb', 'abac', 'abca', 'acab', 'acba'
+]);
+assert(isSetsEqual(actual, expected), desc);
+
+function isSetsEqual(as, bs) {
+  if (as.size !== bs.size) {
+    return false;
+  }
+  for (let a of as) {
+    if (!bs.has(a)) return false;
+  }
+  return true;
+}
+
+function assert(condition, desc) {
+  if (condition) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/reverse-string-in-place.ts b/users/wpcarro/scratch/deepmind/part_two/reverse-string-in-place.ts
new file mode 100644
index 0000000000..d714dfef99
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/reverse-string-in-place.ts
@@ -0,0 +1,13 @@
+// Reverse array of characters, `xs`, mutatively.
+function reverse(xs: Array<string>) {
+  let i: number = 0;
+  let j: number = xs.length - 1;
+
+  while (i < j) {
+    let tmp = xs[i];
+    xs[i] = xs[j]
+    xs[j] = tmp
+    i += 1
+    j -= 1
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/reverse-words.py b/users/wpcarro/scratch/deepmind/part_two/reverse-words.py
new file mode 100644
index 0000000000..033d11244c
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/reverse-words.py
@@ -0,0 +1,74 @@
+import unittest
+
+
+def reverse(xs, i, j):
+    """Reverse array of characters, xs, in-place."""
+    while i < j:
+        xs[i], xs[j] = xs[j], xs[i]
+        i += 1
+        j -= 1
+
+
+def reverse_words(xs):
+    punctuation = None
+    if len(xs) > 0 and xs[-1] in ".?!":
+        punctuation = xs.pop()
+    reverse(xs, 0, len(xs) - 1)
+    i = 0
+    j = i
+    while j < len(xs):
+        while j < len(xs) and xs[j] != ' ':
+            j += 1
+        reverse(xs, i, j - 1)
+        j += 1
+        i = j
+    if punctuation:
+        xs.append(punctuation)
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_one_word(self):
+        message = list('vault')
+        reverse_words(message)
+        expected = list('vault')
+        self.assertEqual(message, expected)
+
+    def test_two_words(self):
+        message = list('thief cake')
+        reverse_words(message)
+        expected = list('cake thief')
+        self.assertEqual(message, expected)
+
+    def test_three_words(self):
+        message = list('one another get')
+        reverse_words(message)
+        expected = list('get another one')
+        self.assertEqual(message, expected)
+
+    def test_multiple_words_same_length(self):
+        message = list('rat the ate cat the')
+        reverse_words(message)
+        expected = list('the cat ate the rat')
+        self.assertEqual(message, expected)
+
+    def test_multiple_words_different_lengths(self):
+        message = list('yummy is cake bundt chocolate')
+        reverse_words(message)
+        expected = list('chocolate bundt cake is yummy')
+        self.assertEqual(message, expected)
+
+    def test_empty_string(self):
+        message = list('')
+        reverse_words(message)
+        expected = list('')
+        self.assertEqual(message, expected)
+
+    def test_bonus_support_punctuation(self):
+        message = list('yummy is cake bundt chocolate this!')
+        reverse_words(message)
+        expected = list('this chocolate bundt cake is yummy!')
+        self.assertEqual(message, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/second-largest-item-in-bst.ts b/users/wpcarro/scratch/deepmind/part_two/second-largest-item-in-bst.ts
new file mode 100644
index 0000000000..4c5e57607d
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/second-largest-item-in-bst.ts
@@ -0,0 +1,219 @@
+/*******************************************************************************
+ * Setup
+ ******************************************************************************/
+
+interface BinaryTreeNode {
+  value: number;
+  left: BinaryTreeNode;
+  right: BinaryTreeNode;
+}
+
+class BinaryTreeNode {
+  constructor(value: number) {
+    this.value = value;
+    this.left  = null;
+    this.right = null;
+  }
+
+  insertLeft(value: number): BinaryTreeNode {
+    this.left = new BinaryTreeNode(value);
+    return this.left;
+  }
+
+  insertRight(value: number): BinaryTreeNode {
+    this.right = new BinaryTreeNode(value);
+    return this.right;
+  }
+}
+
+/*******************************************************************************
+ * First solution
+ ******************************************************************************/
+
+/**
+ * I first solved this problem using O(n) space and O(n*log(n))
+ * time. InterviewCake informs me that we can improve both the time and the
+ * space performance.
+ */
+function findSecondLargest_first(node: BinaryTreeNode): number {
+  const stack: Array<BinaryTreeNode> = [];
+  const xs: Array<number> = [];
+  stack.push(node);
+
+  while (stack.length > 0) {
+    const node = stack.pop()
+
+    xs.push(node.value);
+
+    if (node.left) {
+      stack.push(node.left);
+    }
+    if (node.right) {
+      stack.push(node.right);
+    }
+  }
+
+  xs.sort();
+
+  if (xs.length < 2) {
+    throw new Error('Cannot find the second largest element in a BST with fewer than two elements.');
+  } else {
+    return xs[xs.length - 2];
+  }
+}
+
+/*******************************************************************************
+ * Second solution
+ ******************************************************************************/
+
+/**
+ * My second solution accumulates a list of the values in the tree using an
+ * in-order traversal. This reduces the runtime costs from O(n*log(n)) from the
+ * previous solution to O(n). The memory cost is still O(n), which InterviewCake
+ * informs me can be reduced to O(1).
+ */
+function findSecondLargest_second(node: BinaryTreeNode): number {
+  const xs: Array<number> = accumulateInorder(node);
+
+  if (xs.length < 2) {
+    throw new Error('Cannot find the second largest element in a BST with fewer than two elements.');
+  } else {
+    return xs[xs.length - 2];
+  }
+}
+
+/**
+ * Returns an array containing the values of the tree, `node`, sorted in-order
+ * (i.e. from smallest-to-largest).
+ */
+function accumulateInorder(node: BinaryTreeNode): Array<number> {
+  let result = [];
+
+  if (node.left) {
+    result = result.concat(accumulateInorder(node.left));
+  }
+  result.push(node.value)
+  if (node.right) {
+    result = result.concat(accumulateInorder(node.right));
+  }
+
+  return result;
+}
+
+/*******************************************************************************
+ * Third solution
+ ******************************************************************************/
+
+/**
+ * Returns the largest number in a BST.
+ */
+function findLargest(node: BinaryTreeNode): number {
+  let curr: BinaryTreeNode = node;
+
+  while (curr.right) {
+    curr = curr.right;
+  }
+
+  return curr.value;
+}
+
+/**
+ * Returns the second largest number in a BST
+ */
+function findSecondLargest(node: BinaryTreeNode): number {
+  let curr = node;
+  let parent = null;
+
+  while (curr.right) {
+    parent = curr;
+    curr = curr.right
+  }
+
+  if (curr.left) {
+    return findLargest(curr.left);
+  }
+  else {
+    return parent.value;
+  }
+}
+
+
+// Tests
+let desc = 'full tree';
+let treeRoot = new BinaryTreeNode(50);
+let leftNode = treeRoot.insertLeft(30);
+leftNode.insertLeft(10);
+leftNode.insertRight(40);
+let rightNode = treeRoot.insertRight(70);
+rightNode.insertLeft(60);
+rightNode.insertRight(80);
+assertEquals(findSecondLargest(treeRoot), 70, desc);
+
+desc = 'largest has a left child';
+treeRoot = new BinaryTreeNode(50);
+leftNode = treeRoot.insertLeft(30);
+leftNode.insertLeft(10);
+leftNode.insertRight(40);
+rightNode = treeRoot.insertRight(70);
+rightNode.insertLeft(60);
+assertEquals(findSecondLargest(treeRoot), 60, desc);
+
+desc = 'largest has a left subtree';
+treeRoot = new BinaryTreeNode(50);
+leftNode = treeRoot.insertLeft(30);
+leftNode.insertLeft(10);
+leftNode.insertRight(40);
+rightNode = treeRoot.insertRight(70);
+leftNode = rightNode.insertLeft(60);
+leftNode.insertRight(65);
+leftNode = leftNode.insertLeft(55);
+leftNode.insertRight(58);
+assertEquals(findSecondLargest(treeRoot), 65, desc);
+
+desc = 'second largest is root node';
+treeRoot = new BinaryTreeNode(50);
+leftNode = treeRoot.insertLeft(30);
+leftNode.insertLeft(10);
+leftNode.insertRight(40);
+rightNode = treeRoot.insertRight(70);
+assertEquals(findSecondLargest(treeRoot), 50, desc);
+
+desc = 'descending linked list';
+treeRoot = new BinaryTreeNode(50);
+leftNode = treeRoot.insertLeft(40);
+leftNode = leftNode.insertLeft(30);
+leftNode = leftNode.insertLeft(20);
+leftNode = leftNode.insertLeft(10);
+assertEquals(findSecondLargest(treeRoot), 40, desc);
+
+desc = 'ascending linked list';
+treeRoot = new BinaryTreeNode(50);
+rightNode = treeRoot.insertRight(60);
+rightNode = rightNode.insertRight(70);
+rightNode = rightNode.insertRight(80);
+assertEquals(findSecondLargest(treeRoot), 70, desc);
+
+desc = 'one node tree';
+treeRoot = new BinaryTreeNode(50);
+assertThrowsError(() => findSecondLargest(treeRoot), desc);
+
+desc = 'when tree is empty';
+treeRoot = null;
+assertThrowsError(() => findSecondLargest(treeRoot), desc);
+
+function assertEquals(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`)
+  }
+}
+
+function assertThrowsError(func, desc) {
+  try {
+    func();
+    console.log(`${desc} ... FAIL`);
+  } catch (e) {
+    console.log(`${desc} ... PASS`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/shell.nix b/users/wpcarro/scratch/deepmind/part_two/shell.nix
new file mode 100644
index 0000000000..f1b02c4d2e
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/shell.nix
@@ -0,0 +1,10 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    nodejs
+    python3
+    go
+    goimports
+  ];
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/shuffle.py b/users/wpcarro/scratch/deepmind/part_two/shuffle.py
new file mode 100644
index 0000000000..fdc5a8bd80
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/shuffle.py
@@ -0,0 +1,20 @@
+import random
+
+
+def get_random(floor, ceiling):
+    return random.randrange(floor, ceiling + 1)
+
+
+def shuffle(xs):
+    n = len(xs)
+    for i in range(n - 1):
+        j = get_random(i + 1, n - 1)
+        xs[i], xs[j] = xs[j], xs[i]
+
+
+sample_list = [1, 2, 3, 4, 5]
+print('Sample list:', sample_list)
+
+print('Shuffling sample list...')
+shuffle(sample_list)
+print(sample_list)
diff --git a/users/wpcarro/scratch/deepmind/part_two/stock-price.py b/users/wpcarro/scratch/deepmind/part_two/stock-price.py
new file mode 100644
index 0000000000..56a3c20ea0
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/stock-price.py
@@ -0,0 +1,54 @@
+import unittest
+
+
+def get_max_profit(xs):
+    if len(xs) < 2:
+        raise Exception('Can only trade with two or more ticker values.')
+    lowest_buy = xs[0]
+    max_profit = None
+    for x in xs[1:]:
+        if not max_profit:
+            max_profit = x - lowest_buy
+        else:
+            max_profit = max(max_profit, x - lowest_buy)
+        lowest_buy = min(lowest_buy, x)
+    return max_profit
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_price_goes_up_then_down(self):
+        actual = get_max_profit([1, 5, 3, 2])
+        expected = 4
+        self.assertEqual(actual, expected)
+
+    def test_price_goes_down_then_up(self):
+        actual = get_max_profit([7, 2, 8, 9])
+        expected = 7
+        self.assertEqual(actual, expected)
+
+    def test_price_goes_up_all_day(self):
+        actual = get_max_profit([1, 6, 7, 9])
+        expected = 8
+        self.assertEqual(actual, expected)
+
+    def test_price_goes_down_all_day(self):
+        actual = get_max_profit([9, 7, 4, 1])
+        expected = -2
+        self.assertEqual(actual, expected)
+
+    def test_price_stays_the_same_all_day(self):
+        actual = get_max_profit([1, 1, 1, 1])
+        expected = 0
+        self.assertEqual(actual, expected)
+
+    def test_error_with_empty_prices(self):
+        with self.assertRaises(Exception):
+            get_max_profit([])
+
+    def test_error_with_one_price(self):
+        with self.assertRaises(Exception):
+            get_max_profit([1])
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/todo.org b/users/wpcarro/scratch/deepmind/part_two/todo.org
new file mode 100644
index 0000000000..9c76da7541
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/todo.org
@@ -0,0 +1,77 @@
+* Array and string manipulation
+** DONE Merging Meeting Times
+** DONE Reverse String in Place
+** DONE Reverse Words
+** DONE Merge Sorted Arrays
+** DONE Cafe Order Checker
+* Hashing and hash tables
+** DONE Inflight Entertainment
+** DONE Permutation Palindrome
+** DONE Word Cloud Data
+** DONE Top Scores
+* Greedy Algorithms
+** DONE Apple Stocks
+** DONE Highest Product of 3
+** DONE Product of All Other Numbers
+** DONE Cafe Order Checker
+** DONE In-Place Shuffle
+* Sorting, searching, and logarithms
+** DONE Find Rotation Point
+** TODO Find Repeat, Space Edition
+** DONE Top Scores
+** DONE Merging Meeting Times
+* Trees and graphs
+** DONE Balanced Binary Tree
+** DONE Binary Search Tree Checker
+** DONE 2nd Largest Item in a Binary Search Tree
+** DONE Graph Coloring
+** DONE MeshMessage
+** DONE Find Repeat, Space Edition BEAST MODE
+* Dynamic programming and recursion
+** DONE Recursive String Permutations
+** DONE Compute nth Fibonacci Number
+** TODO Making Change
+** TODO The Cake Thief
+** DONE Balanced Binary Tree
+** DONE Binary Search Tree Checker
+** DONE 2nd Largest Item in a Binary Search Tree
+* Queues and stacks
+** TODO Largest Stack
+** TODO Implement A Queue With Two Stacks
+** TODO Parenthesis Matching
+** TODO Bracket Validator
+* Linked lists
+** DONE Delete Node
+** TODO Does This Linked List Have A Cycle?
+** TODO Reverse A Linked List
+** TODO Kth to Last Node in a Singly-Linked List
+** DONE Find Repeat, Space Edition BEAST MODE
+* System design
+** TODO URL Shortener
+** TODO MillionGazillion
+** TODO Find Duplicate Files
+* General programming
+** TODO Rectangular Love
+** TODO Temperature Tracker
+* Bit manipulation
+** TODO Binary Numbers
+** TODO The Stolen Breakfast Drone
+* Combinatorics, probability, and other math
+** TODO Which Appears Twice
+** TODO Find in Ordered Set
+** DONE In-Place Shuffle
+** TODO Simulate 5-sided die
+** TODO Simulate 7-sided die
+** TODO Two Egg Problem
+* JavaScript
+** TODO JavaScript Scope
+** TODO What's Wrong with This JavaScript?
+* Coding interview tips
+** TODO How The Coding Interview Works
+** TODO General Coding Interview Advice
+** TODO Impostor Syndrome
+** TODO Why You Hit Dead Ends
+** TODO Tips for Getting Unstuck
+** TODO The 24 Hours Before Your Interview
+** TODO Beating Behavioral Questions
+** TODO Managing Your Interview Timeline
diff --git a/users/wpcarro/scratch/deepmind/part_two/top-scores.py b/users/wpcarro/scratch/deepmind/part_two/top-scores.py
new file mode 100644
index 0000000000..0ac349c1f8
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/top-scores.py
@@ -0,0 +1,47 @@
+import unittest
+
+
+def sort_scores(xs, highest_possible_score):
+    result = []
+    buckets = [0] * highest_possible_score
+
+    for x in xs:
+        buckets[x - 1] += 1
+
+    for i in range(highest_possible_score - 1, -1, -1):
+        if buckets[i] > 0:
+            for _ in range(buckets[i]):
+                result.append(i + 1)
+
+    return result
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_no_scores(self):
+        actual = sort_scores([], 100)
+        expected = []
+        self.assertEqual(actual, expected)
+
+    def test_one_score(self):
+        actual = sort_scores([55], 100)
+        expected = [55]
+        self.assertEqual(actual, expected)
+
+    def test_two_scores(self):
+        actual = sort_scores([30, 60], 100)
+        expected = [60, 30]
+        self.assertEqual(actual, expected)
+
+    def test_many_scores(self):
+        actual = sort_scores([37, 89, 41, 65, 91, 53], 100)
+        expected = [91, 89, 65, 53, 41, 37]
+        self.assertEqual(actual, expected)
+
+    def test_repeated_scores(self):
+        actual = sort_scores([20, 10, 30, 30, 10, 20], 100)
+        expected = [30, 30, 20, 20, 10, 10]
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/deepmind/part_two/top-scores.ts b/users/wpcarro/scratch/deepmind/part_two/top-scores.ts
new file mode 100644
index 0000000000..79c10c8832
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/top-scores.ts
@@ -0,0 +1,57 @@
+function sortScores(xs: Array<number>, highest: number): Array<number> {
+  const counts: Array<number> = [];
+  const result: Array<number> = [];
+
+  // Initialize counts
+  for (let i = 0; i <= highest; i += 1) {
+    counts.push(0);
+  }
+
+  for (let i = 0; i < xs.length; i += 1) {
+    counts[xs[i]] += 1;
+  }
+
+  for (let i = highest; i >= 0; i -= 1) {
+    let count: number = counts[i];
+
+    for (let j = 0; j < count; j += 1) {
+      result.push(i);
+    }
+  }
+
+  return result;
+}
+
+// Tests
+let desc = "no scores";
+let actual = sortScores([], 100);
+let expected = [];
+assertEqual(JSON.stringify(actual), JSON.stringify(expected), desc);
+
+desc = "one score";
+actual = sortScores([55], 100);
+expected = [55];
+assertEqual(JSON.stringify(actual), JSON.stringify(expected), desc);
+
+desc = "two scores";
+actual = sortScores([30, 60], 100);
+expected = [60, 30];
+assertEqual(JSON.stringify(actual), JSON.stringify(expected), desc);
+
+desc = "many scores";
+actual = sortScores([37, 89, 41, 65, 91, 53], 100);
+expected = [91, 89, 65, 53, 41, 37];
+assertEqual(JSON.stringify(actual), JSON.stringify(expected), desc);
+
+desc = "repeated scores";
+actual = sortScores([20, 10, 30, 30, 10, 20], 100);
+expected = [30, 30, 20, 20, 10, 10];
+assertEqual(JSON.stringify(actual), JSON.stringify(expected), desc);
+
+function assertEqual(a, b, desc) {
+  if (a === b) {
+    console.log(`${desc} ... PASS`);
+  } else {
+    console.log(`${desc} ... FAIL: ${a} != ${b}`);
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/tsconfig.json b/users/wpcarro/scratch/deepmind/part_two/tsconfig.json
new file mode 100644
index 0000000000..9b6918ca37
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "compilerOptions": {
+    "downlevelIteration": true,
+    "target": "es5",
+    "lib": ["es6", "dom"]
+  }
+}
diff --git a/users/wpcarro/scratch/deepmind/part_two/word-cloud.py b/users/wpcarro/scratch/deepmind/part_two/word-cloud.py
new file mode 100644
index 0000000000..36ace8405f
--- /dev/null
+++ b/users/wpcarro/scratch/deepmind/part_two/word-cloud.py
@@ -0,0 +1,79 @@
+import unittest
+import re
+from collections import Counter
+
+
+class WordCloudData(object):
+    def __init__(self, x):
+        x = x.replace('...', ' ').replace(' - ', ' ')
+        x = ''.join(c for c in x if c not in ',.!?;:')
+        self.words_to_counts = dict(
+            Counter(x.lower() for x in re.split(r'\s+', x)))
+
+
+# Tests
+class Test(unittest.TestCase):
+    def test_simple_sentence(self):
+        input = 'I like cake'
+
+        word_cloud = WordCloudData(input)
+        actual = word_cloud.words_to_counts
+
+        expected = {'i': 1, 'like': 1, 'cake': 1}
+        self.assertEqual(actual, expected)
+
+    def test_longer_sentence(self):
+        input = 'Chocolate cake for dinner and pound cake for dessert'
+
+        word_cloud = WordCloudData(input)
+        actual = word_cloud.words_to_counts
+
+        expected = {
+            'and': 1,
+            'pound': 1,
+            'for': 2,
+            'dessert': 1,
+            'chocolate': 1,
+            'dinner': 1,
+            'cake': 2,
+        }
+        self.assertEqual(actual, expected)
+
+    def test_punctuation(self):
+        input = 'Strawberry short cake? Yum!'
+
+        word_cloud = WordCloudData(input)
+        actual = word_cloud.words_to_counts
+
+        expected = {'cake': 1, 'strawberry': 1, 'short': 1, 'yum': 1}
+        self.assertEqual(actual, expected)
+
+    def test_hyphenated_words(self):
+        input = 'Dessert - mille-feuille cake'
+
+        word_cloud = WordCloudData(input)
+        actual = word_cloud.words_to_counts
+
+        expected = {'cake': 1, 'dessert': 1, 'mille-feuille': 1}
+        self.assertEqual(actual, expected)
+
+    def test_ellipses_between_words(self):
+        input = 'Mmm...mmm...decisions...decisions'
+
+        word_cloud = WordCloudData(input)
+        actual = word_cloud.words_to_counts
+
+        expected = {'mmm': 2, 'decisions': 2}
+        self.assertEqual(actual, expected)
+
+    def test_apostrophes(self):
+        input = "Allie's Bakery: Sasha's Cakes"
+
+        word_cloud = WordCloudData(input)
+        actual = word_cloud.words_to_counts
+
+        expected = {"bakery": 1, "cakes": 1, "allie's": 1, "sasha's": 1}
+        self.assertEqual(actual, expected)
+
+
+unittest.main(verbosity=2)
diff --git a/users/wpcarro/scratch/facebook/anglocize-int.py b/users/wpcarro/scratch/facebook/anglocize-int.py
new file mode 100644
index 0000000000..a828230d08
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/anglocize-int.py
@@ -0,0 +1,71 @@
+THOUSAND = int(1e3)
+MILLION = int(1e6)
+BILLION = int(1e9)
+TRILLION = int(1e12)
+
+facts = {
+    1: "One",
+    2: "Two",
+    3: "Three",
+    4: "Four",
+    5: "Five",
+    6: "Six",
+    7: "Seven",
+    8: "Eight",
+    9: "Nine",
+    10: "Ten",
+    11: "Eleven",
+    12: "Twelve",
+    13: "Thirteen",
+    14: "Fourteen",
+    15: "Fifteen",
+    16: "Sixteen",
+    17: "Seventeen",
+    18: "Eighteen",
+    19: "Nineteen",
+    20: "Twenty",
+    30: "Thirty",
+    40: "Forty",
+    50: "Fifty",
+    60: "Sixty",
+    70: "Seventy",
+    80: "Eighty",
+    90: "Ninety",
+    100: "Hundred",
+    THOUSAND: "Thousand",
+    MILLION: "Million",
+    BILLION: "Billion",
+    TRILLION: "Trillion",
+}
+
+def anglocize(x):
+    # ones
+    if x >= 0 and x < 10:
+        pass
+
+    # tens
+    elif x < 100:
+        pass
+
+    # hundreds
+    elif x < THOUSAND:
+        pass
+
+    # thousands
+    elif x < MILLION:
+        pass
+
+    # millions
+    elif x < BILLION:
+        pass
+
+    # billion
+    elif x < TRILLION:
+        pass
+
+    # trillion
+    else:
+        pass
+
+x = 1234
+assert anglocize(x) == "One Thousand, Two Hundred Thirty Four"
diff --git a/users/wpcarro/scratch/facebook/balanced-binary-tree.py b/users/wpcarro/scratch/facebook/balanced-binary-tree.py
new file mode 100644
index 0000000000..afa9706f97
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/balanced-binary-tree.py
@@ -0,0 +1,70 @@
+from collections import deque
+
+class Node(object):
+    # __init__ :: T(A)
+    def __init__(self, value=None, left=None, right=None):
+        self.value = value
+        self.left = left
+        self.right = right
+
+    # insert_left :: T(A) -> A -> T(A)
+    def insert_left(self, value):
+        self.left = Node(value)
+        return self.left
+
+    # insert_right :: T(A) -> A -> T(A)
+    def insert_right(self, value):
+        self.right = Node(value)
+        return self.right
+
+    # is_superbalanced :: T(A) -> Bool
+    def is_superbalanced(self):
+        xs = deque()
+        min_depth, max_depth = float('inf'), float('-inf')
+        xs.append((self, 0))
+        while xs:
+            x, d = xs.popleft()
+            # Only redefine the depths at leaf nodes
+            if not x.left and not x.right:
+                min_depth, max_depth = min(min_depth, d), max(max_depth, d)
+            if x.left:
+                xs.append((x.left, d + 1))
+            if x.right:
+                xs.append((x.right, d + 1))
+        return max_depth - min_depth <= 1
+
+    # __repr__ :: T(A) -> String
+    def __repr__(self):
+        result = ''
+        xs = deque()
+        xs.append((self, 0))
+        while xs:
+            node, indent = xs.popleft()
+            result += '{i}{x}\n'.format(i=' ' * indent, x=node.value)
+            if node.left:
+                xs.append((node.left, indent + 2))
+            if node.right:
+                xs.append((node.right, indent + 2))
+        return result
+
+# from_array :: List(A) -> T(A)
+def from_array(values):
+    xs = deque()
+    root = Node()
+    xs.append(root)
+    for value in values:
+        node = xs.popleft()
+        node.value = value
+        node.left = Node()
+        xs.append(node.left)
+        node.right = Node()
+        xs.append(node.right)
+    return root
+
+x = from_array([1, 1, 1, 1, 1, 1, 1])
+print(x)
+print(x.is_superbalanced())
+
+x = Node(1, Node(2), Node(3))
+print(x)
+print(x.is_superbalanced())
diff --git a/users/wpcarro/scratch/facebook/breakfast-generator.py b/users/wpcarro/scratch/facebook/breakfast-generator.py
new file mode 100644
index 0000000000..df9b5015ad
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/breakfast-generator.py
@@ -0,0 +1,112 @@
+# After being inspired by...
+# craftinginterpreters.com/representing-code.html
+# ...I'm implementing the breakfast generator that the author describes
+# therein.
+
+import random
+import string
+
+# Breakfast
+
+def breakfast():
+    fn = random.choice([
+        lambda: " ".join([protein(), "with", breakfast(), "on the side"]),
+        lambda: protein(),
+        lambda: bread(),
+    ])
+    return fn()
+
+def protein():
+    fn = random.choice([
+        lambda: " ".join([qualifier(), "crispy", "bacon"]),
+        lambda: "sausage",
+        lambda: " ".join([cooking_method(), "sausage"]),
+    ])
+    return fn()
+
+def qualifier():
+    fn = random.choice([
+        lambda: "really",
+        lambda: "super",
+        lambda: " ".join(["really", qualifier()]),
+    ])
+    return fn()
+
+def cooking_method():
+    return random.choice([
+        "scrambled",
+        "poached",
+        "fried",
+    ])
+
+def bread():
+    return random.choice([
+        "toast",
+        "biscuits",
+        "English muffin",
+    ])
+
+print(breakfast())
+
+# Expression Language
+
+# Because Python is a strictly evaluated language any functions that are
+# mutually recursive won't terminate and will overflow our stack. Therefore, any
+# non-terminals expressed in an alternative are wrapped in lambdas as thunks.
+
+def expression():
+    fn = random.choice([
+        lambda: literal(),
+        lambda: binary(),
+    ])
+    return fn()
+
+def literal():
+    return str(random.randint(0, 100))
+
+def binary():
+    return " ".join([expression(), operator(), expression()])
+
+def operator():
+    return random.choice(["+", "*"])
+
+print(expression())
+
+# Lox
+
+def lox_expression():
+    fn = random.choice([
+        lambda: lox_literal(),
+        lambda: lox_unary(),
+        lambda: lox_binary(),
+        lambda: lox_grouping(),
+    ])
+    return fn()
+
+def lox_literal():
+    fn = random.choice([
+        lambda: str(random.randint(0, 100)),
+        lambda: lox_string(),
+        lambda: random.choice(["true", "false"]),
+        lambda: "nil",
+    ])
+    return fn()
+
+def lox_string():
+    return "\"{}\"".format(
+        "".join(random.choice(string.ascii_lowercase)
+                for _ in range(random.randint(0, 25))))
+
+def lox_grouping():
+    return "(" + lox_expression() + ")"
+
+def lox_unary():
+    return random.choice(["-", "!"]) + lox_expression()
+
+def lox_binary():
+    return lox_expression() + lox_operator() + lox_expression()
+
+def lox_operator():
+    return random.choice(["==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"])
+
+print(lox_expression())
diff --git a/users/wpcarro/scratch/facebook/bst-checker.py b/users/wpcarro/scratch/facebook/bst-checker.py
new file mode 100644
index 0000000000..7ef63a9531
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/bst-checker.py
@@ -0,0 +1,49 @@
+from collections import deque
+
+class Node(object):
+    def __init__(self, value, left=None, right=None):
+        self.value = value
+        self.left = left
+        self.right = right
+
+    def is_bst(self):
+        s = []
+        s.append((float('-inf'), self, float('inf')))
+        while s:
+            lo, node, hi = s.pop()
+            if lo <= node.value <= hi:
+                node.left and s.append((lo, node.left, node.value))
+                node.right and s.append((node.value, node.right, hi))
+            else:
+                return False
+        return True
+
+
+x = Node(
+    50,
+    Node(
+        17,
+        Node(
+            12,
+            Node(9),
+            Node(14),
+        ),
+        Node(
+            23,
+            Node(19),
+        ),
+    ),
+    Node(
+        72,
+        Node(
+            54,
+            None,
+            Node(67)
+        ),
+        Node(76),
+    ),
+)
+
+
+assert x.is_bst()
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/cafe-order-checker.py b/users/wpcarro/scratch/facebook/cafe-order-checker.py
new file mode 100644
index 0000000000..9d88a68069
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/cafe-order-checker.py
@@ -0,0 +1,19 @@
+def orders_are_sorted(take_out, dine_in, audit):
+    if len(take_out) + len(dine_in) != len(audit):
+        return False
+
+    i, j = 0, 0
+    for x in audit:
+        if i < len(take_out) and take_out[i] == x:
+            i += 1
+        elif j < len(dine_in) and dine_in[j] == x:
+            j += 1
+        else:
+            return False
+    return True
+
+
+assert orders_are_sorted([1,3,5], [2,4,6], [1,2,4,3,6,5])
+assert not orders_are_sorted([1,3,5], [2,4,6], [1,2,4,5,6,3])
+assert orders_are_sorted([], [2,4,6], [2,4,6])
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/cake_thief.py b/users/wpcarro/scratch/facebook/cake_thief.py
new file mode 100644
index 0000000000..90a2add066
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/cake_thief.py
@@ -0,0 +1,61 @@
+from math import floor
+
+def print_table(table):
+    print('\n-- TABLE --')
+    for row in range(len(table)):
+        x = ''
+        for col in range(len(table[row])):
+            x += ' ' + str(table[row][col])
+        print(x)
+
+def leftover(capacity, kg):
+    n = floor(capacity / kg)
+    return n, capacity - (n * kg)
+
+def init_table(num_rows, num_cols):
+    table = []
+    for _ in range(num_rows):
+        row = []
+        for _ in range(num_cols):
+            row.append(0)
+        table.append(row)
+    return table
+
+def get(table, row, col):
+    if row < 0 or col < 0:
+        return 0
+    return table[row][col]
+
+def max_haul(items, capacity):
+    table = init_table(len(items), capacity)
+
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            curr_capacity = col + 1
+            kg, val = items[row]
+            # A
+            a = get(table, row - 1, col)
+            # B
+            n, lo = leftover(curr_capacity, kg)
+            b = (val * n) + get(table, row - 1, lo - 1)
+            # commit
+            if kg > curr_capacity:
+                table[row][col] = a
+            else:
+                print(n, lo)
+                table[row][col] = max([a, b])
+            print_table(table)
+    return table[-1][-1]
+
+# There are multiple variants of this problem:
+#   1. We're allowed to take multiple of each item.
+#   2. We can only take one of each item.
+#   3. We can only take a fixed amount of each item.
+
+items = [(7,160), (3,90), (2,15)]
+capacity = 20
+result = max_haul(items, capacity)
+expected = None
+print("Result: {} == Expected: {}".format(result, expected))
+assert result == expected
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/camping-knapsack.py b/users/wpcarro/scratch/facebook/camping-knapsack.py
new file mode 100644
index 0000000000..add59ed409
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/camping-knapsack.py
@@ -0,0 +1,46 @@
+from utils import get, init_table, print_table
+
+def max_haul(capacity, items, names):
+    table = init_table(rows=len(items), cols=capacity, default=0)
+    items_table = init_table(rows=len(items), cols=capacity, default=[])
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            kg, value = items[row]
+            curr_capacity = col + 1
+
+            if kg > curr_capacity:
+                a = 0
+            else:
+                a = value + get(table, row - 1, curr_capacity - kg - 1)
+            b = get(table, row - 1, col)
+
+            if a > b:
+                rest = get(items_table, row - 1, curr_capacity - kg - 1)
+                knapsack = [names.get(items[row])]
+                if rest:
+                    knapsack += rest
+            else:
+                knapsack = get(items_table, row - 1, col)
+
+            table[row][col] = max([a, b])
+            items_table[row][col] = knapsack
+        print_table(table)
+    return items_table[-1][-1]
+
+water = (3, 10)
+book = (1, 3)
+food = (2, 9)
+jacket = (2, 5)
+camera = (1, 6)
+items = [water, book, food, jacket, camera]
+result = max_haul(6, items, {
+    water: 'water',
+    book: 'book',
+    food: 'food',
+    jacket: 'jacket',
+    camera: 'camera',
+})
+expected = ['camera', 'food', 'water']
+print(result, expected)
+assert result == expected
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/coin.py b/users/wpcarro/scratch/facebook/coin.py
new file mode 100644
index 0000000000..354e2dfb58
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/coin.py
@@ -0,0 +1,50 @@
+def init_table(rows=0, cols=0, default=None):
+    table = []
+    for _ in range(rows):
+        row = []
+        for _ in range(cols):
+            row.append(default)
+        table.append(row)
+    return table
+
+def print_table(table):
+    result = ''
+    for row in range(len(table)):
+        x = ''
+        for col in range(len(table[row])):
+            x += str(table[row][col]) + ' '
+        result += x + '\n'
+    print(result)
+
+def get(table, row, col):
+    if row < 0 or col < 0:
+        return 0
+    else:
+        return table[row][col]
+
+def make_change(coins, amt):
+    table = init_table(rows=len(coins), cols=amt, default=0)
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            coin = coins[row]
+            curr_amt = col + 1
+            pull_down = get(table, row - 1, col)
+
+            if curr_amt < coin:
+                table[row][col] = pull_down
+            elif curr_amt == coin:
+                table[row][col] = pull_down + 1
+            else:
+                leftover = get(table, row, curr_amt - coin - 1)
+                table[row][col] = pull_down + leftover
+
+    print_table(table)
+    return table[-1][-1]
+
+#   1 2 3 4
+# 1 1 1 1 1
+# 2 1 1 2 2
+# 3 1 1 3 4
+
+result = make_change([3,2,1], 4)
+print(result)
diff --git a/users/wpcarro/scratch/facebook/count-islands.py b/users/wpcarro/scratch/facebook/count-islands.py
new file mode 100644
index 0000000000..b876319b2f
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/count-islands.py
@@ -0,0 +1,53 @@
+from collections import deque
+
+def maybe_queue(row, col, game, q, seen):
+    """
+    Add coordinate, (`row`, `col`), to the queue, `q`, as long as it exists in
+    the map, `game`, and it is not already present in `seen`.
+    """
+    if row >= 0 and row < len(game) and col >= 0 and col < len(game[0]):
+        if game[row][col] == 'L' and (row, col) not in seen:
+            q.append((row, col))
+            seen.add((row, col))
+
+def visit_island(row, col, game, seen):
+    """
+    Starting at the coordinate, (`row`, `col`), in the map, `game`, visit all
+    surrounding tiles marked as land by adding them to the `seen` set.
+    """
+    q = deque()
+    q.append((row, col))
+    while q:
+        row, col = q.popleft()
+        maybe_queue(row - 1, col, game, q, seen) # UP
+        maybe_queue(row + 1, col, game, q, seen) # DOWN
+        maybe_queue(row, col - 1, game, q, seen) # LEFT
+        maybe_queue(row, col + 1, game, q, seen) # RIGHT
+
+def count_islands(game):
+    """
+    Return the number of contiguous land tiles in the map, `game`.
+    """
+    result = 0
+    seen = set()
+    for row in range(len(game)):
+        for col in range(len(game[row])):
+            if game[row][col] == 'L' and (row, col) not in seen:
+                visit_island(row, col, game, seen)
+                result += 1
+    return result
+
+################################################################################
+# Tests
+################################################################################
+
+game = [
+    "LWLWWW",
+    "LLLWWW",
+    "WWWLLW",
+]
+
+result = count_islands(game)
+print(result)
+assert result == 2
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/delete-node.py b/users/wpcarro/scratch/facebook/delete-node.py
new file mode 100644
index 0000000000..4034449ef0
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/delete-node.py
@@ -0,0 +1,19 @@
+from linked_list import Node, from_list
+
+def delete(node):
+    if not node.next:
+        node.value = None
+    else:
+        node.value = node.next.value
+        node.next = node.next.next
+
+one = Node(1)
+two = Node(2)
+three = Node(3)
+
+one.next = two
+two.next = three
+
+print(one)
+delete(two)
+print(one)
diff --git a/users/wpcarro/scratch/facebook/dijkstras.py b/users/wpcarro/scratch/facebook/dijkstras.py
new file mode 100644
index 0000000000..7031701994
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/dijkstras.py
@@ -0,0 +1,38 @@
+from heapq import heappush, heappop
+import random
+
+# Dijkstra's algorithm will traverse a directed graph with weighted edges. If
+# the edges aren't weighted, we can pretend that each edges weighs 1. The
+# algorithm will find the shortest path between points A and B.
+
+def dijkstra(a, b, graph):
+    h = []
+    seen = set()
+    heappush(h, (0, a, [a], []))
+    while h:
+        km, x, path, steps = heappop(h)
+
+        if x == b:
+            for a, b, d in steps:
+                print("{} -> {} => {}".format(a, b, d))
+            return path, km
+
+        seen.add(x)
+        for c, dist in graph[x]:
+            if c not in seen:
+                heappush(h, (km + dist, c, path + [c], steps + [(x, c, dist)]))
+    return [], float('inf')
+
+graph = {
+    1: [(3, 9), (2, 7), (6, 14)],
+    2: [(1, 7), (3, 10), (4, 15)],
+    3: [(1, 9), (6, 2), (4, 11), (2, 10)],
+    4: [(5, 6), (2, 15), (3, 11)],
+    5: [(4, 6), (6, 9)],
+    6: [(5, 9), (3, 2), (1, 14)],
+}
+
+beg = random.choice(list(graph.keys()))
+end = random.choice(list(graph.keys()))
+print("Searching for the shortest path from {} -> {}".format(beg, end))
+print(dijkstra(beg, end, graph))
diff --git a/users/wpcarro/scratch/facebook/edit-distance.py b/users/wpcarro/scratch/facebook/edit-distance.py
new file mode 100644
index 0000000000..a5b744f30f
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/edit-distance.py
@@ -0,0 +1,47 @@
+def print_grid(grid):
+    result = []
+    for row in grid:
+        result.append(" ".join(str(c) for c in row))
+    return print("\n".join(result))
+
+def edit_distance(a, b):
+    """
+    Compute the "edit distance" to transform string `a` into string `b`.
+    """
+    grid = []
+    for row in range(len(a) + 1):
+        r = []
+        for col in range(len(b) + 1):
+            r.append(0)
+        grid.append(r)
+
+    # left-to-right
+    # populate grid[0][i]
+    for col in range(len(grid[0])):
+        grid[0][col] = col
+
+    # top-to-bottom
+    # populate grid[i][0]
+    for row in range(len(grid)):
+        grid[row][0] = row
+
+    for row in range(1, len(grid)):
+        for col in range(1, len(grid[row])):
+            # last characters are the same
+            if a[0:row][-1] == b[0:col][-1]:
+                grid[row][col] = grid[row - 1][col - 1]
+            else:
+                # substitution
+                s = 1 + grid[row - 1][col - 1]
+                # deletion
+                d = 1 + grid[row - 1][col]
+                # insertion
+                i = 1 + grid[row][col - 1]
+                grid[row][col] = min(s, d, i)
+    print_grid(grid)
+    return grid[-1][-1]
+
+result = edit_distance("pizza", "pisa")
+print(result)
+assert result == 2
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/evaluator.hs b/users/wpcarro/scratch/facebook/evaluator.hs
new file mode 100644
index 0000000000..1ba46a7548
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/evaluator.hs
@@ -0,0 +1,39 @@
+module Evaluator where
+
+data Token
+  = TokenInt Integer
+  | TokenAdd
+  | TokenMultiply
+  deriving (Eq, Show)
+
+newtype AST = AST [Token]
+  deriving (Eq, Show)
+
+tokens :: [Token]
+tokens =
+  [ TokenInt 13
+  , TokenAdd
+  , TokenInt 2
+  , TokenMultiply
+  , TokenInt 4
+  , TokenAdd
+  , TokenInt 7
+  , TokenAdd
+  , TokenInt 3
+  , TokenMultiply
+  , TokenInt 8
+  ]
+
+-- expression     -> addition ;
+-- addition       -> multiplication ( "+" multiplication )* ;
+-- multiplication -> terminal ( "*" terminal )* ;
+-- terminal       -> NUMBER ;
+
+parseExpression :: [Token] -> ([Token], AST)
+parseExpression tokens = do
+  lhs, rest = parseMultiplication tokens
+
+parseMulitplication :: [Token] -> ([Token], AST)
+
+main :: IO ()
+main = print $ parse tokens
diff --git a/users/wpcarro/scratch/facebook/evaluator.py b/users/wpcarro/scratch/facebook/evaluator.py
new file mode 100644
index 0000000000..14deb66a8f
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/evaluator.py
@@ -0,0 +1,234 @@
+# After stumbling through my first technical screen, I'm going to drill
+# algorithms for implementing evaluators for a toy expression language:
+# e.g. 2 + 13 * 3 + 5 * 2
+#
+# As of now, I'm aware of a few algorithms for solving this:
+#   - DONE: Convert infix expression to Polish notation and evaluate the Polish
+#     notation.
+#   - DONE: Evaluate the tokens using two stacks and avoid converting it.
+#   - DONE: Create a tree of depth two to encode the operator precedence and
+#     evaluate that AST.
+#   - TODO: Convert the infix expression to a prefix expression
+#   - TODO: Write a recursive descent parser and evaluate the AST.
+
+operators = {
+    '*': 1,
+    '+': 0,
+}
+
+def tokenize(xs):
+    result = []
+    i = 0
+    while i < len(xs):
+        current = xs[i]
+        if current == ' ':
+            i += 1
+            continue
+        elif current in operators.keys():
+            result.append(current)
+            i += 1
+        else:
+            i += 1
+            while i < len(xs) and xs[i] in {str(n) for n in range(10)}:
+                current += xs[i]
+                i += 1
+            result.append(int(current))
+    return result
+
+# Convert infix to postfix; evaluate postfix
+# I believe this is known as the Shunting-Yards algorithm
+def postfix(tokens):
+    result = []
+    s = []
+    for token in tokens:
+        if type(token) == int:
+            result.append(token)
+        else:
+            while s and operators[token] < operators[s[-1]]:
+                result.append(s.pop())
+            s.append(token)
+    while s:
+        result.append(s.pop())
+    return result
+
+def do_evaluate_with_polish_notation(tokens):
+    s = []
+    for token in tokens:
+        if token == '*':
+            s.append(s.pop() * s.pop())
+        elif token == '+':
+            s.append(s.pop() + s.pop())
+        else:
+            s.append(token)
+    return s[-1]
+
+def evaluate_with_polish_notation(expr):
+    tokens = tokenize(expr)
+    print("Tokens:  {}".format(tokens))
+    pn = postfix(tokens)
+    print("Postfix: {}".format(pn))
+    result = do_evaluate_with_polish_notation(pn)
+    print("Result:  {}".format(result))
+    return result
+
+# Evaluate Tokens
+
+def apply_operator(op, a, b):
+    if op == '*':
+        return a * b
+    elif op == '+':
+        return a + b
+
+def do_evaluate_tokens(tokens):
+    vals = []
+    ops = []
+    for token in tokens:
+        if type(token) == int:
+            vals.append(token)
+        elif token == '*':
+            ops.append(token)
+        elif token == '+':
+            while ops and operators[token] < operators[ops[-1]]:
+                vals.append(apply_operator(ops.pop(), vals.pop(), vals.pop()))
+            ops.append(token)
+        else:
+            raise Exception("Unexpected token: {}".format(token))
+    while ops:
+        vals.append(apply_operator(ops.pop(), vals.pop(), vals.pop()))
+    return vals[-1]
+
+def evaluate_tokens(expr):
+    tokens = tokenize(expr)
+    print("Tokens:  {}".format(tokens))
+    result = do_evaluate_tokens(tokens)
+    print("Result:  {}".format(result))
+    return result
+
+# Ad Hoc Tree
+
+def parse(tokens):
+    result = []
+    series = []
+    for token in tokens:
+        if type(token) == int:
+            series.append(token)
+        elif token == '*':
+            continue
+        elif token == '+':
+            result.append(series)
+            series = []
+        else:
+            raise Exception("Unexpected token: {}".format(token))
+    result.append(series)
+    return result
+
+def product(xs):
+    result = 1
+    for x in xs:
+        result *= x
+    return result
+
+def do_evaluate_ad_hoc_tree(ast):
+    return sum([product(xs) for xs in ast])
+
+def evaluate_ad_hoc_tree(expr):
+    tokens = tokenize(expr)
+    print("Tokens:  {}".format(tokens))
+    ast = parse(tokens)
+    print("AST:     {}".format(ast))
+    result = do_evaluate_ad_hoc_tree(ast)
+    print("Result:  {}".format(result))
+    return result
+
+# Recursive Descent Parser
+
+# expression     -> addition ;
+# addition       -> multiplication ( "+" multiplication )* ;
+# multiplication -> terminal ( "*" terminal )* ;
+# terminal       -> NUMBER ;
+
+class Parser(object):
+    def __init__(self, tokens):
+        self.tokens = tokens
+        self.i = 0
+
+    # mutations
+    def advance(self):
+        self.i += 1
+
+    def consume(self):
+        result = self.curr()
+        self.advance()
+        return result
+
+    # predicates
+    def match(self, x):
+        if self.curr() == x:
+            self.advance()
+            return True
+        return False
+
+    def tokens_available(self):
+        return self.i < len(self.tokens)
+
+    # getters
+    def prev(self):
+        return self.tokens[self.i - 1]
+
+    def curr(self):
+        return self.tokens[self.i] if self.tokens_available() else None
+
+    def next(self):
+        return self.tokens[self.i + 1]
+
+def parse_expression(tokens):
+    parser = Parser(tokens)
+    return parse_addition(parser)
+
+def parse_addition(parser):
+    result = parse_multiplication(parser)
+    while parser.match("+"):
+        op = parser.prev()
+        rhs = parse_multiplication(parser)
+        result = ["+", result, rhs]
+    return result
+
+def parse_multiplication(parser):
+    result = parse_terminal(parser)
+    while parser.match("*"):
+        op = parser.prev()
+        rhs = parse_terminal(parser)
+        result = ["*", result, rhs]
+    return result
+
+def parse_terminal(parser):
+    # If we reach here, the current token *must* be a number.
+    return parser.consume()
+
+def evaluate_ast(ast):
+    if type(ast) == int:
+        return ast
+    else:
+        op, lhs, rhs = ast[0], ast[1], ast[2]
+        return apply_operator(op, evaluate_ast(lhs), evaluate_ast(rhs))
+
+def evaluate_recursive_descent(expr):
+    tokens = tokenize(expr)
+    print("Tokens:  {}".format(tokens))
+    ast = parse_expression(tokens)
+    print("AST:     {}".format(ast))
+    result = evaluate_ast(ast)
+    return result
+
+methods = {
+    'Polish Notation': evaluate_with_polish_notation,
+    'Evaluate Tokens': evaluate_tokens,
+    'Ad Hoc Tree': evaluate_ad_hoc_tree,
+    'Recursive Descent': evaluate_recursive_descent,
+}
+
+for name, fn in methods.items():
+    expr = "13 + 2 * 4 + 7 + 3 * 8"
+    print("Evaluating \"{}\" using the \"{}\" method...".format(expr, name))
+    assert fn(expr) == eval(expr)
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/find-duplicate-beast-mode.py b/users/wpcarro/scratch/facebook/find-duplicate-beast-mode.py
new file mode 100644
index 0000000000..e246415efd
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/find-duplicate-beast-mode.py
@@ -0,0 +1,57 @@
+def advance(position, xs):
+    """
+    Return the next element in `xs` pointed to by the current `position`.
+    """
+    return xs[position - 1]
+
+def find_duplicate(xs):
+    """
+    Find the duplicate integer in the list, `xs`.
+    """
+    beg = xs[-1]
+    a = beg
+    b = advance(a, xs)
+    # Find the first element of the cycle
+    cycle_beg = None
+    while a != b:
+        cycle_beg = a
+        a = advance(a, xs)
+        b = advance(b, xs)
+        b = advance(b, xs)
+    # The duplicate element is the element before the `cycle_beg`
+    a = beg
+    result = None
+    while a != cycle_beg:
+        result = a
+        a = advance(a, xs)
+    return result
+
+def find_duplicate(xs):
+    """
+    This is the solution that InterviewCake.com suggests.
+    """
+    # find length of the cycle
+    beg = xs[-1]
+    a = beg
+    for _ in range(len(xs)):
+        a = advance(a, xs)
+    element = a
+    a = advance(a, xs)
+    n = 1
+    while a != element:
+        a = advance(a, xs)
+        n += 1
+    # find the first element in the cycle
+    a, b = beg, beg
+    for _ in range(n):
+        b = advance(b, xs)
+    while a != b:
+        a = advance(a, xs)
+        b = advance(b, xs)
+    return a
+
+xs = [2, 3, 1, 3]
+result = find_duplicate(xs)
+print(result)
+assert result == 3
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/find-duplicate-optimize-for-space.py b/users/wpcarro/scratch/facebook/find-duplicate-optimize-for-space.py
new file mode 100644
index 0000000000..7c491aef60
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/find-duplicate-optimize-for-space.py
@@ -0,0 +1,22 @@
+import random
+
+def find_duplicate(xs):
+    print(xs)
+    # entry point in our cycle is the duplicate
+    i = xs[0]
+    j = xs[xs[0]]
+    while i != j:
+        print(i, xs[i], j, xs[j])
+        i = xs[i]
+        j = xs[xs[j]]
+    # detect cycle
+    j = 0
+    while i != j:
+        i = xs[i]
+        j = xs[j]
+    return xs[i]
+
+n = random.randint(5, 10)
+xs = [random.randint(0, n - 1) for _ in range(n)]
+result = find_duplicate(xs)
+print(xs, result)
diff --git a/users/wpcarro/scratch/facebook/find-rotation-point.py b/users/wpcarro/scratch/facebook/find-rotation-point.py
new file mode 100644
index 0000000000..3636be4d93
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/find-rotation-point.py
@@ -0,0 +1,47 @@
+from math import floor
+
+def find_rotation(xs):
+    if xs[0] < xs[-1]:
+        return xs[0]
+    beg, end = 0, len(xs) - 1
+    found = False
+    count = 10
+    while not found and count >= 0:
+        i = beg + floor((end - beg) / 2)
+        if xs[beg] < xs[i]:
+            beg = i
+            i = beg + floor((end - beg) / 2)
+        elif xs[beg] > xs[i]:
+            end = i
+        found = xs[i - 1] > xs[i]
+        count -= 1
+    return xs[i]
+
+
+xs = [(['ptolemaic',
+        'retrograde',
+        'supplant',
+        'undulate',
+        'xenoepist',
+        'zebra',
+        'asymptote',
+        'babka',
+        'banoffee',
+        'engender',
+        'karpatka',
+        'othellolagkage',
+        ], "asymptote"),
+      (['asymptote',
+        'babka',
+        'banoffee',
+        'engender',
+        'karpatka',
+        'othellolagkage',
+        ], "asymptote"),
+      ]
+
+for x, expected in xs:
+    result = find_rotation(x)
+    print(x, result)
+    assert result == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/find-unique-int-among-duplicates.py b/users/wpcarro/scratch/facebook/find-unique-int-among-duplicates.py
new file mode 100644
index 0000000000..56032aa05c
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/find-unique-int-among-duplicates.py
@@ -0,0 +1,17 @@
+import random
+
+def find_duplicate(xs):
+    mini, maxi, acc = xs[0], xs[0], xs[0]
+    for i in range(1, len(xs)):
+        mini = min(mini, xs[i])
+        maxi = max(maxi, xs[i])
+        acc = acc ^ xs[i]
+    mask = mini
+    for i in range(mini + 1, maxi + 1):
+        mask = mask ^ i
+    return mask ^ acc
+
+xs = [5, 3, 4, 1, 5, 2]
+print(xs)
+result = find_duplicate(xs)
+print(result)
diff --git a/users/wpcarro/scratch/facebook/graph-coloring.py b/users/wpcarro/scratch/facebook/graph-coloring.py
new file mode 100644
index 0000000000..e5b6d9c893
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/graph-coloring.py
@@ -0,0 +1,60 @@
+from collections import deque
+
+class Palette(object):
+    def __init__(self, n):
+        self.i = 0
+        self.colors = list(range(n))
+
+    def get(self):
+        return self.colors[self.i]
+
+    def advance(self):
+        self.i += 1 % len(self.colors)
+
+class GraphNode(object):
+    def __init__(self, label):
+        self.label = label
+        self.neighbors = set()
+        self.color = None
+
+    def __repr__(self):
+        result = []
+        xs = deque()
+        xs.append(self)
+        seen = set()
+        while xs:
+            node = xs.popleft()
+            result.append('{} ({})'.format(node.label, str(node.color)))
+            for c in node.neighbors:
+                if c.label not in seen:
+                    xs.append(c)
+                    seen.add(node.label)
+        return ', '.join(result)
+
+def color_graph(graph, d):
+    seen = set()
+    start = graph
+    xs = deque()
+    palette = Palette(d + 1)
+    xs.append((start, palette.get()))
+    while xs:
+        x, color = xs.popleft()
+        x.color = color
+        for c in x.neighbors:
+            if c.label not in seen:
+                palette.advance()
+                xs.append((c, palette.get()))
+                seen.add(x.label)
+
+a = GraphNode('a')
+b = GraphNode('b')
+c = GraphNode('c')
+
+a.neighbors.add(b)
+b.neighbors.add(a)
+b.neighbors.add(c)
+c.neighbors.add(b)
+
+print(a)
+color_graph(a, 3)
+print(a)
diff --git a/users/wpcarro/scratch/facebook/hard/binary-adder.py b/users/wpcarro/scratch/facebook/hard/binary-adder.py
new file mode 100644
index 0000000000..f79a9f22b3
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/hard/binary-adder.py
@@ -0,0 +1,22 @@
+import random
+
+def add(a, b):
+    """
+    Return the sum of `a` and `b`.
+    """
+    if b == 0:
+        return a
+    sum = a ^ b
+    carry = (a & b) << 1
+    return add(sum, carry)
+
+################################################################################
+# Tests
+################################################################################
+
+for _ in range(10):
+    x, y = random.randint(0, 100), random.randint(0, 100)
+    print("{} + {} = {} == {}".format(x, y, x + y, add(x, y)))
+    assert add(x, y) == x + y
+    print("Pass!")
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/hard/fisher-yates.py b/users/wpcarro/scratch/facebook/hard/fisher-yates.py
new file mode 100644
index 0000000000..200d1613dd
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/hard/fisher-yates.py
@@ -0,0 +1,7 @@
+import random
+
+def shuffle(xs):
+    n = len(xs)
+    for i in range(n):
+        j = random.randint(i, n - 1)
+        xs[i], xs[j] = xs[j], xs[i]
diff --git a/users/wpcarro/scratch/facebook/hard/random-choice.py b/users/wpcarro/scratch/facebook/hard/random-choice.py
new file mode 100644
index 0000000000..a5c6e4e6ee
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/hard/random-choice.py
@@ -0,0 +1,50 @@
+import random
+
+# This class of problems is known as "resevoir sampling".
+def choose_a(m, xs):
+    """
+    Randomly choose `m` elements from `xs`.
+    This algorithm runs in linear time with respect to the size of `xs`.
+    """
+    result = [None] * m
+    for i in range(len(xs)):
+        j = random.randint(0, i)
+        if j < m:
+            result[j] = xs[i]
+    return result
+
+def choose_b(m, xs):
+    """
+    This algorithm, which copies `xs`, which runs in linear time, and then
+    shuffles the copies, which also runs in linear time, achieves the same
+    result as `choose_a` and both run in linear time.
+
+    `choose_a` is still preferable since it has a coefficient of one, while this
+    version has a coefficient of two because it copies + shuffles.
+    """
+    ys = xs[:]
+    random.shuffle(ys)
+    return ys[:m]
+
+def choose_c(m, xs):
+    """
+    This is one, possibly inefficient, way to randomly sample `m` elements from
+    `xs`.
+    """
+    choices = set()
+    while len(choices) < m:
+        choices.add(random.randint(0, len(xs) - 1))
+    return [xs[i] for i in choices]
+
+# ROYGBIV
+xs = [
+    'red',
+    'orange',
+    'yellow',
+    'green',
+    'blue',
+    'indigo',
+    'violet',
+]
+print(choose_b(3, xs))
+print(choose_c(3, xs))
diff --git a/users/wpcarro/scratch/facebook/hard/suffix-tree.py b/users/wpcarro/scratch/facebook/hard/suffix-tree.py
new file mode 100644
index 0000000000..782678fb82
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/hard/suffix-tree.py
@@ -0,0 +1,93 @@
+import random
+from collections import deque
+
+def exists(pattern, tree):
+    """
+    Return true if `pattern` exists in `tree`.
+    """
+    if len(pattern) == 0:
+        return True
+    if len(pattern) == 1:
+        for branch in tree:
+            if branch[0] == pattern[0]:
+                return True
+        return False
+    for branch in tree:
+        if branch[0] == pattern[0]:
+            return exists(pattern[1:], branch[1])
+    return False
+
+# Branch :: (Char, [Branch])
+# SuffixTree :: [Branch]
+
+def suffix_tree(xs):
+    """
+    Create a suffix tree from the input string, `xs`.
+    """
+    root = []
+    for i in range(len(xs)):
+        curr = xs[i:]
+        parent = root
+        for c1 in curr:
+            grafted = False
+            for c2, children in parent:
+                if c1 == c2:
+                    grafted = True
+                    parent = children
+            if grafted:
+                continue
+            else:
+                children = []
+                child = (c1, children)
+                parent.append(child)
+                parent = children
+    return root
+
+def suffix_tree(x):
+    """
+    Creates a suffix from the input string, `x`. This implementation uses a
+    stack.
+    """
+    result = [None, []]
+    q = deque()
+    for i in range(len(x)):
+        q.append((result, x[i:]))
+    while q:
+        parent, x = q.popleft()
+        s = []
+        s.append((parent, x))
+        while s:
+            parent, x = s.pop()
+            if not x:
+                continue
+            c, rest = x[0], x[1:]
+            grafted = False
+            for child in parent[1]:
+                if c == child[0]:
+                    s.append((child, rest))
+                    grafted = True
+            if not grafted:
+                child = [c, []]
+                parent[1].append(child)
+                s.append((child, rest))
+    return result[1]
+
+################################################################################
+# Tests
+################################################################################
+
+x = random.choice(["burrito", "pizza", "guacamole"])
+tree = suffix_tree(x)
+for branch in tree:
+    print(branch)
+
+for _ in range(3):
+    n = len(x)
+    i, j = random.randint(0, n), random.randint(0, n)
+    pattern = x[min(i, j):max(i, j)]
+    print("Checking \"{}\" for \"{}\" ...".format(x, pattern))
+    print("Result: {}".format(exists(pattern, tree)))
+    pattern = random.choice(["foo", "bar", "baz"])
+    print("Checking \"{}\" for \"{}\" ...".format(x, pattern))
+    print("Result: {}".format(exists(pattern, tree)))
+    print()
diff --git a/users/wpcarro/scratch/facebook/heap.py b/users/wpcarro/scratch/facebook/heap.py
new file mode 100644
index 0000000000..0c0dce91b4
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/heap.py
@@ -0,0 +1,30 @@
+from math import floor
+
+class Heap(object):
+    def __init__(self):
+        self.xs = [None]
+        self.i = 1
+
+    def __repr__(self):
+        return "[{}]".format(", ".join(str(x) for x in self.xs[1:]))
+
+    def insert(self, x):
+        if len(self.xs) == 1:
+            self.xs.append(x)
+            self.i += 1
+            return
+        self.xs.append(x)
+        i = self.i
+        while i != 1 and self.xs[floor(i / 2)] > self.xs[i]:
+            self.xs[floor(i / 2)], self.xs[i] = self.xs[i], self.xs[floor(i / 2)]
+            i = floor(i / 2)
+        self.i += 1
+
+    def root(self):
+        return self.xs[1]
+
+xs = Heap()
+print(xs)
+for x in [12, 15, 14, 21, 1, 10]:
+    xs.insert(x)
+    print(xs)
diff --git a/users/wpcarro/scratch/facebook/highest-product-of-3.py b/users/wpcarro/scratch/facebook/highest-product-of-3.py
new file mode 100644
index 0000000000..c237b8e52e
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/highest-product-of-3.py
@@ -0,0 +1,20 @@
+def hi_product(xs):
+    lowest_one, highest_one = min(xs[0], xs[1]), max(xs[0], xs[1])
+    lowest_two, highest_two = xs[0] * xs[1], xs[0] * xs[1]
+    highest = float('-inf')
+    for x in xs[2:]:
+        highest = max(highest, highest_two * x, lowest_two * x)
+        lowest_one = min(lowest_one, x)
+        highest_one = max(highest_one, x)
+        lowest_two = min(lowest_two, highest_one * x, lowest_one * x)
+        highest_two = max(highest_two, highest_one * x, lowest_one * x)
+    return highest
+
+xs = [([-10,-10,1,3,2], 300),
+      ([1,10,-5,1,-100], 5000)]
+
+for x, expected in xs:
+    result = hi_product(x)
+    print(x, result)
+    assert result == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/infix-to-postfix.py b/users/wpcarro/scratch/facebook/infix-to-postfix.py
new file mode 100644
index 0000000000..4c6d64494d
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/infix-to-postfix.py
@@ -0,0 +1,51 @@
+operators = {
+    '*': 1,
+    '+': 0,
+}
+
+def tokenize(xs):
+    result = []
+    i = 0
+    while i < len(xs):
+        current = xs[i]
+        if current in operators.keys():
+            result.append(current)
+            i += 1
+            continue
+        else:
+            i += 1
+            while i < len(xs) and xs[i] in {str(n) for n in range(10)}:
+                current += xs[i]
+                i += 1
+            result.append(int(current))
+    return result
+
+def postfix(xs):
+    result = []
+    s = []
+    for x in xs:
+        if x in operators.keys():
+            while s and operators[s[-1]] >= operators[x]:
+                result.append(s.pop())
+            s.append(x)
+        else:
+            result.append(x)
+    while s:
+        result.append(s.pop())
+    return result
+
+def evaluate(xs):
+    s = []
+    for x in xs:
+        print(s, x)
+        if x == '*':
+            s.append(s.pop() * s.pop())
+        elif x == '+':
+            s.append(s.pop() + s.pop())
+        else:
+            s.append(x)
+        print(s)
+    return s[-1]
+
+
+print(evaluate(postfix(tokenize("12+3*10"))))
diff --git a/users/wpcarro/scratch/facebook/inflight-entertainment.py b/users/wpcarro/scratch/facebook/inflight-entertainment.py
new file mode 100644
index 0000000000..7ddea5350a
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/inflight-entertainment.py
@@ -0,0 +1,29 @@
+from random import choice
+from utils import init_table
+
+def get(movie, seeking):
+    return any([movie in xs for xs in seeking.values()])
+
+def set_complement(movie, seeking):
+    for duration, xs in seeking.items():
+        seeking[duration].add(duration - movie)
+
+def choose_movies(tolerance, duration, movies):
+    seeking = {duration + i: set() for i in range(-1 * tolerance, tolerance + 1)}
+    for movie in movies:
+        if get(movie, seeking):
+            return movie, duration - movie
+        else:
+            set_complement(movie, seeking)
+    return None
+
+tolerance = 20
+duration = choice([1, 2, 3]) * choice([1, 2]) * choice([15, 30, 45])
+movies = [choice([1, 2, 3]) * choice([15, 30, 45]) for _ in range(10)]
+print("Seeking two movies for a duration of [{}, {}] minutes".format(duration - tolerance, duration + tolerance))
+print(movies)
+result = choose_movies(tolerance, duration, movies)
+if result:
+    print("{} + {} = {}".format(result[0], result[1], duration))
+else:
+    print(":( We're sad because we couldn't find two movies for a {} minute flight".format(duration))
diff --git a/users/wpcarro/scratch/facebook/intersecting-linked-lists.py b/users/wpcarro/scratch/facebook/intersecting-linked-lists.py
new file mode 100644
index 0000000000..80ac01dafd
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/intersecting-linked-lists.py
@@ -0,0 +1,34 @@
+class LinkedList(object):
+    def __init__(self, x):
+        self.val = x
+        self.next = None
+
+    def __repr__(self):
+        if self.next:
+            return "{} -> {}".format(self.val, self.next)
+        return "{}".format(self.val)
+
+def find_intersection(a, b):
+    init_a, init_b = a, b
+
+    while a != b:
+        a = a.next if a.next else init_b
+        b = b.next if b.next else init_a
+
+    return a
+
+# make A...
+e1 = LinkedList(5)
+d1 = LinkedList(2); d1.next = e1
+c1 = LinkedList(3); c1.next = d1 # shared
+b1 = LinkedList(1); b1.next = c1 # shared
+a1 = LinkedList(4); a1.next = b1 # shared
+
+# make B...
+c2 = LinkedList(1); c2.next = c1
+b2 = LinkedList(5); b2.next = c2
+a2 = LinkedList(6); a2.next = b2
+
+print(a1)
+print(a2)
+print(find_intersection(a1, a2).val)
diff --git a/users/wpcarro/scratch/facebook/interview-cake/bst-checker.py b/users/wpcarro/scratch/facebook/interview-cake/bst-checker.py
new file mode 100644
index 0000000000..bbd52fa9c6
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/bst-checker.py
@@ -0,0 +1,14 @@
+def is_valid(node):
+    """
+    Return True if `node` is a valid binary search tree.
+    """
+    s = []
+    s.append((float('-inf'), node, float('inf')))
+    while s:
+        lo, node, hi = s.pop()
+        if lo <= node.value <= hi:
+            node.lhs and s.append((lo, node.lhs, node.value))
+            node.rhs and s.append((node.value, node.rhs, hi))
+        else:
+            return False
+    return True
diff --git a/users/wpcarro/scratch/facebook/interview-cake/cafe-order-checker.py b/users/wpcarro/scratch/facebook/interview-cake/cafe-order-checker.py
new file mode 100644
index 0000000000..688c340b98
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/cafe-order-checker.py
@@ -0,0 +1,34 @@
+def valid(take_out, dine_in, served):
+    # edge case
+    if len(take_out) + len(dine_in) != len(served):
+        return False
+    i = 0
+    j = 0
+    k = 0
+    while i < len(take_out) and j < len(dine_in):
+        if take_out[i] == served[k]:
+            i += 1
+        elif dine_in[j] == served[k]:
+            j += 1
+        else:
+            return False
+        k += 1
+    # take out
+    while i < len(take_out):
+        if take_out[i] != served[k]:
+            return False
+        i += 1
+    # dine in
+    while j < len(dine_in):
+        if dine_in[j] != served[k]:
+            return False
+        j += 1
+    return True
+
+take_out = [17, 8, 24]
+dine_in  = [12, 19, 2]
+served   = [17, 8, 12, 19, 24, 2]
+result = valid(take_out, dine_in, served)
+print(result)
+assert result
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/interview-cake/linked-list-cycles.py b/users/wpcarro/scratch/facebook/interview-cake/linked-list-cycles.py
new file mode 100644
index 0000000000..523ecd959d
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/linked-list-cycles.py
@@ -0,0 +1,70 @@
+def contains_cycle(node):
+    """
+    Return True if the linked-list, `node`, contains a cycle.
+    """
+    if not node:
+        return False
+    a = node
+    b = node.next
+    while a != b:
+        a = a.next
+        if b and b.next and b.next.next:
+            b = b.next.next
+        else:
+            return False
+    return True
+
+################################################################################
+# Bonus
+################################################################################
+
+def first_node_in_cycle(node):
+    """
+    Given that the linked-list, `node`, contains a cycle, return the first
+    element of that cycle.
+    """
+    # enter the cycle
+    a = node
+    b = node.next
+    while a != b:
+        a = a.next
+        b = b.next.next
+
+    # get the length of the cycle
+    beg = a
+    a = a.next
+    n = 1
+    while a != beg:
+        a = a.next
+        n += 1
+
+    # run b n-steps ahead of a
+    a = node
+    b = node
+    for _ in range(n):
+        b = b.next
+
+    # where they intersect is the answer
+    while a != b:
+        a = a.next
+        b = b.next
+    return a
+
+################################################################################
+# Tests
+################################################################################
+
+class Node(object):
+    def __init__(self, value, next=None):
+        self.value = value
+        self.next = next
+    def __repr__(self):
+        return "Node({}) -> ...".format(self.value)
+
+d = Node('d')
+c = Node('c', d)
+b = Node('b', c)
+a = Node('a', b)
+d.next = b
+
+print(first_node_in_cycle(a))
diff --git a/users/wpcarro/scratch/facebook/interview-cake/merge-sorted-arrays.py b/users/wpcarro/scratch/facebook/interview-cake/merge-sorted-arrays.py
new file mode 100644
index 0000000000..877bb218fd
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/merge-sorted-arrays.py
@@ -0,0 +1,30 @@
+def merge_sorted(xs, ys):
+    result = []
+    i = 0
+    j = 0
+    while i < len(xs) and j < len(ys):
+        if xs[i] <= ys[j]:
+            result.append(xs[i])
+            i += 1
+        else:
+            result.append(ys[j])
+            j += 1
+    while i < len(xs):
+        result.append(xs[i])
+        i += 1
+    while j < len(xs):
+        result.append(ys[j])
+        j += 1
+    return result
+
+################################################################################
+# Tests
+################################################################################
+
+xs = [3, 4, 6, 10, 11, 15]
+ys = [1, 5, 8, 12, 14, 19]
+result = merge_sorted(xs, ys)
+print(result)
+assert len(result) == len(xs) + len(ys)
+assert result == [1, 3, 4, 5, 6, 8, 10, 11, 12, 14, 15, 19]
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/interview-cake/nth-fibonacci.py b/users/wpcarro/scratch/facebook/interview-cake/nth-fibonacci.py
new file mode 100644
index 0000000000..4629798cf7
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/nth-fibonacci.py
@@ -0,0 +1,6 @@
+def fib(n):
+    cache = (0, 1)
+    for _ in range(n):
+        a, b = cache
+        cache = (b, a + b)
+    return cache[0]
diff --git a/users/wpcarro/scratch/facebook/interview-cake/permutation-palindrome.py b/users/wpcarro/scratch/facebook/interview-cake/permutation-palindrome.py
new file mode 100644
index 0000000000..ced3b336e0
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/permutation-palindrome.py
@@ -0,0 +1,8 @@
+from collections import Counter
+
+def permutation_can_be_palindrome(x):
+    odd = 0
+    for _, n in Counter(x):
+        if n % 0 != 0:
+            odd += 1
+    return odd <= 1
diff --git a/users/wpcarro/scratch/facebook/interview-cake/queue-two-stacks.py b/users/wpcarro/scratch/facebook/interview-cake/queue-two-stacks.py
new file mode 100644
index 0000000000..bfa465f98d
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/interview-cake/queue-two-stacks.py
@@ -0,0 +1,17 @@
+class Queue(object):
+    def __init__(self):
+        self.lhs = []
+        self.rhs = []
+
+    def enqueue(self, x):
+        self.lhs.append(x)
+
+    def dequeue(self):
+        if self.rhs:
+            return self.rhs.pop()
+        while self.lhs:
+            self.rhs.append(self.lhs.pop())
+        if self.rhs:
+            return self.rhs.pop()
+        else:
+            raise Exception("Attempting to remove an item from an empty queue")
diff --git a/users/wpcarro/scratch/facebook/knapsack-faq.py b/users/wpcarro/scratch/facebook/knapsack-faq.py
new file mode 100644
index 0000000000..ae04f5eb96
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/knapsack-faq.py
@@ -0,0 +1,42 @@
+from utils import get, init_table, print_table
+
+# This problem has a few variants:
+#   - limited supply of each item
+#   - unlimited supply of each item
+#   - fractional amounts of each item (e.g. rice)
+
+def max_haul(capacity, items):
+    min_kg = min([kg for _, kg in items])
+    max_kg = max([kg for _, kg in items])
+
+    cols = int(max_kg / min_kg)
+    fr_col_index = lambda index: min_kg * index + min_kg
+    to_col_index = lambda capacity: int((capacity - min_kg) * cols / max_kg)
+
+    table = init_table(rows=len(items), cols=cols, default=0)
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            curr_capacity = fr_col_index(col)
+            value, kg = items[row]
+
+            if kg > curr_capacity:
+                a = 0
+            else:
+                a = value + get(table, row - 1, to_col_index(curr_capacity - kg))
+
+            b = get(table, row - 1, col)
+            table[row][col] = max([a, b])
+        print_table(table)
+    return table[-1][-1]
+
+guitar = (1500, 1)
+stereo = (3000, 4)
+laptop = (2000, 3)
+necklace = (2000, 0.5)
+items = [necklace, guitar, stereo, laptop]
+capacity = 4
+result = max_haul(capacity, items)
+expected = 4000
+print(result, expected)
+assert result == expected
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/kth-to-last-node-in-singly-linked-list.py b/users/wpcarro/scratch/facebook/kth-to-last-node-in-singly-linked-list.py
new file mode 100644
index 0000000000..dd258d924d
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/kth-to-last-node-in-singly-linked-list.py
@@ -0,0 +1,26 @@
+from linked_list import Node, from_list
+
+def kth_to_last_node(k, node):
+    one = node
+    two = node
+    for _ in range(k - 1):
+        if not one:
+            return None
+        one = one.next
+    while one.next:
+        one = one.next
+        two = two.next
+    return two.value
+
+
+xs = from_list(["Angel Food", "Bundt", "Cheese", "Devil's Food", "Eccles"])
+result = kth_to_last_node(2, xs)
+print(result)
+assert result == "Devil's Food"
+print("Success!")
+
+xs = from_list(["Angel Food", "Bundt"])
+result = kth_to_last_node(30, xs)
+print(result)
+assert result is None
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/language.py b/users/wpcarro/scratch/facebook/language.py
new file mode 100644
index 0000000000..b57f469b49
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/language.py
@@ -0,0 +1,70 @@
+import random
+
+# Write an evaluator for a small language:
+#   - operators: '+', '*'
+#   - operands:  Integers
+#
+# E.g. evaluate("2+14*90+5*16")
+
+def tokenize(xs):
+    result = []
+    i = 0
+    while i < len(xs):
+        current = xs[i]
+        if current in {'*', '+'}:
+            result.append(current)
+            i += 1
+            continue
+        elif current == ' ':
+            i += 1
+            continue
+        else:
+            i += 1
+            while i < len(xs) and xs[i] in {str(x) for x in range(10)}:
+                current += xs[i]
+                i += 1
+            result.append(int(current))
+    return result
+
+def ast(tokens):
+    result = []
+    series = []
+    for token in tokens:
+        if token == '+':
+            result.append(series)
+            series = []
+        elif token == '*':
+            continue
+        else:
+            series.append(token)
+    if series:
+        result.append(series)
+    return result
+
+def product(xs):
+    result = 1
+    for x in xs:
+        result *= x
+    return result
+
+def evaluate(x):
+    tokens = tokenize(x)
+    tree = ast(tokens)
+    return sum([product(xs) for xs in tree])
+
+n = 7
+operands = [random.randint(0, 100) for _ in range(n)]
+operators = [random.choice(['+','*']) for _ in range(n - 1)]
+expr = []
+for i in range(n - 1):
+    expr.append(operands[i])
+    expr.append(operators[i])
+expr.append(operands[-1])
+
+expr = ' '.join([str(x) for x in expr])
+print("Expression: {}".format(expr))
+print("Tokens: {}".format(tokenize(expr)))
+print("AST: {}".format(ast(tokenize(expr))))
+print("Answer: {}".format(evaluate(expr)))
+assert evaluate(expr) == eval(expr)
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/language2.py b/users/wpcarro/scratch/facebook/language2.py
new file mode 100644
index 0000000000..3aebd45483
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/language2.py
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+def tokenize(xs):
+    result = []
+    i = 0
+    while i < len(xs):
+        curr = xs[i]
+        if curr in {'*','+'}:
+            result.append(curr)
+            i += 1
+            continue
+        i += 1
+        while i < len(xs) and xs[i] in {str(x) for x in range(10)}:
+            curr += xs[i]
+            i += 1
+        result.append(int(curr))
+    return result
+
+def parse(tokens):
+    result = []
+    series = []
+    for token in tokens:
+        if token == '*':
+            continue
+        elif token == '+':
+            result.append(series)
+            series = []
+        else:
+            series.append(token)
+    if series:
+        result.append(series)
+    return result
+
+def product(xs):
+    result = 1
+    for x in xs:
+        result *= x
+    return result
+
+def evaluate(tree):
+    return sum([product(xs) for xs in tree])
+
+print(evaluate(parse(tokenize("2+30*8*9+10"))))
diff --git a/users/wpcarro/scratch/facebook/largest-contiguous-sum.py b/users/wpcarro/scratch/facebook/largest-contiguous-sum.py
new file mode 100644
index 0000000000..7761bf1c61
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/largest-contiguous-sum.py
@@ -0,0 +1,15 @@
+def find_sum(xs):
+    result = float('-inf')
+    streak = 0
+    for x in xs:
+        result = max(result, streak, x)
+        if streak + x <= 0:
+            streak = x
+        else:
+            streak += x
+    return result
+
+
+x = [2,-8,3,-2,4,-10]
+assert find_sum(x) == 5
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/largest-stack.py b/users/wpcarro/scratch/facebook/largest-stack.py
new file mode 100644
index 0000000000..052db44153
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/largest-stack.py
@@ -0,0 +1,49 @@
+from stack import Stack, from_list
+from heapq import heapify, heappush, heappop
+from random import shuffle
+
+class MaxStack(Stack):
+    def __init__(self):
+        self.max = Stack()
+        super().__init__()
+
+    def __repr__(self):
+        return super().__repr__()
+
+    def push(self, x):
+        super().push(x)
+        max = self.get_max()
+        if not max:
+            self.max.push(x)
+        else:
+            self.max.push(max if x < max else x)
+
+    def pop(self):
+        self.max.pop()
+        return super().pop()
+
+    def get_max(self):
+        return self.max.peek()
+
+xs = list(range(1, 11))
+shuffle(xs)
+stack = MaxStack()
+for x in xs:
+    stack.push(x)
+
+print(stack)
+result = stack.get_max()
+print(result)
+assert result == 10
+
+popped = stack.pop()
+print("Popped: {}".format(popped))
+print(stack)
+while popped != 10:
+    assert stack.get_max() == 10
+    popped = stack.pop()
+    print("Popped: {}".format(popped))
+    print(stack)
+
+assert stack.get_max() != 10
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/leetcode.org b/users/wpcarro/scratch/facebook/leetcode.org
new file mode 100644
index 0000000000..6e915faf29
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/leetcode.org
@@ -0,0 +1,163 @@
+# This list is from:
+# https://www.teamblind.com/post/New-Year-Gift---Curated-List-of-Top-100-LeetCode-Questions-to-Save-Your-Time-OaM1orEU
+* Array
+** DONE Two Sum
+   https://leetcode.com/problems/two-sum/
+** DONE Best Time to Buy and Sell Stock
+   https://leetcode.com/problems/best-time-to-buy-and-sell-stock/
+** DONE Contains Duplicate
+   https://leetcode.com/problems/contains-duplicate/
+** DONE Product of Array Except Self
+   https://leetcode.com/problems/product-of-array-except-self/
+** DONE Maximum Subarray
+   https://leetcode.com/problems/maximum-subarray/
+** DONE Maximum Product Subarray
+   https://leetcode.com/problems/maximum-product-subarray/
+** DONE Find Minimum in Rotated Sorted Array
+   https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/
+** DONE Search in Rotated Sorted Array
+   https://leetcode.com/problems/search-in-rotated-sorted-array/
+** DONE 3Sum
+   https://leetcode.com/problems/3sum/
+** DONE Container With Most Water
+   https://leetcode.com/problems/container-with-most-water/
+* Binary
+** DONE Sum of Two Integers
+   https://leetcode.com/problems/sum-of-two-integers/
+** DONE Number of 1 Bits
+   https://leetcode.com/problems/number-of-1-bits/
+** TODO Counting Bits
+   https://leetcode.com/problems/counting-bits/
+** DONE Missing Number
+   https://leetcode.com/problems/missing-number/
+** TODO Reverse Bits
+   https://leetcode.com/problems/reverse-bits/
+* Dynamic Programming
+** DONE Climbing Stairs
+   https://leetcode.com/problems/climbing-stairs/
+** TODO Coin Change
+   https://leetcode.com/problems/coin-change/
+** TODO Longest Increasing Subsequence
+   https://leetcode.com/problems/longest-increasing-subsequence/
+** TODO Longest Common Subsequence
+** DONE Word Break Problem
+   https://leetcode.com/problems/word-break/
+** TODO Combination Sum
+   https://leetcode.com/problems/combination-sum-iv/
+** TODO House Robber
+   https://leetcode.com/problems/house-robber/
+** TODO House Robber II
+   https://leetcode.com/problems/house-robber-ii/
+** TODO Decode Ways
+   https://leetcode.com/problems/decode-ways/
+** TODO Unique Paths
+   https://leetcode.com/problems/unique-paths/
+** TODO Jump Game
+   https://leetcode.com/problems/jump-game/
+* Graph
+** DONE Clone Graph
+   https://leetcode.com/problems/clone-graph/
+** DONE Course Schedule
+   https://leetcode.com/problems/course-schedule/
+** TODO Pacific Atlantic Water Flow
+   https://leetcode.com/problems/pacific-atlantic-water-flow/
+** DONE Number of Islands
+   https://leetcode.com/problems/number-of-islands/
+** TODO Longest Consecutive Sequence
+   https://leetcode.com/problems/longest-consecutive-sequence/
+** TODO Alien Dictionary (Leetcode Premium)
+   https://leetcode.com/problems/alien-dictionary/
+** DONE Graph Valid Tree (Leetcode Premium)
+   https://leetcode.com/problems/graph-valid-tree/
+** DONE Number of Connected Components in an Undirected Graph (Leetcode Premium)
+   https://leetcode.com/problems/number-of-connected-components-in-an-undirected-graph/
+* Interval
+** TODO Insert Interval
+   https://leetcode.com/problems/insert-interval/
+** DONE Merge Intervals
+   https://leetcode.com/problems/merge-intervals/
+** TODO No Overlapping Intervals
+   https://leetcode.com/problems/non-overlapping-intervals/
+** DONE Meeting Rooms (Leetcode Premium)
+   https://leetcode.com/problems/meeting-rooms/
+** TODO Meeting Rooms II (Leetcode Premium)
+   https://leetcode.com/problems/meeting-rooms-ii/
+* Linked List
+** DONE Reverse a Linked List
+   https://leetcode.com/problems/reverse-linked-list/
+** DONE Detect Cycle in a Linked List
+   https://leetcode.com/problems/linked-list-cycle/
+** DONE Merge Two Sorted Lists
+   https://leetcode.com/problems/merge-two-sorted-lists/
+** DONE Merge K Sorted Lists
+   https://leetcode.com/problems/merge-k-sorted-lists/
+** DONE Remove Nth Node From End Of List
+   https://leetcode.com/problems/remove-nth-node-from-end-of-list/
+** DONE Reorder List
+   https://leetcode.com/problems/reorder-list/
+* Matrix
+** DONE Set Matrix Zeroes
+   https://leetcode.com/problems/set-matrix-zeroes/
+** DONE Spiral Matrix
+   https://leetcode.com/problems/spiral-matrix/
+** TODO Rotate Image
+   https://leetcode.com/problems/rotate-image/
+** DONE Word Search
+   https://leetcode.com/problems/word-search/
+* String
+** TODO Longest Substring Without Repeating Characters
+   https://leetcode.com/problems/longest-substring-without-repeating-characters/
+** TODO Longest Repeating Character Replacement
+   https://leetcode.com/problems/longest-repeating-character-replacement/
+** TODO Minimum Window Substring
+   https://leetcode.com/problems/minimum-window-substring/
+** DONE Valid Anagram
+   https://leetcode.com/problems/valid-anagram/
+** DONE Group Anagrams
+   https://leetcode.com/problems/group-anagrams/
+** DONE Valid Parentheses
+   https://leetcode.com/problems/valid-parentheses/
+** DONE Valid Palindrome
+   https://leetcode.com/problems/valid-palindrome/
+** TODO Longest Palindromic Substring
+   https://leetcode.com/problems/longest-palindromic-substring/
+** TODO Palindromic Substrings
+   https://leetcode.com/problems/palindromic-substrings/
+** DONE Encode and Decode Strings (Leetcode Premium)
+   https://leetcode.com/problems/encode-and-decode-strings/
+* Tree
+** DONE Maximum Depth of Binary Tree
+   https://leetcode.com/problems/maximum-depth-of-binary-tree/
+** DONE Same Tree
+   https://leetcode.com/problems/same-tree/
+** DONE Invert/Flip Binary Tree
+   https://leetcode.com/problems/invert-binary-tree/
+** DONE Binary Tree Maximum Path Sum
+   https://leetcode.com/problems/binary-tree-maximum-path-sum/
+** DONE Binary Tree Level Order Traversal
+   https://leetcode.com/problems/binary-tree-level-order-traversal/
+** DONE Serialize and Deserialize Binary Tree
+   https://leetcode.com/problems/serialize-and-deserialize-binary-tree/
+** DONE Subtree of Another Tree
+   https://leetcode.com/problems/subtree-of-another-tree/
+** DONE Construct Binary Tree from Preorder and Inorder Traversal
+   https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
+** DONE Validate Binary Search Tree
+   https://leetcode.com/problems/validate-binary-search-tree/
+** DONE Kth Smallest Element in a BST
+   https://leetcode.com/problems/kth-smallest-element-in-a-bst/
+** DONE Lowest Common Ancestor of BST
+   https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
+** DONE Implement Trie (Prefix Tree)
+   https://leetcode.com/problems/implement-trie-prefix-tree/
+** DONE Add and Search Word
+   https://leetcode.com/problems/add-and-search-word-data-structure-design/
+** DONE Word Search II
+   https://leetcode.com/problems/word-search-ii/
+* Heap
+** DONE Merge K Sorted Lists
+   https://leetcode.com/problems/merge-k-sorted-lists/
+** DONE Top K Frequent Elements
+   https://leetcode.com/problems/top-k-frequent-elements/
+** DONE Find Median from Data Stream
+   https://leetcode.com/problems/find-median-from-data-stream/
diff --git a/users/wpcarro/scratch/facebook/linked-list-cycles.py b/users/wpcarro/scratch/facebook/linked-list-cycles.py
new file mode 100644
index 0000000000..56f54d4978
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/linked-list-cycles.py
@@ -0,0 +1,26 @@
+import random
+
+from linked_list import Node
+
+def contains_cycle(node):
+    one = node
+    two = node
+    while two.next and two.next.next:
+        one = one.next
+        two = two.next.next
+        if one == two:
+            return True
+    return False
+
+xs = Node(1, Node(2, Node(3)))
+assert not contains_cycle(xs)
+print("Success!")
+
+a = Node(1)
+b = Node(2)
+c = Node(3)
+a.next = b
+b.next = c
+c.next = random.choice([a, b, c])
+assert contains_cycle(a)
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/linked_list.py b/users/wpcarro/scratch/facebook/linked_list.py
new file mode 100644
index 0000000000..1ae7061e83
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/linked_list.py
@@ -0,0 +1,22 @@
+class Node(object):
+    def __init__(self, value=None, next=None):
+        self.value = value
+        self.next = next
+
+    def __repr__(self):
+        result = []
+        node = self
+        while node:
+            result.append(str(node.value))
+            node = node.next
+        return 'LinkedList({xs})'.format(xs=', '.join(result))
+
+def from_list(xs):
+    head = Node(xs[0])
+    node = head
+    for x in xs[1:]:
+        node.next = Node(x)
+        node = node.next
+    return head
+
+list = from_list(['A', 'B', 'C'])
diff --git a/users/wpcarro/scratch/facebook/london-knapsack.py b/users/wpcarro/scratch/facebook/london-knapsack.py
new file mode 100644
index 0000000000..a034fb4961
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/london-knapsack.py
@@ -0,0 +1,42 @@
+from utils import get, init_table, print_table
+
+def optimal_itinerary(duration, items):
+    min_duration = min([duration for duration, _ in items])
+    max_duration = max([duration for duration, _ in items])
+    table = init_table(rows=len(items), cols=int(max_duration / min_duration), default=0)
+    to_index = lambda duration: int(duration / min_duration) - 1
+    to_duration = lambda i: i * min_duration + min_duration
+
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            curr_duration = to_duration(col)
+            duration, value = items[row]
+            if duration > curr_duration:
+                a = 0
+            else:
+                a = value + get(table, row - 1, to_index(curr_duration - duration))
+            b = get(table, row - 1, col)
+            table[row][col] = max([a, b])
+
+        print_table(table)
+    return table[-1][-1]
+
+# You're in London for two days, and you'd like to see the following
+# attractions. How can you maximize your time spent in London?
+westminster = (0.5, 7)
+globe_theater = (0.5, 6)
+national_gallery = (1, 9)
+british_museum = (2, 9)
+st_pauls_cathedral = (0.5, 8)
+items = [
+    westminster,
+    globe_theater,
+    national_gallery,
+    british_museum,
+    st_pauls_cathedral,
+]
+result = optimal_itinerary(2, items)
+expected = 24
+print(result, expected)
+assert result == expected
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/longest-common-substring.py b/users/wpcarro/scratch/facebook/longest-common-substring.py
new file mode 100644
index 0000000000..8a838db45d
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/longest-common-substring.py
@@ -0,0 +1,20 @@
+from utils import get, init_table, print_table
+
+def longest_common_substring(a, b):
+    """
+    Computes the length of the longest string that's present in both `a` and
+    `b`.
+    """
+    table = init_table(rows=len(b), cols=len(a), default=0)
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            if b[row] == a[col]:
+                table[row][col] = 1 + get(table, row - 1, col - 1)
+    return max([max(row) for row in table])
+
+dictionary = ["fish", "vista"]
+result = [longest_common_substring("hish", x) for x in dictionary]
+expected = [3, 2]
+print(result, expected)
+assert result == expected
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/merge-sorted-arrays.py b/users/wpcarro/scratch/facebook/merge-sorted-arrays.py
new file mode 100644
index 0000000000..ae9377ad11
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/merge-sorted-arrays.py
@@ -0,0 +1,44 @@
+def merge_sorted(xs, ys):
+    result = []
+    i, j = 0, 0
+
+    while i < len(xs) and j < len(ys):
+        if xs[i] <= ys[j]:
+            result.append(xs[i])
+            i += 1
+        else:
+            result.append(ys[j])
+            j += 1
+
+    while i < len(xs):
+        result.append(xs[i])
+        i += 1
+
+    while j < len(ys):
+        result.append(ys[j])
+        j += 1
+
+    return result
+
+# A
+result = merge_sorted([3, 4, 6, 10, 11, 15], [1, 5, 8, 12, 14, 19])
+print(result)
+assert result == [1, 3, 4, 5, 6, 8, 10, 11, 12, 14, 15, 19]
+
+# B
+result = merge_sorted([], [1,2,3])
+print(result)
+assert result == [1,2,3]
+
+# C
+result = merge_sorted([1,2,3], [])
+print(result)
+assert result == [1,2,3]
+
+# D
+result = merge_sorted([], [])
+print(result)
+assert result == []
+
+# Wahoo!
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/merging-ranges.py b/users/wpcarro/scratch/facebook/merging-ranges.py
new file mode 100644
index 0000000000..6da44572ee
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/merging-ranges.py
@@ -0,0 +1,23 @@
+
+def merge(xs):
+    xs.sort()
+    result = xs[0:1]
+    for a, b in xs[1:]:
+        y, z = result[-1]
+        if a <= z:
+            result[-1] = (y, max(b, z))
+        else:
+            result.append((a, b))
+    return result
+
+inputs = [([(0,1),(3,5),(4,8),(10,12),(9,10)], [(0,1),(3,8),(9,12)]),
+          ([(1,2),(2,3)], [(1,3)]),
+          ([(1,5),(2,3)], [(1,5)]),
+          ([(1,10),(2,6),(3,5),(7,9)], [(1,10)]),
+          ]
+for x, expected in inputs:
+    result = merge(x)
+    print(x)
+    print(result)
+    assert result == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/mesh-message.py b/users/wpcarro/scratch/facebook/mesh-message.py
new file mode 100644
index 0000000000..8438b059d8
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/mesh-message.py
@@ -0,0 +1,40 @@
+from heapq import heappush, heappop
+import random
+
+def shortest_path(a, b, graph):
+    seen = set()
+    h = []
+    heappush(h, (0, a, [a]))
+    while h:
+        km, x, path = heappop(h)
+        if x == b:
+            return path
+        for c in graph[x]:
+            if c not in seen:
+                heappush(h, (km + 1, c, path + [c]))
+    raise Exception("We were unable to find a path from {} to {}".format(a, b))
+
+graph = {
+    'Min'     : ['William', 'Jayden', 'Omar'],
+    'William' : ['Min', 'Noam'],
+    'Jayden'  : ['Min', 'Amelia', 'Ren', 'Noam'],
+    'Ren'     : ['Jayden', 'Omar'],
+    'Amelia'  : ['Jayden', 'Adam', 'Miguel'],
+    'Adam'    : ['Amelia', 'Miguel', 'Sofia', 'Lucas'],
+    'Miguel'  : ['Amelia', 'Adam', 'Liam', 'Nathan'],
+    'Noam'    : ['Nathan', 'Jayden', 'William'],
+    'Omar'    : ['Ren', 'Min', 'Scott'],
+    'Liam'    : ['Ren'],
+    'Nathan'  : ['Noam'],
+    'Scott'   : [],
+}
+
+result = shortest_path('Jayden', 'Adam', graph)
+print(result)
+assert result == ['Jayden', 'Amelia', 'Adam']
+print('Success!')
+
+beg = random.choice(list(graph.keys()))
+end = random.choice(list(graph.keys()))
+print("Attempting to find the shortest path between {} and {}".format(beg, end))
+print(shortest_path(beg, end, graph))
diff --git a/users/wpcarro/scratch/facebook/moderate/decompress-xml.py b/users/wpcarro/scratch/facebook/moderate/decompress-xml.py
new file mode 100644
index 0000000000..b22983ed7a
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/moderate/decompress-xml.py
@@ -0,0 +1,98 @@
+import string
+from parser import Parser
+
+mapping = {
+    1: "family",
+    2: "person",
+    3: "firstName",
+    4: "lastName",
+    5: "state",
+}
+
+def parse_int(i, xs):
+    result = ""
+    while i < len(xs) and xs[i] in string.digits:
+        result += xs[i]
+        i += 1
+    return i, int(result)
+
+def parse_string(i, xs):
+    result = ""
+    while xs[i+1] not in string.digits:
+        result += xs[i]
+        i += 1
+    return i, result
+
+def tokenize(xs):
+    result = []
+    i = 0
+    while i < len(xs):
+        if xs[i] in string.digits:
+            i, n = parse_int(i, xs)
+            result.append(n)
+        elif xs[i] in string.ascii_letters:
+            i, x = parse_string(i, xs)
+            result.append(x)
+        elif xs[i] == " ":
+            i += 1
+            continue
+    return result
+
+def parse(xs):
+    parser = Parser(tokenize(xs))
+    return parse_element(parser)
+
+# Element   -> Tag Attribute* End Element* End ;
+# Tag       -> INTEGER ;
+# Value     -> STRING End ;
+# Attribute -> Tag Value ;
+# End       -> 0 ;
+
+def parse_element(parser):
+    if type(parser.curr()) == str:
+        return parser.consume()
+    tag_id = parser.expect_predicate(lambda x: type(x) == int)
+    tag = mapping[tag_id]
+    attrs = parse_attrs(parser)
+    parser.expect([0])
+    children = []
+    while not parser.exhausted() and parser.curr() != 0:
+        children.append(parse_element(parser))
+    parser.expect([0])
+    return [tag, attrs, children]
+
+def parse_attrs(parser):
+    result = []
+    while parser.curr() != 0:
+        tag_id = parser.expect_predicate(lambda x: type(x) == int)
+        tag = mapping[tag_id]
+        value = parser.consume()
+        result.append((tag, value))
+    return result
+
+def stringify_xml(tree, indent=0):
+    if type(tree) == str:
+        return tree
+    result = ""
+    tag, attrs, children = tree
+
+    str_attrs = []
+    for k, v in attrs:
+        str_attrs.append("{}=\"{}\"".format(k, v))
+    str_attrs = (" " if str_attrs else "") + " ".join(str_attrs)
+
+    str_children = []
+    for child in children:
+        str_children.append(" " * 2 * indent + stringify_xml(child, indent + 1))
+    str_children = "\n".join(str_children)
+
+    result += "{}<{}{}>\n{}{}\n{}</{}>".format(
+        " " * 2 * indent, tag, str_attrs, " " * 2 * indent, str_children,
+        " " * 2 * indent, tag)
+    return result
+
+x = "1 4 McDowell 5 CA 0 2 3 Gayle 0 Some Message 0 0"
+print("Input:   {}".format(x))
+print("Tokens:  {}".format(tokenize(x)))
+print("Parsed:  {}".format(parse(x)))
+print("{}".format(stringify_xml(parse(x))))
diff --git a/users/wpcarro/scratch/facebook/moderate/find-pairs-for-sum.py b/users/wpcarro/scratch/facebook/moderate/find-pairs-for-sum.py
new file mode 100644
index 0000000000..69c2fc4312
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/moderate/find-pairs-for-sum.py
@@ -0,0 +1,19 @@
+import random
+
+def find_pairs(xs, n):
+    """
+    Return all pairs of integers in `xs` that sum to `n`.
+    """
+    seeking = set()
+    result = set()
+    for x in xs:
+        if x in seeking:
+            result.add((n - x, x))
+        else:
+            seeking.add(n - x)
+    return result
+
+xs = [random.randint(1, 10) for _ in range(10)]
+n = random.randint(1, 10) + random.randint(1, 10)
+print("Seeking all pairs in {} for {}...".format(xs, n))
+print(find_pairs(xs, n))
diff --git a/users/wpcarro/scratch/facebook/moderate/parser.py b/users/wpcarro/scratch/facebook/moderate/parser.py
new file mode 100644
index 0000000000..57dfb058c0
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/moderate/parser.py
@@ -0,0 +1,37 @@
+class Parser(object):
+    def __init__(self, tokens):
+        self.tokens = tokens
+        self.i = 0
+
+    def prev(self):
+        return self.tokens[self.i - 1]
+
+    def curr(self):
+        return self.tokens[self.i]
+
+    def next(self):
+        return self.tokens[self.i + 1]
+
+    def consume(self):
+        if not self.exhausted():
+            self.i += 1
+            return self.prev()
+
+    def match(self, xs):
+        if not self.exhausted() and self.curr() in xs:
+            self.consume()
+            return True
+        return False
+
+    def expect(self, xs):
+        if not self.match(xs):
+            raise Exception("Expected token \"{}\" but received \"{}\"".format(xs, self.curr()))
+        return self.prev()
+
+    def expect_predicate(self, predicate):
+        if predicate(self.curr()):
+            return self.consume()
+        raise Exception("Expected token \"{}\" to pass predicate, but it did not".format(self.curr()))
+
+    def exhausted(self):
+        return self.i >= len(self.tokens)
diff --git a/users/wpcarro/scratch/facebook/moderate/rand7.py b/users/wpcarro/scratch/facebook/moderate/rand7.py
new file mode 100644
index 0000000000..ed3a7cea80
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/moderate/rand7.py
@@ -0,0 +1,25 @@
+# Define a function, rand7, that generates a random number [0,7), using only
+# rand5, which generates a random number [0,5).
+
+import random
+from collections import Counter
+
+# Returns [0,4]
+def rand5():
+    return random.randint(0,4)
+
+# Return [0,6]
+def rand7_a():
+    return sum(rand5() for _ in range(7)) % 7
+
+# Return [0,6]
+def rand7_b():
+    x = 5 * rand5() + rand5()
+    if x < 21:
+        return x % 7
+    return rand7_b()
+
+c = Counter([rand7_a() for _ in range(100000)])
+print(c)
+c = Counter([rand7_b() for _ in range(100000)])
+print(c)
diff --git a/users/wpcarro/scratch/facebook/moderate/tic-tac-toe-checker.py b/users/wpcarro/scratch/facebook/moderate/tic-tac-toe-checker.py
new file mode 100644
index 0000000000..342c29be6b
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/moderate/tic-tac-toe-checker.py
@@ -0,0 +1,99 @@
+import random
+
+def print_board(board):
+    result = []
+    for row in range(len(board)):
+        r = []
+        for col in range(len(board[row])):
+            cell = board[row][col]
+            if not cell:
+                r.append("-")
+            else:
+                r.append(cell)
+        result.append(" | ".join(r))
+    print("\n---------\n".join(result))
+
+def init_board():
+    result = []
+    for row in range(3):
+        r = []
+        for col in range(3):
+            r.append(None)
+        result.append(r)
+    return result
+
+def check(board, player):
+    print_board(board)
+    print()
+    if player not in "XO":
+        raise Exception("Only checking the board for Xs or Os. You supplied {}".format(player))
+    dn, ax, ddg, udg = "DOWN", "ACROSS", "DOWN_DIAGONAL", "UP_DIAGONAL"
+    ways = [
+        [[dn, ax, ddg], [dn], [dn, udg]],
+        [[ax], [], []],
+        [[ax], [], []],
+    ]
+    for row in range(len(board)):
+        for col in range(len(board[row])):
+            if board[row][col] == player:
+                xs = ways[row][col]
+                for x in xs:
+                    if x == dn:
+                        if {player} == {board[row+1][col], board[row+2][col]}:
+                            return True
+                    if x == ax:
+                        if {player} == {board[row][col+1], board[row][col+2]}:
+                            return True
+                    if x == ddg:
+                        if {player} == {board[row+1][col+1], board[row+2][col+2]}:
+                            return True
+                    if x == udg:
+                        if {player} == {board[row+1][col-1], board[row+2][col-2]}:
+                            return True
+    return False
+
+def op(player):
+    return "X" if player == "O" else "O"
+
+dn_win = lambda p: [
+    [op(p), p, None],
+    [op(p), p, None],
+    [None,  p, None],
+]
+
+ax_win = lambda p: [
+    [p, p, p],
+    [op(p), op(p), None],
+    [None, None, None],
+]
+
+ddg_win = lambda p: [
+    [p, None, None],
+    [op(p), p, None],
+    [op(p), None, p],
+]
+
+udg_win = lambda p: [
+    [op(p), None, p],
+    [op(p), p, None],
+    [p, None, None],
+]
+
+# Down
+p = random.choice(["X", "O"])
+assert check(dn_win(p), p) == True
+assert check(dn_win(p), op(p)) == False
+# Across
+p = random.choice(["X", "O"])
+assert check(ax_win(p), p) == True
+assert check(ax_win(p), op(p)) == False
+# Down Diagonally
+p = random.choice(["X", "O"])
+assert check(ddg_win(p), p) == True
+assert check(ddg_win(p), op(p)) == False
+# Down Diagonally
+p = random.choice(["X", "O"])
+assert check(udg_win(p), p) == True
+assert check(udg_win(p), op(p)) == False
+# Success
+print("Tests pass!")
diff --git a/users/wpcarro/scratch/facebook/moderate/unsorted-substring.py b/users/wpcarro/scratch/facebook/moderate/unsorted-substring.py
new file mode 100644
index 0000000000..de7326b058
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/moderate/unsorted-substring.py
@@ -0,0 +1,67 @@
+# Write a function that accepts an array of integers and returns the indices for
+# the starting and ending integers that, if their elements were sorted, the
+# entire array would be sorted.
+
+################################################################################
+# First Attempt
+################################################################################
+
+def unsorted_substring(xs):
+    ys = xs[:]; ys.sort()
+    m = 0
+    while xs[m] == ys[m]:
+        m += 1
+        if m >= len(xs):
+            return -1, -1
+    n = len(xs) - 1
+    while xs[n] == ys[n]:
+        n -= 1
+    return m, n
+
+################################################################################
+# Second Attempt
+################################################################################
+
+def unsorted_substring_2(xs):
+    beg = 1
+    while xs[beg - 1] <= xs[beg]:
+        beg += 1
+        if beg >= len(xs):
+            return -1, -1
+    end = len(xs) - 2
+    while xs[end + 1] >= xs[end]:
+        end -= 1
+
+    min_mid = xs[beg]
+    max_mid = xs[beg]
+    i = beg + 1
+    while i <= end:
+        min_mid = min(min_mid, xs[i])
+        max_mid = max(max_mid, xs[i])
+        i += 1
+
+    # beg -= 1 until max(lhs) <= min(mid)
+    while beg - 1 >= 0 and xs[beg - 1] >= min_mid:
+        beg -= 1
+
+    # end += 1 while max(mid) <= min(rhs)
+    while end + 1 < len(xs) and max_mid >= xs[end + 1]:
+        end += 1
+    return beg, end
+
+################################################################################
+# Tests
+################################################################################
+
+xs = [
+    [1,2,4,7,10,11,7,12,6,7,16,18,19],
+    [1,2,3,4],
+    [4,3,2,1],
+    [1,3,2,4],
+    [2,1,3,4],
+]
+
+for x in xs:
+    print("Testing: {}".format(x))
+    print("1) {}".format(unsorted_substring(x)))
+    print("2) {}".format(unsorted_substring_2(x)))
diff --git a/users/wpcarro/scratch/facebook/move-zeroes-to-end.py b/users/wpcarro/scratch/facebook/move-zeroes-to-end.py
new file mode 100644
index 0000000000..1535b5a9fa
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/move-zeroes-to-end.py
@@ -0,0 +1,62 @@
+from collections import deque
+
+def move_zeroes_to_end_quadratic(xs):
+    """
+    This solution is suboptimal. It runs in quadratic time, and it uses constant
+    space.
+    """
+    i = 0
+    while i < len(xs) - 1:
+        if xs[i] == 0:
+            j = i + 1
+            while j < len(xs) and xs[j] == 0:
+                j += 1
+            if j >= len(xs):
+                break
+            xs[i], xs[j] = xs[j], xs[i]
+        i += 1
+
+def move_zeroes_to_end_linear(xs):
+    """
+    This solution is clever. It runs in linear time proportionate to the number
+    of elements in `xs`, and has linear space proportionate to the number of
+    consecutive zeroes in `xs`.
+    """
+    q = deque()
+    for i in range(len(xs)):
+        if xs[i] == 0:
+            q.append(i)
+        else:
+            if q:
+                j = q.popleft()
+                xs[i], xs[j] = xs[j], xs[i]
+                q.append(i)
+
+def move_zeroes_to_end_linear_constant_space(xs):
+    """
+    This is the optimal solution. It runs in linear time and uses constant
+    space.
+    """
+    i = 0
+    for j in range(len(xs)):
+        if xs[j] != 0:
+            xs[i], xs[j] = xs[j], xs[i]
+            i += 1
+
+
+################################################################################
+# Tests
+################################################################################
+
+xss = [
+    [1, 2, 0, 3, 4, 0, 0, 5, 0],
+    [0, 1, 2, 0, 3, 4],
+    [0, 0],
+]
+
+f = move_zeroes_to_end_linear_constant_space
+
+for xs in xss:
+    print(xs)
+    f(xs)
+    print(xs)
diff --git a/users/wpcarro/scratch/facebook/mst.py b/users/wpcarro/scratch/facebook/mst.py
new file mode 100644
index 0000000000..81aa5cd487
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/mst.py
@@ -0,0 +1,71 @@
+from heapq import heappush, heappop
+import random
+
+def to_vertex_list(graph):
+    result = {}
+    for a, b, kg in graph:
+        if a in result:
+            result[a].append((b, kg))
+        else:
+            result[a] = [(b, kg)]
+        if b in result:
+            result[b].append((a, kg))
+        else:
+            result[b] = [(a, kg)]
+    return result
+
+def mst(graph):
+    graph = to_vertex_list(graph)
+    beg = random.choice(list(graph.keys()))
+    h = []
+    result = []
+    seen = set()
+    for c, kg in graph[beg]:
+        heappush(h, (kg, beg, c))
+    while h:
+        kg, beg, end = heappop(h)
+        # detect cycles
+        if end in seen:
+            continue
+        # use the edge
+        seen.add(beg)
+        seen.add(end)
+        result.append((beg, end))
+        for c, kg in graph[end]:
+            heappush(h, (kg, end, c))
+    return result
+
+graphs = [
+    [
+        ('A', 'B', 7),
+        ('A', 'D', 5),
+        ('B', 'D', 9),
+        ('E', 'D', 15),
+        ('F', 'D', 6),
+        ('F', 'G', 11),
+        ('F', 'E', 8),
+        ('G', 'E', 9),
+        ('C', 'E', 5),
+        ('B', 'E', 7),
+        ('B', 'C', 8),
+    ],
+    [
+        ('A', 'B', 4),
+        ('A', 'C', 8),
+        ('B', 'C', 11),
+        ('B', 'E', 8),
+        ('C', 'D', 7),
+        ('C', 'F', 1),
+        ('D', 'E', 2),
+        ('D', 'F', 6),
+        ('E', 'G', 7),
+        ('E', 'H', 4),
+        ('F', 'H', 2),
+        ('G', 'H', 14),
+        ('G', 'I', 9),
+        ('H', 'I', 10),
+    ],
+]
+
+for graph in graphs:
+    print(mst(graph))
diff --git a/users/wpcarro/scratch/facebook/n-queens.py b/users/wpcarro/scratch/facebook/n-queens.py
new file mode 100644
index 0000000000..fc9326886c
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/n-queens.py
@@ -0,0 +1,46 @@
+def print_board(board):
+    result = []
+    for row in range(8):
+        r = []
+        for col in range(8):
+            r.append("X" if col == board[row] else "-")
+        result.append(" ".join(r))
+    print("\n".join(result))
+    print()
+
+def can_place(board, row, col):
+    column_occupied = not any([board[i] == col for i in range(row)])
+
+    diagonals_clear = True
+    for r in range(row):
+        w = abs(col - board[r])
+        h = abs(r - row)
+        if w == h:
+            diagonals_clear = False
+            break
+
+    return all([column_occupied, diagonals_clear])
+
+def init_board():
+    board = []
+    for row in range(8):
+        board.append(None)
+    return board
+
+def copy_board(board):
+    return board[:]
+
+def n_queens():
+    do_n_queens(init_board(), 0, 0)
+
+def do_n_queens(board, row, col):
+    if row == 8:
+        print_board(board)
+        return
+    for i in range(col, 8):
+        if can_place(board, row, i):
+            copy = copy_board(board)
+            copy[row] = i
+            do_n_queens(copy, row + 1, 0)
+
+n_queens()
diff --git a/users/wpcarro/scratch/facebook/nearby-words.py b/users/wpcarro/scratch/facebook/nearby-words.py
new file mode 100644
index 0000000000..d2fc3cf5cf
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/nearby-words.py
@@ -0,0 +1,33 @@
+def nearby_chars(c):
+    keyboard = [
+        "qwertyuiop",
+        "asdfghjkl",
+        "zxcvbnm",
+    ]
+
+    for row in keyboard:
+        for i in range(len(row)):
+            if row[i] == c:
+                result = set()
+                if i + 1 < len(row):
+                    result.add(row[i + 1])
+                if i - 1 >= 0:
+                    result.add(row[i - 1])
+                return result
+
+def is_word(word):
+    words = {
+        "hello",
+    }
+    return word in words
+
+def nearby_words(x):
+    result = set()
+    for i in range(len(x)):
+        for c in nearby_chars(x[i]):
+            candidate = x[0:i] + c + x[i+1:]
+            if is_word(candidate):
+                result.add(candidate)
+    return result
+
+print(nearby_words('gello'))
diff --git a/users/wpcarro/scratch/facebook/node.py b/users/wpcarro/scratch/facebook/node.py
new file mode 100644
index 0000000000..4e24983af7
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/node.py
@@ -0,0 +1,38 @@
+class Node(object):
+    def __init__(self, value, left=None, right=None):
+        self.value = value
+        self.left = left
+        self.right = right
+
+    def insert_left(self, value):
+        self.left = Node(value)
+        return self.left
+
+    def insert_right(self, value):
+        self.right = Node(value)
+        return self.right
+
+tree = Node(
+    50,
+    Node(
+        17,
+        Node(
+            12,
+            Node(9),
+            Node(14),
+        ),
+        Node(
+            23,
+            Node(19),
+        ),
+    ),
+    Node(
+        72,
+        Node(
+            54,
+            None,
+            Node(67)
+        ),
+        Node(76),
+    ),
+)
diff --git a/users/wpcarro/scratch/facebook/nth-fibonacci.py b/users/wpcarro/scratch/facebook/nth-fibonacci.py
new file mode 100644
index 0000000000..f524067b3b
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/nth-fibonacci.py
@@ -0,0 +1,13 @@
+# 0, 1, 1, 2, 3, 5
+def fib(n):
+    if n < 0:
+        raise Exception("Need to supply an index that's >= 0. Not: {}".format(n))
+    elif n in {0, 1}:
+        return n
+    state = [0, 1]
+    for i in range(1, n):
+        state[0], state[1] = state[1], state[0] + state[1]
+    return state[-1]
+
+for i in range(10):
+    print("fib({}) => {}".format(i, fib(i)))
diff --git a/users/wpcarro/scratch/facebook/onsite.txt b/users/wpcarro/scratch/facebook/onsite.txt
new file mode 100644
index 0000000000..b5242c4bd3
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/onsite.txt
@@ -0,0 +1,22 @@
+** Behavior Interview **
+- Can I work in an unstructured environment?
+- Do I have a growth mindset?
+- How do I handle conflict?
+- Am I empathic?
+- Am I a self-starter?
+- What is my communication style?
+- Do I persevere?
+- <forgot to write this one down>
+
+** Design Interview **
+- requirement gathering, problem exploring
+- component analysis
+- quantitative analysis
+- trade-offs
+- bottlenecks, weaknesses
+- securing data (e.g. PII)
+
+Consider:
+- pagination
+- push/pull requests
+- API design
diff --git a/users/wpcarro/scratch/facebook/parsing/json.py b/users/wpcarro/scratch/facebook/parsing/json.py
new file mode 100644
index 0000000000..3975e973fe
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/parsing/json.py
@@ -0,0 +1,121 @@
+from parser import Parser
+
+# As an exercise to stress-test my understanding of recursive descent parsers,
+# I'm attempting to write a JSON parser without referencing any existing BNF
+# descriptions of JSON or existing JSON parser implementations.
+#
+# I'm only parsing a subset of JSON: enough to parse `sample`. Here is the BNF
+# that I wrote to describe my expected input:
+#
+# expression -> object
+# object     -> '{' ( STRING ':' expression ) ( ',' STRING ':' expression )* '}'
+#            |  array
+# array      -> '[' expression ( ',' expression )* ']'
+#            |  literal
+# literal    -> STRING | INT
+
+def tokenize(xs):
+    """
+    Return a list of tokens from the string input, `xs`.
+    """
+    result = []
+    i = 0
+    while i < len(xs):
+        # single characters
+        if xs[i] in ",{}:[]":
+            result.append(xs[i])
+            i += 1
+        # strings
+        elif xs[i] == "\"":
+            curr = xs[i]
+            i += 1
+            while xs[i] != "\"":
+                curr += xs[i]
+                i += 1
+            curr += xs[i]
+            result.append(curr)
+            i += 1
+        # integers
+        elif xs[i] in "0123456789":
+            curr = xs[i]
+            i += 1
+            while xs[i] in "0123456789":
+                curr += xs[i]
+                i += 1
+            result.append(int(curr))
+        # whitespace
+        elif xs[i] in {" ", "\n"}:
+            i += 1
+    return result
+
+def parse_json(x):
+    """
+    Attempt to parse the string, `x`, into JSON.
+    """
+    tokens = tokenize(x)
+    return parse_object(Parser(tokens))
+
+def parse_object(parser):
+    if parser.match(['{']):
+        key = parse_string(parser)
+        parser.expect([':'])
+        value = parse_object(parser)
+        result = [(key, value)]
+        while parser.match([',']):
+            key = parse_string(parser)
+            parser.match([':'])
+            value = parse_object(parser)
+            result.append((key, value))
+        return result
+    return parse_array(parser)
+
+def parse_array(parser):
+    if parser.match(['[']):
+        if parser.match([']']):
+            return []
+        result = [parse_object(parser)]
+        while parser.match([',']):
+            result.append(parse_object(parser))
+        parser.expect([']'])
+        return result
+    else:
+        return parse_literal(parser)
+
+def parse_string(parser):
+    if parser.curr().startswith("\""):
+        return parser.consume()
+    else:
+        raise Exception("Unexpected token: {}".format(parser.curr()))
+
+def parse_literal(parser):
+    return parser.consume()
+
+sample = """
+{
+  "glossary": {
+    "title": "example glossary",
+    "GlossDiv": {
+      "title": "S",
+      "GlossList": {
+        "GlossEntry": {
+          "ID": "SGML",
+          "SortAs": "SGML",
+          "GlossTerm": "Standard Generalized Markup Language",
+          "Acronym": "SGML",
+          "Abbrev": "ISO 8879:1986",
+          "GlossDef": {
+            "para": "A meta-markup language, used to create markup languages such as DocBook.",
+            "GlossSeeAlso": [
+              "GML",
+              "XML"
+            ]
+          },
+          "GlossSee": "markup"
+        }
+      }
+    }
+  }
+}
+"""
+
+print(parse_json(sample))
diff --git a/users/wpcarro/scratch/facebook/parsing/parser.py b/users/wpcarro/scratch/facebook/parsing/parser.py
new file mode 100644
index 0000000000..407bff61c9
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/parsing/parser.py
@@ -0,0 +1,28 @@
+class Parser(object):
+    def __init__(self, tokens):
+        self.tokens = tokens
+        self.i = 0
+
+    def prev(self):
+        return self.tokens[self.i - 1]
+
+    def curr(self):
+        return self.tokens[self.i]
+
+    def consume(self):
+        if not self.exhausted():
+            self.i += 1
+            return self.prev()
+
+    def match(self, xs):
+        if not self.exhausted() and self.curr() in xs:
+            self.consume()
+            return True
+        return False
+
+    def expect(self, xs):
+        if not self.match(xs):
+            raise Exception("Expected token \"{}\" but received \"{}\"".format(xs, self.curr()))
+
+    def exhausted(self):
+        return self.i >= len(self.tokens)
diff --git a/users/wpcarro/scratch/facebook/parsing/regex.py b/users/wpcarro/scratch/facebook/parsing/regex.py
new file mode 100644
index 0000000000..7fc2ef34e2
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/parsing/regex.py
@@ -0,0 +1,184 @@
+# Writing a small proof-of-concept...
+#   - lexer
+#   - parser
+#   - compiler
+# ...for regex.
+#
+# BNF
+# expression -> ( char_class | CHAR ) quantifier? ( "|" expression )*
+# char_class -> "[" CHAR+ "]"
+# quantifier -> "?" | "*" | "+" | "{" INT? "," INT? "}"
+#
+# Of the numerous things I do not support, here are a few items of which I'm
+# aware:
+#   - alternatives:   (a|b)
+#   - capture groups: (ab)cd
+
+from parser import Parser
+import string
+
+################################################################################
+# Top-Level API
+################################################################################
+
+def tokenize(xs):
+    """
+    Transform `xs` into a list of tokens.
+
+    Also: expand shorthand symbols using the following table:
+      - ? -> {0,1}
+      - * -> {0,}
+      - + -> {1,}
+    """
+    result = []
+    i = 0
+    shorthand = {
+        "?": ["{", 0, ",", 1, "}"],
+        "*": ["{", 0, ",", "}"],
+        "+": ["{", 1, ",", "}"],
+    }
+    while i < len(xs):
+        if xs[i] in shorthand:
+            for c in shorthand[xs[i]]:
+                result.append(c)
+            i += 1
+        elif xs[i] == "{":
+            result.append(xs[i])
+            i += 1
+            curr = ""
+            while xs[i] in string.digits:
+                curr += xs[i]
+                i += 1
+            result.append(int(curr))
+            assert xs[i] == ","
+            result.append(",")
+            i += 1
+            curr = ""
+            while xs[i] in string.digits:
+                curr += xs[i]
+                i += 1
+            result.append(int(curr))
+        else:
+            result.append(xs[i])
+            i += 1
+    return result
+
+def parse(expr):
+    """
+    Tokenize `expr` and convert it into a parse-tree.
+    """
+    tokens = tokenize(expr)
+    return parse_tokens(tokens)
+
+def compile(xs):
+    """
+    Transform `xs`, a parse-tree representing a regex, into a function that
+    accepts a string, and returns the substring that the regex matches.
+    """
+    def fn(input):
+        match = ""
+        i = 0
+        for x in xs:
+            matches, q = x[1], x[2]
+            lo, hi = q[1], q[2]
+            for j in range(lo):
+                if i < len(input) and input[i] in matches:
+                    match += input[i]
+                    i += 1
+                else:
+                    print("Failed to match {} with {}".format(input[i], matches))
+                    return None
+            if hi == float('inf'):
+                while i < len(input) and input[i] in matches:
+                    match += input[i]
+                    i += 1
+            else:
+                for j in range(hi - lo):
+                    if i < len(input) and input[i] in matches:
+                        match += input[i]
+                        i += 1
+        return match
+    return fn
+
+################################################################################
+# Helper Functions
+################################################################################
+
+def parse_tokens(tokens):
+    result = []
+    parser = Parser(tokens)
+    while not parser.exhausted():
+        result.append(parse_expression(parser))
+    return result
+
+def parse_expression(parser):
+    if parser.curr() == "[":
+        return parse_character_class(parser)
+    else:
+        return parse_character(parser)
+
+def parse_character_class(parser):
+    parser.expect("[")
+    beg = parser.consume()
+    parser.expect("-")
+    end = parser.consume()
+    parser.expect("]")
+    if parser.curr() == "{":
+        q = parse_quantifier(parser)
+    return char_class(xs=expand_range(beg, end), q=q)
+
+def parse_quantifier(parser):
+    parser.expect("{")
+    if parser.match([","]):
+        end = parser.consume()
+        parser.expect("}")
+        return quantifier(beg=0, end=end)
+    else:
+        beg = parser.consume()
+        parser.expect(",")
+        if parser.match(["}"]):
+            return quantifier(beg=beg)
+        else:
+            end = parser.consume()
+            parser.expect("}")
+            return quantifier(beg=beg, end=end)
+
+def parse_character(parser):
+    c = parser.consume()
+    q = None
+    if parser.curr() == "{":
+        q = parse_quantifier(parser)
+    return char_class(xs={c}, q=q)
+
+def char_class(xs=set(), q=None):
+    if not q:
+        q = quantifier(beg=1, end=1)
+    return ["CHARACTER_CLASS", xs, q]
+
+def expand_range(beg, end):
+    # TODO: Implement this
+    return {string.printable[i]
+            for i in range(string.printable.index(beg),
+                           string.printable.index(end) + 1)}
+
+def quantifier(beg=0, end=float('inf')):
+    return ['QUANTIFIER', beg, end]
+
+################################################################################
+# Tests
+################################################################################
+
+xs = [
+    ("[a-c]*[0-9]{2,3}", ["dog"]),
+    ("ca+t?", ["cat", "caaaat", "ca", "dog"]),
+]
+
+for re, inputs in xs:
+    print("Regex:  {}".format(re))
+    print("Tokens: {}".format(tokenize(re)))
+    print("Parsed: {}".format(parse(re)))
+    print("\nTESTS")
+    for input in inputs:
+        print("Attempting to match \"{}\"...".format(input))
+        parser = compile(parse(re))
+        print("Result: \"{}\"\n".format(parser(input)))
diff --git a/users/wpcarro/scratch/facebook/permutation-palindrome.py b/users/wpcarro/scratch/facebook/permutation-palindrome.py
new file mode 100644
index 0000000000..30603578ff
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/permutation-palindrome.py
@@ -0,0 +1,17 @@
+from collections import Counter
+
+def is_palindrome(x):
+    return len([count for _, count in Counter(x).items() if count % 2 == 1]) <= 1
+
+
+xs = [("civic", True),
+      ("ivicc", True),
+      ("civil", False),
+      ("livci", False)]
+
+for x, expected in xs:
+    result = is_palindrome(x)
+    print(x)
+    print(result)
+    assert result == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/polynomial-rolling-hash.py b/users/wpcarro/scratch/facebook/polynomial-rolling-hash.py
new file mode 100644
index 0000000000..0c7b7cb5a0
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/polynomial-rolling-hash.py
@@ -0,0 +1,72 @@
+def compute_hash(x):
+    """
+    Compute a unique fingerprint for the string input, `x`, as an integer using
+    the following equation:
+
+    x[0] * P^0 + x[1] * P^1 + ... x[n-1] * P^(n-1) % M
+
+    P and M are constants where P represents the next available prime number
+    that's GTE the number of unique characters you'll be hashing. In the case of
+    all lowercase characters, of which there are 26, the next available prime
+    number is 31.
+    """
+    p = 31
+    m = int(10e9) + 9 # large prime number
+    power = 0
+    result = 0
+    for c in x:
+        result += ord(c) * p**power
+        power += 1
+    return result % m
+
+class HashTable(object):
+    def __init__(self, size):
+        """
+        Create a hash table with `size` buckets.
+        """
+        buckets = []
+        for _ in range(size):
+            buckets.append([])
+        self.xs = buckets
+        self.compute_hash = lambda k: compute_hash(k) % size
+
+    def __repr__(self):
+        result = []
+        for bucket in self.xs:
+            for entry in bucket:
+                result.append(entry)
+        return "HashTable({})".format(",".join(str(x) for x in result))
+
+    def get(self, key):
+        """
+        Attempt to retrieve value stored under `key`.
+        """
+        h = self.compute_hash(key)
+        for k, v in self.xs[h]:
+            if k == key:
+                return v
+        return None
+
+    def put(self, key, val):
+        """
+        Set `key` to `val`; update value at `key` if it already exists.
+        """
+        h = self.compute_hash(key)
+        for i in range(len(self.xs[h])):
+            # Update entry if the key exists...
+            if self.xs[h][i][0] == key:
+                self.xs[h][i] = (key, val)
+                return None
+        # ...create a new entry otherwise
+        self.xs[h].append((key, val))
+
+    def delete(self, key):
+        """
+        Remove entry `key` from the hash table.
+        """
+        h = self.compute_hash(key)
+        for i in range(len(self.xs[h])):
+            k, v = self.xs[h][i]
+            if k == key:
+                self.xs[h].remove((k, v))
+                return
diff --git a/users/wpcarro/scratch/facebook/product-of-all-other-numbers.py b/users/wpcarro/scratch/facebook/product-of-all-other-numbers.py
new file mode 100644
index 0000000000..d381386b62
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/product-of-all-other-numbers.py
@@ -0,0 +1,33 @@
+from random import randint
+from math import floor
+
+# loop {forwards, backwards, up, down}
+# through a table of values, [[a]].
+
+def product(xs):
+    n = len(xs)
+    lhs = [1] * (n + 1)
+    for i in range(1, n):
+        lhs[i] = lhs[i - 1] * xs[i - 1]
+    rhs = [1] * (n + 1)
+    for i in range(n - 1, 0, -1):
+        rhs[i] = rhs[i + 1] * xs[i]
+    result = []
+    for i in range(n):
+        result.append(lhs[i] * rhs[i + 1])
+    return result
+
+def computed_expected(xs):
+    product = 1
+    for x in xs:
+        product *= x
+    return [floor(product / x) for x in xs]
+
+xs = [randint(1, 10) for _ in range(5)]
+expected = computed_expected(xs)
+result = product(xs)
+print(xs, result, expected)
+assert result == expected
+print("Success!")
+
+print(product([2, 4, 3, 10, 5]))
diff --git a/users/wpcarro/scratch/facebook/queue-two-stacks.py b/users/wpcarro/scratch/facebook/queue-two-stacks.py
new file mode 100644
index 0000000000..a71abeb005
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/queue-two-stacks.py
@@ -0,0 +1,20 @@
+from stack import Stack
+
+class Queue(object):
+    def __init__(self):
+        self.lhs = Stack()
+        self.rhs = Stack()
+
+    def enqueue(self, x):
+        self.rhs.push(x)
+
+    def dequeue(self, x):
+        y = self.rhs.pop()
+        while y:
+            self.lhs.push(y)
+            y = self.rhs.pop()
+        result = self.lhs.pop()
+        y = self.lhs.pop()
+        while y:
+            self.rhs.push(y)
+        return result
diff --git a/users/wpcarro/scratch/facebook/rabin-karp.py b/users/wpcarro/scratch/facebook/rabin-karp.py
new file mode 100644
index 0000000000..53a47b2783
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/rabin-karp.py
@@ -0,0 +1,27 @@
+def substring_exists(corpus, pattern):
+    """
+    Return True if `pattern` appears in `corpus`.
+
+    This function runs in O(m) time where n is equal to the length of
+    `corpus`. To improve the efficiency of this algorithm, use a hashing
+    function the reduces the number of collisions, which will consequently
+    reduce the number of string-to-string, linear comparisons.
+    """
+    m, n = len(corpus), len(pattern)
+    a = sum(ord(c) for c in corpus[0:n])
+    b = sum(ord(c) for c in pattern)
+
+    # (clumsily) prevent an off-by-one error...
+    if a == b and corpus[0:n] == pattern:
+        return True
+
+    for i in range(1, m - n):
+        # Update the hash of corpus by subtracting the hash of the character
+        # that is sliding out of view and adding the hash of the character that
+        # is sliding into view.
+        a = a - ord(corpus[i - 1]) + ord(corpus[i + n - 1])
+        # Integer comparison in O(0) time followed by string comparison in O(m)
+        # time.
+        if a == b and corpus[i:i + n] == pattern:
+            return True
+    return False
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/magic-index.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/magic-index.py
new file mode 100644
index 0000000000..03b2de015d
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/magic-index.py
@@ -0,0 +1,33 @@
+from math import floor
+
+def find_magic_index_brute(xs):
+    for i in range(len(xs)):
+        if xs[i] == i:
+            return i
+    return -1
+
+def mid(lo, hi):
+    return lo + floor((hi - lo) / 2)
+
+def find_magic_index(xs):
+    lo, hi = 0, len(xs) - 1
+    return do_find_magic_index(xs, 0, len(xs) - 1)
+
+def do_find_magic_index(xs, lo, hi):
+    pass
+
+xss = [
+    [],
+    [-1,0,2,4,5,6],
+    [1,1,1,1,1,5],
+    [-2,-2,-2,-2,4],
+    [1,2,3,4,5],
+]
+
+for xs in xss:
+    print(xs)
+    a = find_magic_index_brute(xs)
+    b = find_magic_index(xs)
+    print(a, b)
+    assert a == b
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/making-change.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/making-change.py
new file mode 100644
index 0000000000..30c95a66c3
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/making-change.py
@@ -0,0 +1,56 @@
+# Given an infinite supply of:
+#   - quarters
+#   - dimes
+#   - nickels
+#   - pennies
+# Write a function to count the number of ways to make change of n.
+
+def get(table, row, col):
+    """
+    Defensively get cell `row`, `col` from `table`.
+    """
+    if row < 0 or row >= len(table):
+        return 0
+    if col < 0 or col >= len(table[0]):
+        return 0
+    return table[row][col]
+
+def print_table(table):
+    print('\n'.join([
+        ','.join([str(col) for col in table[row]])
+        for row in range(len(table))]))
+
+def init_table(rows=0, cols=0, default=0):
+    result = []
+    for row in range(rows):
+        r = []
+        for col in range(cols):
+            r.append(default)
+        result.append(r)
+    return result
+
+def make_change(n):
+    coins = [1, 5, 10, 25]
+    table = init_table(rows=len(coins), cols=n)
+
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            curr_coin = coins[row]
+            curr_n = col + 1
+            # a
+            a = get(table, row - 1, col)
+            # b
+            b = get(table, row, curr_n - curr_coin - 1)
+            # c
+            c = 1 if curr_coin <= curr_n else 0
+            # commit
+            if curr_coin == curr_n:
+                table[row][col] = a + c
+            else:
+                table[row][col] = a + b * c
+            # debug
+            print_table(table)
+            print()
+    return table[-1][-1]
+
+print(make_change(7))
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/paint-fill.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/paint-fill.py
new file mode 100644
index 0000000000..e9e7f6a9c1
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/paint-fill.py
@@ -0,0 +1,36 @@
+from collection import deque
+
+def fill(point, canvas, color):
+    if x not in canvas:
+        return
+    elif y not in canvas[x]:
+        return
+
+    x, y = point
+    if canvas[y][x] == color:
+        return
+    canvas[y][x] = color
+    fill((x + 1, y), canvas, color)
+    fill((x - 1, y), canvas, color)
+    fill((x, y + 1), canvas, color)
+    fill((x, y - 1), canvas, color)
+
+def fill_bfs(point, canvas, color):
+    x, y = point
+    if x not in canvas:
+        return None
+    if y not in canvas[x]:
+        return None
+    xs = deque()
+    xs.append((x, y))
+    while xs:
+        x, y = xs.popleft()
+        canvas[y][x] = color
+        for x2, y2 in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:
+            if x2 not in canvas:
+                continue
+            elif y2 not in canvas[x2]:
+                continue
+            if canvas[y2][x2] != color:
+                xs.append((x2, y2))
+    return None
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/parenthesize-bools.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/parenthesize-bools.py
new file mode 100644
index 0000000000..f406d64e65
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/parenthesize-bools.py
@@ -0,0 +1,114 @@
+# BNF
+# expression -> bool ( ( '|' | '&' | '^' ) bool )*
+# bool       -> '0' | '1'
+
+def tokenize(xs):
+    result = []
+    for c in xs:
+        if c == '0':
+            result.append(0)
+        elif c == '1':
+            result.append(1)
+        elif c in "&|^":
+            result.append(c)
+        else:
+            raise Exception("Unexpected token, \"{}\"".format(c))
+    return result
+
+class Parser(object):
+    def __init__(self, tokens):
+        self.tokens = tokens
+        self.i = 0
+
+    def prev(self):
+        return self.tokens[self.i - 1]
+
+    def curr(self):
+        return self.tokens[self.i]
+
+    def match(self, xs):
+        if self.exhausted():
+            return False
+        if (self.curr() in xs):
+            self.consume()
+            return True
+        return False
+
+    def consume(self):
+        result = self.curr()
+        self.i += 1
+        return result
+
+    def exhausted(self):
+        return self.i >= len(self.tokens)
+
+def recursive_descent(tokens):
+    parser = Parser(tokens)
+    return parse_expression(parser)
+
+def parse_expression(parser):
+    lhs = parse_bool(parser)
+    while parser.match(['|', '&', '^']):
+        op = parser.prev()
+        rhs = parse_expression(parser)
+        lhs = [op, lhs, rhs]
+    return lhs
+
+def parse_bool(parser):
+    if parser.curr() == 0:
+        parser.consume()
+        return False
+    elif parser.curr() == 1:
+        parser.consume()
+        return True
+    else:
+        raise Exception("Unexpected token: {}".format(parser.curr()))
+
+def f(expr, result):
+    tokens = tokenize(expr)
+    tree = recursive_descent(tokens)
+    return do_f(tree, result)
+
+def do_f(tree, result):
+    if type(tree) == bool:
+        if tree == result:
+            return 1
+        else:
+            return 0
+
+    op, lhs, rhs = tree[0], tree[1], tree[2]
+    truth_tables = {
+        True: {
+            '|': [
+                (True, True),
+                (True, False),
+                (False, True),
+            ],
+            '&': [
+                (True, True),
+            ],
+            '^': [
+                (True, False),
+                (False, True),
+            ],
+        },
+        False: {
+            '|': [
+                (False, False),
+            ],
+            '&': [
+                (False, False),
+                (True, False),
+                (False, True),
+            ],
+            '^': [
+                (True, True),
+                (False, False),
+            ],
+        }
+    }
+
+    return sum([do_f(lhs, x) * do_f(rhs, y) for x, y in truth_tables[result][op]])
+
+print(f("1^0|0|1", False))
+print(f("1|0|1|1", False))
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/permutations.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/permutations.py
new file mode 100644
index 0000000000..e23972d418
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/permutations.py
@@ -0,0 +1,13 @@
+def char_and_rest(i, xs):
+    return xs[i], xs[:i] + xs[i+1:]
+
+# perms :: String -> [String]
+def perms(xs):
+    if len(xs) == 1:
+        return [xs]
+    result = []
+    for c, rest in [char_and_rest(i, xs) for i in range(len(xs))]:
+        result += [c + perm for perm in perms(rest)]
+    return result
+
+print(perms("cat"))
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/robot-grid-traversal.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/robot-grid-traversal.py
new file mode 100644
index 0000000000..9ccc08526a
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/robot-grid-traversal.py
@@ -0,0 +1,28 @@
+import random
+
+def factorial(n):
+    result = 1
+    for i in range(1, n + 1):
+        result *= i
+    return result
+
+def travel(a, b):
+    if a == b:
+        return 1
+
+    ax, ay = a
+    bx, by = b
+    if ax > bx or ay > by:
+        return 0
+
+    return sum([travel((ax + 1, ay), b), travel((ax, ay + 1), b)])
+
+def travel_compute(a, b):
+    bx, by = b
+    return int(factorial(bx + by) / (factorial(bx) * factorial(by)))
+
+a = (0, 0)
+b = (random.randint(1, 10), random.randint(1, 10))
+print("Travelling to {}, {}".format(b[0], b[1]))
+print(travel(a, b))
+print(travel_compute(a, b))
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/staircase.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/staircase.py
new file mode 100644
index 0000000000..5eb4a85606
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/staircase.py
@@ -0,0 +1 @@
+# accidentally deleted my solution... TBI (again)
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/subsets.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/subsets.py
new file mode 100644
index 0000000000..a6d26aa850
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/subsets.py
@@ -0,0 +1,41 @@
+# take-aways:
+#   - Use integers as lists of boolean values
+#   - Use 1 << n to compute 2^n where n = len(xs)
+
+def set_from_int(xs, n):
+    result = []
+    for i in range(len(xs)):
+        if n & (1 << i) != 0:
+            result.append(xs[i])
+    return result
+
+# subsets :: Set a -> List (Set a)
+def subsets(xs):
+    n = len(xs)
+    return [set_from_int(xs, i) for i in range(1 << n)]
+
+#   0 1 2
+# 0 N Y Y
+# 1 _ N Y
+# 2 _ _ N
+
+# For my interview, be able to compute *permutations* and *combinations*
+
+# This differs from permutations because this is about finding combinations...
+#
+# bottom-up
+# 0 =>        { }
+# 1 =>  {3}   {4}   {3}
+# 2 => {5,4} {5,3} {4,3}
+
+xs = [
+    ([], [[]]),
+    ([5], [[], [5]]),
+    ([5,4], [[],[5],[4],[5,4]]),
+]
+
+for x, expected in xs:
+    result = subsets(x)
+    print("subsets({}) => {} == {}".format(x, result, expected))
+    assert result == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/valid-parens.py b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/valid-parens.py
new file mode 100644
index 0000000000..56f2c0b274
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursion-and-dynamic-programming/valid-parens.py
@@ -0,0 +1,50 @@
+def valid_parens(n):
+    if n == 0:
+        return []
+    if n == 1:
+        return ["()"]
+
+    result = set()
+    for x in valid_parens(n - 1):
+        result.add("({})".format(x))
+        result.add("(){}".format(x))
+        result.add("{}()".format(x))
+    return result
+
+def valid_parens_efficient(n):
+    result = []
+    curr = [''] * n**2
+    do_valid_parens_efficient(result, curr, 0, n, n)
+    return result
+
+def do_valid_parens_efficient(result, curr, i, lhs, rhs):
+    if lhs == 0 and rhs == 0:
+        result.append(''.join(curr))
+    else:
+        if lhs > 0:
+            curr[i] = '('
+            do_valid_parens_efficient(result, curr, i + 1, lhs - 1, rhs)
+        if rhs > lhs:
+            curr[i] = ')'
+            do_valid_parens_efficient(result, curr, i + 1, lhs, rhs - 1)
+
+# Avoids recursion by using either a stack or a queue. I think this version is
+# easier to understand.
+def valid_parens_efficient_2(n):
+    result = []
+    xs = []
+    xs.append(('', n, n))
+    while xs:
+        curr, lhs, rhs = xs.pop()
+        print(curr)
+        if lhs == 0 and rhs == 0:
+            result.append(''.join(curr))
+        if lhs > 0:
+            xs.append((curr + '(', lhs - 1, rhs))
+        if rhs > lhs:
+            xs.append((curr + ')', lhs, rhs - 1))
+    return result
+
+# print(valid_parens(4))
+print(valid_parens_efficient(3))
+print(valid_parens_efficient_2(3))
diff --git a/users/wpcarro/scratch/facebook/recursive-string-permutations.py b/users/wpcarro/scratch/facebook/recursive-string-permutations.py
new file mode 100644
index 0000000000..e4c61eff9f
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/recursive-string-permutations.py
@@ -0,0 +1,19 @@
+# permutations: no repeat characters
+
+def char_and_rest(i, xs):
+    return xs[i], xs[0:i] + xs[i + 1:]
+
+def permutations(xs):
+    if len(xs) == 1:
+        return [xs]
+    result = []
+    for c, rest in [char_and_rest(i, xs) for i in range(len(xs))]:
+        result += [c + perm for perm in permutations(rest)]
+    return result
+
+expected = ["cat", "cta", "act", "atc", "tca", "tac"]
+result = permutations("cat")
+print(result, expected)
+assert len(result) == len(expected)
+assert result == expected
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/reverse-linked-list.py b/users/wpcarro/scratch/facebook/reverse-linked-list.py
new file mode 100644
index 0000000000..820726733f
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/reverse-linked-list.py
@@ -0,0 +1,25 @@
+from linked_list import Node
+
+def reverse(node):
+    prev, curr, next = None, node, node.next
+
+    while curr:
+        curr.next = prev
+        prev = curr
+        curr = next
+        next = curr.next if curr else None
+    return prev
+
+one = Node(1)
+two = Node(2)
+three = Node(3)
+one.next = two
+two.next = three
+
+print(one)
+result = reverse(one)
+print(result)
+assert all([result == three,
+            three.next == two,
+            two.next == one])
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/reverse-string-in-place.py b/users/wpcarro/scratch/facebook/reverse-string-in-place.py
new file mode 100644
index 0000000000..72cd6c27a3
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/reverse-string-in-place.py
@@ -0,0 +1,14 @@
+# reverse :: [Char] -> ()
+def reverse(xs):
+    i = 0
+    j = len(xs) - 1
+    while i < j:
+        xs[i], xs[j] = xs[j], xs[i]
+        i += 1
+        j -= 1
+
+xs = [list("testing"), list("a"), list("to")]
+for x in xs:
+    print(x)
+    reverse(x)
+    print(x)
diff --git a/users/wpcarro/scratch/facebook/reverse-words.py b/users/wpcarro/scratch/facebook/reverse-words.py
new file mode 100644
index 0000000000..5a38b828a3
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/reverse-words.py
@@ -0,0 +1,8 @@
+# reverse_word :: [Char] -> ()
+def reverse_words(x):
+    pass
+
+x = list("This is a test")
+print(''.join(x))
+reverse_words(x)
+print(''.join(result))
diff --git a/users/wpcarro/scratch/facebook/scratch.py b/users/wpcarro/scratch/facebook/scratch.py
new file mode 100644
index 0000000000..e772d75847
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/scratch.py
@@ -0,0 +1,94 @@
+# This is a scratch pad for randomly selected questions
+
+# def char_and_rest(i, xs):
+#     return xs[i], xs[:i] + xs[i+1:]
+
+# def perms(xs):
+#     if len(xs) == 1:
+#         return [xs]
+#     result = []
+#     for i in range(len(xs)):
+#         c, rest = char_and_rest(i, xs)
+#         for perm in perms(rest):
+#             result.append(c + ''.join(perm))
+#     return result
+
+# print(perms(list("woah")))
+
+# def f(take_out, dine_in, served):
+#     j, k = 0, 0
+#     for i in range(len(served)):
+#         if j < len(take_out) and served[i] == take_out[j]:
+#             j += 1
+#         elif k < len(dine_in) and served[i] == dine_in[k]:
+#             k += 1
+#         else:
+#             return False
+#     if j < len(take_out) or k < len(dine_in):
+#         return False
+#     return True
+
+# take_out = [17, 8, 24]
+# dine_in = [12, 19, 2]
+# served = [17, 8, 12, 19, 24, 2]
+# print(f(take_out, dine_in, served))
+
+# def match(a, b):
+#     if a == '{':
+#         return b == '}'
+#     if a == '[':
+#         return b == ']'
+#     if a == '(':
+#         return b == ')'
+#     return False
+
+# def f(xs):
+#     s = []
+#     for c in xs:
+#         if c in {'{', '[', '('}:
+#             s.append(c)
+#         elif c in {'}', ']', ')'}:
+#             opener = s.pop()
+#             if not match(opener, c):
+#                 return False
+#     return len(s) == 0
+
+# assert f("{[]()}")
+# assert f("{[(])}") == False
+# assert f("{[}") == False
+# print("Success!")
+
+# def valid_bst(node):
+#     lhs = max_bst_value(node.left) if node.left else float('-inf')
+#     rhs = min_bst_value(node.right) if node.right else float('inf')
+
+#     return and([
+#         lhs <= node.value,
+#         rhs > node.value,
+#         valid_bst(node.left),
+#         valid_bst(node.right),
+#     ])
+
+import random
+import math
+
+def shuffle(xs):
+    n = len(xs)
+    for i in range(n - 1):
+        j = random.randint(i + 1, n - 1)
+        xs[i], xs[j] = xs[j], xs[i]
+    return xs
+
+def as_card(i):
+    if i not in range(1, 53):
+        raise Exception("Not a card")
+    # 1
+    suit = ['Hearts', 'Clubs', 'Diamonds', 'Spades'][math.floor((i - 1) / 13)]
+    n = ['Ace',2,3,4,5,6,7,8,9,10,'Jack','Queen','King'][(i - 1) % 13]
+    return '{} of {}'.format(n, suit)
+
+xs = list(range(1, 53))
+print(xs)
+shuffle(xs)
+for x in xs:
+    print(as_card(x))
diff --git a/users/wpcarro/scratch/facebook/second-largest-item-in-bst.py b/users/wpcarro/scratch/facebook/second-largest-item-in-bst.py
new file mode 100644
index 0000000000..2815dec9ee
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/second-largest-item-in-bst.py
@@ -0,0 +1,22 @@
+from collections import deque
+from node import Node, tree
+
+def find_largest(node):
+    while node.right:
+        node = node.right
+    return node.value
+
+def find_second_largest(node):
+    # parent of the rightmost, when rightmost is leaf
+    # max(rightmost.left)
+    prev = None
+    while node.right:
+        prev = node
+        node = node.right
+    if node.left:
+        return find_largest(node.left)
+    else:
+        return prev.value
+
+assert find_second_largest(tree) == 72
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/shuffle.py b/users/wpcarro/scratch/facebook/shuffle.py
new file mode 100644
index 0000000000..21a6a96c60
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/shuffle.py
@@ -0,0 +1,17 @@
+from random import randint
+
+def get_random(i, j):
+    return randint(i, j)
+
+def shuffle(xs):
+    for i in range(len(xs)):
+        j = get_random(i, len(xs) - 1)
+        xs[i], xs[j] = xs[j], xs[i]
+
+xs = list(range(1, 53))
+print(xs)
+assert len(set(xs)) == 52
+shuffle(xs)
+assert len(set(xs)) == 52
+print(xs)
+print("Success!")
diff --git a/users/wpcarro/scratch/facebook/stack.py b/users/wpcarro/scratch/facebook/stack.py
new file mode 100644
index 0000000000..2a843e2216
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/stack.py
@@ -0,0 +1,25 @@
+class Stack(object):
+    def __init__(self):
+        self.items = []
+
+    def __repr__(self):
+        return self.items.__repr__()
+
+    def push(self, x):
+        self.items.append(x)
+
+    def pop(self):
+        if not self.items:
+            return None
+        return self.items.pop()
+
+    def peek(self):
+        if not self.items:
+            return None
+        return self.items[-1]
+
+def from_list(xs):
+    result = Stack()
+    for x in xs:
+        result.push(x)
+    return result
diff --git a/users/wpcarro/scratch/facebook/stacking-boxes.py b/users/wpcarro/scratch/facebook/stacking-boxes.py
new file mode 100644
index 0000000000..7a3304bc51
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/stacking-boxes.py
@@ -0,0 +1,50 @@
+from random import randint
+
+class Box(object):
+    def __init__(self, w, h, d):
+        self.width  = w
+        self.depth  = d
+        self.height = h
+
+    def __repr__(self):
+        return "{}x{}x{}".format(self.width, self.depth, self.height)
+
+    def lt(self, b):
+        return all([
+            self.width  < b.width,
+            self.height < b.height,
+            self.depth  < b.depth,
+        ])
+
+    def gt(self, b):
+        return all([
+            self.width  > b.width,
+            self.height > b.height,
+            self.depth  > b.depth,
+        ])
+
+def random_box():
+    return Box(
+        randint(1, 10),
+        randint(1, 10),
+        randint(1, 10),
+    )
+
+xs = [random_box() for _ in range(5)]
+
+def highest_stack(xs, cache={}):
+    if not xs:
+        return 0
+    heights = []
+    for i in range(len(xs)):
+        x, rest = xs[i], xs[0:i] + xs[i+1:]
+        if cache and x in cache:
+            height = cache[x]
+        else:
+            height = x.height + highest_stack([b for b in rest if x.gt(b)], cache)
+            cache[x] = height
+        heights += [height]
+    return max(heights)
+
+print(xs)
+print(highest_stack(xs))
diff --git a/users/wpcarro/scratch/facebook/stock-price.py b/users/wpcarro/scratch/facebook/stock-price.py
new file mode 100644
index 0000000000..8e42f81523
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/stock-price.py
@@ -0,0 +1,16 @@
+def max_profit(xs):
+    buy = xs[0]
+    profit = xs[1] - xs[0]
+    for price in xs[1:]:
+        profit = max(profit, price - buy)
+        buy = min(buy, price)
+    return profit
+
+xs = [([10,7,5,8,11,9], 6),
+      ([10,8,7,6,5], -1)]
+
+for x, expected in xs:
+    result = max_profit(x)
+    print(x, result)
+    assert result == expected
+    print("Success!")
diff --git a/users/wpcarro/scratch/facebook/todo.org b/users/wpcarro/scratch/facebook/todo.org
new file mode 100644
index 0000000000..6ac99267db
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/todo.org
@@ -0,0 +1,60 @@
+* Array and string manipulation
+** DONE Merging Meeting Times
+** DONE Reverse String in Place
+** TODO Reverse Words
+** DONE Merge Sorted Arrays
+** DONE Cafe Order Checker
+* Hashing and hash tables
+** DONE Inflight Entertainment
+** DONE Permutation Palindrome
+** DONE Word Cloud Data
+** DONE Top Scores
+* Greedy Algorithms
+** DONE Apple Stocks
+** DONE Highest Product of 3
+** DONE Product of All Other Numbers
+** DONE Cafe Order Checker
+** DONE In-Place Shuffle
+* Sorting, searching, and logarithms
+** DONE Find Rotation Point
+** TODO Find Repeat, Space Edition
+** DONE Top Scores
+** DONE Merging Meeting Times
+* Trees and graphs
+** DONE Balanced Binary Tree
+** DONE Binary Search Tree Checker
+** DONE 2nd Largest Item in a Binary Search Tree
+** DONE Graph Coloring
+** DONE MeshMessage
+** DONE Find Repeat, Space Edition BEAST MODE
+* Dynamic programming and recursion
+** DONE Recursive String Permutations
+** DONE Compute nth Fibonacci Number
+** DONE Making Change
+** DONE The Cake Thief
+** DONE Balanced Binary Tree
+** DONE Binary Search Tree Checker
+** DONE 2nd Largest Item in a Binary Search Tree
+* Queues and stacks
+** DONE Largest Stack
+** DONE Implement A Queue With Two Stacks
+** DONE Parenthesis Matching
+** DONE Bracket Validator
+* Linked lists
+** DONE Delete Node
+** DONE Does This Linked List Have A Cycle?
+** DONE Reverse A Linked List
+** DONE Kth to Last Node in a Singly-Linked List
+** DONE Find Repeat, Space Edition BEAST MODE
+* General programming
+** TODO Rectangular Love
+** TODO Temperature Tracker
+* Bit manipulation
+** DONE The Stolen Breakfast Drone
+* Combinatorics, probability, and other math
+** TODO Which Appears Twice
+** TODO Find in Ordered Set
+** TODO In-Place Shuffle
+** TODO Simulate 5-sided die
+** TODO Simulate 7-sided die
+** TODO Two Egg Problem
diff --git a/users/wpcarro/scratch/facebook/top-scores.py b/users/wpcarro/scratch/facebook/top-scores.py
new file mode 100644
index 0000000000..c8a10ae5f1
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/top-scores.py
@@ -0,0 +1,20 @@
+import random
+from collections import deque
+
+def sorted(xs):
+    result = [0] * 100
+    for x in xs:
+        result[x - 1] += 1
+
+    answer = deque()
+    for i in range(len(result)):
+        x = result[i]
+        for _ in range(x):
+            answer.appendleft(i + 1)
+
+    return list(answer)
+
+scores = [random.choice(range(70, 100)) for _ in range(20)]
+print(scores)
+result = sorted(scores)
+print(result)
diff --git a/users/wpcarro/scratch/facebook/topo-sort.py b/users/wpcarro/scratch/facebook/topo-sort.py
new file mode 100644
index 0000000000..874005a019
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/topo-sort.py
@@ -0,0 +1,61 @@
+import random
+from heapq import heappush, heappop
+from collections import deque
+
+# A topological sort returns the vertices of a graph sorted in an ascending
+# order by the number of incoming edges each vertex has.
+#
+# A few algorithms for solving this exist, and at the time of this writing, I
+# know none. I'm going to focus on two:
+#   1. Kahn's
+#   2. DFS (TODO)
+
+def count_in_edges(graph):
+    result = {k: 0 for k in graph.keys()}
+    for xs in graph.values():
+        for x in xs:
+            result[x] += 1
+    return result
+
+# Kahn's algorithm for returning a topological sorting of the vertices in
+# `graph`.
+def kahns_sort(graph):
+    result = []
+    q = deque()
+    in_edges = count_in_edges(graph)
+    for x in [k for k, v in in_edges.items() if v == 0]:
+        q.append(x)
+    while q:
+        x = q.popleft()
+        result.append(x)
+        for c in graph[x]:
+            in_edges[c] -= 1
+            if in_edges[c] == 0:
+                q.append(c)
+    return result
+
+graphs = [
+    {
+        0: [],
+        1: [],
+        2: [3],
+        3: [1],
+        4: [0, 1],
+        5: [0, 2],
+    },
+    {
+        'A': ['C', 'D'],
+        'B': ['D', 'E'],
+        'C': [],
+        'D': ['F', 'G'],
+        'E': [],
+        'F': [],
+        'G': ['I'],
+        'H': ['I'],
+        'I': [],
+    }
+]
+
+print("--- Kahn's --- ")
+for graph in graphs:
+    print(kahns_sort(graph))
diff --git a/users/wpcarro/scratch/facebook/traversals.py b/users/wpcarro/scratch/facebook/traversals.py
new file mode 100644
index 0000000000..e2565a3231
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/traversals.py
@@ -0,0 +1,100 @@
+from math import floor
+
+# Lists
+def cycle_backwards(times, xs):
+    n = len(xs)
+    for i in range(n * times):
+        print(xs[n - 1 - i % n])
+
+def cycle_forwards(times, xs):
+    n = len(xs)
+    for i in range(n * times):
+        print(xs[i % n])
+
+def backwards(xs):
+    n = len(xs)
+    for i in range(n):
+        print(xs[n - 1 - i])
+
+def forwards(xs):
+    for i in range(len(xs)):
+        print(xs[i])
+
+xs = [2, 5, 6, 9, 12]
+
+print("Forwards")
+forwards(xs)
+print("Backwards")
+backwards(xs)
+print("Cycle forwards")
+cycle_forwards(2, xs)
+print("Cycle backwards")
+cycle_backwards(2, xs)
+
+# Tables
+def tblr(table):
+    for row in range(len(table)):
+        for col in range(len(table[row])):
+            print(table[row][col])
+
+def tbrl(table):
+    for row in range(len(table)):
+        n = len(table[row])
+        for col in range(n):
+            print(table[row][n - 1 - col])
+
+def btlr(table):
+    n = len(table)
+    for row in range(n):
+        for col in range(len(table[row])):
+            print(table[n - 1 - row][col])
+
+def btrl(table):
+    rows = len(table)
+    for row in range(rows):
+        cols = len(table[row])
+        for col in range(cols):
+            print(table[rows - 1 - row][cols - 1 - col])
+
+def special(table):
+    rows = len(table)
+    cols = len(table[0])
+    for col in range(cols):
+        for row in range(rows):
+            print(table[row][col])
+
+def double_bonus(table):
+    rows = len(table)
+    cols = len(table[0])
+    for i in range(rows):
+        row = i
+        for col in range(cols):
+            print(table[row][col % cols])
+            row = (row + 1) % rows
+
+def free(table):
+    rows = len(table)
+    cols = len(table[0])
+    d = rows * cols
+    for i in range(d):
+        row = floor((i % d) / cols)
+        col = i % cols
+        print(table[row][col])
+
+table = [[1,2,3,4],
+         [5,6,7,8]]
+
+print("Top->Bottom, Left->Right")
+tblr(table)
+print("Top->Bottom, Right->Left")
+tbrl(table)
+print("Bottom->Top, Left->Right")
+btlr(table)
+print("Bottom->Top, Right->Left")
+btrl(table)
+print("Special")
+special(table)
+print("2x Bonus")
+double_bonus(table)
+print("Free")
+free(table)
diff --git a/users/wpcarro/scratch/facebook/utils.py b/users/wpcarro/scratch/facebook/utils.py
new file mode 100644
index 0000000000..9a3e8a045e
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/utils.py
@@ -0,0 +1,19 @@
+def init_table(rows=0, cols=0, default=None):
+    table = []
+    for row in range(rows):
+        x = []
+        for col in range(cols):
+            x.append(default)
+        table.append(x)
+    return table
+
+def get(table, row, col, default=0):
+    if row < 0 or col < 0:
+        return default
+    return table[row][col]
+
+def print_table(table):
+    result = []
+    for row in range(len(table)):
+        result.append(' '.join([str(cell) for cell in table[row]]))
+    print('\n'.join(result))
diff --git a/users/wpcarro/scratch/facebook/word-cloud.py b/users/wpcarro/scratch/facebook/word-cloud.py
new file mode 100644
index 0000000000..88422e3631
--- /dev/null
+++ b/users/wpcarro/scratch/facebook/word-cloud.py
@@ -0,0 +1,32 @@
+def normalize(x):
+    noise = ".,;-"
+    for y in noise:
+        if x.endswith(y):
+            return normalize(x[0:-1])
+        if x.startswith(y):
+            return normalize(x[1:])
+    return x.lower()
+
+def word_cloud(xs):
+    result = dict()
+
+    for x in xs.split(' '):
+        k = normalize(x)
+        if k in result:
+            result[k] += 1
+        else:
+            result[k] = 1
+
+    return result
+
+result = word_cloud("This is just the beginning. The UK will lockdown again.")
+assert result.get('this') == 1
+assert result.get('is') == 1
+assert result.get('just') == 1
+assert result.get('the') == 2
+assert result.get('beginning') == 1
+assert result.get('uk') == 1
+assert result.get('will') == 1
+assert result.get('lockdown') == 1
+assert result.get('again') == 1
+print("Success!")
diff --git a/users/wpcarro/scratch/groceries/.envrc b/users/wpcarro/scratch/groceries/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/scratch/groceries/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/scratch/groceries/export.hs b/users/wpcarro/scratch/groceries/export.hs
new file mode 100644
index 0000000000..ed43c9a3e8
--- /dev/null
+++ b/users/wpcarro/scratch/groceries/export.hs
@@ -0,0 +1,22 @@
+module Main where
+
+import qualified Data.List as L
+
+(|>) :: a -> (a -> b) -> b
+x |> f = f x
+
+-- | Ignore items with zero quantity (i.e. "0x") and comments (i.e. "#")
+isUndesirableOutput :: String -> Bool
+isUndesirableOutput x =
+  (L.isPrefixOf "- 0x" x) || (L.isPrefixOf "#" x)
+
+-- | Run this to export the grocery list.
+main :: IO ()
+main = do
+  content <- readFile "./list.org"
+  content
+    |> lines
+    |> filter (not . isUndesirableOutput)
+    |> unlines
+    |> putStrLn
+  pure ()
diff --git a/users/wpcarro/scratch/groceries/list.org b/users/wpcarro/scratch/groceries/list.org
new file mode 100644
index 0000000000..a823b2a8eb
--- /dev/null
+++ b/users/wpcarro/scratch/groceries/list.org
@@ -0,0 +1,112 @@
+# The sections are sorted such that the first section is likely the first area
+# in the grocery store you'll encounter.
+#
+# This version is written for Tesco Metro in London Bridge.
+* Beer
+- 0x beer (6x)
+* Bread
+- 0x GF bread
+- 0x flour
+- 0x GF flour
+* Produce
+- 0x brocoli
+- 0x green beans
+- 0x green asparagus
+- 2x spinach greens
+- 0x romaine lettuce head
+- 0x tomatoes
+- 0x zucchini
+- 0x lemons
+- 1x limes
+- 0x large carrot
+- 2x garlic
+- 1x green onions
+- 0x onions
+- 0x avocado
+- 0x basil plant
+- 0x jalapeno
+- 0x red pepper
+- 0x green pepper
+- 0x cherry tomatoes
+- 0x potato
+- 0x bag dry black beans
+- 1x Scotch Bonnet pepper
+* Spices
+- 0x onion powder
+- 0x garlic powder
+- 0x chicken bouillon
+- 0x oregano
+- 0x red pepper flakes
+- 0x basil plant
+- 0x cilantro plant
+* Meat
+- 0x sausages
+- 0x steak
+- 0x chicken breasts
+- 0x chicken legs
+- 0x lamb
+- 0x ground beef
+* Frozen
+- 0x Salmon
+- 0x white fish
+- 0x shrimp
+- 0x bag green beans
+- 1x bag peas
+- 0x bag corn
+* Dairy
+- 1x unsalted butter
+- 0x coconut milk
+- 2x egg cartons (12x each)
+- 2x sour cream
+- 0x cheddar cheese
+- 0x parmesan
+- 0x gouda
+- 0x random cheese
+* Pasta
+- 0x box of quinoa
+- 0x box of rice
+- 1x GF pasta
+- 0x tortellini / ravioli
+- 0x tomato sauce
+- 0x tomato paste
+- 0x can diced tomatoes
+- 0x pesto
+* Oil
+- 0x olive oil
+- 0x sesame oil
+- 0x avocado oil
+- 0x coconut oil
+- 0x white wine vinegar
+* Condiments
+- 0x red Tabasco
+- 0x green Tabasco
+- 0x habanero Tabasco
+- 0x BBQ sauce
+- 0x french mustard
+- 0x ketchup
+- 0x oyster sauce
+- 0x soy sauce
+- 0x Srirachi sauce
+* Nuts
+- 0x almonds
+- 0x walnuts
+- 0x peanuts
+- 0x cashews
+- 0x Brazil nuts
+- 0x mixed nuts
+- 0x peanuts
+- 2x peanut butter
+* Sugar
+- 0x Lindt chocolate
+* Asian
+- 0x red curry
+- 0x green curry
+- 0x coconut cream
+* Wine
+- 0x red wine
+- 0x white wine
+* Miscellaneous
+- 0x coffee beans
+- 0x tea
+- 0x AA batteries
+- 0x rubbing alcohol
diff --git a/users/wpcarro/scratch/groceries/shell.nix b/users/wpcarro/scratch/groceries/shell.nix
new file mode 100644
index 0000000000..0c6a298bf2
--- /dev/null
+++ b/users/wpcarro/scratch/groceries/shell.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: [ ];
+}
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/.envrc b/users/wpcarro/scratch/haskell-programming-from-first-principles/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/.ghci b/users/wpcarro/scratch/haskell-programming-from-first-principles/.ghci
new file mode 100644
index 0000000000..12aab7f08e
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/.ghci
@@ -0,0 +1 @@
+:set prompt "> "
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/applicative.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/applicative.hs
new file mode 100644
index 0000000000..8259606da3
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/applicative.hs
@@ -0,0 +1,213 @@
+module ApplicativeScratch where
+
+import Data.Function ((&))
+
+import Control.Applicative (liftA3)
+import qualified Data.List as List
+import qualified GHC.Base as Base
+
+--------------------------------------------------------------------------------
+
+-- xs :: [(Integer, Integer)]
+-- xs = zip [1..3] [4..6]
+
+-- added :: Maybe Integer
+-- added =
+--   (+3) <$> (lookup 3 xs)
+
+--------------------------------------------------------------------------------
+
+-- y :: Maybe Integer
+-- y = lookup 3 xs
+
+-- z :: Maybe Integer
+-- z = lookup 2 xs
+
+-- tupled :: Maybe (Integer, Integer)
+-- tupled = Base.liftA2 (,) y z
+
+--------------------------------------------------------------------------------
+
+-- x :: Maybe Int
+-- x = List.elemIndex 3 [1..5]
+
+-- y :: Maybe Int
+-- y = List.elemIndex 4 [1..5]
+
+-- maxed :: Maybe Int
+-- maxed = Base.liftA2 max x y
+
+--------------------------------------------------------------------------------
+
+xs = [1..3]
+ys = [4..6]
+
+x :: Maybe Integer
+x = lookup 3 $ zip xs ys
+
+y :: Maybe Integer
+y = lookup 2 $ zip xs ys
+
+summed :: Maybe Integer
+summed = sum <$> Base.liftA2 (,) x y
+
+--------------------------------------------------------------------------------
+
+newtype Identity a = Identity a deriving (Eq, Show)
+
+instance Functor Identity where
+  fmap f (Identity x) = Identity (f x)
+
+instance Applicative Identity where
+  pure = Identity
+  (Identity f) <*> (Identity x) = Identity (f x)
+
+--------------------------------------------------------------------------------
+
+newtype Constant a b =
+  Constant { getConstant :: a }
+  deriving (Eq, Ord, Show)
+
+instance Functor (Constant a) where
+  fmap _ (Constant x) = Constant x
+
+instance Monoid a => Applicative (Constant a) where
+  pure _ = Constant mempty
+  (Constant x) <*> (Constant y) = Constant (x <> y)
+
+--------------------------------------------------------------------------------
+
+one = const <$> Just "Hello" <*> Just "World"
+
+two :: Maybe (Integer, Integer, String, [Integer])
+two = (,,,) <$> (Just 90)
+            <*> (Just 10)
+            <*> (Just "Tierness")
+            <*> (Just [1..3])
+
+--------------------------------------------------------------------------------
+
+data List a = Nil | Cons a (List a) deriving (Eq, Show)
+
+instance Semigroup (List a) where
+  Nil <> xs = xs
+  xs <> Nil = xs
+  (Cons x xs) <> ys = Cons x (xs <> ys)
+
+instance Functor List where
+  fmap f Nil = Nil
+  fmap f (Cons x xs) = Cons (f x) (fmap f xs)
+
+instance Applicative List where
+  pure x = Cons x Nil
+  Nil <*> _ = Nil
+  _ <*> Nil = Nil
+  (Cons f fs) <*> xs =
+    (f <$> xs) <> (fs <*> xs)
+
+toList :: List a -> [a]
+toList Nil = []
+toList (Cons x xs) = x : toList xs
+
+fromList :: [a] -> List a
+fromList [] = Nil
+fromList (x:xs) = Cons x (fromList xs)
+
+--------------------------------------------------------------------------------
+
+newtype ZipList' a =
+  ZipList' [a]
+  deriving (Eq, Show)
+
+-- instance Eq a => EqProp (ZipList' a) where
+--   (ZipList' lhs) =-= (ZipList' rhs) =
+--     (take 1000 lhs) `eq` (take 1000 rhs)
+
+instance Functor ZipList' where
+  fmap f (ZipList' xs) = ZipList' $ fmap f xs
+
+instance Applicative ZipList' where
+  pure x = ZipList' (repeat x)
+  (ZipList' fs) <*> (ZipList' xs) =
+    ZipList' $ zipWith ($) fs xs
+
+--------------------------------------------------------------------------------
+
+data Validation e a
+  = Failure e
+  | Success a
+  deriving (Eq, Show)
+
+instance Functor (Validation e) where
+  fmap f (Failure x) = Failure x
+  fmap f (Success x) = Success (f x)
+
+instance Monoid e => Applicative (Validation e) where
+  pure = undefined
+  (Success f) <*> (Success x) = Success (f x)
+  _ <*> (Failure x) = Failure x
+  (Failure x) <*> _ = Failure x
+
+data Error
+  = DivideByZero
+  | StackOverflow
+  deriving (Eq, Show)
+
+--------------------------------------------------------------------------------
+
+stops :: String
+stops = "pbtdkg"
+
+vowels :: String
+vowels = "aeiou"
+
+combos :: [a] -> [b] -> [c] -> [(a, b, c)]
+combos xs ys zs =
+  liftA3 (,,) xs ys zs
+
+--------------------------------------------------------------------------------
+
+data Pair a = Pair a a deriving Show
+
+instance Functor Pair where
+  fmap f (Pair x y) = Pair (f x) (f y)
+
+instance Applicative Pair where
+  pure x = Pair x x
+  (Pair f g) <*> (Pair x y) = Pair (f x) (g x)
+
+p :: Pair Integer
+p = Pair 1 2
+
+--------------------------------------------------------------------------------
+
+data Two a b = Two a b
+
+instance Functor (Two a) where
+  fmap f (Two x y) = Two x (f y)
+
+instance Monoid a => Applicative (Two a) where
+  pure x = Two mempty x
+  _ <*> _ = undefined
+
+--------------------------------------------------------------------------------
+
+data Three a b c = Three a b c
+
+instance Functor (Three a b) where
+  fmap f (Three x y z) = Three x y (f z)
+
+instance (Monoid a, Monoid b) => Applicative (Three a b) where
+  pure x = Three mempty mempty x
+  (Three a b f) <*> (Three x y z) = Three (a <> x) (b <> y) (f z)
+
+--------------------------------------------------------------------------------
+
+data Three' a b = Three' a b b
+
+instance Functor (Three' a) where
+  fmap f (Three' x y z) = Three' x (f y) (f z)
+
+instance Monoid a => Applicative (Three' a) where
+  pure x = Three' mempty x x
+  (Three' a f g) <*> (Three' x y z) = Three' (a <> x) (f y) (g z)
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/basic-libraries.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/basic-libraries.hs
new file mode 100644
index 0000000000..bb1f89987e
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/basic-libraries.hs
@@ -0,0 +1,60 @@
+module BasicLibrariesScratch where
+
+import Data.Function ((&))
+
+--------------------------------------------------------------------------------
+newtype DList a = DL { unDL :: [a] -> [a] }
+
+instance (Show a) => Show (DList a) where
+  show (DL x) = "DL " ++ show (x [])
+
+-- | Create an empty difference list.
+emptyDList :: DList a
+emptyDList = DL $ \xs -> xs
+{-# INLINE emptyDList #-}
+
+-- | Create a difference list with `x` as the only member.
+singleton :: a -> DList a
+singleton x =  DL $ \xs -> x : xs
+{-# INLINE singleton #-}
+
+-- | Convert the DList into a list.
+toList :: DList a -> [a]
+toList (DL unDL) = unDL mempty
+{-# INLINE toList #-}
+
+-- | Add an element to the end of a DList.
+infixr `snoc`
+snoc :: a -> DList a -> DList a
+snoc x (DL xs) = DL $ \ys -> xs (x : ys)
+{-# INLINE snoc #-}
+
+-- | Add an element to the beginning of a DList.
+infixr `cons`
+cons :: a -> DList a -> DList a
+cons x (DL xs) = DL $ \ys -> x : xs ys
+{-# INLINE cons #-}
+
+-- | Combine two DLists together.
+append :: DList a -> DList a -> DList a
+append (DL xs) (DL ys) = DL $ \zs -> zs & ys & xs
+{-# INLINE append #-}
+
+--------------------------------------------------------------------------------
+data Queue a =
+  Queue { one :: [a]
+        , two :: [a]
+        } deriving (Show, Eq)
+
+emptyQueue :: Queue a
+emptyQueue = Queue mempty mempty
+
+enqueue :: a -> Queue a -> Queue a
+enqueue x (Queue en de) = Queue (x:en) de
+
+dequeue :: Queue a -> Maybe (a, Queue a)
+dequeue (Queue [] []) = Nothing
+dequeue (Queue en []) =
+  let (d:de) = reverse en
+  in Just (d, Queue de [])
+dequeue (Queue en (d:de)) = Just (d, Queue en de)
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/composing-types.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/composing-types.hs
new file mode 100644
index 0000000000..378cfb7cea
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/composing-types.hs
@@ -0,0 +1,75 @@
+module ComposingTypesScratch where
+
+import Data.Function ((&))
+import Data.Bifunctor
+
+import qualified Data.Foldable as F
+
+--------------------------------------------------------------------------------
+
+newtype Identity a =
+  Identity { getIdentity :: a }
+  deriving (Eq, Show)
+
+newtype Compose f g a =
+  Compose { getCompose :: f (g a) }
+  deriving (Eq, Show)
+
+--------------------------------------------------------------------------------
+
+instance (Functor f, Functor g) => Functor (Compose f g) where
+  fmap f (Compose getCompose) = Compose $ (fmap . fmap) f getCompose
+
+instance (Applicative f, Applicative g) => Applicative (Compose f g) where
+  pure x = x & pure & pure & Compose
+  fgf <*> fga = undefined
+
+--------------------------------------------------------------------------------
+
+instance (Foldable f, Foldable g) => Foldable (Compose f g) where
+  foldMap toMonoid x = undefined
+
+instance (Traversable f, Traversable g) => Traversable (Compose f g) where
+  traverse = undefined
+
+--------------------------------------------------------------------------------
+
+data Deux a b = Deux a b deriving (Show, Eq)
+
+instance Bifunctor Deux where
+  bimap f g (Deux x y) = Deux (f x) (g y)
+
+data Const a b = Const a deriving (Show, Eq)
+
+instance Bifunctor Const where
+  bimap f _ (Const x) = Const (f x)
+
+data Drei a b c = Drei a b c deriving (Show, Eq)
+
+instance Bifunctor (Drei a) where
+  bimap f g (Drei x y z) = Drei x (f y) (g z)
+
+data SuperDrei a b c = SuperDrei a b deriving (Show, Eq)
+
+instance Bifunctor (SuperDrei a) where
+  bimap f g (SuperDrei x y) = SuperDrei x (f y)
+
+data SemiDrei a b c = SemiDrei a deriving (Show, Eq)
+
+instance Bifunctor (SemiDrei a) where
+  bimap _ _ (SemiDrei x) = SemiDrei x
+
+data Quadriceps a b c d = Quadzzz a b c d
+
+instance Bifunctor (Quadriceps a b) where
+  bimap f g (Quadzzz w x y z) = Quadzzz w x (f y) (g z)
+
+-- | Analogue for Either
+data LeftRight a b
+  = Failure a
+  | Success b
+  deriving (Show, Eq)
+
+instance Bifunctor LeftRight where
+  bimap f _ (Failure x) = Failure (f x)
+  bimap _ g (Success y) = Success (g y)
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/foldable.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/foldable.hs
new file mode 100644
index 0000000000..5b59d9e9ba
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/foldable.hs
@@ -0,0 +1,107 @@
+module FoldableScratch where
+
+import Data.Function ((&))
+
+--------------------------------------------------------------------------------
+
+sum :: (Foldable t, Num a) => t a -> a
+sum xs =
+  foldr (+) 0 xs
+
+product :: (Foldable t, Num a) => t a -> a
+product xs =
+  foldr (*) 1 xs
+
+elem :: (Foldable t, Eq a) => a -> t a -> Bool
+elem y xs =
+  foldr (\x acc -> if acc then acc else y == x) False xs
+
+minimum :: (Foldable t, Ord a) => t a -> Maybe a
+minimum xs =
+  foldr (\x acc ->
+           case acc of
+             Nothing   -> Just x
+             Just curr -> Just (min curr x)) Nothing xs
+
+maximum :: (Foldable t, Ord a) => t a -> Maybe a
+maximum xs =
+  foldr (\x acc ->
+           case acc of
+             Nothing   -> Nothing
+             Just curr -> Just (max curr x)) Nothing xs
+
+-- TODO: How could I use QuickCheck to see if Prelude.null and this null return
+-- the same results for the same inputs?
+null :: (Foldable t) => t a -> Bool
+null xs =
+  foldr (\_ _ -> False) True xs
+
+length :: (Foldable t) => t a -> Int
+length xs =
+  foldr (\_ acc -> acc + 1) 0 xs
+
+toList :: (Foldable t) => t a -> [a]
+toList xs =
+  reverse $ foldr (\x acc -> x : acc) [] xs
+
+fold :: (Foldable t, Monoid m) => t m -> m
+fold xs =
+  foldr mappend mempty xs
+
+foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
+foldMap f xs =
+  foldr (\x acc -> mappend (f x) acc) mempty xs
+
+--------------------------------------------------------------------------------
+
+data List a = Nil | Cons a (List a) deriving (Eq, Show)
+
+instance Foldable List where
+  foldr f acc (Cons x rest) = foldr f (f x acc) rest
+  foldr f acc Nil = acc
+
+fromList :: [a] -> List a
+fromList [] = Nil
+fromList (x:rest) = Cons x (fromList rest)
+
+--------------------------------------------------------------------------------
+
+data Constant a b = Constant b deriving (Eq, Show)
+
+-- TODO: Is this correct?
+instance Foldable (Constant a) where
+  foldr f acc (Constant x) = f x acc
+
+--------------------------------------------------------------------------------
+
+data Two a b = Two a b deriving (Eq, Show)
+
+instance Foldable (Two a) where
+  foldr f acc (Two x y) = f y acc
+
+--------------------------------------------------------------------------------
+
+data Three a b c = Three a b c deriving (Eq, Show)
+
+instance Foldable (Three a b) where
+  foldr f acc (Three x y z) = f z acc
+
+--------------------------------------------------------------------------------
+
+data Three' a b = Three' a b b deriving (Eq, Show)
+
+instance Foldable (Three' a) where
+  foldr f acc (Three' x y z) = acc & f z & f y
+
+--------------------------------------------------------------------------------
+
+data Four' a b = Four' a b b b deriving (Eq, Show)
+
+instance Foldable (Four' a) where
+  foldr f acc (Four' w x y z) = acc & f z & f y & f x
+
+--------------------------------------------------------------------------------
+
+filterF :: (Applicative f, Foldable t, Monoid (f a)) => (a -> Bool) -> t a -> f a
+filterF pred xs =
+  foldr (\x acc -> if pred x then pure x `mappend` acc else acc) mempty xs
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/io.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/io.hs
new file mode 100644
index 0000000000..1de8937fce
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/io.hs
@@ -0,0 +1,35 @@
+module IOScratch where
+
+import qualified System.Environment as SE
+import qualified System.IO as SIO
+--------------------------------------------------------------------------------
+
+docs :: String
+docs = "Pass -e to encrypt and -d to decrypt."
+
+encryptStdin :: IO ()
+encryptStdin = do
+  char <- SIO.hGetChar SIO.stdin
+  -- encrypt char
+  SIO.hPutStr SIO.stdout [char]
+
+decryptStdin :: IO ()
+decryptStdin = do
+  char <- SIO.hGetChar SIO.stdin
+  -- decrypt char
+  SIO.hPutStr SIO.stdout [char]
+
+main :: IO ()
+main = do
+  args <- SE.getArgs
+  case args of
+    [] ->
+      putStrLn $ "You did not pass enough arguments. " ++ docs
+    ["-e"] ->
+      encryptStdin
+    ["-d"] ->
+      decryptStdin
+    [x] ->
+      putStrLn $ "You passed an unsupported option: " ++ x ++ ". " ++ docs
+    _ ->
+      putStrLn $ "You passed too many arguments. " ++ docs
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/monad-transformers.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/monad-transformers.hs
new file mode 100644
index 0000000000..3a780fc16c
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/monad-transformers.hs
@@ -0,0 +1,183 @@
+module MonadTransformersScratch where
+
+import Control.Monad
+import qualified Control.Monad.Trans.Maybe as M
+import qualified Control.Monad.Trans.Reader as R
+import qualified Control.Monad.Trans.State as S
+import Data.Function ((&))
+--------------------------------------------------------------------------------
+
+newtype MaybeT m a =
+  MaybeT { runMaybeT :: m (Maybe a) }
+
+instance (Functor f) => Functor (MaybeT f) where
+  fmap f (MaybeT run) =
+    MaybeT $ (fmap . fmap) f run
+
+instance (Applicative m) => Applicative (MaybeT m) where
+  pure x = x & pure & pure & MaybeT
+  _ <*> _ = undefined
+
+instance (Monad m) => Monad (MaybeT m) where
+  return = pure
+  (MaybeT ma) >>= f = MaybeT $ do
+    maybeX <- ma
+    case maybeX of
+      Nothing -> pure Nothing
+      Just x -> x & f & runMaybeT
+
+--------------------------------------------------------------------------------
+
+newtype EitherT e m a =
+  EitherT { runEitherT :: m (Either e a) }
+
+instance (Functor m) => Functor (EitherT e m) where
+  fmap f (EitherT mEither) =
+    EitherT $ (fmap . fmap) f mEither
+
+instance (Applicative m) => Applicative (EitherT e m) where
+  pure x = EitherT $ (pure . pure) x
+  EitherT mEitherF <*> EitherT mEitherX =
+    EitherT $ (fmap (<*>) mEitherF) <*> mEitherX
+
+instance (Monad m) => Monad (EitherT e m) where
+  return = pure
+  EitherT mEitherX >>= f = EitherT $ do
+    eitherX <- mEitherX
+    case eitherX of
+      Left x -> pure $ Left x
+      Right x -> runEitherT $ f x
+
+swapEither :: Either l r -> Either r l
+swapEither (Left x) = Right x
+swapEither (Right x) = Left x
+
+swapEitherT :: (Functor m) => EitherT e m a -> EitherT a m e
+swapEitherT (EitherT mEitherX) =
+  EitherT $ fmap swapEither mEitherX
+
+eitherT :: Monad m => (a -> m c) -> (b -> m c) -> EitherT a m b -> m c
+eitherT aToMC bToMC (EitherT mEitherX) = do
+  eitherX <- mEitherX
+  case eitherX of
+    Left x -> aToMC x
+    Right x -> bToMC x
+
+--------------------------------------------------------------------------------
+
+newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
+
+instance (Functor m) => Functor (ReaderT r m) where
+  fmap f (ReaderT rma) =
+    ReaderT $ (fmap . fmap) f rma
+
+instance (Applicative m) => Applicative (ReaderT r m) where
+  pure x = x & pure & pure & ReaderT
+  ReaderT f <*> ReaderT x = ReaderT $ fmap (<*>) f <*> x
+
+-- instance (Monad m) => Monad (ReaderT r m) where
+--   return = pure
+--   ReaderT rma >>= f =
+--     ReaderT $ \r -> do
+--       a <- rma r
+--       runReaderT (f a) r
+-- --------------------------------------------------------------------------------
+
+rDec :: Num a => R.Reader a a
+rDec = R.ReaderT $ \x -> pure $ x + 1
+
+rShow :: Show a => R.Reader a String
+rShow = R.ReaderT $ \x -> pure $ show x
+
+rPrintAndInc :: (Num a, Show a) => R.ReaderT a IO a
+rPrintAndInc = R.ReaderT $ \x ->
+  putStrLn ("Hi: " ++ show x) >> pure (x + 1)
+
+sPrintIncAccum :: (Num a, Show a) => S.StateT a IO String
+sPrintIncAccum = S.StateT $ \x -> do
+  putStrLn ("Hi: " ++ show x)
+  pure (show x, x + 1)
+
+--------------------------------------------------------------------------------
+
+isValid :: String -> Bool
+isValid v = '!' `elem` v
+
+maybeExcite :: M.MaybeT IO String
+maybeExcite = M.MaybeT $ do
+  x <- getLine
+  putStrLn ""
+  case isValid x of
+    False -> pure Nothing
+    True -> pure $ Just x
+
+doExcite :: IO ()
+doExcite = do
+  putStr "Say something *exciting*: "
+  excite <- M.runMaybeT maybeExcite
+  case excite of
+    Nothing -> putStrLn "Gonna need some more excitement..."
+    Just x  -> putStrLn "Now THAT'S exciting...nice!"
+
+--------------------------------------------------------------------------------
+
+data Participant
+  = Man
+  | Machine
+  deriving (Show, Eq)
+
+newtype Hand = Hand (Integer, Integer) deriving (Show, Eq)
+
+newtype Score = Score (Integer, Integer) deriving (Show, Eq)
+
+getLineLn :: String -> IO String
+getLineLn prompt = do
+  putStr prompt
+  x <- getLine
+  putStrLn ""
+  pure x
+
+promptGuess :: IO Hand
+promptGuess = do
+  fingers <- getLineLn "How many fingers (0-5): "
+  guess <- getLineLn "Guess: "
+  pure $ Hand (read guess, read fingers)
+
+aiGuess :: IO Hand
+aiGuess = pure $ Hand (2, 3)
+
+whoWon :: Hand -> Hand -> Maybe Participant
+whoWon (Hand (guessA, fingersA)) (Hand (guessB, fingersB))
+  | guessA == guessB && guessA == (fingersA + fingersB) = Nothing
+  | guessA == (fingersA + fingersB) = Just Man
+  | guessB == (fingersA + fingersB) = Just Machine
+  | otherwise = Nothing
+
+initScore :: Score
+initScore = Score (0, 0)
+
+printScore :: Score -> IO ()
+printScore (Score (man, machine)) =
+  putStrLn $ "Man: " ++ show man ++ " Machine: " ++ show machine
+
+startMorra :: S.StateT Score IO ()
+startMorra = S.StateT $ \(Score (man, machine)) -> do
+  Hand (guessA, fingersA) <- promptGuess
+  Hand (guessB, fingersB) <- aiGuess
+  putStrLn $ "P: " ++ show fingersA ++ "," ++ show guessA
+  putStrLn $ "C: " ++ show fingersB ++ "," ++ show guessB
+  case whoWon (Hand (guessA, fingersA)) (Hand (guessB, fingersB)) of
+    Nothing -> do
+      putStrLn "Nobody won..."
+      printScore (Score (man, machine))
+      pure ((), Score (man, machine))
+    Just Man -> do
+      putStrLn "Man won!"
+      printScore (Score (man + 1, machine))
+      pure ((), Score (man + 1, machine))
+    Just Machine -> do
+      putStrLn "Oh no... Machine won..."
+      printScore (Score (man, machine + 1))
+      pure ((), Score (man, machine + 1))
+
+playMorra = S.runStateT (forever startMorra) initScore
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/monad.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/monad.hs
new file mode 100644
index 0000000000..2f80b457b1
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/monad.hs
@@ -0,0 +1,178 @@
+module MonadScratch where
+
+import Data.Function ((&))
+import Test.QuickCheck
+import Test.QuickCheck.Checkers
+import Control.Applicative (liftA2)
+import qualified Control.Monad as Monad
+
+--------------------------------------------------------------------------------
+
+bind :: Monad m => (a -> m b) -> m a -> m b
+bind f x = Monad.join $ fmap f x
+
+--------------------------------------------------------------------------------
+
+fTrigger :: Functor f => f (Int, String, [Int])
+fTrigger = undefined
+
+aTrigger :: Applicative a => a (Int, String, [Int])
+aTrigger = undefined
+
+mTrigger :: Monad m => m (Int, String, [Int])
+mTrigger = undefined
+
+--------------------------------------------------------------------------------
+
+data Sum a b
+  = Fst a
+  | Snd b
+  deriving (Eq, Show)
+
+instance (Eq a, Eq b) => EqProp (Sum a b) where
+  (=-=) = eq
+
+instance (Arbitrary a, Arbitrary b) => Arbitrary (Sum a b) where
+  arbitrary = frequency [ (1, Fst <$> arbitrary)
+                        , (1, Snd <$> arbitrary)
+                        ]
+
+instance Functor (Sum a) where
+  fmap f (Fst x) = Fst x
+  fmap f (Snd x) = Snd (f x)
+
+instance Applicative (Sum a) where
+  pure x = Snd x
+  (Snd f) <*> (Snd x) = Snd (f x)
+  (Snd f) <*> (Fst x) = Fst x
+  (Fst x) <*> _ = Fst x
+
+instance Monad (Sum a) where
+  (Fst x) >>= _ = Fst x
+  (Snd x) >>= f = f x
+
+--------------------------------------------------------------------------------
+
+data Nope a = NopeDotJpg deriving (Eq, Show)
+
+instance Arbitrary (Nope a) where
+  arbitrary = pure NopeDotJpg
+
+instance EqProp (Nope a) where
+  (=-=) = eq
+
+instance Functor Nope where
+  fmap f _ = NopeDotJpg
+
+instance Applicative Nope where
+  pure _ = NopeDotJpg
+  _ <*> _ = NopeDotJpg
+
+instance Monad Nope where
+  NopeDotJpg >>= f = NopeDotJpg
+
+--------------------------------------------------------------------------------
+
+data BahEither b a
+  = PLeft a
+  | PRight b
+  deriving (Eq, Show)
+
+instance (Arbitrary b, Arbitrary a) => Arbitrary (BahEither b a) where
+  arbitrary = frequency [ (1, PLeft <$> arbitrary)
+                        , (1, PRight <$> arbitrary)
+                        ]
+
+instance (Eq a, Eq b) => EqProp (BahEither a b) where
+  (=-=) = eq
+
+instance Functor (BahEither b) where
+  fmap f (PLeft x) = PLeft (f x)
+  fmap _ (PRight x) = PRight x
+
+instance Applicative (BahEither b) where
+  pure = PLeft
+  (PRight x) <*> _ = PRight x
+  (PLeft f) <*> (PLeft x) = PLeft (f x)
+  _ <*> (PRight x) = PRight x
+
+instance Monad (BahEither b) where
+  (PRight x) >>= _ = PRight x
+  (PLeft x) >>= f = f x
+
+--------------------------------------------------------------------------------
+
+newtype Identity a = Identity a
+  deriving (Eq, Ord, Show)
+
+instance Functor Identity where
+  fmap f (Identity x) = Identity (f x)
+
+instance Applicative Identity where
+  pure = Identity
+  (Identity f) <*> (Identity x) = Identity (f x)
+
+instance Monad Identity where
+  (Identity x) >>= f = f x
+
+--------------------------------------------------------------------------------
+
+data List a
+  = Nil
+  | Cons a (List a)
+  deriving (Eq, Show)
+
+instance Arbitrary a => Arbitrary (List a) where
+  arbitrary = frequency [ (1, pure Nil)
+                        , (1, Cons <$> arbitrary <*> arbitrary)
+                        ]
+
+instance Eq a => EqProp (List a) where
+  (=-=) = eq
+
+fromList :: [a] -> List a
+fromList [] = Nil
+fromList (x:xs) = Cons x (fromList xs)
+
+instance Semigroup (List a) where
+  Nil <> xs = xs
+  xs <> Nil = xs
+  (Cons x xs) <> ys =
+    Cons x (xs <> ys)
+
+instance Functor List where
+  fmap f Nil = Nil
+  fmap f (Cons x xs) = Cons (f x) (fmap f xs)
+
+instance Applicative List where
+  pure x = Cons x Nil
+  Nil <*> _ = Nil
+  _ <*> Nil = Nil
+  (Cons f fs) <*> xs =
+    (f <$> xs) <> (fs <*> xs)
+
+instance Monad List where
+  Nil >>= _ = Nil
+  (Cons x xs) >>= f = (f x) <> (xs >>= f)
+
+--------------------------------------------------------------------------------
+
+j :: Monad m => m (m a) -> m a
+j = Monad.join
+
+l1 :: Monad m => (a -> b) -> m a -> m b
+l1 = Monad.liftM
+
+l2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
+l2 = Monad.liftM2
+
+a :: Monad m => m a -> m (a -> b) -> m b
+a = flip (<*>)
+
+meh :: Monad m => [a] -> (a -> m b) -> m [b]
+meh xs f = flipType $ f <$> xs
+
+flipType :: Monad m => [m a] -> m [a]
+flipType [] = pure mempty
+flipType (m:ms) =
+  m >>= (\x -> (x:) <$> flipType ms)
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/non-strictness.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/non-strictness.hs
new file mode 100644
index 0000000000..42608fb0c9
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/non-strictness.hs
@@ -0,0 +1,6 @@
+module NonStrictnessScratch where
+
+x = undefined
+y = "blah"
+main = do
+  print $ snd (x, x `seq` y)
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/reader.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/reader.hs
new file mode 100644
index 0000000000..7cb7b4a1bb
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/reader.hs
@@ -0,0 +1,149 @@
+module Reader where
+
+import Data.Char
+import Data.Function ((&))
+import Data.Functor ((<&>))
+import qualified Control.Applicative as A
+import qualified Data.Maybe as MB
+
+cap :: String -> String
+cap xs = xs <&> toUpper
+
+rev :: String -> String
+rev = reverse
+
+compose :: String -> String
+compose xs = xs & rev . cap
+
+fmapped :: String -> String
+fmapped xs = xs & rev <$> cap
+
+tupled :: String -> (String, String)
+tupled xs = A.liftA2 (,) cap rev $ xs
+
+tupled' :: String -> (String, String)
+tupled' = do
+  capResult <- cap
+  revResult <- rev
+  pure (revResult, capResult)
+
+--------------------------------------------------------------------------------
+
+newtype Reader r a = Reader { runReader :: r -> a }
+
+ask :: Reader a a
+ask = Reader id
+
+--------------------------------------------------------------------------------
+
+newtype HumanName = HumanName String
+  deriving (Eq, Show)
+
+newtype DogName = DogName String
+  deriving (Eq, Show)
+
+newtype Address = Address String
+  deriving (Eq, Show)
+
+data Person
+  = Person
+  { humanName :: HumanName
+  , dogName :: DogName
+  , address :: Address
+  } deriving (Eq, Show)
+
+data Dog
+  = Dog
+  { dogsName :: DogName
+  , dogsAddress :: Address
+  } deriving (Eq, Show)
+
+pers :: Person
+pers =
+  Person (HumanName "Big Bird")
+         (DogName "Barkley")
+         (Address "Sesame Street")
+
+chris :: Person
+chris =
+  Person (HumanName "Chris Allen")
+         (DogName "Papu")
+         (Address "Austin")
+
+getDog :: Person -> Dog
+getDog p =
+  Dog (dogName p) (address p)
+
+getDogR :: Person -> Dog
+getDogR =
+  A.liftA2 Dog dogName address
+
+--------------------------------------------------------------------------------
+
+myLiftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
+myLiftA2 f x y =
+  f <$> x <*> y
+
+asks :: (r -> a) -> Reader r a
+asks f = Reader f
+
+--------------------------------------------------------------------------------
+
+instance Functor (Reader a) where
+  fmap f (Reader ab) = Reader $ f . ab
+
+instance Applicative (Reader a) where
+  pure x = Reader $ \_ -> x
+  (Reader rab) <*> (Reader ra) = Reader $ do
+    ab <- rab
+    fmap ab ra
+
+--------------------------------------------------------------------------------
+
+instance Monad (Reader r) where
+  return = pure
+  -- (>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
+  (Reader x) >>= f = undefined
+
+--------------------------------------------------------------------------------
+
+x = [1..3]
+y = [4..6]
+z = [7..9]
+
+xs :: Maybe Integer
+xs = zip x y & lookup 3
+
+ys :: Maybe Integer
+ys = zip y z & lookup 6
+
+zs :: Maybe Integer
+zs = zip x y & lookup 4
+
+z' :: Integer -> Maybe Integer
+z' n = zip x y & lookup n
+
+x1 :: Maybe (Integer, Integer)
+x1 = A.liftA2 (,) xs ys
+
+x2 :: Maybe (Integer, Integer)
+x2 = A.liftA2 (,) ys zs
+
+x3 :: Integer -> (Maybe Integer, Maybe Integer)
+x3 n = (z' n, z' n)
+
+summed :: Num a => (a, a) -> a
+summed (x, y) = x + y
+
+bolt :: Integer -> Bool
+bolt x = x > 3 && x < 8
+
+main :: IO ()
+main = do
+  print $ sequenceA [Just 3, Just 2, Just 1]
+  print $ sequenceA [x, y]
+  print $ sequenceA [xs, ys]
+  print $ summed <$> ((,) <$> xs <*> ys)
+  print $ bolt 7
+  print $ bolt <$> z
+  print $ sequenceA [(>3), (<8) ,even] 7
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/shell.nix b/users/wpcarro/scratch/haskell-programming-from-first-principles/shell.nix
new file mode 100644
index 0000000000..49dbe746d3
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/shell.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: with hpkgs; [
+    quickcheck-simple
+    checkers
+  ];
+}
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/state.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/state.hs
new file mode 100644
index 0000000000..f63e0ecdf1
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/state.hs
@@ -0,0 +1,93 @@
+module StateScratch where
+
+--------------------------------------------------------------------------------
+import System.Random
+-- import Control.Monad.Trans.State
+import Data.Function ((&))
+
+import qualified Control.Applicative as Ap
+import qualified Control.Monad as M
+--------------------------------------------------------------------------------
+
+data Die
+  = DieOne
+  | DieTwo
+  | DieThree
+  | DieFour
+  | DieFive
+  | DieSix
+  deriving (Eq, Show)
+
+intToDie :: Integer -> Maybe Die
+intToDie 1 = Just DieOne
+intToDie 2 = Just DieTwo
+intToDie 3 = Just DieThree
+intToDie 4 = Just DieFour
+intToDie 5 = Just DieFive
+intToDie 6 = Just DieSix
+intToDie _ = Nothing
+
+rollDie :: Moi StdGen Die
+rollDie = do
+  (n, s) <- randomR (1, 6)
+  case intToDie n of
+    Just d  -> pure (d, s)
+    Nothing -> pure (DieOne, s)
+
+rollsToGetN :: Integer -> StdGen -> [Die]
+rollsToGetN n g = go 0 [] g
+  where
+    go sum result gen
+      | sum >= n = result
+      | otherwise =
+        let (dice, nextGen) = randomR (1, 6) gen
+        in case intToDie dice of
+          Nothing -> go (sum + dice) result nextGen
+          Just d  -> go (sum + dice) (d : result) nextGen
+
+--------------------------------------------------------------------------------
+
+newtype Moi s a = Moi { runMoi :: s -> (a, s) }
+
+instance Functor (Moi s) where
+  fmap f (Moi run) =
+    Moi $ \s -> let (x, t) = run s
+                in (f x, t)
+
+instance Applicative (Moi s) where
+  pure x = Moi $ \s -> (x, s)
+  (Moi f) <*> (Moi run) =
+    Moi $ \s -> let (g, t) = f s
+                    (x, u) = run t
+                in (g x, u)
+
+instance Monad (Moi s) where
+  (Moi run1) >>= f =
+    Moi $ \s -> let (x, t) = run1 s
+                    (Moi run2) = f x
+                in run2 t
+
+--------------------------------------------------------------------------------
+
+fizzBuzz :: Integer -> String
+fizzBuzz n | n `mod` 15 == 0 = "FizzBuzz"
+           | n `mod`  5 == 0 = "Buzz"
+           | n `mod`  3 == 0 = "Fizz"
+           | otherwise       = show n
+
+--------------------------------------------------------------------------------
+
+get :: Moi s s
+get = Moi $ \s -> (s, s)
+
+put :: s -> Moi s ()
+put x = Moi $ \s -> ((), x)
+
+exec :: Moi s a -> s -> s
+exec (Moi run) x = x & run & snd
+
+eval :: Moi s a -> s -> a
+eval (Moi run) x = x & run & fst
+
+modify :: (s -> s) -> Moi s ()
+modify f = Moi $ \s -> ((), f s)
diff --git a/users/wpcarro/scratch/haskell-programming-from-first-principles/traversable.hs b/users/wpcarro/scratch/haskell-programming-from-first-principles/traversable.hs
new file mode 100644
index 0000000000..5dc4ea411b
--- /dev/null
+++ b/users/wpcarro/scratch/haskell-programming-from-first-principles/traversable.hs
@@ -0,0 +1,131 @@
+module TraversableScratch where
+
+import qualified Data.Foldable as F
+
+import Test.QuickCheck
+
+newtype Identity a = Identity a
+  deriving (Eq, Ord, Show)
+
+instance Functor Identity where
+  fmap f (Identity x) = Identity (f x)
+
+instance Foldable Identity where
+  foldMap f (Identity x) = f x
+
+instance Traversable Identity where
+  traverse f (Identity x) = Identity <$> f x
+
+--------------------------------------------------------------------------------
+
+data Optional a
+  = Nada
+  | Some a
+  deriving (Eq, Show)
+
+instance Functor Optional where
+  fmap f Nada = Nada
+  fmap f (Some x) = Some (f x)
+
+instance Foldable Optional where
+  foldMap f Nada = mempty
+  foldMap f (Some x) = f x
+
+instance Traversable Optional where
+  traverse f Nada = pure Nada
+  traverse f (Some x) = Some <$> f x
+
+--------------------------------------------------------------------------------
+
+data List a = Nil | Cons a (List a) deriving (Eq, Show)
+
+instance Functor List where
+  fmap _ Nil = Nil
+  fmap f (Cons x xs) = Cons (f x) (fmap f xs)
+
+instance Foldable List where
+  foldMap f Nil = mempty
+  foldMap f (Cons x xs) = mappend (f x) (foldMap f xs)
+
+instance Traversable List where
+  sequenceA Nil = pure Nil
+  sequenceA (Cons x xs) = Cons <$> x <*> sequenceA xs
+
+--------------------------------------------------------------------------------
+
+data Three a b c = Three a b c
+  deriving (Eq, Show)
+
+instance Functor (Three a b) where
+  fmap f (Three x y z) = Three x y (f z)
+
+instance Foldable (Three a b) where
+  foldMap f (Three _ _ z) = f z
+
+instance Traversable (Three a b) where
+  sequenceA (Three x y z) = (\z' -> Three x y z') <$> z
+
+--------------------------------------------------------------------------------
+
+data Pair a b = Pair a b
+  deriving (Eq, Show)
+
+instance Functor (Pair a) where
+  fmap f (Pair x y) = Pair x (f y)
+
+instance Foldable (Pair a) where
+  foldMap f (Pair x y) = f y
+
+instance Traversable (Pair a) where
+  sequenceA (Pair x y) = (\y' -> Pair x y') <$> y
+
+--------------------------------------------------------------------------------
+
+data Big a b = Big a b b
+  deriving (Eq, Show)
+
+instance Functor (Big a) where
+  fmap f (Big x y z) = Big x (f y) (f z)
+
+instance Foldable (Big a) where
+  foldMap f (Big x y z) = f y <> f z
+
+instance Traversable (Big a) where
+  sequenceA (Big x y z) = (\y' z' -> Big x y' z') <$> y <*> z
+
+--------------------------------------------------------------------------------
+
+data Bigger a b = Bigger a b b b
+  deriving (Eq, Show)
+
+instance Functor (Bigger a) where
+  fmap f (Bigger w x y z) = Bigger w (f x) (f y) (f z)
+
+instance Foldable (Bigger a) where
+  foldMap f (Bigger w x y z) = f x <> f y <> f z
+
+instance Traversable (Bigger a) where
+  sequenceA (Bigger w x y z) = (\x' y' z' -> Bigger w x' y' z') <$> x <*> y <*> z
+
+--------------------------------------------------------------------------------
+
+data Tree a
+  = Empty
+  | Leaf a
+  | Node (Tree a) a (Tree a)
+  deriving (Eq, Show)
+
+instance Functor Tree where
+  fmap f Empty = Empty
+  fmap f (Leaf x) = Leaf (f x)
+  fmap f (Node lhs x rhs) = Node (fmap f lhs) (f x) (fmap f rhs)
+
+instance Foldable Tree where
+  foldMap f Empty = mempty
+  foldMap f (Leaf x) = f x
+  foldMap f (Node lhs x rhs) = (foldMap f lhs) <> (f x) <> (foldMap f rhs)
+
+instance Traversable Tree where
+  sequenceA Empty = pure Empty
+  sequenceA (Leaf x) = Leaf <$> x
+  sequenceA (Node lhs x rhs) = Node <$> sequenceA lhs <*> x <*> sequenceA rhs
diff --git a/users/wpcarro/scratch/picoctf/.skip-subtree b/users/wpcarro/scratch/picoctf/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/.skip-subtree
diff --git a/users/wpcarro/scratch/picoctf/README.md b/users/wpcarro/scratch/picoctf/README.md
new file mode 100644
index 0000000000..03a49817f7
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/README.md
@@ -0,0 +1,3 @@
+# picoCTF
+
+My solutions for some of the questsions at https://play.picoctf.org/practice.
diff --git a/users/wpcarro/scratch/picoctf/challenge_144.py b/users/wpcarro/scratch/picoctf/challenge_144.py
new file mode 100644
index 0000000000..570a7fd5a7
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_144.py
@@ -0,0 +1,11 @@
+def rotate_alpha(x, n):
+    def rotate_char(c, n):
+        offset = 'A' if c.isupper() else 'a'
+        return chr((ord(c) - ord(offset) + n) % 26 + ord(offset))
+    return "".join([rotate_char(c, n) if c.isalpha() else c for c in x])
+
+xs = [
+    "cvpbPGS{arkg_gvzr_V'yy_gel_2_ebhaqf_bs_ebg13_Ncualgvd}",
+]
+for x in xs:
+    print(rotate_alpha(x, 13))
diff --git a/users/wpcarro/scratch/picoctf/challenge_156.py b/users/wpcarro/scratch/picoctf/challenge_156.py
new file mode 100644
index 0000000000..8c87a1ce76
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_156.py
@@ -0,0 +1,13 @@
+bytestring = [
+    112, 105, 99, 111, 67, 84, 70, 123, 103, 48, 48, 100, 95, 107, 49, 116,
+    116, 121, 33, 95, 110, 49, 99, 51, 95, 107, 49, 116, 116, 121, 33, 95, 57,
+    98, 51, 98, 55, 51, 57, 50, 125, 10,
+]
+
+def decode(xs):
+    result = []
+    for x in xs:
+        result.append(chr(x))
+    return "".join(result)
+
+print(decode(bytestring))
diff --git a/users/wpcarro/scratch/picoctf/challenge_166/ende.py b/users/wpcarro/scratch/picoctf/challenge_166/ende.py
new file mode 100644
index 0000000000..08395f9209
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_166/ende.py
@@ -0,0 +1,60 @@
+
+import sys
+import base64
+from cryptography.fernet import Fernet
+
+
+
+usage_msg = "Usage: "+ sys.argv[0] +" (-e/-d) [file]"
+help_msg = usage_msg + "\n" +\
+        "Examples:\n" +\
+        "  To decrypt a file named 'pole.txt', do: " +\
+        "'$ python "+ sys.argv[0] +" -d pole.txt'\n"
+
+
+
+if len(sys.argv) < 2 or len(sys.argv) > 4:
+    print(usage_msg)
+    sys.exit(1)
+
+
+
+if sys.argv[1] == "-e":
+    if len(sys.argv) < 4:
+        sim_sala_bim = input("Please enter the password:")
+    else:
+        sim_sala_bim = sys.argv[3]
+
+    ssb_b64 = base64.b64encode(sim_sala_bim.encode())
+    c = Fernet(ssb_b64)
+
+    with open(sys.argv[2], "rb") as f:
+        data = f.read()
+        data_c = c.encrypt(data)
+        sys.stdout.write(data_c.decode())
+
+
+elif sys.argv[1] == "-d":
+    if len(sys.argv) < 4:
+        sim_sala_bim = input("Please enter the password:")
+    else:
+        sim_sala_bim = sys.argv[3]
+
+    ssb_b64 = base64.b64encode(sim_sala_bim.encode())
+    c = Fernet(ssb_b64)
+
+    with open(sys.argv[2], "r") as f:
+        data = f.read()
+        data_c = c.decrypt(data.encode())
+        sys.stdout.buffer.write(data_c)
+
+
+elif sys.argv[1] == "-h" or sys.argv[1] == "--help":
+    print(help_msg)
+    sys.exit(1)
+
+
+else:
+    print("Unrecognized first argument: "+ sys.argv[1])
+    print("Please use '-e', '-d', or '-h'.")
+
diff --git a/users/wpcarro/scratch/picoctf/challenge_166/flag.txt.en b/users/wpcarro/scratch/picoctf/challenge_166/flag.txt.en
new file mode 100644
index 0000000000..1c4d245811
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_166/flag.txt.en
@@ -0,0 +1 @@
+gAAAAABgUAIWsYfVayn4m1dKle5X91HrZW_MIRAW4ILPgf4gD6jalLF4PysYB5_YTpDwclcQPqw_0xTxanpJ_Urx5Vi6mTeBA_rWPA_WQLvVXXHp1mG3EpOgY8Na1_NIAfc9LceH_L2o
\ No newline at end of file
diff --git a/users/wpcarro/scratch/picoctf/challenge_166/pw.txt b/users/wpcarro/scratch/picoctf/challenge_166/pw.txt
new file mode 100644
index 0000000000..a4c1c7ae66
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_166/pw.txt
@@ -0,0 +1 @@
+67c6cc9667c6cc9667c6cc9667c6cc96
diff --git a/users/wpcarro/scratch/picoctf/challenge_166/shell.nix b/users/wpcarro/scratch/picoctf/challenge_166/shell.nix
new file mode 100644
index 0000000000..85d3865a51
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_166/shell.nix
@@ -0,0 +1,8 @@
+{ pkgs, ... }:
+
+let
+  python = pkgs.python3.withPackages (pypkgs: with pypkgs; [
+    cryptography
+  ]);
+in
+python.env
diff --git a/users/wpcarro/scratch/picoctf/challenge_170/README.md b/users/wpcarro/scratch/picoctf/challenge_170/README.md
new file mode 100644
index 0000000000..2507208f5c
--- /dev/null
+++ b/users/wpcarro/scratch/picoctf/challenge_170/README.md
@@ -0,0 +1,11 @@
+# challenge 170
+
+The following should work on most Linux distros, but it didn't for me on NixOS:
+
+```shell
+chmod u+x ./warm
+./warm -h
+```
+
+So instead, just call `strings` on the exectuable to search for the help text,
+which contains the flag.
diff --git a/users/wpcarro/scratch/simple-select/README.md b/users/wpcarro/scratch/simple-select/README.md
new file mode 100644
index 0000000000..69e5707302
--- /dev/null
+++ b/users/wpcarro/scratch/simple-select/README.md
@@ -0,0 +1,71 @@
+# Simple Select
+
+- Simple Select is a less expressive but more ergonomic query language for
+  tabular data than SQL.
+- `slx` is a command-line tool for querying CSVs using the Simple Select query
+  language.
+
+Simple Select queries look like this: `director:"Tarantino" OR director:"Scorsese"`.
+
+## Example
+
+Say we have the following data in a CSV:
+
+```csv
+title,year,rating,director
+"Spirited Away",2001,8.5,"Hayao Miyazaki"
+Andhadhun,2018,8.1,"Sriram Raghavan"
+Dangal,2016,8.3,"Sriram Raghavan"
+"Avengers: Infinity War",2019,8.4,"Anthony Russo"
+Alien,1979,8.4,"Ridley Scott"
+...
+```
+
+We can invoke `slx` like so...
+
+```
+$ slx -f /tmp/movies.csv
+```
+
+...and then query using the REPL:
+
+```
+> director:/S.*m/ OR director:"Hayao"
+Andhadhun       2018    8.1     1       Sriram Raghavan 0       1
+Dangal  2016    8.3     1       Sriram Raghavan 0       1
+Howls Moving Castle     2004    8.2     0       Hayao Miyazaki  1       1
+Judgment at Nuremberg   1961    8.1     0       Stanley Kramer  0       0
+Laputa: Castle in the Sky       1986    8.0     0       Hayao Miyazaki  1       1
+Nausicaa of the Valley of the Wind      1984    8.0     0       Hayao Miyazaki  1       1
+Network 1976    8.1     0       Sidney Lumet    0       0
+```
+
+## Warning
+
+Simple Select is **not intended for production use**. I wrote this as a toy
+project for my own consumption. There are quite a few bugs of which I'm aware
+and quite a few other features that I'd like to support but haven't had time to
+support just yet.
+
+Why publish it then? Maybe this project will inspire drive-by contributions or
+other, better-implemented spin-offs.
+
+## Wish List
+
+Speaking of drive-by contributions, here are some things that I'd like to
+support:
+
+- Implicit `AND` conjunctions (`director:/Tarantino/ year:"2000"` instead of
+  `director:/Tarantino/ AND year:"2000"`)
+- Support for types like numbers, dates (`year:2000` instead of `year:"2000"`)
+- `slx` should support CSV *and* (at the very least) sqlite3 file formats (open
+  to other formats as well)
+- Regexes should be the default query primitive (`director:Tarantino` instead of
+  `director:/Tarantino/`)
+- Improve parsing errors (including surfacing errors to the user)
+- Support for reading from `STDIN` and issuing queries from the command-line
+- Unit-testing
+- Configurable delimiters for output data (right now it's just `\t`)
+- (Maybe) rewrite in a faster, more-type-safe languages (e.g. Rust)
+
+I'm likely missing other FRs, bugs, so please file issues!
diff --git a/users/wpcarro/scratch/simple-select/main.py b/users/wpcarro/scratch/simple-select/main.py
new file mode 100644
index 0000000000..3ae6c5d60e
--- /dev/null
+++ b/users/wpcarro/scratch/simple-select/main.py
@@ -0,0 +1,262 @@
+from argparse import ArgumentParser
+
+import csv
+from parser import Parser
+import sqlite3
+import string
+from scanner import Scanner
+import re
+import readline
+
+################################################################################
+# Predicates
+################################################################################
+
+def is_alpha(c):
+  return c in string.ascii_letters
+
+def is_digit(c):
+  return c in "0123456789"
+
+def is_alphanumeric(c):
+  return is_alpha(c) or is_digit(c)
+
+def is_whitespace(c):
+  return c in " \r\t\n"
+
+################################################################################
+# Tokenizer
+################################################################################
+
+AND    = ("CONJUNCTION", "AND")
+OR     = ("CONJUNCTION", "OR")
+NOT    = ("PUNCTUATION", "NOT")
+COLON  = ("PUNCTUATION", "COLON")
+LPAREN = ("PUNCTUATION", "LPAREN")
+RPAREN = ("PUNCTUATION", "RPAREN")
+
+def tokenize(x):
+  s = Scanner(x)
+  tokens = scan_tokens(s)
+  return tokens
+
+def scan_tokens(s):
+  result = []
+  while not s.exhausted():
+    if is_whitespace(s.peek()):
+      s.advance()
+    else:
+      result.append(scan_token(s))
+  return result
+
+def scan_token(s):
+  punctuation = {
+      "-": NOT,
+      ":": COLON,
+      "(": LPAREN,
+      ")": RPAREN,
+  }
+  c = s.peek()
+  if c in punctuation:
+    s.advance()
+    return punctuation[c]
+  if c == "\"":
+    return tokenize_string(s)
+  if c == "/":
+    return tokenize_regex(s)
+  if is_alpha(c):
+    return tokenize_identifier(s)
+
+def tokenize_string(s):
+  s.advance() # ignore opening 2x-quote
+  current = ""
+  while s.peek() != "\"" and not s.exhausted():
+    current += s.advance()
+  if s.exhausted():
+    raise Exception("Unterminated string")
+  s.advance() # ignore closing 2x-quote
+  return ("STRING", current)
+
+def tokenize_regex(s):
+  s.advance() # ignore opening forward-slash
+  current = ""
+  while s.peek() != "/" and not s.exhausted():
+    current += s.advance()
+  if s.exhausted():
+    raise Exception("Unterminated regex")
+  s.advance() # ignore closing forward-slash
+  return ("REGEX", current)
+
+def tokenize_identifier(s):
+  conjunctions = {
+      "AND",
+      "OR",
+  }
+  current = s.advance()
+  while is_alphanumeric(s.peek()):
+    current += s.advance()
+  if current.upper() in conjunctions:
+    return ("CONJUNCTION", current.upper())
+  else:
+    return ("IDENTIFIER", current)
+
+################################################################################
+# Parser
+################################################################################
+
+# EBNF
+# Note: we order expression types by ascending levels of precedence.
+#
+# expression  -> conjunction ;
+# conjunction -> selection ( ( "AND" | "OR" )? selection )* ;
+# selection   -> "-"? IDENTIFIER ":" ( REGEX | STRING ) | grouping ;
+# grouping    -> REGEX | STRING | "(" expression ")" ;
+
+def parse(x):
+  tokens = tokenize(x)
+  p = Parser(tokens)
+  return expression(p)
+
+def expression(p):
+  return conjunction(p)
+
+def conjunction(p):
+  lhs = selection(p)
+
+  # TODO(wpcarro): Support default AND conjuctions when they're undefined.
+  while not p.exhausted() and p.match({AND, OR}):
+    conj = p.peek(n=-1)
+    rhs = selection(p)
+    lhs = ("CONJUNCTION", conj[1], lhs, rhs)
+
+  return lhs
+
+def selection(p):
+  negate = False
+  if p.peek() == NOT:
+    negate = True
+    p.advance()
+
+  if p.peek()[0] != "IDENTIFIER":
+    return grouping(p)
+
+  ident = p.expect(lambda x: x[0] == "IDENTIFIER")
+  colon = p.expect(lambda x: x[1] == "COLON")
+  value = p.expect(lambda x: x[0] in {"REGEX", "STRING"})
+  return ("SELECTION", negate, ident[1], value)
+
+def grouping(p):
+  if p.peek()[0] == "REGEX":
+    return p.advance()
+
+  if p.peek()[0] == "STRING":
+    return p.advance()
+
+  if p.peek() == LPAREN:
+    p.advance()
+    expr = expression(p)
+    p.expect(lambda x: x == RPAREN)
+    return ("GROUPING", expr)
+
+################################################################################
+# Compiler
+################################################################################
+
+def compile(source, table, columns):
+  ast = parse(source)
+  return "SELECT * FROM {} WHERE {};".format(table, do_compile(ast, columns))
+
+def do_compile(ast, columns):
+  if ast[0] == "REGEX":
+    cols = "({})".format(" || ".join(columns))
+    return "{} REGEXP '.*{}.*'".format(cols, ast[1])
+
+  if ast[0] == "STRING":
+    cols = "({})".format(" || ".join(columns))
+    return "{} LIKE '%{}%'".format(cols, ast[1])
+
+  if ast[0] == "SELECTION":
+    return compile_selection(ast)
+
+  if ast[0] == "CONJUNCTION":
+    _, conj, lhs, rhs = ast
+    lhs = do_compile(lhs, columns)
+    rhs = do_compile(rhs, columns)
+    return "{} {} {}".format(lhs, conj, rhs)
+
+  if ast[0] == "GROUPING":
+    return "({})".format(do_compile(ast[1], columns))
+
+  raise Exception("Unexpected AST: \"{}\"".format(ast))
+
+def compile_selection(ast):
+  _, negate, column, query = ast
+  match = compile_query(negate, query)
+  return "{} {}".format(column, match)
+
+def compile_query(negate, query):
+  query_type, query_string = query
+  if query_type == "REGEX":
+    if negate:
+      return "NOT REGEXP '.*{}.*'".format(query_string)
+    return "REGEXP '.*{}.*'".format(query_string)
+
+  if query_type == "STRING":
+    if negate:
+      return "NOT LIKE '%{}%'".format(query_string)
+    return "LIKE '%{}%'".format(query_string)
+
+################################################################################
+# Helper Functions
+################################################################################
+
+def regexp(expr, x):
+  reg = re.compile(expr)
+  return reg.search(x) is not None
+
+################################################################################
+# Main
+################################################################################
+
+def main(csv_path=None, debug=False):
+  # Import CSV to SQLite
+  table = "main"
+  con = sqlite3.connect(":memory:")
+
+  con.create_function("REGEXP", 2, regexp)
+
+  cur = con.cursor()
+  with open(csv_path, "r") as f:
+    r = csv.DictReader(f)
+    columns = next(r).keys()
+
+    # TODO(wpcarro): Use safer interpolation variant of "?" here and throughout.
+    cur.execute("CREATE TABLE {} ({});".format(table, ",".join(columns)))
+    rows = [tuple(row[col] for col in columns) for row in r]
+    cur.executemany("INSERT INTO {} ({}) VALUES ({});".format(table, ",".join(columns), ",".join("?" for _ in columns)), rows)
+    con.commit()
+
+  while True:
+    x = input("> ")
+
+    if debug:
+      print("tokens:\t{}".format(tokenize(x)))
+      print("AST:\t{}".format(parse(x)))
+      print("query:\t\"{}\"".format(compile(x, table, columns)))
+
+    try:
+      compile(x, table, columns)
+      for row in cur.execute(compile(x, table, columns)):
+        print("\t".join(str(cell) for cell in row))
+    except:
+      print("Compilation error.")
+
+  # TODO(wpcarro): Trap exits and ensure cleanup always runs.
+  con.close()
+
+if __name__ == "__main__":
+  parser = ArgumentParser()
+  parser.add_argument("-f", "--file", dest="file", help="Path to the CSV from which to read", metavar="PATH")
+  parser.add_argument("-d", "--debug", dest="debug", default=False, action="store_true", help="Enable debugging")
+  args = parser.parse_args()
+  main(csv_path=args.file, debug=args.debug)
diff --git a/users/wpcarro/scratch/simple-select/parser.py b/users/wpcarro/scratch/simple-select/parser.py
new file mode 100644
index 0000000000..d26f970e57
--- /dev/null
+++ b/users/wpcarro/scratch/simple-select/parser.py
@@ -0,0 +1,31 @@
+class Parser(object):
+    def __init__(self, tokens):
+        self.tokens = tokens
+        self.i = 0
+
+    def exhausted(self):
+        return self.i >= len(self.tokens)
+
+    def peek(self, n=0):
+        return self.tokens[self.i + n]
+
+    def advance(self):
+        if not self.exhausted():
+            self.i += 1
+        return self.peek(n=-1)
+
+    def match(self, xs):
+        if self.peek() in xs:
+            self.advance()
+            return True
+        return False
+
+    def test(self, predicate):
+        return predicate(self.tokens, self.i)
+
+    def expect(self, predicate):
+        if self.exhausted():
+            raise Exception("Unexpected EOL")
+        if predicate(self.peek()):
+            return self.advance()
+        raise Exception("Unexpected token: \"{}\"".format(self.peek()))
diff --git a/users/wpcarro/scratch/simple-select/scanner.py b/users/wpcarro/scratch/simple-select/scanner.py
new file mode 100644
index 0000000000..5dae68aee5
--- /dev/null
+++ b/users/wpcarro/scratch/simple-select/scanner.py
@@ -0,0 +1,27 @@
+# According to Crafting Interpreters, the only two primitives that a
+# scanner/lexer needs are peek and advance; other functions (e.g. match) are
+# nice-to-haves.
+class Scanner(object):
+  def __init__(self, chars):
+    self.i = 0
+    self.chars = chars
+
+  def exhausted(self):
+    return self.i >= len(self.chars)
+
+  def peek(self, n=0):
+    return self.chars[self.i + n] if self.i in range(0, len(self.chars)) else '\0'
+
+  def advance(self):
+    result = self.peek()
+    self.i += 1
+    return result
+
+  def match(self, x):
+    if self.exhausted():
+      return False
+    if self.peek() == x:
+      self.advance()
+      return True
+    else:
+      return False
diff --git a/users/wpcarro/secrets.json.secret b/users/wpcarro/secrets.json.secret
new file mode 100644
index 0000000000..d4c02bf693
--- /dev/null
+++ b/users/wpcarro/secrets.json.secret
Binary files differdiff --git a/users/wpcarro/terraform/.gitignore b/users/wpcarro/terraform/.gitignore
new file mode 100644
index 0000000000..f437e99d80
--- /dev/null
+++ b/users/wpcarro/terraform/.gitignore
@@ -0,0 +1,4 @@
+*.tfstate
+*.tfstate.backup
+.terraform.lock.hcl
+.terraform/**/*
\ No newline at end of file
diff --git a/users/wpcarro/terraform/default.nix b/users/wpcarro/terraform/default.nix
new file mode 100644
index 0000000000..b8625c3b8d
--- /dev/null
+++ b/users/wpcarro/terraform/default.nix
@@ -0,0 +1,192 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  inherit (builtins) concatLists concatStringsSep toJSON unsafeDiscardStringContext;
+  inherit (depot.users) wpcarro;
+  inherit (pkgs) writeText;
+
+  images = import (pkgs.path + "/nixos/modules/virtualisation/gce-images.nix");
+  nixosImage = images."20.09";
+in
+{
+  googleCloudVM =
+    { project
+    , name
+    , region
+    , zone
+    , configuration
+    , extraConfig ? { }
+    ,
+    }:
+    let
+      inherit (configuration.users.users) root;
+      inherit (configuration.networking) firewall;
+
+      # Convert NixOS-style port numbers to Terraform-style.
+      asStrings = xs: map toString xs;
+      asRanges = xs: map (x: "${toString x.from}-${toString x.to}") xs;
+
+      sshKeys = concatStringsSep "\n"
+        (map (key: "root:${key}") root.openssh.authorizedKeys.keys);
+
+      os = depot.ops.nixos.nixosFor (_: {
+        imports = [
+          (pkgs.path + "/nixos/modules/virtualisation/google-compute-image.nix")
+          configuration
+        ];
+
+        networking.hostName = name;
+
+        fileSystems."/nix" = {
+          device = "/dev/disk/by-label/google-${name}-disk";
+          fsType = "ext4";
+        };
+      });
+
+      osRoot = os.config.system.build.toplevel;
+      osPath = unsafeDiscardStringContext (toString osRoot.outPath);
+      drvPath = unsafeDiscardStringContext (toString osRoot.drvPath);
+    in
+    {
+      inherit drvPath osPath;
+      json = writeText "terraform.tf.json" (toJSON (lib.recursiveUpdate extraConfig {
+        provider.google = {
+          inherit project region zone;
+        };
+
+        resource.google_compute_instance."${name}" = {
+          inherit name zone;
+          machine_type = "e2-standard-2";
+
+          tags = [
+            "http-server"
+            "https-server"
+            "${name}-firewall"
+          ];
+
+          boot_disk = {
+            device_name = "boot";
+            initialize_params = {
+              size = 10;
+              image = "projects/nixos-cloud/global/images/${nixosImage.name}";
+            };
+          };
+
+          attached_disk = {
+            source = "\${google_compute_disk.${name}.id}";
+            device_name = "${name}-disk";
+          };
+
+          network_interface = {
+            network = "default";
+            subnetwork = "default";
+            access_config = { };
+          };
+
+          # Copy root's SSH keys from the NixOS configuration and expose them to the
+          # metadata server.
+          metadata = {
+            inherit sshKeys;
+            ssh-keys = sshKeys;
+
+            # NixOS's fetch-instance-ssh-keys.bash relies on these fields being
+            # available on the metadata server.
+            ssh_host_ed25519_key = "\${tls_private_key.${name}.private_key_pem}";
+            ssh_host_ed25519_key_pub = "\${tls_private_key.${name}.public_key_pem}";
+
+            # Even though we have SSH access, having oslogin can still be useful for
+            # troubleshooting in the browser if for some reason SSH isn't working as
+            # expected.
+            enable-oslogin = "TRUE";
+          };
+
+          service_account.scopes = [ "cloud-platform" ];
+        };
+
+        resource.tls_private_key."${name}" = {
+          algorithm = "ECDSA";
+          ecdsa_curve = "P384";
+        };
+
+        resource.google_compute_firewall."${name}" = {
+          name = "${name}-firewall";
+          network = "default";
+
+          # Read the firewall configuration from the NixOS configuration.
+          allow = [
+            {
+              protocol = "tcp";
+              ports = concatLists [
+                (asStrings (firewall.allowedTCPPorts or [ ]))
+                (asRanges (firewall.allowedTCPPortRanges or [ ]))
+              ];
+            }
+            {
+              protocol = "udp";
+              ports = concatLists [
+                (asStrings (firewall.allowedUDPPorts or [ ]))
+                (asRanges (firewall.allowedUDPPortRanges or [ ]))
+              ];
+            }
+          ];
+          source_ranges = [ "0.0.0.0/0" ];
+        };
+
+        resource.google_compute_disk."${name}" = {
+          inherit zone;
+          name = "${name}-disk";
+          size = 100;
+        };
+
+        resource.null_resource.deploy_nixos = {
+          triggers = {
+            # Redeploy when the NixOS configuration changes.
+            os = "${osPath}";
+            # Redeploy when a new machine is provisioned.
+            machine_id = "\${google_compute_instance.${name}.id}";
+          };
+
+          connection = {
+            host = "\${google_compute_instance.${name}.network_interface[0].access_config[0].nat_ip}";
+          };
+
+          provisioner = [
+            { remote-exec.inline = [ "true" ]; }
+            {
+              local-exec.command = ''
+                export PATH="${pkgs.openssh}/bin:$PATH"
+
+                scratch="$(mktemp -d)"
+                function cleanup() {
+                  rm -rf $scratch
+                }
+                trap cleanup EXIT
+
+                # write out ssh key
+                echo -n "''${tls_private_key.${name}.private_key_pem}" > $scratch/id_rsa.pem
+                chmod 0600 $scratch/id_rsa.pem
+
+                export NIX_SSHOPTS="\
+                  -o StrictHostKeyChecking=no\
+                  -o UserKnownHostsFile=/dev/null\
+                  -o GlobalKnownHostsFile=/dev/null\
+                  -o IdentityFile=$scratch/id_rsa.pem
+                "
+
+                nix-build ${drvPath}
+                nix-copy-closure --to \
+                  root@''${google_compute_instance.${name}.network_interface[0].access_config[0].nat_ip} \
+                  ${osPath} --gzip --use-substitutes
+              '';
+            }
+            {
+              remote-exec.inline = [
+                "nix-env --profile /nix/var/nix/profiles/system --set ${osPath}"
+                "${osPath}/bin/switch-to-configuration switch"
+              ];
+            }
+          ];
+        };
+      }));
+    };
+}
diff --git a/users/wpcarro/todo-lists/cta-curriculum.csv b/users/wpcarro/todo-lists/cta-curriculum.csv
new file mode 100644
index 0000000000..7ebc82d645
--- /dev/null
+++ b/users/wpcarro/todo-lists/cta-curriculum.csv
@@ -0,0 +1,108 @@
+name,position,goal

+collar choke,back,submission

+bow & arrow choke,back,submission

+rear naked choke,back,submission

+armlock,back,submission

+collar choke escape,back,escape

+framing defense,back,defense

+pulling the arm to the other side (I),back,defense

+pulling the arm to the other side (II),back,defense

+back escape to 1x-leg,back,escape

+changing sides,back,defense

+opening closed guard (sleeve),closed guard,escape

+opening closed guard (hips),closed guard,escape

+opening closed guard (hips -> sleeve),closed guard,escape

+catucada (I),closed guard,sweep

+catucada (II),closed guard,sweep

+sit-up sweep,closed guard,sweep

+scissor sweep,closed guard,sweep

+2x ankle sweep,closed guard,sweep

+sit-up sweep -> kimura (I),closed guard,submission

+sit-up sweep -> kimura (variations),closed guard,submission

+omoplata,closed guard,submission

+omoplata escape -> side control,closed guard,escape

+omoplata escape -> standing,closed guard,escape

+overhook triangle (I),closed guard,submission

+overhook triangle (II),closed guard,submission

+armlock,closed guard,submission

+flower sweep,closed guard,sweep

+kimura,closed guard,submission

+triangle defense,closed guard,defense

+triangle escape,closed guard,escape

+armlock escape,closed guard,escape

+half guard -> closed guard (I),half guard,transition

+half guard -> closed guard (II),half guard,transition

+upa,half guard,sweep

+half guard -> back,half guard,transition

+underhook sweep,half guard,sweep

+knee slide pass (backstep),half guard,pass

+knee slide pass (hip-switch, knee-cut),half guard,pass

+knee slide pass (push the knee),half guard,pass

+knee slide pass (2x-hook magic),half guard,pass

+tripod pass (backstep),half guard,pass

+tripod pass (hip-switch, knee-cut),half guard,pass

+tripod pass (push the knee),half guard,pass

+tripod pass (2x-hook magic),half guard,pass

+keylock,mount,submission

+keylock -> armlock (I),mount,submission

+keylock -> armlock (II),mount,submission

+upa,mount,sweep

+cross-choke defense,mount,defense

+keylock escape,mount,escape

+hip press escape (straight back),mount,escape

+hip press escape (sideways),mount,escape

+elbow escape,mount,escape

+ezekiel choke,mount,submission

+retaining low mount,mount,retention

+retaining high mount,mount,retention

+armlock,mount,submission

+armlock escape,mount,escape

+armlock (breaking the grips),mount,submission

+cross-choke (I),mount,submission

+cross-choke (II),mount,submission

+bull pass,open guard,pass

+2x-under,open guard,pass

+1x-under,open guard,pass

+1x-under -> half guard,open guard,pass

+straight ankle lock,open guard,submission

+straight ankle lock defense,open guard,defense

+straight ankle lock defense -> mount,open guard,escape

+side control -> mount,side control,transition

+armlock (same side),side control,submission

+armlock escape (hitchhiker),side control,escape

+kimura,side control,submission

+kimura -> armlock,side control,submission

+kimura (breaking the grips),side control,submission

+escape (doorstop),side control,escape

+modern hip escape,side control,escape

+escape,side control,escape

+kesagatame escape,side control,escape

+kesagatame escape (from punches),side control,escape

+retention,knee on belly,retention

+armlock,knee on belly,submission

+knee on belly escape,knee on belly,escape

+knee on belly -> mount,knee on belly,transition

+pulling closed guard,standing,transition

+pulling to armbar,standing,submission

+pendulum sweep,standing,sweep

+2x-ankle sweep,standing,sweep

+collar drag to 1x-leg,standing,sweep

+collar drag sweep,standing,sweep

+collar drag (seated),standing,transition

+hip throw from neck control,standing,escape

+hip throw to armbar,standing,submission

+osoto gari from neck control,standing,escape

+osoto gari to armbar,standing,submission

+basic osoto gari,standing,takedown

+1x-leg,standing,takedown

+guillotine (arm out),standing,submission

+guillotine (arm out) escape,standing,escape

+headlock escape,standing,escape

+headlock escape (from punches),standing,escape

+guillotine (arm in),standing,submission

+guillotine (arm in) escape,standing,escape

+outside trip -> 2x-leg,standing,takedown

+bear hug escape,standing,escape

+body lock escape,standing,escape

+2x-leg,standing,takedown

+2x-leg sprawl defense to back,standing,defense
\ No newline at end of file
diff --git a/users/wpcarro/todo-lists/imdb/db.sqlite3 b/users/wpcarro/todo-lists/imdb/db.sqlite3
new file mode 100644
index 0000000000..bb893387ec
--- /dev/null
+++ b/users/wpcarro/todo-lists/imdb/db.sqlite3
Binary files differdiff --git a/users/wpcarro/todo-lists/imdb/imdb-top-250.org b/users/wpcarro/todo-lists/imdb/imdb-top-250.org
new file mode 100644
index 0000000000..58a52392ca
--- /dev/null
+++ b/users/wpcarro/todo-lists/imdb/imdb-top-250.org
@@ -0,0 +1,256 @@
+# A few years ago, I set a goal to watch every movie on IMDb.com's "Top 250"
+# movies list. The list changes frequently, so I took a snapshot of it so that
+# I wouldn't be trying to hit a moving target.
+#
+# Here is my progress thus far:
+* IMDB Top 250
+** DONE The Shawshank Redemption
+** DONE The Godfather
+** DONE The Dark Knight
+** DONE The Godfather: Part II
+** DONE The Lord of the Rings: The Return of the King
+** DONE Pulp Fiction
+** DONE Schindler's List
+** DONE The Good, the Bad and the Ugly
+** DONE 12 Angry Men
+** DONE Inception
+** DONE Fight Club
+** DONE The Lord of the Rings: The Fellowship of the Ring
+** DONE Forrest Gump
+** DONE The Lord of the Rings: The Two Towers
+** DONE The Matrix
+** DONE Goodfellas
+** TODO Star Wars: Episode V - The Empire Strikes Back
+** DONE One Flew Over the Cuckoo's Nest
+** DONE Seven Samurai
+** DONE Interstellar
+** DONE City of God
+** TODO Spirited Away
+** DONE Saving Private Ryan
+** DONE The Green Mile
+** DONE Life Is Beautiful
+** DONE The Usual Suspects
+** DONE Se7en
+** DONE Leon
+** DONE The Silence of the Lambs
+** TODO Star Wars: Episode IV - A New Hope
+** DONE It's a Wonderful Life
+** DONE Andhadhun
+** DONE Dangal
+** DONE Spider-Man: Into the Spider-Verse
+** TODO Avengers: Infinity War
+** DONE Whiplash
+** DONE Untouchable
+** DONE The Prestige
+** DONE The Departed
+** DONE The Pianist
+** DONE Memento
+** DONE Gladiator
+** DONE American History X
+** DONE The Lion King
+** DONE Terminator 2: Judgment Day
+** DONE Cinema Paradiso
+** DONE Grave of the Fireflies
+** DONE Back to the Future
+** DONE Indiana Jones and the Raiders of the Lost Ark
+** DONE Apocalypse Now
+** TODO Alien
+** DONE Once Upon a Time in the West
+** DONE Psycho
+** DONE Rear Window
+** DONE Casablanca
+** TODO The Great Dictator
+** TODO Modern Times
+** TODO City Lights
+** TODO Kimi no na wa.
+** DONE Coco
+** DONE Django Unchained
+** DONE The Dark Knight Rises
+** DONE 3 Idiots
+** TODO Taare Zameen Par
+** DONE WALL·E
+** TODO Babam ve Oglum
+** DONE The Lives of Others
+** DONE Old boy
+** DONE American Beauty
+** DONE Princess Mononoke
+** DONE Braveheart
+** TODO Aliens
+** DONE Once Upon a Time in America
+** TODO Das Boot
+** DONE The Shining
+** DONE Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb
+** TODO Witness for the Prosecution
+** DONE Paths of Glory
+** TODO Sunset Blvd.
+** DONE Green Book
+** DONE The Hunt
+** DONE Jodaeiye Nader az Simin
+** DONE Incendies
+** DONE Toy Story 3
+** DONE Inglourious Basterds
+** DONE Eternal Sunshine of the Spotless Mind
+** DONE Amelie
+** DONE Snatch
+** DONE Requiem for a Dream
+** TODO Neon Genesis Evangelion: The End of Evangelion
+** DONE L.A. Confidential
+** DONE Good Will Hunting
+** TODO Bacheha-Ye aseman
+** TODO Eskiya
+** DONE Toy Story
+** DONE Reservoir Dogs
+** DONE Full Metal Jacket
+** DONE Amadeus
+** DONE Scarface
+** TODO Star Wars: Episode VI - Return of the Jedi
+** DONE Taxi Driver
+** DONE Monty Python and the Holy Grail
+** DONE The Sting
+** DONE A Clockwork Orange
+** DONE 2001: A Space Odyssey
+** TODO For a Few Dollars More
+** TODO To Kill a Mockingbird
+** TODO Lawrence of Arabia
+** TODO Yojimbo
+** DONE The Apartment
+** TODO North by Northwest
+** DONE Vertigo
+** TODO Singin' in the Rain
+** TODO Ikiru
+** TODO Rashomon
+** TODO All About Eve
+** TODO Bicycle Thieves
+** TODO Double Indemnity
+** TODO Citizen Kane
+** TODO M
+** TODO Metropolis
+** TODO The Kid
+** DONE Three Billboards Outside Ebbing, Missouri
+** DONE Room
+** TODO PK
+** DONE Inside Out
+** DONE El secreto de sus ojos
+** DONE Warrior
+** DONE Up
+** DONE The Wolf of Wall Street
+** DONE There Will Be Blood
+** DONE Pan's Labyrinth
+** DONE V for Vendetta
+** TODO Rang De Basanti
+** DONE Batman Begins
+** DONE Downfall
+** TODO Howl's Moving Castle
+** DONE A Beautiful Mind
+** DONE Lock, Stock and Two Smoking Barrels
+** DONE Trainspotting
+** DONE Heat
+** DONE Casino
+** DONE Unforgiven
+** TODO Indiana Jones and the Last Crusade
+** DONE My Neighbour Totoro
+** DONE Die Hard
+** TODO Come and See
+** TODO Ran
+** DONE Blade Runner
+** DONE Raging Bull
+** TODO The Elephant Man
+** DONE Chinatown
+** TODO Andrei Rublev
+** DONE The Great Escape
+** TODO Judgment at Nuremberg
+** TODO Some Like It Hot
+** TODO Wild Strawberries
+** TODO The Seventh Seal
+** TODO The Bridge on the River Kwai
+** TODO On the Waterfront
+** TODO Dial M for Murder
+** TODO Tokyo Story
+** TODO The Third Man
+** TODO The Treasure of the Sierra Madre
+** TODO Mr. Smith Goes to Washington
+** TODO Gone with the Wind
+** TODO Sunrise: A Song of Two Humans
+** TODO The General
+** TODO The Gold Rush
+** TODO Sherlock Jr.
+** DONE The Handmaiden
+** DONE Logan
+** TODO Relatos salvajes
+** DONE The Grand Budapest Hotel
+** DONE Gone Girl
+** DONE Hacksaw Ridge
+** TODO 12 Years a Slave
+** DONE Guardians of the Galaxy
+** DONE Rush
+** DONE Spotlight
+** TODO Song of the Sea
+** TODO The Help
+** DONE Prisoners
+** DONE Mad Max: Fury Road
+** DONE Gran Torino
+** TODO Harry Potter and the Deathly Hallows: Part 2
+** DONE Shutter Island
+** DONE Hachi: A Dog's Tale
+** DONE Mary and Max
+** DONE How to Train Your Dragon
+** DONE Into the Wild
+** DONE No Country for Old Men
+** DONE Million Dollar Baby
+** DONE Hotel Rwanda
+** TODO Before Sunset
+** TODO Memories of Murder
+** DONE Kill Bill: Vol. 1
+** DONE Finding Nemo
+** DONE Catch Me If You Can
+** TODO Donnie Darko
+** DONE Amores Perros
+** DONE Monsters, Inc.
+** DONE The Sixth Sense
+** DONE The Truman Show
+** DONE The Big Lebowski
+** TODO In the Mood for Love
+** DONE Fargo
+** TODO La Haine
+** TODO Before Sunrise
+** TODO Three Colours: Red
+** DONE Jurassic Park
+** DONE In the Name of the Father
+** DONE Dead Poets Society
+** TODO Akira
+** DONE The Princess Bride
+** TODO Laputa: Castle in the Sky
+** DONE Stand by Me
+** DONE Platoon
+** TODO Paris, Texas
+** TODO Nausicaa of the Valley of the Wind
+** DONE The Thing
+** TODO Gandhi
+** TODO Fanny and Alexander
+** TODO Stalker
+** DONE Life of Brian
+** DONE The Deer Hunter
+** TODO Rocky
+** TODO Network
+** TODO Barry Lyndon
+** TODO Butch Cassidy and the Sundance Kid
+** DONE Cool Hand Luke
+** TODO Persona
+** TODO The 400 Blows
+** TODO Ben-Hur
+** TODO The Nights of Cabiria
+** TODO Les Diaboliques
+** TODO The Wages of Fear
+** TODO The Best Years of Our Lives
+** TODO The Maltese Falcon
+** TODO Rebecca
+** TODO The Grapes of Wrath
+** TODO It Happened One Night
+** TODO La passion de Jeanne d'Arc
+** DONE Pirates of the Caribbean: The Curse of the Black Pearl
+** DONE Groundhog Day
+** DONE Beauty and the Beast
+** DONE The Terminator
+** DONE Jaws
+** DONE The Exorcist
+** DONE The Wizard of Oz
diff --git a/users/wpcarro/todo-lists/imdb/scratch.sql b/users/wpcarro/todo-lists/imdb/scratch.sql
new file mode 100644
index 0000000000..6835c73bd8
--- /dev/null
+++ b/users/wpcarro/todo-lists/imdb/scratch.sql
@@ -0,0 +1,65 @@
+-- which directors appear most often
+SELECT director, COUNT(*)
+FROM Movies
+GROUP BY director
+ORDER BY COUNT(*) DESC
+LIMIT 10;
+
+-- top-rated, most recent movies
+SELECT *
+FROM (
+  SELECT *
+  FROM Movies
+  ORDER BY rating DESC
+  LIMIT 20
+)
+ORDER BY YEAR DESC;
+
+-- top-rated, most recent movies (ignore foreign)
+SELECT *
+FROM (
+  SELECT *
+  FROM Movies
+  WHERE requiresSubtitles = 0
+  ORDER BY rating DESC
+  LIMIT 20
+)
+ORDER BY YEAR DESC;
+
+-- most recent movies
+SELECT *
+FROM Movies
+ORDER BY YEAR DESC
+LIMIT 15;
+
+-- most recent movies (ignore foreign)
+SELECT *
+FROM Movies
+WHERE requiresSubtitles = 0
+ORDER BY YEAR DESC
+LIMIT 10;
+
+-- only cartoons
+SELECT *
+FROM Movies
+WHERE isCartoon = true;
+
+-- only cartoons (ignore foreign)
+SELECT *
+FROM Movies
+WHERE isCartoon = true AND requiresSubtitles = false;
+
+-- show the movies from the directors that show up on the list more than once.
+SELECT *
+FROM Movies
+WHERE director in (
+  SELECT director
+  FROM (
+    SELECT director, COUNT(*) as num
+    FROM Movies
+    GROUP BY director
+    HAVING num > 1
+    ORDER BY num DESC
+  )
+)
+ORDER BY director, rating DESC, year DESC;
diff --git a/users/wpcarro/todo-lists/paul-graham-essays.org b/users/wpcarro/todo-lists/paul-graham-essays.org
new file mode 100644
index 0000000000..7cddcef478
--- /dev/null
+++ b/users/wpcarro/todo-lists/paul-graham-essays.org
@@ -0,0 +1,190 @@
+# I'd like to read all of Paul Graham's essays. I cannot rely on my web browser
+# to tell me which I've already read, so I'm resorting to an org file.
+* TODO How to Write Usefully
+* DONE Being a Noob
+* TODO Haters
+* TODO The Two Kinds of Moderate
+* TODO Fashionable Problems
+* TODO Having Kids
+* DONE The Lesson to Unlearn
+* TODO Novelty and Heresy
+* TODO The Bus Ticket Theory of Genius
+* TODO General and Surprising
+* DONE Charisma / Power
+* TODO The Risk of Discovery
+* TODO How to Make Pittsburgh a Startup Hub
+* TODO Life is Short
+* TODO Economic Inequality
+* TODO The Refragmentation
+* TODO Jessica Livingston
+* TODO A Way to Detect Bias
+* TODO Write Like You Talk
+* TODO Default Alive or Default Dead?
+* TODO Why It's Safe for Founders to Be Nice
+* TODO Change Your Name
+* TODO What Microsoft Is this the Altair Basic of?
+* TODO The Ronco Principle
+* TODO What Doesn't Seem Like Work?
+* TODO Don't Talk to Corp Dev
+* TODO Let the Other 95% of Great Programmers In
+* TODO How to Be an Expert in a Changing World
+* TODO How You Know
+* TODO The Fatal Pinch
+* DONE Mean People Fail
+* TODO Before the Startup
+* TODO How to Raise Money
+* TODO Investor Herd Dynamics
+* TODO How to Convince Investors
+* TODO Do Things that Don't Scale
+* TODO Startup Investing Trends
+* TODO How to Get Startup Ideas
+* TODO The Hardware Renaissance
+* TODO Startup = Growth
+* TODO Black Swan Farming
+* TODO The Top of My Todo List
+* TODO Writing and Speaking
+* TODO How Y Combinator Started
+* TODO Defining Property
+* TODO Frighteningly Ambitious Startup Ideas
+* TODO A Word to the Resourceful
+* TODO Schlep Blindness
+* TODO Snapshot: Viaweb, June 1998
+* TODO Why Startup Hubs Work
+* TODO The Patent Pledge
+* TODO Subject: Airbnb
+* TODO Founder Control
+* TODO Tablets
+* TODO What We Look for in Founders
+* TODO The New Funding Landscape
+* TODO Where to See Silicon Valley
+* TODO High Resolution Fundraising
+* TODO What Happened to Yahoo
+* TODO The Future of Startup Funding
+* TODO The Acceleration of Addictiveness
+* TODO The Top Idea in Your Mind
+* TODO How to Lose Time and Money
+* TODO Organic Startup Ideas
+* TODO Apple's Mistake
+* TODO What Startups Are Really Like
+* TODO Persuade xor Discover
+* TODO Post-Medium Publishing
+* TODO The List of N Things
+* TODO The Anatomy of Determination
+* TODO What Kate Saw in Silicon Valley
+* TODO The Trouble with the Segway
+* TODO Ramen Profitable
+* DONE Maker's Schedule, Manager's Schedule
+* TODO A Local Revolution?
+* TODO Why Twitter is a Big Deal
+* TODO The Founder Visa
+* TODO Five Founders
+* TODO Relentlessly Resourceful
+* TODO How to Be an Angel Investor
+* TODO Why TV Lost
+* TODO Can You Buy a Silicon Valley?  Maybe.
+* TODO What I've Learned from Hacker News
+* TODO Startups in 13 Sentences
+* TODO Keep Your Identity Small
+* TODO After Credentials
+* TODO Could VC be a Casualty of the Recession?
+* TODO The High-Res Society
+* TODO The Other Half of "Artists Ship"
+* TODO Why to Start a Startup in a Bad Economy
+* TODO A Fundraising Survival Guide
+* TODO The Pooled-Risk Company Management Company
+* TODO Cities and Ambition
+* TODO Disconnecting Distraction
+* TODO Lies We Tell Kids
+* TODO Be Good
+* TODO Why There Aren't More Googles
+* TODO Some Heroes
+* TODO How to Disagree
+* TODO You Weren't Meant to Have a Boss
+* TODO A New Venture Animal
+* TODO Trolls
+* TODO Six Principles for Making New Things
+* TODO Why to Move to a Startup Hub
+* TODO The Future of Web Startups
+* TODO How to Do Philosophy
+* TODO News from the Front
+* TODO How Not to Die
+* TODO Holding a Program in One's Head
+* TODO Stuff
+* TODO The Equity Equation
+* TODO An Alternative Theory of Unions
+* TODO The Hacker's Guide to Investors
+* TODO Two Kinds of Judgement
+* TODO Microsoft is Dead
+* TODO Why to Not Not Start a Startup
+* TODO Is It Worth Being Wise?
+* TODO Learning from Founders
+* TODO How Art Can Be Good
+* TODO The 18 Mistakes That Kill Startups
+* TODO A Student's Guide to Startups
+* TODO How to Present to Investors
+* TODO Copy What You Like
+* TODO The Island Test
+* TODO The Power of the Marginal
+* TODO Why Startups Condense in America
+* TODO How to Be Silicon Valley
+* TODO The Hardest Lessons for Startups to Learn
+* TODO See Randomness
+* TODO Are Software Patents Evil?
+* TODO 6,631,372
+* TODO Why YC
+* TODO How to Do What You Love
+* TODO Good and Bad Procrastination
+* TODO Web 2.0
+* TODO How to Fund a Startup
+* TODO The Venture Capital Squeeze
+* TODO Ideas for Startups
+* TODO What I Did this Summer
+* TODO Inequality and Risk
+* TODO After the Ladder
+* TODO What Business Can Learn from Open Source
+* TODO Hiring is Obsolete
+* TODO The Submarine
+* TODO Why Smart People Have Bad Ideas
+* TODO Return of the Mac
+* DONE Writing,  Briefly
+* TODO Undergraduation
+* TODO A Unified Theory of VC Suckage
+* TODO How to Start a Startup
+* TODO What You'll Wish You'd Known
+* TODO Made in USA
+* TODO It's Charisma, Stupid
+* TODO Bradley's Ghost
+* TODO A Version 1.0
+* TODO What the Bubble Got Right
+* TODO The Age of the Essay
+* TODO The Python Paradox
+* TODO Great Hackers
+* TODO Mind the Gap
+* TODO How to Make Wealth
+* TODO The Word "Hacker"
+* TODO What You Can't Say
+* TODO Filters that Fight Back
+* TODO Hackers and Painters
+* TODO If Lisp is So Great
+* TODO The Hundred-Year Language
+* TODO Why Nerds are Unpopular
+* TODO Better Bayesian Filtering
+* TODO Design and Research
+* TODO A Plan for Spam
+* TODO Revenge of the Nerds
+* TODO Succinctness is Power
+* TODO What Languages Fix
+* DONE Taste for Makers
+* TODO Why Arc Isn't Especially Object-Oriented
+* TODO What Made Lisp Different
+* TODO The Other Road Ahead
+* TODO The Roots of Lisp
+* DONE Five Questions about Language Design
+* DONE Being Popular
+* DONE Java's Cover
+* DONE Beating the Averages
+* DONE Lisp for Web-Based Applications
+* TODO Chapter 1 of Ansi Common Lisp
+* TODO Chapter 2 of Ansi Common Lisp
+* DONE Programming Bottom-Up
+* DONE This Year We Can End the Death Penalty in California
diff --git a/users/wpcarro/todo-lists/travel-hitlist.md b/users/wpcarro/todo-lists/travel-hitlist.md
new file mode 100644
index 0000000000..058ff6b274
--- /dev/null
+++ b/users/wpcarro/todo-lists/travel-hitlist.md
@@ -0,0 +1,83 @@
+# Hit List
+
+A crude journal of cities I have visited and cities I would like to visit.
+
+# Europe
+* ~~Berlin, Germany~~
+* ~~Hamburg, Germany~~
+* Munich, Germany
+* Heidelberg, Germany
+* ~~Geneva, Switzerland~~
+* Bern, Switzerland
+* Zurich, Switzerland
+* Lausanne, Switzerland
+* ~~Grenoble, France~~
+* ~~Lyons, France~~
+* ~~Paris, France~~
+* ~~Aix-en-Provence, France~~
+* ~~Bordeaux, France~~
+* Monaco, France
+* ~~Ibiza, Spain~~
+* ~~Formentera, Spain~~
+* Barcelona, Spain
+* ~~Lisbon, Portugal~~
+* ~~Lagos, Portugal~~
+* ~~Rome, Italy~~
+* ~~Venice, Italy~~
+* Cinque Terre, Italy
+* Milan, Italy
+* Florence, Italy
+* Oslo, Norway
+* Bergen, Norway
+* Copenhagen, Denmark
+* Reykjavik, Iceland
+* Stockholm, Sweden
+* Gothenburg, Sweden
+* ~~Amsterdam, Netherlands~~
+* Dubrovnik, Croatia
+* Split, Croatia
+* Lake Bled, Slovenia
+* Santorini, Greece
+* Vienna, Austria
+* Salzburg, Austria
+* Hallstatt, Austria
+* St. Petersburg, Russia
+* ~~London, England~~
+* Cambridge, England
+* Chester, England
+* Edinburgh, Scotland
+* ~~Dublin, Ireland~~
+* Galway, Ireland
+* Luxembourg, Luxembourg
+* Cappadocia, Turkey
+* Istanbul, Turkey
+* Ankara, Turkey
+
+# North and South America
+* Montreal, Canada
+* Quebec City, Canada
+* Vancouver, Canada
+* Oahu Hawaii, USA
+* Chicago, USA
+* New Orleans, USA
+* Mexico City, Mexico
+* Cabo San Lucas, Mexico
+* Rio de Janerio, Brazil
+* Cartegena, Colombia
+
+# Asia / Pacific
+* Gold Coast, Australia
+* Sydney, Australia
+* Auckland, New Zealand
+* Kohphiphi Islands, Thailand
+* Hong Kong, China
+* Shanghai, China
+* Xitang, China
+* Tokyo, Japan
+* Kyoto, Japan
+* Seoul, South Korea
+
+# Middle East
+* Jaffa, Israel
+* Tel Aviv, Israel
+* Beirut, Lebanon
diff --git a/users/wpcarro/tools/monzo_ynab/.envrc b/users/wpcarro/tools/monzo_ynab/.envrc
new file mode 100644
index 0000000000..2e3b53cd61
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/.envrc
@@ -0,0 +1,9 @@
+source_up
+use_nix
+# TODO(wpcarro): Prefer age-nix solution if possible.
+export monzo_client_id="$(jq -j '.monzo | .clientId' < $WPCARRO/secrets.json)"
+export monzo_client_secret="$(jq -j '.monzo | .clientSecret' < $WPCARRO/secrets.json)"
+export ynab_personal_access_token="$(jq -j '.ynab | .personalAccessToken' < $WPCARRO/secrets.json)"
+export ynab_account_id="$(jq -j '.ynab | .accountId' < $WPCARRO/secrets.json)"
+export ynab_budget_id="$(jq -j '.ynab | .budgetId' < $WPCARRO/secrets.json)"
+export store_path="$(pwd)"
diff --git a/users/wpcarro/tools/monzo_ynab/.gitignore b/users/wpcarro/tools/monzo_ynab/.gitignore
new file mode 100644
index 0000000000..e92078303b
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/.gitignore
@@ -0,0 +1,3 @@
+/ynab/fixture.json
+/monzo/fixture.json
+/kv.json
diff --git a/users/wpcarro/tools/monzo_ynab/.skip-subtree b/users/wpcarro/tools/monzo_ynab/.skip-subtree
new file mode 100644
index 0000000000..8db1f814f6
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/.skip-subtree
@@ -0,0 +1,2 @@
+Subdirectories of this folder should not be imported since they are
+internal to buildGo.nix and incompatible with readTree.
diff --git a/users/wpcarro/tools/monzo_ynab/README.md b/users/wpcarro/tools/monzo_ynab/README.md
new file mode 100644
index 0000000000..c0c0c772f6
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/README.md
@@ -0,0 +1,41 @@
+# monzo_ynab
+
+Exporting Monzo transactions to my YouNeedABudget.com (i.e. YNAB) account. YNAB
+unfortunately doesn't currently offer an Monzo integration. As a workaround and
+a practical excuse to learn Go, I decided to write one myself.
+
+This job is going to run N times per 24 hours. Monzo offers webhooks for
+reacting to certain types of events. I don't expect I'll need realtime data for
+my YNAB integration. That may change, however, so it's worth noting.
+
+## Installation
+
+Like many other packages in this repository, `monzo_ynab` is packaged using
+Nix. To install and use, you have two options:
+
+You can install using `nix-build` and then run the resulting
+`./result/bin/monzo_ynab`.
+
+```shell
+> nix-build . && ./result/bin/monzo_ynab
+```
+
+Or you can install using `nix-env` if you'd like to create the `monzo_ynab`
+symlink.
+
+```shell
+> nix-env -iA users.wpcarro.monzo_ynab
+```
+
+## Deployment
+
+While this project is currently not deployed, my plan is to host it on Google
+Cloud and run it as a Cloud Run application. What I don't yet know is whether or
+not this is feasible or a good idea. One complication that I foresee is that the
+OAuth 2.0 login flow requires a web browser until the access token and refresh
+tokens are acquired. I'm unsure how to workaround this at the moment.
+
+For more information about the general packaging and deployment strategies I'm
+currently using, refer to the [deployments][deploy] writeup.
+
+[deploy]: ../deploy/README.md
diff --git a/users/wpcarro/tools/monzo_ynab/auth.go b/users/wpcarro/tools/monzo_ynab/auth.go
new file mode 100644
index 0000000000..b66bacb106
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/auth.go
@@ -0,0 +1,101 @@
+package auth
+
+////////////////////////////////////////////////////////////////////////////////
+// Dependencies
+////////////////////////////////////////////////////////////////////////////////
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"utils"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Constants
+////////////////////////////////////////////////////////////////////////////////
+
+var (
+	BROWSER      = os.Getenv("BROWSER")
+	REDIRECT_URI = "http://localhost:8080/authorization-code"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Types
+////////////////////////////////////////////////////////////////////////////////
+
+// This is the response returned from Monzo when we exchange our authorization
+// code for an access token. While Monzo returns additional fields, I'm only
+// interested in AccessToken and RefreshToken.
+type accessTokenResponse struct {
+	AccessToken  string `json:"access_token"`
+	RefreshToken string `json:"refresh_token"`
+	ExpiresIn    int    `json:"expires_in"`
+}
+
+type Tokens struct {
+	AccessToken  string
+	RefreshToken string
+	ExpiresIn    int
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Functions
+////////////////////////////////////////////////////////////////////////////////
+
+// Returns the access token and refresh tokens for the Monzo API.
+func GetTokensFromAuthCode(authCode string, clientID string, clientSecret string) *Tokens {
+	res, err := http.PostForm("https://api.monzo.com/oauth2/token", url.Values{
+		"grant_type":    {"authorization_code"},
+		"client_id":     {clientID},
+		"client_secret": {clientSecret},
+		"redirect_uri":  {REDIRECT_URI},
+		"code":          {authCode},
+	})
+	utils.FailOn(err)
+	defer res.Body.Close()
+	payload := &accessTokenResponse{}
+	json.NewDecoder(res.Body).Decode(payload)
+
+	return &Tokens{payload.AccessToken, payload.RefreshToken, payload.ExpiresIn}
+}
+
+// Open a web browser to allow the user to authorize this application. Return
+// the authorization code sent from Monzo.
+func GetAuthCode(clientID string) string {
+	// TODO(wpcarro): Consider generating a random string for the state when the
+	// application starts instead of hardcoding it here.
+	state := "xyz123"
+	url := fmt.Sprintf(
+		"https://auth.monzo.com/?client_id=%s&redirect_uri=%s&response_type=code&state=%s",
+		clientID, REDIRECT_URI, state)
+	exec.Command(BROWSER, url).Start()
+
+	authCode := make(chan string)
+	go func() {
+		log.Fatal(http.ListenAndServe(":8080",
+			http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				// 1. Get authorization code from Monzo.
+				if req.URL.Path == "/authorization-code" {
+					params := req.URL.Query()
+					reqState := params["state"][0]
+					code := params["code"][0]
+
+					if reqState != state {
+						log.Fatalf("Value for state returned by Monzo does not equal our state. %s != %s", reqState, state)
+					}
+					authCode <- code
+
+					fmt.Fprintf(w, "Authorized!")
+				} else {
+					log.Printf("Unhandled request: %v\n", *req)
+				}
+			})))
+	}()
+	result := <-authCode
+	return result
+}
diff --git a/users/wpcarro/tools/monzo_ynab/job.nix b/users/wpcarro/tools/monzo_ynab/job.nix
new file mode 100644
index 0000000000..f710b73cef
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/job.nix
@@ -0,0 +1,15 @@
+{ depot, ... }:
+
+let
+  inherit (depot.users.wpcarro) gopkgs;
+in
+depot.nix.buildGo.program {
+  name = "job";
+  srcs = [
+    ./main.go
+  ];
+  deps = with gopkgs; [
+    kv
+    utils
+  ];
+}
diff --git a/users/wpcarro/tools/monzo_ynab/main.go b/users/wpcarro/tools/monzo_ynab/main.go
new file mode 100644
index 0000000000..bf37071381
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/main.go
@@ -0,0 +1,44 @@
+// Exporting Monzo transactions to my YouNeedABudget.com (i.e. YNAB)
+// account. YNAB unfortunately doesn't currently offer an Monzo integration. As
+// a workaround and a practical excuse to learn Go, I decided to write one
+// myself.
+//
+// This job is going to run N times per 24 hours. Monzo offers webhooks for
+// reacting to certain types of events. I don't expect I'll need realtime data
+// for my YNAB integration. That may change, however, so it's worth noting.
+
+package main
+
+import (
+	"monzoSerde"
+	"os"
+)
+
+var (
+	ynabAccountID = os.Getenv("ynab_account_id")
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Business Logic
+////////////////////////////////////////////////////////////////////////////////
+
+// Convert a Monzo transaction struct, `tx`, into a YNAB transaction struct.
+func toYnab(tx monzoSerde.Transaction) ynabSerde.Transaction {
+	return ynabSerde.Transaction{
+		Id:        tx.Id,
+		Date:      tx.Created,
+		Amount:    tx.Amount,
+		Memo:      tx.Notes,
+		AccountId: ynabAccountID,
+	}
+}
+
+func main() {
+	txs := monzo.TransactionsLast24Hours()
+	var ynabTxs []ynabSerde.Transaction
+	for tx := range txs {
+		append(ynabTxs, toYnab(tx))
+	}
+	ynab.PostTransactions(ynabTxs)
+	os.Exit(0)
+}
diff --git a/users/wpcarro/tools/monzo_ynab/monzo/client.go b/users/wpcarro/tools/monzo_ynab/monzo/client.go
new file mode 100644
index 0000000000..8c6c41e29f
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/monzo/client.go
@@ -0,0 +1,52 @@
+package monzoClient
+
+import (
+	"fmt"
+	"log"
+	"monzoSerde"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+	"tokens"
+	"utils"
+)
+
+const (
+	accountID = "pizza"
+)
+
+type Client struct{}
+
+// Ensure that the token server is running and return a new instance of a Client
+// struct.
+func Create() *Client {
+	tokens.StartServer()
+	time.Sleep(time.Second * 1)
+	return &Client{}
+}
+
+// Returns a slice of transactions from the last 24 hours.
+func (c *Client) Transactions24Hours() []monzoSerde.Transaction {
+	token := tokens.AccessToken()
+	form := url.Values{"account_id": {accountID}}
+	client := http.Client{}
+	req, _ := http.NewRequest("POST", "https://api.monzo.com/transactions",
+		strings.NewReader(form.Encode()))
+	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Add("User-Agent", "monzo-ynab")
+	res, err := client.Do(req)
+
+	utils.DebugRequest(req)
+	utils.DebugResponse(res)
+
+	if err != nil {
+		utils.DebugRequest(req)
+		utils.DebugResponse(res)
+		log.Fatal(err)
+	}
+	defer res.Body.Close()
+
+	return []monzoSerde.Transaction{}
+}
diff --git a/users/wpcarro/tools/monzo_ynab/monzo/serde.go b/users/wpcarro/tools/monzo_ynab/monzo/serde.go
new file mode 100644
index 0000000000..a38585eca6
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/monzo/serde.go
@@ -0,0 +1,82 @@
+// This package hosts the serialization and deserialization logic for all of the
+// data types with which our application interacts from the Monzo API.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"time"
+)
+
+type TxMetadata struct {
+	FasterPayment string `json:"faster_payment"`
+	FpsPaymentId  string `json:"fps_payment_id"`
+	Insertion     string `json:"insertion"`
+	Notes         string `json:"notes"`
+	Trn           string `json:"trn"`
+}
+
+type TxCounterparty struct {
+	AccountNumber string `json:"account_number"`
+	Name          string `json:"name"`
+	SortCode      string `json:"sort_code"`
+	UserId        string `json:"user_id"`
+}
+
+type Transaction struct {
+	Id                         string    `json:"id"`
+	Created                    time.Time `json:"created"`
+	Description                string    `json:"description"`
+	Amount                     int       `json:"amount"`
+	Currency                   string    `json:"currency"`
+	Notes                      string    `json:"notes"`
+	Metadata                   TxMetadata
+	AccountBalance             int            `json:"account_balance"`
+	International              interface{}    `json:"international"`
+	Category                   string         `json:"category"`
+	IsLoad                     bool           `json:"is_load"`
+	Settled                    time.Time      `json:"settled"`
+	LocalAmount                int            `json:"local_amount"`
+	LocalCurrency              string         `json:"local_currency"`
+	Updated                    time.Time      `json:"updated"`
+	AccountId                  string         `json:"account_id"`
+	UserId                     string         `json:"user_id"`
+	Counterparty               TxCounterparty `json:"counterparty"`
+	Scheme                     string         `json:"scheme"`
+	DedupeId                   string         `json:"dedupe_id"`
+	Originator                 bool           `json:"originator"`
+	IncludeInSpending          bool           `json:"include_in_spending"`
+	CanBeExcludedFromBreakdown bool           `json:"can_be_excluded_from_breakdown"`
+	CanBeMadeSubscription      bool           `json:"can_be_made_subscription"`
+	CanSplitTheBill            bool           `json:"can_split_the_bill"`
+	CanAddToTab                bool           `json:"can_add_to_tab"`
+	AmountIsPending            bool           `json:"amount_is_pending"`
+	// Fees interface{} `json:"fees"`
+	// Merchant interface `json:"merchant"`
+	// Labels interface{} `json:"labels"`
+	// Attachments interface{} `json:"attachments"`
+	// Categories interface{} `json:"categories"`
+}
+
+// Attempts to encode a Monzo transaction struct into a string.
+func serializeTx(tx *Transaction) (string, error) {
+	x, err := json.Marshal(tx)
+	return string(x), err
+}
+
+// Attempts to parse a string encoding a transaction presumably sent from a
+// Monzo server.
+func deserializeTx(x string) (*Transaction, error) {
+	target := &Transaction{}
+	err := json.Unmarshal([]byte(x), target)
+	return target, err
+}
+
+func main() {
+	b, _ := ioutil.ReadFile("./fixture.json")
+	tx := string(b)
+	target, _ := deserializeTx(tx)
+	out, _ := serializeTx(target)
+	fmt.Println(out)
+}
diff --git a/users/wpcarro/tools/monzo_ynab/requests.txt b/users/wpcarro/tools/monzo_ynab/requests.txt
new file mode 100644
index 0000000000..2da17c0b32
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/requests.txt
@@ -0,0 +1,80 @@
+################################################################################
+# YNAB
+################################################################################
+:ynab = https://api.youneedabudget.com/v1
+:ynab-access-token := (getenv "ynab_personal_access_token")
+:ynab-budget-id := (getenv "ynab_budget_id")
+:ynab-account-id := (getenv "ynab_account_id")
+
+# Test
+GET :ynab/budgets
+Authorization: Bearer :ynab-access-token
+
+# List transactions
+GET :ynab/budgets/:ynab-budget-id/transactions
+Authorization: Bearer :ynab-access-token
+
+# Post transactions
+POST :ynab/budgets/:ynab-budget-id/transactions
+Authorization: Bearer :ynab-access-token
+Content-Type: application/json
+{
+  "transactions": [
+    {
+      "account_id": ":ynab-account-id",
+      "date": "2019-12-30",
+      "amount": 10000,
+      "payee_name": "Richard Stallman",
+      "memo": "Not so free software after all...",
+      "cleared": "cleared",
+      "approved": true,
+      "flag_color": "red",
+      "import_id": "xyz-123"
+    }
+  ]
+}
+
+################################################################################
+# Monzo
+################################################################################
+:monzo = https://api.monzo.com
+:monzo-access-token := (getenv "monzo_cached_access_token")
+:monzo-refresh-token := (getenv "monzo_cached_refresh_token")
+:monzo-client-id := (getenv "monzo_client_id")
+:monzo-client-secret := (getenv "monzo_client_secret")
+:monzo-account-id := (getenv "monzo_account_id")
+
+# List transactions
+GET :monzo/transactions
+Authorization: Bearer :monzo-access-token
+account_id==:monzo-account-id
+
+# Refresh access token
+# According from the docs, the access token expires in 6 hours.
+POST :monzo/oauth2/token
+Content-Type: application/x-www-form-urlencoded
+Authorization: Bearer :monzo-access-token
+grant_type=refresh_token&client_id=:monzo-client-id&client_secret=:monzo-client-secret&refresh_token=:monzo-refresh-token
+
+################################################################################
+# Tokens server
+################################################################################
+:tokens = http://localhost:4242
+
+# Get tokens
+GET :tokens/tokens
+
+# Get application state for debugging purposes
+GET :tokens/state
+
+# Force refresh tokens
+POST :tokens/refresh-tokens
+
+# Set tokens
+POST :tokens/set-tokens
+Content-Type: application/json
+{
+  "access_token": "access-token",
+  "refresh_token": "refresh-token",
+  "expires_in": 120
+}
diff --git a/users/wpcarro/tools/monzo_ynab/shell.nix b/users/wpcarro/tools/monzo_ynab/shell.nix
new file mode 100644
index 0000000000..f777c13fef
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    go
+    goimports
+    godef
+  ];
+}
diff --git a/users/wpcarro/tools/monzo_ynab/tokens.go b/users/wpcarro/tools/monzo_ynab/tokens.go
new file mode 100644
index 0000000000..4be967ccb8
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/tokens.go
@@ -0,0 +1,283 @@
+// Creating a Tokens server to manage my access and refresh tokens. Keeping this
+// as a separate server allows me to develop and use the access tokens without
+// going through client authorization.
+package main
+
+////////////////////////////////////////////////////////////////////////////////
+// Dependencies
+////////////////////////////////////////////////////////////////////////////////
+
+import (
+	"auth"
+	"encoding/json"
+	"fmt"
+	"io"
+	"kv"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+	"utils"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Types
+////////////////////////////////////////////////////////////////////////////////
+
+// This is the response from Monzo's API after we request an access token
+// refresh.
+type refreshTokenResponse struct {
+	AccessToken  string `json:"access_token"`
+	RefreshToken string `json:"refresh_token"`
+	ClientId     string `json:"client_id"`
+	ExpiresIn    int    `json:"expires_in"`
+}
+
+// This is the shape of the request from clients wishing to set state of the
+// server.
+type setTokensRequest struct {
+	AccessToken  string `json:"access_token"`
+	RefreshToken string `json:"refresh_token"`
+	ExpiresIn    int    `json:"expires_in"`
+}
+
+// This is our application state.
+type state struct {
+	accessToken  string `json:"access_token"`
+	refreshToken string `json:"refresh_token"`
+}
+
+type readMsg struct {
+	sender chan state
+}
+
+type writeMsg struct {
+	state  state
+	sender chan bool
+}
+
+type channels struct {
+	reads  chan readMsg
+	writes chan writeMsg
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Top-level Definitions
+////////////////////////////////////////////////////////////////////////////////
+
+var chans = &channels{
+	reads:  make(chan readMsg),
+	writes: make(chan writeMsg),
+}
+
+var (
+	monzoClientId     = os.Getenv("monzo_client_id")
+	monzoClientSecret = os.Getenv("monzo_client_secret")
+	storePath         = os.Getenv("store_path")
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Utils
+////////////////////////////////////////////////////////////////////////////////
+
+// Print the access and refresh tokens for debugging.
+func logTokens(access string, refresh string) {
+	log.Printf("Access: %s\n", access)
+	log.Printf("Refresh: %s\n", refresh)
+}
+
+func (state *state) String() string {
+	return fmt.Sprintf("state{\n\taccessToken: \"%s\",\n\trefreshToken: \"%s\"\n}\n", state.accessToken, state.refreshToken)
+}
+
+// Schedule a token refresh for `expiresIn` seconds using the provided
+// `refreshToken`. This will update the application state with the access token
+// and schedule an additional token refresh for the newly acquired tokens.
+func scheduleTokenRefresh(expiresIn int, refreshToken string) {
+	duration := time.Second * time.Duration(expiresIn)
+	timestamp := time.Now().Local().Add(duration)
+	// TODO(wpcarro): Consider adding a more human readable version that will
+	// log the number of hours, minutes, etc. until the next refresh.
+	log.Printf("Scheduling token refresh for %v\n", timestamp)
+	time.Sleep(duration)
+	log.Println("Refreshing tokens now...")
+	accessToken, refreshToken := refreshTokens(refreshToken)
+	log.Println("Successfully refreshed tokens.")
+	logTokens(accessToken, refreshToken)
+	setState(accessToken, refreshToken)
+}
+
+// Exchange existing credentials for a new access token and `refreshToken`. Also
+// schedule the next refresh. This function returns the newly acquired access
+// token and refresh token.
+func refreshTokens(refreshToken string) (string, string) {
+	// TODO(wpcarro): Support retries with exponential backoff.
+	res, err := http.PostForm("https://api.monzo.com/oauth2/token", url.Values{
+		"grant_type":    {"refresh_token"},
+		"client_id":     {monzoClientId},
+		"client_secret": {monzoClientSecret},
+		"refresh_token": {refreshToken},
+	})
+	if res.StatusCode != http.StatusOK {
+		// TODO(wpcarro): Considering panicking here.
+		utils.DebugResponse(res)
+	}
+	if err != nil {
+		utils.DebugResponse(res)
+		log.Fatal("The request to Monzo to refresh our access token failed.", err)
+	}
+	defer res.Body.Close()
+	payload := &refreshTokenResponse{}
+	err = json.NewDecoder(res.Body).Decode(payload)
+	if err != nil {
+		log.Fatal("Could not decode the JSON response from Monzo.", err)
+	}
+
+	go scheduleTokenRefresh(payload.ExpiresIn, payload.RefreshToken)
+
+	// Interestingly, JSON decoding into the refreshTokenResponse can success
+	// even if the decoder doesn't populate any of the fields in the
+	// refreshTokenResponse struct. From what I read, it isn't possible to make
+	// these fields as required using an annotation, so this guard must suffice
+	// for now.
+	if payload.AccessToken == "" || payload.RefreshToken == "" {
+		log.Fatal("JSON parsed correctly but failed to populate token fields.")
+	}
+
+	return payload.AccessToken, payload.RefreshToken
+}
+
+func persistTokens(access string, refresh string) {
+	log.Println("Persisting tokens...")
+	kv.Set(storePath, "monzoAccessToken", access)
+	kv.Set(storePath, "monzoRefreshToken", refresh)
+	log.Println("Successfully persisted tokens.")
+}
+
+// Listen for SIGINT and SIGTERM signals. When received, persist the access and
+// refresh tokens and shutdown the server.
+func handleInterrupts() {
+	// Gracefully handle interruptions.
+	sigs := make(chan os.Signal, 1)
+	done := make(chan bool)
+
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+	go func() {
+		sig := <-sigs
+		log.Printf("Received signal to shutdown. %v\n", sig)
+		state := getState()
+		persistTokens(state.accessToken, state.refreshToken)
+		done <- true
+	}()
+
+	<-done
+	log.Println("Exiting...")
+	os.Exit(0)
+}
+
+// Set `accessToken` and `refreshToken` on application state.
+func setState(accessToken string, refreshToken string) {
+	msg := writeMsg{state{accessToken, refreshToken}, make(chan bool)}
+	chans.writes <- msg
+	<-msg.sender
+}
+
+// Return our application state.
+func getState() state {
+	msg := readMsg{make(chan state)}
+	chans.reads <- msg
+	return <-msg.sender
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Main
+////////////////////////////////////////////////////////////////////////////////
+
+func main() {
+	// Manage application state.
+	go func() {
+		state := &state{}
+		for {
+			select {
+			case msg := <-chans.reads:
+				log.Println("Reading from state...")
+				log.Println(state)
+				msg.sender <- *state
+			case msg := <-chans.writes:
+				log.Println("Writing to state.")
+				log.Printf("Old: %s\n", state)
+				*state = msg.state
+				log.Printf("New: %s\n", state)
+				// As an attempt to maintain consistency between application
+				// state and persisted state, everytime we write to the
+				// application state, we will write to the store.
+				persistTokens(state.accessToken, state.refreshToken)
+				msg.sender <- true
+			}
+		}
+	}()
+
+	// Retrieve cached tokens from store.
+	accessToken := fmt.Sprintf("%v", kv.Get(storePath, "monzoAccessToken"))
+	refreshToken := fmt.Sprintf("%v", kv.Get(storePath, "monzoRefreshToken"))
+
+	log.Println("Attempting to retrieve cached credentials...")
+	logTokens(accessToken, refreshToken)
+
+	if accessToken == "" || refreshToken == "" {
+		log.Println("Cached credentials are absent. Authorizing client...")
+		authCode := auth.GetAuthCode(monzoClientId)
+		tokens := auth.GetTokensFromAuthCode(authCode, monzoClientId, monzoClientSecret)
+		setState(tokens.AccessToken, tokens.RefreshToken)
+		go scheduleTokenRefresh(tokens.ExpiresIn, tokens.RefreshToken)
+	} else {
+		setState(accessToken, refreshToken)
+		// If we have tokens, they may be expiring soon. We don't know because
+		// we aren't storing the expiration timestamp in the state or in the
+		// store. Until we have that information, and to be safe, let's refresh
+		// the tokens.
+		go scheduleTokenRefresh(0, refreshToken)
+	}
+
+	// Gracefully handle shutdowns.
+	go handleInterrupts()
+
+	// Listen to inbound requests.
+	fmt.Println("Listening on http://localhost:4242 ...")
+	log.Fatal(http.ListenAndServe(":4242",
+		http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			if req.URL.Path == "/refresh-tokens" && req.Method == "POST" {
+				state := getState()
+				go scheduleTokenRefresh(0, state.refreshToken)
+				fmt.Fprintf(w, "Done.")
+			} else if req.URL.Path == "/set-tokens" && req.Method == "POST" {
+				// Parse
+				payload := &setTokensRequest{}
+				err := json.NewDecoder(req.Body).Decode(payload)
+				if err != nil {
+					log.Fatal("Could not decode the user's JSON request.", err)
+				}
+
+				// Update application state
+				setState(payload.AccessToken, payload.RefreshToken)
+
+				// Refresh tokens
+				go scheduleTokenRefresh(payload.ExpiresIn, payload.RefreshToken)
+
+				// Ack
+				fmt.Fprintf(w, "Done.")
+			} else if req.URL.Path == "/state" && req.Method == "GET" {
+				// TODO(wpcarro): Ensure that this returns serialized state.
+				w.Header().Set("Content-type", "application/json")
+				state := getState()
+				payload, _ := json.Marshal(state)
+				io.WriteString(w, string(payload))
+			} else {
+				log.Printf("Unhandled request: %v\n", *req)
+			}
+		})))
+}
diff --git a/users/wpcarro/tools/monzo_ynab/tokens.nix b/users/wpcarro/tools/monzo_ynab/tokens.nix
new file mode 100644
index 0000000000..4e2761bc78
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/tokens.nix
@@ -0,0 +1,26 @@
+{ depot, ... }:
+
+let
+  inherit (depot.users.wpcarro) gopkgs;
+
+  auth = depot.nix.buildGo.package {
+    name = "auth";
+    srcs = [
+      ./auth.go
+    ];
+    deps = with gopkgs; [
+      utils
+    ];
+  };
+in
+depot.nix.buildGo.program {
+  name = "token-server";
+  srcs = [
+    ./tokens.go
+  ];
+  deps = with gopkgs; [
+    kv
+    utils
+    auth
+  ];
+}
diff --git a/users/wpcarro/tools/monzo_ynab/ynab/client.go b/users/wpcarro/tools/monzo_ynab/ynab/client.go
new file mode 100644
index 0000000000..b3e9930f62
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/ynab/client.go
@@ -0,0 +1,24 @@
+package client
+
+import (
+	"serde"
+)
+
+// // See requests.txt for more details.
+// func PostTransactions(accountID string, txs []serde.Transaction{}) error {
+// 	return map[string]string{
+// 		"transactions": [
+// 			{
+// 				"account_id": accountID,
+// 					"date": "2019-12-30",
+// 					"amount": 10000,
+// 					"payee_name": "Richard Stallman",
+// 					"memo": "Not so free software after all...",
+// 					"cleared": "cleared",
+// 					"approved": true,
+// 					"flag_color": "red",
+// 					"import_id": "xyz-123"
+// 			}
+// 		]
+// 	}
+// }
diff --git a/users/wpcarro/tools/monzo_ynab/ynab/serde.go b/users/wpcarro/tools/monzo_ynab/ynab/serde.go
new file mode 100644
index 0000000000..53dd33e836
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/ynab/serde.go
@@ -0,0 +1,52 @@
+// This package hosts the serialization and deserialization logic for all of the
+// data types with which our application interacts from the YNAB API.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+type Transaction struct {
+	Id           string    `json:"id"`
+	Date         time.Time `json:"date"`
+	Amount       int       `json:"amount"`
+	Memo         string    `json:"memo"`
+	Cleared      string    `json:"cleared"`
+	Approved     bool      `json:"approved"`
+	FlagColor    string    `json:"flag_color"`
+	AccountId    string    `json:"account_id"`
+	AccountName  string    `json:"account_name"`
+	PayeeId      string    `json:"payeed_id"`
+	PayeeName    string    `json:"payee_name"`
+	CategoryId   string    `json:"category_id"`
+	CategoryName string    `json:"category_name"`
+	Deleted      bool      `json:"deleted"`
+	// TransferAccountId interface{} `json:"transfer_account_id"`
+	// TransferTransactionId interface{} `json:"transfer_transaction_id"`
+	// MatchedTransactionId interface{} `json:"matched_transaction_id"`
+	// ImportId interface{} `json:"import_id"`
+	// Subtransactions interface{} `json:"subtransactions"`
+}
+
+// Attempts to encode a YNAB transaction into a string.
+func serializeTx(tx *Transaction) (string, error) {
+	x, err := json.Marshal(tx)
+	return string(x), err
+}
+
+// Attempts to parse a string encoding a transaction presumably sent from a
+// YNAB server.
+func deserializeTx(x string) (*Transaction, error) {
+	target := &Transaction{}
+	err := json.Unmarshal([]byte(x), target)
+	return target, err
+}
+
+func main() {
+	target, _ := deserializeTx(tx)
+	out, _ := serializeTx(target)
+	fmt.Println(out)
+	fmt.Println(ynabOut)
+}
diff --git a/users/wpcarro/tools/rfcToKindle/LICENSE b/users/wpcarro/tools/rfcToKindle/LICENSE
new file mode 100644
index 0000000000..7a4a3ea242
--- /dev/null
+++ b/users/wpcarro/tools/rfcToKindle/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/users/wpcarro/tools/rfcToKindle/README.md b/users/wpcarro/tools/rfcToKindle/README.md
new file mode 100644
index 0000000000..e7b4fa841e
--- /dev/null
+++ b/users/wpcarro/tools/rfcToKindle/README.md
@@ -0,0 +1,30 @@
+# rfcToKindle
+
+Wirelessly transfer RFC documents to your Kindle to device for an alternative
+medium for reading.
+
+## Installation
+
+`rfcToKindle` makes use of [`buildGo.nix`][2] to package itself.  If you're
+using [Nix][1], you can install `rfcToKindle` using `nix-env`:
+
+```shell
+> nix-env -f https://github.com/wpcarro/rfcToKindle -i
+```
+
+## Usage
+
+```shell
+> rfcToKindle -document rfc6479 -recipient username@kindle.com
+```
+
+## Dependencies
+
+This uses `sendgmr` to send the file to the Kindle. Make sure:
+1. That `sendgmr` is installed and available on $PATH.
+2. That it is configured to work with your preferred email address.
+3. That the email address `sendgmr` is configured to use is whitelisted in
+   your Kindle "Personal Document Settings".
+
+[1]: https://nixos.org/nix/
+[2]: https://git.tazj.in/tree/nix/buildGo
diff --git a/users/wpcarro/tools/rfcToKindle/default.nix b/users/wpcarro/tools/rfcToKindle/default.nix
new file mode 100644
index 0000000000..ca87abdee0
--- /dev/null
+++ b/users/wpcarro/tools/rfcToKindle/default.nix
@@ -0,0 +1,11 @@
+{ depot, ... }:
+
+# TODO: This doesn't depend on `sendgmr` at the moment, but it should. As such,
+# it's an imcomplete packaging.
+depot.nix.buildGo.program {
+  name = "rfcToKindle";
+  srcs = [
+    ./main.go
+  ];
+  deps = [ ];
+}
diff --git a/users/wpcarro/tools/rfcToKindle/main.go b/users/wpcarro/tools/rfcToKindle/main.go
new file mode 100644
index 0000000000..0f4f2dd9ec
--- /dev/null
+++ b/users/wpcarro/tools/rfcToKindle/main.go
@@ -0,0 +1,89 @@
+// Author: wpcarro@gmail.com
+//
+// Wirelessly transfer RFC documents to your Kindle to device for an alternative
+// medium for reading.
+//
+// Usage:
+// ```shell
+// > go run rfcToKindle.go -document rfc6479 -recipient username@kindle.com
+// ```
+//
+// This uses `sendgmr` to send the file to the Kindle. Make sure:
+// 1. That `sendgmr` is installed and available on $PATH.
+// 2. That it is configured to work with your preferred email address.
+// 3. That the email address `sendgmr` is configured to use is whitelisted in
+//    your Kindle "Personal Document Settings".
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+func main() {
+	document := flag.String("document", "", "(Required) The name of the document to fetch. For example \"RFC6479\".")
+	recipient := flag.String("recipient", "", "(Required) The email address of the Kindle device.")
+	subject := flag.String("subject", "", "(Optional) The email address of the Kindle device.")
+	flag.Parse()
+
+	if *document == "" {
+		// TODO: Is log.Fatal the best function to use here?
+		log.Fatal("-document cannot be empty. See -help for more information.")
+	}
+
+	if *recipient == "" {
+		log.Fatal("-recipient cannot be empty. See -help for more information.")
+	}
+
+	*document = strings.ToLower(*document)
+
+	url := fmt.Sprintf("https://www.ietf.org/rfc/%s.txt", *document)
+	resp, err := http.Get(url)
+	fmt.Printf("Downloading %s ... ", url)
+
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	f, err := ioutil.TempFile("", fmt.Sprintf("%s-*.txt", *document))
+	if err != nil {
+		log.Fatal(err)
+	}
+	// TODO: Verify if this is cleaning up or not.
+	defer os.Remove(f.Name())
+
+	_, err = io.Copy(f, resp.Body)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println("done.")
+
+	if *subject == "" {
+		*subject = fmt.Sprintf("%s - Sent from rfcToKindle.go", *document)
+	}
+
+	// Although I couldn't find it documented anywhere, the email sent to the
+	// Kindle must have a body, even if the body isn't used for anything.
+	fmt.Printf("Emailing %s to %s ... ", f.Name(), *recipient)
+	cmd := exec.Command("sendgmr",
+		fmt.Sprintf("--to=%s", *recipient),
+		fmt.Sprintf("--body_file=%s", f.Name()),
+		fmt.Sprintf("--subject=%s", *subject),
+		fmt.Sprintf("--attachment_files=%s", f.Name()))
+	err = cmd.Run()
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println("done.")
+
+	os.Exit(0)
+}
diff --git a/users/wpcarro/tools/run/.envrc b/users/wpcarro/tools/run/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/tools/run/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/tools/run/README.md b/users/wpcarro/tools/run/README.md
new file mode 100644
index 0000000000..fd24495035
--- /dev/null
+++ b/users/wpcarro/tools/run/README.md
@@ -0,0 +1,30 @@
+# run
+
+Simplify the commands you call to run scripts on the command line.
+
+```shell
+> run path/to/file.py
+> run path/to/file.ts
+```
+
+## How?
+
+Define a run.json configuration mapping commands to filename extensions like
+so:
+```json
+{
+  ".ts": "npx ts-node $file",
+  ".py": "python3 $file"
+}
+```
+
+Then call `run path/to/some/file.ts` on the command line, and `npx ts-node
+file.ts` will run.
+
+## Installation
+
+Install `run` using Nix.
+
+```shell
+> nix-env -iA users.wpcarro.run
+```
diff --git a/users/wpcarro/tools/run/default.nix b/users/wpcarro/tools/run/default.nix
new file mode 100644
index 0000000000..807a75c284
--- /dev/null
+++ b/users/wpcarro/tools/run/default.nix
@@ -0,0 +1,11 @@
+{ pkgs, depot, ... }:
+
+depot.nix.buildGo.program {
+  name = "run";
+  srcs = [
+    ./main.go
+  ];
+  deps = with depot.users.wpcarro.gopkgs; [
+    utils
+  ];
+}
diff --git a/users/wpcarro/tools/run/main.go b/users/wpcarro/tools/run/main.go
new file mode 100644
index 0000000000..04906ece91
--- /dev/null
+++ b/users/wpcarro/tools/run/main.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"utils"
+)
+
+func main() {
+	if len(os.Args) != 2 {
+		log.Fatal("You can only call run with a single file at a time.")
+	}
+
+	rulesPath := utils.Resolve("run.json", []string{"/home/wpcarro/.config/run/run.json"})
+	b, err := ioutil.ReadFile(rulesPath)
+	if err != nil {
+		log.Fatal("Could not locate a run.json file: ", err)
+	}
+	rules := map[string]string{}
+	err = json.Unmarshal(b, &rules)
+	if err != nil {
+		log.Fatal("Could not decode run.json as JSON: ", err)
+	}
+
+	fileName := os.Args[1]
+	ext := filepath.Ext(fileName)
+	cmd, ok := rules[ext]
+
+	if !ok {
+		log.Fatalf("No rules for extension, %s, have been defined.", ext)
+	}
+
+	// TODO(wpcarro): Support more sophisticated parsing than just string
+	// splitting. To handle 'cases like this'.
+	tokens := strings.Split(strings.Replace(cmd, "$file", fileName, 1), " ")
+	c := exec.Command(tokens[0], tokens[1:]...)
+	err = c.Start()
+	// TODO(wpcarro): Forward STDERR and STDOUT.
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(c.Wait())
+}
diff --git a/users/wpcarro/tools/run/shell.nix b/users/wpcarro/tools/run/shell.nix
new file mode 100644
index 0000000000..f777c13fef
--- /dev/null
+++ b/users/wpcarro/tools/run/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    go
+    goimports
+    godef
+  ];
+}
diff --git a/users/wpcarro/tools/simple_vim/config.vim b/users/wpcarro/tools/simple_vim/config.vim
new file mode 100644
index 0000000000..bbdcdc5f1a
--- /dev/null
+++ b/users/wpcarro/tools/simple_vim/config.vim
@@ -0,0 +1,101 @@
+" My barebones vimrc without any Vundle dependencies.
+"
+" I'm attempting to optimize the following:
+" - Minimize dependencies
+" - Maximize ergonomics
+" - Maximize Tmux compatibility
+" - Minimize shadowing of existing Vim KBDs
+"
+" Warning: This is currently unstable as it is a work-in-progress.
+"
+" Author: William Carroll <wpcarro@gmail.com>
+
+" Use <Space> as the leader key.
+let mapleader = " "
+nnoremap <leader>ev :tabnew<CR>:edit ~/.vimrc<CR>
+nnoremap <leader>sv :source ~/.vimrc<CR>
+nnoremap <leader>w  :w<CR>
+nnoremap <leader>h  :help 
+
+" increment,decrement numbers
+nnoremap + <C-a>
+" TODO: Restore with better KBD
+" nnoremap - <C-x>
+
+" Visit the CWD
+nnoremap - :e .<CR>
+
+" Turn line numbers on.
+set number
+
+" Prevent lines from wrapping
+set nowrap
+
+" Easily create vertical, horizontal window splits.
+nnoremap sh :vsplit<CR>
+nnoremap sj :split<CR>:wincmd j<CR>
+nnoremap sk :split<CR>
+nnoremap sl :vsplit<CR>:wincmd l<CR>
+
+" Move across window splits.
+" TODO: Change to <M-{h,j,k,l}>.
+nnoremap <C-h> :wincmd h<CR>
+nnoremap <C-j> :wincmd j<CR>
+nnoremap <C-k> :wincmd k<CR>
+nnoremap <C-l> :wincmd l<CR>
+
+" TODO: Support these.
+" nnoremap <M-q> :q<CR>
+" nnoremap <M-h> :wincmd h<CR>
+" nnoremap <M-j> :wincmd j<CR>
+" nnoremap <M-k> :wincmd k<CR>
+" nnoremap <M-l> :wincmd l<CR>
+
+" Use <CR> instead of G to support:
+"        20<CR> - to jump to line 20
+"       d20<CR> - to delete from the current line until line 20
+"   <C-v>20<CR> - to select from the current line until line 20
+nnoremap <CR> G
+onoremap <CR> G
+vnoremap <CR> G
+
+" Easily change modes on keyboards that don't have CapsLock mapped to <Esc>
+inoremap jk      <ESC>
+
+" CRUD tabs.
+nnoremap <TAB>   :tabnext<CR>
+nnoremap <S-TAB> :tabprevious<CR>
+nnoremap <C-t>   :tabnew<CR>:edit .<CR>
+nnoremap <C-w>   :tabclose<CR>
+" TODO: Re-enable these once <M-{h,j,k,l}> are supported.
+" nnoremap <C-l> :+tabmove<CR>
+" nnoremap <C-h> :-tabmove<CR>
+
+" Use H,L to goto beggining,end of a line.
+" Swaps the keys to ensure original functionality of H,L are preserved.
+nnoremap H ^
+nnoremap L $
+nnoremap ^ H
+nnoremap $ L
+
+" Use H,L in visual mode too
+vnoremap H ^
+vnoremap L $
+vnoremap ^ H
+vnoremap $ L
+
+" Emacs hybrid mode
+" TODO: model this after tpope's rsi.vim (Readline-style insertion)
+cnoremap <C-g> <C-c>
+cnoremap <C-a> <C-b>
+inoremap <C-a> <C-o>^
+inoremap <C-e> <C-o>$
+inoremap <C-b> <C-o>h
+inoremap <C-f> <C-o>l
+
+" Indenting
+" The following three settings are based on option 2 of `:help tabstop`
+set tabstop=4
+set shiftwidth=4
+set expandtab
+set autoindent
diff --git a/users/wpcarro/tools/simple_vim/default.nix b/users/wpcarro/tools/simple_vim/default.nix
new file mode 100644
index 0000000000..ae3fd76a46
--- /dev/null
+++ b/users/wpcarro/tools/simple_vim/default.nix
@@ -0,0 +1,5 @@
+{ pkgs, ... }:
+
+pkgs.writeShellScriptBin "simple_vim" ''
+  ${pkgs.neovim}/bin/nvim -u ${./config.vim} "$@"
+''
diff --git a/users/wpcarro/tools/symlinkManager/README.md b/users/wpcarro/tools/symlinkManager/README.md
new file mode 100644
index 0000000000..e8298ea654
--- /dev/null
+++ b/users/wpcarro/tools/symlinkManager/README.md
@@ -0,0 +1,12 @@
+# Dotfile Symlink Manager
+
+Find and delete all symlinks to my dotfiles.
+
+Oftentimes I corrupt the state of my configuration files. The intention with
+this script is to help me clean things up when this happens. An example workflow
+might look like:
+
+```shell
+> symlink-mgr --audit
+> symlink-mgr --seriously
+```
diff --git a/users/wpcarro/tools/symlinkManager/default.nix b/users/wpcarro/tools/symlinkManager/default.nix
new file mode 100644
index 0000000000..7d022828ee
--- /dev/null
+++ b/users/wpcarro/tools/symlinkManager/default.nix
@@ -0,0 +1,14 @@
+{ depot, ... }:
+
+let
+  inherit (depot.users.wpcarro) gopkgs;
+in
+depot.nix.buildGo.program {
+  name = "symlink-mgr";
+  srcs = [
+    ./main.go
+  ];
+  deps = with gopkgs; [
+    utils
+  ];
+}
diff --git a/users/wpcarro/tools/symlinkManager/main.go b/users/wpcarro/tools/symlinkManager/main.go
new file mode 100644
index 0000000000..d99c7cb863
--- /dev/null
+++ b/users/wpcarro/tools/symlinkManager/main.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"utils"
+)
+
+func main() {
+	audit := flag.Bool("audit", false, "Output all symlinks that would be deleted. This is the default behavior. This option is mutually exclusive with the --seriously option.")
+	seriously := flag.Bool("seriously", false, "Actually delete the symlinks. This option is mutually exclusive with the --audit option.")
+	repoName := flag.String("repo-name", "briefcase", "The name of the repository.")
+	flag.Parse()
+
+	if !*audit && !*seriously {
+		log.Fatal(errors.New("Either -audit or -seriously needs to be set."))
+	}
+	if *audit == *seriously {
+		log.Fatal(errors.New("Arguments -audit and -seriously are mutually exclusive"))
+	}
+
+	home, err := os.UserHomeDir()
+	utils.FailOn(err)
+	count := 0
+
+	err = filepath.Walk(home, func(path string, info os.FileInfo, err error) error {
+		if utils.IsSymlink(info.Mode()) {
+			dest, err := os.Readlink(path)
+			utils.FailOn(err)
+
+			predicate := func(dest string) bool {
+				return strings.Contains(dest, *repoName)
+			}
+
+			if predicate(dest) {
+				if *audit {
+					fmt.Printf("%s -> %s\n", path, dest)
+				} else if *seriously {
+					fmt.Printf("rm %s\n", path)
+					err = os.Remove(path)
+					utils.FailOn(err)
+				}
+				count += 1
+			}
+		}
+		return nil
+	})
+	utils.FailOn(err)
+	if *audit {
+		fmt.Printf("Would have deleted %d symlinks.\n", count)
+	} else if *seriously {
+		fmt.Printf("Successfully deleted %d symlinks.\n", count)
+	}
+
+	os.Exit(0)
+}
diff --git a/users/wpcarro/tools/url-blocker/.envrc b/users/wpcarro/tools/url-blocker/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/tools/url-blocker/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/tools/url-blocker/Main.hs b/users/wpcarro/tools/url-blocker/Main.hs
new file mode 100644
index 0000000000..926412ce91
--- /dev/null
+++ b/users/wpcarro/tools/url-blocker/Main.hs
@@ -0,0 +1,205 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE DeriveGeneric #-}
+module Main ( main ) where
+
+--------------------------------------------------------------------------------
+-- Dependencies
+--------------------------------------------------------------------------------
+
+import qualified Data.Maybe as Maybe
+import qualified Data.Time.Clock as Clock
+import qualified Data.Time.Calendar as Calendar
+import qualified Data.Time.LocalTime as LocalTime
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Aeson as Aeson
+import qualified Data.Either.Combinators as Either
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Text as Text
+import qualified Data.Text.IO as TextIO
+import qualified Data.Text.Read as TextRead
+import qualified Data.List as List
+
+import GHC.Generics
+import Data.Aeson ((.:))
+import Data.Text (Text)
+
+--------------------------------------------------------------------------------
+-- Types
+--------------------------------------------------------------------------------
+
+newtype URL = URL { getURL :: Text } deriving (Show, Eq, Generic)
+
+newtype IPAddress = IPAddress { getIPAddress :: Text } deriving (Show)
+
+newtype Domain = Domain { getDomain :: Text } deriving (Show)
+
+newtype Hour = Hour { getHour :: Int } deriving (Show, Eq, Generic)
+
+newtype Minute = Minute { getMinute :: Int } deriving (Show, Eq, Generic)
+
+data EtcHostsEntry = EtcHostsEntry { ip :: IPAddress
+                                   , domains :: [Domain]
+                                   } deriving (Show)
+
+-- | Write these in terms of your system's local time (i.e. `date`).
+data TimeSlot = TimeSlot { beg :: (Hour, Minute)
+                         , end :: (Hour, Minute)
+                         } deriving (Show, Eq, Generic)
+
+data Allowance = Allowance { day :: Calendar.DayOfWeek
+                           , timeslots :: [TimeSlot]
+                           } deriving (Show, Eq, Generic)
+
+data Rule = Rule { urls :: [URL]
+                 , allowed :: [Allowance]
+                 } deriving (Show, Eq, Generic)
+
+--------------------------------------------------------------------------------
+-- Instances
+--------------------------------------------------------------------------------
+
+instance Aeson.FromJSON TimeSlot where
+  parseJSON = Aeson.withText "timeslot" $ \x -> do
+    let [a, b] = Text.splitOn "-" x
+        [ah, am] = Text.splitOn ":" a
+        [bh, bm] = Text.splitOn ":" b
+    case extractTimeSlot ah am bh bm of
+      Left s  -> fail s
+      Right x -> pure x
+    where
+      extractTimeSlot :: Text -> Text -> Text -> Text -> Either String TimeSlot
+      extractTimeSlot ah am bh bm = do
+        (begh, _) <- TextRead.decimal ah
+        (begm, _) <- TextRead.decimal am
+        (endh, _) <- TextRead.decimal bh
+        (endm, _) <- TextRead.decimal bm
+        pure $ TimeSlot{ beg = (Hour begh, Minute begm)
+                       , end = (Hour endh, Minute endm)
+                       }
+
+instance Aeson.FromJSON Allowance where
+  parseJSON = Aeson.withObject "allowance" $ \x -> do
+    day <- x .: "day"
+    timeslots <- x .: "timeslots"
+    pure $ Allowance{day, timeslots}
+
+instance Aeson.FromJSON URL where
+  parseJSON = Aeson.withText "URL" $ \x -> do
+    pure $ URL { getURL = x }
+
+instance Aeson.FromJSON Rule where
+  parseJSON = Aeson.withObject "rule" $ \x -> do
+    urls <- x .: "urls"
+    allowed <- x .: "allowed"
+    pure Rule{urls, allowed}
+
+--------------------------------------------------------------------------------
+-- Functions
+--------------------------------------------------------------------------------
+
+-- | Pipe operator
+(|>) :: a -> (a -> b) -> b
+(|>) a f = f a
+infixl 1 |>
+
+-- | Returns True if the current time falls within any of the `timeslots`.
+isWithinTimeSlot :: LocalTime.LocalTime -> [TimeSlot] -> Bool
+isWithinTimeSlot date timeslots =
+  List.any withinTimeSlot timeslots
+  where
+    withinTimeSlot :: TimeSlot -> Bool
+    withinTimeSlot TimeSlot{ beg = (Hour ah, Minute am)
+                           , end = (Hour bh, Minute bm)
+                           } =
+      let LocalTime.TimeOfDay{LocalTime.todHour, LocalTime.todMin} =
+            LocalTime.localTimeOfDay date
+      in (todHour > ah) && (todMin > am) && (todHour < bh) && (todMin < bm)
+
+-- | Returns True if `day` is the same day as today.
+isToday :: LocalTime.LocalTime -> Calendar.DayOfWeek -> Bool
+isToday date day = today == day
+  where
+    today = Calendar.dayOfWeek (LocalTime.localDay date)
+
+-- | Returns True if a list of none of the `allowances` are valid.
+shouldBeBlocked :: LocalTime.LocalTime -> [Allowance] -> Bool
+shouldBeBlocked _ [] = True
+shouldBeBlocked date allowances = do
+  case filter (isToday date . day) allowances of
+    [Allowance{timeslots}] -> not $ isWithinTimeSlot date timeslots
+    [] -> True
+    -- Error when more than one rule per day
+    _  -> True
+
+-- | Maps an EtcHostsEntry to the line of text url-blocker will append to /etc/hosts.
+serializeEtcHostEntry :: EtcHostsEntry -> Text
+serializeEtcHostEntry EtcHostsEntry{ip, domains} =
+  (getIPAddress ip) <> "\t" <> (Text.unwords $ fmap getDomain domains)
+
+-- | Create an EtcHostsEntry mapping the URLs in `rule` to 127.0.0.1 if the
+-- URLs should be blocked.
+maybeBlockURL :: LocalTime.LocalTime -> Rule -> Maybe EtcHostsEntry
+maybeBlockURL date Rule{urls, allowed} =
+  if shouldBeBlocked date allowed then
+    Just $ EtcHostsEntry { ip = IPAddress "127.0.0.1"
+                        , domains = fmap (Domain . getURL) urls
+                        }
+  else
+    Nothing
+
+-- | Read and parse the rules.json file.
+-- TODO(wpcarro): Properly handle errors for file not found.
+-- TODO(wpcarro): Properly handle errors for parse failures.
+-- TODO(wpcarro): How can we resolve the $HOME directory when this is run as
+-- root?
+getRules :: IO [Rule]
+getRules = do
+  contents <- LazyByteString.readFile "/home/wpcarro/.config/url-blocker/rules.json"
+  let payload = Aeson.eitherDecode contents
+  pure $ Either.fromRight [] payload
+
+-- | Informational header added to /etc/hosts before the entries that
+-- url-blocker adds.
+urlBlockerHeader :: Text
+urlBlockerHeader =
+  Text.unlines [ "################################################################################"
+               , "# Added by url-blocker."
+               , "#"
+               , "# Warning: url-blocker will remove anything that you add beneath this header."
+               , "################################################################################"
+               ]
+
+-- | Removes all entries that url-blocker may have added to /etc/hosts.
+removeURLBlockerEntries :: Text -> Text
+removeURLBlockerEntries etcHosts =
+  case Text.breakOn urlBlockerHeader etcHosts of
+    (etcHosts', _) -> etcHosts'
+
+-- | Appends the newly created `entries` to `etcHosts`.
+addURLBlockerEntries :: Text -> Text -> Text
+addURLBlockerEntries entries etcHosts =
+  Text.unlines [ etcHosts
+               , urlBlockerHeader
+               , entries
+               ]
+
+-- | This script reads the current /etc/hosts, removes any entries that
+-- url-blocker may have added in a previous run, and adds new entries to block
+-- URLs according to the rules.json file.
+main :: IO ()
+main = do
+  rules <- getRules
+  tz <- LocalTime.getCurrentTimeZone
+  ct <- Clock.getCurrentTime
+  let date = LocalTime.utcToLocalTime tz ct
+      entries = rules
+                |> fmap (maybeBlockURL date)
+                |> Maybe.catMaybes
+                |> fmap serializeEtcHostEntry
+                |> Text.unlines
+  existingEtcHosts <- TextIO.readFile "/etc/hosts"
+  existingEtcHosts
+    |> removeURLBlockerEntries
+    |> addURLBlockerEntries entries
+    |> \x -> writeFile "/etc/hosts" (Text.unpack x)
diff --git a/users/wpcarro/tools/url-blocker/README.md b/users/wpcarro/tools/url-blocker/README.md
new file mode 100644
index 0000000000..a6063f3256
--- /dev/null
+++ b/users/wpcarro/tools/url-blocker/README.md
@@ -0,0 +1,47 @@
+# url-blocker
+
+`url-blocker` blocks the URLs that you want to block when you want it to block
+them.
+
+Let's say that you don't want to visit Twitter during the work week. Create the
+file `~/.config/url-blocker/rules.json` with the following contents and
+`url-blocker` will take care of the rest.
+
+```json
+# ~/.config/url-blocker/rules.json
+[
+  {
+    "urls": [
+      "twitter.com",
+      "www.twitter.com",
+    ],
+    "allowed": [
+      {
+        "day": "Saturday",
+        "timeslots": [
+          "00:00-11:59"
+        ]
+      },
+      {
+        "day": "Sunday",
+        "timeslots": [
+          "00:00-11:59"
+        ]
+      }
+    ]
+  }
+]
+```
+
+## Installation
+
+```shell
+$ nix-env -iA users.wpcarro.tools.url-blocker
+```
+
+## How does it work?
+
+`systemd` is intended to run `url-blocker` once every minute. `url-blocker` will
+read `/etc/hosts` and map the URLs defined in `rules.json` to `127.0.0.1` when
+you want them blocked. Because `systemd` run once every minute, `/etc/hosts`
+should be current to the minute as well.
diff --git a/users/wpcarro/tools/url-blocker/default.nix b/users/wpcarro/tools/url-blocker/default.nix
new file mode 100644
index 0000000000..ae24aa41b7
--- /dev/null
+++ b/users/wpcarro/tools/url-blocker/default.nix
@@ -0,0 +1,34 @@
+{ pkgs, ... }:
+
+let
+  ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: [
+    hpkgs.time
+    hpkgs.aeson
+    hpkgs.either
+  ]);
+
+  # This is the systemd service unit
+  service = pkgs.stdenv.mkDerivation {
+    name = "url-blocker";
+    src = builtins.path { path = ./.; name = "url-blocker"; };
+    buildPhase = ''
+      ${ghc}/bin/ghc Main.hs
+    '';
+    installPhase = ''
+      mv ./Main $out
+    '';
+  };
+
+  # This is the systemd timer unit.
+  # Run once every minute.
+  # Give root privilege.
+  systemdUnit = {
+    systemd = {
+      timers.simple-timer = {
+        wantedBy = [ "timers.target" ];
+        partOf = [ ];
+      };
+    };
+  };
+in
+null
diff --git a/users/wpcarro/tools/url-blocker/rules.json b/users/wpcarro/tools/url-blocker/rules.json
new file mode 100644
index 0000000000..95e4dc9a90
--- /dev/null
+++ b/users/wpcarro/tools/url-blocker/rules.json
@@ -0,0 +1,28 @@
+[
+  {
+    "urls": [
+      "facebook.com",
+      "www.facebook.com",
+      "twitter.com",
+      "www.twitter.com",
+      "youtube.com",
+      "www.youtube.com",
+      "instagram.com",
+      "www.instagram.com"
+    ],
+    "allowed": []
+  },
+  {
+    "urls": [
+      "chat.googleplex.com"
+    ],
+    "allowed": [
+      {
+        "day": "Sunday",
+        "timeslots": [
+          "18:35-18:39"
+        ]
+      }
+    ]
+  }
+]
diff --git a/users/wpcarro/tools/url-blocker/shell.nix b/users/wpcarro/tools/url-blocker/shell.nix
new file mode 100644
index 0000000000..aa5c906edc
--- /dev/null
+++ b/users/wpcarro/tools/url-blocker/shell.nix
@@ -0,0 +1,10 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: with hpkgs; [
+    time
+    aeson
+    either
+    hspec
+  ];
+}
diff --git a/users/wpcarro/utils/README.md b/users/wpcarro/utils/README.md
new file mode 100644
index 0000000000..0724dff2a9
--- /dev/null
+++ b/users/wpcarro/utils/README.md
@@ -0,0 +1,8 @@
+# Nix Utils
+
+Nix is useful and well-designed in many cases. Nix's standard library of is not
+a representative example of the value of Nix. I'm hoping to replace some of the
+standard library functions with versions that I find more intuitive and
+ergonomic to consume.
+
+This directcory is where I will host that work.
diff --git a/users/wpcarro/utils/builder.nix b/users/wpcarro/utils/builder.nix
new file mode 100644
index 0000000000..2bc061d366
--- /dev/null
+++ b/users/wpcarro/utils/builder.nix
@@ -0,0 +1,12 @@
+{ pkgs, ... }:
+
+let
+  inherit (pkgs) writeShellScriptBin;
+in
+{
+  # Create a derivation that creates an executable shell script named `as` that
+  # calls the program located at `path`, forwarding all of the arguments.
+  wrapNonNixProgram = { path, as }: writeShellScriptBin as ''
+    exec ${path} "$@"
+  '';
+}
diff --git a/users/wpcarro/utils/default.nix b/users/wpcarro/utils/default.nix
new file mode 100644
index 0000000000..46d30acfa2
--- /dev/null
+++ b/users/wpcarro/utils/default.nix
@@ -0,0 +1,15 @@
+args@{ pkgs, ... }:
+
+# This top-level module exposes all of my utility functions for Nix. It should
+# be used like:
+# ```nix
+# inherit (depot.users.wpcarro.utils) fs;
+# ```
+
+let
+  builder = import ./builder.nix args;
+  fs = import ./fs.nix args;
+in
+{
+  inherit builder fs;
+}
diff --git a/users/wpcarro/utils/fs.nix b/users/wpcarro/utils/fs.nix
new file mode 100644
index 0000000000..d7d5e34e99
--- /dev/null
+++ b/users/wpcarro/utils/fs.nix
@@ -0,0 +1,42 @@
+{ pkgs, ... }:
+
+# `fs` contains utility functions for working with the filesystem.
+
+let
+  inherit (builtins) attrNames hasAttr map readDir;
+  inherit (pkgs.lib) filterAttrs;
+in
+{
+  # Returns a list of all of the regular files in `dir`.
+  files = dir:
+    map (name: dir + "/${name}")
+      (attrNames
+        (filterAttrs (_: type: type == "regular") (readDir dir)));
+
+  # Returns a list of all of the directories in `dir`.
+  dirs = dir:
+    map (name: dir + "/${name}")
+      (attrNames
+        (filterAttrs (_: type: type == "directory") (readDir dir)));
+
+  # Returns a list of paths to all of the `name` files starting at `dir`.
+  find = name: dir:
+    if hasAttr name (readDir dir) then
+      [ (dir + name) ] ++ concatMap findAllDefaultNix (dirs dir)
+    else
+      concatMap findAllDefaultNix (dirs dir);
+
+  # Looks for `name` in `dir`; if it cannot find it, it checks the parent
+  # directory.
+  resolve = name: dir:
+    if hasAttr name (readDir dir) then
+      dir + "/${name}"
+    else
+    # This prevents the function from infinitely recursing and eventually
+    # stack overflowing.
+      if (dirOf dir) == dir then
+        null
+      else
+        resolve name (dirOf dir);
+};
+}
diff --git a/users/wpcarro/website/README.md b/users/wpcarro/website/README.md
new file mode 100644
index 0000000000..30420571c0
--- /dev/null
+++ b/users/wpcarro/website/README.md
@@ -0,0 +1,3 @@
+# wpcarro.dev
+
+https://wpcarro.dev is my personal website. Check it out!
diff --git a/users/wpcarro/website/blog/.skip-subtree b/users/wpcarro/website/blog/.skip-subtree
new file mode 100644
index 0000000000..3a9dbd4d2b
--- /dev/null
+++ b/users/wpcarro/website/blog/.skip-subtree
@@ -0,0 +1 @@
+Subdirectories contain blog posts and static assets only
\ No newline at end of file
diff --git a/users/wpcarro/website/blog/default.nix b/users/wpcarro/website/blog/default.nix
new file mode 100644
index 0000000000..d87b714b6f
--- /dev/null
+++ b/users/wpcarro/website/blog/default.nix
@@ -0,0 +1,46 @@
+{ depot, lib, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) hasAttr filter readFile;
+  inherit (depot.web.blog) post includePost renderPost;
+  inherit (depot.users.wpcarro.website) domain renderTemplate withBrand;
+  inherit (lib.lists) sort;
+
+  config = {
+    name = "bill and his blog";
+    baseUrl = "https://${domain}/blog";
+    footer = "";
+  };
+
+  posts = sort (x: y: x.date > y.date)
+    (filter includePost (list post (import ./posts.nix)));
+
+  rendered = pkgs.runCommandNoCC "blog-posts" { } ''
+    mkdir -p $out
+
+    ${lib.concatStringsSep "\n" (map (post:
+      "cp ${renderPost config post} $out/${post.key}.html"
+    ) posts)}
+  '';
+
+  formatDate = date: readFile (pkgs.runCommandNoCC "date" { } ''
+    date --date='@${toString date}' '+%B %e, %Y' > $out
+  '');
+
+  postsHtml = renderTemplate ./fragments/posts.html {
+    postsHtml = lib.concatStringsSep "\n" (map toPostHtml posts);
+  };
+
+  toPostHtml = post: readFile (renderTemplate ./fragments/post.html {
+    postUrl = "${config.baseUrl}/posts/${post.key}.html";
+    postTitle = post.title;
+    postDate = formatDate post.date;
+  });
+in
+pkgs.runCommandNoCC "blog" { } ''
+  mkdir -p $out
+  cp ${withBrand (readFile postsHtml)} $out/index.html
+  cp -r ${rendered} $out/posts
+''
diff --git a/users/wpcarro/website/blog/fragments/.skip-subtree b/users/wpcarro/website/blog/fragments/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/wpcarro/website/blog/fragments/.skip-subtree
diff --git a/users/wpcarro/website/blog/fragments/post.html b/users/wpcarro/website/blog/fragments/post.html
new file mode 100644
index 0000000000..44593094ec
--- /dev/null
+++ b/users/wpcarro/website/blog/fragments/post.html
@@ -0,0 +1,8 @@
+<li class="pb-6 md:pb-10">
+  <h2 class="text-bold text-xl">
+    <a class="font-bold text-blue-600 hover:underline" href="@postUrl@">
+      @postTitle@
+    </a>
+  </h2>
+  <p class="text-gray-500">@postDate@</p>
+</li>
diff --git a/users/wpcarro/website/blog/fragments/posts.html b/users/wpcarro/website/blog/fragments/posts.html
new file mode 100644
index 0000000000..a85a4b7110
--- /dev/null
+++ b/users/wpcarro/website/blog/fragments/posts.html
@@ -0,0 +1,10 @@
+<div class="max-w-sm md:max-w-prose mx-auto">
+  <section class="pt-8 pb-14">
+    <p class="font-bold pb-3 text-xl">
+      Personal blog by <a class="font-bold text-blue-600 hover:underline" href="@homepage@">Bill</a>.
+    </p>
+    <p class="text-gray-500">&gt; Half-baked musings lossily encoded.</p>
+    <p class="text-gray-500">&gt; - misc reviewer</p>
+  </section>
+  <ul>@postsHtml@</ul>
+</div>
diff --git a/users/wpcarro/website/blog/posts.nix b/users/wpcarro/website/blog/posts.nix
new file mode 100644
index 0000000000..5a58c9309c
--- /dev/null
+++ b/users/wpcarro/website/blog/posts.nix
@@ -0,0 +1,32 @@
+# To format the date, run:
+#   $ date -d "today" +%s
+[
+  {
+    key = "cell-phone-experiment";
+    title = "Cell Phone Experiment";
+    date = 1585800000;
+    content = ./posts/cell-phone-experiment.md;
+    draft = false;
+  }
+  {
+    key = "quassel-google-vm";
+    title = "IRC, GCP, and NixOS";
+    date = 1640404800;
+    content = ./posts/quassel-google-vm.md;
+    draft = true;
+  }
+  {
+    key = "send-mail-as-2fa";
+    title = "2FA and Gmail's \"Send mail as\"";
+    date = 1641497483;
+    content = ./posts/send-mail-as-2fa.md;
+    draft = false;
+  }
+  {
+    key = "auto-reboot-nixos";
+    title = "Automatically Reboot NixOS";
+    date = 1643666914;
+    content = ./posts/auto-reboot-nixos.md;
+    draft = false;
+  }
+]
diff --git a/users/wpcarro/website/blog/posts/auto-reboot-nixos.md b/users/wpcarro/website/blog/posts/auto-reboot-nixos.md
new file mode 100644
index 0000000000..24474e6dfe
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/auto-reboot-nixos.md
@@ -0,0 +1,40 @@
+## Show me the codes
+
+Regularly rebooting machines can be a useful, hygienic practice, but quite
+frankly I cannot be relied on to remember to regularly reboot my machine.
+
+Let's free-up some wetware-RAM by automating this with Nix. The following
+addition to your `configuration.nix` will schedule daily reboots at `03:00`:
+
+```nix
+systemd.timers.auto-reboot = {
+  wantedBy = [ "timers.target" ];
+  timerConfig = {
+    OnCalendar = "*-*-* 03:00:00";
+    Unit = "reboot.target";
+  };
+};
+```
+
+If you want to fiddle with the date format, `systemd-analyze` is your friend:
+
+```shell
+λ systemd-analyze calendar '*-*-* 03:00:00'
+Normalized form: *-*-* 03:00:00
+    Next elapse: Tue 2022-02-01 03:00:00 PST
+       (in UTC): Tue 2022-02-01 11:00:00 UTC
+       From now: 12h left
+```
+
+After calling `nixos-rebuild switch`, you can verify that `systemd` started the
+timer with:
+
+```shell
+λ systemctl list-timers auto-reboot
+#  output omitted because I'm writing this from a different machine
+```
+
+## That's all, folks!
+
+I wanted to keep this post short-and-sweet, to build the habit of posting more
+regularly. Hopefully someone out there found this useful.
diff --git a/users/wpcarro/website/blog/posts/cell-phone-experiment.md b/users/wpcarro/website/blog/posts/cell-phone-experiment.md
new file mode 100644
index 0000000000..c289954a58
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/cell-phone-experiment.md
@@ -0,0 +1,274 @@
+### TL;DR
+
+I will not use my cell phone during March to learn more about how much I depend
+on it.
+
+### Explore/Exploit
+
+Ever since I read Charles Duhigg's book, [The Power of Habit](poh), I try to
+habituate as many aspects of my life as I can.
+
+Making my bed every morning is an example of a habit -- so too is flossing at
+night before bed.
+
+The *exploit* axis of the [explore/exploit tradeoff](exp-exp) endows habits with
+their power. Brian Christian and Tom Griffiths explain this concept more clearly
+than I can in Chapter 2 of their exceptional book, [Algorithms to Live
+By](algos).
+
+Habits are powerful, but if I overly exploit an activity, I may settle on a
+local optimum in lieu of settling on a global optimum; these are the opportunity
+costs of exploiting (i.e. habits) versus exploring (i.e. spontaneity).
+
+But what if it was possible to habituate exploration?
+
+### Monthly challenges
+
+Every month since October 2018, I challenge myself to try something new. In the
+past, monthly challenges have been things like:
+- sign up and take Brazilian Jiu Jitsu classes
+- buy a guitar and learn [Freight Train](https://www.youtube.com/watch?v=IUK8emiWabU)
+- study Italian
+- learn a handstand
+
+Typically for an activity to qualify as a challenge, I must spend *at least
+fifteen minutes* working on it *at least five days* each week.
+
+This month (i.e. March) I'm challenging myself to avoid using my cell phone.
+
+My parents gave me a cell phone when when I was a freshman in High School; I was
+14 years old. I am now 28, so I have been using a cell phone semi-daily for over
+a decade.
+
+While I enjoy the convenience that my cell phone provides me, I am curious to
+suspend my usage to more clearly understand how much I depend on it...
+
+### April
+
+Now it is early April, and I completed March's challenge. So how was it?
+
+Below I outline the parts of using a cell phone that I missed and the parts that
+I surprisingly did not miss. I will also mention the two times that I used my
+cell phone and why.
+
+The first three things that I missed all relate to time.
+
+#### Timekeeping
+
+On the first day I realized that unless I was near a computer, I did not know
+what time it was.
+
+I exclusively use my cell phone as my watch; I do not wear a watch. To adapt, I
+started looking for clocks around my office and while I was taking public
+transportation. Thankfully London posts the current time on the digital train
+schedules. This oriented me while I was traveling, which was also when I needed
+to know the time the most.
+
+Most of the month, however, I never precisely knew what time it was.
+
+#### Alarm clocks
+
+While I anticipated living without an alarm clock prior to the experiment, I
+decided against buying a substitute. Prior to this month, I theorized that
+morning alarms probably disrupt the quality of my sleep. If I'm tired, shouldn't
+I keep sleeping?
+
+As the month progressed and my 24 hour day morphed into a 25 hour day, I learned
+that I would prefer waking up at a set time every day and synchronize my
+schedule with the rest of my timezone.
+
+I am still unsure if alarm clocks are helpful in the long-term. I would have
+slept with the curtains drawn to allow the morning sun to wake me
+up. Unfortunately, I live on the ground floor nearby a brightly shining street
+lamp that spills into my bedroom.
+
+If I lived somewhere more remote (perhaps even a suburb would do) I would like
+to repeat an experiment where I live for a month without an alarm clock.
+
+For now, I must return to the Temple of Chronology and supplicate until Father
+Time restores my sanity.
+
+#### Timers
+
+Using timers motivates me to do a bunch of short tasks like cleaning my flat for
+fifteen minutes, stretching, or reading before bed. Thankfully, I already owned
+a physical timer that I keep in my kitchen. This replaced the timer on my phone
+without disrupting my routine.
+
+#### Maps
+
+Speaking of being disoriented, what about living without maps software?  On the
+few occasions where I traveled somewhere that was unfamiliar to me, I had to
+memorize the directions from my computer before I departed.
+
+At least I didn't need to visit gas stations or museums to buy trifold tourist
+maps...
+
+I once left my office mistakenly assuming that I would download the directions
+to my destination while commuting. As I awaited the office elevator, I realized
+that I had no clue where I was heading.
+
+Thankfully I wasn't far from the safety, comfort, and familiarity of my desktop
+computer -- with its fatty WiFi connection. In no time I was studying Google
+Maps in my web browser and memorizing the directions.
+
+Overall this was hardly an inconvenience, and I think I even enjoyed
+stress-testing my memory: a job that I so often outsource to hardware.
+
+#### Rendezvouses
+
+A couple of times I met friends in various parts of the city. Organizing these
+particular rendezvouses was a novel (read: anachronistic) experience. For all
+you young whippersnappers reading, take out your stone tablets and chisels. I'm
+going to explain how this works:
+
+First I would tell my friends where and when to meet me. I emphasized that I
+would be quite helpless to any changes they might make to the plans once I began
+commuting, which made the commitments unusually more binding.
+
+On one occasion my friend -- who is characteristically prompt, and even chides
+me for when I'm late -- was twenty minutes late for our engagement. My friend is
+German, so I figured I should do my civic duty of alerting the German embassy
+that my friend had broken German code, is obscenely late, and should therefore
+hand-in his passport and renounce his citizenship. After awhile my conscience
+advised me to reconsider.
+
+It was fortunate for both of us that I did not fully understand how late he was.
+Remember: I didn't know what time it was.
+
+I decided this would be a useful opportunity to test my patience, so I loitered
+for twenty minutes outside of our meeting point. He couldn't text me to tell me
+that he was late. I couldn't listen to music, call family or friends, or partake
+in any of the other rituals that modern-day loiterers observe to pass the
+time. In the end he showed up, and it was scarcely a big deal.
+
+This experience made me wonder what the policy for abandoning plans is when
+someone is running late. Before smart phones, how long did people wait? Maybe
+the proper etiquette is to wait long enough for you to absolve yourself of the
+guilt of flaking in the unlikely event that your friend arrives shortly after
+you leave.
+
+So... thirty minutes? I'll call my grandma tomorrow and ask her.
+
+#### Boredom
+
+My phone couldn't entertain me while I queued at the grocery store. Same too
+when I commuted.
+
+I also found myself listening to less music than I usually do. I decided to read
+to occupy the void when I could; this helped me progress towards completing this
+year's [GoodReads challenge][gr-annual].
+
+### Cheating
+
+I used my phone twice during March.
+
+1. Once to use my bank's mobile app to internationally transfer money from my
+   U.K. account to my U.S. account. I could have used [TransferWise's][tw]
+   website, but I didn't.
+2. Another time I used my phone to take pictures of an item that I wanted to
+   sell on [CraigsList][cl]. I could have and perhaps should have used my laptop's
+   webcam, but at the time, I didn't want to. I am accustomed to using my phone
+   to take pictures, and I wanted to sell something.
+
+In both of these cases, prior habits eroded my resolve to stay the course. These
+are useful reminders that habits don't distinguish between helpful and hurtful;
+they just exist.
+
+In total I would estimate that I spent somewhere around fifteen minutes using
+my phone in March. While not perfect:
+
+> Better a diamond with a flaw than a pebble without (Confucius)
+
+### Substitution = Dilution
+
+While the explicit goal of this challenge was to avoid using my cell phone for a
+month, the implicit goal was to disengage from many of the
+[nonessential][essentialism] activities that compete for my attention.
+
+There were some activities that I didn't miss while living without a cell
+phone. This wasn't because I don't value these activities, but rather because I
+can adequately replace them with alternatives.
+
+For texting and making phone calls, I used [Telegram][wtf-telegram]. Telegram
+helped me sustain a healthy relationship with my girlfriend while still honoring
+the constraints of the challenge.
+
+While I appreciated the convenience Telegram provided, I felt that I remained
+about as [available][wtf-availability] during March as I was in February. If I
+ever experiment with drastically reducing my availability, I will be more
+explicit about my objectives.
+
+### Distraction displacement (whack-a-mole)
+
+Because cell phones and other electronics have conditioned my behavior, I
+habitually avoid boredom and seek entertainment. On its face this may not sound
+like a harmful practice. My generation drills the aphorism "you only live once",
+suggesting that we may want to embrace a Hedonistic lifestyle.
+
+Hedonism may or may not be a wise way to play the game of Life. All I know is
+that living a life in which I am often stimulated but proportionately distracted
+appeals increasingly less to me as time progresses.
+
+During March I noticed that once I freed my attention from sending/receiving
+texts, my brain quickly reassigned my attention to maintaining a vigil over the
+other social media outposts that I maintain.
+
+I should also admit that I habitually checked Telegram now that it served as my
+new cell phone. Didn't see that coming...
+
+In another case, once I discovered that I could use Instagram in a web browser
+instead of on my phone, I filled my newfound time and attention on
+[Instagram.com][ig] (don't click!): displacing the time that I spent on an app
+on my phone to time that I spent on a website in a web browser.
+
+Holy whack-a-mole!
+
+Halfway through the month, I wrote a [program to block websites][url-blocker] on
+my computer. Surprisingly this worked and forced me to more deliberately fill
+this hard-fought, foreign time with other activities.
+
+### Easy come, easy go?
+
+As the saying for making friends goes, "easy come, easy go", implying that
+friendships that you easily form can just as easily be destroyed.
+
+Habits invert this creation/destruction relationship. In my experience "easy
+come" implies "difficult to go".
+
+For example, I could easily form the habit of eating chocolate around 15:00 at
+work; curbing this habit would require more effort. When I compare this to the
+difficulty I experienced habituating a meditation practice, and how easily I
+can dislodge my meditation practice, it seems to me that the laws of habits
+dictate "easy come, difficult go; difficult come, easy go".
+
+I suspect that while my cravings for using a cell phone have temporarily ceased,
+they will return shortly after I start using my cell phone. And as if nothing
+happened, I return to where I was at the end of February just before I decided
+to curb my cell phone usage.
+
+Because of this, I'm planning on keeping my cell phone in my closet where I
+stored it during the month of March. As noted, enough substitutes exist for me
+to live a mostly normal life: one where I am not unnecessarily straining the
+relationships of my friends and my family. After all these are the people who
+matter most to me and those who drive me to explore new ways to improve.
+
+I recognize that the "self" in self-experimentation is a misnomer. Can you truly
+conduct an [N of 1 trial][nof1]? My decisions impact the people in my life, and
+I want to thank everyone who tolerates my eccentric and oftentimes annoying
+experimentation.
+
+Thank you for reading.
+
+[pod]: https://www.goodreads.com/book/show/12609433-the-power-of-habit
+[exp-exp]: https://en.wikipedia.org/wiki/Multi-armed_bandit
+[algos]: https://www.goodreads.com/book/show/25666050-algorithms-to-live-by
+[gr-annual]: https://www.goodreads.com/user_challenges/19737920
+[cl]: http://craigslist.com
+[tw]: https://transferwise.com
+[url-blocker]: https://github.com/wpcarro/url-blocker
+[wtf-telegram]: https://telegram.org
+[wtf-availability]: https://landing.google.com/sre/sre-book/chapters/availability-table
+[essentialism]: https://www.goodreads.com/book/show/18077875-essentialism
+[ig]: https://instagram.com
+[nof1]: https://en.wikipedia.org/wiki/N_of_1_trial
diff --git a/users/wpcarro/website/blog/posts/quassel-google-vm.md b/users/wpcarro/website/blog/posts/quassel-google-vm.md
new file mode 100644
index 0000000000..dd74387f8b
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/quassel-google-vm.md
@@ -0,0 +1,34 @@
+# IRC, GCP, and NixOS
+
+- "cannot read /var/lib/acme/wpcarro.dev/full.pem"
+- `sudo stat /var/lib/acme/wpcarro.dev/full.pem` exists
+- `sudo -i`
+- `su quassel` # denied
+- `sudo --user=quassel stat /var/lib/acme/wpcarro.dev/full.pem` exists
+- `groups quassel` quassel
+- `usermod -a -G nginx quassel` exists
+- `groups quassel` quassel, nginx
+- `sudo --user=quassel cat /var/lib/acme/wpcarro.dev/full.pem` exists
+
+# Firewall
+
+- `nmap localhost`
+- `nmap wpcarro.dev`
+- Update `configuration.nix` firewall
+- `nmap localhost`
+- `nmap wpcarro.dev`
+- Edit cloud.google.com Configuration (VPC > Firewall > 6697)
+
+# Quassel
+
+- Test connecting, disconnecting, persisted logs?
+- Change `~quassel@253.253.209.35.bc.googleusercontent.com` -> `~quassel@wpcarro.dev`
+  - cloaking?
+  - rDNS?
+    - `dig wpcarro.dev`       -> `35.209.253.253`
+    - `dig -x 35.209.253.253` -> `253.253.209.35.bc.googleusercontent.com`
+    - From within GCP https://stackoverflow.com/a/47060002 (create the PTR record)
+- `/msg hostserv take hackint/user/$account` add cloaking
+- disconnect/connect from hackint for changes to take affect
+- `/msg hostserv drop` remove cloaking
+- Test can I log-in from another machine?
diff --git a/users/wpcarro/website/blog/posts/send-mail-as-2fa.md b/users/wpcarro/website/blog/posts/send-mail-as-2fa.md
new file mode 100644
index 0000000000..5d18935c7a
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/send-mail-as-2fa.md
@@ -0,0 +1,43 @@
+## Prelude
+
+This is a short story about how I configured myself out of my own email. Posting
+this as an exercise in humility, a tutorial for my future self in case of
+amnesia, and penance for my sins.
+
+## Background
+
+-   I have 2x Gmail accounts: **work** and **personal**.
+-   I configure **work** to send emails as **personal**.
+-   I configure **personal** to forward incoming emails to **work**.
+
+This allows me to use **work** and manage both of my inboxes as one. I recently
+added two-factor authentication (2FA) to **personal**, forgot about it, and
+spent a few days unable to send **personal** emails from any **work** device.
+
+## Symptoms
+
+Whenever I tried to send emails on behalf of **personal**, I'd receive the
+following error message as a reply:
+
+> You're sending this from a different address using the 'Send mail as' feature.
+> The settings for your 'Send mail as' account are misconfigured or out of date.
+> Check those settings and try resending.
+
+Useful error message if you ask me (especially in retrospect), but because I had
+*forgotten* that I setup 2FA for **personal**, I naively assumed this issue
+might magically disappear given enough time... kind of how restarting your
+device resets the state and causes the symptoms of a certain class of bugs to
+disappear.
+
+After a few days of mounting frustration, I decided to take a closer look...
+
+## Solution
+
+-   Create an "App Password" for **personal**:
+    [instructions](https://support.google.com/accounts/answer/185833?hl=en).
+-   Login to **work** and delete **personal** from `Settings > Accounts > Send
+    mail as`.
+-   `Add another email address` for **personal** using the "App Password" you
+    just created.
+
+And now I'm back in business!
diff --git a/users/wpcarro/website/default.nix b/users/wpcarro/website/default.nix
new file mode 100644
index 0000000000..19229aab5a
--- /dev/null
+++ b/users/wpcarro/website/default.nix
@@ -0,0 +1,43 @@
+{ pkgs, depot, ... }:
+
+let
+  inherit (builtins) readFile;
+  inherit (depot.users) wpcarro;
+
+  domain = "billandhiscomputer.com";
+
+  globalVars = {
+    inherit domain;
+    homepage = "https://${domain}/";
+    blog = "https://${domain}/blog";
+    habits = "https://${domain}/habits";
+    github = "https://github.com/wpcarro";
+    linkedin = "https://linkedin.com/in/williampatrickcarroll";
+    depotWork = "https://cs.tvl.fyi/depot/-/blob/users/wpcarro";
+  };
+
+  renderTemplate = src: vars: pkgs.substituteAll (globalVars // vars // {
+    inherit src;
+  });
+
+  withBrand = contentHtml: renderTemplate ./fragments/template.html {
+    inherit contentHtml;
+  };
+in
+{
+  inherit domain renderTemplate withBrand;
+
+  root = pkgs.runCommandNoCC "wpcarro.dev" { } ''
+    mkdir -p $out
+
+    # /
+    cp ${withBrand (readFile (renderTemplate ./fragments/homepage.html {}))} $out/index.html
+
+    # /habits
+    mkdir -p $out/habits
+    cp -r ${wpcarro.website.habit-screens} $out/habits/index.html
+
+    # /blog
+    cp -r ${wpcarro.website.blog} $out/blog
+  '';
+}
diff --git a/users/wpcarro/website/fragments/.skip-subtree b/users/wpcarro/website/fragments/.skip-subtree
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/users/wpcarro/website/fragments/.skip-subtree
diff --git a/users/wpcarro/website/fragments/homepage.html b/users/wpcarro/website/fragments/homepage.html
new file mode 100644
index 0000000000..f515d97400
--- /dev/null
+++ b/users/wpcarro/website/fragments/homepage.html
@@ -0,0 +1,20 @@
+<section class="leading-7">
+  <p class="relative text-3xl text-center font-bold pt-6 md:pt-14 pb-10">
+    Hey! I'm Bill.<span class="pl-10 relative"><span class="block absolute right-0 top-0 transition-transform hover:rotate-90">👋</span></span>
+  </p>
+  <p class="pb-4">
+    I write software. Currently I work as a <b>Site Reliability Engineer</b> for
+    <a class="text-blue-600 font-bold hover:underline" href="https://drive.google.com">Google Drive</a>.
+  </p>
+  <p class="pb-4">
+    I'm <b>wpcarro</b> on
+    <a class="font-bold text-blue-600 hover:underline" href="@github@">GitHub</a>
+    (and elsewhere), but if you're looking for code samples, the majority of
+    my open-source work resides in a magical place called the
+    <a class="font-bold text-blue-600 hover:underline" href="@depotWork@">depot</a>.
+  </p>
+  <p class="pb-4">
+    If I'm not coding, I'm likely meditating, training Jiu Jitsu, or
+    fumbling around on the piano or drums.
+  </p>
+</section>
diff --git a/users/wpcarro/website/fragments/template.html b/users/wpcarro/website/fragments/template.html
new file mode 100644
index 0000000000..241e11a12f
--- /dev/null
+++ b/users/wpcarro/website/fragments/template.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>@domain@</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=JetBrains+Mono">
+    <script src="https://cdn.tailwindcss.com"></script>
+    <script>
+      tailwind.config = {
+        theme: {
+          extend: {
+            fontFamily: {
+                mono: ["JetBrains Mono", "normal"]
+            },
+          },
+        },
+      };
+    </script>
+  </head>
+  <body class="font-mono bg-gray-100">
+    <header class="sticky z-10 transition duration-300 bg-gray-100 top-0 py-6">
+      <div class="flex max-w-sm md:max-w-3xl mx-auto">
+        <div class="flex-1 text-center md:text-left text-xl md:text-base">
+          <a href="@depotWork@/website">
+            <h1 class="font-bold">
+              <span class="text-black">(</span><a class="text-purple-600 hover:underline" href="@depotWork@/website">def</a>&nbsp;<a class="text-green-600 hover:underline text-bold" href="@homepage@">"@domain@"</a><span class="text-black">)</span>
+            </h1>
+          </a>
+        </div>
+        <nav class="flex-1 hidden md:block">
+          <ul class="list-reset flex justify-end space-x-8">
+            <li>
+              <a class="hover:underline" href="@habits@">
+                Habits
+              </a>
+            </li>
+            <li>
+              <a class="hover:underline" href="@blog@">
+                Blog
+              </a>
+            </li>
+            <li>
+              <a class="hover:underline" href="@github@">
+                GitHub
+              </a>
+            </li>
+            <li>
+              <a class="hover:underline" href="@linkedin@">
+                LinkedIn
+              </a>
+            </li>
+          </ul>
+        </nav>
+      </div>
+    </header>
+    <div class="max-w-sm px-2 md:px-0 md:max-w-prose mx-auto">
+      @contentHtml@
+      <footer class="md:hidden pb-6">
+        <h2 class="text-xl font-bold py-4">More Bill?</h2>
+        <ul>
+          <li class="pb-6">
+            <a class="text-blue-600 font-bold" href="@homepage@">
+              Home <span class="text-blue-300">-></span>
+            </a>
+          </li>
+          <li class="pb-6">
+            <a class="text-blue-600 font-bold" href="@blog@">
+              Blog <span class="text-blue-300">-></span>
+            </a>
+          </li>
+          <li class="pb-6">
+            <a class="text-blue-600 font-bold" href="@github@">
+              GitHub <span class="text-blue-300">-></span>
+            </a>
+          </li>
+          <li class="pb-6">
+            <a class="text-blue-600 font-bold" href="@linkedin@">
+              LinkedIn <span class="text-blue-300">-></span>
+            </a>
+          </li>
+          <li class="pb-6">
+            <a class="text-blue-600 font-bold" href="https://www.buymeacoffee.com/billandhisjoe">
+              Caffeinate Bill? <span class="text-blue-300">-></span>
+            </a>
+          </li>
+        </ul>
+      </footer>
+    </div>
+    <script>
+      const $header = document.querySelector("header");
+      const dropShadow = "drop-shadow-md";
+      const update = () => window.scrollY !== 0 ?
+        $header.classList.add(dropShadow) :
+        $header.classList.remove(dropShadow);
+
+      update();
+      document.addEventListener("scroll", update);
+    </script>
+  </body>
+</html>
diff --git a/users/wpcarro/website/habit-screens/.envrc b/users/wpcarro/website/habit-screens/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/website/habit-screens/.gitignore b/users/wpcarro/website/habit-screens/.gitignore
new file mode 100644
index 0000000000..0c1c258f65
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/.gitignore
@@ -0,0 +1,2 @@
+/elm-stuff
+/Main.min.js
diff --git a/users/wpcarro/website/habit-screens/README.md b/users/wpcarro/website/habit-screens/README.md
new file mode 100644
index 0000000000..506cdf9c4a
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/README.md
@@ -0,0 +1,31 @@
+# Habit Screens
+
+Problem: I would like to increase the rate at which I complete my daily, weekly,
+monthly, yearly habits.
+
+Solution: Habit Screens are mounted in strategic locations throughout my
+apartment. Each Habit Screen displays the habits that I should complete that
+day, and I can tap each item to mark it as complete. I will encounter the Habit
+Screens in my bedroom, kitchen, and bathroom, so I will have adequate "cues" to
+focus my attention. By marking each item as complete and tracking the results
+over time, I will have more incentive to maintain my consistency
+(i.e. "reward").
+
+## Elm
+
+Elm has one of the best developer experiences that I'm aware of. The error
+messages are helpful and the entire experience is optimized to improve the ease
+of writing web applications.
+
+### Developing
+
+If you're interested in contributing, the following will create an environment
+in which you can develop:
+
+```shell
+$ nix-shell
+$ npx tailwindcss build index.css -o output.css
+$ elm-live -- src/Main.elm --output=Main.min.js
+```
+
+You can now view your web client at `http://localhost:8000`!
diff --git a/users/wpcarro/website/habit-screens/default.nix b/users/wpcarro/website/habit-screens/default.nix
new file mode 100644
index 0000000000..3036ba1821
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/default.nix
@@ -0,0 +1,63 @@
+{ pkgs, ... }:
+
+with pkgs;
+
+let
+  mkDerivation =
+    { srcs ? ./elm-srcs.nix
+    , src
+    , name
+    , srcdir ? "./src"
+    , targets ? [ ]
+    , registryDat ? ./registry.dat
+    , outputJavaScript ? false
+    }:
+    stdenv.mkDerivation {
+      inherit name src;
+
+      buildInputs = [ elmPackages.elm ]
+        ++ lib.optional outputJavaScript nodePackages.uglify-js;
+
+      buildPhase = elmPackages.fetchElmDeps {
+        elmPackages = import srcs;
+        elmVersion = "0.19.1";
+        inherit registryDat;
+      };
+
+      installPhase =
+        let
+          elmfile = module: "${srcdir}/${builtins.replaceStrings ["."] ["/"] module}.elm";
+          extension = if outputJavaScript then "js" else "html";
+        in
+        ''
+          mkdir -p $out/share/doc
+          ${lib.concatStrings (map (module: ''
+            echo "compiling ${elmfile module}"
+            elm make ${elmfile module} --output $out/${module}.${extension} --docs $out/share/doc/${module}.json
+            ${lib.optionalString outputJavaScript ''
+              echo "minifying ${elmfile module}"
+              uglifyjs $out/${module}.${extension} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \
+                  | uglifyjs --mangle --output $out/${module}.min.${extension}
+            ''}
+          '') targets)}
+        '';
+    };
+  mainDotElm = mkDerivation {
+    name = "elm-app-0.1.0";
+    srcs = ./elm-srcs.nix;
+    src = ./.;
+    targets = [ "Main" ];
+    srcdir = "./src";
+    outputJavaScript = true;
+  };
+in
+stdenv.mkDerivation {
+  name = "habit-screens";
+  buildInputs = [ ];
+  src = builtins.path { path = ./.; name = "habit-screens"; };
+  buildPhase = ''
+    mkdir -p $out
+    cp index.html output.css ${mainDotElm}/Main.min.js $out
+  '';
+  dontInstall = true;
+}
diff --git a/users/wpcarro/website/habit-screens/design.md b/users/wpcarro/website/habit-screens/design.md
new file mode 100644
index 0000000000..f16361ac43
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/design.md
@@ -0,0 +1,43 @@
+# Habit Screens
+
+## MVP
+
+One Android tablet mounted on my bedroom wall displaying habits for that day. I
+can toggle the done/todo states on each item by tapping it. There is no
+server. All of the habits are defined in the client-side codebase. The
+application is available online at wpcarro.dev.
+
+## Ideal
+
+Three Android tablets: one mounted in my bedroom, another in my bathroom, and a
+third in my kitchen. Each tablet has a view of the current state of the
+application and updates in soft real-time.
+
+I track the rates at which I complete each habit and compile all of the metrics
+into a dashboard. When I move a habit from Saturday to Sunday or from Wednesday
+to Monday, it doesn't break the tracking.
+
+When I complete a habit, it quickly renders some consistency information like
+"completing rate since Monday" and "length of current streak".
+
+I don't consider this application that sensitive, but for security purposes I
+would like this application to be accessible within a private network. This is
+something I don't know too much about setting up, but I don't want anyone to be
+able to visit www.BillAndHisHabits.com and change the states of my habits and
+affect the tracking data. Nor do I want anyone to be able to make HTTP requests
+to my server to alter the state of the application without my permission.
+
+## Client
+
+Language: Elm
+
+### Updates across devices
+
+Instead of setting up sockets on my server and subscribing to them from the
+client, I think each device should poll the server once every second (or fewer)
+to maintain UI consistency.
+
+## Server
+
+Language: Haskell
+Database: SQLite
diff --git a/users/wpcarro/website/habit-screens/elm-srcs.nix b/users/wpcarro/website/habit-screens/elm-srcs.nix
new file mode 100644
index 0000000000..7f6f77741a
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/elm-srcs.nix
@@ -0,0 +1,77 @@
+{
+
+  "elm-community/maybe-extra" = {
+    sha256 = "0qslmgswa625d218djd3p62pnqcrz38f5p558mbjl6kc1ss0kzv3";
+    version = "5.2.0";
+  };
+
+  "elm/html" = {
+    sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
+    version = "1.0.0";
+  };
+
+  "elm-community/random-extra" = {
+    sha256 = "1dg2nz77w2cvp16xazbdsxkkw0xc9ycqpkd032faqdyky6gmz9g6";
+    version = "3.1.0";
+  };
+
+  "elm/svg" = {
+    sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
+    version = "1.0.1";
+  };
+
+  "justinmimbs/date" = {
+    sha256 = "1f0wcl8yhlvp3x4rj53rdy4r4ga7lkl6n8fdfh6b96scz2rnxmd4";
+    version = "3.2.1";
+  };
+
+  "elm/browser" = {
+    sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13";
+    version = "1.0.2";
+  };
+
+  "elm/core" = {
+    sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
+    version = "1.0.5";
+  };
+
+  "elm-community/list-extra" = {
+    sha256 = "1ayv3148drynqnxdfwpjxal8vwzgsjqanjg7yxp6lhdcbkxgd3vd";
+    version = "8.2.3";
+  };
+
+  "elm/random" = {
+    sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
+    version = "1.0.0";
+  };
+
+  "elm/time" = {
+    sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
+    version = "1.0.0";
+  };
+
+  "elm/json" = {
+    sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh";
+    version = "1.1.3";
+  };
+
+  "elm/parser" = {
+    sha256 = "0a3cxrvbm7mwg9ykynhp7vjid58zsw03r63qxipxp3z09qks7512";
+    version = "1.1.0";
+  };
+
+  "owanturist/elm-union-find" = {
+    sha256 = "13gm7msnp0gr1lqia5m7m4lhy3m6kvjg37d304whb3psn88wqhj5";
+    version = "1.0.0";
+  };
+
+  "elm/url" = {
+    sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
+    version = "1.0.0";
+  };
+
+  "elm/virtual-dom" = {
+    sha256 = "0q1v5gi4g336bzz1lgwpn5b1639lrn63d8y6k6pimcyismp2i1yg";
+    version = "1.0.2";
+  };
+}
diff --git a/users/wpcarro/website/habit-screens/elm.json b/users/wpcarro/website/habit-screens/elm.json
new file mode 100644
index 0000000000..6839ac4fab
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/elm.json
@@ -0,0 +1,32 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/random": "1.0.0",
+            "elm/svg": "1.0.1",
+            "elm/time": "1.0.0",
+            "elm-community/list-extra": "8.2.3",
+            "elm-community/maybe-extra": "5.2.0",
+            "elm-community/random-extra": "3.1.0",
+            "justinmimbs/date": "3.2.1"
+        },
+        "indirect": {
+            "elm/json": "1.1.3",
+            "elm/parser": "1.1.0",
+            "elm/url": "1.0.0",
+            "elm/virtual-dom": "1.0.2",
+            "owanturist/elm-union-find": "1.0.0"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}
diff --git a/users/wpcarro/website/habit-screens/index.css b/users/wpcarro/website/habit-screens/index.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/users/wpcarro/website/habit-screens/index.html b/users/wpcarro/website/habit-screens/index.html
new file mode 100644
index 0000000000..b587e09012
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/index.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Elm SPA</title>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Chilanka">
+    <link rel="stylesheet" href="./output.css">
+    <style>
+      body {
+          font-family: 'Chilanka';
+      }
+    </style>
+    <script src="./Main.min.js"></script>
+  </head>
+  <body>
+    <div id="mount"></div>
+    <script>
+     Elm.Main.init({node: document.getElementById("mount")});
+    </script>
+  </body>
+</html>
diff --git a/users/wpcarro/website/habit-screens/output.css b/users/wpcarro/website/habit-screens/output.css
new file mode 100644
index 0000000000..b522419aa3
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/output.css
@@ -0,0 +1,103571 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  -webkit-text-decoration: underline dotted;
+          text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}
+
+/**
+ * Manually forked from SUIT CSS Base: https://github.com/suitcss/base
+ * A thin layer on top of normalize.css that provides a starting point more
+ * suitable for web applications.
+ */
+
+/**
+ * Removes the default spacing and border for appropriate elements.
+ */
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+  margin: 0;
+}
+
+button {
+  background-color: transparent;
+  background-image: none;
+}
+
+/**
+ * Work around a Firefox/IE bug where the transparent `button` background
+ * results in a loss of the default `button` focus styles.
+ */
+
+button:focus {
+  outline: 1px dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+}
+
+fieldset {
+  margin: 0;
+  padding: 0;
+}
+
+ol,
+ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+/**
+ * Tailwind custom reset styles
+ */
+
+/**
+ * 1. Use the user's configured `sans` font-family (with Tailwind's default
+ *    sans-serif font stack as a fallback) as a sane default.
+ * 2. Use Tailwind's default "normal" line-height so the user isn't forced
+ *    to override it to ensure consistency even when using the default theme.
+ */
+
+html {
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */
+  line-height: 1.5; /* 2 */
+}
+
+/**
+ * 1. Prevent padding and border from affecting element width.
+ *
+ *    We used to set this in the html element and inherit from
+ *    the parent element for everything else. This caused issues
+ *    in shadow-dom-enhanced elements like <details> where the content
+ *    is wrapped by a div with box-sizing set to `content-box`.
+ *
+ *    https://github.com/mozdevs/cssremedy/issues/4
+ *
+ *
+ * 2. Allow adding a border to an element by just adding a border-width.
+ *
+ *    By default, the way the browser specifies that an element should have no
+ *    border is by setting it's border-style to `none` in the user-agent
+ *    stylesheet.
+ *
+ *    In order to easily add borders to elements by just setting the `border-width`
+ *    property, we change the default border-style for all elements to `solid`, and
+ *    use border-width to hide them instead. This way our `border` utilities only
+ *    need to set the `border-width` property instead of the entire `border`
+ *    shorthand, making our border utilities much more straightforward to compose.
+ *
+ *    https://github.com/tailwindcss/tailwindcss/pull/116
+ */
+
+*,
+::before,
+::after {
+  box-sizing: border-box; /* 1 */
+  border-width: 0; /* 2 */
+  border-style: solid; /* 2 */
+  border-color: #e2e8f0; /* 2 */
+}
+
+/*
+ * Ensure horizontal rules are visible by default
+ */
+
+hr {
+  border-top-width: 1px;
+}
+
+/**
+ * Undo the `border-style: none` reset that Normalize applies to images so that
+ * our `border-{width}` utilities have the expected effect.
+ *
+ * The Normalize reset is unnecessary for us since we default the border-width
+ * to 0 on all elements.
+ *
+ * https://github.com/tailwindcss/tailwindcss/issues/362
+ */
+
+img {
+  border-style: solid;
+}
+
+textarea {
+  resize: vertical;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+  color: #a0aec0;
+}
+
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
+  color: #a0aec0;
+}
+
+input::placeholder,
+textarea::placeholder {
+  color: #a0aec0;
+}
+
+button,
+[role="button"] {
+  cursor: pointer;
+}
+
+table {
+  border-collapse: collapse;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: inherit;
+  font-weight: inherit;
+}
+
+/**
+ * Reset links to optimize for opt-in styling instead of
+ * opt-out.
+ */
+
+a {
+  color: inherit;
+  text-decoration: inherit;
+}
+
+/**
+ * Reset form element properties that are easy to forget to
+ * style explicitly so you don't inadvertently introduce
+ * styles that deviate from your design system. These styles
+ * supplement a partial reset that is already applied by
+ * normalize.css.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  padding: 0;
+  line-height: inherit;
+  color: inherit;
+}
+
+/**
+ * Use the configured 'mono' font family for elements that
+ * are expected to be rendered with a monospace font, falling
+ * back to the system monospace stack if there is no configured
+ * 'mono' font family.
+ */
+
+pre,
+code,
+kbd,
+samp {
+  font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+/**
+ * Make replaced elements `display: block` by default as that's
+ * the behavior you want almost all of the time. Inspired by
+ * CSS Remedy, with `svg` added as well.
+ *
+ * https://github.com/mozdevs/cssremedy/issues/14
+ */
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+  display: block;
+  vertical-align: middle;
+}
+
+/**
+ * Constrain images and videos to the parent width and preserve
+ * their instrinsic aspect ratio.
+ *
+ * https://github.com/mozdevs/cssremedy/issues/14
+ */
+
+img,
+video {
+  max-width: 100%;
+  height: auto;
+}
+
+.container {
+  width: 100%;
+}
+
+@media (min-width: 640px) {
+  .container {
+    max-width: 640px;
+  }
+}
+
+@media (min-width: 768px) {
+  .container {
+    max-width: 768px;
+  }
+}
+
+@media (min-width: 1024px) {
+  .container {
+    max-width: 1024px;
+  }
+}
+
+@media (min-width: 1280px) {
+  .container {
+    max-width: 1280px;
+  }
+}
+
+.space-y-0 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0px * var(--space-y-reverse));
+}
+
+.space-x-0 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0px * var(--space-x-reverse));
+  margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-1 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0.25rem * var(--space-y-reverse));
+}
+
+.space-x-1 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0.25rem * var(--space-x-reverse));
+  margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-2 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0.5rem * var(--space-y-reverse));
+}
+
+.space-x-2 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0.5rem * var(--space-x-reverse));
+  margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-3 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0.75rem * var(--space-y-reverse));
+}
+
+.space-x-3 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0.75rem * var(--space-x-reverse));
+  margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-4 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1rem * var(--space-y-reverse));
+}
+
+.space-x-4 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1rem * var(--space-x-reverse));
+  margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-5 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1.25rem * var(--space-y-reverse));
+}
+
+.space-x-5 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1.25rem * var(--space-x-reverse));
+  margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-6 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1.5rem * var(--space-y-reverse));
+}
+
+.space-x-6 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1.5rem * var(--space-x-reverse));
+  margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-8 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(2rem * var(--space-y-reverse));
+}
+
+.space-x-8 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(2rem * var(--space-x-reverse));
+  margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-10 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(2.5rem * var(--space-y-reverse));
+}
+
+.space-x-10 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(2.5rem * var(--space-x-reverse));
+  margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-12 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(3rem * var(--space-y-reverse));
+}
+
+.space-x-12 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(3rem * var(--space-x-reverse));
+  margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-16 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(4rem * var(--space-y-reverse));
+}
+
+.space-x-16 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(4rem * var(--space-x-reverse));
+  margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-20 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(5rem * var(--space-y-reverse));
+}
+
+.space-x-20 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(5rem * var(--space-x-reverse));
+  margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-24 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(6rem * var(--space-y-reverse));
+}
+
+.space-x-24 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(6rem * var(--space-x-reverse));
+  margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-32 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(8rem * var(--space-y-reverse));
+}
+
+.space-x-32 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(8rem * var(--space-x-reverse));
+  margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-40 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(10rem * var(--space-y-reverse));
+}
+
+.space-x-40 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(10rem * var(--space-x-reverse));
+  margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-48 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(12rem * var(--space-y-reverse));
+}
+
+.space-x-48 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(12rem * var(--space-x-reverse));
+  margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-56 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(14rem * var(--space-y-reverse));
+}
+
+.space-x-56 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(14rem * var(--space-x-reverse));
+  margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-64 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(16rem * var(--space-y-reverse));
+}
+
+.space-x-64 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(16rem * var(--space-x-reverse));
+  margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-px > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1px * var(--space-y-reverse));
+}
+
+.space-x-px > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1px * var(--space-x-reverse));
+  margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-1 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+}
+
+.-space-x-1 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-0.25rem * var(--space-x-reverse));
+  margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-2 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+}
+
+.-space-x-2 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-0.5rem * var(--space-x-reverse));
+  margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-3 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+}
+
+.-space-x-3 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-0.75rem * var(--space-x-reverse));
+  margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-4 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1rem * var(--space-y-reverse));
+}
+
+.-space-x-4 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1rem * var(--space-x-reverse));
+  margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-5 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+}
+
+.-space-x-5 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1.25rem * var(--space-x-reverse));
+  margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-6 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+}
+
+.-space-x-6 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1.5rem * var(--space-x-reverse));
+  margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-8 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-2rem * var(--space-y-reverse));
+}
+
+.-space-x-8 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-2rem * var(--space-x-reverse));
+  margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-10 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+}
+
+.-space-x-10 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-2.5rem * var(--space-x-reverse));
+  margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-12 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-3rem * var(--space-y-reverse));
+}
+
+.-space-x-12 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-3rem * var(--space-x-reverse));
+  margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-16 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-4rem * var(--space-y-reverse));
+}
+
+.-space-x-16 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-4rem * var(--space-x-reverse));
+  margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-20 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-5rem * var(--space-y-reverse));
+}
+
+.-space-x-20 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-5rem * var(--space-x-reverse));
+  margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-24 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-6rem * var(--space-y-reverse));
+}
+
+.-space-x-24 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-6rem * var(--space-x-reverse));
+  margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-32 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-8rem * var(--space-y-reverse));
+}
+
+.-space-x-32 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-8rem * var(--space-x-reverse));
+  margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-40 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-10rem * var(--space-y-reverse));
+}
+
+.-space-x-40 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-10rem * var(--space-x-reverse));
+  margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-48 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-12rem * var(--space-y-reverse));
+}
+
+.-space-x-48 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-12rem * var(--space-x-reverse));
+  margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-56 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-14rem * var(--space-y-reverse));
+}
+
+.-space-x-56 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-14rem * var(--space-x-reverse));
+  margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-64 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-16rem * var(--space-y-reverse));
+}
+
+.-space-x-64 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-16rem * var(--space-x-reverse));
+  margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-px > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1px * var(--space-y-reverse));
+}
+
+.-space-x-px > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1px * var(--space-x-reverse));
+  margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-reverse > :not(template) ~ :not(template) {
+  --space-y-reverse: 1;
+}
+
+.space-x-reverse > :not(template) ~ :not(template) {
+  --space-x-reverse: 1;
+}
+
+.divide-y-0 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(0px * var(--divide-y-reverse));
+}
+
+.divide-x-0 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(0px * var(--divide-x-reverse));
+  border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-2 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(2px * var(--divide-y-reverse));
+}
+
+.divide-x-2 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(2px * var(--divide-x-reverse));
+  border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-4 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(4px * var(--divide-y-reverse));
+}
+
+.divide-x-4 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(4px * var(--divide-x-reverse));
+  border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-8 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(8px * var(--divide-y-reverse));
+}
+
+.divide-x-8 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(8px * var(--divide-x-reverse));
+  border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(1px * var(--divide-y-reverse));
+}
+
+.divide-x > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(1px * var(--divide-x-reverse));
+  border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-reverse > :not(template) ~ :not(template) {
+  --divide-y-reverse: 1;
+}
+
+.divide-x-reverse > :not(template) ~ :not(template) {
+  --divide-x-reverse: 1;
+}
+
+.divide-transparent > :not(template) ~ :not(template) {
+  border-color: transparent;
+}
+
+.divide-current > :not(template) ~ :not(template) {
+  border-color: currentColor;
+}
+
+.divide-black > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--divide-opacity));
+}
+
+.divide-white > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--divide-opacity));
+}
+
+.divide-gray-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--divide-opacity));
+}
+
+.divide-gray-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--divide-opacity));
+}
+
+.divide-gray-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--divide-opacity));
+}
+
+.divide-gray-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--divide-opacity));
+}
+
+.divide-gray-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--divide-opacity));
+}
+
+.divide-gray-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--divide-opacity));
+}
+
+.divide-gray-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--divide-opacity));
+}
+
+.divide-gray-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--divide-opacity));
+}
+
+.divide-gray-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--divide-opacity));
+}
+
+.divide-red-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--divide-opacity));
+}
+
+.divide-red-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--divide-opacity));
+}
+
+.divide-red-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--divide-opacity));
+}
+
+.divide-red-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--divide-opacity));
+}
+
+.divide-red-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--divide-opacity));
+}
+
+.divide-red-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--divide-opacity));
+}
+
+.divide-red-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--divide-opacity));
+}
+
+.divide-red-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--divide-opacity));
+}
+
+.divide-red-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--divide-opacity));
+}
+
+.divide-orange-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--divide-opacity));
+}
+
+.divide-orange-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--divide-opacity));
+}
+
+.divide-orange-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--divide-opacity));
+}
+
+.divide-orange-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--divide-opacity));
+}
+
+.divide-orange-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--divide-opacity));
+}
+
+.divide-orange-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--divide-opacity));
+}
+
+.divide-orange-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--divide-opacity));
+}
+
+.divide-orange-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--divide-opacity));
+}
+
+.divide-orange-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--divide-opacity));
+}
+
+.divide-yellow-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--divide-opacity));
+}
+
+.divide-yellow-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--divide-opacity));
+}
+
+.divide-yellow-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--divide-opacity));
+}
+
+.divide-yellow-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--divide-opacity));
+}
+
+.divide-yellow-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--divide-opacity));
+}
+
+.divide-yellow-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--divide-opacity));
+}
+
+.divide-yellow-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--divide-opacity));
+}
+
+.divide-yellow-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--divide-opacity));
+}
+
+.divide-yellow-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--divide-opacity));
+}
+
+.divide-green-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--divide-opacity));
+}
+
+.divide-green-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--divide-opacity));
+}
+
+.divide-green-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--divide-opacity));
+}
+
+.divide-green-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--divide-opacity));
+}
+
+.divide-green-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--divide-opacity));
+}
+
+.divide-green-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--divide-opacity));
+}
+
+.divide-green-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--divide-opacity));
+}
+
+.divide-green-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--divide-opacity));
+}
+
+.divide-green-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--divide-opacity));
+}
+
+.divide-teal-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--divide-opacity));
+}
+
+.divide-teal-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--divide-opacity));
+}
+
+.divide-teal-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--divide-opacity));
+}
+
+.divide-teal-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--divide-opacity));
+}
+
+.divide-teal-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--divide-opacity));
+}
+
+.divide-teal-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--divide-opacity));
+}
+
+.divide-teal-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--divide-opacity));
+}
+
+.divide-teal-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--divide-opacity));
+}
+
+.divide-teal-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--divide-opacity));
+}
+
+.divide-blue-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--divide-opacity));
+}
+
+.divide-blue-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--divide-opacity));
+}
+
+.divide-blue-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--divide-opacity));
+}
+
+.divide-blue-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--divide-opacity));
+}
+
+.divide-blue-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--divide-opacity));
+}
+
+.divide-blue-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--divide-opacity));
+}
+
+.divide-blue-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--divide-opacity));
+}
+
+.divide-blue-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--divide-opacity));
+}
+
+.divide-blue-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--divide-opacity));
+}
+
+.divide-indigo-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--divide-opacity));
+}
+
+.divide-indigo-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--divide-opacity));
+}
+
+.divide-indigo-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--divide-opacity));
+}
+
+.divide-indigo-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--divide-opacity));
+}
+
+.divide-indigo-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--divide-opacity));
+}
+
+.divide-indigo-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--divide-opacity));
+}
+
+.divide-indigo-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--divide-opacity));
+}
+
+.divide-indigo-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--divide-opacity));
+}
+
+.divide-indigo-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--divide-opacity));
+}
+
+.divide-purple-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--divide-opacity));
+}
+
+.divide-purple-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--divide-opacity));
+}
+
+.divide-purple-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--divide-opacity));
+}
+
+.divide-purple-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--divide-opacity));
+}
+
+.divide-purple-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--divide-opacity));
+}
+
+.divide-purple-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--divide-opacity));
+}
+
+.divide-purple-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--divide-opacity));
+}
+
+.divide-purple-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--divide-opacity));
+}
+
+.divide-purple-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--divide-opacity));
+}
+
+.divide-pink-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--divide-opacity));
+}
+
+.divide-pink-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--divide-opacity));
+}
+
+.divide-pink-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--divide-opacity));
+}
+
+.divide-pink-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--divide-opacity));
+}
+
+.divide-pink-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--divide-opacity));
+}
+
+.divide-pink-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--divide-opacity));
+}
+
+.divide-pink-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--divide-opacity));
+}
+
+.divide-pink-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--divide-opacity));
+}
+
+.divide-pink-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--divide-opacity));
+}
+
+.divide-solid > :not(template) ~ :not(template) {
+  border-style: solid;
+}
+
+.divide-dashed > :not(template) ~ :not(template) {
+  border-style: dashed;
+}
+
+.divide-dotted > :not(template) ~ :not(template) {
+  border-style: dotted;
+}
+
+.divide-double > :not(template) ~ :not(template) {
+  border-style: double;
+}
+
+.divide-none > :not(template) ~ :not(template) {
+  border-style: none;
+}
+
+.divide-opacity-0 > :not(template) ~ :not(template) {
+  --divide-opacity: 0;
+}
+
+.divide-opacity-25 > :not(template) ~ :not(template) {
+  --divide-opacity: 0.25;
+}
+
+.divide-opacity-50 > :not(template) ~ :not(template) {
+  --divide-opacity: 0.5;
+}
+
+.divide-opacity-75 > :not(template) ~ :not(template) {
+  --divide-opacity: 0.75;
+}
+
+.divide-opacity-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border-width: 0;
+}
+
+.not-sr-only {
+  position: static;
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
+  white-space: normal;
+}
+
+.focus\:sr-only:focus {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border-width: 0;
+}
+
+.focus\:not-sr-only:focus {
+  position: static;
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
+  white-space: normal;
+}
+
+.appearance-none {
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+}
+
+.bg-fixed {
+  background-attachment: fixed;
+}
+
+.bg-local {
+  background-attachment: local;
+}
+
+.bg-scroll {
+  background-attachment: scroll;
+}
+
+.bg-clip-border {
+  background-clip: border-box;
+}
+
+.bg-clip-padding {
+  background-clip: padding-box;
+}
+
+.bg-clip-content {
+  background-clip: content-box;
+}
+
+.bg-clip-text {
+  -webkit-background-clip: text;
+          background-clip: text;
+}
+
+.bg-transparent {
+  background-color: transparent;
+}
+
+.bg-current {
+  background-color: currentColor;
+}
+
+.bg-black {
+  --bg-opacity: 1;
+  background-color: #000;
+  background-color: rgba(0, 0, 0, var(--bg-opacity));
+}
+
+.bg-white {
+  --bg-opacity: 1;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, var(--bg-opacity));
+}
+
+.bg-gray-100 {
+  --bg-opacity: 1;
+  background-color: #f7fafc;
+  background-color: rgba(247, 250, 252, var(--bg-opacity));
+}
+
+.bg-gray-200 {
+  --bg-opacity: 1;
+  background-color: #edf2f7;
+  background-color: rgba(237, 242, 247, var(--bg-opacity));
+}
+
+.bg-gray-300 {
+  --bg-opacity: 1;
+  background-color: #e2e8f0;
+  background-color: rgba(226, 232, 240, var(--bg-opacity));
+}
+
+.bg-gray-400 {
+  --bg-opacity: 1;
+  background-color: #cbd5e0;
+  background-color: rgba(203, 213, 224, var(--bg-opacity));
+}
+
+.bg-gray-500 {
+  --bg-opacity: 1;
+  background-color: #a0aec0;
+  background-color: rgba(160, 174, 192, var(--bg-opacity));
+}
+
+.bg-gray-600 {
+  --bg-opacity: 1;
+  background-color: #718096;
+  background-color: rgba(113, 128, 150, var(--bg-opacity));
+}
+
+.bg-gray-700 {
+  --bg-opacity: 1;
+  background-color: #4a5568;
+  background-color: rgba(74, 85, 104, var(--bg-opacity));
+}
+
+.bg-gray-800 {
+  --bg-opacity: 1;
+  background-color: #2d3748;
+  background-color: rgba(45, 55, 72, var(--bg-opacity));
+}
+
+.bg-gray-900 {
+  --bg-opacity: 1;
+  background-color: #1a202c;
+  background-color: rgba(26, 32, 44, var(--bg-opacity));
+}
+
+.bg-red-100 {
+  --bg-opacity: 1;
+  background-color: #fff5f5;
+  background-color: rgba(255, 245, 245, var(--bg-opacity));
+}
+
+.bg-red-200 {
+  --bg-opacity: 1;
+  background-color: #fed7d7;
+  background-color: rgba(254, 215, 215, var(--bg-opacity));
+}
+
+.bg-red-300 {
+  --bg-opacity: 1;
+  background-color: #feb2b2;
+  background-color: rgba(254, 178, 178, var(--bg-opacity));
+}
+
+.bg-red-400 {
+  --bg-opacity: 1;
+  background-color: #fc8181;
+  background-color: rgba(252, 129, 129, var(--bg-opacity));
+}
+
+.bg-red-500 {
+  --bg-opacity: 1;
+  background-color: #f56565;
+  background-color: rgba(245, 101, 101, var(--bg-opacity));
+}
+
+.bg-red-600 {
+  --bg-opacity: 1;
+  background-color: #e53e3e;
+  background-color: rgba(229, 62, 62, var(--bg-opacity));
+}
+
+.bg-red-700 {
+  --bg-opacity: 1;
+  background-color: #c53030;
+  background-color: rgba(197, 48, 48, var(--bg-opacity));
+}
+
+.bg-red-800 {
+  --bg-opacity: 1;
+  background-color: #9b2c2c;
+  background-color: rgba(155, 44, 44, var(--bg-opacity));
+}
+
+.bg-red-900 {
+  --bg-opacity: 1;
+  background-color: #742a2a;
+  background-color: rgba(116, 42, 42, var(--bg-opacity));
+}
+
+.bg-orange-100 {
+  --bg-opacity: 1;
+  background-color: #fffaf0;
+  background-color: rgba(255, 250, 240, var(--bg-opacity));
+}
+
+.bg-orange-200 {
+  --bg-opacity: 1;
+  background-color: #feebc8;
+  background-color: rgba(254, 235, 200, var(--bg-opacity));
+}
+
+.bg-orange-300 {
+  --bg-opacity: 1;
+  background-color: #fbd38d;
+  background-color: rgba(251, 211, 141, var(--bg-opacity));
+}
+
+.bg-orange-400 {
+  --bg-opacity: 1;
+  background-color: #f6ad55;
+  background-color: rgba(246, 173, 85, var(--bg-opacity));
+}
+
+.bg-orange-500 {
+  --bg-opacity: 1;
+  background-color: #ed8936;
+  background-color: rgba(237, 137, 54, var(--bg-opacity));
+}
+
+.bg-orange-600 {
+  --bg-opacity: 1;
+  background-color: #dd6b20;
+  background-color: rgba(221, 107, 32, var(--bg-opacity));
+}
+
+.bg-orange-700 {
+  --bg-opacity: 1;
+  background-color: #c05621;
+  background-color: rgba(192, 86, 33, var(--bg-opacity));
+}
+
+.bg-orange-800 {
+  --bg-opacity: 1;
+  background-color: #9c4221;
+  background-color: rgba(156, 66, 33, var(--bg-opacity));
+}
+
+.bg-orange-900 {
+  --bg-opacity: 1;
+  background-color: #7b341e;
+  background-color: rgba(123, 52, 30, var(--bg-opacity));
+}
+
+.bg-yellow-100 {
+  --bg-opacity: 1;
+  background-color: #fffff0;
+  background-color: rgba(255, 255, 240, var(--bg-opacity));
+}
+
+.bg-yellow-200 {
+  --bg-opacity: 1;
+  background-color: #fefcbf;
+  background-color: rgba(254, 252, 191, var(--bg-opacity));
+}
+
+.bg-yellow-300 {
+  --bg-opacity: 1;
+  background-color: #faf089;
+  background-color: rgba(250, 240, 137, var(--bg-opacity));
+}
+
+.bg-yellow-400 {
+  --bg-opacity: 1;
+  background-color: #f6e05e;
+  background-color: rgba(246, 224, 94, var(--bg-opacity));
+}
+
+.bg-yellow-500 {
+  --bg-opacity: 1;
+  background-color: #ecc94b;
+  background-color: rgba(236, 201, 75, var(--bg-opacity));
+}
+
+.bg-yellow-600 {
+  --bg-opacity: 1;
+  background-color: #d69e2e;
+  background-color: rgba(214, 158, 46, var(--bg-opacity));
+}
+
+.bg-yellow-700 {
+  --bg-opacity: 1;
+  background-color: #b7791f;
+  background-color: rgba(183, 121, 31, var(--bg-opacity));
+}
+
+.bg-yellow-800 {
+  --bg-opacity: 1;
+  background-color: #975a16;
+  background-color: rgba(151, 90, 22, var(--bg-opacity));
+}
+
+.bg-yellow-900 {
+  --bg-opacity: 1;
+  background-color: #744210;
+  background-color: rgba(116, 66, 16, var(--bg-opacity));
+}
+
+.bg-green-100 {
+  --bg-opacity: 1;
+  background-color: #f0fff4;
+  background-color: rgba(240, 255, 244, var(--bg-opacity));
+}
+
+.bg-green-200 {
+  --bg-opacity: 1;
+  background-color: #c6f6d5;
+  background-color: rgba(198, 246, 213, var(--bg-opacity));
+}
+
+.bg-green-300 {
+  --bg-opacity: 1;
+  background-color: #9ae6b4;
+  background-color: rgba(154, 230, 180, var(--bg-opacity));
+}
+
+.bg-green-400 {
+  --bg-opacity: 1;
+  background-color: #68d391;
+  background-color: rgba(104, 211, 145, var(--bg-opacity));
+}
+
+.bg-green-500 {
+  --bg-opacity: 1;
+  background-color: #48bb78;
+  background-color: rgba(72, 187, 120, var(--bg-opacity));
+}
+
+.bg-green-600 {
+  --bg-opacity: 1;
+  background-color: #38a169;
+  background-color: rgba(56, 161, 105, var(--bg-opacity));
+}
+
+.bg-green-700 {
+  --bg-opacity: 1;
+  background-color: #2f855a;
+  background-color: rgba(47, 133, 90, var(--bg-opacity));
+}
+
+.bg-green-800 {
+  --bg-opacity: 1;
+  background-color: #276749;
+  background-color: rgba(39, 103, 73, var(--bg-opacity));
+}
+
+.bg-green-900 {
+  --bg-opacity: 1;
+  background-color: #22543d;
+  background-color: rgba(34, 84, 61, var(--bg-opacity));
+}
+
+.bg-teal-100 {
+  --bg-opacity: 1;
+  background-color: #e6fffa;
+  background-color: rgba(230, 255, 250, var(--bg-opacity));
+}
+
+.bg-teal-200 {
+  --bg-opacity: 1;
+  background-color: #b2f5ea;
+  background-color: rgba(178, 245, 234, var(--bg-opacity));
+}
+
+.bg-teal-300 {
+  --bg-opacity: 1;
+  background-color: #81e6d9;
+  background-color: rgba(129, 230, 217, var(--bg-opacity));
+}
+
+.bg-teal-400 {
+  --bg-opacity: 1;
+  background-color: #4fd1c5;
+  background-color: rgba(79, 209, 197, var(--bg-opacity));
+}
+
+.bg-teal-500 {
+  --bg-opacity: 1;
+  background-color: #38b2ac;
+  background-color: rgba(56, 178, 172, var(--bg-opacity));
+}
+
+.bg-teal-600 {
+  --bg-opacity: 1;
+  background-color: #319795;
+  background-color: rgba(49, 151, 149, var(--bg-opacity));
+}
+
+.bg-teal-700 {
+  --bg-opacity: 1;
+  background-color: #2c7a7b;
+  background-color: rgba(44, 122, 123, var(--bg-opacity));
+}
+
+.bg-teal-800 {
+  --bg-opacity: 1;
+  background-color: #285e61;
+  background-color: rgba(40, 94, 97, var(--bg-opacity));
+}
+
+.bg-teal-900 {
+  --bg-opacity: 1;
+  background-color: #234e52;
+  background-color: rgba(35, 78, 82, var(--bg-opacity));
+}
+
+.bg-blue-100 {
+  --bg-opacity: 1;
+  background-color: #ebf8ff;
+  background-color: rgba(235, 248, 255, var(--bg-opacity));
+}
+
+.bg-blue-200 {
+  --bg-opacity: 1;
+  background-color: #bee3f8;
+  background-color: rgba(190, 227, 248, var(--bg-opacity));
+}
+
+.bg-blue-300 {
+  --bg-opacity: 1;
+  background-color: #90cdf4;
+  background-color: rgba(144, 205, 244, var(--bg-opacity));
+}
+
+.bg-blue-400 {
+  --bg-opacity: 1;
+  background-color: #63b3ed;
+  background-color: rgba(99, 179, 237, var(--bg-opacity));
+}
+
+.bg-blue-500 {
+  --bg-opacity: 1;
+  background-color: #4299e1;
+  background-color: rgba(66, 153, 225, var(--bg-opacity));
+}
+
+.bg-blue-600 {
+  --bg-opacity: 1;
+  background-color: #3182ce;
+  background-color: rgba(49, 130, 206, var(--bg-opacity));
+}
+
+.bg-blue-700 {
+  --bg-opacity: 1;
+  background-color: #2b6cb0;
+  background-color: rgba(43, 108, 176, var(--bg-opacity));
+}
+
+.bg-blue-800 {
+  --bg-opacity: 1;
+  background-color: #2c5282;
+  background-color: rgba(44, 82, 130, var(--bg-opacity));
+}
+
+.bg-blue-900 {
+  --bg-opacity: 1;
+  background-color: #2a4365;
+  background-color: rgba(42, 67, 101, var(--bg-opacity));
+}
+
+.bg-indigo-100 {
+  --bg-opacity: 1;
+  background-color: #ebf4ff;
+  background-color: rgba(235, 244, 255, var(--bg-opacity));
+}
+
+.bg-indigo-200 {
+  --bg-opacity: 1;
+  background-color: #c3dafe;
+  background-color: rgba(195, 218, 254, var(--bg-opacity));
+}
+
+.bg-indigo-300 {
+  --bg-opacity: 1;
+  background-color: #a3bffa;
+  background-color: rgba(163, 191, 250, var(--bg-opacity));
+}
+
+.bg-indigo-400 {
+  --bg-opacity: 1;
+  background-color: #7f9cf5;
+  background-color: rgba(127, 156, 245, var(--bg-opacity));
+}
+
+.bg-indigo-500 {
+  --bg-opacity: 1;
+  background-color: #667eea;
+  background-color: rgba(102, 126, 234, var(--bg-opacity));
+}
+
+.bg-indigo-600 {
+  --bg-opacity: 1;
+  background-color: #5a67d8;
+  background-color: rgba(90, 103, 216, var(--bg-opacity));
+}
+
+.bg-indigo-700 {
+  --bg-opacity: 1;
+  background-color: #4c51bf;
+  background-color: rgba(76, 81, 191, var(--bg-opacity));
+}
+
+.bg-indigo-800 {
+  --bg-opacity: 1;
+  background-color: #434190;
+  background-color: rgba(67, 65, 144, var(--bg-opacity));
+}
+
+.bg-indigo-900 {
+  --bg-opacity: 1;
+  background-color: #3c366b;
+  background-color: rgba(60, 54, 107, var(--bg-opacity));
+}
+
+.bg-purple-100 {
+  --bg-opacity: 1;
+  background-color: #faf5ff;
+  background-color: rgba(250, 245, 255, var(--bg-opacity));
+}
+
+.bg-purple-200 {
+  --bg-opacity: 1;
+  background-color: #e9d8fd;
+  background-color: rgba(233, 216, 253, var(--bg-opacity));
+}
+
+.bg-purple-300 {
+  --bg-opacity: 1;
+  background-color: #d6bcfa;
+  background-color: rgba(214, 188, 250, var(--bg-opacity));
+}
+
+.bg-purple-400 {
+  --bg-opacity: 1;
+  background-color: #b794f4;
+  background-color: rgba(183, 148, 244, var(--bg-opacity));
+}
+
+.bg-purple-500 {
+  --bg-opacity: 1;
+  background-color: #9f7aea;
+  background-color: rgba(159, 122, 234, var(--bg-opacity));
+}
+
+.bg-purple-600 {
+  --bg-opacity: 1;
+  background-color: #805ad5;
+  background-color: rgba(128, 90, 213, var(--bg-opacity));
+}
+
+.bg-purple-700 {
+  --bg-opacity: 1;
+  background-color: #6b46c1;
+  background-color: rgba(107, 70, 193, var(--bg-opacity));
+}
+
+.bg-purple-800 {
+  --bg-opacity: 1;
+  background-color: #553c9a;
+  background-color: rgba(85, 60, 154, var(--bg-opacity));
+}
+
+.bg-purple-900 {
+  --bg-opacity: 1;
+  background-color: #44337a;
+  background-color: rgba(68, 51, 122, var(--bg-opacity));
+}
+
+.bg-pink-100 {
+  --bg-opacity: 1;
+  background-color: #fff5f7;
+  background-color: rgba(255, 245, 247, var(--bg-opacity));
+}
+
+.bg-pink-200 {
+  --bg-opacity: 1;
+  background-color: #fed7e2;
+  background-color: rgba(254, 215, 226, var(--bg-opacity));
+}
+
+.bg-pink-300 {
+  --bg-opacity: 1;
+  background-color: #fbb6ce;
+  background-color: rgba(251, 182, 206, var(--bg-opacity));
+}
+
+.bg-pink-400 {
+  --bg-opacity: 1;
+  background-color: #f687b3;
+  background-color: rgba(246, 135, 179, var(--bg-opacity));
+}
+
+.bg-pink-500 {
+  --bg-opacity: 1;
+  background-color: #ed64a6;
+  background-color: rgba(237, 100, 166, var(--bg-opacity));
+}
+
+.bg-pink-600 {
+  --bg-opacity: 1;
+  background-color: #d53f8c;
+  background-color: rgba(213, 63, 140, var(--bg-opacity));
+}
+
+.bg-pink-700 {
+  --bg-opacity: 1;
+  background-color: #b83280;
+  background-color: rgba(184, 50, 128, var(--bg-opacity));
+}
+
+.bg-pink-800 {
+  --bg-opacity: 1;
+  background-color: #97266d;
+  background-color: rgba(151, 38, 109, var(--bg-opacity));
+}
+
+.bg-pink-900 {
+  --bg-opacity: 1;
+  background-color: #702459;
+  background-color: rgba(112, 36, 89, var(--bg-opacity));
+}
+
+.hover\:bg-transparent:hover {
+  background-color: transparent;
+}
+
+.hover\:bg-current:hover {
+  background-color: currentColor;
+}
+
+.hover\:bg-black:hover {
+  --bg-opacity: 1;
+  background-color: #000;
+  background-color: rgba(0, 0, 0, var(--bg-opacity));
+}
+
+.hover\:bg-white:hover {
+  --bg-opacity: 1;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, var(--bg-opacity));
+}
+
+.hover\:bg-gray-100:hover {
+  --bg-opacity: 1;
+  background-color: #f7fafc;
+  background-color: rgba(247, 250, 252, var(--bg-opacity));
+}
+
+.hover\:bg-gray-200:hover {
+  --bg-opacity: 1;
+  background-color: #edf2f7;
+  background-color: rgba(237, 242, 247, var(--bg-opacity));
+}
+
+.hover\:bg-gray-300:hover {
+  --bg-opacity: 1;
+  background-color: #e2e8f0;
+  background-color: rgba(226, 232, 240, var(--bg-opacity));
+}
+
+.hover\:bg-gray-400:hover {
+  --bg-opacity: 1;
+  background-color: #cbd5e0;
+  background-color: rgba(203, 213, 224, var(--bg-opacity));
+}
+
+.hover\:bg-gray-500:hover {
+  --bg-opacity: 1;
+  background-color: #a0aec0;
+  background-color: rgba(160, 174, 192, var(--bg-opacity));
+}
+
+.hover\:bg-gray-600:hover {
+  --bg-opacity: 1;
+  background-color: #718096;
+  background-color: rgba(113, 128, 150, var(--bg-opacity));
+}
+
+.hover\:bg-gray-700:hover {
+  --bg-opacity: 1;
+  background-color: #4a5568;
+  background-color: rgba(74, 85, 104, var(--bg-opacity));
+}
+
+.hover\:bg-gray-800:hover {
+  --bg-opacity: 1;
+  background-color: #2d3748;
+  background-color: rgba(45, 55, 72, var(--bg-opacity));
+}
+
+.hover\:bg-gray-900:hover {
+  --bg-opacity: 1;
+  background-color: #1a202c;
+  background-color: rgba(26, 32, 44, var(--bg-opacity));
+}
+
+.hover\:bg-red-100:hover {
+  --bg-opacity: 1;
+  background-color: #fff5f5;
+  background-color: rgba(255, 245, 245, var(--bg-opacity));
+}
+
+.hover\:bg-red-200:hover {
+  --bg-opacity: 1;
+  background-color: #fed7d7;
+  background-color: rgba(254, 215, 215, var(--bg-opacity));
+}
+
+.hover\:bg-red-300:hover {
+  --bg-opacity: 1;
+  background-color: #feb2b2;
+  background-color: rgba(254, 178, 178, var(--bg-opacity));
+}
+
+.hover\:bg-red-400:hover {
+  --bg-opacity: 1;
+  background-color: #fc8181;
+  background-color: rgba(252, 129, 129, var(--bg-opacity));
+}
+
+.hover\:bg-red-500:hover {
+  --bg-opacity: 1;
+  background-color: #f56565;
+  background-color: rgba(245, 101, 101, var(--bg-opacity));
+}
+
+.hover\:bg-red-600:hover {
+  --bg-opacity: 1;
+  background-color: #e53e3e;
+  background-color: rgba(229, 62, 62, var(--bg-opacity));
+}
+
+.hover\:bg-red-700:hover {
+  --bg-opacity: 1;
+  background-color: #c53030;
+  background-color: rgba(197, 48, 48, var(--bg-opacity));
+}
+
+.hover\:bg-red-800:hover {
+  --bg-opacity: 1;
+  background-color: #9b2c2c;
+  background-color: rgba(155, 44, 44, var(--bg-opacity));
+}
+
+.hover\:bg-red-900:hover {
+  --bg-opacity: 1;
+  background-color: #742a2a;
+  background-color: rgba(116, 42, 42, var(--bg-opacity));
+}
+
+.hover\:bg-orange-100:hover {
+  --bg-opacity: 1;
+  background-color: #fffaf0;
+  background-color: rgba(255, 250, 240, var(--bg-opacity));
+}
+
+.hover\:bg-orange-200:hover {
+  --bg-opacity: 1;
+  background-color: #feebc8;
+  background-color: rgba(254, 235, 200, var(--bg-opacity));
+}
+
+.hover\:bg-orange-300:hover {
+  --bg-opacity: 1;
+  background-color: #fbd38d;
+  background-color: rgba(251, 211, 141, var(--bg-opacity));
+}
+
+.hover\:bg-orange-400:hover {
+  --bg-opacity: 1;
+  background-color: #f6ad55;
+  background-color: rgba(246, 173, 85, var(--bg-opacity));
+}
+
+.hover\:bg-orange-500:hover {
+  --bg-opacity: 1;
+  background-color: #ed8936;
+  background-color: rgba(237, 137, 54, var(--bg-opacity));
+}
+
+.hover\:bg-orange-600:hover {
+  --bg-opacity: 1;
+  background-color: #dd6b20;
+  background-color: rgba(221, 107, 32, var(--bg-opacity));
+}
+
+.hover\:bg-orange-700:hover {
+  --bg-opacity: 1;
+  background-color: #c05621;
+  background-color: rgba(192, 86, 33, var(--bg-opacity));
+}
+
+.hover\:bg-orange-800:hover {
+  --bg-opacity: 1;
+  background-color: #9c4221;
+  background-color: rgba(156, 66, 33, var(--bg-opacity));
+}
+
+.hover\:bg-orange-900:hover {
+  --bg-opacity: 1;
+  background-color: #7b341e;
+  background-color: rgba(123, 52, 30, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-100:hover {
+  --bg-opacity: 1;
+  background-color: #fffff0;
+  background-color: rgba(255, 255, 240, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-200:hover {
+  --bg-opacity: 1;
+  background-color: #fefcbf;
+  background-color: rgba(254, 252, 191, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-300:hover {
+  --bg-opacity: 1;
+  background-color: #faf089;
+  background-color: rgba(250, 240, 137, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-400:hover {
+  --bg-opacity: 1;
+  background-color: #f6e05e;
+  background-color: rgba(246, 224, 94, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-500:hover {
+  --bg-opacity: 1;
+  background-color: #ecc94b;
+  background-color: rgba(236, 201, 75, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-600:hover {
+  --bg-opacity: 1;
+  background-color: #d69e2e;
+  background-color: rgba(214, 158, 46, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-700:hover {
+  --bg-opacity: 1;
+  background-color: #b7791f;
+  background-color: rgba(183, 121, 31, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-800:hover {
+  --bg-opacity: 1;
+  background-color: #975a16;
+  background-color: rgba(151, 90, 22, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-900:hover {
+  --bg-opacity: 1;
+  background-color: #744210;
+  background-color: rgba(116, 66, 16, var(--bg-opacity));
+}
+
+.hover\:bg-green-100:hover {
+  --bg-opacity: 1;
+  background-color: #f0fff4;
+  background-color: rgba(240, 255, 244, var(--bg-opacity));
+}
+
+.hover\:bg-green-200:hover {
+  --bg-opacity: 1;
+  background-color: #c6f6d5;
+  background-color: rgba(198, 246, 213, var(--bg-opacity));
+}
+
+.hover\:bg-green-300:hover {
+  --bg-opacity: 1;
+  background-color: #9ae6b4;
+  background-color: rgba(154, 230, 180, var(--bg-opacity));
+}
+
+.hover\:bg-green-400:hover {
+  --bg-opacity: 1;
+  background-color: #68d391;
+  background-color: rgba(104, 211, 145, var(--bg-opacity));
+}
+
+.hover\:bg-green-500:hover {
+  --bg-opacity: 1;
+  background-color: #48bb78;
+  background-color: rgba(72, 187, 120, var(--bg-opacity));
+}
+
+.hover\:bg-green-600:hover {
+  --bg-opacity: 1;
+  background-color: #38a169;
+  background-color: rgba(56, 161, 105, var(--bg-opacity));
+}
+
+.hover\:bg-green-700:hover {
+  --bg-opacity: 1;
+  background-color: #2f855a;
+  background-color: rgba(47, 133, 90, var(--bg-opacity));
+}
+
+.hover\:bg-green-800:hover {
+  --bg-opacity: 1;
+  background-color: #276749;
+  background-color: rgba(39, 103, 73, var(--bg-opacity));
+}
+
+.hover\:bg-green-900:hover {
+  --bg-opacity: 1;
+  background-color: #22543d;
+  background-color: rgba(34, 84, 61, var(--bg-opacity));
+}
+
+.hover\:bg-teal-100:hover {
+  --bg-opacity: 1;
+  background-color: #e6fffa;
+  background-color: rgba(230, 255, 250, var(--bg-opacity));
+}
+
+.hover\:bg-teal-200:hover {
+  --bg-opacity: 1;
+  background-color: #b2f5ea;
+  background-color: rgba(178, 245, 234, var(--bg-opacity));
+}
+
+.hover\:bg-teal-300:hover {
+  --bg-opacity: 1;
+  background-color: #81e6d9;
+  background-color: rgba(129, 230, 217, var(--bg-opacity));
+}
+
+.hover\:bg-teal-400:hover {
+  --bg-opacity: 1;
+  background-color: #4fd1c5;
+  background-color: rgba(79, 209, 197, var(--bg-opacity));
+}
+
+.hover\:bg-teal-500:hover {
+  --bg-opacity: 1;
+  background-color: #38b2ac;
+  background-color: rgba(56, 178, 172, var(--bg-opacity));
+}
+
+.hover\:bg-teal-600:hover {
+  --bg-opacity: 1;
+  background-color: #319795;
+  background-color: rgba(49, 151, 149, var(--bg-opacity));
+}
+
+.hover\:bg-teal-700:hover {
+  --bg-opacity: 1;
+  background-color: #2c7a7b;
+  background-color: rgba(44, 122, 123, var(--bg-opacity));
+}
+
+.hover\:bg-teal-800:hover {
+  --bg-opacity: 1;
+  background-color: #285e61;
+  background-color: rgba(40, 94, 97, var(--bg-opacity));
+}
+
+.hover\:bg-teal-900:hover {
+  --bg-opacity: 1;
+  background-color: #234e52;
+  background-color: rgba(35, 78, 82, var(--bg-opacity));
+}
+
+.hover\:bg-blue-100:hover {
+  --bg-opacity: 1;
+  background-color: #ebf8ff;
+  background-color: rgba(235, 248, 255, var(--bg-opacity));
+}
+
+.hover\:bg-blue-200:hover {
+  --bg-opacity: 1;
+  background-color: #bee3f8;
+  background-color: rgba(190, 227, 248, var(--bg-opacity));
+}
+
+.hover\:bg-blue-300:hover {
+  --bg-opacity: 1;
+  background-color: #90cdf4;
+  background-color: rgba(144, 205, 244, var(--bg-opacity));
+}
+
+.hover\:bg-blue-400:hover {
+  --bg-opacity: 1;
+  background-color: #63b3ed;
+  background-color: rgba(99, 179, 237, var(--bg-opacity));
+}
+
+.hover\:bg-blue-500:hover {
+  --bg-opacity: 1;
+  background-color: #4299e1;
+  background-color: rgba(66, 153, 225, var(--bg-opacity));
+}
+
+.hover\:bg-blue-600:hover {
+  --bg-opacity: 1;
+  background-color: #3182ce;
+  background-color: rgba(49, 130, 206, var(--bg-opacity));
+}
+
+.hover\:bg-blue-700:hover {
+  --bg-opacity: 1;
+  background-color: #2b6cb0;
+  background-color: rgba(43, 108, 176, var(--bg-opacity));
+}
+
+.hover\:bg-blue-800:hover {
+  --bg-opacity: 1;
+  background-color: #2c5282;
+  background-color: rgba(44, 82, 130, var(--bg-opacity));
+}
+
+.hover\:bg-blue-900:hover {
+  --bg-opacity: 1;
+  background-color: #2a4365;
+  background-color: rgba(42, 67, 101, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-100:hover {
+  --bg-opacity: 1;
+  background-color: #ebf4ff;
+  background-color: rgba(235, 244, 255, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-200:hover {
+  --bg-opacity: 1;
+  background-color: #c3dafe;
+  background-color: rgba(195, 218, 254, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-300:hover {
+  --bg-opacity: 1;
+  background-color: #a3bffa;
+  background-color: rgba(163, 191, 250, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-400:hover {
+  --bg-opacity: 1;
+  background-color: #7f9cf5;
+  background-color: rgba(127, 156, 245, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-500:hover {
+  --bg-opacity: 1;
+  background-color: #667eea;
+  background-color: rgba(102, 126, 234, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-600:hover {
+  --bg-opacity: 1;
+  background-color: #5a67d8;
+  background-color: rgba(90, 103, 216, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-700:hover {
+  --bg-opacity: 1;
+  background-color: #4c51bf;
+  background-color: rgba(76, 81, 191, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-800:hover {
+  --bg-opacity: 1;
+  background-color: #434190;
+  background-color: rgba(67, 65, 144, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-900:hover {
+  --bg-opacity: 1;
+  background-color: #3c366b;
+  background-color: rgba(60, 54, 107, var(--bg-opacity));
+}
+
+.hover\:bg-purple-100:hover {
+  --bg-opacity: 1;
+  background-color: #faf5ff;
+  background-color: rgba(250, 245, 255, var(--bg-opacity));
+}
+
+.hover\:bg-purple-200:hover {
+  --bg-opacity: 1;
+  background-color: #e9d8fd;
+  background-color: rgba(233, 216, 253, var(--bg-opacity));
+}
+
+.hover\:bg-purple-300:hover {
+  --bg-opacity: 1;
+  background-color: #d6bcfa;
+  background-color: rgba(214, 188, 250, var(--bg-opacity));
+}
+
+.hover\:bg-purple-400:hover {
+  --bg-opacity: 1;
+  background-color: #b794f4;
+  background-color: rgba(183, 148, 244, var(--bg-opacity));
+}
+
+.hover\:bg-purple-500:hover {
+  --bg-opacity: 1;
+  background-color: #9f7aea;
+  background-color: rgba(159, 122, 234, var(--bg-opacity));
+}
+
+.hover\:bg-purple-600:hover {
+  --bg-opacity: 1;
+  background-color: #805ad5;
+  background-color: rgba(128, 90, 213, var(--bg-opacity));
+}
+
+.hover\:bg-purple-700:hover {
+  --bg-opacity: 1;
+  background-color: #6b46c1;
+  background-color: rgba(107, 70, 193, var(--bg-opacity));
+}
+
+.hover\:bg-purple-800:hover {
+  --bg-opacity: 1;
+  background-color: #553c9a;
+  background-color: rgba(85, 60, 154, var(--bg-opacity));
+}
+
+.hover\:bg-purple-900:hover {
+  --bg-opacity: 1;
+  background-color: #44337a;
+  background-color: rgba(68, 51, 122, var(--bg-opacity));
+}
+
+.hover\:bg-pink-100:hover {
+  --bg-opacity: 1;
+  background-color: #fff5f7;
+  background-color: rgba(255, 245, 247, var(--bg-opacity));
+}
+
+.hover\:bg-pink-200:hover {
+  --bg-opacity: 1;
+  background-color: #fed7e2;
+  background-color: rgba(254, 215, 226, var(--bg-opacity));
+}
+
+.hover\:bg-pink-300:hover {
+  --bg-opacity: 1;
+  background-color: #fbb6ce;
+  background-color: rgba(251, 182, 206, var(--bg-opacity));
+}
+
+.hover\:bg-pink-400:hover {
+  --bg-opacity: 1;
+  background-color: #f687b3;
+  background-color: rgba(246, 135, 179, var(--bg-opacity));
+}
+
+.hover\:bg-pink-500:hover {
+  --bg-opacity: 1;
+  background-color: #ed64a6;
+  background-color: rgba(237, 100, 166, var(--bg-opacity));
+}
+
+.hover\:bg-pink-600:hover {
+  --bg-opacity: 1;
+  background-color: #d53f8c;
+  background-color: rgba(213, 63, 140, var(--bg-opacity));
+}
+
+.hover\:bg-pink-700:hover {
+  --bg-opacity: 1;
+  background-color: #b83280;
+  background-color: rgba(184, 50, 128, var(--bg-opacity));
+}
+
+.hover\:bg-pink-800:hover {
+  --bg-opacity: 1;
+  background-color: #97266d;
+  background-color: rgba(151, 38, 109, var(--bg-opacity));
+}
+
+.hover\:bg-pink-900:hover {
+  --bg-opacity: 1;
+  background-color: #702459;
+  background-color: rgba(112, 36, 89, var(--bg-opacity));
+}
+
+.focus\:bg-transparent:focus {
+  background-color: transparent;
+}
+
+.focus\:bg-current:focus {
+  background-color: currentColor;
+}
+
+.focus\:bg-black:focus {
+  --bg-opacity: 1;
+  background-color: #000;
+  background-color: rgba(0, 0, 0, var(--bg-opacity));
+}
+
+.focus\:bg-white:focus {
+  --bg-opacity: 1;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, var(--bg-opacity));
+}
+
+.focus\:bg-gray-100:focus {
+  --bg-opacity: 1;
+  background-color: #f7fafc;
+  background-color: rgba(247, 250, 252, var(--bg-opacity));
+}
+
+.focus\:bg-gray-200:focus {
+  --bg-opacity: 1;
+  background-color: #edf2f7;
+  background-color: rgba(237, 242, 247, var(--bg-opacity));
+}
+
+.focus\:bg-gray-300:focus {
+  --bg-opacity: 1;
+  background-color: #e2e8f0;
+  background-color: rgba(226, 232, 240, var(--bg-opacity));
+}
+
+.focus\:bg-gray-400:focus {
+  --bg-opacity: 1;
+  background-color: #cbd5e0;
+  background-color: rgba(203, 213, 224, var(--bg-opacity));
+}
+
+.focus\:bg-gray-500:focus {
+  --bg-opacity: 1;
+  background-color: #a0aec0;
+  background-color: rgba(160, 174, 192, var(--bg-opacity));
+}
+
+.focus\:bg-gray-600:focus {
+  --bg-opacity: 1;
+  background-color: #718096;
+  background-color: rgba(113, 128, 150, var(--bg-opacity));
+}
+
+.focus\:bg-gray-700:focus {
+  --bg-opacity: 1;
+  background-color: #4a5568;
+  background-color: rgba(74, 85, 104, var(--bg-opacity));
+}
+
+.focus\:bg-gray-800:focus {
+  --bg-opacity: 1;
+  background-color: #2d3748;
+  background-color: rgba(45, 55, 72, var(--bg-opacity));
+}
+
+.focus\:bg-gray-900:focus {
+  --bg-opacity: 1;
+  background-color: #1a202c;
+  background-color: rgba(26, 32, 44, var(--bg-opacity));
+}
+
+.focus\:bg-red-100:focus {
+  --bg-opacity: 1;
+  background-color: #fff5f5;
+  background-color: rgba(255, 245, 245, var(--bg-opacity));
+}
+
+.focus\:bg-red-200:focus {
+  --bg-opacity: 1;
+  background-color: #fed7d7;
+  background-color: rgba(254, 215, 215, var(--bg-opacity));
+}
+
+.focus\:bg-red-300:focus {
+  --bg-opacity: 1;
+  background-color: #feb2b2;
+  background-color: rgba(254, 178, 178, var(--bg-opacity));
+}
+
+.focus\:bg-red-400:focus {
+  --bg-opacity: 1;
+  background-color: #fc8181;
+  background-color: rgba(252, 129, 129, var(--bg-opacity));
+}
+
+.focus\:bg-red-500:focus {
+  --bg-opacity: 1;
+  background-color: #f56565;
+  background-color: rgba(245, 101, 101, var(--bg-opacity));
+}
+
+.focus\:bg-red-600:focus {
+  --bg-opacity: 1;
+  background-color: #e53e3e;
+  background-color: rgba(229, 62, 62, var(--bg-opacity));
+}
+
+.focus\:bg-red-700:focus {
+  --bg-opacity: 1;
+  background-color: #c53030;
+  background-color: rgba(197, 48, 48, var(--bg-opacity));
+}
+
+.focus\:bg-red-800:focus {
+  --bg-opacity: 1;
+  background-color: #9b2c2c;
+  background-color: rgba(155, 44, 44, var(--bg-opacity));
+}
+
+.focus\:bg-red-900:focus {
+  --bg-opacity: 1;
+  background-color: #742a2a;
+  background-color: rgba(116, 42, 42, var(--bg-opacity));
+}
+
+.focus\:bg-orange-100:focus {
+  --bg-opacity: 1;
+  background-color: #fffaf0;
+  background-color: rgba(255, 250, 240, var(--bg-opacity));
+}
+
+.focus\:bg-orange-200:focus {
+  --bg-opacity: 1;
+  background-color: #feebc8;
+  background-color: rgba(254, 235, 200, var(--bg-opacity));
+}
+
+.focus\:bg-orange-300:focus {
+  --bg-opacity: 1;
+  background-color: #fbd38d;
+  background-color: rgba(251, 211, 141, var(--bg-opacity));
+}
+
+.focus\:bg-orange-400:focus {
+  --bg-opacity: 1;
+  background-color: #f6ad55;
+  background-color: rgba(246, 173, 85, var(--bg-opacity));
+}
+
+.focus\:bg-orange-500:focus {
+  --bg-opacity: 1;
+  background-color: #ed8936;
+  background-color: rgba(237, 137, 54, var(--bg-opacity));
+}
+
+.focus\:bg-orange-600:focus {
+  --bg-opacity: 1;
+  background-color: #dd6b20;
+  background-color: rgba(221, 107, 32, var(--bg-opacity));
+}
+
+.focus\:bg-orange-700:focus {
+  --bg-opacity: 1;
+  background-color: #c05621;
+  background-color: rgba(192, 86, 33, var(--bg-opacity));
+}
+
+.focus\:bg-orange-800:focus {
+  --bg-opacity: 1;
+  background-color: #9c4221;
+  background-color: rgba(156, 66, 33, var(--bg-opacity));
+}
+
+.focus\:bg-orange-900:focus {
+  --bg-opacity: 1;
+  background-color: #7b341e;
+  background-color: rgba(123, 52, 30, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-100:focus {
+  --bg-opacity: 1;
+  background-color: #fffff0;
+  background-color: rgba(255, 255, 240, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-200:focus {
+  --bg-opacity: 1;
+  background-color: #fefcbf;
+  background-color: rgba(254, 252, 191, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-300:focus {
+  --bg-opacity: 1;
+  background-color: #faf089;
+  background-color: rgba(250, 240, 137, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-400:focus {
+  --bg-opacity: 1;
+  background-color: #f6e05e;
+  background-color: rgba(246, 224, 94, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-500:focus {
+  --bg-opacity: 1;
+  background-color: #ecc94b;
+  background-color: rgba(236, 201, 75, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-600:focus {
+  --bg-opacity: 1;
+  background-color: #d69e2e;
+  background-color: rgba(214, 158, 46, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-700:focus {
+  --bg-opacity: 1;
+  background-color: #b7791f;
+  background-color: rgba(183, 121, 31, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-800:focus {
+  --bg-opacity: 1;
+  background-color: #975a16;
+  background-color: rgba(151, 90, 22, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-900:focus {
+  --bg-opacity: 1;
+  background-color: #744210;
+  background-color: rgba(116, 66, 16, var(--bg-opacity));
+}
+
+.focus\:bg-green-100:focus {
+  --bg-opacity: 1;
+  background-color: #f0fff4;
+  background-color: rgba(240, 255, 244, var(--bg-opacity));
+}
+
+.focus\:bg-green-200:focus {
+  --bg-opacity: 1;
+  background-color: #c6f6d5;
+  background-color: rgba(198, 246, 213, var(--bg-opacity));
+}
+
+.focus\:bg-green-300:focus {
+  --bg-opacity: 1;
+  background-color: #9ae6b4;
+  background-color: rgba(154, 230, 180, var(--bg-opacity));
+}
+
+.focus\:bg-green-400:focus {
+  --bg-opacity: 1;
+  background-color: #68d391;
+  background-color: rgba(104, 211, 145, var(--bg-opacity));
+}
+
+.focus\:bg-green-500:focus {
+  --bg-opacity: 1;
+  background-color: #48bb78;
+  background-color: rgba(72, 187, 120, var(--bg-opacity));
+}
+
+.focus\:bg-green-600:focus {
+  --bg-opacity: 1;
+  background-color: #38a169;
+  background-color: rgba(56, 161, 105, var(--bg-opacity));
+}
+
+.focus\:bg-green-700:focus {
+  --bg-opacity: 1;
+  background-color: #2f855a;
+  background-color: rgba(47, 133, 90, var(--bg-opacity));
+}
+
+.focus\:bg-green-800:focus {
+  --bg-opacity: 1;
+  background-color: #276749;
+  background-color: rgba(39, 103, 73, var(--bg-opacity));
+}
+
+.focus\:bg-green-900:focus {
+  --bg-opacity: 1;
+  background-color: #22543d;
+  background-color: rgba(34, 84, 61, var(--bg-opacity));
+}
+
+.focus\:bg-teal-100:focus {
+  --bg-opacity: 1;
+  background-color: #e6fffa;
+  background-color: rgba(230, 255, 250, var(--bg-opacity));
+}
+
+.focus\:bg-teal-200:focus {
+  --bg-opacity: 1;
+  background-color: #b2f5ea;
+  background-color: rgba(178, 245, 234, var(--bg-opacity));
+}
+
+.focus\:bg-teal-300:focus {
+  --bg-opacity: 1;
+  background-color: #81e6d9;
+  background-color: rgba(129, 230, 217, var(--bg-opacity));
+}
+
+.focus\:bg-teal-400:focus {
+  --bg-opacity: 1;
+  background-color: #4fd1c5;
+  background-color: rgba(79, 209, 197, var(--bg-opacity));
+}
+
+.focus\:bg-teal-500:focus {
+  --bg-opacity: 1;
+  background-color: #38b2ac;
+  background-color: rgba(56, 178, 172, var(--bg-opacity));
+}
+
+.focus\:bg-teal-600:focus {
+  --bg-opacity: 1;
+  background-color: #319795;
+  background-color: rgba(49, 151, 149, var(--bg-opacity));
+}
+
+.focus\:bg-teal-700:focus {
+  --bg-opacity: 1;
+  background-color: #2c7a7b;
+  background-color: rgba(44, 122, 123, var(--bg-opacity));
+}
+
+.focus\:bg-teal-800:focus {
+  --bg-opacity: 1;
+  background-color: #285e61;
+  background-color: rgba(40, 94, 97, var(--bg-opacity));
+}
+
+.focus\:bg-teal-900:focus {
+  --bg-opacity: 1;
+  background-color: #234e52;
+  background-color: rgba(35, 78, 82, var(--bg-opacity));
+}
+
+.focus\:bg-blue-100:focus {
+  --bg-opacity: 1;
+  background-color: #ebf8ff;
+  background-color: rgba(235, 248, 255, var(--bg-opacity));
+}
+
+.focus\:bg-blue-200:focus {
+  --bg-opacity: 1;
+  background-color: #bee3f8;
+  background-color: rgba(190, 227, 248, var(--bg-opacity));
+}
+
+.focus\:bg-blue-300:focus {
+  --bg-opacity: 1;
+  background-color: #90cdf4;
+  background-color: rgba(144, 205, 244, var(--bg-opacity));
+}
+
+.focus\:bg-blue-400:focus {
+  --bg-opacity: 1;
+  background-color: #63b3ed;
+  background-color: rgba(99, 179, 237, var(--bg-opacity));
+}
+
+.focus\:bg-blue-500:focus {
+  --bg-opacity: 1;
+  background-color: #4299e1;
+  background-color: rgba(66, 153, 225, var(--bg-opacity));
+}
+
+.focus\:bg-blue-600:focus {
+  --bg-opacity: 1;
+  background-color: #3182ce;
+  background-color: rgba(49, 130, 206, var(--bg-opacity));
+}
+
+.focus\:bg-blue-700:focus {
+  --bg-opacity: 1;
+  background-color: #2b6cb0;
+  background-color: rgba(43, 108, 176, var(--bg-opacity));
+}
+
+.focus\:bg-blue-800:focus {
+  --bg-opacity: 1;
+  background-color: #2c5282;
+  background-color: rgba(44, 82, 130, var(--bg-opacity));
+}
+
+.focus\:bg-blue-900:focus {
+  --bg-opacity: 1;
+  background-color: #2a4365;
+  background-color: rgba(42, 67, 101, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-100:focus {
+  --bg-opacity: 1;
+  background-color: #ebf4ff;
+  background-color: rgba(235, 244, 255, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-200:focus {
+  --bg-opacity: 1;
+  background-color: #c3dafe;
+  background-color: rgba(195, 218, 254, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-300:focus {
+  --bg-opacity: 1;
+  background-color: #a3bffa;
+  background-color: rgba(163, 191, 250, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-400:focus {
+  --bg-opacity: 1;
+  background-color: #7f9cf5;
+  background-color: rgba(127, 156, 245, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-500:focus {
+  --bg-opacity: 1;
+  background-color: #667eea;
+  background-color: rgba(102, 126, 234, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-600:focus {
+  --bg-opacity: 1;
+  background-color: #5a67d8;
+  background-color: rgba(90, 103, 216, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-700:focus {
+  --bg-opacity: 1;
+  background-color: #4c51bf;
+  background-color: rgba(76, 81, 191, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-800:focus {
+  --bg-opacity: 1;
+  background-color: #434190;
+  background-color: rgba(67, 65, 144, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-900:focus {
+  --bg-opacity: 1;
+  background-color: #3c366b;
+  background-color: rgba(60, 54, 107, var(--bg-opacity));
+}
+
+.focus\:bg-purple-100:focus {
+  --bg-opacity: 1;
+  background-color: #faf5ff;
+  background-color: rgba(250, 245, 255, var(--bg-opacity));
+}
+
+.focus\:bg-purple-200:focus {
+  --bg-opacity: 1;
+  background-color: #e9d8fd;
+  background-color: rgba(233, 216, 253, var(--bg-opacity));
+}
+
+.focus\:bg-purple-300:focus {
+  --bg-opacity: 1;
+  background-color: #d6bcfa;
+  background-color: rgba(214, 188, 250, var(--bg-opacity));
+}
+
+.focus\:bg-purple-400:focus {
+  --bg-opacity: 1;
+  background-color: #b794f4;
+  background-color: rgba(183, 148, 244, var(--bg-opacity));
+}
+
+.focus\:bg-purple-500:focus {
+  --bg-opacity: 1;
+  background-color: #9f7aea;
+  background-color: rgba(159, 122, 234, var(--bg-opacity));
+}
+
+.focus\:bg-purple-600:focus {
+  --bg-opacity: 1;
+  background-color: #805ad5;
+  background-color: rgba(128, 90, 213, var(--bg-opacity));
+}
+
+.focus\:bg-purple-700:focus {
+  --bg-opacity: 1;
+  background-color: #6b46c1;
+  background-color: rgba(107, 70, 193, var(--bg-opacity));
+}
+
+.focus\:bg-purple-800:focus {
+  --bg-opacity: 1;
+  background-color: #553c9a;
+  background-color: rgba(85, 60, 154, var(--bg-opacity));
+}
+
+.focus\:bg-purple-900:focus {
+  --bg-opacity: 1;
+  background-color: #44337a;
+  background-color: rgba(68, 51, 122, var(--bg-opacity));
+}
+
+.focus\:bg-pink-100:focus {
+  --bg-opacity: 1;
+  background-color: #fff5f7;
+  background-color: rgba(255, 245, 247, var(--bg-opacity));
+}
+
+.focus\:bg-pink-200:focus {
+  --bg-opacity: 1;
+  background-color: #fed7e2;
+  background-color: rgba(254, 215, 226, var(--bg-opacity));
+}
+
+.focus\:bg-pink-300:focus {
+  --bg-opacity: 1;
+  background-color: #fbb6ce;
+  background-color: rgba(251, 182, 206, var(--bg-opacity));
+}
+
+.focus\:bg-pink-400:focus {
+  --bg-opacity: 1;
+  background-color: #f687b3;
+  background-color: rgba(246, 135, 179, var(--bg-opacity));
+}
+
+.focus\:bg-pink-500:focus {
+  --bg-opacity: 1;
+  background-color: #ed64a6;
+  background-color: rgba(237, 100, 166, var(--bg-opacity));
+}
+
+.focus\:bg-pink-600:focus {
+  --bg-opacity: 1;
+  background-color: #d53f8c;
+  background-color: rgba(213, 63, 140, var(--bg-opacity));
+}
+
+.focus\:bg-pink-700:focus {
+  --bg-opacity: 1;
+  background-color: #b83280;
+  background-color: rgba(184, 50, 128, var(--bg-opacity));
+}
+
+.focus\:bg-pink-800:focus {
+  --bg-opacity: 1;
+  background-color: #97266d;
+  background-color: rgba(151, 38, 109, var(--bg-opacity));
+}
+
+.focus\:bg-pink-900:focus {
+  --bg-opacity: 1;
+  background-color: #702459;
+  background-color: rgba(112, 36, 89, var(--bg-opacity));
+}
+
+.bg-none {
+  background-image: none;
+}
+
+.bg-gradient-to-t {
+  background-image: linear-gradient(to top, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-tr {
+  background-image: linear-gradient(to top right, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-r {
+  background-image: linear-gradient(to right, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-br {
+  background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-b {
+  background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-bl {
+  background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-l {
+  background-image: linear-gradient(to left, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-tl {
+  background-image: linear-gradient(to top left, var(--gradient-color-stops));
+}
+
+.from-transparent {
+  --gradient-from-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.from-current {
+  --gradient-from-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.from-black {
+  --gradient-from-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.from-white {
+  --gradient-from-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.from-gray-100 {
+  --gradient-from-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.from-gray-200 {
+  --gradient-from-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.from-gray-300 {
+  --gradient-from-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.from-gray-400 {
+  --gradient-from-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.from-gray-500 {
+  --gradient-from-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.from-gray-600 {
+  --gradient-from-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.from-gray-700 {
+  --gradient-from-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.from-gray-800 {
+  --gradient-from-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.from-gray-900 {
+  --gradient-from-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.from-red-100 {
+  --gradient-from-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.from-red-200 {
+  --gradient-from-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.from-red-300 {
+  --gradient-from-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.from-red-400 {
+  --gradient-from-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.from-red-500 {
+  --gradient-from-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.from-red-600 {
+  --gradient-from-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.from-red-700 {
+  --gradient-from-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.from-red-800 {
+  --gradient-from-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.from-red-900 {
+  --gradient-from-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.from-orange-100 {
+  --gradient-from-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.from-orange-200 {
+  --gradient-from-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.from-orange-300 {
+  --gradient-from-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.from-orange-400 {
+  --gradient-from-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.from-orange-500 {
+  --gradient-from-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.from-orange-600 {
+  --gradient-from-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.from-orange-700 {
+  --gradient-from-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.from-orange-800 {
+  --gradient-from-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.from-orange-900 {
+  --gradient-from-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.from-yellow-100 {
+  --gradient-from-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.from-yellow-200 {
+  --gradient-from-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.from-yellow-300 {
+  --gradient-from-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.from-yellow-400 {
+  --gradient-from-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.from-yellow-500 {
+  --gradient-from-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.from-yellow-600 {
+  --gradient-from-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.from-yellow-700 {
+  --gradient-from-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.from-yellow-800 {
+  --gradient-from-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.from-yellow-900 {
+  --gradient-from-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.from-green-100 {
+  --gradient-from-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.from-green-200 {
+  --gradient-from-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.from-green-300 {
+  --gradient-from-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.from-green-400 {
+  --gradient-from-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.from-green-500 {
+  --gradient-from-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.from-green-600 {
+  --gradient-from-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.from-green-700 {
+  --gradient-from-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.from-green-800 {
+  --gradient-from-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.from-green-900 {
+  --gradient-from-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.from-teal-100 {
+  --gradient-from-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.from-teal-200 {
+  --gradient-from-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.from-teal-300 {
+  --gradient-from-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.from-teal-400 {
+  --gradient-from-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.from-teal-500 {
+  --gradient-from-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.from-teal-600 {
+  --gradient-from-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.from-teal-700 {
+  --gradient-from-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.from-teal-800 {
+  --gradient-from-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.from-teal-900 {
+  --gradient-from-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.from-blue-100 {
+  --gradient-from-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.from-blue-200 {
+  --gradient-from-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.from-blue-300 {
+  --gradient-from-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.from-blue-400 {
+  --gradient-from-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.from-blue-500 {
+  --gradient-from-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.from-blue-600 {
+  --gradient-from-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.from-blue-700 {
+  --gradient-from-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.from-blue-800 {
+  --gradient-from-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.from-blue-900 {
+  --gradient-from-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.from-indigo-100 {
+  --gradient-from-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.from-indigo-200 {
+  --gradient-from-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.from-indigo-300 {
+  --gradient-from-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.from-indigo-400 {
+  --gradient-from-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.from-indigo-500 {
+  --gradient-from-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.from-indigo-600 {
+  --gradient-from-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.from-indigo-700 {
+  --gradient-from-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.from-indigo-800 {
+  --gradient-from-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.from-indigo-900 {
+  --gradient-from-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.from-purple-100 {
+  --gradient-from-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.from-purple-200 {
+  --gradient-from-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.from-purple-300 {
+  --gradient-from-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.from-purple-400 {
+  --gradient-from-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.from-purple-500 {
+  --gradient-from-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.from-purple-600 {
+  --gradient-from-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.from-purple-700 {
+  --gradient-from-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.from-purple-800 {
+  --gradient-from-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.from-purple-900 {
+  --gradient-from-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.from-pink-100 {
+  --gradient-from-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.from-pink-200 {
+  --gradient-from-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.from-pink-300 {
+  --gradient-from-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.from-pink-400 {
+  --gradient-from-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.from-pink-500 {
+  --gradient-from-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.from-pink-600 {
+  --gradient-from-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.from-pink-700 {
+  --gradient-from-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.from-pink-800 {
+  --gradient-from-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.from-pink-900 {
+  --gradient-from-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.via-transparent {
+  --gradient-via-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.via-current {
+  --gradient-via-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.via-black {
+  --gradient-via-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.via-white {
+  --gradient-via-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.via-gray-100 {
+  --gradient-via-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.via-gray-200 {
+  --gradient-via-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.via-gray-300 {
+  --gradient-via-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.via-gray-400 {
+  --gradient-via-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.via-gray-500 {
+  --gradient-via-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.via-gray-600 {
+  --gradient-via-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.via-gray-700 {
+  --gradient-via-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.via-gray-800 {
+  --gradient-via-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.via-gray-900 {
+  --gradient-via-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.via-red-100 {
+  --gradient-via-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.via-red-200 {
+  --gradient-via-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.via-red-300 {
+  --gradient-via-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.via-red-400 {
+  --gradient-via-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.via-red-500 {
+  --gradient-via-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.via-red-600 {
+  --gradient-via-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.via-red-700 {
+  --gradient-via-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.via-red-800 {
+  --gradient-via-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.via-red-900 {
+  --gradient-via-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.via-orange-100 {
+  --gradient-via-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.via-orange-200 {
+  --gradient-via-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.via-orange-300 {
+  --gradient-via-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.via-orange-400 {
+  --gradient-via-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.via-orange-500 {
+  --gradient-via-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.via-orange-600 {
+  --gradient-via-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.via-orange-700 {
+  --gradient-via-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.via-orange-800 {
+  --gradient-via-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.via-orange-900 {
+  --gradient-via-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.via-yellow-100 {
+  --gradient-via-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.via-yellow-200 {
+  --gradient-via-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.via-yellow-300 {
+  --gradient-via-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.via-yellow-400 {
+  --gradient-via-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.via-yellow-500 {
+  --gradient-via-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.via-yellow-600 {
+  --gradient-via-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.via-yellow-700 {
+  --gradient-via-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.via-yellow-800 {
+  --gradient-via-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.via-yellow-900 {
+  --gradient-via-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.via-green-100 {
+  --gradient-via-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.via-green-200 {
+  --gradient-via-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.via-green-300 {
+  --gradient-via-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.via-green-400 {
+  --gradient-via-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.via-green-500 {
+  --gradient-via-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.via-green-600 {
+  --gradient-via-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.via-green-700 {
+  --gradient-via-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.via-green-800 {
+  --gradient-via-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.via-green-900 {
+  --gradient-via-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.via-teal-100 {
+  --gradient-via-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.via-teal-200 {
+  --gradient-via-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.via-teal-300 {
+  --gradient-via-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.via-teal-400 {
+  --gradient-via-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.via-teal-500 {
+  --gradient-via-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.via-teal-600 {
+  --gradient-via-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.via-teal-700 {
+  --gradient-via-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.via-teal-800 {
+  --gradient-via-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.via-teal-900 {
+  --gradient-via-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.via-blue-100 {
+  --gradient-via-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.via-blue-200 {
+  --gradient-via-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.via-blue-300 {
+  --gradient-via-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.via-blue-400 {
+  --gradient-via-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.via-blue-500 {
+  --gradient-via-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.via-blue-600 {
+  --gradient-via-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.via-blue-700 {
+  --gradient-via-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.via-blue-800 {
+  --gradient-via-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.via-blue-900 {
+  --gradient-via-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.via-indigo-100 {
+  --gradient-via-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.via-indigo-200 {
+  --gradient-via-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.via-indigo-300 {
+  --gradient-via-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.via-indigo-400 {
+  --gradient-via-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.via-indigo-500 {
+  --gradient-via-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.via-indigo-600 {
+  --gradient-via-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.via-indigo-700 {
+  --gradient-via-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.via-indigo-800 {
+  --gradient-via-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.via-indigo-900 {
+  --gradient-via-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.via-purple-100 {
+  --gradient-via-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.via-purple-200 {
+  --gradient-via-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.via-purple-300 {
+  --gradient-via-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.via-purple-400 {
+  --gradient-via-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.via-purple-500 {
+  --gradient-via-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.via-purple-600 {
+  --gradient-via-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.via-purple-700 {
+  --gradient-via-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.via-purple-800 {
+  --gradient-via-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.via-purple-900 {
+  --gradient-via-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.via-pink-100 {
+  --gradient-via-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.via-pink-200 {
+  --gradient-via-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.via-pink-300 {
+  --gradient-via-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.via-pink-400 {
+  --gradient-via-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.via-pink-500 {
+  --gradient-via-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.via-pink-600 {
+  --gradient-via-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.via-pink-700 {
+  --gradient-via-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.via-pink-800 {
+  --gradient-via-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.via-pink-900 {
+  --gradient-via-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.to-transparent {
+  --gradient-to-color: transparent;
+}
+
+.to-current {
+  --gradient-to-color: currentColor;
+}
+
+.to-black {
+  --gradient-to-color: #000;
+}
+
+.to-white {
+  --gradient-to-color: #fff;
+}
+
+.to-gray-100 {
+  --gradient-to-color: #f7fafc;
+}
+
+.to-gray-200 {
+  --gradient-to-color: #edf2f7;
+}
+
+.to-gray-300 {
+  --gradient-to-color: #e2e8f0;
+}
+
+.to-gray-400 {
+  --gradient-to-color: #cbd5e0;
+}
+
+.to-gray-500 {
+  --gradient-to-color: #a0aec0;
+}
+
+.to-gray-600 {
+  --gradient-to-color: #718096;
+}
+
+.to-gray-700 {
+  --gradient-to-color: #4a5568;
+}
+
+.to-gray-800 {
+  --gradient-to-color: #2d3748;
+}
+
+.to-gray-900 {
+  --gradient-to-color: #1a202c;
+}
+
+.to-red-100 {
+  --gradient-to-color: #fff5f5;
+}
+
+.to-red-200 {
+  --gradient-to-color: #fed7d7;
+}
+
+.to-red-300 {
+  --gradient-to-color: #feb2b2;
+}
+
+.to-red-400 {
+  --gradient-to-color: #fc8181;
+}
+
+.to-red-500 {
+  --gradient-to-color: #f56565;
+}
+
+.to-red-600 {
+  --gradient-to-color: #e53e3e;
+}
+
+.to-red-700 {
+  --gradient-to-color: #c53030;
+}
+
+.to-red-800 {
+  --gradient-to-color: #9b2c2c;
+}
+
+.to-red-900 {
+  --gradient-to-color: #742a2a;
+}
+
+.to-orange-100 {
+  --gradient-to-color: #fffaf0;
+}
+
+.to-orange-200 {
+  --gradient-to-color: #feebc8;
+}
+
+.to-orange-300 {
+  --gradient-to-color: #fbd38d;
+}
+
+.to-orange-400 {
+  --gradient-to-color: #f6ad55;
+}
+
+.to-orange-500 {
+  --gradient-to-color: #ed8936;
+}
+
+.to-orange-600 {
+  --gradient-to-color: #dd6b20;
+}
+
+.to-orange-700 {
+  --gradient-to-color: #c05621;
+}
+
+.to-orange-800 {
+  --gradient-to-color: #9c4221;
+}
+
+.to-orange-900 {
+  --gradient-to-color: #7b341e;
+}
+
+.to-yellow-100 {
+  --gradient-to-color: #fffff0;
+}
+
+.to-yellow-200 {
+  --gradient-to-color: #fefcbf;
+}
+
+.to-yellow-300 {
+  --gradient-to-color: #faf089;
+}
+
+.to-yellow-400 {
+  --gradient-to-color: #f6e05e;
+}
+
+.to-yellow-500 {
+  --gradient-to-color: #ecc94b;
+}
+
+.to-yellow-600 {
+  --gradient-to-color: #d69e2e;
+}
+
+.to-yellow-700 {
+  --gradient-to-color: #b7791f;
+}
+
+.to-yellow-800 {
+  --gradient-to-color: #975a16;
+}
+
+.to-yellow-900 {
+  --gradient-to-color: #744210;
+}
+
+.to-green-100 {
+  --gradient-to-color: #f0fff4;
+}
+
+.to-green-200 {
+  --gradient-to-color: #c6f6d5;
+}
+
+.to-green-300 {
+  --gradient-to-color: #9ae6b4;
+}
+
+.to-green-400 {
+  --gradient-to-color: #68d391;
+}
+
+.to-green-500 {
+  --gradient-to-color: #48bb78;
+}
+
+.to-green-600 {
+  --gradient-to-color: #38a169;
+}
+
+.to-green-700 {
+  --gradient-to-color: #2f855a;
+}
+
+.to-green-800 {
+  --gradient-to-color: #276749;
+}
+
+.to-green-900 {
+  --gradient-to-color: #22543d;
+}
+
+.to-teal-100 {
+  --gradient-to-color: #e6fffa;
+}
+
+.to-teal-200 {
+  --gradient-to-color: #b2f5ea;
+}
+
+.to-teal-300 {
+  --gradient-to-color: #81e6d9;
+}
+
+.to-teal-400 {
+  --gradient-to-color: #4fd1c5;
+}
+
+.to-teal-500 {
+  --gradient-to-color: #38b2ac;
+}
+
+.to-teal-600 {
+  --gradient-to-color: #319795;
+}
+
+.to-teal-700 {
+  --gradient-to-color: #2c7a7b;
+}
+
+.to-teal-800 {
+  --gradient-to-color: #285e61;
+}
+
+.to-teal-900 {
+  --gradient-to-color: #234e52;
+}
+
+.to-blue-100 {
+  --gradient-to-color: #ebf8ff;
+}
+
+.to-blue-200 {
+  --gradient-to-color: #bee3f8;
+}
+
+.to-blue-300 {
+  --gradient-to-color: #90cdf4;
+}
+
+.to-blue-400 {
+  --gradient-to-color: #63b3ed;
+}
+
+.to-blue-500 {
+  --gradient-to-color: #4299e1;
+}
+
+.to-blue-600 {
+  --gradient-to-color: #3182ce;
+}
+
+.to-blue-700 {
+  --gradient-to-color: #2b6cb0;
+}
+
+.to-blue-800 {
+  --gradient-to-color: #2c5282;
+}
+
+.to-blue-900 {
+  --gradient-to-color: #2a4365;
+}
+
+.to-indigo-100 {
+  --gradient-to-color: #ebf4ff;
+}
+
+.to-indigo-200 {
+  --gradient-to-color: #c3dafe;
+}
+
+.to-indigo-300 {
+  --gradient-to-color: #a3bffa;
+}
+
+.to-indigo-400 {
+  --gradient-to-color: #7f9cf5;
+}
+
+.to-indigo-500 {
+  --gradient-to-color: #667eea;
+}
+
+.to-indigo-600 {
+  --gradient-to-color: #5a67d8;
+}
+
+.to-indigo-700 {
+  --gradient-to-color: #4c51bf;
+}
+
+.to-indigo-800 {
+  --gradient-to-color: #434190;
+}
+
+.to-indigo-900 {
+  --gradient-to-color: #3c366b;
+}
+
+.to-purple-100 {
+  --gradient-to-color: #faf5ff;
+}
+
+.to-purple-200 {
+  --gradient-to-color: #e9d8fd;
+}
+
+.to-purple-300 {
+  --gradient-to-color: #d6bcfa;
+}
+
+.to-purple-400 {
+  --gradient-to-color: #b794f4;
+}
+
+.to-purple-500 {
+  --gradient-to-color: #9f7aea;
+}
+
+.to-purple-600 {
+  --gradient-to-color: #805ad5;
+}
+
+.to-purple-700 {
+  --gradient-to-color: #6b46c1;
+}
+
+.to-purple-800 {
+  --gradient-to-color: #553c9a;
+}
+
+.to-purple-900 {
+  --gradient-to-color: #44337a;
+}
+
+.to-pink-100 {
+  --gradient-to-color: #fff5f7;
+}
+
+.to-pink-200 {
+  --gradient-to-color: #fed7e2;
+}
+
+.to-pink-300 {
+  --gradient-to-color: #fbb6ce;
+}
+
+.to-pink-400 {
+  --gradient-to-color: #f687b3;
+}
+
+.to-pink-500 {
+  --gradient-to-color: #ed64a6;
+}
+
+.to-pink-600 {
+  --gradient-to-color: #d53f8c;
+}
+
+.to-pink-700 {
+  --gradient-to-color: #b83280;
+}
+
+.to-pink-800 {
+  --gradient-to-color: #97266d;
+}
+
+.to-pink-900 {
+  --gradient-to-color: #702459;
+}
+
+.hover\:from-transparent:hover {
+  --gradient-from-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:from-current:hover {
+  --gradient-from-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:from-black:hover {
+  --gradient-from-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:from-white:hover {
+  --gradient-from-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:from-gray-100:hover {
+  --gradient-from-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.hover\:from-gray-200:hover {
+  --gradient-from-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.hover\:from-gray-300:hover {
+  --gradient-from-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.hover\:from-gray-400:hover {
+  --gradient-from-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.hover\:from-gray-500:hover {
+  --gradient-from-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.hover\:from-gray-600:hover {
+  --gradient-from-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.hover\:from-gray-700:hover {
+  --gradient-from-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.hover\:from-gray-800:hover {
+  --gradient-from-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.hover\:from-gray-900:hover {
+  --gradient-from-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.hover\:from-red-100:hover {
+  --gradient-from-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.hover\:from-red-200:hover {
+  --gradient-from-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.hover\:from-red-300:hover {
+  --gradient-from-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.hover\:from-red-400:hover {
+  --gradient-from-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.hover\:from-red-500:hover {
+  --gradient-from-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.hover\:from-red-600:hover {
+  --gradient-from-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.hover\:from-red-700:hover {
+  --gradient-from-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.hover\:from-red-800:hover {
+  --gradient-from-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.hover\:from-red-900:hover {
+  --gradient-from-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.hover\:from-orange-100:hover {
+  --gradient-from-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.hover\:from-orange-200:hover {
+  --gradient-from-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.hover\:from-orange-300:hover {
+  --gradient-from-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.hover\:from-orange-400:hover {
+  --gradient-from-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.hover\:from-orange-500:hover {
+  --gradient-from-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.hover\:from-orange-600:hover {
+  --gradient-from-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.hover\:from-orange-700:hover {
+  --gradient-from-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.hover\:from-orange-800:hover {
+  --gradient-from-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.hover\:from-orange-900:hover {
+  --gradient-from-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.hover\:from-yellow-100:hover {
+  --gradient-from-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.hover\:from-yellow-200:hover {
+  --gradient-from-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.hover\:from-yellow-300:hover {
+  --gradient-from-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.hover\:from-yellow-400:hover {
+  --gradient-from-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.hover\:from-yellow-500:hover {
+  --gradient-from-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.hover\:from-yellow-600:hover {
+  --gradient-from-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.hover\:from-yellow-700:hover {
+  --gradient-from-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.hover\:from-yellow-800:hover {
+  --gradient-from-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.hover\:from-yellow-900:hover {
+  --gradient-from-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.hover\:from-green-100:hover {
+  --gradient-from-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.hover\:from-green-200:hover {
+  --gradient-from-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.hover\:from-green-300:hover {
+  --gradient-from-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.hover\:from-green-400:hover {
+  --gradient-from-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.hover\:from-green-500:hover {
+  --gradient-from-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.hover\:from-green-600:hover {
+  --gradient-from-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.hover\:from-green-700:hover {
+  --gradient-from-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.hover\:from-green-800:hover {
+  --gradient-from-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.hover\:from-green-900:hover {
+  --gradient-from-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.hover\:from-teal-100:hover {
+  --gradient-from-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.hover\:from-teal-200:hover {
+  --gradient-from-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.hover\:from-teal-300:hover {
+  --gradient-from-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.hover\:from-teal-400:hover {
+  --gradient-from-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.hover\:from-teal-500:hover {
+  --gradient-from-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.hover\:from-teal-600:hover {
+  --gradient-from-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.hover\:from-teal-700:hover {
+  --gradient-from-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.hover\:from-teal-800:hover {
+  --gradient-from-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.hover\:from-teal-900:hover {
+  --gradient-from-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.hover\:from-blue-100:hover {
+  --gradient-from-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.hover\:from-blue-200:hover {
+  --gradient-from-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.hover\:from-blue-300:hover {
+  --gradient-from-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.hover\:from-blue-400:hover {
+  --gradient-from-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.hover\:from-blue-500:hover {
+  --gradient-from-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.hover\:from-blue-600:hover {
+  --gradient-from-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.hover\:from-blue-700:hover {
+  --gradient-from-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.hover\:from-blue-800:hover {
+  --gradient-from-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.hover\:from-blue-900:hover {
+  --gradient-from-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.hover\:from-indigo-100:hover {
+  --gradient-from-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.hover\:from-indigo-200:hover {
+  --gradient-from-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.hover\:from-indigo-300:hover {
+  --gradient-from-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.hover\:from-indigo-400:hover {
+  --gradient-from-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.hover\:from-indigo-500:hover {
+  --gradient-from-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.hover\:from-indigo-600:hover {
+  --gradient-from-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.hover\:from-indigo-700:hover {
+  --gradient-from-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.hover\:from-indigo-800:hover {
+  --gradient-from-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.hover\:from-indigo-900:hover {
+  --gradient-from-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.hover\:from-purple-100:hover {
+  --gradient-from-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.hover\:from-purple-200:hover {
+  --gradient-from-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.hover\:from-purple-300:hover {
+  --gradient-from-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.hover\:from-purple-400:hover {
+  --gradient-from-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.hover\:from-purple-500:hover {
+  --gradient-from-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.hover\:from-purple-600:hover {
+  --gradient-from-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.hover\:from-purple-700:hover {
+  --gradient-from-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.hover\:from-purple-800:hover {
+  --gradient-from-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.hover\:from-purple-900:hover {
+  --gradient-from-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.hover\:from-pink-100:hover {
+  --gradient-from-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.hover\:from-pink-200:hover {
+  --gradient-from-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.hover\:from-pink-300:hover {
+  --gradient-from-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.hover\:from-pink-400:hover {
+  --gradient-from-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.hover\:from-pink-500:hover {
+  --gradient-from-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.hover\:from-pink-600:hover {
+  --gradient-from-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.hover\:from-pink-700:hover {
+  --gradient-from-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.hover\:from-pink-800:hover {
+  --gradient-from-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.hover\:from-pink-900:hover {
+  --gradient-from-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.hover\:via-transparent:hover {
+  --gradient-via-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:via-current:hover {
+  --gradient-via-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:via-black:hover {
+  --gradient-via-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:via-white:hover {
+  --gradient-via-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:via-gray-100:hover {
+  --gradient-via-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.hover\:via-gray-200:hover {
+  --gradient-via-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.hover\:via-gray-300:hover {
+  --gradient-via-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.hover\:via-gray-400:hover {
+  --gradient-via-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.hover\:via-gray-500:hover {
+  --gradient-via-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.hover\:via-gray-600:hover {
+  --gradient-via-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.hover\:via-gray-700:hover {
+  --gradient-via-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.hover\:via-gray-800:hover {
+  --gradient-via-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.hover\:via-gray-900:hover {
+  --gradient-via-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.hover\:via-red-100:hover {
+  --gradient-via-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.hover\:via-red-200:hover {
+  --gradient-via-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.hover\:via-red-300:hover {
+  --gradient-via-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.hover\:via-red-400:hover {
+  --gradient-via-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.hover\:via-red-500:hover {
+  --gradient-via-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.hover\:via-red-600:hover {
+  --gradient-via-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.hover\:via-red-700:hover {
+  --gradient-via-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.hover\:via-red-800:hover {
+  --gradient-via-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.hover\:via-red-900:hover {
+  --gradient-via-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.hover\:via-orange-100:hover {
+  --gradient-via-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.hover\:via-orange-200:hover {
+  --gradient-via-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.hover\:via-orange-300:hover {
+  --gradient-via-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.hover\:via-orange-400:hover {
+  --gradient-via-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.hover\:via-orange-500:hover {
+  --gradient-via-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.hover\:via-orange-600:hover {
+  --gradient-via-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.hover\:via-orange-700:hover {
+  --gradient-via-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.hover\:via-orange-800:hover {
+  --gradient-via-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.hover\:via-orange-900:hover {
+  --gradient-via-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.hover\:via-yellow-100:hover {
+  --gradient-via-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.hover\:via-yellow-200:hover {
+  --gradient-via-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.hover\:via-yellow-300:hover {
+  --gradient-via-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.hover\:via-yellow-400:hover {
+  --gradient-via-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.hover\:via-yellow-500:hover {
+  --gradient-via-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.hover\:via-yellow-600:hover {
+  --gradient-via-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.hover\:via-yellow-700:hover {
+  --gradient-via-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.hover\:via-yellow-800:hover {
+  --gradient-via-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.hover\:via-yellow-900:hover {
+  --gradient-via-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.hover\:via-green-100:hover {
+  --gradient-via-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.hover\:via-green-200:hover {
+  --gradient-via-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.hover\:via-green-300:hover {
+  --gradient-via-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.hover\:via-green-400:hover {
+  --gradient-via-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.hover\:via-green-500:hover {
+  --gradient-via-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.hover\:via-green-600:hover {
+  --gradient-via-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.hover\:via-green-700:hover {
+  --gradient-via-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.hover\:via-green-800:hover {
+  --gradient-via-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.hover\:via-green-900:hover {
+  --gradient-via-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.hover\:via-teal-100:hover {
+  --gradient-via-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.hover\:via-teal-200:hover {
+  --gradient-via-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.hover\:via-teal-300:hover {
+  --gradient-via-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.hover\:via-teal-400:hover {
+  --gradient-via-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.hover\:via-teal-500:hover {
+  --gradient-via-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.hover\:via-teal-600:hover {
+  --gradient-via-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.hover\:via-teal-700:hover {
+  --gradient-via-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.hover\:via-teal-800:hover {
+  --gradient-via-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.hover\:via-teal-900:hover {
+  --gradient-via-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.hover\:via-blue-100:hover {
+  --gradient-via-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.hover\:via-blue-200:hover {
+  --gradient-via-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.hover\:via-blue-300:hover {
+  --gradient-via-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.hover\:via-blue-400:hover {
+  --gradient-via-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.hover\:via-blue-500:hover {
+  --gradient-via-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.hover\:via-blue-600:hover {
+  --gradient-via-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.hover\:via-blue-700:hover {
+  --gradient-via-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.hover\:via-blue-800:hover {
+  --gradient-via-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.hover\:via-blue-900:hover {
+  --gradient-via-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.hover\:via-indigo-100:hover {
+  --gradient-via-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.hover\:via-indigo-200:hover {
+  --gradient-via-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.hover\:via-indigo-300:hover {
+  --gradient-via-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.hover\:via-indigo-400:hover {
+  --gradient-via-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.hover\:via-indigo-500:hover {
+  --gradient-via-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.hover\:via-indigo-600:hover {
+  --gradient-via-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.hover\:via-indigo-700:hover {
+  --gradient-via-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.hover\:via-indigo-800:hover {
+  --gradient-via-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.hover\:via-indigo-900:hover {
+  --gradient-via-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.hover\:via-purple-100:hover {
+  --gradient-via-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.hover\:via-purple-200:hover {
+  --gradient-via-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.hover\:via-purple-300:hover {
+  --gradient-via-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.hover\:via-purple-400:hover {
+  --gradient-via-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.hover\:via-purple-500:hover {
+  --gradient-via-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.hover\:via-purple-600:hover {
+  --gradient-via-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.hover\:via-purple-700:hover {
+  --gradient-via-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.hover\:via-purple-800:hover {
+  --gradient-via-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.hover\:via-purple-900:hover {
+  --gradient-via-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.hover\:via-pink-100:hover {
+  --gradient-via-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.hover\:via-pink-200:hover {
+  --gradient-via-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.hover\:via-pink-300:hover {
+  --gradient-via-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.hover\:via-pink-400:hover {
+  --gradient-via-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.hover\:via-pink-500:hover {
+  --gradient-via-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.hover\:via-pink-600:hover {
+  --gradient-via-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.hover\:via-pink-700:hover {
+  --gradient-via-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.hover\:via-pink-800:hover {
+  --gradient-via-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.hover\:via-pink-900:hover {
+  --gradient-via-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.hover\:to-transparent:hover {
+  --gradient-to-color: transparent;
+}
+
+.hover\:to-current:hover {
+  --gradient-to-color: currentColor;
+}
+
+.hover\:to-black:hover {
+  --gradient-to-color: #000;
+}
+
+.hover\:to-white:hover {
+  --gradient-to-color: #fff;
+}
+
+.hover\:to-gray-100:hover {
+  --gradient-to-color: #f7fafc;
+}
+
+.hover\:to-gray-200:hover {
+  --gradient-to-color: #edf2f7;
+}
+
+.hover\:to-gray-300:hover {
+  --gradient-to-color: #e2e8f0;
+}
+
+.hover\:to-gray-400:hover {
+  --gradient-to-color: #cbd5e0;
+}
+
+.hover\:to-gray-500:hover {
+  --gradient-to-color: #a0aec0;
+}
+
+.hover\:to-gray-600:hover {
+  --gradient-to-color: #718096;
+}
+
+.hover\:to-gray-700:hover {
+  --gradient-to-color: #4a5568;
+}
+
+.hover\:to-gray-800:hover {
+  --gradient-to-color: #2d3748;
+}
+
+.hover\:to-gray-900:hover {
+  --gradient-to-color: #1a202c;
+}
+
+.hover\:to-red-100:hover {
+  --gradient-to-color: #fff5f5;
+}
+
+.hover\:to-red-200:hover {
+  --gradient-to-color: #fed7d7;
+}
+
+.hover\:to-red-300:hover {
+  --gradient-to-color: #feb2b2;
+}
+
+.hover\:to-red-400:hover {
+  --gradient-to-color: #fc8181;
+}
+
+.hover\:to-red-500:hover {
+  --gradient-to-color: #f56565;
+}
+
+.hover\:to-red-600:hover {
+  --gradient-to-color: #e53e3e;
+}
+
+.hover\:to-red-700:hover {
+  --gradient-to-color: #c53030;
+}
+
+.hover\:to-red-800:hover {
+  --gradient-to-color: #9b2c2c;
+}
+
+.hover\:to-red-900:hover {
+  --gradient-to-color: #742a2a;
+}
+
+.hover\:to-orange-100:hover {
+  --gradient-to-color: #fffaf0;
+}
+
+.hover\:to-orange-200:hover {
+  --gradient-to-color: #feebc8;
+}
+
+.hover\:to-orange-300:hover {
+  --gradient-to-color: #fbd38d;
+}
+
+.hover\:to-orange-400:hover {
+  --gradient-to-color: #f6ad55;
+}
+
+.hover\:to-orange-500:hover {
+  --gradient-to-color: #ed8936;
+}
+
+.hover\:to-orange-600:hover {
+  --gradient-to-color: #dd6b20;
+}
+
+.hover\:to-orange-700:hover {
+  --gradient-to-color: #c05621;
+}
+
+.hover\:to-orange-800:hover {
+  --gradient-to-color: #9c4221;
+}
+
+.hover\:to-orange-900:hover {
+  --gradient-to-color: #7b341e;
+}
+
+.hover\:to-yellow-100:hover {
+  --gradient-to-color: #fffff0;
+}
+
+.hover\:to-yellow-200:hover {
+  --gradient-to-color: #fefcbf;
+}
+
+.hover\:to-yellow-300:hover {
+  --gradient-to-color: #faf089;
+}
+
+.hover\:to-yellow-400:hover {
+  --gradient-to-color: #f6e05e;
+}
+
+.hover\:to-yellow-500:hover {
+  --gradient-to-color: #ecc94b;
+}
+
+.hover\:to-yellow-600:hover {
+  --gradient-to-color: #d69e2e;
+}
+
+.hover\:to-yellow-700:hover {
+  --gradient-to-color: #b7791f;
+}
+
+.hover\:to-yellow-800:hover {
+  --gradient-to-color: #975a16;
+}
+
+.hover\:to-yellow-900:hover {
+  --gradient-to-color: #744210;
+}
+
+.hover\:to-green-100:hover {
+  --gradient-to-color: #f0fff4;
+}
+
+.hover\:to-green-200:hover {
+  --gradient-to-color: #c6f6d5;
+}
+
+.hover\:to-green-300:hover {
+  --gradient-to-color: #9ae6b4;
+}
+
+.hover\:to-green-400:hover {
+  --gradient-to-color: #68d391;
+}
+
+.hover\:to-green-500:hover {
+  --gradient-to-color: #48bb78;
+}
+
+.hover\:to-green-600:hover {
+  --gradient-to-color: #38a169;
+}
+
+.hover\:to-green-700:hover {
+  --gradient-to-color: #2f855a;
+}
+
+.hover\:to-green-800:hover {
+  --gradient-to-color: #276749;
+}
+
+.hover\:to-green-900:hover {
+  --gradient-to-color: #22543d;
+}
+
+.hover\:to-teal-100:hover {
+  --gradient-to-color: #e6fffa;
+}
+
+.hover\:to-teal-200:hover {
+  --gradient-to-color: #b2f5ea;
+}
+
+.hover\:to-teal-300:hover {
+  --gradient-to-color: #81e6d9;
+}
+
+.hover\:to-teal-400:hover {
+  --gradient-to-color: #4fd1c5;
+}
+
+.hover\:to-teal-500:hover {
+  --gradient-to-color: #38b2ac;
+}
+
+.hover\:to-teal-600:hover {
+  --gradient-to-color: #319795;
+}
+
+.hover\:to-teal-700:hover {
+  --gradient-to-color: #2c7a7b;
+}
+
+.hover\:to-teal-800:hover {
+  --gradient-to-color: #285e61;
+}
+
+.hover\:to-teal-900:hover {
+  --gradient-to-color: #234e52;
+}
+
+.hover\:to-blue-100:hover {
+  --gradient-to-color: #ebf8ff;
+}
+
+.hover\:to-blue-200:hover {
+  --gradient-to-color: #bee3f8;
+}
+
+.hover\:to-blue-300:hover {
+  --gradient-to-color: #90cdf4;
+}
+
+.hover\:to-blue-400:hover {
+  --gradient-to-color: #63b3ed;
+}
+
+.hover\:to-blue-500:hover {
+  --gradient-to-color: #4299e1;
+}
+
+.hover\:to-blue-600:hover {
+  --gradient-to-color: #3182ce;
+}
+
+.hover\:to-blue-700:hover {
+  --gradient-to-color: #2b6cb0;
+}
+
+.hover\:to-blue-800:hover {
+  --gradient-to-color: #2c5282;
+}
+
+.hover\:to-blue-900:hover {
+  --gradient-to-color: #2a4365;
+}
+
+.hover\:to-indigo-100:hover {
+  --gradient-to-color: #ebf4ff;
+}
+
+.hover\:to-indigo-200:hover {
+  --gradient-to-color: #c3dafe;
+}
+
+.hover\:to-indigo-300:hover {
+  --gradient-to-color: #a3bffa;
+}
+
+.hover\:to-indigo-400:hover {
+  --gradient-to-color: #7f9cf5;
+}
+
+.hover\:to-indigo-500:hover {
+  --gradient-to-color: #667eea;
+}
+
+.hover\:to-indigo-600:hover {
+  --gradient-to-color: #5a67d8;
+}
+
+.hover\:to-indigo-700:hover {
+  --gradient-to-color: #4c51bf;
+}
+
+.hover\:to-indigo-800:hover {
+  --gradient-to-color: #434190;
+}
+
+.hover\:to-indigo-900:hover {
+  --gradient-to-color: #3c366b;
+}
+
+.hover\:to-purple-100:hover {
+  --gradient-to-color: #faf5ff;
+}
+
+.hover\:to-purple-200:hover {
+  --gradient-to-color: #e9d8fd;
+}
+
+.hover\:to-purple-300:hover {
+  --gradient-to-color: #d6bcfa;
+}
+
+.hover\:to-purple-400:hover {
+  --gradient-to-color: #b794f4;
+}
+
+.hover\:to-purple-500:hover {
+  --gradient-to-color: #9f7aea;
+}
+
+.hover\:to-purple-600:hover {
+  --gradient-to-color: #805ad5;
+}
+
+.hover\:to-purple-700:hover {
+  --gradient-to-color: #6b46c1;
+}
+
+.hover\:to-purple-800:hover {
+  --gradient-to-color: #553c9a;
+}
+
+.hover\:to-purple-900:hover {
+  --gradient-to-color: #44337a;
+}
+
+.hover\:to-pink-100:hover {
+  --gradient-to-color: #fff5f7;
+}
+
+.hover\:to-pink-200:hover {
+  --gradient-to-color: #fed7e2;
+}
+
+.hover\:to-pink-300:hover {
+  --gradient-to-color: #fbb6ce;
+}
+
+.hover\:to-pink-400:hover {
+  --gradient-to-color: #f687b3;
+}
+
+.hover\:to-pink-500:hover {
+  --gradient-to-color: #ed64a6;
+}
+
+.hover\:to-pink-600:hover {
+  --gradient-to-color: #d53f8c;
+}
+
+.hover\:to-pink-700:hover {
+  --gradient-to-color: #b83280;
+}
+
+.hover\:to-pink-800:hover {
+  --gradient-to-color: #97266d;
+}
+
+.hover\:to-pink-900:hover {
+  --gradient-to-color: #702459;
+}
+
+.focus\:from-transparent:focus {
+  --gradient-from-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:from-current:focus {
+  --gradient-from-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:from-black:focus {
+  --gradient-from-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:from-white:focus {
+  --gradient-from-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:from-gray-100:focus {
+  --gradient-from-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.focus\:from-gray-200:focus {
+  --gradient-from-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.focus\:from-gray-300:focus {
+  --gradient-from-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.focus\:from-gray-400:focus {
+  --gradient-from-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.focus\:from-gray-500:focus {
+  --gradient-from-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.focus\:from-gray-600:focus {
+  --gradient-from-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.focus\:from-gray-700:focus {
+  --gradient-from-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.focus\:from-gray-800:focus {
+  --gradient-from-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.focus\:from-gray-900:focus {
+  --gradient-from-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.focus\:from-red-100:focus {
+  --gradient-from-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.focus\:from-red-200:focus {
+  --gradient-from-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.focus\:from-red-300:focus {
+  --gradient-from-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.focus\:from-red-400:focus {
+  --gradient-from-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.focus\:from-red-500:focus {
+  --gradient-from-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.focus\:from-red-600:focus {
+  --gradient-from-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.focus\:from-red-700:focus {
+  --gradient-from-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.focus\:from-red-800:focus {
+  --gradient-from-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.focus\:from-red-900:focus {
+  --gradient-from-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.focus\:from-orange-100:focus {
+  --gradient-from-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.focus\:from-orange-200:focus {
+  --gradient-from-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.focus\:from-orange-300:focus {
+  --gradient-from-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.focus\:from-orange-400:focus {
+  --gradient-from-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.focus\:from-orange-500:focus {
+  --gradient-from-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.focus\:from-orange-600:focus {
+  --gradient-from-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.focus\:from-orange-700:focus {
+  --gradient-from-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.focus\:from-orange-800:focus {
+  --gradient-from-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.focus\:from-orange-900:focus {
+  --gradient-from-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.focus\:from-yellow-100:focus {
+  --gradient-from-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.focus\:from-yellow-200:focus {
+  --gradient-from-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.focus\:from-yellow-300:focus {
+  --gradient-from-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.focus\:from-yellow-400:focus {
+  --gradient-from-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.focus\:from-yellow-500:focus {
+  --gradient-from-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.focus\:from-yellow-600:focus {
+  --gradient-from-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.focus\:from-yellow-700:focus {
+  --gradient-from-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.focus\:from-yellow-800:focus {
+  --gradient-from-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.focus\:from-yellow-900:focus {
+  --gradient-from-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.focus\:from-green-100:focus {
+  --gradient-from-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.focus\:from-green-200:focus {
+  --gradient-from-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.focus\:from-green-300:focus {
+  --gradient-from-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.focus\:from-green-400:focus {
+  --gradient-from-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.focus\:from-green-500:focus {
+  --gradient-from-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.focus\:from-green-600:focus {
+  --gradient-from-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.focus\:from-green-700:focus {
+  --gradient-from-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.focus\:from-green-800:focus {
+  --gradient-from-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.focus\:from-green-900:focus {
+  --gradient-from-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.focus\:from-teal-100:focus {
+  --gradient-from-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.focus\:from-teal-200:focus {
+  --gradient-from-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.focus\:from-teal-300:focus {
+  --gradient-from-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.focus\:from-teal-400:focus {
+  --gradient-from-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.focus\:from-teal-500:focus {
+  --gradient-from-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.focus\:from-teal-600:focus {
+  --gradient-from-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.focus\:from-teal-700:focus {
+  --gradient-from-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.focus\:from-teal-800:focus {
+  --gradient-from-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.focus\:from-teal-900:focus {
+  --gradient-from-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.focus\:from-blue-100:focus {
+  --gradient-from-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.focus\:from-blue-200:focus {
+  --gradient-from-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.focus\:from-blue-300:focus {
+  --gradient-from-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.focus\:from-blue-400:focus {
+  --gradient-from-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.focus\:from-blue-500:focus {
+  --gradient-from-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.focus\:from-blue-600:focus {
+  --gradient-from-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.focus\:from-blue-700:focus {
+  --gradient-from-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.focus\:from-blue-800:focus {
+  --gradient-from-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.focus\:from-blue-900:focus {
+  --gradient-from-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.focus\:from-indigo-100:focus {
+  --gradient-from-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.focus\:from-indigo-200:focus {
+  --gradient-from-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.focus\:from-indigo-300:focus {
+  --gradient-from-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.focus\:from-indigo-400:focus {
+  --gradient-from-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.focus\:from-indigo-500:focus {
+  --gradient-from-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.focus\:from-indigo-600:focus {
+  --gradient-from-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.focus\:from-indigo-700:focus {
+  --gradient-from-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.focus\:from-indigo-800:focus {
+  --gradient-from-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.focus\:from-indigo-900:focus {
+  --gradient-from-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.focus\:from-purple-100:focus {
+  --gradient-from-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.focus\:from-purple-200:focus {
+  --gradient-from-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.focus\:from-purple-300:focus {
+  --gradient-from-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.focus\:from-purple-400:focus {
+  --gradient-from-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.focus\:from-purple-500:focus {
+  --gradient-from-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.focus\:from-purple-600:focus {
+  --gradient-from-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.focus\:from-purple-700:focus {
+  --gradient-from-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.focus\:from-purple-800:focus {
+  --gradient-from-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.focus\:from-purple-900:focus {
+  --gradient-from-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.focus\:from-pink-100:focus {
+  --gradient-from-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.focus\:from-pink-200:focus {
+  --gradient-from-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.focus\:from-pink-300:focus {
+  --gradient-from-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.focus\:from-pink-400:focus {
+  --gradient-from-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.focus\:from-pink-500:focus {
+  --gradient-from-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.focus\:from-pink-600:focus {
+  --gradient-from-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.focus\:from-pink-700:focus {
+  --gradient-from-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.focus\:from-pink-800:focus {
+  --gradient-from-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.focus\:from-pink-900:focus {
+  --gradient-from-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.focus\:via-transparent:focus {
+  --gradient-via-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:via-current:focus {
+  --gradient-via-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:via-black:focus {
+  --gradient-via-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:via-white:focus {
+  --gradient-via-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:via-gray-100:focus {
+  --gradient-via-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.focus\:via-gray-200:focus {
+  --gradient-via-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.focus\:via-gray-300:focus {
+  --gradient-via-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.focus\:via-gray-400:focus {
+  --gradient-via-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.focus\:via-gray-500:focus {
+  --gradient-via-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.focus\:via-gray-600:focus {
+  --gradient-via-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.focus\:via-gray-700:focus {
+  --gradient-via-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.focus\:via-gray-800:focus {
+  --gradient-via-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.focus\:via-gray-900:focus {
+  --gradient-via-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.focus\:via-red-100:focus {
+  --gradient-via-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.focus\:via-red-200:focus {
+  --gradient-via-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.focus\:via-red-300:focus {
+  --gradient-via-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.focus\:via-red-400:focus {
+  --gradient-via-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.focus\:via-red-500:focus {
+  --gradient-via-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.focus\:via-red-600:focus {
+  --gradient-via-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.focus\:via-red-700:focus {
+  --gradient-via-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.focus\:via-red-800:focus {
+  --gradient-via-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.focus\:via-red-900:focus {
+  --gradient-via-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.focus\:via-orange-100:focus {
+  --gradient-via-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.focus\:via-orange-200:focus {
+  --gradient-via-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.focus\:via-orange-300:focus {
+  --gradient-via-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.focus\:via-orange-400:focus {
+  --gradient-via-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.focus\:via-orange-500:focus {
+  --gradient-via-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.focus\:via-orange-600:focus {
+  --gradient-via-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.focus\:via-orange-700:focus {
+  --gradient-via-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.focus\:via-orange-800:focus {
+  --gradient-via-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.focus\:via-orange-900:focus {
+  --gradient-via-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.focus\:via-yellow-100:focus {
+  --gradient-via-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.focus\:via-yellow-200:focus {
+  --gradient-via-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.focus\:via-yellow-300:focus {
+  --gradient-via-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.focus\:via-yellow-400:focus {
+  --gradient-via-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.focus\:via-yellow-500:focus {
+  --gradient-via-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.focus\:via-yellow-600:focus {
+  --gradient-via-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.focus\:via-yellow-700:focus {
+  --gradient-via-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.focus\:via-yellow-800:focus {
+  --gradient-via-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.focus\:via-yellow-900:focus {
+  --gradient-via-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.focus\:via-green-100:focus {
+  --gradient-via-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.focus\:via-green-200:focus {
+  --gradient-via-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.focus\:via-green-300:focus {
+  --gradient-via-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.focus\:via-green-400:focus {
+  --gradient-via-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.focus\:via-green-500:focus {
+  --gradient-via-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.focus\:via-green-600:focus {
+  --gradient-via-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.focus\:via-green-700:focus {
+  --gradient-via-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.focus\:via-green-800:focus {
+  --gradient-via-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.focus\:via-green-900:focus {
+  --gradient-via-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.focus\:via-teal-100:focus {
+  --gradient-via-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.focus\:via-teal-200:focus {
+  --gradient-via-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.focus\:via-teal-300:focus {
+  --gradient-via-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.focus\:via-teal-400:focus {
+  --gradient-via-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.focus\:via-teal-500:focus {
+  --gradient-via-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.focus\:via-teal-600:focus {
+  --gradient-via-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.focus\:via-teal-700:focus {
+  --gradient-via-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.focus\:via-teal-800:focus {
+  --gradient-via-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.focus\:via-teal-900:focus {
+  --gradient-via-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.focus\:via-blue-100:focus {
+  --gradient-via-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.focus\:via-blue-200:focus {
+  --gradient-via-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.focus\:via-blue-300:focus {
+  --gradient-via-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.focus\:via-blue-400:focus {
+  --gradient-via-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.focus\:via-blue-500:focus {
+  --gradient-via-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.focus\:via-blue-600:focus {
+  --gradient-via-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.focus\:via-blue-700:focus {
+  --gradient-via-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.focus\:via-blue-800:focus {
+  --gradient-via-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.focus\:via-blue-900:focus {
+  --gradient-via-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.focus\:via-indigo-100:focus {
+  --gradient-via-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.focus\:via-indigo-200:focus {
+  --gradient-via-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.focus\:via-indigo-300:focus {
+  --gradient-via-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.focus\:via-indigo-400:focus {
+  --gradient-via-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.focus\:via-indigo-500:focus {
+  --gradient-via-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.focus\:via-indigo-600:focus {
+  --gradient-via-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.focus\:via-indigo-700:focus {
+  --gradient-via-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.focus\:via-indigo-800:focus {
+  --gradient-via-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.focus\:via-indigo-900:focus {
+  --gradient-via-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.focus\:via-purple-100:focus {
+  --gradient-via-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.focus\:via-purple-200:focus {
+  --gradient-via-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.focus\:via-purple-300:focus {
+  --gradient-via-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.focus\:via-purple-400:focus {
+  --gradient-via-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.focus\:via-purple-500:focus {
+  --gradient-via-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.focus\:via-purple-600:focus {
+  --gradient-via-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.focus\:via-purple-700:focus {
+  --gradient-via-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.focus\:via-purple-800:focus {
+  --gradient-via-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.focus\:via-purple-900:focus {
+  --gradient-via-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.focus\:via-pink-100:focus {
+  --gradient-via-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.focus\:via-pink-200:focus {
+  --gradient-via-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.focus\:via-pink-300:focus {
+  --gradient-via-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.focus\:via-pink-400:focus {
+  --gradient-via-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.focus\:via-pink-500:focus {
+  --gradient-via-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.focus\:via-pink-600:focus {
+  --gradient-via-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.focus\:via-pink-700:focus {
+  --gradient-via-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.focus\:via-pink-800:focus {
+  --gradient-via-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.focus\:via-pink-900:focus {
+  --gradient-via-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.focus\:to-transparent:focus {
+  --gradient-to-color: transparent;
+}
+
+.focus\:to-current:focus {
+  --gradient-to-color: currentColor;
+}
+
+.focus\:to-black:focus {
+  --gradient-to-color: #000;
+}
+
+.focus\:to-white:focus {
+  --gradient-to-color: #fff;
+}
+
+.focus\:to-gray-100:focus {
+  --gradient-to-color: #f7fafc;
+}
+
+.focus\:to-gray-200:focus {
+  --gradient-to-color: #edf2f7;
+}
+
+.focus\:to-gray-300:focus {
+  --gradient-to-color: #e2e8f0;
+}
+
+.focus\:to-gray-400:focus {
+  --gradient-to-color: #cbd5e0;
+}
+
+.focus\:to-gray-500:focus {
+  --gradient-to-color: #a0aec0;
+}
+
+.focus\:to-gray-600:focus {
+  --gradient-to-color: #718096;
+}
+
+.focus\:to-gray-700:focus {
+  --gradient-to-color: #4a5568;
+}
+
+.focus\:to-gray-800:focus {
+  --gradient-to-color: #2d3748;
+}
+
+.focus\:to-gray-900:focus {
+  --gradient-to-color: #1a202c;
+}
+
+.focus\:to-red-100:focus {
+  --gradient-to-color: #fff5f5;
+}
+
+.focus\:to-red-200:focus {
+  --gradient-to-color: #fed7d7;
+}
+
+.focus\:to-red-300:focus {
+  --gradient-to-color: #feb2b2;
+}
+
+.focus\:to-red-400:focus {
+  --gradient-to-color: #fc8181;
+}
+
+.focus\:to-red-500:focus {
+  --gradient-to-color: #f56565;
+}
+
+.focus\:to-red-600:focus {
+  --gradient-to-color: #e53e3e;
+}
+
+.focus\:to-red-700:focus {
+  --gradient-to-color: #c53030;
+}
+
+.focus\:to-red-800:focus {
+  --gradient-to-color: #9b2c2c;
+}
+
+.focus\:to-red-900:focus {
+  --gradient-to-color: #742a2a;
+}
+
+.focus\:to-orange-100:focus {
+  --gradient-to-color: #fffaf0;
+}
+
+.focus\:to-orange-200:focus {
+  --gradient-to-color: #feebc8;
+}
+
+.focus\:to-orange-300:focus {
+  --gradient-to-color: #fbd38d;
+}
+
+.focus\:to-orange-400:focus {
+  --gradient-to-color: #f6ad55;
+}
+
+.focus\:to-orange-500:focus {
+  --gradient-to-color: #ed8936;
+}
+
+.focus\:to-orange-600:focus {
+  --gradient-to-color: #dd6b20;
+}
+
+.focus\:to-orange-700:focus {
+  --gradient-to-color: #c05621;
+}
+
+.focus\:to-orange-800:focus {
+  --gradient-to-color: #9c4221;
+}
+
+.focus\:to-orange-900:focus {
+  --gradient-to-color: #7b341e;
+}
+
+.focus\:to-yellow-100:focus {
+  --gradient-to-color: #fffff0;
+}
+
+.focus\:to-yellow-200:focus {
+  --gradient-to-color: #fefcbf;
+}
+
+.focus\:to-yellow-300:focus {
+  --gradient-to-color: #faf089;
+}
+
+.focus\:to-yellow-400:focus {
+  --gradient-to-color: #f6e05e;
+}
+
+.focus\:to-yellow-500:focus {
+  --gradient-to-color: #ecc94b;
+}
+
+.focus\:to-yellow-600:focus {
+  --gradient-to-color: #d69e2e;
+}
+
+.focus\:to-yellow-700:focus {
+  --gradient-to-color: #b7791f;
+}
+
+.focus\:to-yellow-800:focus {
+  --gradient-to-color: #975a16;
+}
+
+.focus\:to-yellow-900:focus {
+  --gradient-to-color: #744210;
+}
+
+.focus\:to-green-100:focus {
+  --gradient-to-color: #f0fff4;
+}
+
+.focus\:to-green-200:focus {
+  --gradient-to-color: #c6f6d5;
+}
+
+.focus\:to-green-300:focus {
+  --gradient-to-color: #9ae6b4;
+}
+
+.focus\:to-green-400:focus {
+  --gradient-to-color: #68d391;
+}
+
+.focus\:to-green-500:focus {
+  --gradient-to-color: #48bb78;
+}
+
+.focus\:to-green-600:focus {
+  --gradient-to-color: #38a169;
+}
+
+.focus\:to-green-700:focus {
+  --gradient-to-color: #2f855a;
+}
+
+.focus\:to-green-800:focus {
+  --gradient-to-color: #276749;
+}
+
+.focus\:to-green-900:focus {
+  --gradient-to-color: #22543d;
+}
+
+.focus\:to-teal-100:focus {
+  --gradient-to-color: #e6fffa;
+}
+
+.focus\:to-teal-200:focus {
+  --gradient-to-color: #b2f5ea;
+}
+
+.focus\:to-teal-300:focus {
+  --gradient-to-color: #81e6d9;
+}
+
+.focus\:to-teal-400:focus {
+  --gradient-to-color: #4fd1c5;
+}
+
+.focus\:to-teal-500:focus {
+  --gradient-to-color: #38b2ac;
+}
+
+.focus\:to-teal-600:focus {
+  --gradient-to-color: #319795;
+}
+
+.focus\:to-teal-700:focus {
+  --gradient-to-color: #2c7a7b;
+}
+
+.focus\:to-teal-800:focus {
+  --gradient-to-color: #285e61;
+}
+
+.focus\:to-teal-900:focus {
+  --gradient-to-color: #234e52;
+}
+
+.focus\:to-blue-100:focus {
+  --gradient-to-color: #ebf8ff;
+}
+
+.focus\:to-blue-200:focus {
+  --gradient-to-color: #bee3f8;
+}
+
+.focus\:to-blue-300:focus {
+  --gradient-to-color: #90cdf4;
+}
+
+.focus\:to-blue-400:focus {
+  --gradient-to-color: #63b3ed;
+}
+
+.focus\:to-blue-500:focus {
+  --gradient-to-color: #4299e1;
+}
+
+.focus\:to-blue-600:focus {
+  --gradient-to-color: #3182ce;
+}
+
+.focus\:to-blue-700:focus {
+  --gradient-to-color: #2b6cb0;
+}
+
+.focus\:to-blue-800:focus {
+  --gradient-to-color: #2c5282;
+}
+
+.focus\:to-blue-900:focus {
+  --gradient-to-color: #2a4365;
+}
+
+.focus\:to-indigo-100:focus {
+  --gradient-to-color: #ebf4ff;
+}
+
+.focus\:to-indigo-200:focus {
+  --gradient-to-color: #c3dafe;
+}
+
+.focus\:to-indigo-300:focus {
+  --gradient-to-color: #a3bffa;
+}
+
+.focus\:to-indigo-400:focus {
+  --gradient-to-color: #7f9cf5;
+}
+
+.focus\:to-indigo-500:focus {
+  --gradient-to-color: #667eea;
+}
+
+.focus\:to-indigo-600:focus {
+  --gradient-to-color: #5a67d8;
+}
+
+.focus\:to-indigo-700:focus {
+  --gradient-to-color: #4c51bf;
+}
+
+.focus\:to-indigo-800:focus {
+  --gradient-to-color: #434190;
+}
+
+.focus\:to-indigo-900:focus {
+  --gradient-to-color: #3c366b;
+}
+
+.focus\:to-purple-100:focus {
+  --gradient-to-color: #faf5ff;
+}
+
+.focus\:to-purple-200:focus {
+  --gradient-to-color: #e9d8fd;
+}
+
+.focus\:to-purple-300:focus {
+  --gradient-to-color: #d6bcfa;
+}
+
+.focus\:to-purple-400:focus {
+  --gradient-to-color: #b794f4;
+}
+
+.focus\:to-purple-500:focus {
+  --gradient-to-color: #9f7aea;
+}
+
+.focus\:to-purple-600:focus {
+  --gradient-to-color: #805ad5;
+}
+
+.focus\:to-purple-700:focus {
+  --gradient-to-color: #6b46c1;
+}
+
+.focus\:to-purple-800:focus {
+  --gradient-to-color: #553c9a;
+}
+
+.focus\:to-purple-900:focus {
+  --gradient-to-color: #44337a;
+}
+
+.focus\:to-pink-100:focus {
+  --gradient-to-color: #fff5f7;
+}
+
+.focus\:to-pink-200:focus {
+  --gradient-to-color: #fed7e2;
+}
+
+.focus\:to-pink-300:focus {
+  --gradient-to-color: #fbb6ce;
+}
+
+.focus\:to-pink-400:focus {
+  --gradient-to-color: #f687b3;
+}
+
+.focus\:to-pink-500:focus {
+  --gradient-to-color: #ed64a6;
+}
+
+.focus\:to-pink-600:focus {
+  --gradient-to-color: #d53f8c;
+}
+
+.focus\:to-pink-700:focus {
+  --gradient-to-color: #b83280;
+}
+
+.focus\:to-pink-800:focus {
+  --gradient-to-color: #97266d;
+}
+
+.focus\:to-pink-900:focus {
+  --gradient-to-color: #702459;
+}
+
+.bg-opacity-0 {
+  --bg-opacity: 0;
+}
+
+.bg-opacity-25 {
+  --bg-opacity: 0.25;
+}
+
+.bg-opacity-50 {
+  --bg-opacity: 0.5;
+}
+
+.bg-opacity-75 {
+  --bg-opacity: 0.75;
+}
+
+.bg-opacity-100 {
+  --bg-opacity: 1;
+}
+
+.hover\:bg-opacity-0:hover {
+  --bg-opacity: 0;
+}
+
+.hover\:bg-opacity-25:hover {
+  --bg-opacity: 0.25;
+}
+
+.hover\:bg-opacity-50:hover {
+  --bg-opacity: 0.5;
+}
+
+.hover\:bg-opacity-75:hover {
+  --bg-opacity: 0.75;
+}
+
+.hover\:bg-opacity-100:hover {
+  --bg-opacity: 1;
+}
+
+.focus\:bg-opacity-0:focus {
+  --bg-opacity: 0;
+}
+
+.focus\:bg-opacity-25:focus {
+  --bg-opacity: 0.25;
+}
+
+.focus\:bg-opacity-50:focus {
+  --bg-opacity: 0.5;
+}
+
+.focus\:bg-opacity-75:focus {
+  --bg-opacity: 0.75;
+}
+
+.focus\:bg-opacity-100:focus {
+  --bg-opacity: 1;
+}
+
+.bg-bottom {
+  background-position: bottom;
+}
+
+.bg-center {
+  background-position: center;
+}
+
+.bg-left {
+  background-position: left;
+}
+
+.bg-left-bottom {
+  background-position: left bottom;
+}
+
+.bg-left-top {
+  background-position: left top;
+}
+
+.bg-right {
+  background-position: right;
+}
+
+.bg-right-bottom {
+  background-position: right bottom;
+}
+
+.bg-right-top {
+  background-position: right top;
+}
+
+.bg-top {
+  background-position: top;
+}
+
+.bg-repeat {
+  background-repeat: repeat;
+}
+
+.bg-no-repeat {
+  background-repeat: no-repeat;
+}
+
+.bg-repeat-x {
+  background-repeat: repeat-x;
+}
+
+.bg-repeat-y {
+  background-repeat: repeat-y;
+}
+
+.bg-repeat-round {
+  background-repeat: round;
+}
+
+.bg-repeat-space {
+  background-repeat: space;
+}
+
+.bg-auto {
+  background-size: auto;
+}
+
+.bg-cover {
+  background-size: cover;
+}
+
+.bg-contain {
+  background-size: contain;
+}
+
+.border-collapse {
+  border-collapse: collapse;
+}
+
+.border-separate {
+  border-collapse: separate;
+}
+
+.border-transparent {
+  border-color: transparent;
+}
+
+.border-current {
+  border-color: currentColor;
+}
+
+.border-black {
+  --border-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--border-opacity));
+}
+
+.border-white {
+  --border-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--border-opacity));
+}
+
+.border-gray-100 {
+  --border-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--border-opacity));
+}
+
+.border-gray-200 {
+  --border-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--border-opacity));
+}
+
+.border-gray-300 {
+  --border-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--border-opacity));
+}
+
+.border-gray-400 {
+  --border-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--border-opacity));
+}
+
+.border-gray-500 {
+  --border-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--border-opacity));
+}
+
+.border-gray-600 {
+  --border-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--border-opacity));
+}
+
+.border-gray-700 {
+  --border-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--border-opacity));
+}
+
+.border-gray-800 {
+  --border-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--border-opacity));
+}
+
+.border-gray-900 {
+  --border-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--border-opacity));
+}
+
+.border-red-100 {
+  --border-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--border-opacity));
+}
+
+.border-red-200 {
+  --border-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--border-opacity));
+}
+
+.border-red-300 {
+  --border-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--border-opacity));
+}
+
+.border-red-400 {
+  --border-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--border-opacity));
+}
+
+.border-red-500 {
+  --border-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--border-opacity));
+}
+
+.border-red-600 {
+  --border-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--border-opacity));
+}
+
+.border-red-700 {
+  --border-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--border-opacity));
+}
+
+.border-red-800 {
+  --border-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--border-opacity));
+}
+
+.border-red-900 {
+  --border-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--border-opacity));
+}
+
+.border-orange-100 {
+  --border-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--border-opacity));
+}
+
+.border-orange-200 {
+  --border-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--border-opacity));
+}
+
+.border-orange-300 {
+  --border-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--border-opacity));
+}
+
+.border-orange-400 {
+  --border-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--border-opacity));
+}
+
+.border-orange-500 {
+  --border-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--border-opacity));
+}
+
+.border-orange-600 {
+  --border-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--border-opacity));
+}
+
+.border-orange-700 {
+  --border-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--border-opacity));
+}
+
+.border-orange-800 {
+  --border-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--border-opacity));
+}
+
+.border-orange-900 {
+  --border-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--border-opacity));
+}
+
+.border-yellow-100 {
+  --border-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--border-opacity));
+}
+
+.border-yellow-200 {
+  --border-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--border-opacity));
+}
+
+.border-yellow-300 {
+  --border-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--border-opacity));
+}
+
+.border-yellow-400 {
+  --border-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--border-opacity));
+}
+
+.border-yellow-500 {
+  --border-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--border-opacity));
+}
+
+.border-yellow-600 {
+  --border-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--border-opacity));
+}
+
+.border-yellow-700 {
+  --border-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--border-opacity));
+}
+
+.border-yellow-800 {
+  --border-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--border-opacity));
+}
+
+.border-yellow-900 {
+  --border-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--border-opacity));
+}
+
+.border-green-100 {
+  --border-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--border-opacity));
+}
+
+.border-green-200 {
+  --border-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--border-opacity));
+}
+
+.border-green-300 {
+  --border-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--border-opacity));
+}
+
+.border-green-400 {
+  --border-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--border-opacity));
+}
+
+.border-green-500 {
+  --border-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--border-opacity));
+}
+
+.border-green-600 {
+  --border-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--border-opacity));
+}
+
+.border-green-700 {
+  --border-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--border-opacity));
+}
+
+.border-green-800 {
+  --border-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--border-opacity));
+}
+
+.border-green-900 {
+  --border-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--border-opacity));
+}
+
+.border-teal-100 {
+  --border-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--border-opacity));
+}
+
+.border-teal-200 {
+  --border-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--border-opacity));
+}
+
+.border-teal-300 {
+  --border-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--border-opacity));
+}
+
+.border-teal-400 {
+  --border-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--border-opacity));
+}
+
+.border-teal-500 {
+  --border-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--border-opacity));
+}
+
+.border-teal-600 {
+  --border-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--border-opacity));
+}
+
+.border-teal-700 {
+  --border-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--border-opacity));
+}
+
+.border-teal-800 {
+  --border-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--border-opacity));
+}
+
+.border-teal-900 {
+  --border-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--border-opacity));
+}
+
+.border-blue-100 {
+  --border-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--border-opacity));
+}
+
+.border-blue-200 {
+  --border-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--border-opacity));
+}
+
+.border-blue-300 {
+  --border-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--border-opacity));
+}
+
+.border-blue-400 {
+  --border-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--border-opacity));
+}
+
+.border-blue-500 {
+  --border-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--border-opacity));
+}
+
+.border-blue-600 {
+  --border-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--border-opacity));
+}
+
+.border-blue-700 {
+  --border-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--border-opacity));
+}
+
+.border-blue-800 {
+  --border-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--border-opacity));
+}
+
+.border-blue-900 {
+  --border-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--border-opacity));
+}
+
+.border-indigo-100 {
+  --border-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--border-opacity));
+}
+
+.border-indigo-200 {
+  --border-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--border-opacity));
+}
+
+.border-indigo-300 {
+  --border-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--border-opacity));
+}
+
+.border-indigo-400 {
+  --border-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--border-opacity));
+}
+
+.border-indigo-500 {
+  --border-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--border-opacity));
+}
+
+.border-indigo-600 {
+  --border-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--border-opacity));
+}
+
+.border-indigo-700 {
+  --border-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--border-opacity));
+}
+
+.border-indigo-800 {
+  --border-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--border-opacity));
+}
+
+.border-indigo-900 {
+  --border-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--border-opacity));
+}
+
+.border-purple-100 {
+  --border-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--border-opacity));
+}
+
+.border-purple-200 {
+  --border-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--border-opacity));
+}
+
+.border-purple-300 {
+  --border-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--border-opacity));
+}
+
+.border-purple-400 {
+  --border-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--border-opacity));
+}
+
+.border-purple-500 {
+  --border-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--border-opacity));
+}
+
+.border-purple-600 {
+  --border-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--border-opacity));
+}
+
+.border-purple-700 {
+  --border-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--border-opacity));
+}
+
+.border-purple-800 {
+  --border-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--border-opacity));
+}
+
+.border-purple-900 {
+  --border-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--border-opacity));
+}
+
+.border-pink-100 {
+  --border-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--border-opacity));
+}
+
+.border-pink-200 {
+  --border-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--border-opacity));
+}
+
+.border-pink-300 {
+  --border-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--border-opacity));
+}
+
+.border-pink-400 {
+  --border-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--border-opacity));
+}
+
+.border-pink-500 {
+  --border-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--border-opacity));
+}
+
+.border-pink-600 {
+  --border-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--border-opacity));
+}
+
+.border-pink-700 {
+  --border-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--border-opacity));
+}
+
+.border-pink-800 {
+  --border-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--border-opacity));
+}
+
+.border-pink-900 {
+  --border-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--border-opacity));
+}
+
+.hover\:border-transparent:hover {
+  border-color: transparent;
+}
+
+.hover\:border-current:hover {
+  border-color: currentColor;
+}
+
+.hover\:border-black:hover {
+  --border-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--border-opacity));
+}
+
+.hover\:border-white:hover {
+  --border-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--border-opacity));
+}
+
+.hover\:border-gray-100:hover {
+  --border-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--border-opacity));
+}
+
+.hover\:border-gray-200:hover {
+  --border-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--border-opacity));
+}
+
+.hover\:border-gray-300:hover {
+  --border-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--border-opacity));
+}
+
+.hover\:border-gray-400:hover {
+  --border-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--border-opacity));
+}
+
+.hover\:border-gray-500:hover {
+  --border-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--border-opacity));
+}
+
+.hover\:border-gray-600:hover {
+  --border-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--border-opacity));
+}
+
+.hover\:border-gray-700:hover {
+  --border-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--border-opacity));
+}
+
+.hover\:border-gray-800:hover {
+  --border-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--border-opacity));
+}
+
+.hover\:border-gray-900:hover {
+  --border-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--border-opacity));
+}
+
+.hover\:border-red-100:hover {
+  --border-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--border-opacity));
+}
+
+.hover\:border-red-200:hover {
+  --border-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--border-opacity));
+}
+
+.hover\:border-red-300:hover {
+  --border-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--border-opacity));
+}
+
+.hover\:border-red-400:hover {
+  --border-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--border-opacity));
+}
+
+.hover\:border-red-500:hover {
+  --border-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--border-opacity));
+}
+
+.hover\:border-red-600:hover {
+  --border-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--border-opacity));
+}
+
+.hover\:border-red-700:hover {
+  --border-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--border-opacity));
+}
+
+.hover\:border-red-800:hover {
+  --border-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--border-opacity));
+}
+
+.hover\:border-red-900:hover {
+  --border-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--border-opacity));
+}
+
+.hover\:border-orange-100:hover {
+  --border-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--border-opacity));
+}
+
+.hover\:border-orange-200:hover {
+  --border-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--border-opacity));
+}
+
+.hover\:border-orange-300:hover {
+  --border-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--border-opacity));
+}
+
+.hover\:border-orange-400:hover {
+  --border-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--border-opacity));
+}
+
+.hover\:border-orange-500:hover {
+  --border-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--border-opacity));
+}
+
+.hover\:border-orange-600:hover {
+  --border-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--border-opacity));
+}
+
+.hover\:border-orange-700:hover {
+  --border-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--border-opacity));
+}
+
+.hover\:border-orange-800:hover {
+  --border-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--border-opacity));
+}
+
+.hover\:border-orange-900:hover {
+  --border-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--border-opacity));
+}
+
+.hover\:border-yellow-100:hover {
+  --border-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--border-opacity));
+}
+
+.hover\:border-yellow-200:hover {
+  --border-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--border-opacity));
+}
+
+.hover\:border-yellow-300:hover {
+  --border-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--border-opacity));
+}
+
+.hover\:border-yellow-400:hover {
+  --border-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--border-opacity));
+}
+
+.hover\:border-yellow-500:hover {
+  --border-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--border-opacity));
+}
+
+.hover\:border-yellow-600:hover {
+  --border-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--border-opacity));
+}
+
+.hover\:border-yellow-700:hover {
+  --border-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--border-opacity));
+}
+
+.hover\:border-yellow-800:hover {
+  --border-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--border-opacity));
+}
+
+.hover\:border-yellow-900:hover {
+  --border-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--border-opacity));
+}
+
+.hover\:border-green-100:hover {
+  --border-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--border-opacity));
+}
+
+.hover\:border-green-200:hover {
+  --border-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--border-opacity));
+}
+
+.hover\:border-green-300:hover {
+  --border-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--border-opacity));
+}
+
+.hover\:border-green-400:hover {
+  --border-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--border-opacity));
+}
+
+.hover\:border-green-500:hover {
+  --border-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--border-opacity));
+}
+
+.hover\:border-green-600:hover {
+  --border-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--border-opacity));
+}
+
+.hover\:border-green-700:hover {
+  --border-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--border-opacity));
+}
+
+.hover\:border-green-800:hover {
+  --border-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--border-opacity));
+}
+
+.hover\:border-green-900:hover {
+  --border-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--border-opacity));
+}
+
+.hover\:border-teal-100:hover {
+  --border-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--border-opacity));
+}
+
+.hover\:border-teal-200:hover {
+  --border-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--border-opacity));
+}
+
+.hover\:border-teal-300:hover {
+  --border-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--border-opacity));
+}
+
+.hover\:border-teal-400:hover {
+  --border-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--border-opacity));
+}
+
+.hover\:border-teal-500:hover {
+  --border-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--border-opacity));
+}
+
+.hover\:border-teal-600:hover {
+  --border-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--border-opacity));
+}
+
+.hover\:border-teal-700:hover {
+  --border-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--border-opacity));
+}
+
+.hover\:border-teal-800:hover {
+  --border-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--border-opacity));
+}
+
+.hover\:border-teal-900:hover {
+  --border-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--border-opacity));
+}
+
+.hover\:border-blue-100:hover {
+  --border-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--border-opacity));
+}
+
+.hover\:border-blue-200:hover {
+  --border-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--border-opacity));
+}
+
+.hover\:border-blue-300:hover {
+  --border-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--border-opacity));
+}
+
+.hover\:border-blue-400:hover {
+  --border-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--border-opacity));
+}
+
+.hover\:border-blue-500:hover {
+  --border-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--border-opacity));
+}
+
+.hover\:border-blue-600:hover {
+  --border-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--border-opacity));
+}
+
+.hover\:border-blue-700:hover {
+  --border-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--border-opacity));
+}
+
+.hover\:border-blue-800:hover {
+  --border-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--border-opacity));
+}
+
+.hover\:border-blue-900:hover {
+  --border-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--border-opacity));
+}
+
+.hover\:border-indigo-100:hover {
+  --border-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--border-opacity));
+}
+
+.hover\:border-indigo-200:hover {
+  --border-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--border-opacity));
+}
+
+.hover\:border-indigo-300:hover {
+  --border-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--border-opacity));
+}
+
+.hover\:border-indigo-400:hover {
+  --border-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--border-opacity));
+}
+
+.hover\:border-indigo-500:hover {
+  --border-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--border-opacity));
+}
+
+.hover\:border-indigo-600:hover {
+  --border-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--border-opacity));
+}
+
+.hover\:border-indigo-700:hover {
+  --border-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--border-opacity));
+}
+
+.hover\:border-indigo-800:hover {
+  --border-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--border-opacity));
+}
+
+.hover\:border-indigo-900:hover {
+  --border-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--border-opacity));
+}
+
+.hover\:border-purple-100:hover {
+  --border-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--border-opacity));
+}
+
+.hover\:border-purple-200:hover {
+  --border-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--border-opacity));
+}
+
+.hover\:border-purple-300:hover {
+  --border-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--border-opacity));
+}
+
+.hover\:border-purple-400:hover {
+  --border-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--border-opacity));
+}
+
+.hover\:border-purple-500:hover {
+  --border-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--border-opacity));
+}
+
+.hover\:border-purple-600:hover {
+  --border-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--border-opacity));
+}
+
+.hover\:border-purple-700:hover {
+  --border-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--border-opacity));
+}
+
+.hover\:border-purple-800:hover {
+  --border-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--border-opacity));
+}
+
+.hover\:border-purple-900:hover {
+  --border-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--border-opacity));
+}
+
+.hover\:border-pink-100:hover {
+  --border-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--border-opacity));
+}
+
+.hover\:border-pink-200:hover {
+  --border-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--border-opacity));
+}
+
+.hover\:border-pink-300:hover {
+  --border-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--border-opacity));
+}
+
+.hover\:border-pink-400:hover {
+  --border-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--border-opacity));
+}
+
+.hover\:border-pink-500:hover {
+  --border-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--border-opacity));
+}
+
+.hover\:border-pink-600:hover {
+  --border-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--border-opacity));
+}
+
+.hover\:border-pink-700:hover {
+  --border-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--border-opacity));
+}
+
+.hover\:border-pink-800:hover {
+  --border-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--border-opacity));
+}
+
+.hover\:border-pink-900:hover {
+  --border-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--border-opacity));
+}
+
+.focus\:border-transparent:focus {
+  border-color: transparent;
+}
+
+.focus\:border-current:focus {
+  border-color: currentColor;
+}
+
+.focus\:border-black:focus {
+  --border-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--border-opacity));
+}
+
+.focus\:border-white:focus {
+  --border-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--border-opacity));
+}
+
+.focus\:border-gray-100:focus {
+  --border-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--border-opacity));
+}
+
+.focus\:border-gray-200:focus {
+  --border-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--border-opacity));
+}
+
+.focus\:border-gray-300:focus {
+  --border-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--border-opacity));
+}
+
+.focus\:border-gray-400:focus {
+  --border-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--border-opacity));
+}
+
+.focus\:border-gray-500:focus {
+  --border-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--border-opacity));
+}
+
+.focus\:border-gray-600:focus {
+  --border-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--border-opacity));
+}
+
+.focus\:border-gray-700:focus {
+  --border-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--border-opacity));
+}
+
+.focus\:border-gray-800:focus {
+  --border-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--border-opacity));
+}
+
+.focus\:border-gray-900:focus {
+  --border-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--border-opacity));
+}
+
+.focus\:border-red-100:focus {
+  --border-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--border-opacity));
+}
+
+.focus\:border-red-200:focus {
+  --border-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--border-opacity));
+}
+
+.focus\:border-red-300:focus {
+  --border-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--border-opacity));
+}
+
+.focus\:border-red-400:focus {
+  --border-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--border-opacity));
+}
+
+.focus\:border-red-500:focus {
+  --border-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--border-opacity));
+}
+
+.focus\:border-red-600:focus {
+  --border-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--border-opacity));
+}
+
+.focus\:border-red-700:focus {
+  --border-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--border-opacity));
+}
+
+.focus\:border-red-800:focus {
+  --border-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--border-opacity));
+}
+
+.focus\:border-red-900:focus {
+  --border-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--border-opacity));
+}
+
+.focus\:border-orange-100:focus {
+  --border-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--border-opacity));
+}
+
+.focus\:border-orange-200:focus {
+  --border-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--border-opacity));
+}
+
+.focus\:border-orange-300:focus {
+  --border-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--border-opacity));
+}
+
+.focus\:border-orange-400:focus {
+  --border-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--border-opacity));
+}
+
+.focus\:border-orange-500:focus {
+  --border-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--border-opacity));
+}
+
+.focus\:border-orange-600:focus {
+  --border-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--border-opacity));
+}
+
+.focus\:border-orange-700:focus {
+  --border-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--border-opacity));
+}
+
+.focus\:border-orange-800:focus {
+  --border-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--border-opacity));
+}
+
+.focus\:border-orange-900:focus {
+  --border-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--border-opacity));
+}
+
+.focus\:border-yellow-100:focus {
+  --border-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--border-opacity));
+}
+
+.focus\:border-yellow-200:focus {
+  --border-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--border-opacity));
+}
+
+.focus\:border-yellow-300:focus {
+  --border-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--border-opacity));
+}
+
+.focus\:border-yellow-400:focus {
+  --border-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--border-opacity));
+}
+
+.focus\:border-yellow-500:focus {
+  --border-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--border-opacity));
+}
+
+.focus\:border-yellow-600:focus {
+  --border-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--border-opacity));
+}
+
+.focus\:border-yellow-700:focus {
+  --border-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--border-opacity));
+}
+
+.focus\:border-yellow-800:focus {
+  --border-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--border-opacity));
+}
+
+.focus\:border-yellow-900:focus {
+  --border-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--border-opacity));
+}
+
+.focus\:border-green-100:focus {
+  --border-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--border-opacity));
+}
+
+.focus\:border-green-200:focus {
+  --border-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--border-opacity));
+}
+
+.focus\:border-green-300:focus {
+  --border-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--border-opacity));
+}
+
+.focus\:border-green-400:focus {
+  --border-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--border-opacity));
+}
+
+.focus\:border-green-500:focus {
+  --border-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--border-opacity));
+}
+
+.focus\:border-green-600:focus {
+  --border-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--border-opacity));
+}
+
+.focus\:border-green-700:focus {
+  --border-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--border-opacity));
+}
+
+.focus\:border-green-800:focus {
+  --border-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--border-opacity));
+}
+
+.focus\:border-green-900:focus {
+  --border-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--border-opacity));
+}
+
+.focus\:border-teal-100:focus {
+  --border-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--border-opacity));
+}
+
+.focus\:border-teal-200:focus {
+  --border-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--border-opacity));
+}
+
+.focus\:border-teal-300:focus {
+  --border-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--border-opacity));
+}
+
+.focus\:border-teal-400:focus {
+  --border-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--border-opacity));
+}
+
+.focus\:border-teal-500:focus {
+  --border-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--border-opacity));
+}
+
+.focus\:border-teal-600:focus {
+  --border-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--border-opacity));
+}
+
+.focus\:border-teal-700:focus {
+  --border-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--border-opacity));
+}
+
+.focus\:border-teal-800:focus {
+  --border-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--border-opacity));
+}
+
+.focus\:border-teal-900:focus {
+  --border-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--border-opacity));
+}
+
+.focus\:border-blue-100:focus {
+  --border-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--border-opacity));
+}
+
+.focus\:border-blue-200:focus {
+  --border-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--border-opacity));
+}
+
+.focus\:border-blue-300:focus {
+  --border-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--border-opacity));
+}
+
+.focus\:border-blue-400:focus {
+  --border-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--border-opacity));
+}
+
+.focus\:border-blue-500:focus {
+  --border-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--border-opacity));
+}
+
+.focus\:border-blue-600:focus {
+  --border-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--border-opacity));
+}
+
+.focus\:border-blue-700:focus {
+  --border-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--border-opacity));
+}
+
+.focus\:border-blue-800:focus {
+  --border-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--border-opacity));
+}
+
+.focus\:border-blue-900:focus {
+  --border-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--border-opacity));
+}
+
+.focus\:border-indigo-100:focus {
+  --border-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--border-opacity));
+}
+
+.focus\:border-indigo-200:focus {
+  --border-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--border-opacity));
+}
+
+.focus\:border-indigo-300:focus {
+  --border-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--border-opacity));
+}
+
+.focus\:border-indigo-400:focus {
+  --border-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--border-opacity));
+}
+
+.focus\:border-indigo-500:focus {
+  --border-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--border-opacity));
+}
+
+.focus\:border-indigo-600:focus {
+  --border-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--border-opacity));
+}
+
+.focus\:border-indigo-700:focus {
+  --border-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--border-opacity));
+}
+
+.focus\:border-indigo-800:focus {
+  --border-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--border-opacity));
+}
+
+.focus\:border-indigo-900:focus {
+  --border-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--border-opacity));
+}
+
+.focus\:border-purple-100:focus {
+  --border-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--border-opacity));
+}
+
+.focus\:border-purple-200:focus {
+  --border-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--border-opacity));
+}
+
+.focus\:border-purple-300:focus {
+  --border-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--border-opacity));
+}
+
+.focus\:border-purple-400:focus {
+  --border-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--border-opacity));
+}
+
+.focus\:border-purple-500:focus {
+  --border-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--border-opacity));
+}
+
+.focus\:border-purple-600:focus {
+  --border-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--border-opacity));
+}
+
+.focus\:border-purple-700:focus {
+  --border-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--border-opacity));
+}
+
+.focus\:border-purple-800:focus {
+  --border-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--border-opacity));
+}
+
+.focus\:border-purple-900:focus {
+  --border-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--border-opacity));
+}
+
+.focus\:border-pink-100:focus {
+  --border-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--border-opacity));
+}
+
+.focus\:border-pink-200:focus {
+  --border-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--border-opacity));
+}
+
+.focus\:border-pink-300:focus {
+  --border-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--border-opacity));
+}
+
+.focus\:border-pink-400:focus {
+  --border-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--border-opacity));
+}
+
+.focus\:border-pink-500:focus {
+  --border-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--border-opacity));
+}
+
+.focus\:border-pink-600:focus {
+  --border-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--border-opacity));
+}
+
+.focus\:border-pink-700:focus {
+  --border-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--border-opacity));
+}
+
+.focus\:border-pink-800:focus {
+  --border-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--border-opacity));
+}
+
+.focus\:border-pink-900:focus {
+  --border-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--border-opacity));
+}
+
+.border-opacity-0 {
+  --border-opacity: 0;
+}
+
+.border-opacity-25 {
+  --border-opacity: 0.25;
+}
+
+.border-opacity-50 {
+  --border-opacity: 0.5;
+}
+
+.border-opacity-75 {
+  --border-opacity: 0.75;
+}
+
+.border-opacity-100 {
+  --border-opacity: 1;
+}
+
+.hover\:border-opacity-0:hover {
+  --border-opacity: 0;
+}
+
+.hover\:border-opacity-25:hover {
+  --border-opacity: 0.25;
+}
+
+.hover\:border-opacity-50:hover {
+  --border-opacity: 0.5;
+}
+
+.hover\:border-opacity-75:hover {
+  --border-opacity: 0.75;
+}
+
+.hover\:border-opacity-100:hover {
+  --border-opacity: 1;
+}
+
+.focus\:border-opacity-0:focus {
+  --border-opacity: 0;
+}
+
+.focus\:border-opacity-25:focus {
+  --border-opacity: 0.25;
+}
+
+.focus\:border-opacity-50:focus {
+  --border-opacity: 0.5;
+}
+
+.focus\:border-opacity-75:focus {
+  --border-opacity: 0.75;
+}
+
+.focus\:border-opacity-100:focus {
+  --border-opacity: 1;
+}
+
+.rounded-none {
+  border-radius: 0;
+}
+
+.rounded-sm {
+  border-radius: 0.125rem;
+}
+
+.rounded {
+  border-radius: 0.25rem;
+}
+
+.rounded-md {
+  border-radius: 0.375rem;
+}
+
+.rounded-lg {
+  border-radius: 0.5rem;
+}
+
+.rounded-full {
+  border-radius: 9999px;
+}
+
+.rounded-t-none {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.rounded-r-none {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.rounded-b-none {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.rounded-l-none {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.rounded-t-sm {
+  border-top-left-radius: 0.125rem;
+  border-top-right-radius: 0.125rem;
+}
+
+.rounded-r-sm {
+  border-top-right-radius: 0.125rem;
+  border-bottom-right-radius: 0.125rem;
+}
+
+.rounded-b-sm {
+  border-bottom-right-radius: 0.125rem;
+  border-bottom-left-radius: 0.125rem;
+}
+
+.rounded-l-sm {
+  border-top-left-radius: 0.125rem;
+  border-bottom-left-radius: 0.125rem;
+}
+
+.rounded-t {
+  border-top-left-radius: 0.25rem;
+  border-top-right-radius: 0.25rem;
+}
+
+.rounded-r {
+  border-top-right-radius: 0.25rem;
+  border-bottom-right-radius: 0.25rem;
+}
+
+.rounded-b {
+  border-bottom-right-radius: 0.25rem;
+  border-bottom-left-radius: 0.25rem;
+}
+
+.rounded-l {
+  border-top-left-radius: 0.25rem;
+  border-bottom-left-radius: 0.25rem;
+}
+
+.rounded-t-md {
+  border-top-left-radius: 0.375rem;
+  border-top-right-radius: 0.375rem;
+}
+
+.rounded-r-md {
+  border-top-right-radius: 0.375rem;
+  border-bottom-right-radius: 0.375rem;
+}
+
+.rounded-b-md {
+  border-bottom-right-radius: 0.375rem;
+  border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-l-md {
+  border-top-left-radius: 0.375rem;
+  border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-t-lg {
+  border-top-left-radius: 0.5rem;
+  border-top-right-radius: 0.5rem;
+}
+
+.rounded-r-lg {
+  border-top-right-radius: 0.5rem;
+  border-bottom-right-radius: 0.5rem;
+}
+
+.rounded-b-lg {
+  border-bottom-right-radius: 0.5rem;
+  border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-l-lg {
+  border-top-left-radius: 0.5rem;
+  border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-t-full {
+  border-top-left-radius: 9999px;
+  border-top-right-radius: 9999px;
+}
+
+.rounded-r-full {
+  border-top-right-radius: 9999px;
+  border-bottom-right-radius: 9999px;
+}
+
+.rounded-b-full {
+  border-bottom-right-radius: 9999px;
+  border-bottom-left-radius: 9999px;
+}
+
+.rounded-l-full {
+  border-top-left-radius: 9999px;
+  border-bottom-left-radius: 9999px;
+}
+
+.rounded-tl-none {
+  border-top-left-radius: 0;
+}
+
+.rounded-tr-none {
+  border-top-right-radius: 0;
+}
+
+.rounded-br-none {
+  border-bottom-right-radius: 0;
+}
+
+.rounded-bl-none {
+  border-bottom-left-radius: 0;
+}
+
+.rounded-tl-sm {
+  border-top-left-radius: 0.125rem;
+}
+
+.rounded-tr-sm {
+  border-top-right-radius: 0.125rem;
+}
+
+.rounded-br-sm {
+  border-bottom-right-radius: 0.125rem;
+}
+
+.rounded-bl-sm {
+  border-bottom-left-radius: 0.125rem;
+}
+
+.rounded-tl {
+  border-top-left-radius: 0.25rem;
+}
+
+.rounded-tr {
+  border-top-right-radius: 0.25rem;
+}
+
+.rounded-br {
+  border-bottom-right-radius: 0.25rem;
+}
+
+.rounded-bl {
+  border-bottom-left-radius: 0.25rem;
+}
+
+.rounded-tl-md {
+  border-top-left-radius: 0.375rem;
+}
+
+.rounded-tr-md {
+  border-top-right-radius: 0.375rem;
+}
+
+.rounded-br-md {
+  border-bottom-right-radius: 0.375rem;
+}
+
+.rounded-bl-md {
+  border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-tl-lg {
+  border-top-left-radius: 0.5rem;
+}
+
+.rounded-tr-lg {
+  border-top-right-radius: 0.5rem;
+}
+
+.rounded-br-lg {
+  border-bottom-right-radius: 0.5rem;
+}
+
+.rounded-bl-lg {
+  border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-tl-full {
+  border-top-left-radius: 9999px;
+}
+
+.rounded-tr-full {
+  border-top-right-radius: 9999px;
+}
+
+.rounded-br-full {
+  border-bottom-right-radius: 9999px;
+}
+
+.rounded-bl-full {
+  border-bottom-left-radius: 9999px;
+}
+
+.border-solid {
+  border-style: solid;
+}
+
+.border-dashed {
+  border-style: dashed;
+}
+
+.border-dotted {
+  border-style: dotted;
+}
+
+.border-double {
+  border-style: double;
+}
+
+.border-none {
+  border-style: none;
+}
+
+.border-0 {
+  border-width: 0;
+}
+
+.border-2 {
+  border-width: 2px;
+}
+
+.border-4 {
+  border-width: 4px;
+}
+
+.border-8 {
+  border-width: 8px;
+}
+
+.border {
+  border-width: 1px;
+}
+
+.border-t-0 {
+  border-top-width: 0;
+}
+
+.border-r-0 {
+  border-right-width: 0;
+}
+
+.border-b-0 {
+  border-bottom-width: 0;
+}
+
+.border-l-0 {
+  border-left-width: 0;
+}
+
+.border-t-2 {
+  border-top-width: 2px;
+}
+
+.border-r-2 {
+  border-right-width: 2px;
+}
+
+.border-b-2 {
+  border-bottom-width: 2px;
+}
+
+.border-l-2 {
+  border-left-width: 2px;
+}
+
+.border-t-4 {
+  border-top-width: 4px;
+}
+
+.border-r-4 {
+  border-right-width: 4px;
+}
+
+.border-b-4 {
+  border-bottom-width: 4px;
+}
+
+.border-l-4 {
+  border-left-width: 4px;
+}
+
+.border-t-8 {
+  border-top-width: 8px;
+}
+
+.border-r-8 {
+  border-right-width: 8px;
+}
+
+.border-b-8 {
+  border-bottom-width: 8px;
+}
+
+.border-l-8 {
+  border-left-width: 8px;
+}
+
+.border-t {
+  border-top-width: 1px;
+}
+
+.border-r {
+  border-right-width: 1px;
+}
+
+.border-b {
+  border-bottom-width: 1px;
+}
+
+.border-l {
+  border-left-width: 1px;
+}
+
+.box-border {
+  box-sizing: border-box;
+}
+
+.box-content {
+  box-sizing: content-box;
+}
+
+.cursor-auto {
+  cursor: auto;
+}
+
+.cursor-default {
+  cursor: default;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.cursor-wait {
+  cursor: wait;
+}
+
+.cursor-text {
+  cursor: text;
+}
+
+.cursor-move {
+  cursor: move;
+}
+
+.cursor-not-allowed {
+  cursor: not-allowed;
+}
+
+.block {
+  display: block;
+}
+
+.inline-block {
+  display: inline-block;
+}
+
+.inline {
+  display: inline;
+}
+
+.flex {
+  display: flex;
+}
+
+.inline-flex {
+  display: inline-flex;
+}
+
+.table {
+  display: table;
+}
+
+.table-caption {
+  display: table-caption;
+}
+
+.table-cell {
+  display: table-cell;
+}
+
+.table-column {
+  display: table-column;
+}
+
+.table-column-group {
+  display: table-column-group;
+}
+
+.table-footer-group {
+  display: table-footer-group;
+}
+
+.table-header-group {
+  display: table-header-group;
+}
+
+.table-row-group {
+  display: table-row-group;
+}
+
+.table-row {
+  display: table-row;
+}
+
+.flow-root {
+  display: flow-root;
+}
+
+.grid {
+  display: grid;
+}
+
+.inline-grid {
+  display: inline-grid;
+}
+
+.contents {
+  display: contents;
+}
+
+.hidden {
+  display: none;
+}
+
+.flex-row {
+  flex-direction: row;
+}
+
+.flex-row-reverse {
+  flex-direction: row-reverse;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.flex-col-reverse {
+  flex-direction: column-reverse;
+}
+
+.flex-wrap {
+  flex-wrap: wrap;
+}
+
+.flex-wrap-reverse {
+  flex-wrap: wrap-reverse;
+}
+
+.flex-no-wrap {
+  flex-wrap: nowrap;
+}
+
+.place-items-auto {
+  place-items: auto;
+}
+
+.place-items-start {
+  place-items: start;
+}
+
+.place-items-end {
+  place-items: end;
+}
+
+.place-items-center {
+  place-items: center;
+}
+
+.place-items-stretch {
+  place-items: stretch;
+}
+
+.place-content-center {
+  place-content: center;
+}
+
+.place-content-start {
+  place-content: start;
+}
+
+.place-content-end {
+  place-content: end;
+}
+
+.place-content-between {
+  place-content: space-between;
+}
+
+.place-content-around {
+  place-content: space-around;
+}
+
+.place-content-evenly {
+  place-content: space-evenly;
+}
+
+.place-content-stretch {
+  place-content: stretch;
+}
+
+.place-self-auto {
+  place-self: auto;
+}
+
+.place-self-start {
+  place-self: start;
+}
+
+.place-self-end {
+  place-self: end;
+}
+
+.place-self-center {
+  place-self: center;
+}
+
+.place-self-stretch {
+  place-self: stretch;
+}
+
+.items-start {
+  align-items: flex-start;
+}
+
+.items-end {
+  align-items: flex-end;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.items-baseline {
+  align-items: baseline;
+}
+
+.items-stretch {
+  align-items: stretch;
+}
+
+.content-center {
+  align-content: center;
+}
+
+.content-start {
+  align-content: flex-start;
+}
+
+.content-end {
+  align-content: flex-end;
+}
+
+.content-between {
+  align-content: space-between;
+}
+
+.content-around {
+  align-content: space-around;
+}
+
+.content-evenly {
+  align-content: space-evenly;
+}
+
+.self-auto {
+  align-self: auto;
+}
+
+.self-start {
+  align-self: flex-start;
+}
+
+.self-end {
+  align-self: flex-end;
+}
+
+.self-center {
+  align-self: center;
+}
+
+.self-stretch {
+  align-self: stretch;
+}
+
+.justify-items-auto {
+  justify-items: auto;
+}
+
+.justify-items-start {
+  justify-items: start;
+}
+
+.justify-items-end {
+  justify-items: end;
+}
+
+.justify-items-center {
+  justify-items: center;
+}
+
+.justify-items-stretch {
+  justify-items: stretch;
+}
+
+.justify-start {
+  justify-content: flex-start;
+}
+
+.justify-end {
+  justify-content: flex-end;
+}
+
+.justify-center {
+  justify-content: center;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.justify-around {
+  justify-content: space-around;
+}
+
+.justify-evenly {
+  justify-content: space-evenly;
+}
+
+.justify-self-auto {
+  justify-self: auto;
+}
+
+.justify-self-start {
+  justify-self: start;
+}
+
+.justify-self-end {
+  justify-self: end;
+}
+
+.justify-self-center {
+  justify-self: center;
+}
+
+.justify-self-stretch {
+  justify-self: stretch;
+}
+
+.flex-1 {
+  flex: 1 1 0%;
+}
+
+.flex-auto {
+  flex: 1 1 auto;
+}
+
+.flex-initial {
+  flex: 0 1 auto;
+}
+
+.flex-none {
+  flex: none;
+}
+
+.flex-grow-0 {
+  flex-grow: 0;
+}
+
+.flex-grow {
+  flex-grow: 1;
+}
+
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+
+.flex-shrink {
+  flex-shrink: 1;
+}
+
+.order-1 {
+  order: 1;
+}
+
+.order-2 {
+  order: 2;
+}
+
+.order-3 {
+  order: 3;
+}
+
+.order-4 {
+  order: 4;
+}
+
+.order-5 {
+  order: 5;
+}
+
+.order-6 {
+  order: 6;
+}
+
+.order-7 {
+  order: 7;
+}
+
+.order-8 {
+  order: 8;
+}
+
+.order-9 {
+  order: 9;
+}
+
+.order-10 {
+  order: 10;
+}
+
+.order-11 {
+  order: 11;
+}
+
+.order-12 {
+  order: 12;
+}
+
+.order-first {
+  order: -9999;
+}
+
+.order-last {
+  order: 9999;
+}
+
+.order-none {
+  order: 0;
+}
+
+.float-right {
+  float: right;
+}
+
+.float-left {
+  float: left;
+}
+
+.float-none {
+  float: none;
+}
+
+.clearfix:after {
+  content: "";
+  display: table;
+  clear: both;
+}
+
+.clear-left {
+  clear: left;
+}
+
+.clear-right {
+  clear: right;
+}
+
+.clear-both {
+  clear: both;
+}
+
+.clear-none {
+  clear: none;
+}
+
+.font-sans {
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+.font-serif {
+  font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+}
+
+.font-mono {
+  font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+.font-hairline {
+  font-weight: 100;
+}
+
+.font-thin {
+  font-weight: 200;
+}
+
+.font-light {
+  font-weight: 300;
+}
+
+.font-normal {
+  font-weight: 400;
+}
+
+.font-medium {
+  font-weight: 500;
+}
+
+.font-semibold {
+  font-weight: 600;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+
+.font-extrabold {
+  font-weight: 800;
+}
+
+.font-black {
+  font-weight: 900;
+}
+
+.hover\:font-hairline:hover {
+  font-weight: 100;
+}
+
+.hover\:font-thin:hover {
+  font-weight: 200;
+}
+
+.hover\:font-light:hover {
+  font-weight: 300;
+}
+
+.hover\:font-normal:hover {
+  font-weight: 400;
+}
+
+.hover\:font-medium:hover {
+  font-weight: 500;
+}
+
+.hover\:font-semibold:hover {
+  font-weight: 600;
+}
+
+.hover\:font-bold:hover {
+  font-weight: 700;
+}
+
+.hover\:font-extrabold:hover {
+  font-weight: 800;
+}
+
+.hover\:font-black:hover {
+  font-weight: 900;
+}
+
+.focus\:font-hairline:focus {
+  font-weight: 100;
+}
+
+.focus\:font-thin:focus {
+  font-weight: 200;
+}
+
+.focus\:font-light:focus {
+  font-weight: 300;
+}
+
+.focus\:font-normal:focus {
+  font-weight: 400;
+}
+
+.focus\:font-medium:focus {
+  font-weight: 500;
+}
+
+.focus\:font-semibold:focus {
+  font-weight: 600;
+}
+
+.focus\:font-bold:focus {
+  font-weight: 700;
+}
+
+.focus\:font-extrabold:focus {
+  font-weight: 800;
+}
+
+.focus\:font-black:focus {
+  font-weight: 900;
+}
+
+.h-0 {
+  height: 0;
+}
+
+.h-1 {
+  height: 0.25rem;
+}
+
+.h-2 {
+  height: 0.5rem;
+}
+
+.h-3 {
+  height: 0.75rem;
+}
+
+.h-4 {
+  height: 1rem;
+}
+
+.h-5 {
+  height: 1.25rem;
+}
+
+.h-6 {
+  height: 1.5rem;
+}
+
+.h-8 {
+  height: 2rem;
+}
+
+.h-10 {
+  height: 2.5rem;
+}
+
+.h-12 {
+  height: 3rem;
+}
+
+.h-16 {
+  height: 4rem;
+}
+
+.h-20 {
+  height: 5rem;
+}
+
+.h-24 {
+  height: 6rem;
+}
+
+.h-32 {
+  height: 8rem;
+}
+
+.h-40 {
+  height: 10rem;
+}
+
+.h-48 {
+  height: 12rem;
+}
+
+.h-56 {
+  height: 14rem;
+}
+
+.h-64 {
+  height: 16rem;
+}
+
+.h-auto {
+  height: auto;
+}
+
+.h-px {
+  height: 1px;
+}
+
+.h-full {
+  height: 100%;
+}
+
+.h-screen {
+  height: 100vh;
+}
+
+.text-xs {
+  font-size: 0.75rem;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+}
+
+.text-base {
+  font-size: 1rem;
+}
+
+.text-lg {
+  font-size: 1.125rem;
+}
+
+.text-xl {
+  font-size: 1.25rem;
+}
+
+.text-2xl {
+  font-size: 1.5rem;
+}
+
+.text-3xl {
+  font-size: 1.875rem;
+}
+
+.text-4xl {
+  font-size: 2.25rem;
+}
+
+.text-5xl {
+  font-size: 3rem;
+}
+
+.text-6xl {
+  font-size: 4rem;
+}
+
+.leading-3 {
+  line-height: .75rem;
+}
+
+.leading-4 {
+  line-height: 1rem;
+}
+
+.leading-5 {
+  line-height: 1.25rem;
+}
+
+.leading-6 {
+  line-height: 1.5rem;
+}
+
+.leading-7 {
+  line-height: 1.75rem;
+}
+
+.leading-8 {
+  line-height: 2rem;
+}
+
+.leading-9 {
+  line-height: 2.25rem;
+}
+
+.leading-10 {
+  line-height: 2.5rem;
+}
+
+.leading-none {
+  line-height: 1;
+}
+
+.leading-tight {
+  line-height: 1.25;
+}
+
+.leading-snug {
+  line-height: 1.375;
+}
+
+.leading-normal {
+  line-height: 1.5;
+}
+
+.leading-relaxed {
+  line-height: 1.625;
+}
+
+.leading-loose {
+  line-height: 2;
+}
+
+.list-inside {
+  list-style-position: inside;
+}
+
+.list-outside {
+  list-style-position: outside;
+}
+
+.list-none {
+  list-style-type: none;
+}
+
+.list-disc {
+  list-style-type: disc;
+}
+
+.list-decimal {
+  list-style-type: decimal;
+}
+
+.m-0 {
+  margin: 0;
+}
+
+.m-1 {
+  margin: 0.25rem;
+}
+
+.m-2 {
+  margin: 0.5rem;
+}
+
+.m-3 {
+  margin: 0.75rem;
+}
+
+.m-4 {
+  margin: 1rem;
+}
+
+.m-5 {
+  margin: 1.25rem;
+}
+
+.m-6 {
+  margin: 1.5rem;
+}
+
+.m-8 {
+  margin: 2rem;
+}
+
+.m-10 {
+  margin: 2.5rem;
+}
+
+.m-12 {
+  margin: 3rem;
+}
+
+.m-16 {
+  margin: 4rem;
+}
+
+.m-20 {
+  margin: 5rem;
+}
+
+.m-24 {
+  margin: 6rem;
+}
+
+.m-32 {
+  margin: 8rem;
+}
+
+.m-40 {
+  margin: 10rem;
+}
+
+.m-48 {
+  margin: 12rem;
+}
+
+.m-56 {
+  margin: 14rem;
+}
+
+.m-64 {
+  margin: 16rem;
+}
+
+.m-auto {
+  margin: auto;
+}
+
+.m-px {
+  margin: 1px;
+}
+
+.-m-1 {
+  margin: -0.25rem;
+}
+
+.-m-2 {
+  margin: -0.5rem;
+}
+
+.-m-3 {
+  margin: -0.75rem;
+}
+
+.-m-4 {
+  margin: -1rem;
+}
+
+.-m-5 {
+  margin: -1.25rem;
+}
+
+.-m-6 {
+  margin: -1.5rem;
+}
+
+.-m-8 {
+  margin: -2rem;
+}
+
+.-m-10 {
+  margin: -2.5rem;
+}
+
+.-m-12 {
+  margin: -3rem;
+}
+
+.-m-16 {
+  margin: -4rem;
+}
+
+.-m-20 {
+  margin: -5rem;
+}
+
+.-m-24 {
+  margin: -6rem;
+}
+
+.-m-32 {
+  margin: -8rem;
+}
+
+.-m-40 {
+  margin: -10rem;
+}
+
+.-m-48 {
+  margin: -12rem;
+}
+
+.-m-56 {
+  margin: -14rem;
+}
+
+.-m-64 {
+  margin: -16rem;
+}
+
+.-m-px {
+  margin: -1px;
+}
+
+.my-0 {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.mx-0 {
+  margin-left: 0;
+  margin-right: 0;
+}
+
+.my-1 {
+  margin-top: 0.25rem;
+  margin-bottom: 0.25rem;
+}
+
+.mx-1 {
+  margin-left: 0.25rem;
+  margin-right: 0.25rem;
+}
+
+.my-2 {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+
+.mx-2 {
+  margin-left: 0.5rem;
+  margin-right: 0.5rem;
+}
+
+.my-3 {
+  margin-top: 0.75rem;
+  margin-bottom: 0.75rem;
+}
+
+.mx-3 {
+  margin-left: 0.75rem;
+  margin-right: 0.75rem;
+}
+
+.my-4 {
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+}
+
+.mx-4 {
+  margin-left: 1rem;
+  margin-right: 1rem;
+}
+
+.my-5 {
+  margin-top: 1.25rem;
+  margin-bottom: 1.25rem;
+}
+
+.mx-5 {
+  margin-left: 1.25rem;
+  margin-right: 1.25rem;
+}
+
+.my-6 {
+  margin-top: 1.5rem;
+  margin-bottom: 1.5rem;
+}
+
+.mx-6 {
+  margin-left: 1.5rem;
+  margin-right: 1.5rem;
+}
+
+.my-8 {
+  margin-top: 2rem;
+  margin-bottom: 2rem;
+}
+
+.mx-8 {
+  margin-left: 2rem;
+  margin-right: 2rem;
+}
+
+.my-10 {
+  margin-top: 2.5rem;
+  margin-bottom: 2.5rem;
+}
+
+.mx-10 {
+  margin-left: 2.5rem;
+  margin-right: 2.5rem;
+}
+
+.my-12 {
+  margin-top: 3rem;
+  margin-bottom: 3rem;
+}
+
+.mx-12 {
+  margin-left: 3rem;
+  margin-right: 3rem;
+}
+
+.my-16 {
+  margin-top: 4rem;
+  margin-bottom: 4rem;
+}
+
+.mx-16 {
+  margin-left: 4rem;
+  margin-right: 4rem;
+}
+
+.my-20 {
+  margin-top: 5rem;
+  margin-bottom: 5rem;
+}
+
+.mx-20 {
+  margin-left: 5rem;
+  margin-right: 5rem;
+}
+
+.my-24 {
+  margin-top: 6rem;
+  margin-bottom: 6rem;
+}
+
+.mx-24 {
+  margin-left: 6rem;
+  margin-right: 6rem;
+}
+
+.my-32 {
+  margin-top: 8rem;
+  margin-bottom: 8rem;
+}
+
+.mx-32 {
+  margin-left: 8rem;
+  margin-right: 8rem;
+}
+
+.my-40 {
+  margin-top: 10rem;
+  margin-bottom: 10rem;
+}
+
+.mx-40 {
+  margin-left: 10rem;
+  margin-right: 10rem;
+}
+
+.my-48 {
+  margin-top: 12rem;
+  margin-bottom: 12rem;
+}
+
+.mx-48 {
+  margin-left: 12rem;
+  margin-right: 12rem;
+}
+
+.my-56 {
+  margin-top: 14rem;
+  margin-bottom: 14rem;
+}
+
+.mx-56 {
+  margin-left: 14rem;
+  margin-right: 14rem;
+}
+
+.my-64 {
+  margin-top: 16rem;
+  margin-bottom: 16rem;
+}
+
+.mx-64 {
+  margin-left: 16rem;
+  margin-right: 16rem;
+}
+
+.my-auto {
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
+.mx-auto {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.my-px {
+  margin-top: 1px;
+  margin-bottom: 1px;
+}
+
+.mx-px {
+  margin-left: 1px;
+  margin-right: 1px;
+}
+
+.-my-1 {
+  margin-top: -0.25rem;
+  margin-bottom: -0.25rem;
+}
+
+.-mx-1 {
+  margin-left: -0.25rem;
+  margin-right: -0.25rem;
+}
+
+.-my-2 {
+  margin-top: -0.5rem;
+  margin-bottom: -0.5rem;
+}
+
+.-mx-2 {
+  margin-left: -0.5rem;
+  margin-right: -0.5rem;
+}
+
+.-my-3 {
+  margin-top: -0.75rem;
+  margin-bottom: -0.75rem;
+}
+
+.-mx-3 {
+  margin-left: -0.75rem;
+  margin-right: -0.75rem;
+}
+
+.-my-4 {
+  margin-top: -1rem;
+  margin-bottom: -1rem;
+}
+
+.-mx-4 {
+  margin-left: -1rem;
+  margin-right: -1rem;
+}
+
+.-my-5 {
+  margin-top: -1.25rem;
+  margin-bottom: -1.25rem;
+}
+
+.-mx-5 {
+  margin-left: -1.25rem;
+  margin-right: -1.25rem;
+}
+
+.-my-6 {
+  margin-top: -1.5rem;
+  margin-bottom: -1.5rem;
+}
+
+.-mx-6 {
+  margin-left: -1.5rem;
+  margin-right: -1.5rem;
+}
+
+.-my-8 {
+  margin-top: -2rem;
+  margin-bottom: -2rem;
+}
+
+.-mx-8 {
+  margin-left: -2rem;
+  margin-right: -2rem;
+}
+
+.-my-10 {
+  margin-top: -2.5rem;
+  margin-bottom: -2.5rem;
+}
+
+.-mx-10 {
+  margin-left: -2.5rem;
+  margin-right: -2.5rem;
+}
+
+.-my-12 {
+  margin-top: -3rem;
+  margin-bottom: -3rem;
+}
+
+.-mx-12 {
+  margin-left: -3rem;
+  margin-right: -3rem;
+}
+
+.-my-16 {
+  margin-top: -4rem;
+  margin-bottom: -4rem;
+}
+
+.-mx-16 {
+  margin-left: -4rem;
+  margin-right: -4rem;
+}
+
+.-my-20 {
+  margin-top: -5rem;
+  margin-bottom: -5rem;
+}
+
+.-mx-20 {
+  margin-left: -5rem;
+  margin-right: -5rem;
+}
+
+.-my-24 {
+  margin-top: -6rem;
+  margin-bottom: -6rem;
+}
+
+.-mx-24 {
+  margin-left: -6rem;
+  margin-right: -6rem;
+}
+
+.-my-32 {
+  margin-top: -8rem;
+  margin-bottom: -8rem;
+}
+
+.-mx-32 {
+  margin-left: -8rem;
+  margin-right: -8rem;
+}
+
+.-my-40 {
+  margin-top: -10rem;
+  margin-bottom: -10rem;
+}
+
+.-mx-40 {
+  margin-left: -10rem;
+  margin-right: -10rem;
+}
+
+.-my-48 {
+  margin-top: -12rem;
+  margin-bottom: -12rem;
+}
+
+.-mx-48 {
+  margin-left: -12rem;
+  margin-right: -12rem;
+}
+
+.-my-56 {
+  margin-top: -14rem;
+  margin-bottom: -14rem;
+}
+
+.-mx-56 {
+  margin-left: -14rem;
+  margin-right: -14rem;
+}
+
+.-my-64 {
+  margin-top: -16rem;
+  margin-bottom: -16rem;
+}
+
+.-mx-64 {
+  margin-left: -16rem;
+  margin-right: -16rem;
+}
+
+.-my-px {
+  margin-top: -1px;
+  margin-bottom: -1px;
+}
+
+.-mx-px {
+  margin-left: -1px;
+  margin-right: -1px;
+}
+
+.mt-0 {
+  margin-top: 0;
+}
+
+.mr-0 {
+  margin-right: 0;
+}
+
+.mb-0 {
+  margin-bottom: 0;
+}
+
+.ml-0 {
+  margin-left: 0;
+}
+
+.mt-1 {
+  margin-top: 0.25rem;
+}
+
+.mr-1 {
+  margin-right: 0.25rem;
+}
+
+.mb-1 {
+  margin-bottom: 0.25rem;
+}
+
+.ml-1 {
+  margin-left: 0.25rem;
+}
+
+.mt-2 {
+  margin-top: 0.5rem;
+}
+
+.mr-2 {
+  margin-right: 0.5rem;
+}
+
+.mb-2 {
+  margin-bottom: 0.5rem;
+}
+
+.ml-2 {
+  margin-left: 0.5rem;
+}
+
+.mt-3 {
+  margin-top: 0.75rem;
+}
+
+.mr-3 {
+  margin-right: 0.75rem;
+}
+
+.mb-3 {
+  margin-bottom: 0.75rem;
+}
+
+.ml-3 {
+  margin-left: 0.75rem;
+}
+
+.mt-4 {
+  margin-top: 1rem;
+}
+
+.mr-4 {
+  margin-right: 1rem;
+}
+
+.mb-4 {
+  margin-bottom: 1rem;
+}
+
+.ml-4 {
+  margin-left: 1rem;
+}
+
+.mt-5 {
+  margin-top: 1.25rem;
+}
+
+.mr-5 {
+  margin-right: 1.25rem;
+}
+
+.mb-5 {
+  margin-bottom: 1.25rem;
+}
+
+.ml-5 {
+  margin-left: 1.25rem;
+}
+
+.mt-6 {
+  margin-top: 1.5rem;
+}
+
+.mr-6 {
+  margin-right: 1.5rem;
+}
+
+.mb-6 {
+  margin-bottom: 1.5rem;
+}
+
+.ml-6 {
+  margin-left: 1.5rem;
+}
+
+.mt-8 {
+  margin-top: 2rem;
+}
+
+.mr-8 {
+  margin-right: 2rem;
+}
+
+.mb-8 {
+  margin-bottom: 2rem;
+}
+
+.ml-8 {
+  margin-left: 2rem;
+}
+
+.mt-10 {
+  margin-top: 2.5rem;
+}
+
+.mr-10 {
+  margin-right: 2.5rem;
+}
+
+.mb-10 {
+  margin-bottom: 2.5rem;
+}
+
+.ml-10 {
+  margin-left: 2.5rem;
+}
+
+.mt-12 {
+  margin-top: 3rem;
+}
+
+.mr-12 {
+  margin-right: 3rem;
+}
+
+.mb-12 {
+  margin-bottom: 3rem;
+}
+
+.ml-12 {
+  margin-left: 3rem;
+}
+
+.mt-16 {
+  margin-top: 4rem;
+}
+
+.mr-16 {
+  margin-right: 4rem;
+}
+
+.mb-16 {
+  margin-bottom: 4rem;
+}
+
+.ml-16 {
+  margin-left: 4rem;
+}
+
+.mt-20 {
+  margin-top: 5rem;
+}
+
+.mr-20 {
+  margin-right: 5rem;
+}
+
+.mb-20 {
+  margin-bottom: 5rem;
+}
+
+.ml-20 {
+  margin-left: 5rem;
+}
+
+.mt-24 {
+  margin-top: 6rem;
+}
+
+.mr-24 {
+  margin-right: 6rem;
+}
+
+.mb-24 {
+  margin-bottom: 6rem;
+}
+
+.ml-24 {
+  margin-left: 6rem;
+}
+
+.mt-32 {
+  margin-top: 8rem;
+}
+
+.mr-32 {
+  margin-right: 8rem;
+}
+
+.mb-32 {
+  margin-bottom: 8rem;
+}
+
+.ml-32 {
+  margin-left: 8rem;
+}
+
+.mt-40 {
+  margin-top: 10rem;
+}
+
+.mr-40 {
+  margin-right: 10rem;
+}
+
+.mb-40 {
+  margin-bottom: 10rem;
+}
+
+.ml-40 {
+  margin-left: 10rem;
+}
+
+.mt-48 {
+  margin-top: 12rem;
+}
+
+.mr-48 {
+  margin-right: 12rem;
+}
+
+.mb-48 {
+  margin-bottom: 12rem;
+}
+
+.ml-48 {
+  margin-left: 12rem;
+}
+
+.mt-56 {
+  margin-top: 14rem;
+}
+
+.mr-56 {
+  margin-right: 14rem;
+}
+
+.mb-56 {
+  margin-bottom: 14rem;
+}
+
+.ml-56 {
+  margin-left: 14rem;
+}
+
+.mt-64 {
+  margin-top: 16rem;
+}
+
+.mr-64 {
+  margin-right: 16rem;
+}
+
+.mb-64 {
+  margin-bottom: 16rem;
+}
+
+.ml-64 {
+  margin-left: 16rem;
+}
+
+.mt-auto {
+  margin-top: auto;
+}
+
+.mr-auto {
+  margin-right: auto;
+}
+
+.mb-auto {
+  margin-bottom: auto;
+}
+
+.ml-auto {
+  margin-left: auto;
+}
+
+.mt-px {
+  margin-top: 1px;
+}
+
+.mr-px {
+  margin-right: 1px;
+}
+
+.mb-px {
+  margin-bottom: 1px;
+}
+
+.ml-px {
+  margin-left: 1px;
+}
+
+.-mt-1 {
+  margin-top: -0.25rem;
+}
+
+.-mr-1 {
+  margin-right: -0.25rem;
+}
+
+.-mb-1 {
+  margin-bottom: -0.25rem;
+}
+
+.-ml-1 {
+  margin-left: -0.25rem;
+}
+
+.-mt-2 {
+  margin-top: -0.5rem;
+}
+
+.-mr-2 {
+  margin-right: -0.5rem;
+}
+
+.-mb-2 {
+  margin-bottom: -0.5rem;
+}
+
+.-ml-2 {
+  margin-left: -0.5rem;
+}
+
+.-mt-3 {
+  margin-top: -0.75rem;
+}
+
+.-mr-3 {
+  margin-right: -0.75rem;
+}
+
+.-mb-3 {
+  margin-bottom: -0.75rem;
+}
+
+.-ml-3 {
+  margin-left: -0.75rem;
+}
+
+.-mt-4 {
+  margin-top: -1rem;
+}
+
+.-mr-4 {
+  margin-right: -1rem;
+}
+
+.-mb-4 {
+  margin-bottom: -1rem;
+}
+
+.-ml-4 {
+  margin-left: -1rem;
+}
+
+.-mt-5 {
+  margin-top: -1.25rem;
+}
+
+.-mr-5 {
+  margin-right: -1.25rem;
+}
+
+.-mb-5 {
+  margin-bottom: -1.25rem;
+}
+
+.-ml-5 {
+  margin-left: -1.25rem;
+}
+
+.-mt-6 {
+  margin-top: -1.5rem;
+}
+
+.-mr-6 {
+  margin-right: -1.5rem;
+}
+
+.-mb-6 {
+  margin-bottom: -1.5rem;
+}
+
+.-ml-6 {
+  margin-left: -1.5rem;
+}
+
+.-mt-8 {
+  margin-top: -2rem;
+}
+
+.-mr-8 {
+  margin-right: -2rem;
+}
+
+.-mb-8 {
+  margin-bottom: -2rem;
+}
+
+.-ml-8 {
+  margin-left: -2rem;
+}
+
+.-mt-10 {
+  margin-top: -2.5rem;
+}
+
+.-mr-10 {
+  margin-right: -2.5rem;
+}
+
+.-mb-10 {
+  margin-bottom: -2.5rem;
+}
+
+.-ml-10 {
+  margin-left: -2.5rem;
+}
+
+.-mt-12 {
+  margin-top: -3rem;
+}
+
+.-mr-12 {
+  margin-right: -3rem;
+}
+
+.-mb-12 {
+  margin-bottom: -3rem;
+}
+
+.-ml-12 {
+  margin-left: -3rem;
+}
+
+.-mt-16 {
+  margin-top: -4rem;
+}
+
+.-mr-16 {
+  margin-right: -4rem;
+}
+
+.-mb-16 {
+  margin-bottom: -4rem;
+}
+
+.-ml-16 {
+  margin-left: -4rem;
+}
+
+.-mt-20 {
+  margin-top: -5rem;
+}
+
+.-mr-20 {
+  margin-right: -5rem;
+}
+
+.-mb-20 {
+  margin-bottom: -5rem;
+}
+
+.-ml-20 {
+  margin-left: -5rem;
+}
+
+.-mt-24 {
+  margin-top: -6rem;
+}
+
+.-mr-24 {
+  margin-right: -6rem;
+}
+
+.-mb-24 {
+  margin-bottom: -6rem;
+}
+
+.-ml-24 {
+  margin-left: -6rem;
+}
+
+.-mt-32 {
+  margin-top: -8rem;
+}
+
+.-mr-32 {
+  margin-right: -8rem;
+}
+
+.-mb-32 {
+  margin-bottom: -8rem;
+}
+
+.-ml-32 {
+  margin-left: -8rem;
+}
+
+.-mt-40 {
+  margin-top: -10rem;
+}
+
+.-mr-40 {
+  margin-right: -10rem;
+}
+
+.-mb-40 {
+  margin-bottom: -10rem;
+}
+
+.-ml-40 {
+  margin-left: -10rem;
+}
+
+.-mt-48 {
+  margin-top: -12rem;
+}
+
+.-mr-48 {
+  margin-right: -12rem;
+}
+
+.-mb-48 {
+  margin-bottom: -12rem;
+}
+
+.-ml-48 {
+  margin-left: -12rem;
+}
+
+.-mt-56 {
+  margin-top: -14rem;
+}
+
+.-mr-56 {
+  margin-right: -14rem;
+}
+
+.-mb-56 {
+  margin-bottom: -14rem;
+}
+
+.-ml-56 {
+  margin-left: -14rem;
+}
+
+.-mt-64 {
+  margin-top: -16rem;
+}
+
+.-mr-64 {
+  margin-right: -16rem;
+}
+
+.-mb-64 {
+  margin-bottom: -16rem;
+}
+
+.-ml-64 {
+  margin-left: -16rem;
+}
+
+.-mt-px {
+  margin-top: -1px;
+}
+
+.-mr-px {
+  margin-right: -1px;
+}
+
+.-mb-px {
+  margin-bottom: -1px;
+}
+
+.-ml-px {
+  margin-left: -1px;
+}
+
+.max-h-full {
+  max-height: 100%;
+}
+
+.max-h-screen {
+  max-height: 100vh;
+}
+
+.max-w-none {
+  max-width: none;
+}
+
+.max-w-xs {
+  max-width: 20rem;
+}
+
+.max-w-sm {
+  max-width: 24rem;
+}
+
+.max-w-md {
+  max-width: 28rem;
+}
+
+.max-w-lg {
+  max-width: 32rem;
+}
+
+.max-w-xl {
+  max-width: 36rem;
+}
+
+.max-w-2xl {
+  max-width: 42rem;
+}
+
+.max-w-3xl {
+  max-width: 48rem;
+}
+
+.max-w-4xl {
+  max-width: 56rem;
+}
+
+.max-w-5xl {
+  max-width: 64rem;
+}
+
+.max-w-6xl {
+  max-width: 72rem;
+}
+
+.max-w-full {
+  max-width: 100%;
+}
+
+.max-w-screen-sm {
+  max-width: 640px;
+}
+
+.max-w-screen-md {
+  max-width: 768px;
+}
+
+.max-w-screen-lg {
+  max-width: 1024px;
+}
+
+.max-w-screen-xl {
+  max-width: 1280px;
+}
+
+.min-h-0 {
+  min-height: 0;
+}
+
+.min-h-full {
+  min-height: 100%;
+}
+
+.min-h-screen {
+  min-height: 100vh;
+}
+
+.min-w-0 {
+  min-width: 0;
+}
+
+.min-w-full {
+  min-width: 100%;
+}
+
+.object-contain {
+  -o-object-fit: contain;
+     object-fit: contain;
+}
+
+.object-cover {
+  -o-object-fit: cover;
+     object-fit: cover;
+}
+
+.object-fill {
+  -o-object-fit: fill;
+     object-fit: fill;
+}
+
+.object-none {
+  -o-object-fit: none;
+     object-fit: none;
+}
+
+.object-scale-down {
+  -o-object-fit: scale-down;
+     object-fit: scale-down;
+}
+
+.object-bottom {
+  -o-object-position: bottom;
+     object-position: bottom;
+}
+
+.object-center {
+  -o-object-position: center;
+     object-position: center;
+}
+
+.object-left {
+  -o-object-position: left;
+     object-position: left;
+}
+
+.object-left-bottom {
+  -o-object-position: left bottom;
+     object-position: left bottom;
+}
+
+.object-left-top {
+  -o-object-position: left top;
+     object-position: left top;
+}
+
+.object-right {
+  -o-object-position: right;
+     object-position: right;
+}
+
+.object-right-bottom {
+  -o-object-position: right bottom;
+     object-position: right bottom;
+}
+
+.object-right-top {
+  -o-object-position: right top;
+     object-position: right top;
+}
+
+.object-top {
+  -o-object-position: top;
+     object-position: top;
+}
+
+.opacity-0 {
+  opacity: 0;
+}
+
+.opacity-25 {
+  opacity: 0.25;
+}
+
+.opacity-50 {
+  opacity: 0.5;
+}
+
+.opacity-75 {
+  opacity: 0.75;
+}
+
+.opacity-100 {
+  opacity: 1;
+}
+
+.hover\:opacity-0:hover {
+  opacity: 0;
+}
+
+.hover\:opacity-25:hover {
+  opacity: 0.25;
+}
+
+.hover\:opacity-50:hover {
+  opacity: 0.5;
+}
+
+.hover\:opacity-75:hover {
+  opacity: 0.75;
+}
+
+.hover\:opacity-100:hover {
+  opacity: 1;
+}
+
+.focus\:opacity-0:focus {
+  opacity: 0;
+}
+
+.focus\:opacity-25:focus {
+  opacity: 0.25;
+}
+
+.focus\:opacity-50:focus {
+  opacity: 0.5;
+}
+
+.focus\:opacity-75:focus {
+  opacity: 0.75;
+}
+
+.focus\:opacity-100:focus {
+  opacity: 1;
+}
+
+.outline-none {
+  outline: 0;
+}
+
+.focus\:outline-none:focus {
+  outline: 0;
+}
+
+.overflow-auto {
+  overflow: auto;
+}
+
+.overflow-hidden {
+  overflow: hidden;
+}
+
+.overflow-visible {
+  overflow: visible;
+}
+
+.overflow-scroll {
+  overflow: scroll;
+}
+
+.overflow-x-auto {
+  overflow-x: auto;
+}
+
+.overflow-y-auto {
+  overflow-y: auto;
+}
+
+.overflow-x-hidden {
+  overflow-x: hidden;
+}
+
+.overflow-y-hidden {
+  overflow-y: hidden;
+}
+
+.overflow-x-visible {
+  overflow-x: visible;
+}
+
+.overflow-y-visible {
+  overflow-y: visible;
+}
+
+.overflow-x-scroll {
+  overflow-x: scroll;
+}
+
+.overflow-y-scroll {
+  overflow-y: scroll;
+}
+
+.scrolling-touch {
+  -webkit-overflow-scrolling: touch;
+}
+
+.scrolling-auto {
+  -webkit-overflow-scrolling: auto;
+}
+
+.overscroll-auto {
+  -ms-scroll-chaining: chained;
+      overscroll-behavior: auto;
+}
+
+.overscroll-contain {
+  -ms-scroll-chaining: none;
+      overscroll-behavior: contain;
+}
+
+.overscroll-none {
+  -ms-scroll-chaining: none;
+      overscroll-behavior: none;
+}
+
+.overscroll-y-auto {
+  overscroll-behavior-y: auto;
+}
+
+.overscroll-y-contain {
+  overscroll-behavior-y: contain;
+}
+
+.overscroll-y-none {
+  overscroll-behavior-y: none;
+}
+
+.overscroll-x-auto {
+  overscroll-behavior-x: auto;
+}
+
+.overscroll-x-contain {
+  overscroll-behavior-x: contain;
+}
+
+.overscroll-x-none {
+  overscroll-behavior-x: none;
+}
+
+.p-0 {
+  padding: 0;
+}
+
+.p-1 {
+  padding: 0.25rem;
+}
+
+.p-2 {
+  padding: 0.5rem;
+}
+
+.p-3 {
+  padding: 0.75rem;
+}
+
+.p-4 {
+  padding: 1rem;
+}
+
+.p-5 {
+  padding: 1.25rem;
+}
+
+.p-6 {
+  padding: 1.5rem;
+}
+
+.p-8 {
+  padding: 2rem;
+}
+
+.p-10 {
+  padding: 2.5rem;
+}
+
+.p-12 {
+  padding: 3rem;
+}
+
+.p-16 {
+  padding: 4rem;
+}
+
+.p-20 {
+  padding: 5rem;
+}
+
+.p-24 {
+  padding: 6rem;
+}
+
+.p-32 {
+  padding: 8rem;
+}
+
+.p-40 {
+  padding: 10rem;
+}
+
+.p-48 {
+  padding: 12rem;
+}
+
+.p-56 {
+  padding: 14rem;
+}
+
+.p-64 {
+  padding: 16rem;
+}
+
+.p-px {
+  padding: 1px;
+}
+
+.py-0 {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.px-0 {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.py-1 {
+  padding-top: 0.25rem;
+  padding-bottom: 0.25rem;
+}
+
+.px-1 {
+  padding-left: 0.25rem;
+  padding-right: 0.25rem;
+}
+
+.py-2 {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.px-2 {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+
+.py-3 {
+  padding-top: 0.75rem;
+  padding-bottom: 0.75rem;
+}
+
+.px-3 {
+  padding-left: 0.75rem;
+  padding-right: 0.75rem;
+}
+
+.py-4 {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.px-4 {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+
+.py-5 {
+  padding-top: 1.25rem;
+  padding-bottom: 1.25rem;
+}
+
+.px-5 {
+  padding-left: 1.25rem;
+  padding-right: 1.25rem;
+}
+
+.py-6 {
+  padding-top: 1.5rem;
+  padding-bottom: 1.5rem;
+}
+
+.px-6 {
+  padding-left: 1.5rem;
+  padding-right: 1.5rem;
+}
+
+.py-8 {
+  padding-top: 2rem;
+  padding-bottom: 2rem;
+}
+
+.px-8 {
+  padding-left: 2rem;
+  padding-right: 2rem;
+}
+
+.py-10 {
+  padding-top: 2.5rem;
+  padding-bottom: 2.5rem;
+}
+
+.px-10 {
+  padding-left: 2.5rem;
+  padding-right: 2.5rem;
+}
+
+.py-12 {
+  padding-top: 3rem;
+  padding-bottom: 3rem;
+}
+
+.px-12 {
+  padding-left: 3rem;
+  padding-right: 3rem;
+}
+
+.py-16 {
+  padding-top: 4rem;
+  padding-bottom: 4rem;
+}
+
+.px-16 {
+  padding-left: 4rem;
+  padding-right: 4rem;
+}
+
+.py-20 {
+  padding-top: 5rem;
+  padding-bottom: 5rem;
+}
+
+.px-20 {
+  padding-left: 5rem;
+  padding-right: 5rem;
+}
+
+.py-24 {
+  padding-top: 6rem;
+  padding-bottom: 6rem;
+}
+
+.px-24 {
+  padding-left: 6rem;
+  padding-right: 6rem;
+}
+
+.py-32 {
+  padding-top: 8rem;
+  padding-bottom: 8rem;
+}
+
+.px-32 {
+  padding-left: 8rem;
+  padding-right: 8rem;
+}
+
+.py-40 {
+  padding-top: 10rem;
+  padding-bottom: 10rem;
+}
+
+.px-40 {
+  padding-left: 10rem;
+  padding-right: 10rem;
+}
+
+.py-48 {
+  padding-top: 12rem;
+  padding-bottom: 12rem;
+}
+
+.px-48 {
+  padding-left: 12rem;
+  padding-right: 12rem;
+}
+
+.py-56 {
+  padding-top: 14rem;
+  padding-bottom: 14rem;
+}
+
+.px-56 {
+  padding-left: 14rem;
+  padding-right: 14rem;
+}
+
+.py-64 {
+  padding-top: 16rem;
+  padding-bottom: 16rem;
+}
+
+.px-64 {
+  padding-left: 16rem;
+  padding-right: 16rem;
+}
+
+.py-px {
+  padding-top: 1px;
+  padding-bottom: 1px;
+}
+
+.px-px {
+  padding-left: 1px;
+  padding-right: 1px;
+}
+
+.pt-0 {
+  padding-top: 0;
+}
+
+.pr-0 {
+  padding-right: 0;
+}
+
+.pb-0 {
+  padding-bottom: 0;
+}
+
+.pl-0 {
+  padding-left: 0;
+}
+
+.pt-1 {
+  padding-top: 0.25rem;
+}
+
+.pr-1 {
+  padding-right: 0.25rem;
+}
+
+.pb-1 {
+  padding-bottom: 0.25rem;
+}
+
+.pl-1 {
+  padding-left: 0.25rem;
+}
+
+.pt-2 {
+  padding-top: 0.5rem;
+}
+
+.pr-2 {
+  padding-right: 0.5rem;
+}
+
+.pb-2 {
+  padding-bottom: 0.5rem;
+}
+
+.pl-2 {
+  padding-left: 0.5rem;
+}
+
+.pt-3 {
+  padding-top: 0.75rem;
+}
+
+.pr-3 {
+  padding-right: 0.75rem;
+}
+
+.pb-3 {
+  padding-bottom: 0.75rem;
+}
+
+.pl-3 {
+  padding-left: 0.75rem;
+}
+
+.pt-4 {
+  padding-top: 1rem;
+}
+
+.pr-4 {
+  padding-right: 1rem;
+}
+
+.pb-4 {
+  padding-bottom: 1rem;
+}
+
+.pl-4 {
+  padding-left: 1rem;
+}
+
+.pt-5 {
+  padding-top: 1.25rem;
+}
+
+.pr-5 {
+  padding-right: 1.25rem;
+}
+
+.pb-5 {
+  padding-bottom: 1.25rem;
+}
+
+.pl-5 {
+  padding-left: 1.25rem;
+}
+
+.pt-6 {
+  padding-top: 1.5rem;
+}
+
+.pr-6 {
+  padding-right: 1.5rem;
+}
+
+.pb-6 {
+  padding-bottom: 1.5rem;
+}
+
+.pl-6 {
+  padding-left: 1.5rem;
+}
+
+.pt-8 {
+  padding-top: 2rem;
+}
+
+.pr-8 {
+  padding-right: 2rem;
+}
+
+.pb-8 {
+  padding-bottom: 2rem;
+}
+
+.pl-8 {
+  padding-left: 2rem;
+}
+
+.pt-10 {
+  padding-top: 2.5rem;
+}
+
+.pr-10 {
+  padding-right: 2.5rem;
+}
+
+.pb-10 {
+  padding-bottom: 2.5rem;
+}
+
+.pl-10 {
+  padding-left: 2.5rem;
+}
+
+.pt-12 {
+  padding-top: 3rem;
+}
+
+.pr-12 {
+  padding-right: 3rem;
+}
+
+.pb-12 {
+  padding-bottom: 3rem;
+}
+
+.pl-12 {
+  padding-left: 3rem;
+}
+
+.pt-16 {
+  padding-top: 4rem;
+}
+
+.pr-16 {
+  padding-right: 4rem;
+}
+
+.pb-16 {
+  padding-bottom: 4rem;
+}
+
+.pl-16 {
+  padding-left: 4rem;
+}
+
+.pt-20 {
+  padding-top: 5rem;
+}
+
+.pr-20 {
+  padding-right: 5rem;
+}
+
+.pb-20 {
+  padding-bottom: 5rem;
+}
+
+.pl-20 {
+  padding-left: 5rem;
+}
+
+.pt-24 {
+  padding-top: 6rem;
+}
+
+.pr-24 {
+  padding-right: 6rem;
+}
+
+.pb-24 {
+  padding-bottom: 6rem;
+}
+
+.pl-24 {
+  padding-left: 6rem;
+}
+
+.pt-32 {
+  padding-top: 8rem;
+}
+
+.pr-32 {
+  padding-right: 8rem;
+}
+
+.pb-32 {
+  padding-bottom: 8rem;
+}
+
+.pl-32 {
+  padding-left: 8rem;
+}
+
+.pt-40 {
+  padding-top: 10rem;
+}
+
+.pr-40 {
+  padding-right: 10rem;
+}
+
+.pb-40 {
+  padding-bottom: 10rem;
+}
+
+.pl-40 {
+  padding-left: 10rem;
+}
+
+.pt-48 {
+  padding-top: 12rem;
+}
+
+.pr-48 {
+  padding-right: 12rem;
+}
+
+.pb-48 {
+  padding-bottom: 12rem;
+}
+
+.pl-48 {
+  padding-left: 12rem;
+}
+
+.pt-56 {
+  padding-top: 14rem;
+}
+
+.pr-56 {
+  padding-right: 14rem;
+}
+
+.pb-56 {
+  padding-bottom: 14rem;
+}
+
+.pl-56 {
+  padding-left: 14rem;
+}
+
+.pt-64 {
+  padding-top: 16rem;
+}
+
+.pr-64 {
+  padding-right: 16rem;
+}
+
+.pb-64 {
+  padding-bottom: 16rem;
+}
+
+.pl-64 {
+  padding-left: 16rem;
+}
+
+.pt-px {
+  padding-top: 1px;
+}
+
+.pr-px {
+  padding-right: 1px;
+}
+
+.pb-px {
+  padding-bottom: 1px;
+}
+
+.pl-px {
+  padding-left: 1px;
+}
+
+.placeholder-transparent::-moz-placeholder {
+  color: transparent;
+}
+
+.placeholder-transparent:-ms-input-placeholder {
+  color: transparent;
+}
+
+.placeholder-transparent::placeholder {
+  color: transparent;
+}
+
+.placeholder-current::-moz-placeholder {
+  color: currentColor;
+}
+
+.placeholder-current:-ms-input-placeholder {
+  color: currentColor;
+}
+
+.placeholder-current::placeholder {
+  color: currentColor;
+}
+
+.placeholder-black::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.placeholder-black:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.placeholder-black::placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.placeholder-white::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.placeholder-white:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.placeholder-white::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.placeholder-gray-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.placeholder-gray-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.placeholder-gray-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.placeholder-gray-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.placeholder-gray-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.placeholder-gray-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.placeholder-gray-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.placeholder-gray-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.placeholder-gray-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.placeholder-gray-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.placeholder-gray-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.placeholder-gray-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.placeholder-gray-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.placeholder-gray-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.placeholder-gray-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.placeholder-gray-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.placeholder-gray-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.placeholder-gray-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.placeholder-gray-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.placeholder-gray-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.placeholder-gray-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.placeholder-gray-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.placeholder-gray-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.placeholder-gray-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.placeholder-gray-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.placeholder-gray-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.placeholder-gray-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.placeholder-red-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.placeholder-red-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.placeholder-red-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.placeholder-red-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.placeholder-red-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.placeholder-red-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.placeholder-red-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.placeholder-red-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.placeholder-red-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.placeholder-red-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.placeholder-red-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.placeholder-red-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.placeholder-red-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.placeholder-red-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.placeholder-red-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.placeholder-red-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.placeholder-red-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.placeholder-red-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.placeholder-red-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.placeholder-red-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.placeholder-red-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.placeholder-red-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.placeholder-red-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.placeholder-orange-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.placeholder-orange-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.placeholder-orange-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.placeholder-orange-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.placeholder-orange-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.placeholder-orange-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.placeholder-orange-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.placeholder-orange-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.placeholder-orange-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.placeholder-orange-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.placeholder-orange-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.placeholder-orange-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.placeholder-orange-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.placeholder-orange-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.placeholder-orange-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.placeholder-orange-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.placeholder-orange-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.placeholder-orange-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.placeholder-orange-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.placeholder-orange-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.placeholder-orange-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.placeholder-green-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.placeholder-green-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.placeholder-green-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.placeholder-green-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.placeholder-green-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.placeholder-green-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.placeholder-green-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.placeholder-green-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.placeholder-green-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.placeholder-green-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.placeholder-green-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.placeholder-green-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.placeholder-green-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.placeholder-green-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.placeholder-green-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.placeholder-green-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.placeholder-green-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.placeholder-green-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.placeholder-green-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.placeholder-green-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.placeholder-green-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.placeholder-green-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.placeholder-green-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.placeholder-green-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.placeholder-green-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.placeholder-green-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.placeholder-green-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.placeholder-teal-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.placeholder-teal-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.placeholder-teal-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.placeholder-teal-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.placeholder-teal-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.placeholder-teal-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.placeholder-teal-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.placeholder-teal-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.placeholder-teal-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.placeholder-teal-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.placeholder-teal-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.placeholder-teal-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.placeholder-teal-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.placeholder-teal-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.placeholder-teal-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.placeholder-teal-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.placeholder-teal-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.placeholder-teal-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.placeholder-teal-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.placeholder-teal-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.placeholder-teal-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.placeholder-teal-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.placeholder-teal-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.placeholder-teal-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.placeholder-teal-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.placeholder-teal-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.placeholder-teal-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.placeholder-blue-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.placeholder-blue-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.placeholder-blue-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.placeholder-blue-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.placeholder-blue-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.placeholder-blue-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.placeholder-blue-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.placeholder-blue-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.placeholder-blue-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.placeholder-blue-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.placeholder-blue-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.placeholder-blue-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.placeholder-blue-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.placeholder-blue-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.placeholder-blue-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.placeholder-blue-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.placeholder-blue-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.placeholder-blue-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.placeholder-blue-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.placeholder-blue-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.placeholder-blue-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.placeholder-blue-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.placeholder-blue-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.placeholder-blue-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.placeholder-blue-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.placeholder-blue-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.placeholder-blue-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.placeholder-purple-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.placeholder-purple-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.placeholder-purple-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.placeholder-purple-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.placeholder-purple-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.placeholder-purple-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.placeholder-purple-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.placeholder-purple-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.placeholder-purple-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.placeholder-purple-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.placeholder-purple-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.placeholder-purple-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.placeholder-purple-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.placeholder-purple-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.placeholder-purple-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.placeholder-purple-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.placeholder-purple-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.placeholder-purple-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.placeholder-purple-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.placeholder-purple-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.placeholder-purple-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.placeholder-purple-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.placeholder-purple-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.placeholder-purple-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.placeholder-purple-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.placeholder-purple-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.placeholder-purple-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.placeholder-pink-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.placeholder-pink-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.placeholder-pink-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.placeholder-pink-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.placeholder-pink-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.placeholder-pink-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.placeholder-pink-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.placeholder-pink-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.placeholder-pink-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.placeholder-pink-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.placeholder-pink-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.placeholder-pink-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.placeholder-pink-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.placeholder-pink-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.placeholder-pink-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.placeholder-pink-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.placeholder-pink-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.placeholder-pink-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.placeholder-pink-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.placeholder-pink-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.placeholder-pink-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.placeholder-pink-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.placeholder-pink-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.placeholder-pink-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.placeholder-pink-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.placeholder-pink-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.placeholder-pink-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-transparent:focus::-moz-placeholder {
+  color: transparent;
+}
+
+.focus\:placeholder-transparent:focus:-ms-input-placeholder {
+  color: transparent;
+}
+
+.focus\:placeholder-transparent:focus::placeholder {
+  color: transparent;
+}
+
+.focus\:placeholder-current:focus::-moz-placeholder {
+  color: currentColor;
+}
+
+.focus\:placeholder-current:focus:-ms-input-placeholder {
+  color: currentColor;
+}
+
+.focus\:placeholder-current:focus::placeholder {
+  color: currentColor;
+}
+
+.focus\:placeholder-black:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-black:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-black:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-white:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-white:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-white:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.placeholder-opacity-0::-moz-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.placeholder-opacity-0:-ms-input-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.placeholder-opacity-0::placeholder {
+  --placeholder-opacity: 0;
+}
+
+.placeholder-opacity-25::-moz-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.placeholder-opacity-25:-ms-input-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.placeholder-opacity-25::placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.placeholder-opacity-50::-moz-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.placeholder-opacity-50:-ms-input-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.placeholder-opacity-50::placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.placeholder-opacity-75::-moz-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.placeholder-opacity-75:-ms-input-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.placeholder-opacity-75::placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.placeholder-opacity-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.placeholder-opacity-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.placeholder-opacity-100::placeholder {
+  --placeholder-opacity: 1;
+}
+
+.focus\:placeholder-opacity-0:focus::-moz-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.focus\:placeholder-opacity-0:focus::placeholder {
+  --placeholder-opacity: 0;
+}
+
+.focus\:placeholder-opacity-25:focus::-moz-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.focus\:placeholder-opacity-25:focus::placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.focus\:placeholder-opacity-50:focus::-moz-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.focus\:placeholder-opacity-50:focus::placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.focus\:placeholder-opacity-75:focus::-moz-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.focus\:placeholder-opacity-75:focus::placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.focus\:placeholder-opacity-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.focus\:placeholder-opacity-100:focus::placeholder {
+  --placeholder-opacity: 1;
+}
+
+.pointer-events-none {
+  pointer-events: none;
+}
+
+.pointer-events-auto {
+  pointer-events: auto;
+}
+
+.static {
+  position: static;
+}
+
+.fixed {
+  position: fixed;
+}
+
+.absolute {
+  position: absolute;
+}
+
+.relative {
+  position: relative;
+}
+
+.sticky {
+  position: -webkit-sticky;
+  position: sticky;
+}
+
+.inset-0 {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.inset-auto {
+  top: auto;
+  right: auto;
+  bottom: auto;
+  left: auto;
+}
+
+.inset-y-0 {
+  top: 0;
+  bottom: 0;
+}
+
+.inset-x-0 {
+  right: 0;
+  left: 0;
+}
+
+.inset-y-auto {
+  top: auto;
+  bottom: auto;
+}
+
+.inset-x-auto {
+  right: auto;
+  left: auto;
+}
+
+.top-0 {
+  top: 0;
+}
+
+.right-0 {
+  right: 0;
+}
+
+.bottom-0 {
+  bottom: 0;
+}
+
+.left-0 {
+  left: 0;
+}
+
+.top-auto {
+  top: auto;
+}
+
+.right-auto {
+  right: auto;
+}
+
+.bottom-auto {
+  bottom: auto;
+}
+
+.left-auto {
+  left: auto;
+}
+
+.resize-none {
+  resize: none;
+}
+
+.resize-y {
+  resize: vertical;
+}
+
+.resize-x {
+  resize: horizontal;
+}
+
+.resize {
+  resize: both;
+}
+
+.shadow-xs {
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.shadow-sm {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.shadow {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.shadow-md {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.shadow-lg {
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.shadow-xl {
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.shadow-2xl {
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.shadow-inner {
+  box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+}
+
+.shadow-outline {
+  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+}
+
+.shadow-none {
+  box-shadow: none;
+}
+
+.hover\:shadow-xs:hover {
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.hover\:shadow-sm:hover {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.hover\:shadow:hover {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.hover\:shadow-md:hover {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.hover\:shadow-lg:hover {
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.hover\:shadow-xl:hover {
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.hover\:shadow-2xl:hover {
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.hover\:shadow-inner:hover {
+  box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+}
+
+.hover\:shadow-outline:hover {
+  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+}
+
+.hover\:shadow-none:hover {
+  box-shadow: none;
+}
+
+.focus\:shadow-xs:focus {
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.focus\:shadow-sm:focus {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.focus\:shadow:focus {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.focus\:shadow-md:focus {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.focus\:shadow-lg:focus {
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.focus\:shadow-xl:focus {
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.focus\:shadow-2xl:focus {
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.focus\:shadow-inner:focus {
+  box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+}
+
+.focus\:shadow-outline:focus {
+  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+}
+
+.focus\:shadow-none:focus {
+  box-shadow: none;
+}
+
+.fill-current {
+  fill: currentColor;
+}
+
+.stroke-current {
+  stroke: currentColor;
+}
+
+.stroke-0 {
+  stroke-width: 0;
+}
+
+.stroke-1 {
+  stroke-width: 1;
+}
+
+.stroke-2 {
+  stroke-width: 2;
+}
+
+.table-auto {
+  table-layout: auto;
+}
+
+.table-fixed {
+  table-layout: fixed;
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.text-justify {
+  text-align: justify;
+}
+
+.text-transparent {
+  color: transparent;
+}
+
+.text-current {
+  color: currentColor;
+}
+
+.text-black {
+  --text-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--text-opacity));
+}
+
+.text-white {
+  --text-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--text-opacity));
+}
+
+.text-gray-100 {
+  --text-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--text-opacity));
+}
+
+.text-gray-200 {
+  --text-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--text-opacity));
+}
+
+.text-gray-300 {
+  --text-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--text-opacity));
+}
+
+.text-gray-400 {
+  --text-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--text-opacity));
+}
+
+.text-gray-500 {
+  --text-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--text-opacity));
+}
+
+.text-gray-600 {
+  --text-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--text-opacity));
+}
+
+.text-gray-700 {
+  --text-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--text-opacity));
+}
+
+.text-gray-800 {
+  --text-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--text-opacity));
+}
+
+.text-gray-900 {
+  --text-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--text-opacity));
+}
+
+.text-red-100 {
+  --text-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--text-opacity));
+}
+
+.text-red-200 {
+  --text-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--text-opacity));
+}
+
+.text-red-300 {
+  --text-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--text-opacity));
+}
+
+.text-red-400 {
+  --text-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--text-opacity));
+}
+
+.text-red-500 {
+  --text-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--text-opacity));
+}
+
+.text-red-600 {
+  --text-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--text-opacity));
+}
+
+.text-red-700 {
+  --text-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--text-opacity));
+}
+
+.text-red-800 {
+  --text-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--text-opacity));
+}
+
+.text-red-900 {
+  --text-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--text-opacity));
+}
+
+.text-orange-100 {
+  --text-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--text-opacity));
+}
+
+.text-orange-200 {
+  --text-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--text-opacity));
+}
+
+.text-orange-300 {
+  --text-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--text-opacity));
+}
+
+.text-orange-400 {
+  --text-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--text-opacity));
+}
+
+.text-orange-500 {
+  --text-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--text-opacity));
+}
+
+.text-orange-600 {
+  --text-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--text-opacity));
+}
+
+.text-orange-700 {
+  --text-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--text-opacity));
+}
+
+.text-orange-800 {
+  --text-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--text-opacity));
+}
+
+.text-orange-900 {
+  --text-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--text-opacity));
+}
+
+.text-yellow-100 {
+  --text-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--text-opacity));
+}
+
+.text-yellow-200 {
+  --text-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--text-opacity));
+}
+
+.text-yellow-300 {
+  --text-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--text-opacity));
+}
+
+.text-yellow-400 {
+  --text-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--text-opacity));
+}
+
+.text-yellow-500 {
+  --text-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--text-opacity));
+}
+
+.text-yellow-600 {
+  --text-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--text-opacity));
+}
+
+.text-yellow-700 {
+  --text-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--text-opacity));
+}
+
+.text-yellow-800 {
+  --text-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--text-opacity));
+}
+
+.text-yellow-900 {
+  --text-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--text-opacity));
+}
+
+.text-green-100 {
+  --text-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--text-opacity));
+}
+
+.text-green-200 {
+  --text-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--text-opacity));
+}
+
+.text-green-300 {
+  --text-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--text-opacity));
+}
+
+.text-green-400 {
+  --text-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--text-opacity));
+}
+
+.text-green-500 {
+  --text-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--text-opacity));
+}
+
+.text-green-600 {
+  --text-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--text-opacity));
+}
+
+.text-green-700 {
+  --text-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--text-opacity));
+}
+
+.text-green-800 {
+  --text-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--text-opacity));
+}
+
+.text-green-900 {
+  --text-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--text-opacity));
+}
+
+.text-teal-100 {
+  --text-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--text-opacity));
+}
+
+.text-teal-200 {
+  --text-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--text-opacity));
+}
+
+.text-teal-300 {
+  --text-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--text-opacity));
+}
+
+.text-teal-400 {
+  --text-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--text-opacity));
+}
+
+.text-teal-500 {
+  --text-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--text-opacity));
+}
+
+.text-teal-600 {
+  --text-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--text-opacity));
+}
+
+.text-teal-700 {
+  --text-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--text-opacity));
+}
+
+.text-teal-800 {
+  --text-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--text-opacity));
+}
+
+.text-teal-900 {
+  --text-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--text-opacity));
+}
+
+.text-blue-100 {
+  --text-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--text-opacity));
+}
+
+.text-blue-200 {
+  --text-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--text-opacity));
+}
+
+.text-blue-300 {
+  --text-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--text-opacity));
+}
+
+.text-blue-400 {
+  --text-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--text-opacity));
+}
+
+.text-blue-500 {
+  --text-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--text-opacity));
+}
+
+.text-blue-600 {
+  --text-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--text-opacity));
+}
+
+.text-blue-700 {
+  --text-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--text-opacity));
+}
+
+.text-blue-800 {
+  --text-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--text-opacity));
+}
+
+.text-blue-900 {
+  --text-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--text-opacity));
+}
+
+.text-indigo-100 {
+  --text-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--text-opacity));
+}
+
+.text-indigo-200 {
+  --text-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--text-opacity));
+}
+
+.text-indigo-300 {
+  --text-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--text-opacity));
+}
+
+.text-indigo-400 {
+  --text-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--text-opacity));
+}
+
+.text-indigo-500 {
+  --text-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--text-opacity));
+}
+
+.text-indigo-600 {
+  --text-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--text-opacity));
+}
+
+.text-indigo-700 {
+  --text-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--text-opacity));
+}
+
+.text-indigo-800 {
+  --text-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--text-opacity));
+}
+
+.text-indigo-900 {
+  --text-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--text-opacity));
+}
+
+.text-purple-100 {
+  --text-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--text-opacity));
+}
+
+.text-purple-200 {
+  --text-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--text-opacity));
+}
+
+.text-purple-300 {
+  --text-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--text-opacity));
+}
+
+.text-purple-400 {
+  --text-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--text-opacity));
+}
+
+.text-purple-500 {
+  --text-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--text-opacity));
+}
+
+.text-purple-600 {
+  --text-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--text-opacity));
+}
+
+.text-purple-700 {
+  --text-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--text-opacity));
+}
+
+.text-purple-800 {
+  --text-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--text-opacity));
+}
+
+.text-purple-900 {
+  --text-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--text-opacity));
+}
+
+.text-pink-100 {
+  --text-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--text-opacity));
+}
+
+.text-pink-200 {
+  --text-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--text-opacity));
+}
+
+.text-pink-300 {
+  --text-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--text-opacity));
+}
+
+.text-pink-400 {
+  --text-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--text-opacity));
+}
+
+.text-pink-500 {
+  --text-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--text-opacity));
+}
+
+.text-pink-600 {
+  --text-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--text-opacity));
+}
+
+.text-pink-700 {
+  --text-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--text-opacity));
+}
+
+.text-pink-800 {
+  --text-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--text-opacity));
+}
+
+.text-pink-900 {
+  --text-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--text-opacity));
+}
+
+.hover\:text-transparent:hover {
+  color: transparent;
+}
+
+.hover\:text-current:hover {
+  color: currentColor;
+}
+
+.hover\:text-black:hover {
+  --text-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--text-opacity));
+}
+
+.hover\:text-white:hover {
+  --text-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--text-opacity));
+}
+
+.hover\:text-gray-100:hover {
+  --text-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--text-opacity));
+}
+
+.hover\:text-gray-200:hover {
+  --text-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--text-opacity));
+}
+
+.hover\:text-gray-300:hover {
+  --text-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--text-opacity));
+}
+
+.hover\:text-gray-400:hover {
+  --text-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--text-opacity));
+}
+
+.hover\:text-gray-500:hover {
+  --text-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--text-opacity));
+}
+
+.hover\:text-gray-600:hover {
+  --text-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--text-opacity));
+}
+
+.hover\:text-gray-700:hover {
+  --text-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--text-opacity));
+}
+
+.hover\:text-gray-800:hover {
+  --text-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--text-opacity));
+}
+
+.hover\:text-gray-900:hover {
+  --text-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--text-opacity));
+}
+
+.hover\:text-red-100:hover {
+  --text-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--text-opacity));
+}
+
+.hover\:text-red-200:hover {
+  --text-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--text-opacity));
+}
+
+.hover\:text-red-300:hover {
+  --text-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--text-opacity));
+}
+
+.hover\:text-red-400:hover {
+  --text-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--text-opacity));
+}
+
+.hover\:text-red-500:hover {
+  --text-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--text-opacity));
+}
+
+.hover\:text-red-600:hover {
+  --text-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--text-opacity));
+}
+
+.hover\:text-red-700:hover {
+  --text-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--text-opacity));
+}
+
+.hover\:text-red-800:hover {
+  --text-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--text-opacity));
+}
+
+.hover\:text-red-900:hover {
+  --text-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--text-opacity));
+}
+
+.hover\:text-orange-100:hover {
+  --text-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--text-opacity));
+}
+
+.hover\:text-orange-200:hover {
+  --text-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--text-opacity));
+}
+
+.hover\:text-orange-300:hover {
+  --text-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--text-opacity));
+}
+
+.hover\:text-orange-400:hover {
+  --text-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--text-opacity));
+}
+
+.hover\:text-orange-500:hover {
+  --text-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--text-opacity));
+}
+
+.hover\:text-orange-600:hover {
+  --text-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--text-opacity));
+}
+
+.hover\:text-orange-700:hover {
+  --text-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--text-opacity));
+}
+
+.hover\:text-orange-800:hover {
+  --text-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--text-opacity));
+}
+
+.hover\:text-orange-900:hover {
+  --text-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--text-opacity));
+}
+
+.hover\:text-yellow-100:hover {
+  --text-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--text-opacity));
+}
+
+.hover\:text-yellow-200:hover {
+  --text-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--text-opacity));
+}
+
+.hover\:text-yellow-300:hover {
+  --text-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--text-opacity));
+}
+
+.hover\:text-yellow-400:hover {
+  --text-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--text-opacity));
+}
+
+.hover\:text-yellow-500:hover {
+  --text-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--text-opacity));
+}
+
+.hover\:text-yellow-600:hover {
+  --text-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--text-opacity));
+}
+
+.hover\:text-yellow-700:hover {
+  --text-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--text-opacity));
+}
+
+.hover\:text-yellow-800:hover {
+  --text-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--text-opacity));
+}
+
+.hover\:text-yellow-900:hover {
+  --text-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--text-opacity));
+}
+
+.hover\:text-green-100:hover {
+  --text-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--text-opacity));
+}
+
+.hover\:text-green-200:hover {
+  --text-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--text-opacity));
+}
+
+.hover\:text-green-300:hover {
+  --text-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--text-opacity));
+}
+
+.hover\:text-green-400:hover {
+  --text-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--text-opacity));
+}
+
+.hover\:text-green-500:hover {
+  --text-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--text-opacity));
+}
+
+.hover\:text-green-600:hover {
+  --text-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--text-opacity));
+}
+
+.hover\:text-green-700:hover {
+  --text-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--text-opacity));
+}
+
+.hover\:text-green-800:hover {
+  --text-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--text-opacity));
+}
+
+.hover\:text-green-900:hover {
+  --text-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--text-opacity));
+}
+
+.hover\:text-teal-100:hover {
+  --text-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--text-opacity));
+}
+
+.hover\:text-teal-200:hover {
+  --text-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--text-opacity));
+}
+
+.hover\:text-teal-300:hover {
+  --text-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--text-opacity));
+}
+
+.hover\:text-teal-400:hover {
+  --text-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--text-opacity));
+}
+
+.hover\:text-teal-500:hover {
+  --text-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--text-opacity));
+}
+
+.hover\:text-teal-600:hover {
+  --text-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--text-opacity));
+}
+
+.hover\:text-teal-700:hover {
+  --text-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--text-opacity));
+}
+
+.hover\:text-teal-800:hover {
+  --text-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--text-opacity));
+}
+
+.hover\:text-teal-900:hover {
+  --text-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--text-opacity));
+}
+
+.hover\:text-blue-100:hover {
+  --text-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--text-opacity));
+}
+
+.hover\:text-blue-200:hover {
+  --text-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--text-opacity));
+}
+
+.hover\:text-blue-300:hover {
+  --text-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--text-opacity));
+}
+
+.hover\:text-blue-400:hover {
+  --text-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--text-opacity));
+}
+
+.hover\:text-blue-500:hover {
+  --text-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--text-opacity));
+}
+
+.hover\:text-blue-600:hover {
+  --text-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--text-opacity));
+}
+
+.hover\:text-blue-700:hover {
+  --text-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--text-opacity));
+}
+
+.hover\:text-blue-800:hover {
+  --text-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--text-opacity));
+}
+
+.hover\:text-blue-900:hover {
+  --text-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--text-opacity));
+}
+
+.hover\:text-indigo-100:hover {
+  --text-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--text-opacity));
+}
+
+.hover\:text-indigo-200:hover {
+  --text-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--text-opacity));
+}
+
+.hover\:text-indigo-300:hover {
+  --text-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--text-opacity));
+}
+
+.hover\:text-indigo-400:hover {
+  --text-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--text-opacity));
+}
+
+.hover\:text-indigo-500:hover {
+  --text-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--text-opacity));
+}
+
+.hover\:text-indigo-600:hover {
+  --text-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--text-opacity));
+}
+
+.hover\:text-indigo-700:hover {
+  --text-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--text-opacity));
+}
+
+.hover\:text-indigo-800:hover {
+  --text-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--text-opacity));
+}
+
+.hover\:text-indigo-900:hover {
+  --text-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--text-opacity));
+}
+
+.hover\:text-purple-100:hover {
+  --text-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--text-opacity));
+}
+
+.hover\:text-purple-200:hover {
+  --text-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--text-opacity));
+}
+
+.hover\:text-purple-300:hover {
+  --text-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--text-opacity));
+}
+
+.hover\:text-purple-400:hover {
+  --text-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--text-opacity));
+}
+
+.hover\:text-purple-500:hover {
+  --text-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--text-opacity));
+}
+
+.hover\:text-purple-600:hover {
+  --text-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--text-opacity));
+}
+
+.hover\:text-purple-700:hover {
+  --text-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--text-opacity));
+}
+
+.hover\:text-purple-800:hover {
+  --text-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--text-opacity));
+}
+
+.hover\:text-purple-900:hover {
+  --text-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--text-opacity));
+}
+
+.hover\:text-pink-100:hover {
+  --text-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--text-opacity));
+}
+
+.hover\:text-pink-200:hover {
+  --text-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--text-opacity));
+}
+
+.hover\:text-pink-300:hover {
+  --text-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--text-opacity));
+}
+
+.hover\:text-pink-400:hover {
+  --text-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--text-opacity));
+}
+
+.hover\:text-pink-500:hover {
+  --text-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--text-opacity));
+}
+
+.hover\:text-pink-600:hover {
+  --text-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--text-opacity));
+}
+
+.hover\:text-pink-700:hover {
+  --text-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--text-opacity));
+}
+
+.hover\:text-pink-800:hover {
+  --text-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--text-opacity));
+}
+
+.hover\:text-pink-900:hover {
+  --text-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--text-opacity));
+}
+
+.focus\:text-transparent:focus {
+  color: transparent;
+}
+
+.focus\:text-current:focus {
+  color: currentColor;
+}
+
+.focus\:text-black:focus {
+  --text-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--text-opacity));
+}
+
+.focus\:text-white:focus {
+  --text-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--text-opacity));
+}
+
+.focus\:text-gray-100:focus {
+  --text-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--text-opacity));
+}
+
+.focus\:text-gray-200:focus {
+  --text-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--text-opacity));
+}
+
+.focus\:text-gray-300:focus {
+  --text-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--text-opacity));
+}
+
+.focus\:text-gray-400:focus {
+  --text-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--text-opacity));
+}
+
+.focus\:text-gray-500:focus {
+  --text-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--text-opacity));
+}
+
+.focus\:text-gray-600:focus {
+  --text-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--text-opacity));
+}
+
+.focus\:text-gray-700:focus {
+  --text-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--text-opacity));
+}
+
+.focus\:text-gray-800:focus {
+  --text-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--text-opacity));
+}
+
+.focus\:text-gray-900:focus {
+  --text-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--text-opacity));
+}
+
+.focus\:text-red-100:focus {
+  --text-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--text-opacity));
+}
+
+.focus\:text-red-200:focus {
+  --text-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--text-opacity));
+}
+
+.focus\:text-red-300:focus {
+  --text-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--text-opacity));
+}
+
+.focus\:text-red-400:focus {
+  --text-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--text-opacity));
+}
+
+.focus\:text-red-500:focus {
+  --text-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--text-opacity));
+}
+
+.focus\:text-red-600:focus {
+  --text-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--text-opacity));
+}
+
+.focus\:text-red-700:focus {
+  --text-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--text-opacity));
+}
+
+.focus\:text-red-800:focus {
+  --text-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--text-opacity));
+}
+
+.focus\:text-red-900:focus {
+  --text-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--text-opacity));
+}
+
+.focus\:text-orange-100:focus {
+  --text-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--text-opacity));
+}
+
+.focus\:text-orange-200:focus {
+  --text-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--text-opacity));
+}
+
+.focus\:text-orange-300:focus {
+  --text-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--text-opacity));
+}
+
+.focus\:text-orange-400:focus {
+  --text-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--text-opacity));
+}
+
+.focus\:text-orange-500:focus {
+  --text-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--text-opacity));
+}
+
+.focus\:text-orange-600:focus {
+  --text-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--text-opacity));
+}
+
+.focus\:text-orange-700:focus {
+  --text-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--text-opacity));
+}
+
+.focus\:text-orange-800:focus {
+  --text-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--text-opacity));
+}
+
+.focus\:text-orange-900:focus {
+  --text-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--text-opacity));
+}
+
+.focus\:text-yellow-100:focus {
+  --text-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--text-opacity));
+}
+
+.focus\:text-yellow-200:focus {
+  --text-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--text-opacity));
+}
+
+.focus\:text-yellow-300:focus {
+  --text-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--text-opacity));
+}
+
+.focus\:text-yellow-400:focus {
+  --text-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--text-opacity));
+}
+
+.focus\:text-yellow-500:focus {
+  --text-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--text-opacity));
+}
+
+.focus\:text-yellow-600:focus {
+  --text-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--text-opacity));
+}
+
+.focus\:text-yellow-700:focus {
+  --text-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--text-opacity));
+}
+
+.focus\:text-yellow-800:focus {
+  --text-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--text-opacity));
+}
+
+.focus\:text-yellow-900:focus {
+  --text-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--text-opacity));
+}
+
+.focus\:text-green-100:focus {
+  --text-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--text-opacity));
+}
+
+.focus\:text-green-200:focus {
+  --text-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--text-opacity));
+}
+
+.focus\:text-green-300:focus {
+  --text-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--text-opacity));
+}
+
+.focus\:text-green-400:focus {
+  --text-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--text-opacity));
+}
+
+.focus\:text-green-500:focus {
+  --text-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--text-opacity));
+}
+
+.focus\:text-green-600:focus {
+  --text-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--text-opacity));
+}
+
+.focus\:text-green-700:focus {
+  --text-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--text-opacity));
+}
+
+.focus\:text-green-800:focus {
+  --text-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--text-opacity));
+}
+
+.focus\:text-green-900:focus {
+  --text-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--text-opacity));
+}
+
+.focus\:text-teal-100:focus {
+  --text-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--text-opacity));
+}
+
+.focus\:text-teal-200:focus {
+  --text-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--text-opacity));
+}
+
+.focus\:text-teal-300:focus {
+  --text-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--text-opacity));
+}
+
+.focus\:text-teal-400:focus {
+  --text-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--text-opacity));
+}
+
+.focus\:text-teal-500:focus {
+  --text-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--text-opacity));
+}
+
+.focus\:text-teal-600:focus {
+  --text-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--text-opacity));
+}
+
+.focus\:text-teal-700:focus {
+  --text-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--text-opacity));
+}
+
+.focus\:text-teal-800:focus {
+  --text-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--text-opacity));
+}
+
+.focus\:text-teal-900:focus {
+  --text-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--text-opacity));
+}
+
+.focus\:text-blue-100:focus {
+  --text-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--text-opacity));
+}
+
+.focus\:text-blue-200:focus {
+  --text-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--text-opacity));
+}
+
+.focus\:text-blue-300:focus {
+  --text-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--text-opacity));
+}
+
+.focus\:text-blue-400:focus {
+  --text-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--text-opacity));
+}
+
+.focus\:text-blue-500:focus {
+  --text-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--text-opacity));
+}
+
+.focus\:text-blue-600:focus {
+  --text-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--text-opacity));
+}
+
+.focus\:text-blue-700:focus {
+  --text-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--text-opacity));
+}
+
+.focus\:text-blue-800:focus {
+  --text-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--text-opacity));
+}
+
+.focus\:text-blue-900:focus {
+  --text-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--text-opacity));
+}
+
+.focus\:text-indigo-100:focus {
+  --text-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--text-opacity));
+}
+
+.focus\:text-indigo-200:focus {
+  --text-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--text-opacity));
+}
+
+.focus\:text-indigo-300:focus {
+  --text-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--text-opacity));
+}
+
+.focus\:text-indigo-400:focus {
+  --text-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--text-opacity));
+}
+
+.focus\:text-indigo-500:focus {
+  --text-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--text-opacity));
+}
+
+.focus\:text-indigo-600:focus {
+  --text-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--text-opacity));
+}
+
+.focus\:text-indigo-700:focus {
+  --text-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--text-opacity));
+}
+
+.focus\:text-indigo-800:focus {
+  --text-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--text-opacity));
+}
+
+.focus\:text-indigo-900:focus {
+  --text-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--text-opacity));
+}
+
+.focus\:text-purple-100:focus {
+  --text-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--text-opacity));
+}
+
+.focus\:text-purple-200:focus {
+  --text-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--text-opacity));
+}
+
+.focus\:text-purple-300:focus {
+  --text-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--text-opacity));
+}
+
+.focus\:text-purple-400:focus {
+  --text-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--text-opacity));
+}
+
+.focus\:text-purple-500:focus {
+  --text-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--text-opacity));
+}
+
+.focus\:text-purple-600:focus {
+  --text-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--text-opacity));
+}
+
+.focus\:text-purple-700:focus {
+  --text-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--text-opacity));
+}
+
+.focus\:text-purple-800:focus {
+  --text-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--text-opacity));
+}
+
+.focus\:text-purple-900:focus {
+  --text-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--text-opacity));
+}
+
+.focus\:text-pink-100:focus {
+  --text-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--text-opacity));
+}
+
+.focus\:text-pink-200:focus {
+  --text-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--text-opacity));
+}
+
+.focus\:text-pink-300:focus {
+  --text-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--text-opacity));
+}
+
+.focus\:text-pink-400:focus {
+  --text-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--text-opacity));
+}
+
+.focus\:text-pink-500:focus {
+  --text-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--text-opacity));
+}
+
+.focus\:text-pink-600:focus {
+  --text-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--text-opacity));
+}
+
+.focus\:text-pink-700:focus {
+  --text-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--text-opacity));
+}
+
+.focus\:text-pink-800:focus {
+  --text-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--text-opacity));
+}
+
+.focus\:text-pink-900:focus {
+  --text-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--text-opacity));
+}
+
+.text-opacity-0 {
+  --text-opacity: 0;
+}
+
+.text-opacity-25 {
+  --text-opacity: 0.25;
+}
+
+.text-opacity-50 {
+  --text-opacity: 0.5;
+}
+
+.text-opacity-75 {
+  --text-opacity: 0.75;
+}
+
+.text-opacity-100 {
+  --text-opacity: 1;
+}
+
+.hover\:text-opacity-0:hover {
+  --text-opacity: 0;
+}
+
+.hover\:text-opacity-25:hover {
+  --text-opacity: 0.25;
+}
+
+.hover\:text-opacity-50:hover {
+  --text-opacity: 0.5;
+}
+
+.hover\:text-opacity-75:hover {
+  --text-opacity: 0.75;
+}
+
+.hover\:text-opacity-100:hover {
+  --text-opacity: 1;
+}
+
+.focus\:text-opacity-0:focus {
+  --text-opacity: 0;
+}
+
+.focus\:text-opacity-25:focus {
+  --text-opacity: 0.25;
+}
+
+.focus\:text-opacity-50:focus {
+  --text-opacity: 0.5;
+}
+
+.focus\:text-opacity-75:focus {
+  --text-opacity: 0.75;
+}
+
+.focus\:text-opacity-100:focus {
+  --text-opacity: 1;
+}
+
+.italic {
+  font-style: italic;
+}
+
+.not-italic {
+  font-style: normal;
+}
+
+.uppercase {
+  text-transform: uppercase;
+}
+
+.lowercase {
+  text-transform: lowercase;
+}
+
+.capitalize {
+  text-transform: capitalize;
+}
+
+.normal-case {
+  text-transform: none;
+}
+
+.underline {
+  text-decoration: underline;
+}
+
+.line-through {
+  text-decoration: line-through;
+}
+
+.no-underline {
+  text-decoration: none;
+}
+
+.hover\:underline:hover {
+  text-decoration: underline;
+}
+
+.hover\:line-through:hover {
+  text-decoration: line-through;
+}
+
+.hover\:no-underline:hover {
+  text-decoration: none;
+}
+
+.focus\:underline:focus {
+  text-decoration: underline;
+}
+
+.focus\:line-through:focus {
+  text-decoration: line-through;
+}
+
+.focus\:no-underline:focus {
+  text-decoration: none;
+}
+
+.antialiased {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.subpixel-antialiased {
+  -webkit-font-smoothing: auto;
+  -moz-osx-font-smoothing: auto;
+}
+
+.ordinal, .slashed-zero, .lining-nums, .oldstyle-nums, .proportional-nums, .tabular-nums, .diagonal-fractions, .stacked-fractions {
+  --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+  font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+}
+
+.normal-nums {
+  font-variant-numeric: normal;
+}
+
+.ordinal {
+  --font-variant-numeric-ordinal: ordinal;
+}
+
+.slashed-zero {
+  --font-variant-numeric-slashed-zero: slashed-zero;
+}
+
+.lining-nums {
+  --font-variant-numeric-figure: lining-nums;
+}
+
+.oldstyle-nums {
+  --font-variant-numeric-figure: oldstyle-nums;
+}
+
+.proportional-nums {
+  --font-variant-numeric-spacing: proportional-nums;
+}
+
+.tabular-nums {
+  --font-variant-numeric-spacing: tabular-nums;
+}
+
+.diagonal-fractions {
+  --font-variant-numeric-fraction: diagonal-fractions;
+}
+
+.stacked-fractions {
+  --font-variant-numeric-fraction: stacked-fractions;
+}
+
+.tracking-tighter {
+  letter-spacing: -0.05em;
+}
+
+.tracking-tight {
+  letter-spacing: -0.025em;
+}
+
+.tracking-normal {
+  letter-spacing: 0;
+}
+
+.tracking-wide {
+  letter-spacing: 0.025em;
+}
+
+.tracking-wider {
+  letter-spacing: 0.05em;
+}
+
+.tracking-widest {
+  letter-spacing: 0.1em;
+}
+
+.select-none {
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+}
+
+.select-text {
+  -webkit-user-select: text;
+     -moz-user-select: text;
+      -ms-user-select: text;
+          user-select: text;
+}
+
+.select-all {
+  -webkit-user-select: all;
+     -moz-user-select: all;
+      -ms-user-select: all;
+          user-select: all;
+}
+
+.select-auto {
+  -webkit-user-select: auto;
+     -moz-user-select: auto;
+      -ms-user-select: auto;
+          user-select: auto;
+}
+
+.align-baseline {
+  vertical-align: baseline;
+}
+
+.align-top {
+  vertical-align: top;
+}
+
+.align-middle {
+  vertical-align: middle;
+}
+
+.align-bottom {
+  vertical-align: bottom;
+}
+
+.align-text-top {
+  vertical-align: text-top;
+}
+
+.align-text-bottom {
+  vertical-align: text-bottom;
+}
+
+.visible {
+  visibility: visible;
+}
+
+.invisible {
+  visibility: hidden;
+}
+
+.whitespace-normal {
+  white-space: normal;
+}
+
+.whitespace-no-wrap {
+  white-space: nowrap;
+}
+
+.whitespace-pre {
+  white-space: pre;
+}
+
+.whitespace-pre-line {
+  white-space: pre-line;
+}
+
+.whitespace-pre-wrap {
+  white-space: pre-wrap;
+}
+
+.break-normal {
+  overflow-wrap: normal;
+  word-break: normal;
+}
+
+.break-words {
+  overflow-wrap: break-word;
+}
+
+.break-all {
+  word-break: break-all;
+}
+
+.truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.w-0 {
+  width: 0;
+}
+
+.w-1 {
+  width: 0.25rem;
+}
+
+.w-2 {
+  width: 0.5rem;
+}
+
+.w-3 {
+  width: 0.75rem;
+}
+
+.w-4 {
+  width: 1rem;
+}
+
+.w-5 {
+  width: 1.25rem;
+}
+
+.w-6 {
+  width: 1.5rem;
+}
+
+.w-8 {
+  width: 2rem;
+}
+
+.w-10 {
+  width: 2.5rem;
+}
+
+.w-12 {
+  width: 3rem;
+}
+
+.w-16 {
+  width: 4rem;
+}
+
+.w-20 {
+  width: 5rem;
+}
+
+.w-24 {
+  width: 6rem;
+}
+
+.w-32 {
+  width: 8rem;
+}
+
+.w-40 {
+  width: 10rem;
+}
+
+.w-48 {
+  width: 12rem;
+}
+
+.w-56 {
+  width: 14rem;
+}
+
+.w-64 {
+  width: 16rem;
+}
+
+.w-auto {
+  width: auto;
+}
+
+.w-px {
+  width: 1px;
+}
+
+.w-1\/2 {
+  width: 50%;
+}
+
+.w-1\/3 {
+  width: 33.333333%;
+}
+
+.w-2\/3 {
+  width: 66.666667%;
+}
+
+.w-1\/4 {
+  width: 25%;
+}
+
+.w-2\/4 {
+  width: 50%;
+}
+
+.w-3\/4 {
+  width: 75%;
+}
+
+.w-1\/5 {
+  width: 20%;
+}
+
+.w-2\/5 {
+  width: 40%;
+}
+
+.w-3\/5 {
+  width: 60%;
+}
+
+.w-4\/5 {
+  width: 80%;
+}
+
+.w-1\/6 {
+  width: 16.666667%;
+}
+
+.w-2\/6 {
+  width: 33.333333%;
+}
+
+.w-3\/6 {
+  width: 50%;
+}
+
+.w-4\/6 {
+  width: 66.666667%;
+}
+
+.w-5\/6 {
+  width: 83.333333%;
+}
+
+.w-1\/12 {
+  width: 8.333333%;
+}
+
+.w-2\/12 {
+  width: 16.666667%;
+}
+
+.w-3\/12 {
+  width: 25%;
+}
+
+.w-4\/12 {
+  width: 33.333333%;
+}
+
+.w-5\/12 {
+  width: 41.666667%;
+}
+
+.w-6\/12 {
+  width: 50%;
+}
+
+.w-7\/12 {
+  width: 58.333333%;
+}
+
+.w-8\/12 {
+  width: 66.666667%;
+}
+
+.w-9\/12 {
+  width: 75%;
+}
+
+.w-10\/12 {
+  width: 83.333333%;
+}
+
+.w-11\/12 {
+  width: 91.666667%;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.w-screen {
+  width: 100vw;
+}
+
+.z-0 {
+  z-index: 0;
+}
+
+.z-10 {
+  z-index: 10;
+}
+
+.z-20 {
+  z-index: 20;
+}
+
+.z-30 {
+  z-index: 30;
+}
+
+.z-40 {
+  z-index: 40;
+}
+
+.z-50 {
+  z-index: 50;
+}
+
+.z-auto {
+  z-index: auto;
+}
+
+.gap-0 {
+  grid-gap: 0;
+  gap: 0;
+}
+
+.gap-1 {
+  grid-gap: 0.25rem;
+  gap: 0.25rem;
+}
+
+.gap-2 {
+  grid-gap: 0.5rem;
+  gap: 0.5rem;
+}
+
+.gap-3 {
+  grid-gap: 0.75rem;
+  gap: 0.75rem;
+}
+
+.gap-4 {
+  grid-gap: 1rem;
+  gap: 1rem;
+}
+
+.gap-5 {
+  grid-gap: 1.25rem;
+  gap: 1.25rem;
+}
+
+.gap-6 {
+  grid-gap: 1.5rem;
+  gap: 1.5rem;
+}
+
+.gap-8 {
+  grid-gap: 2rem;
+  gap: 2rem;
+}
+
+.gap-10 {
+  grid-gap: 2.5rem;
+  gap: 2.5rem;
+}
+
+.gap-12 {
+  grid-gap: 3rem;
+  gap: 3rem;
+}
+
+.gap-16 {
+  grid-gap: 4rem;
+  gap: 4rem;
+}
+
+.gap-20 {
+  grid-gap: 5rem;
+  gap: 5rem;
+}
+
+.gap-24 {
+  grid-gap: 6rem;
+  gap: 6rem;
+}
+
+.gap-32 {
+  grid-gap: 8rem;
+  gap: 8rem;
+}
+
+.gap-40 {
+  grid-gap: 10rem;
+  gap: 10rem;
+}
+
+.gap-48 {
+  grid-gap: 12rem;
+  gap: 12rem;
+}
+
+.gap-56 {
+  grid-gap: 14rem;
+  gap: 14rem;
+}
+
+.gap-64 {
+  grid-gap: 16rem;
+  gap: 16rem;
+}
+
+.gap-px {
+  grid-gap: 1px;
+  gap: 1px;
+}
+
+.col-gap-0 {
+  grid-column-gap: 0;
+  -moz-column-gap: 0;
+       column-gap: 0;
+}
+
+.col-gap-1 {
+  grid-column-gap: 0.25rem;
+  -moz-column-gap: 0.25rem;
+       column-gap: 0.25rem;
+}
+
+.col-gap-2 {
+  grid-column-gap: 0.5rem;
+  -moz-column-gap: 0.5rem;
+       column-gap: 0.5rem;
+}
+
+.col-gap-3 {
+  grid-column-gap: 0.75rem;
+  -moz-column-gap: 0.75rem;
+       column-gap: 0.75rem;
+}
+
+.col-gap-4 {
+  grid-column-gap: 1rem;
+  -moz-column-gap: 1rem;
+       column-gap: 1rem;
+}
+
+.col-gap-5 {
+  grid-column-gap: 1.25rem;
+  -moz-column-gap: 1.25rem;
+       column-gap: 1.25rem;
+}
+
+.col-gap-6 {
+  grid-column-gap: 1.5rem;
+  -moz-column-gap: 1.5rem;
+       column-gap: 1.5rem;
+}
+
+.col-gap-8 {
+  grid-column-gap: 2rem;
+  -moz-column-gap: 2rem;
+       column-gap: 2rem;
+}
+
+.col-gap-10 {
+  grid-column-gap: 2.5rem;
+  -moz-column-gap: 2.5rem;
+       column-gap: 2.5rem;
+}
+
+.col-gap-12 {
+  grid-column-gap: 3rem;
+  -moz-column-gap: 3rem;
+       column-gap: 3rem;
+}
+
+.col-gap-16 {
+  grid-column-gap: 4rem;
+  -moz-column-gap: 4rem;
+       column-gap: 4rem;
+}
+
+.col-gap-20 {
+  grid-column-gap: 5rem;
+  -moz-column-gap: 5rem;
+       column-gap: 5rem;
+}
+
+.col-gap-24 {
+  grid-column-gap: 6rem;
+  -moz-column-gap: 6rem;
+       column-gap: 6rem;
+}
+
+.col-gap-32 {
+  grid-column-gap: 8rem;
+  -moz-column-gap: 8rem;
+       column-gap: 8rem;
+}
+
+.col-gap-40 {
+  grid-column-gap: 10rem;
+  -moz-column-gap: 10rem;
+       column-gap: 10rem;
+}
+
+.col-gap-48 {
+  grid-column-gap: 12rem;
+  -moz-column-gap: 12rem;
+       column-gap: 12rem;
+}
+
+.col-gap-56 {
+  grid-column-gap: 14rem;
+  -moz-column-gap: 14rem;
+       column-gap: 14rem;
+}
+
+.col-gap-64 {
+  grid-column-gap: 16rem;
+  -moz-column-gap: 16rem;
+       column-gap: 16rem;
+}
+
+.col-gap-px {
+  grid-column-gap: 1px;
+  -moz-column-gap: 1px;
+       column-gap: 1px;
+}
+
+.gap-x-0 {
+  grid-column-gap: 0;
+  -moz-column-gap: 0;
+       column-gap: 0;
+}
+
+.gap-x-1 {
+  grid-column-gap: 0.25rem;
+  -moz-column-gap: 0.25rem;
+       column-gap: 0.25rem;
+}
+
+.gap-x-2 {
+  grid-column-gap: 0.5rem;
+  -moz-column-gap: 0.5rem;
+       column-gap: 0.5rem;
+}
+
+.gap-x-3 {
+  grid-column-gap: 0.75rem;
+  -moz-column-gap: 0.75rem;
+       column-gap: 0.75rem;
+}
+
+.gap-x-4 {
+  grid-column-gap: 1rem;
+  -moz-column-gap: 1rem;
+       column-gap: 1rem;
+}
+
+.gap-x-5 {
+  grid-column-gap: 1.25rem;
+  -moz-column-gap: 1.25rem;
+       column-gap: 1.25rem;
+}
+
+.gap-x-6 {
+  grid-column-gap: 1.5rem;
+  -moz-column-gap: 1.5rem;
+       column-gap: 1.5rem;
+}
+
+.gap-x-8 {
+  grid-column-gap: 2rem;
+  -moz-column-gap: 2rem;
+       column-gap: 2rem;
+}
+
+.gap-x-10 {
+  grid-column-gap: 2.5rem;
+  -moz-column-gap: 2.5rem;
+       column-gap: 2.5rem;
+}
+
+.gap-x-12 {
+  grid-column-gap: 3rem;
+  -moz-column-gap: 3rem;
+       column-gap: 3rem;
+}
+
+.gap-x-16 {
+  grid-column-gap: 4rem;
+  -moz-column-gap: 4rem;
+       column-gap: 4rem;
+}
+
+.gap-x-20 {
+  grid-column-gap: 5rem;
+  -moz-column-gap: 5rem;
+       column-gap: 5rem;
+}
+
+.gap-x-24 {
+  grid-column-gap: 6rem;
+  -moz-column-gap: 6rem;
+       column-gap: 6rem;
+}
+
+.gap-x-32 {
+  grid-column-gap: 8rem;
+  -moz-column-gap: 8rem;
+       column-gap: 8rem;
+}
+
+.gap-x-40 {
+  grid-column-gap: 10rem;
+  -moz-column-gap: 10rem;
+       column-gap: 10rem;
+}
+
+.gap-x-48 {
+  grid-column-gap: 12rem;
+  -moz-column-gap: 12rem;
+       column-gap: 12rem;
+}
+
+.gap-x-56 {
+  grid-column-gap: 14rem;
+  -moz-column-gap: 14rem;
+       column-gap: 14rem;
+}
+
+.gap-x-64 {
+  grid-column-gap: 16rem;
+  -moz-column-gap: 16rem;
+       column-gap: 16rem;
+}
+
+.gap-x-px {
+  grid-column-gap: 1px;
+  -moz-column-gap: 1px;
+       column-gap: 1px;
+}
+
+.row-gap-0 {
+  grid-row-gap: 0;
+  row-gap: 0;
+}
+
+.row-gap-1 {
+  grid-row-gap: 0.25rem;
+  row-gap: 0.25rem;
+}
+
+.row-gap-2 {
+  grid-row-gap: 0.5rem;
+  row-gap: 0.5rem;
+}
+
+.row-gap-3 {
+  grid-row-gap: 0.75rem;
+  row-gap: 0.75rem;
+}
+
+.row-gap-4 {
+  grid-row-gap: 1rem;
+  row-gap: 1rem;
+}
+
+.row-gap-5 {
+  grid-row-gap: 1.25rem;
+  row-gap: 1.25rem;
+}
+
+.row-gap-6 {
+  grid-row-gap: 1.5rem;
+  row-gap: 1.5rem;
+}
+
+.row-gap-8 {
+  grid-row-gap: 2rem;
+  row-gap: 2rem;
+}
+
+.row-gap-10 {
+  grid-row-gap: 2.5rem;
+  row-gap: 2.5rem;
+}
+
+.row-gap-12 {
+  grid-row-gap: 3rem;
+  row-gap: 3rem;
+}
+
+.row-gap-16 {
+  grid-row-gap: 4rem;
+  row-gap: 4rem;
+}
+
+.row-gap-20 {
+  grid-row-gap: 5rem;
+  row-gap: 5rem;
+}
+
+.row-gap-24 {
+  grid-row-gap: 6rem;
+  row-gap: 6rem;
+}
+
+.row-gap-32 {
+  grid-row-gap: 8rem;
+  row-gap: 8rem;
+}
+
+.row-gap-40 {
+  grid-row-gap: 10rem;
+  row-gap: 10rem;
+}
+
+.row-gap-48 {
+  grid-row-gap: 12rem;
+  row-gap: 12rem;
+}
+
+.row-gap-56 {
+  grid-row-gap: 14rem;
+  row-gap: 14rem;
+}
+
+.row-gap-64 {
+  grid-row-gap: 16rem;
+  row-gap: 16rem;
+}
+
+.row-gap-px {
+  grid-row-gap: 1px;
+  row-gap: 1px;
+}
+
+.gap-y-0 {
+  grid-row-gap: 0;
+  row-gap: 0;
+}
+
+.gap-y-1 {
+  grid-row-gap: 0.25rem;
+  row-gap: 0.25rem;
+}
+
+.gap-y-2 {
+  grid-row-gap: 0.5rem;
+  row-gap: 0.5rem;
+}
+
+.gap-y-3 {
+  grid-row-gap: 0.75rem;
+  row-gap: 0.75rem;
+}
+
+.gap-y-4 {
+  grid-row-gap: 1rem;
+  row-gap: 1rem;
+}
+
+.gap-y-5 {
+  grid-row-gap: 1.25rem;
+  row-gap: 1.25rem;
+}
+
+.gap-y-6 {
+  grid-row-gap: 1.5rem;
+  row-gap: 1.5rem;
+}
+
+.gap-y-8 {
+  grid-row-gap: 2rem;
+  row-gap: 2rem;
+}
+
+.gap-y-10 {
+  grid-row-gap: 2.5rem;
+  row-gap: 2.5rem;
+}
+
+.gap-y-12 {
+  grid-row-gap: 3rem;
+  row-gap: 3rem;
+}
+
+.gap-y-16 {
+  grid-row-gap: 4rem;
+  row-gap: 4rem;
+}
+
+.gap-y-20 {
+  grid-row-gap: 5rem;
+  row-gap: 5rem;
+}
+
+.gap-y-24 {
+  grid-row-gap: 6rem;
+  row-gap: 6rem;
+}
+
+.gap-y-32 {
+  grid-row-gap: 8rem;
+  row-gap: 8rem;
+}
+
+.gap-y-40 {
+  grid-row-gap: 10rem;
+  row-gap: 10rem;
+}
+
+.gap-y-48 {
+  grid-row-gap: 12rem;
+  row-gap: 12rem;
+}
+
+.gap-y-56 {
+  grid-row-gap: 14rem;
+  row-gap: 14rem;
+}
+
+.gap-y-64 {
+  grid-row-gap: 16rem;
+  row-gap: 16rem;
+}
+
+.gap-y-px {
+  grid-row-gap: 1px;
+  row-gap: 1px;
+}
+
+.grid-flow-row {
+  grid-auto-flow: row;
+}
+
+.grid-flow-col {
+  grid-auto-flow: column;
+}
+
+.grid-flow-row-dense {
+  grid-auto-flow: row dense;
+}
+
+.grid-flow-col-dense {
+  grid-auto-flow: column dense;
+}
+
+.grid-cols-1 {
+  grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+.grid-cols-2 {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.grid-cols-3 {
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.grid-cols-4 {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.grid-cols-5 {
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+}
+
+.grid-cols-6 {
+  grid-template-columns: repeat(6, minmax(0, 1fr));
+}
+
+.grid-cols-7 {
+  grid-template-columns: repeat(7, minmax(0, 1fr));
+}
+
+.grid-cols-8 {
+  grid-template-columns: repeat(8, minmax(0, 1fr));
+}
+
+.grid-cols-9 {
+  grid-template-columns: repeat(9, minmax(0, 1fr));
+}
+
+.grid-cols-10 {
+  grid-template-columns: repeat(10, minmax(0, 1fr));
+}
+
+.grid-cols-11 {
+  grid-template-columns: repeat(11, minmax(0, 1fr));
+}
+
+.grid-cols-12 {
+  grid-template-columns: repeat(12, minmax(0, 1fr));
+}
+
+.grid-cols-none {
+  grid-template-columns: none;
+}
+
+.col-auto {
+  grid-column: auto;
+}
+
+.col-span-1 {
+  grid-column: span 1 / span 1;
+}
+
+.col-span-2 {
+  grid-column: span 2 / span 2;
+}
+
+.col-span-3 {
+  grid-column: span 3 / span 3;
+}
+
+.col-span-4 {
+  grid-column: span 4 / span 4;
+}
+
+.col-span-5 {
+  grid-column: span 5 / span 5;
+}
+
+.col-span-6 {
+  grid-column: span 6 / span 6;
+}
+
+.col-span-7 {
+  grid-column: span 7 / span 7;
+}
+
+.col-span-8 {
+  grid-column: span 8 / span 8;
+}
+
+.col-span-9 {
+  grid-column: span 9 / span 9;
+}
+
+.col-span-10 {
+  grid-column: span 10 / span 10;
+}
+
+.col-span-11 {
+  grid-column: span 11 / span 11;
+}
+
+.col-span-12 {
+  grid-column: span 12 / span 12;
+}
+
+.col-start-1 {
+  grid-column-start: 1;
+}
+
+.col-start-2 {
+  grid-column-start: 2;
+}
+
+.col-start-3 {
+  grid-column-start: 3;
+}
+
+.col-start-4 {
+  grid-column-start: 4;
+}
+
+.col-start-5 {
+  grid-column-start: 5;
+}
+
+.col-start-6 {
+  grid-column-start: 6;
+}
+
+.col-start-7 {
+  grid-column-start: 7;
+}
+
+.col-start-8 {
+  grid-column-start: 8;
+}
+
+.col-start-9 {
+  grid-column-start: 9;
+}
+
+.col-start-10 {
+  grid-column-start: 10;
+}
+
+.col-start-11 {
+  grid-column-start: 11;
+}
+
+.col-start-12 {
+  grid-column-start: 12;
+}
+
+.col-start-13 {
+  grid-column-start: 13;
+}
+
+.col-start-auto {
+  grid-column-start: auto;
+}
+
+.col-end-1 {
+  grid-column-end: 1;
+}
+
+.col-end-2 {
+  grid-column-end: 2;
+}
+
+.col-end-3 {
+  grid-column-end: 3;
+}
+
+.col-end-4 {
+  grid-column-end: 4;
+}
+
+.col-end-5 {
+  grid-column-end: 5;
+}
+
+.col-end-6 {
+  grid-column-end: 6;
+}
+
+.col-end-7 {
+  grid-column-end: 7;
+}
+
+.col-end-8 {
+  grid-column-end: 8;
+}
+
+.col-end-9 {
+  grid-column-end: 9;
+}
+
+.col-end-10 {
+  grid-column-end: 10;
+}
+
+.col-end-11 {
+  grid-column-end: 11;
+}
+
+.col-end-12 {
+  grid-column-end: 12;
+}
+
+.col-end-13 {
+  grid-column-end: 13;
+}
+
+.col-end-auto {
+  grid-column-end: auto;
+}
+
+.grid-rows-1 {
+  grid-template-rows: repeat(1, minmax(0, 1fr));
+}
+
+.grid-rows-2 {
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+}
+
+.grid-rows-3 {
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+
+.grid-rows-4 {
+  grid-template-rows: repeat(4, minmax(0, 1fr));
+}
+
+.grid-rows-5 {
+  grid-template-rows: repeat(5, minmax(0, 1fr));
+}
+
+.grid-rows-6 {
+  grid-template-rows: repeat(6, minmax(0, 1fr));
+}
+
+.grid-rows-none {
+  grid-template-rows: none;
+}
+
+.row-auto {
+  grid-row: auto;
+}
+
+.row-span-1 {
+  grid-row: span 1 / span 1;
+}
+
+.row-span-2 {
+  grid-row: span 2 / span 2;
+}
+
+.row-span-3 {
+  grid-row: span 3 / span 3;
+}
+
+.row-span-4 {
+  grid-row: span 4 / span 4;
+}
+
+.row-span-5 {
+  grid-row: span 5 / span 5;
+}
+
+.row-span-6 {
+  grid-row: span 6 / span 6;
+}
+
+.row-start-1 {
+  grid-row-start: 1;
+}
+
+.row-start-2 {
+  grid-row-start: 2;
+}
+
+.row-start-3 {
+  grid-row-start: 3;
+}
+
+.row-start-4 {
+  grid-row-start: 4;
+}
+
+.row-start-5 {
+  grid-row-start: 5;
+}
+
+.row-start-6 {
+  grid-row-start: 6;
+}
+
+.row-start-7 {
+  grid-row-start: 7;
+}
+
+.row-start-auto {
+  grid-row-start: auto;
+}
+
+.row-end-1 {
+  grid-row-end: 1;
+}
+
+.row-end-2 {
+  grid-row-end: 2;
+}
+
+.row-end-3 {
+  grid-row-end: 3;
+}
+
+.row-end-4 {
+  grid-row-end: 4;
+}
+
+.row-end-5 {
+  grid-row-end: 5;
+}
+
+.row-end-6 {
+  grid-row-end: 6;
+}
+
+.row-end-7 {
+  grid-row-end: 7;
+}
+
+.row-end-auto {
+  grid-row-end: auto;
+}
+
+.transform {
+  --transform-translate-x: 0;
+  --transform-translate-y: 0;
+  --transform-rotate: 0;
+  --transform-skew-x: 0;
+  --transform-skew-y: 0;
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+  transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+}
+
+.transform-none {
+  transform: none;
+}
+
+.origin-center {
+  transform-origin: center;
+}
+
+.origin-top {
+  transform-origin: top;
+}
+
+.origin-top-right {
+  transform-origin: top right;
+}
+
+.origin-right {
+  transform-origin: right;
+}
+
+.origin-bottom-right {
+  transform-origin: bottom right;
+}
+
+.origin-bottom {
+  transform-origin: bottom;
+}
+
+.origin-bottom-left {
+  transform-origin: bottom left;
+}
+
+.origin-left {
+  transform-origin: left;
+}
+
+.origin-top-left {
+  transform-origin: top left;
+}
+
+.scale-0 {
+  --transform-scale-x: 0;
+  --transform-scale-y: 0;
+}
+
+.scale-50 {
+  --transform-scale-x: .5;
+  --transform-scale-y: .5;
+}
+
+.scale-75 {
+  --transform-scale-x: .75;
+  --transform-scale-y: .75;
+}
+
+.scale-90 {
+  --transform-scale-x: .9;
+  --transform-scale-y: .9;
+}
+
+.scale-95 {
+  --transform-scale-x: .95;
+  --transform-scale-y: .95;
+}
+
+.scale-100 {
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+}
+
+.scale-105 {
+  --transform-scale-x: 1.05;
+  --transform-scale-y: 1.05;
+}
+
+.scale-110 {
+  --transform-scale-x: 1.1;
+  --transform-scale-y: 1.1;
+}
+
+.scale-125 {
+  --transform-scale-x: 1.25;
+  --transform-scale-y: 1.25;
+}
+
+.scale-150 {
+  --transform-scale-x: 1.5;
+  --transform-scale-y: 1.5;
+}
+
+.scale-x-0 {
+  --transform-scale-x: 0;
+}
+
+.scale-x-50 {
+  --transform-scale-x: .5;
+}
+
+.scale-x-75 {
+  --transform-scale-x: .75;
+}
+
+.scale-x-90 {
+  --transform-scale-x: .9;
+}
+
+.scale-x-95 {
+  --transform-scale-x: .95;
+}
+
+.scale-x-100 {
+  --transform-scale-x: 1;
+}
+
+.scale-x-105 {
+  --transform-scale-x: 1.05;
+}
+
+.scale-x-110 {
+  --transform-scale-x: 1.1;
+}
+
+.scale-x-125 {
+  --transform-scale-x: 1.25;
+}
+
+.scale-x-150 {
+  --transform-scale-x: 1.5;
+}
+
+.scale-y-0 {
+  --transform-scale-y: 0;
+}
+
+.scale-y-50 {
+  --transform-scale-y: .5;
+}
+
+.scale-y-75 {
+  --transform-scale-y: .75;
+}
+
+.scale-y-90 {
+  --transform-scale-y: .9;
+}
+
+.scale-y-95 {
+  --transform-scale-y: .95;
+}
+
+.scale-y-100 {
+  --transform-scale-y: 1;
+}
+
+.scale-y-105 {
+  --transform-scale-y: 1.05;
+}
+
+.scale-y-110 {
+  --transform-scale-y: 1.1;
+}
+
+.scale-y-125 {
+  --transform-scale-y: 1.25;
+}
+
+.scale-y-150 {
+  --transform-scale-y: 1.5;
+}
+
+.hover\:scale-0:hover {
+  --transform-scale-x: 0;
+  --transform-scale-y: 0;
+}
+
+.hover\:scale-50:hover {
+  --transform-scale-x: .5;
+  --transform-scale-y: .5;
+}
+
+.hover\:scale-75:hover {
+  --transform-scale-x: .75;
+  --transform-scale-y: .75;
+}
+
+.hover\:scale-90:hover {
+  --transform-scale-x: .9;
+  --transform-scale-y: .9;
+}
+
+.hover\:scale-95:hover {
+  --transform-scale-x: .95;
+  --transform-scale-y: .95;
+}
+
+.hover\:scale-100:hover {
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+}
+
+.hover\:scale-105:hover {
+  --transform-scale-x: 1.05;
+  --transform-scale-y: 1.05;
+}
+
+.hover\:scale-110:hover {
+  --transform-scale-x: 1.1;
+  --transform-scale-y: 1.1;
+}
+
+.hover\:scale-125:hover {
+  --transform-scale-x: 1.25;
+  --transform-scale-y: 1.25;
+}
+
+.hover\:scale-150:hover {
+  --transform-scale-x: 1.5;
+  --transform-scale-y: 1.5;
+}
+
+.hover\:scale-x-0:hover {
+  --transform-scale-x: 0;
+}
+
+.hover\:scale-x-50:hover {
+  --transform-scale-x: .5;
+}
+
+.hover\:scale-x-75:hover {
+  --transform-scale-x: .75;
+}
+
+.hover\:scale-x-90:hover {
+  --transform-scale-x: .9;
+}
+
+.hover\:scale-x-95:hover {
+  --transform-scale-x: .95;
+}
+
+.hover\:scale-x-100:hover {
+  --transform-scale-x: 1;
+}
+
+.hover\:scale-x-105:hover {
+  --transform-scale-x: 1.05;
+}
+
+.hover\:scale-x-110:hover {
+  --transform-scale-x: 1.1;
+}
+
+.hover\:scale-x-125:hover {
+  --transform-scale-x: 1.25;
+}
+
+.hover\:scale-x-150:hover {
+  --transform-scale-x: 1.5;
+}
+
+.hover\:scale-y-0:hover {
+  --transform-scale-y: 0;
+}
+
+.hover\:scale-y-50:hover {
+  --transform-scale-y: .5;
+}
+
+.hover\:scale-y-75:hover {
+  --transform-scale-y: .75;
+}
+
+.hover\:scale-y-90:hover {
+  --transform-scale-y: .9;
+}
+
+.hover\:scale-y-95:hover {
+  --transform-scale-y: .95;
+}
+
+.hover\:scale-y-100:hover {
+  --transform-scale-y: 1;
+}
+
+.hover\:scale-y-105:hover {
+  --transform-scale-y: 1.05;
+}
+
+.hover\:scale-y-110:hover {
+  --transform-scale-y: 1.1;
+}
+
+.hover\:scale-y-125:hover {
+  --transform-scale-y: 1.25;
+}
+
+.hover\:scale-y-150:hover {
+  --transform-scale-y: 1.5;
+}
+
+.focus\:scale-0:focus {
+  --transform-scale-x: 0;
+  --transform-scale-y: 0;
+}
+
+.focus\:scale-50:focus {
+  --transform-scale-x: .5;
+  --transform-scale-y: .5;
+}
+
+.focus\:scale-75:focus {
+  --transform-scale-x: .75;
+  --transform-scale-y: .75;
+}
+
+.focus\:scale-90:focus {
+  --transform-scale-x: .9;
+  --transform-scale-y: .9;
+}
+
+.focus\:scale-95:focus {
+  --transform-scale-x: .95;
+  --transform-scale-y: .95;
+}
+
+.focus\:scale-100:focus {
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+}
+
+.focus\:scale-105:focus {
+  --transform-scale-x: 1.05;
+  --transform-scale-y: 1.05;
+}
+
+.focus\:scale-110:focus {
+  --transform-scale-x: 1.1;
+  --transform-scale-y: 1.1;
+}
+
+.focus\:scale-125:focus {
+  --transform-scale-x: 1.25;
+  --transform-scale-y: 1.25;
+}
+
+.focus\:scale-150:focus {
+  --transform-scale-x: 1.5;
+  --transform-scale-y: 1.5;
+}
+
+.focus\:scale-x-0:focus {
+  --transform-scale-x: 0;
+}
+
+.focus\:scale-x-50:focus {
+  --transform-scale-x: .5;
+}
+
+.focus\:scale-x-75:focus {
+  --transform-scale-x: .75;
+}
+
+.focus\:scale-x-90:focus {
+  --transform-scale-x: .9;
+}
+
+.focus\:scale-x-95:focus {
+  --transform-scale-x: .95;
+}
+
+.focus\:scale-x-100:focus {
+  --transform-scale-x: 1;
+}
+
+.focus\:scale-x-105:focus {
+  --transform-scale-x: 1.05;
+}
+
+.focus\:scale-x-110:focus {
+  --transform-scale-x: 1.1;
+}
+
+.focus\:scale-x-125:focus {
+  --transform-scale-x: 1.25;
+}
+
+.focus\:scale-x-150:focus {
+  --transform-scale-x: 1.5;
+}
+
+.focus\:scale-y-0:focus {
+  --transform-scale-y: 0;
+}
+
+.focus\:scale-y-50:focus {
+  --transform-scale-y: .5;
+}
+
+.focus\:scale-y-75:focus {
+  --transform-scale-y: .75;
+}
+
+.focus\:scale-y-90:focus {
+  --transform-scale-y: .9;
+}
+
+.focus\:scale-y-95:focus {
+  --transform-scale-y: .95;
+}
+
+.focus\:scale-y-100:focus {
+  --transform-scale-y: 1;
+}
+
+.focus\:scale-y-105:focus {
+  --transform-scale-y: 1.05;
+}
+
+.focus\:scale-y-110:focus {
+  --transform-scale-y: 1.1;
+}
+
+.focus\:scale-y-125:focus {
+  --transform-scale-y: 1.25;
+}
+
+.focus\:scale-y-150:focus {
+  --transform-scale-y: 1.5;
+}
+
+.rotate-0 {
+  --transform-rotate: 0;
+}
+
+.rotate-45 {
+  --transform-rotate: 45deg;
+}
+
+.rotate-90 {
+  --transform-rotate: 90deg;
+}
+
+.rotate-180 {
+  --transform-rotate: 180deg;
+}
+
+.-rotate-180 {
+  --transform-rotate: -180deg;
+}
+
+.-rotate-90 {
+  --transform-rotate: -90deg;
+}
+
+.-rotate-45 {
+  --transform-rotate: -45deg;
+}
+
+.hover\:rotate-0:hover {
+  --transform-rotate: 0;
+}
+
+.hover\:rotate-45:hover {
+  --transform-rotate: 45deg;
+}
+
+.hover\:rotate-90:hover {
+  --transform-rotate: 90deg;
+}
+
+.hover\:rotate-180:hover {
+  --transform-rotate: 180deg;
+}
+
+.hover\:-rotate-180:hover {
+  --transform-rotate: -180deg;
+}
+
+.hover\:-rotate-90:hover {
+  --transform-rotate: -90deg;
+}
+
+.hover\:-rotate-45:hover {
+  --transform-rotate: -45deg;
+}
+
+.focus\:rotate-0:focus {
+  --transform-rotate: 0;
+}
+
+.focus\:rotate-45:focus {
+  --transform-rotate: 45deg;
+}
+
+.focus\:rotate-90:focus {
+  --transform-rotate: 90deg;
+}
+
+.focus\:rotate-180:focus {
+  --transform-rotate: 180deg;
+}
+
+.focus\:-rotate-180:focus {
+  --transform-rotate: -180deg;
+}
+
+.focus\:-rotate-90:focus {
+  --transform-rotate: -90deg;
+}
+
+.focus\:-rotate-45:focus {
+  --transform-rotate: -45deg;
+}
+
+.translate-x-0 {
+  --transform-translate-x: 0;
+}
+
+.translate-x-1 {
+  --transform-translate-x: 0.25rem;
+}
+
+.translate-x-2 {
+  --transform-translate-x: 0.5rem;
+}
+
+.translate-x-3 {
+  --transform-translate-x: 0.75rem;
+}
+
+.translate-x-4 {
+  --transform-translate-x: 1rem;
+}
+
+.translate-x-5 {
+  --transform-translate-x: 1.25rem;
+}
+
+.translate-x-6 {
+  --transform-translate-x: 1.5rem;
+}
+
+.translate-x-8 {
+  --transform-translate-x: 2rem;
+}
+
+.translate-x-10 {
+  --transform-translate-x: 2.5rem;
+}
+
+.translate-x-12 {
+  --transform-translate-x: 3rem;
+}
+
+.translate-x-16 {
+  --transform-translate-x: 4rem;
+}
+
+.translate-x-20 {
+  --transform-translate-x: 5rem;
+}
+
+.translate-x-24 {
+  --transform-translate-x: 6rem;
+}
+
+.translate-x-32 {
+  --transform-translate-x: 8rem;
+}
+
+.translate-x-40 {
+  --transform-translate-x: 10rem;
+}
+
+.translate-x-48 {
+  --transform-translate-x: 12rem;
+}
+
+.translate-x-56 {
+  --transform-translate-x: 14rem;
+}
+
+.translate-x-64 {
+  --transform-translate-x: 16rem;
+}
+
+.translate-x-px {
+  --transform-translate-x: 1px;
+}
+
+.-translate-x-1 {
+  --transform-translate-x: -0.25rem;
+}
+
+.-translate-x-2 {
+  --transform-translate-x: -0.5rem;
+}
+
+.-translate-x-3 {
+  --transform-translate-x: -0.75rem;
+}
+
+.-translate-x-4 {
+  --transform-translate-x: -1rem;
+}
+
+.-translate-x-5 {
+  --transform-translate-x: -1.25rem;
+}
+
+.-translate-x-6 {
+  --transform-translate-x: -1.5rem;
+}
+
+.-translate-x-8 {
+  --transform-translate-x: -2rem;
+}
+
+.-translate-x-10 {
+  --transform-translate-x: -2.5rem;
+}
+
+.-translate-x-12 {
+  --transform-translate-x: -3rem;
+}
+
+.-translate-x-16 {
+  --transform-translate-x: -4rem;
+}
+
+.-translate-x-20 {
+  --transform-translate-x: -5rem;
+}
+
+.-translate-x-24 {
+  --transform-translate-x: -6rem;
+}
+
+.-translate-x-32 {
+  --transform-translate-x: -8rem;
+}
+
+.-translate-x-40 {
+  --transform-translate-x: -10rem;
+}
+
+.-translate-x-48 {
+  --transform-translate-x: -12rem;
+}
+
+.-translate-x-56 {
+  --transform-translate-x: -14rem;
+}
+
+.-translate-x-64 {
+  --transform-translate-x: -16rem;
+}
+
+.-translate-x-px {
+  --transform-translate-x: -1px;
+}
+
+.-translate-x-full {
+  --transform-translate-x: -100%;
+}
+
+.-translate-x-1\/2 {
+  --transform-translate-x: -50%;
+}
+
+.translate-x-1\/2 {
+  --transform-translate-x: 50%;
+}
+
+.translate-x-full {
+  --transform-translate-x: 100%;
+}
+
+.translate-y-0 {
+  --transform-translate-y: 0;
+}
+
+.translate-y-1 {
+  --transform-translate-y: 0.25rem;
+}
+
+.translate-y-2 {
+  --transform-translate-y: 0.5rem;
+}
+
+.translate-y-3 {
+  --transform-translate-y: 0.75rem;
+}
+
+.translate-y-4 {
+  --transform-translate-y: 1rem;
+}
+
+.translate-y-5 {
+  --transform-translate-y: 1.25rem;
+}
+
+.translate-y-6 {
+  --transform-translate-y: 1.5rem;
+}
+
+.translate-y-8 {
+  --transform-translate-y: 2rem;
+}
+
+.translate-y-10 {
+  --transform-translate-y: 2.5rem;
+}
+
+.translate-y-12 {
+  --transform-translate-y: 3rem;
+}
+
+.translate-y-16 {
+  --transform-translate-y: 4rem;
+}
+
+.translate-y-20 {
+  --transform-translate-y: 5rem;
+}
+
+.translate-y-24 {
+  --transform-translate-y: 6rem;
+}
+
+.translate-y-32 {
+  --transform-translate-y: 8rem;
+}
+
+.translate-y-40 {
+  --transform-translate-y: 10rem;
+}
+
+.translate-y-48 {
+  --transform-translate-y: 12rem;
+}
+
+.translate-y-56 {
+  --transform-translate-y: 14rem;
+}
+
+.translate-y-64 {
+  --transform-translate-y: 16rem;
+}
+
+.translate-y-px {
+  --transform-translate-y: 1px;
+}
+
+.-translate-y-1 {
+  --transform-translate-y: -0.25rem;
+}
+
+.-translate-y-2 {
+  --transform-translate-y: -0.5rem;
+}
+
+.-translate-y-3 {
+  --transform-translate-y: -0.75rem;
+}
+
+.-translate-y-4 {
+  --transform-translate-y: -1rem;
+}
+
+.-translate-y-5 {
+  --transform-translate-y: -1.25rem;
+}
+
+.-translate-y-6 {
+  --transform-translate-y: -1.5rem;
+}
+
+.-translate-y-8 {
+  --transform-translate-y: -2rem;
+}
+
+.-translate-y-10 {
+  --transform-translate-y: -2.5rem;
+}
+
+.-translate-y-12 {
+  --transform-translate-y: -3rem;
+}
+
+.-translate-y-16 {
+  --transform-translate-y: -4rem;
+}
+
+.-translate-y-20 {
+  --transform-translate-y: -5rem;
+}
+
+.-translate-y-24 {
+  --transform-translate-y: -6rem;
+}
+
+.-translate-y-32 {
+  --transform-translate-y: -8rem;
+}
+
+.-translate-y-40 {
+  --transform-translate-y: -10rem;
+}
+
+.-translate-y-48 {
+  --transform-translate-y: -12rem;
+}
+
+.-translate-y-56 {
+  --transform-translate-y: -14rem;
+}
+
+.-translate-y-64 {
+  --transform-translate-y: -16rem;
+}
+
+.-translate-y-px {
+  --transform-translate-y: -1px;
+}
+
+.-translate-y-full {
+  --transform-translate-y: -100%;
+}
+
+.-translate-y-1\/2 {
+  --transform-translate-y: -50%;
+}
+
+.translate-y-1\/2 {
+  --transform-translate-y: 50%;
+}
+
+.translate-y-full {
+  --transform-translate-y: 100%;
+}
+
+.hover\:translate-x-0:hover {
+  --transform-translate-x: 0;
+}
+
+.hover\:translate-x-1:hover {
+  --transform-translate-x: 0.25rem;
+}
+
+.hover\:translate-x-2:hover {
+  --transform-translate-x: 0.5rem;
+}
+
+.hover\:translate-x-3:hover {
+  --transform-translate-x: 0.75rem;
+}
+
+.hover\:translate-x-4:hover {
+  --transform-translate-x: 1rem;
+}
+
+.hover\:translate-x-5:hover {
+  --transform-translate-x: 1.25rem;
+}
+
+.hover\:translate-x-6:hover {
+  --transform-translate-x: 1.5rem;
+}
+
+.hover\:translate-x-8:hover {
+  --transform-translate-x: 2rem;
+}
+
+.hover\:translate-x-10:hover {
+  --transform-translate-x: 2.5rem;
+}
+
+.hover\:translate-x-12:hover {
+  --transform-translate-x: 3rem;
+}
+
+.hover\:translate-x-16:hover {
+  --transform-translate-x: 4rem;
+}
+
+.hover\:translate-x-20:hover {
+  --transform-translate-x: 5rem;
+}
+
+.hover\:translate-x-24:hover {
+  --transform-translate-x: 6rem;
+}
+
+.hover\:translate-x-32:hover {
+  --transform-translate-x: 8rem;
+}
+
+.hover\:translate-x-40:hover {
+  --transform-translate-x: 10rem;
+}
+
+.hover\:translate-x-48:hover {
+  --transform-translate-x: 12rem;
+}
+
+.hover\:translate-x-56:hover {
+  --transform-translate-x: 14rem;
+}
+
+.hover\:translate-x-64:hover {
+  --transform-translate-x: 16rem;
+}
+
+.hover\:translate-x-px:hover {
+  --transform-translate-x: 1px;
+}
+
+.hover\:-translate-x-1:hover {
+  --transform-translate-x: -0.25rem;
+}
+
+.hover\:-translate-x-2:hover {
+  --transform-translate-x: -0.5rem;
+}
+
+.hover\:-translate-x-3:hover {
+  --transform-translate-x: -0.75rem;
+}
+
+.hover\:-translate-x-4:hover {
+  --transform-translate-x: -1rem;
+}
+
+.hover\:-translate-x-5:hover {
+  --transform-translate-x: -1.25rem;
+}
+
+.hover\:-translate-x-6:hover {
+  --transform-translate-x: -1.5rem;
+}
+
+.hover\:-translate-x-8:hover {
+  --transform-translate-x: -2rem;
+}
+
+.hover\:-translate-x-10:hover {
+  --transform-translate-x: -2.5rem;
+}
+
+.hover\:-translate-x-12:hover {
+  --transform-translate-x: -3rem;
+}
+
+.hover\:-translate-x-16:hover {
+  --transform-translate-x: -4rem;
+}
+
+.hover\:-translate-x-20:hover {
+  --transform-translate-x: -5rem;
+}
+
+.hover\:-translate-x-24:hover {
+  --transform-translate-x: -6rem;
+}
+
+.hover\:-translate-x-32:hover {
+  --transform-translate-x: -8rem;
+}
+
+.hover\:-translate-x-40:hover {
+  --transform-translate-x: -10rem;
+}
+
+.hover\:-translate-x-48:hover {
+  --transform-translate-x: -12rem;
+}
+
+.hover\:-translate-x-56:hover {
+  --transform-translate-x: -14rem;
+}
+
+.hover\:-translate-x-64:hover {
+  --transform-translate-x: -16rem;
+}
+
+.hover\:-translate-x-px:hover {
+  --transform-translate-x: -1px;
+}
+
+.hover\:-translate-x-full:hover {
+  --transform-translate-x: -100%;
+}
+
+.hover\:-translate-x-1\/2:hover {
+  --transform-translate-x: -50%;
+}
+
+.hover\:translate-x-1\/2:hover {
+  --transform-translate-x: 50%;
+}
+
+.hover\:translate-x-full:hover {
+  --transform-translate-x: 100%;
+}
+
+.hover\:translate-y-0:hover {
+  --transform-translate-y: 0;
+}
+
+.hover\:translate-y-1:hover {
+  --transform-translate-y: 0.25rem;
+}
+
+.hover\:translate-y-2:hover {
+  --transform-translate-y: 0.5rem;
+}
+
+.hover\:translate-y-3:hover {
+  --transform-translate-y: 0.75rem;
+}
+
+.hover\:translate-y-4:hover {
+  --transform-translate-y: 1rem;
+}
+
+.hover\:translate-y-5:hover {
+  --transform-translate-y: 1.25rem;
+}
+
+.hover\:translate-y-6:hover {
+  --transform-translate-y: 1.5rem;
+}
+
+.hover\:translate-y-8:hover {
+  --transform-translate-y: 2rem;
+}
+
+.hover\:translate-y-10:hover {
+  --transform-translate-y: 2.5rem;
+}
+
+.hover\:translate-y-12:hover {
+  --transform-translate-y: 3rem;
+}
+
+.hover\:translate-y-16:hover {
+  --transform-translate-y: 4rem;
+}
+
+.hover\:translate-y-20:hover {
+  --transform-translate-y: 5rem;
+}
+
+.hover\:translate-y-24:hover {
+  --transform-translate-y: 6rem;
+}
+
+.hover\:translate-y-32:hover {
+  --transform-translate-y: 8rem;
+}
+
+.hover\:translate-y-40:hover {
+  --transform-translate-y: 10rem;
+}
+
+.hover\:translate-y-48:hover {
+  --transform-translate-y: 12rem;
+}
+
+.hover\:translate-y-56:hover {
+  --transform-translate-y: 14rem;
+}
+
+.hover\:translate-y-64:hover {
+  --transform-translate-y: 16rem;
+}
+
+.hover\:translate-y-px:hover {
+  --transform-translate-y: 1px;
+}
+
+.hover\:-translate-y-1:hover {
+  --transform-translate-y: -0.25rem;
+}
+
+.hover\:-translate-y-2:hover {
+  --transform-translate-y: -0.5rem;
+}
+
+.hover\:-translate-y-3:hover {
+  --transform-translate-y: -0.75rem;
+}
+
+.hover\:-translate-y-4:hover {
+  --transform-translate-y: -1rem;
+}
+
+.hover\:-translate-y-5:hover {
+  --transform-translate-y: -1.25rem;
+}
+
+.hover\:-translate-y-6:hover {
+  --transform-translate-y: -1.5rem;
+}
+
+.hover\:-translate-y-8:hover {
+  --transform-translate-y: -2rem;
+}
+
+.hover\:-translate-y-10:hover {
+  --transform-translate-y: -2.5rem;
+}
+
+.hover\:-translate-y-12:hover {
+  --transform-translate-y: -3rem;
+}
+
+.hover\:-translate-y-16:hover {
+  --transform-translate-y: -4rem;
+}
+
+.hover\:-translate-y-20:hover {
+  --transform-translate-y: -5rem;
+}
+
+.hover\:-translate-y-24:hover {
+  --transform-translate-y: -6rem;
+}
+
+.hover\:-translate-y-32:hover {
+  --transform-translate-y: -8rem;
+}
+
+.hover\:-translate-y-40:hover {
+  --transform-translate-y: -10rem;
+}
+
+.hover\:-translate-y-48:hover {
+  --transform-translate-y: -12rem;
+}
+
+.hover\:-translate-y-56:hover {
+  --transform-translate-y: -14rem;
+}
+
+.hover\:-translate-y-64:hover {
+  --transform-translate-y: -16rem;
+}
+
+.hover\:-translate-y-px:hover {
+  --transform-translate-y: -1px;
+}
+
+.hover\:-translate-y-full:hover {
+  --transform-translate-y: -100%;
+}
+
+.hover\:-translate-y-1\/2:hover {
+  --transform-translate-y: -50%;
+}
+
+.hover\:translate-y-1\/2:hover {
+  --transform-translate-y: 50%;
+}
+
+.hover\:translate-y-full:hover {
+  --transform-translate-y: 100%;
+}
+
+.focus\:translate-x-0:focus {
+  --transform-translate-x: 0;
+}
+
+.focus\:translate-x-1:focus {
+  --transform-translate-x: 0.25rem;
+}
+
+.focus\:translate-x-2:focus {
+  --transform-translate-x: 0.5rem;
+}
+
+.focus\:translate-x-3:focus {
+  --transform-translate-x: 0.75rem;
+}
+
+.focus\:translate-x-4:focus {
+  --transform-translate-x: 1rem;
+}
+
+.focus\:translate-x-5:focus {
+  --transform-translate-x: 1.25rem;
+}
+
+.focus\:translate-x-6:focus {
+  --transform-translate-x: 1.5rem;
+}
+
+.focus\:translate-x-8:focus {
+  --transform-translate-x: 2rem;
+}
+
+.focus\:translate-x-10:focus {
+  --transform-translate-x: 2.5rem;
+}
+
+.focus\:translate-x-12:focus {
+  --transform-translate-x: 3rem;
+}
+
+.focus\:translate-x-16:focus {
+  --transform-translate-x: 4rem;
+}
+
+.focus\:translate-x-20:focus {
+  --transform-translate-x: 5rem;
+}
+
+.focus\:translate-x-24:focus {
+  --transform-translate-x: 6rem;
+}
+
+.focus\:translate-x-32:focus {
+  --transform-translate-x: 8rem;
+}
+
+.focus\:translate-x-40:focus {
+  --transform-translate-x: 10rem;
+}
+
+.focus\:translate-x-48:focus {
+  --transform-translate-x: 12rem;
+}
+
+.focus\:translate-x-56:focus {
+  --transform-translate-x: 14rem;
+}
+
+.focus\:translate-x-64:focus {
+  --transform-translate-x: 16rem;
+}
+
+.focus\:translate-x-px:focus {
+  --transform-translate-x: 1px;
+}
+
+.focus\:-translate-x-1:focus {
+  --transform-translate-x: -0.25rem;
+}
+
+.focus\:-translate-x-2:focus {
+  --transform-translate-x: -0.5rem;
+}
+
+.focus\:-translate-x-3:focus {
+  --transform-translate-x: -0.75rem;
+}
+
+.focus\:-translate-x-4:focus {
+  --transform-translate-x: -1rem;
+}
+
+.focus\:-translate-x-5:focus {
+  --transform-translate-x: -1.25rem;
+}
+
+.focus\:-translate-x-6:focus {
+  --transform-translate-x: -1.5rem;
+}
+
+.focus\:-translate-x-8:focus {
+  --transform-translate-x: -2rem;
+}
+
+.focus\:-translate-x-10:focus {
+  --transform-translate-x: -2.5rem;
+}
+
+.focus\:-translate-x-12:focus {
+  --transform-translate-x: -3rem;
+}
+
+.focus\:-translate-x-16:focus {
+  --transform-translate-x: -4rem;
+}
+
+.focus\:-translate-x-20:focus {
+  --transform-translate-x: -5rem;
+}
+
+.focus\:-translate-x-24:focus {
+  --transform-translate-x: -6rem;
+}
+
+.focus\:-translate-x-32:focus {
+  --transform-translate-x: -8rem;
+}
+
+.focus\:-translate-x-40:focus {
+  --transform-translate-x: -10rem;
+}
+
+.focus\:-translate-x-48:focus {
+  --transform-translate-x: -12rem;
+}
+
+.focus\:-translate-x-56:focus {
+  --transform-translate-x: -14rem;
+}
+
+.focus\:-translate-x-64:focus {
+  --transform-translate-x: -16rem;
+}
+
+.focus\:-translate-x-px:focus {
+  --transform-translate-x: -1px;
+}
+
+.focus\:-translate-x-full:focus {
+  --transform-translate-x: -100%;
+}
+
+.focus\:-translate-x-1\/2:focus {
+  --transform-translate-x: -50%;
+}
+
+.focus\:translate-x-1\/2:focus {
+  --transform-translate-x: 50%;
+}
+
+.focus\:translate-x-full:focus {
+  --transform-translate-x: 100%;
+}
+
+.focus\:translate-y-0:focus {
+  --transform-translate-y: 0;
+}
+
+.focus\:translate-y-1:focus {
+  --transform-translate-y: 0.25rem;
+}
+
+.focus\:translate-y-2:focus {
+  --transform-translate-y: 0.5rem;
+}
+
+.focus\:translate-y-3:focus {
+  --transform-translate-y: 0.75rem;
+}
+
+.focus\:translate-y-4:focus {
+  --transform-translate-y: 1rem;
+}
+
+.focus\:translate-y-5:focus {
+  --transform-translate-y: 1.25rem;
+}
+
+.focus\:translate-y-6:focus {
+  --transform-translate-y: 1.5rem;
+}
+
+.focus\:translate-y-8:focus {
+  --transform-translate-y: 2rem;
+}
+
+.focus\:translate-y-10:focus {
+  --transform-translate-y: 2.5rem;
+}
+
+.focus\:translate-y-12:focus {
+  --transform-translate-y: 3rem;
+}
+
+.focus\:translate-y-16:focus {
+  --transform-translate-y: 4rem;
+}
+
+.focus\:translate-y-20:focus {
+  --transform-translate-y: 5rem;
+}
+
+.focus\:translate-y-24:focus {
+  --transform-translate-y: 6rem;
+}
+
+.focus\:translate-y-32:focus {
+  --transform-translate-y: 8rem;
+}
+
+.focus\:translate-y-40:focus {
+  --transform-translate-y: 10rem;
+}
+
+.focus\:translate-y-48:focus {
+  --transform-translate-y: 12rem;
+}
+
+.focus\:translate-y-56:focus {
+  --transform-translate-y: 14rem;
+}
+
+.focus\:translate-y-64:focus {
+  --transform-translate-y: 16rem;
+}
+
+.focus\:translate-y-px:focus {
+  --transform-translate-y: 1px;
+}
+
+.focus\:-translate-y-1:focus {
+  --transform-translate-y: -0.25rem;
+}
+
+.focus\:-translate-y-2:focus {
+  --transform-translate-y: -0.5rem;
+}
+
+.focus\:-translate-y-3:focus {
+  --transform-translate-y: -0.75rem;
+}
+
+.focus\:-translate-y-4:focus {
+  --transform-translate-y: -1rem;
+}
+
+.focus\:-translate-y-5:focus {
+  --transform-translate-y: -1.25rem;
+}
+
+.focus\:-translate-y-6:focus {
+  --transform-translate-y: -1.5rem;
+}
+
+.focus\:-translate-y-8:focus {
+  --transform-translate-y: -2rem;
+}
+
+.focus\:-translate-y-10:focus {
+  --transform-translate-y: -2.5rem;
+}
+
+.focus\:-translate-y-12:focus {
+  --transform-translate-y: -3rem;
+}
+
+.focus\:-translate-y-16:focus {
+  --transform-translate-y: -4rem;
+}
+
+.focus\:-translate-y-20:focus {
+  --transform-translate-y: -5rem;
+}
+
+.focus\:-translate-y-24:focus {
+  --transform-translate-y: -6rem;
+}
+
+.focus\:-translate-y-32:focus {
+  --transform-translate-y: -8rem;
+}
+
+.focus\:-translate-y-40:focus {
+  --transform-translate-y: -10rem;
+}
+
+.focus\:-translate-y-48:focus {
+  --transform-translate-y: -12rem;
+}
+
+.focus\:-translate-y-56:focus {
+  --transform-translate-y: -14rem;
+}
+
+.focus\:-translate-y-64:focus {
+  --transform-translate-y: -16rem;
+}
+
+.focus\:-translate-y-px:focus {
+  --transform-translate-y: -1px;
+}
+
+.focus\:-translate-y-full:focus {
+  --transform-translate-y: -100%;
+}
+
+.focus\:-translate-y-1\/2:focus {
+  --transform-translate-y: -50%;
+}
+
+.focus\:translate-y-1\/2:focus {
+  --transform-translate-y: 50%;
+}
+
+.focus\:translate-y-full:focus {
+  --transform-translate-y: 100%;
+}
+
+.skew-x-0 {
+  --transform-skew-x: 0;
+}
+
+.skew-x-3 {
+  --transform-skew-x: 3deg;
+}
+
+.skew-x-6 {
+  --transform-skew-x: 6deg;
+}
+
+.skew-x-12 {
+  --transform-skew-x: 12deg;
+}
+
+.-skew-x-12 {
+  --transform-skew-x: -12deg;
+}
+
+.-skew-x-6 {
+  --transform-skew-x: -6deg;
+}
+
+.-skew-x-3 {
+  --transform-skew-x: -3deg;
+}
+
+.skew-y-0 {
+  --transform-skew-y: 0;
+}
+
+.skew-y-3 {
+  --transform-skew-y: 3deg;
+}
+
+.skew-y-6 {
+  --transform-skew-y: 6deg;
+}
+
+.skew-y-12 {
+  --transform-skew-y: 12deg;
+}
+
+.-skew-y-12 {
+  --transform-skew-y: -12deg;
+}
+
+.-skew-y-6 {
+  --transform-skew-y: -6deg;
+}
+
+.-skew-y-3 {
+  --transform-skew-y: -3deg;
+}
+
+.hover\:skew-x-0:hover {
+  --transform-skew-x: 0;
+}
+
+.hover\:skew-x-3:hover {
+  --transform-skew-x: 3deg;
+}
+
+.hover\:skew-x-6:hover {
+  --transform-skew-x: 6deg;
+}
+
+.hover\:skew-x-12:hover {
+  --transform-skew-x: 12deg;
+}
+
+.hover\:-skew-x-12:hover {
+  --transform-skew-x: -12deg;
+}
+
+.hover\:-skew-x-6:hover {
+  --transform-skew-x: -6deg;
+}
+
+.hover\:-skew-x-3:hover {
+  --transform-skew-x: -3deg;
+}
+
+.hover\:skew-y-0:hover {
+  --transform-skew-y: 0;
+}
+
+.hover\:skew-y-3:hover {
+  --transform-skew-y: 3deg;
+}
+
+.hover\:skew-y-6:hover {
+  --transform-skew-y: 6deg;
+}
+
+.hover\:skew-y-12:hover {
+  --transform-skew-y: 12deg;
+}
+
+.hover\:-skew-y-12:hover {
+  --transform-skew-y: -12deg;
+}
+
+.hover\:-skew-y-6:hover {
+  --transform-skew-y: -6deg;
+}
+
+.hover\:-skew-y-3:hover {
+  --transform-skew-y: -3deg;
+}
+
+.focus\:skew-x-0:focus {
+  --transform-skew-x: 0;
+}
+
+.focus\:skew-x-3:focus {
+  --transform-skew-x: 3deg;
+}
+
+.focus\:skew-x-6:focus {
+  --transform-skew-x: 6deg;
+}
+
+.focus\:skew-x-12:focus {
+  --transform-skew-x: 12deg;
+}
+
+.focus\:-skew-x-12:focus {
+  --transform-skew-x: -12deg;
+}
+
+.focus\:-skew-x-6:focus {
+  --transform-skew-x: -6deg;
+}
+
+.focus\:-skew-x-3:focus {
+  --transform-skew-x: -3deg;
+}
+
+.focus\:skew-y-0:focus {
+  --transform-skew-y: 0;
+}
+
+.focus\:skew-y-3:focus {
+  --transform-skew-y: 3deg;
+}
+
+.focus\:skew-y-6:focus {
+  --transform-skew-y: 6deg;
+}
+
+.focus\:skew-y-12:focus {
+  --transform-skew-y: 12deg;
+}
+
+.focus\:-skew-y-12:focus {
+  --transform-skew-y: -12deg;
+}
+
+.focus\:-skew-y-6:focus {
+  --transform-skew-y: -6deg;
+}
+
+.focus\:-skew-y-3:focus {
+  --transform-skew-y: -3deg;
+}
+
+.transition-none {
+  transition-property: none;
+}
+
+.transition-all {
+  transition-property: all;
+}
+
+.transition {
+  transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+}
+
+.transition-colors {
+  transition-property: background-color, border-color, color, fill, stroke;
+}
+
+.transition-opacity {
+  transition-property: opacity;
+}
+
+.transition-shadow {
+  transition-property: box-shadow;
+}
+
+.transition-transform {
+  transition-property: transform;
+}
+
+.ease-linear {
+  transition-timing-function: linear;
+}
+
+.ease-in {
+  transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+}
+
+.ease-out {
+  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+}
+
+.ease-in-out {
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.duration-75 {
+  transition-duration: 75ms;
+}
+
+.duration-100 {
+  transition-duration: 100ms;
+}
+
+.duration-150 {
+  transition-duration: 150ms;
+}
+
+.duration-200 {
+  transition-duration: 200ms;
+}
+
+.duration-300 {
+  transition-duration: 300ms;
+}
+
+.duration-500 {
+  transition-duration: 500ms;
+}
+
+.duration-700 {
+  transition-duration: 700ms;
+}
+
+.duration-1000 {
+  transition-duration: 1000ms;
+}
+
+.delay-75 {
+  transition-delay: 75ms;
+}
+
+.delay-100 {
+  transition-delay: 100ms;
+}
+
+.delay-150 {
+  transition-delay: 150ms;
+}
+
+.delay-200 {
+  transition-delay: 200ms;
+}
+
+.delay-300 {
+  transition-delay: 300ms;
+}
+
+.delay-500 {
+  transition-delay: 500ms;
+}
+
+.delay-700 {
+  transition-delay: 700ms;
+}
+
+.delay-1000 {
+  transition-delay: 1000ms;
+}
+
+@-webkit-keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@-webkit-keyframes ping {
+  75%, 100% {
+    transform: scale(2);
+    opacity: 0;
+  }
+}
+
+@keyframes ping {
+  75%, 100% {
+    transform: scale(2);
+    opacity: 0;
+  }
+}
+
+@-webkit-keyframes pulse {
+  50% {
+    opacity: .5;
+  }
+}
+
+@keyframes pulse {
+  50% {
+    opacity: .5;
+  }
+}
+
+@-webkit-keyframes bounce {
+  0%, 100% {
+    transform: translateY(-25%);
+    -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
+            animation-timing-function: cubic-bezier(0.8,0,1,1);
+  }
+
+  50% {
+    transform: none;
+    -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
+            animation-timing-function: cubic-bezier(0,0,0.2,1);
+  }
+}
+
+@keyframes bounce {
+  0%, 100% {
+    transform: translateY(-25%);
+    -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
+            animation-timing-function: cubic-bezier(0.8,0,1,1);
+  }
+
+  50% {
+    transform: none;
+    -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
+            animation-timing-function: cubic-bezier(0,0,0.2,1);
+  }
+}
+
+.animate-none {
+  -webkit-animation: none;
+          animation: none;
+}
+
+.animate-spin {
+  -webkit-animation: spin 1s linear infinite;
+          animation: spin 1s linear infinite;
+}
+
+.animate-ping {
+  -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+          animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+}
+
+.animate-pulse {
+  -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+          animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+.animate-bounce {
+  -webkit-animation: bounce 1s infinite;
+          animation: bounce 1s infinite;
+}
+
+@media (min-width: 640px) {
+  .sm\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .sm\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .sm\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .sm\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .sm\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .sm\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .sm\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .sm\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .sm\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .sm\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .sm\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .sm\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .sm\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .sm\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .sm\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .sm\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .sm\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .sm\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .sm\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .sm\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .sm\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .sm\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .sm\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .sm\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .sm\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .sm\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .sm\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .sm\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .sm\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .sm\:bg-local {
+    background-attachment: local;
+  }
+
+  .sm\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .sm\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .sm\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .sm\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .sm\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .sm\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .sm\:bg-current {
+    background-color: currentColor;
+  }
+
+  .sm\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .sm\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .sm\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .sm\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .sm\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .sm\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .sm\:bg-none {
+    background-image: none;
+  }
+
+  .sm\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .sm\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .sm\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .sm\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .sm\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .sm\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .sm\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .sm\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .sm\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .sm\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .sm\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .sm\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .sm\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .sm\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .sm\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .sm\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .sm\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .sm\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .sm\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .sm\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .sm\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .sm\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .sm\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .sm\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .sm\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .sm\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .sm\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .sm\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .sm\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .sm\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .sm\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .sm\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .sm\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .sm\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .sm\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .sm\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .sm\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .sm\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .sm\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .sm\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .sm\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .sm\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .sm\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .sm\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .sm\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .sm\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .sm\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .sm\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .sm\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .sm\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .sm\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .sm\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .sm\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .sm\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .sm\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .sm\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .sm\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .sm\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .sm\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .sm\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .sm\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .sm\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .sm\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .sm\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .sm\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .sm\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .sm\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .sm\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .sm\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .sm\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .sm\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .sm\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .sm\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .sm\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .sm\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .sm\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .sm\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .sm\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .sm\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .sm\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .sm\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .sm\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .sm\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .sm\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .sm\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .sm\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .sm\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .sm\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .sm\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .sm\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .sm\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .sm\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .sm\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .sm\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .sm\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .sm\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .sm\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .sm\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .sm\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .sm\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .sm\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .sm\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .sm\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .sm\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .sm\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .sm\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .sm\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .sm\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .sm\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .sm\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .sm\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .sm\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .sm\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .sm\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .sm\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .sm\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .sm\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .sm\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .sm\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .sm\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .sm\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .sm\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .sm\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .sm\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .sm\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .sm\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .sm\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .sm\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .sm\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .sm\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .sm\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .sm\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .sm\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .sm\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .sm\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .sm\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .sm\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .sm\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .sm\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .sm\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .sm\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .sm\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .sm\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .sm\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .sm\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .sm\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .sm\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .sm\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .sm\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .sm\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .sm\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .sm\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .sm\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .sm\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .sm\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .sm\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .sm\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .sm\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .sm\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .sm\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .sm\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .sm\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .sm\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .sm\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .sm\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .sm\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .sm\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .sm\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .sm\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .sm\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .sm\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .sm\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .sm\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .sm\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .sm\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .sm\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .sm\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .sm\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .sm\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .sm\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .sm\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .sm\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .sm\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .sm\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .sm\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .sm\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .sm\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .sm\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .sm\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .sm\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .sm\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .sm\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .sm\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .sm\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .sm\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .sm\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .sm\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .sm\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .sm\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .sm\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .sm\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .sm\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .sm\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .sm\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .sm\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .sm\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .sm\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .sm\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .sm\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .sm\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .sm\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .sm\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .sm\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .sm\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .sm\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .sm\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .sm\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .sm\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .sm\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .sm\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .sm\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .sm\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .sm\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .sm\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .sm\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .sm\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .sm\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .sm\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .sm\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .sm\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .sm\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .sm\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .sm\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .sm\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .sm\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .sm\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .sm\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .sm\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .sm\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .sm\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .sm\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .sm\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .sm\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .sm\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .sm\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .sm\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .sm\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .sm\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .sm\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .sm\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .sm\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .sm\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .sm\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .sm\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .sm\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .sm\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .sm\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .sm\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .sm\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .sm\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .sm\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .sm\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .sm\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .sm\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .sm\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .sm\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .sm\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .sm\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .sm\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .sm\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .sm\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .sm\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .sm\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .sm\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .sm\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .sm\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .sm\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .sm\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .sm\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .sm\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .sm\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .sm\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .sm\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .sm\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .sm\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .sm\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .sm\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .sm\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .sm\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .sm\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .sm\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .sm\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .sm\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .sm\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .sm\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .sm\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .sm\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .sm\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .sm\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .sm\:bg-center {
+    background-position: center;
+  }
+
+  .sm\:bg-left {
+    background-position: left;
+  }
+
+  .sm\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .sm\:bg-left-top {
+    background-position: left top;
+  }
+
+  .sm\:bg-right {
+    background-position: right;
+  }
+
+  .sm\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .sm\:bg-right-top {
+    background-position: right top;
+  }
+
+  .sm\:bg-top {
+    background-position: top;
+  }
+
+  .sm\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .sm\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .sm\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .sm\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .sm\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .sm\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .sm\:bg-auto {
+    background-size: auto;
+  }
+
+  .sm\:bg-cover {
+    background-size: cover;
+  }
+
+  .sm\:bg-contain {
+    background-size: contain;
+  }
+
+  .sm\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .sm\:border-separate {
+    border-collapse: separate;
+  }
+
+  .sm\:border-transparent {
+    border-color: transparent;
+  }
+
+  .sm\:border-current {
+    border-color: currentColor;
+  }
+
+  .sm\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .sm\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .sm\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .sm\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .sm\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .sm\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .sm\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .sm\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .sm\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .sm\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .sm\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .sm\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .sm\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .sm\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .sm\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .sm\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .sm\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .sm\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .sm\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .sm\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .sm\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .sm\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .sm\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .sm\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .sm\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .sm\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .sm\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .sm\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .sm\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .sm\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .sm\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .sm\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .sm\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .sm\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .sm\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .sm\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .sm\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .sm\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .sm\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .sm\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .sm\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .sm\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .sm\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .sm\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .sm\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .sm\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .sm\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .sm\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .sm\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .sm\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .sm\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .sm\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .sm\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .sm\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .sm\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .sm\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .sm\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .sm\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .sm\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .sm\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .sm\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .sm\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .sm\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .sm\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .sm\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .sm\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .sm\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .sm\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .sm\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .sm\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .sm\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .sm\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .sm\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .sm\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .sm\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .sm\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .sm\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .sm\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .sm\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .sm\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .sm\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .sm\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .sm\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .sm\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .sm\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .sm\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .sm\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .sm\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .sm\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .sm\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .sm\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .sm\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .sm\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .sm\:rounded-none {
+    border-radius: 0;
+  }
+
+  .sm\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .sm\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .sm\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .sm\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .sm\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .sm\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .sm\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .sm\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .sm\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .sm\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .sm\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .sm\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .sm\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .sm\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .sm\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .sm\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .sm\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .sm\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .sm\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .sm\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .sm\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .sm\:border-solid {
+    border-style: solid;
+  }
+
+  .sm\:border-dashed {
+    border-style: dashed;
+  }
+
+  .sm\:border-dotted {
+    border-style: dotted;
+  }
+
+  .sm\:border-double {
+    border-style: double;
+  }
+
+  .sm\:border-none {
+    border-style: none;
+  }
+
+  .sm\:border-0 {
+    border-width: 0;
+  }
+
+  .sm\:border-2 {
+    border-width: 2px;
+  }
+
+  .sm\:border-4 {
+    border-width: 4px;
+  }
+
+  .sm\:border-8 {
+    border-width: 8px;
+  }
+
+  .sm\:border {
+    border-width: 1px;
+  }
+
+  .sm\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .sm\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .sm\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .sm\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .sm\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .sm\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .sm\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .sm\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .sm\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .sm\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .sm\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .sm\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .sm\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .sm\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .sm\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .sm\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .sm\:border-t {
+    border-top-width: 1px;
+  }
+
+  .sm\:border-r {
+    border-right-width: 1px;
+  }
+
+  .sm\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .sm\:border-l {
+    border-left-width: 1px;
+  }
+
+  .sm\:box-border {
+    box-sizing: border-box;
+  }
+
+  .sm\:box-content {
+    box-sizing: content-box;
+  }
+
+  .sm\:cursor-auto {
+    cursor: auto;
+  }
+
+  .sm\:cursor-default {
+    cursor: default;
+  }
+
+  .sm\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .sm\:cursor-wait {
+    cursor: wait;
+  }
+
+  .sm\:cursor-text {
+    cursor: text;
+  }
+
+  .sm\:cursor-move {
+    cursor: move;
+  }
+
+  .sm\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .sm\:block {
+    display: block;
+  }
+
+  .sm\:inline-block {
+    display: inline-block;
+  }
+
+  .sm\:inline {
+    display: inline;
+  }
+
+  .sm\:flex {
+    display: flex;
+  }
+
+  .sm\:inline-flex {
+    display: inline-flex;
+  }
+
+  .sm\:table {
+    display: table;
+  }
+
+  .sm\:table-caption {
+    display: table-caption;
+  }
+
+  .sm\:table-cell {
+    display: table-cell;
+  }
+
+  .sm\:table-column {
+    display: table-column;
+  }
+
+  .sm\:table-column-group {
+    display: table-column-group;
+  }
+
+  .sm\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .sm\:table-header-group {
+    display: table-header-group;
+  }
+
+  .sm\:table-row-group {
+    display: table-row-group;
+  }
+
+  .sm\:table-row {
+    display: table-row;
+  }
+
+  .sm\:flow-root {
+    display: flow-root;
+  }
+
+  .sm\:grid {
+    display: grid;
+  }
+
+  .sm\:inline-grid {
+    display: inline-grid;
+  }
+
+  .sm\:contents {
+    display: contents;
+  }
+
+  .sm\:hidden {
+    display: none;
+  }
+
+  .sm\:flex-row {
+    flex-direction: row;
+  }
+
+  .sm\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .sm\:flex-col {
+    flex-direction: column;
+  }
+
+  .sm\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .sm\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .sm\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .sm\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .sm\:place-items-auto {
+    place-items: auto;
+  }
+
+  .sm\:place-items-start {
+    place-items: start;
+  }
+
+  .sm\:place-items-end {
+    place-items: end;
+  }
+
+  .sm\:place-items-center {
+    place-items: center;
+  }
+
+  .sm\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .sm\:place-content-center {
+    place-content: center;
+  }
+
+  .sm\:place-content-start {
+    place-content: start;
+  }
+
+  .sm\:place-content-end {
+    place-content: end;
+  }
+
+  .sm\:place-content-between {
+    place-content: space-between;
+  }
+
+  .sm\:place-content-around {
+    place-content: space-around;
+  }
+
+  .sm\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .sm\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .sm\:place-self-auto {
+    place-self: auto;
+  }
+
+  .sm\:place-self-start {
+    place-self: start;
+  }
+
+  .sm\:place-self-end {
+    place-self: end;
+  }
+
+  .sm\:place-self-center {
+    place-self: center;
+  }
+
+  .sm\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .sm\:items-start {
+    align-items: flex-start;
+  }
+
+  .sm\:items-end {
+    align-items: flex-end;
+  }
+
+  .sm\:items-center {
+    align-items: center;
+  }
+
+  .sm\:items-baseline {
+    align-items: baseline;
+  }
+
+  .sm\:items-stretch {
+    align-items: stretch;
+  }
+
+  .sm\:content-center {
+    align-content: center;
+  }
+
+  .sm\:content-start {
+    align-content: flex-start;
+  }
+
+  .sm\:content-end {
+    align-content: flex-end;
+  }
+
+  .sm\:content-between {
+    align-content: space-between;
+  }
+
+  .sm\:content-around {
+    align-content: space-around;
+  }
+
+  .sm\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .sm\:self-auto {
+    align-self: auto;
+  }
+
+  .sm\:self-start {
+    align-self: flex-start;
+  }
+
+  .sm\:self-end {
+    align-self: flex-end;
+  }
+
+  .sm\:self-center {
+    align-self: center;
+  }
+
+  .sm\:self-stretch {
+    align-self: stretch;
+  }
+
+  .sm\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .sm\:justify-items-start {
+    justify-items: start;
+  }
+
+  .sm\:justify-items-end {
+    justify-items: end;
+  }
+
+  .sm\:justify-items-center {
+    justify-items: center;
+  }
+
+  .sm\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .sm\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .sm\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .sm\:justify-center {
+    justify-content: center;
+  }
+
+  .sm\:justify-between {
+    justify-content: space-between;
+  }
+
+  .sm\:justify-around {
+    justify-content: space-around;
+  }
+
+  .sm\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .sm\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .sm\:justify-self-start {
+    justify-self: start;
+  }
+
+  .sm\:justify-self-end {
+    justify-self: end;
+  }
+
+  .sm\:justify-self-center {
+    justify-self: center;
+  }
+
+  .sm\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .sm\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .sm\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .sm\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .sm\:flex-none {
+    flex: none;
+  }
+
+  .sm\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .sm\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .sm\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .sm\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .sm\:order-1 {
+    order: 1;
+  }
+
+  .sm\:order-2 {
+    order: 2;
+  }
+
+  .sm\:order-3 {
+    order: 3;
+  }
+
+  .sm\:order-4 {
+    order: 4;
+  }
+
+  .sm\:order-5 {
+    order: 5;
+  }
+
+  .sm\:order-6 {
+    order: 6;
+  }
+
+  .sm\:order-7 {
+    order: 7;
+  }
+
+  .sm\:order-8 {
+    order: 8;
+  }
+
+  .sm\:order-9 {
+    order: 9;
+  }
+
+  .sm\:order-10 {
+    order: 10;
+  }
+
+  .sm\:order-11 {
+    order: 11;
+  }
+
+  .sm\:order-12 {
+    order: 12;
+  }
+
+  .sm\:order-first {
+    order: -9999;
+  }
+
+  .sm\:order-last {
+    order: 9999;
+  }
+
+  .sm\:order-none {
+    order: 0;
+  }
+
+  .sm\:float-right {
+    float: right;
+  }
+
+  .sm\:float-left {
+    float: left;
+  }
+
+  .sm\:float-none {
+    float: none;
+  }
+
+  .sm\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .sm\:clear-left {
+    clear: left;
+  }
+
+  .sm\:clear-right {
+    clear: right;
+  }
+
+  .sm\:clear-both {
+    clear: both;
+  }
+
+  .sm\:clear-none {
+    clear: none;
+  }
+
+  .sm\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .sm\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .sm\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .sm\:font-hairline {
+    font-weight: 100;
+  }
+
+  .sm\:font-thin {
+    font-weight: 200;
+  }
+
+  .sm\:font-light {
+    font-weight: 300;
+  }
+
+  .sm\:font-normal {
+    font-weight: 400;
+  }
+
+  .sm\:font-medium {
+    font-weight: 500;
+  }
+
+  .sm\:font-semibold {
+    font-weight: 600;
+  }
+
+  .sm\:font-bold {
+    font-weight: 700;
+  }
+
+  .sm\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .sm\:font-black {
+    font-weight: 900;
+  }
+
+  .sm\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .sm\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .sm\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .sm\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .sm\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .sm\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .sm\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .sm\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .sm\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .sm\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .sm\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .sm\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .sm\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .sm\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .sm\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .sm\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .sm\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .sm\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .sm\:h-0 {
+    height: 0;
+  }
+
+  .sm\:h-1 {
+    height: 0.25rem;
+  }
+
+  .sm\:h-2 {
+    height: 0.5rem;
+  }
+
+  .sm\:h-3 {
+    height: 0.75rem;
+  }
+
+  .sm\:h-4 {
+    height: 1rem;
+  }
+
+  .sm\:h-5 {
+    height: 1.25rem;
+  }
+
+  .sm\:h-6 {
+    height: 1.5rem;
+  }
+
+  .sm\:h-8 {
+    height: 2rem;
+  }
+
+  .sm\:h-10 {
+    height: 2.5rem;
+  }
+
+  .sm\:h-12 {
+    height: 3rem;
+  }
+
+  .sm\:h-16 {
+    height: 4rem;
+  }
+
+  .sm\:h-20 {
+    height: 5rem;
+  }
+
+  .sm\:h-24 {
+    height: 6rem;
+  }
+
+  .sm\:h-32 {
+    height: 8rem;
+  }
+
+  .sm\:h-40 {
+    height: 10rem;
+  }
+
+  .sm\:h-48 {
+    height: 12rem;
+  }
+
+  .sm\:h-56 {
+    height: 14rem;
+  }
+
+  .sm\:h-64 {
+    height: 16rem;
+  }
+
+  .sm\:h-auto {
+    height: auto;
+  }
+
+  .sm\:h-px {
+    height: 1px;
+  }
+
+  .sm\:h-full {
+    height: 100%;
+  }
+
+  .sm\:h-screen {
+    height: 100vh;
+  }
+
+  .sm\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .sm\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .sm\:text-base {
+    font-size: 1rem;
+  }
+
+  .sm\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .sm\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .sm\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .sm\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .sm\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .sm\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .sm\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .sm\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .sm\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .sm\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .sm\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .sm\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .sm\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .sm\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .sm\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .sm\:leading-none {
+    line-height: 1;
+  }
+
+  .sm\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .sm\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .sm\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .sm\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .sm\:leading-loose {
+    line-height: 2;
+  }
+
+  .sm\:list-inside {
+    list-style-position: inside;
+  }
+
+  .sm\:list-outside {
+    list-style-position: outside;
+  }
+
+  .sm\:list-none {
+    list-style-type: none;
+  }
+
+  .sm\:list-disc {
+    list-style-type: disc;
+  }
+
+  .sm\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .sm\:m-0 {
+    margin: 0;
+  }
+
+  .sm\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .sm\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .sm\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .sm\:m-4 {
+    margin: 1rem;
+  }
+
+  .sm\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .sm\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .sm\:m-8 {
+    margin: 2rem;
+  }
+
+  .sm\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .sm\:m-12 {
+    margin: 3rem;
+  }
+
+  .sm\:m-16 {
+    margin: 4rem;
+  }
+
+  .sm\:m-20 {
+    margin: 5rem;
+  }
+
+  .sm\:m-24 {
+    margin: 6rem;
+  }
+
+  .sm\:m-32 {
+    margin: 8rem;
+  }
+
+  .sm\:m-40 {
+    margin: 10rem;
+  }
+
+  .sm\:m-48 {
+    margin: 12rem;
+  }
+
+  .sm\:m-56 {
+    margin: 14rem;
+  }
+
+  .sm\:m-64 {
+    margin: 16rem;
+  }
+
+  .sm\:m-auto {
+    margin: auto;
+  }
+
+  .sm\:m-px {
+    margin: 1px;
+  }
+
+  .sm\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .sm\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .sm\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .sm\:-m-4 {
+    margin: -1rem;
+  }
+
+  .sm\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .sm\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .sm\:-m-8 {
+    margin: -2rem;
+  }
+
+  .sm\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .sm\:-m-12 {
+    margin: -3rem;
+  }
+
+  .sm\:-m-16 {
+    margin: -4rem;
+  }
+
+  .sm\:-m-20 {
+    margin: -5rem;
+  }
+
+  .sm\:-m-24 {
+    margin: -6rem;
+  }
+
+  .sm\:-m-32 {
+    margin: -8rem;
+  }
+
+  .sm\:-m-40 {
+    margin: -10rem;
+  }
+
+  .sm\:-m-48 {
+    margin: -12rem;
+  }
+
+  .sm\:-m-56 {
+    margin: -14rem;
+  }
+
+  .sm\:-m-64 {
+    margin: -16rem;
+  }
+
+  .sm\:-m-px {
+    margin: -1px;
+  }
+
+  .sm\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .sm\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .sm\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .sm\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .sm\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .sm\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .sm\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .sm\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .sm\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .sm\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .sm\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .sm\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .sm\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .sm\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .sm\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .sm\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .sm\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .sm\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .sm\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .sm\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .sm\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .sm\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .sm\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .sm\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .sm\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .sm\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .sm\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .sm\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .sm\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .sm\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .sm\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .sm\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .sm\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .sm\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .sm\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .sm\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .sm\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .sm\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .sm\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .sm\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .sm\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .sm\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .sm\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .sm\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .sm\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .sm\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .sm\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .sm\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .sm\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .sm\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .sm\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .sm\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .sm\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .sm\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .sm\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .sm\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .sm\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .sm\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .sm\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .sm\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .sm\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .sm\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .sm\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .sm\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .sm\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .sm\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .sm\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .sm\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .sm\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .sm\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .sm\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .sm\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .sm\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .sm\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .sm\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .sm\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .sm\:mt-0 {
+    margin-top: 0;
+  }
+
+  .sm\:mr-0 {
+    margin-right: 0;
+  }
+
+  .sm\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .sm\:ml-0 {
+    margin-left: 0;
+  }
+
+  .sm\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .sm\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .sm\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .sm\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .sm\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .sm\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .sm\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .sm\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .sm\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .sm\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .sm\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .sm\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .sm\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .sm\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .sm\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .sm\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .sm\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .sm\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .sm\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .sm\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .sm\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .sm\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .sm\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .sm\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .sm\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .sm\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .sm\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .sm\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .sm\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .sm\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .sm\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .sm\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .sm\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .sm\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .sm\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .sm\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .sm\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .sm\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .sm\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .sm\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .sm\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .sm\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .sm\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .sm\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .sm\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .sm\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .sm\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .sm\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .sm\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .sm\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .sm\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .sm\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .sm\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .sm\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .sm\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .sm\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .sm\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .sm\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .sm\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .sm\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .sm\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .sm\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .sm\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .sm\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .sm\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .sm\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .sm\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .sm\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .sm\:mt-auto {
+    margin-top: auto;
+  }
+
+  .sm\:mr-auto {
+    margin-right: auto;
+  }
+
+  .sm\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .sm\:ml-auto {
+    margin-left: auto;
+  }
+
+  .sm\:mt-px {
+    margin-top: 1px;
+  }
+
+  .sm\:mr-px {
+    margin-right: 1px;
+  }
+
+  .sm\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .sm\:ml-px {
+    margin-left: 1px;
+  }
+
+  .sm\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .sm\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .sm\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .sm\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .sm\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .sm\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .sm\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .sm\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .sm\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .sm\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .sm\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .sm\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .sm\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .sm\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .sm\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .sm\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .sm\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .sm\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .sm\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .sm\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .sm\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .sm\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .sm\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .sm\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .sm\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .sm\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .sm\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .sm\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .sm\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .sm\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .sm\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .sm\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .sm\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .sm\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .sm\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .sm\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .sm\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .sm\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .sm\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .sm\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .sm\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .sm\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .sm\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .sm\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .sm\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .sm\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .sm\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .sm\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .sm\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .sm\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .sm\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .sm\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .sm\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .sm\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .sm\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .sm\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .sm\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .sm\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .sm\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .sm\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .sm\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .sm\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .sm\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .sm\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .sm\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .sm\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .sm\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .sm\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .sm\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .sm\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .sm\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .sm\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .sm\:max-h-full {
+    max-height: 100%;
+  }
+
+  .sm\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .sm\:max-w-none {
+    max-width: none;
+  }
+
+  .sm\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .sm\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .sm\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .sm\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .sm\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .sm\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .sm\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .sm\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .sm\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .sm\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .sm\:max-w-full {
+    max-width: 100%;
+  }
+
+  .sm\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .sm\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .sm\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .sm\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .sm\:min-h-0 {
+    min-height: 0;
+  }
+
+  .sm\:min-h-full {
+    min-height: 100%;
+  }
+
+  .sm\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .sm\:min-w-0 {
+    min-width: 0;
+  }
+
+  .sm\:min-w-full {
+    min-width: 100%;
+  }
+
+  .sm\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .sm\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .sm\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .sm\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .sm\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .sm\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .sm\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .sm\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .sm\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .sm\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .sm\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .sm\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .sm\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .sm\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .sm\:opacity-0 {
+    opacity: 0;
+  }
+
+  .sm\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .sm\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .sm\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .sm\:opacity-100 {
+    opacity: 1;
+  }
+
+  .sm\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .sm\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .sm\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .sm\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .sm\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .sm\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .sm\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .sm\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .sm\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .sm\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .sm\:outline-none {
+    outline: 0;
+  }
+
+  .sm\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .sm\:overflow-auto {
+    overflow: auto;
+  }
+
+  .sm\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .sm\:overflow-visible {
+    overflow: visible;
+  }
+
+  .sm\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .sm\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .sm\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .sm\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .sm\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .sm\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .sm\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .sm\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .sm\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .sm\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .sm\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .sm\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .sm\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .sm\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .sm\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .sm\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .sm\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .sm\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .sm\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .sm\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .sm\:p-0 {
+    padding: 0;
+  }
+
+  .sm\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .sm\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .sm\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .sm\:p-4 {
+    padding: 1rem;
+  }
+
+  .sm\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .sm\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .sm\:p-8 {
+    padding: 2rem;
+  }
+
+  .sm\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .sm\:p-12 {
+    padding: 3rem;
+  }
+
+  .sm\:p-16 {
+    padding: 4rem;
+  }
+
+  .sm\:p-20 {
+    padding: 5rem;
+  }
+
+  .sm\:p-24 {
+    padding: 6rem;
+  }
+
+  .sm\:p-32 {
+    padding: 8rem;
+  }
+
+  .sm\:p-40 {
+    padding: 10rem;
+  }
+
+  .sm\:p-48 {
+    padding: 12rem;
+  }
+
+  .sm\:p-56 {
+    padding: 14rem;
+  }
+
+  .sm\:p-64 {
+    padding: 16rem;
+  }
+
+  .sm\:p-px {
+    padding: 1px;
+  }
+
+  .sm\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .sm\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .sm\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .sm\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .sm\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .sm\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .sm\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .sm\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .sm\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .sm\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .sm\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .sm\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .sm\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .sm\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .sm\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .sm\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .sm\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .sm\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .sm\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .sm\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .sm\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .sm\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .sm\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .sm\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .sm\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .sm\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .sm\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .sm\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .sm\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .sm\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .sm\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .sm\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .sm\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .sm\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .sm\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .sm\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .sm\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .sm\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .sm\:pt-0 {
+    padding-top: 0;
+  }
+
+  .sm\:pr-0 {
+    padding-right: 0;
+  }
+
+  .sm\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .sm\:pl-0 {
+    padding-left: 0;
+  }
+
+  .sm\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .sm\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .sm\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .sm\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .sm\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .sm\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .sm\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .sm\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .sm\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .sm\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .sm\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .sm\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .sm\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .sm\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .sm\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .sm\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .sm\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .sm\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .sm\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .sm\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .sm\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .sm\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .sm\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .sm\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .sm\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .sm\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .sm\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .sm\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .sm\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .sm\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .sm\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .sm\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .sm\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .sm\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .sm\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .sm\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .sm\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .sm\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .sm\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .sm\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .sm\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .sm\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .sm\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .sm\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .sm\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .sm\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .sm\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .sm\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .sm\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .sm\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .sm\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .sm\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .sm\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .sm\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .sm\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .sm\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .sm\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .sm\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .sm\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .sm\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .sm\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .sm\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .sm\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .sm\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .sm\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .sm\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .sm\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .sm\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .sm\:pt-px {
+    padding-top: 1px;
+  }
+
+  .sm\:pr-px {
+    padding-right: 1px;
+  }
+
+  .sm\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .sm\:pl-px {
+    padding-left: 1px;
+  }
+
+  .sm\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .sm\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .sm\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .sm\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .sm\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .sm\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .sm\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .sm\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .sm\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .sm\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .sm\:static {
+    position: static;
+  }
+
+  .sm\:fixed {
+    position: fixed;
+  }
+
+  .sm\:absolute {
+    position: absolute;
+  }
+
+  .sm\:relative {
+    position: relative;
+  }
+
+  .sm\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .sm\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .sm\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .sm\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .sm\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .sm\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .sm\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .sm\:top-0 {
+    top: 0;
+  }
+
+  .sm\:right-0 {
+    right: 0;
+  }
+
+  .sm\:bottom-0 {
+    bottom: 0;
+  }
+
+  .sm\:left-0 {
+    left: 0;
+  }
+
+  .sm\:top-auto {
+    top: auto;
+  }
+
+  .sm\:right-auto {
+    right: auto;
+  }
+
+  .sm\:bottom-auto {
+    bottom: auto;
+  }
+
+  .sm\:left-auto {
+    left: auto;
+  }
+
+  .sm\:resize-none {
+    resize: none;
+  }
+
+  .sm\:resize-y {
+    resize: vertical;
+  }
+
+  .sm\:resize-x {
+    resize: horizontal;
+  }
+
+  .sm\:resize {
+    resize: both;
+  }
+
+  .sm\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .sm\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .sm\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .sm\:shadow-none {
+    box-shadow: none;
+  }
+
+  .sm\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .sm\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .sm\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .sm\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .sm\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .sm\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .sm\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .sm\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .sm\:fill-current {
+    fill: currentColor;
+  }
+
+  .sm\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .sm\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .sm\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .sm\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .sm\:table-auto {
+    table-layout: auto;
+  }
+
+  .sm\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .sm\:text-left {
+    text-align: left;
+  }
+
+  .sm\:text-center {
+    text-align: center;
+  }
+
+  .sm\:text-right {
+    text-align: right;
+  }
+
+  .sm\:text-justify {
+    text-align: justify;
+  }
+
+  .sm\:text-transparent {
+    color: transparent;
+  }
+
+  .sm\:text-current {
+    color: currentColor;
+  }
+
+  .sm\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .sm\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .sm\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .sm\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .sm\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .sm\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .sm\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .sm\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .sm\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .sm\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .sm\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .sm\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .sm\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .sm\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .sm\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .sm\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .sm\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .sm\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .sm\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .sm\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .sm\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .sm\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .sm\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .sm\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .sm\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .sm\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .sm\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .sm\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .sm\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .sm\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .sm\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .sm\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .sm\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .sm\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .sm\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .sm\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .sm\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .sm\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .sm\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .sm\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .sm\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .sm\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .sm\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .sm\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .sm\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .sm\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .sm\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .sm\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .sm\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .sm\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .sm\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .sm\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .sm\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .sm\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .sm\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .sm\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .sm\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .sm\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .sm\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .sm\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .sm\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .sm\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .sm\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .sm\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .sm\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .sm\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .sm\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .sm\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .sm\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .sm\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .sm\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .sm\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .sm\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .sm\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .sm\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .sm\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .sm\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .sm\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .sm\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .sm\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .sm\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .sm\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .sm\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .sm\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .sm\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .sm\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .sm\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .sm\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .sm\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .sm\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .sm\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .sm\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .sm\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .sm\:italic {
+    font-style: italic;
+  }
+
+  .sm\:not-italic {
+    font-style: normal;
+  }
+
+  .sm\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .sm\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .sm\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .sm\:normal-case {
+    text-transform: none;
+  }
+
+  .sm\:underline {
+    text-decoration: underline;
+  }
+
+  .sm\:line-through {
+    text-decoration: line-through;
+  }
+
+  .sm\:no-underline {
+    text-decoration: none;
+  }
+
+  .sm\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .sm\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .sm\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .sm\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .sm\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .sm\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .sm\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .sm\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .sm\:ordinal, .sm\:slashed-zero, .sm\:lining-nums, .sm\:oldstyle-nums, .sm\:proportional-nums, .sm\:tabular-nums, .sm\:diagonal-fractions, .sm\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .sm\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .sm\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .sm\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .sm\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .sm\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .sm\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .sm\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .sm\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .sm\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .sm\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .sm\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .sm\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .sm\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .sm\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .sm\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .sm\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .sm\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .sm\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .sm\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .sm\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .sm\:align-top {
+    vertical-align: top;
+  }
+
+  .sm\:align-middle {
+    vertical-align: middle;
+  }
+
+  .sm\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .sm\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .sm\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .sm\:visible {
+    visibility: visible;
+  }
+
+  .sm\:invisible {
+    visibility: hidden;
+  }
+
+  .sm\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .sm\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .sm\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .sm\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .sm\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .sm\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .sm\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .sm\:break-all {
+    word-break: break-all;
+  }
+
+  .sm\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .sm\:w-0 {
+    width: 0;
+  }
+
+  .sm\:w-1 {
+    width: 0.25rem;
+  }
+
+  .sm\:w-2 {
+    width: 0.5rem;
+  }
+
+  .sm\:w-3 {
+    width: 0.75rem;
+  }
+
+  .sm\:w-4 {
+    width: 1rem;
+  }
+
+  .sm\:w-5 {
+    width: 1.25rem;
+  }
+
+  .sm\:w-6 {
+    width: 1.5rem;
+  }
+
+  .sm\:w-8 {
+    width: 2rem;
+  }
+
+  .sm\:w-10 {
+    width: 2.5rem;
+  }
+
+  .sm\:w-12 {
+    width: 3rem;
+  }
+
+  .sm\:w-16 {
+    width: 4rem;
+  }
+
+  .sm\:w-20 {
+    width: 5rem;
+  }
+
+  .sm\:w-24 {
+    width: 6rem;
+  }
+
+  .sm\:w-32 {
+    width: 8rem;
+  }
+
+  .sm\:w-40 {
+    width: 10rem;
+  }
+
+  .sm\:w-48 {
+    width: 12rem;
+  }
+
+  .sm\:w-56 {
+    width: 14rem;
+  }
+
+  .sm\:w-64 {
+    width: 16rem;
+  }
+
+  .sm\:w-auto {
+    width: auto;
+  }
+
+  .sm\:w-px {
+    width: 1px;
+  }
+
+  .sm\:w-1\/2 {
+    width: 50%;
+  }
+
+  .sm\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .sm\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .sm\:w-1\/4 {
+    width: 25%;
+  }
+
+  .sm\:w-2\/4 {
+    width: 50%;
+  }
+
+  .sm\:w-3\/4 {
+    width: 75%;
+  }
+
+  .sm\:w-1\/5 {
+    width: 20%;
+  }
+
+  .sm\:w-2\/5 {
+    width: 40%;
+  }
+
+  .sm\:w-3\/5 {
+    width: 60%;
+  }
+
+  .sm\:w-4\/5 {
+    width: 80%;
+  }
+
+  .sm\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .sm\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .sm\:w-3\/6 {
+    width: 50%;
+  }
+
+  .sm\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .sm\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .sm\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .sm\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .sm\:w-3\/12 {
+    width: 25%;
+  }
+
+  .sm\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .sm\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .sm\:w-6\/12 {
+    width: 50%;
+  }
+
+  .sm\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .sm\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .sm\:w-9\/12 {
+    width: 75%;
+  }
+
+  .sm\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .sm\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .sm\:w-full {
+    width: 100%;
+  }
+
+  .sm\:w-screen {
+    width: 100vw;
+  }
+
+  .sm\:z-0 {
+    z-index: 0;
+  }
+
+  .sm\:z-10 {
+    z-index: 10;
+  }
+
+  .sm\:z-20 {
+    z-index: 20;
+  }
+
+  .sm\:z-30 {
+    z-index: 30;
+  }
+
+  .sm\:z-40 {
+    z-index: 40;
+  }
+
+  .sm\:z-50 {
+    z-index: 50;
+  }
+
+  .sm\:z-auto {
+    z-index: auto;
+  }
+
+  .sm\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .sm\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .sm\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .sm\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .sm\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .sm\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .sm\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .sm\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .sm\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .sm\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .sm\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .sm\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .sm\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .sm\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .sm\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .sm\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .sm\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .sm\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .sm\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .sm\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .sm\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .sm\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .sm\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .sm\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .sm\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .sm\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .sm\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .sm\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .sm\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .sm\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .sm\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .sm\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .sm\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .sm\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .sm\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .sm\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .sm\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .sm\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .sm\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .sm\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .sm\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .sm\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .sm\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .sm\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .sm\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .sm\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .sm\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .sm\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .sm\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .sm\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .sm\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .sm\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .sm\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .sm\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .sm\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .sm\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .sm\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .sm\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .sm\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .sm\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .sm\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .sm\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .sm\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .sm\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .sm\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .sm\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .sm\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .sm\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .sm\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .sm\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .sm\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .sm\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .sm\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .sm\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .sm\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .sm\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .sm\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .sm\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .sm\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .sm\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .sm\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .sm\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .sm\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .sm\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .sm\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .sm\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .sm\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .sm\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .sm\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .sm\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .sm\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .sm\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .sm\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .sm\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .sm\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .sm\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .sm\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .sm\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .sm\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .sm\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .sm\:col-auto {
+    grid-column: auto;
+  }
+
+  .sm\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .sm\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .sm\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .sm\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .sm\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .sm\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .sm\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .sm\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .sm\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .sm\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .sm\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .sm\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .sm\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .sm\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .sm\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .sm\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .sm\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .sm\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .sm\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .sm\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .sm\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .sm\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .sm\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .sm\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .sm\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .sm\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .sm\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .sm\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .sm\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .sm\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .sm\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .sm\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .sm\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .sm\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .sm\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .sm\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .sm\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .sm\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .sm\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .sm\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .sm\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .sm\:row-auto {
+    grid-row: auto;
+  }
+
+  .sm\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .sm\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .sm\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .sm\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .sm\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .sm\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .sm\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .sm\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .sm\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .sm\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .sm\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .sm\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .sm\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .sm\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .sm\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .sm\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .sm\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .sm\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .sm\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .sm\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .sm\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .sm\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .sm\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .sm\:transform-none {
+    transform: none;
+  }
+
+  .sm\:origin-center {
+    transform-origin: center;
+  }
+
+  .sm\:origin-top {
+    transform-origin: top;
+  }
+
+  .sm\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .sm\:origin-right {
+    transform-origin: right;
+  }
+
+  .sm\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .sm\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .sm\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .sm\:origin-left {
+    transform-origin: left;
+  }
+
+  .sm\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .sm\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .sm\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .sm\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .sm\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .sm\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .sm\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .sm\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .sm\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .sm\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .sm\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .sm\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .sm\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .sm\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .sm\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .sm\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .sm\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .sm\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .sm\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .sm\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .sm\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .sm\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .sm\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .sm\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .sm\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .sm\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .sm\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .sm\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .sm\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .sm\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .sm\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .sm\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .sm\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .sm\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .sm\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .sm\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .sm\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .sm\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .sm\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .sm\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .sm\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .sm\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .sm\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .sm\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .sm\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .sm\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .sm\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .sm\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .sm\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .sm\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .sm\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .sm\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .sm\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .sm\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .sm\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .sm\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .sm\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .sm\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .sm\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .sm\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .sm\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .sm\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .sm\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .sm\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .sm\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .sm\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .sm\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .sm\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .sm\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .sm\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .sm\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .sm\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .sm\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .sm\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .sm\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .sm\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .sm\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .sm\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .sm\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .sm\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .sm\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .sm\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .sm\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .sm\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .sm\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .sm\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .sm\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .sm\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .sm\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .sm\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .sm\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .sm\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .sm\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .sm\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .sm\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .sm\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .sm\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .sm\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .sm\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .sm\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .sm\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .sm\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .sm\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .sm\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .sm\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .sm\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .sm\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .sm\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .sm\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .sm\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .sm\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .sm\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .sm\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .sm\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .sm\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .sm\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .sm\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .sm\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .sm\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .sm\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .sm\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .sm\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .sm\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .sm\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .sm\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .sm\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .sm\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .sm\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .sm\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .sm\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .sm\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .sm\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .sm\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .sm\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .sm\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .sm\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .sm\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .sm\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .sm\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .sm\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .sm\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .sm\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .sm\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .sm\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .sm\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .sm\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .sm\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .sm\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .sm\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .sm\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .sm\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .sm\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .sm\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .sm\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .sm\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .sm\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .sm\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .sm\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .sm\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .sm\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .sm\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .sm\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .sm\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .sm\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .sm\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .sm\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .sm\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .sm\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .sm\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .sm\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .sm\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .sm\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .sm\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .sm\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .sm\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .sm\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .sm\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .sm\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .sm\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .sm\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .sm\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .sm\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .sm\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .sm\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .sm\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .sm\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .sm\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .sm\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .sm\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .sm\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .sm\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .sm\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .sm\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .sm\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .sm\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .sm\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .sm\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .sm\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .sm\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .sm\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .sm\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .sm\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .sm\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .sm\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .sm\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .sm\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .sm\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .sm\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .sm\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .sm\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .sm\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .sm\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .sm\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .sm\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .sm\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .sm\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .sm\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .sm\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .sm\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .sm\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .sm\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .sm\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .sm\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .sm\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .sm\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .sm\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .sm\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .sm\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .sm\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .sm\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .sm\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .sm\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .sm\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .sm\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .sm\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .sm\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .sm\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .sm\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .sm\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .sm\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .sm\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .sm\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .sm\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .sm\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .sm\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .sm\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .sm\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .sm\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .sm\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .sm\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .sm\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .sm\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .sm\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .sm\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .sm\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .sm\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .sm\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .sm\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .sm\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .sm\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .sm\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .sm\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .sm\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .sm\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .sm\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .sm\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .sm\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .sm\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .sm\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .sm\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .sm\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .sm\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .sm\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .sm\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .sm\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .sm\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .sm\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .sm\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .sm\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .sm\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .sm\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .sm\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .sm\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .sm\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .sm\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .sm\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .sm\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .sm\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .sm\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .sm\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .sm\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .sm\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .sm\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .sm\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .sm\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .sm\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .sm\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .sm\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .sm\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .sm\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .sm\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .sm\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .sm\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .sm\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .sm\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .sm\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .sm\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .sm\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .sm\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .sm\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .sm\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .sm\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .sm\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .sm\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .sm\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .sm\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .sm\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .sm\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .sm\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .sm\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .sm\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .sm\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .sm\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .sm\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .sm\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .sm\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .sm\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .sm\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .sm\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .sm\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .sm\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .sm\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .sm\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .sm\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .sm\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .sm\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .sm\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .sm\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .sm\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .sm\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .sm\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .sm\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .sm\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .sm\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .sm\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .sm\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .sm\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .sm\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .sm\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .sm\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .sm\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .sm\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .sm\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .sm\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .sm\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .sm\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .sm\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .sm\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .sm\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .sm\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .sm\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .sm\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .sm\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .sm\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .sm\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .sm\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .sm\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .sm\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .sm\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .sm\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .sm\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .sm\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .sm\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .sm\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .sm\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .sm\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .sm\:transition-none {
+    transition-property: none;
+  }
+
+  .sm\:transition-all {
+    transition-property: all;
+  }
+
+  .sm\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .sm\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .sm\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .sm\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .sm\:transition-transform {
+    transition-property: transform;
+  }
+
+  .sm\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .sm\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .sm\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .sm\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .sm\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .sm\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .sm\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .sm\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .sm\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .sm\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .sm\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .sm\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .sm\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .sm\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .sm\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .sm\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .sm\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .sm\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .sm\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .sm\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .sm\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .sm\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .sm\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .sm\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .sm\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
+
+@media (min-width: 768px) {
+  .md\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .md\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .md\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .md\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .md\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .md\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .md\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .md\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .md\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .md\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .md\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .md\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .md\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .md\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .md\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .md\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .md\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .md\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .md\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .md\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .md\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .md\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .md\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .md\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .md\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .md\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .md\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .md\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .md\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .md\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .md\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .md\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .md\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .md\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .md\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .md\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .md\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .md\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .md\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .md\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .md\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .md\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .md\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .md\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .md\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .md\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .md\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .md\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .md\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .md\:bg-local {
+    background-attachment: local;
+  }
+
+  .md\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .md\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .md\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .md\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .md\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .md\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .md\:bg-current {
+    background-color: currentColor;
+  }
+
+  .md\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .md\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .md\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .md\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .md\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .md\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .md\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .md\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .md\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .md\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .md\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .md\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .md\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .md\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .md\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .md\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .md\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .md\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .md\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .md\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .md\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .md\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .md\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .md\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .md\:bg-none {
+    background-image: none;
+  }
+
+  .md\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .md\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .md\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .md\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .md\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .md\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .md\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .md\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .md\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .md\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .md\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .md\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .md\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .md\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .md\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .md\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .md\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .md\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .md\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .md\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .md\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .md\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .md\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .md\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .md\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .md\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .md\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .md\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .md\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .md\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .md\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .md\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .md\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .md\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .md\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .md\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .md\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .md\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .md\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .md\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .md\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .md\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .md\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .md\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .md\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .md\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .md\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .md\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .md\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .md\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .md\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .md\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .md\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .md\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .md\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .md\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .md\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .md\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .md\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .md\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .md\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .md\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .md\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .md\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .md\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .md\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .md\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .md\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .md\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .md\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .md\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .md\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .md\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .md\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .md\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .md\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .md\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .md\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .md\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .md\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .md\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .md\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .md\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .md\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .md\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .md\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .md\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .md\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .md\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .md\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .md\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .md\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .md\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .md\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .md\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .md\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .md\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .md\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .md\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .md\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .md\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .md\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .md\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .md\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .md\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .md\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .md\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .md\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .md\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .md\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .md\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .md\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .md\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .md\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .md\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .md\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .md\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .md\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .md\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .md\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .md\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .md\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .md\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .md\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .md\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .md\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .md\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .md\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .md\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .md\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .md\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .md\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .md\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .md\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .md\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .md\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .md\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .md\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .md\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .md\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .md\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .md\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .md\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .md\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .md\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .md\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .md\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .md\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .md\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .md\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .md\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .md\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .md\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .md\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .md\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .md\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .md\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .md\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .md\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .md\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .md\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .md\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .md\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .md\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .md\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .md\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .md\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .md\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .md\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .md\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .md\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .md\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .md\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .md\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .md\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .md\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .md\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .md\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .md\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .md\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .md\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .md\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .md\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .md\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .md\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .md\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .md\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .md\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .md\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .md\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .md\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .md\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .md\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .md\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .md\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .md\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .md\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .md\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .md\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .md\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .md\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .md\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .md\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .md\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .md\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .md\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .md\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .md\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .md\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .md\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .md\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .md\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .md\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .md\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .md\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .md\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .md\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .md\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .md\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .md\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .md\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .md\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .md\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .md\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .md\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .md\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .md\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .md\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .md\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .md\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .md\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .md\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .md\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .md\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .md\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .md\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .md\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .md\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .md\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .md\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .md\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .md\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .md\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .md\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .md\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .md\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .md\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .md\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .md\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .md\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .md\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .md\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .md\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .md\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .md\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .md\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .md\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .md\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .md\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .md\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .md\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .md\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .md\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .md\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .md\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .md\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .md\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .md\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .md\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .md\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .md\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .md\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .md\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .md\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .md\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .md\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .md\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .md\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .md\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .md\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .md\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .md\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .md\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .md\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .md\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .md\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .md\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .md\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .md\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .md\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .md\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .md\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .md\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .md\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .md\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .md\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .md\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .md\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .md\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .md\:bg-center {
+    background-position: center;
+  }
+
+  .md\:bg-left {
+    background-position: left;
+  }
+
+  .md\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .md\:bg-left-top {
+    background-position: left top;
+  }
+
+  .md\:bg-right {
+    background-position: right;
+  }
+
+  .md\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .md\:bg-right-top {
+    background-position: right top;
+  }
+
+  .md\:bg-top {
+    background-position: top;
+  }
+
+  .md\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .md\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .md\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .md\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .md\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .md\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .md\:bg-auto {
+    background-size: auto;
+  }
+
+  .md\:bg-cover {
+    background-size: cover;
+  }
+
+  .md\:bg-contain {
+    background-size: contain;
+  }
+
+  .md\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .md\:border-separate {
+    border-collapse: separate;
+  }
+
+  .md\:border-transparent {
+    border-color: transparent;
+  }
+
+  .md\:border-current {
+    border-color: currentColor;
+  }
+
+  .md\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .md\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .md\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .md\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .md\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .md\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .md\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .md\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .md\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .md\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .md\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .md\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .md\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .md\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .md\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .md\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .md\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .md\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .md\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .md\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .md\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .md\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .md\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .md\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .md\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .md\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .md\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .md\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .md\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .md\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .md\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .md\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .md\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .md\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .md\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .md\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .md\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .md\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .md\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .md\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .md\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .md\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .md\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .md\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .md\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .md\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .md\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .md\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .md\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .md\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .md\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .md\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .md\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .md\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .md\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .md\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .md\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .md\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .md\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .md\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .md\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .md\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .md\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .md\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .md\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .md\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .md\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .md\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .md\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .md\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .md\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .md\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .md\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .md\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .md\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .md\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .md\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .md\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .md\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .md\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .md\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .md\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .md\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .md\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .md\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .md\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .md\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .md\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .md\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .md\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .md\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .md\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .md\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .md\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .md\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .md\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .md\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .md\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .md\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .md\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .md\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .md\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .md\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .md\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .md\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .md\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .md\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .md\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .md\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .md\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .md\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .md\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .md\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .md\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .md\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .md\:rounded-none {
+    border-radius: 0;
+  }
+
+  .md\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .md\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .md\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .md\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .md\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .md\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .md\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .md\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .md\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .md\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .md\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .md\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .md\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .md\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .md\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .md\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .md\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .md\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .md\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .md\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .md\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .md\:border-solid {
+    border-style: solid;
+  }
+
+  .md\:border-dashed {
+    border-style: dashed;
+  }
+
+  .md\:border-dotted {
+    border-style: dotted;
+  }
+
+  .md\:border-double {
+    border-style: double;
+  }
+
+  .md\:border-none {
+    border-style: none;
+  }
+
+  .md\:border-0 {
+    border-width: 0;
+  }
+
+  .md\:border-2 {
+    border-width: 2px;
+  }
+
+  .md\:border-4 {
+    border-width: 4px;
+  }
+
+  .md\:border-8 {
+    border-width: 8px;
+  }
+
+  .md\:border {
+    border-width: 1px;
+  }
+
+  .md\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .md\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .md\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .md\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .md\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .md\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .md\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .md\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .md\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .md\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .md\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .md\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .md\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .md\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .md\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .md\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .md\:border-t {
+    border-top-width: 1px;
+  }
+
+  .md\:border-r {
+    border-right-width: 1px;
+  }
+
+  .md\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .md\:border-l {
+    border-left-width: 1px;
+  }
+
+  .md\:box-border {
+    box-sizing: border-box;
+  }
+
+  .md\:box-content {
+    box-sizing: content-box;
+  }
+
+  .md\:cursor-auto {
+    cursor: auto;
+  }
+
+  .md\:cursor-default {
+    cursor: default;
+  }
+
+  .md\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .md\:cursor-wait {
+    cursor: wait;
+  }
+
+  .md\:cursor-text {
+    cursor: text;
+  }
+
+  .md\:cursor-move {
+    cursor: move;
+  }
+
+  .md\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .md\:block {
+    display: block;
+  }
+
+  .md\:inline-block {
+    display: inline-block;
+  }
+
+  .md\:inline {
+    display: inline;
+  }
+
+  .md\:flex {
+    display: flex;
+  }
+
+  .md\:inline-flex {
+    display: inline-flex;
+  }
+
+  .md\:table {
+    display: table;
+  }
+
+  .md\:table-caption {
+    display: table-caption;
+  }
+
+  .md\:table-cell {
+    display: table-cell;
+  }
+
+  .md\:table-column {
+    display: table-column;
+  }
+
+  .md\:table-column-group {
+    display: table-column-group;
+  }
+
+  .md\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .md\:table-header-group {
+    display: table-header-group;
+  }
+
+  .md\:table-row-group {
+    display: table-row-group;
+  }
+
+  .md\:table-row {
+    display: table-row;
+  }
+
+  .md\:flow-root {
+    display: flow-root;
+  }
+
+  .md\:grid {
+    display: grid;
+  }
+
+  .md\:inline-grid {
+    display: inline-grid;
+  }
+
+  .md\:contents {
+    display: contents;
+  }
+
+  .md\:hidden {
+    display: none;
+  }
+
+  .md\:flex-row {
+    flex-direction: row;
+  }
+
+  .md\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .md\:flex-col {
+    flex-direction: column;
+  }
+
+  .md\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .md\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .md\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .md\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .md\:place-items-auto {
+    place-items: auto;
+  }
+
+  .md\:place-items-start {
+    place-items: start;
+  }
+
+  .md\:place-items-end {
+    place-items: end;
+  }
+
+  .md\:place-items-center {
+    place-items: center;
+  }
+
+  .md\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .md\:place-content-center {
+    place-content: center;
+  }
+
+  .md\:place-content-start {
+    place-content: start;
+  }
+
+  .md\:place-content-end {
+    place-content: end;
+  }
+
+  .md\:place-content-between {
+    place-content: space-between;
+  }
+
+  .md\:place-content-around {
+    place-content: space-around;
+  }
+
+  .md\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .md\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .md\:place-self-auto {
+    place-self: auto;
+  }
+
+  .md\:place-self-start {
+    place-self: start;
+  }
+
+  .md\:place-self-end {
+    place-self: end;
+  }
+
+  .md\:place-self-center {
+    place-self: center;
+  }
+
+  .md\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .md\:items-start {
+    align-items: flex-start;
+  }
+
+  .md\:items-end {
+    align-items: flex-end;
+  }
+
+  .md\:items-center {
+    align-items: center;
+  }
+
+  .md\:items-baseline {
+    align-items: baseline;
+  }
+
+  .md\:items-stretch {
+    align-items: stretch;
+  }
+
+  .md\:content-center {
+    align-content: center;
+  }
+
+  .md\:content-start {
+    align-content: flex-start;
+  }
+
+  .md\:content-end {
+    align-content: flex-end;
+  }
+
+  .md\:content-between {
+    align-content: space-between;
+  }
+
+  .md\:content-around {
+    align-content: space-around;
+  }
+
+  .md\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .md\:self-auto {
+    align-self: auto;
+  }
+
+  .md\:self-start {
+    align-self: flex-start;
+  }
+
+  .md\:self-end {
+    align-self: flex-end;
+  }
+
+  .md\:self-center {
+    align-self: center;
+  }
+
+  .md\:self-stretch {
+    align-self: stretch;
+  }
+
+  .md\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .md\:justify-items-start {
+    justify-items: start;
+  }
+
+  .md\:justify-items-end {
+    justify-items: end;
+  }
+
+  .md\:justify-items-center {
+    justify-items: center;
+  }
+
+  .md\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .md\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .md\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .md\:justify-center {
+    justify-content: center;
+  }
+
+  .md\:justify-between {
+    justify-content: space-between;
+  }
+
+  .md\:justify-around {
+    justify-content: space-around;
+  }
+
+  .md\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .md\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .md\:justify-self-start {
+    justify-self: start;
+  }
+
+  .md\:justify-self-end {
+    justify-self: end;
+  }
+
+  .md\:justify-self-center {
+    justify-self: center;
+  }
+
+  .md\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .md\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .md\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .md\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .md\:flex-none {
+    flex: none;
+  }
+
+  .md\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .md\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .md\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .md\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .md\:order-1 {
+    order: 1;
+  }
+
+  .md\:order-2 {
+    order: 2;
+  }
+
+  .md\:order-3 {
+    order: 3;
+  }
+
+  .md\:order-4 {
+    order: 4;
+  }
+
+  .md\:order-5 {
+    order: 5;
+  }
+
+  .md\:order-6 {
+    order: 6;
+  }
+
+  .md\:order-7 {
+    order: 7;
+  }
+
+  .md\:order-8 {
+    order: 8;
+  }
+
+  .md\:order-9 {
+    order: 9;
+  }
+
+  .md\:order-10 {
+    order: 10;
+  }
+
+  .md\:order-11 {
+    order: 11;
+  }
+
+  .md\:order-12 {
+    order: 12;
+  }
+
+  .md\:order-first {
+    order: -9999;
+  }
+
+  .md\:order-last {
+    order: 9999;
+  }
+
+  .md\:order-none {
+    order: 0;
+  }
+
+  .md\:float-right {
+    float: right;
+  }
+
+  .md\:float-left {
+    float: left;
+  }
+
+  .md\:float-none {
+    float: none;
+  }
+
+  .md\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .md\:clear-left {
+    clear: left;
+  }
+
+  .md\:clear-right {
+    clear: right;
+  }
+
+  .md\:clear-both {
+    clear: both;
+  }
+
+  .md\:clear-none {
+    clear: none;
+  }
+
+  .md\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .md\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .md\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .md\:font-hairline {
+    font-weight: 100;
+  }
+
+  .md\:font-thin {
+    font-weight: 200;
+  }
+
+  .md\:font-light {
+    font-weight: 300;
+  }
+
+  .md\:font-normal {
+    font-weight: 400;
+  }
+
+  .md\:font-medium {
+    font-weight: 500;
+  }
+
+  .md\:font-semibold {
+    font-weight: 600;
+  }
+
+  .md\:font-bold {
+    font-weight: 700;
+  }
+
+  .md\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .md\:font-black {
+    font-weight: 900;
+  }
+
+  .md\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .md\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .md\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .md\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .md\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .md\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .md\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .md\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .md\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .md\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .md\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .md\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .md\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .md\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .md\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .md\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .md\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .md\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .md\:h-0 {
+    height: 0;
+  }
+
+  .md\:h-1 {
+    height: 0.25rem;
+  }
+
+  .md\:h-2 {
+    height: 0.5rem;
+  }
+
+  .md\:h-3 {
+    height: 0.75rem;
+  }
+
+  .md\:h-4 {
+    height: 1rem;
+  }
+
+  .md\:h-5 {
+    height: 1.25rem;
+  }
+
+  .md\:h-6 {
+    height: 1.5rem;
+  }
+
+  .md\:h-8 {
+    height: 2rem;
+  }
+
+  .md\:h-10 {
+    height: 2.5rem;
+  }
+
+  .md\:h-12 {
+    height: 3rem;
+  }
+
+  .md\:h-16 {
+    height: 4rem;
+  }
+
+  .md\:h-20 {
+    height: 5rem;
+  }
+
+  .md\:h-24 {
+    height: 6rem;
+  }
+
+  .md\:h-32 {
+    height: 8rem;
+  }
+
+  .md\:h-40 {
+    height: 10rem;
+  }
+
+  .md\:h-48 {
+    height: 12rem;
+  }
+
+  .md\:h-56 {
+    height: 14rem;
+  }
+
+  .md\:h-64 {
+    height: 16rem;
+  }
+
+  .md\:h-auto {
+    height: auto;
+  }
+
+  .md\:h-px {
+    height: 1px;
+  }
+
+  .md\:h-full {
+    height: 100%;
+  }
+
+  .md\:h-screen {
+    height: 100vh;
+  }
+
+  .md\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .md\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .md\:text-base {
+    font-size: 1rem;
+  }
+
+  .md\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .md\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .md\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .md\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .md\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .md\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .md\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .md\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .md\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .md\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .md\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .md\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .md\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .md\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .md\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .md\:leading-none {
+    line-height: 1;
+  }
+
+  .md\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .md\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .md\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .md\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .md\:leading-loose {
+    line-height: 2;
+  }
+
+  .md\:list-inside {
+    list-style-position: inside;
+  }
+
+  .md\:list-outside {
+    list-style-position: outside;
+  }
+
+  .md\:list-none {
+    list-style-type: none;
+  }
+
+  .md\:list-disc {
+    list-style-type: disc;
+  }
+
+  .md\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .md\:m-0 {
+    margin: 0;
+  }
+
+  .md\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .md\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .md\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .md\:m-4 {
+    margin: 1rem;
+  }
+
+  .md\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .md\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .md\:m-8 {
+    margin: 2rem;
+  }
+
+  .md\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .md\:m-12 {
+    margin: 3rem;
+  }
+
+  .md\:m-16 {
+    margin: 4rem;
+  }
+
+  .md\:m-20 {
+    margin: 5rem;
+  }
+
+  .md\:m-24 {
+    margin: 6rem;
+  }
+
+  .md\:m-32 {
+    margin: 8rem;
+  }
+
+  .md\:m-40 {
+    margin: 10rem;
+  }
+
+  .md\:m-48 {
+    margin: 12rem;
+  }
+
+  .md\:m-56 {
+    margin: 14rem;
+  }
+
+  .md\:m-64 {
+    margin: 16rem;
+  }
+
+  .md\:m-auto {
+    margin: auto;
+  }
+
+  .md\:m-px {
+    margin: 1px;
+  }
+
+  .md\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .md\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .md\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .md\:-m-4 {
+    margin: -1rem;
+  }
+
+  .md\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .md\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .md\:-m-8 {
+    margin: -2rem;
+  }
+
+  .md\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .md\:-m-12 {
+    margin: -3rem;
+  }
+
+  .md\:-m-16 {
+    margin: -4rem;
+  }
+
+  .md\:-m-20 {
+    margin: -5rem;
+  }
+
+  .md\:-m-24 {
+    margin: -6rem;
+  }
+
+  .md\:-m-32 {
+    margin: -8rem;
+  }
+
+  .md\:-m-40 {
+    margin: -10rem;
+  }
+
+  .md\:-m-48 {
+    margin: -12rem;
+  }
+
+  .md\:-m-56 {
+    margin: -14rem;
+  }
+
+  .md\:-m-64 {
+    margin: -16rem;
+  }
+
+  .md\:-m-px {
+    margin: -1px;
+  }
+
+  .md\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .md\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .md\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .md\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .md\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .md\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .md\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .md\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .md\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .md\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .md\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .md\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .md\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .md\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .md\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .md\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .md\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .md\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .md\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .md\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .md\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .md\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .md\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .md\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .md\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .md\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .md\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .md\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .md\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .md\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .md\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .md\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .md\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .md\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .md\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .md\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .md\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .md\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .md\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .md\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .md\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .md\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .md\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .md\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .md\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .md\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .md\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .md\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .md\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .md\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .md\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .md\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .md\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .md\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .md\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .md\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .md\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .md\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .md\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .md\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .md\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .md\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .md\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .md\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .md\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .md\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .md\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .md\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .md\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .md\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .md\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .md\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .md\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .md\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .md\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .md\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .md\:mt-0 {
+    margin-top: 0;
+  }
+
+  .md\:mr-0 {
+    margin-right: 0;
+  }
+
+  .md\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .md\:ml-0 {
+    margin-left: 0;
+  }
+
+  .md\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .md\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .md\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .md\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .md\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .md\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .md\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .md\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .md\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .md\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .md\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .md\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .md\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .md\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .md\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .md\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .md\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .md\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .md\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .md\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .md\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .md\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .md\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .md\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .md\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .md\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .md\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .md\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .md\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .md\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .md\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .md\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .md\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .md\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .md\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .md\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .md\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .md\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .md\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .md\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .md\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .md\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .md\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .md\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .md\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .md\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .md\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .md\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .md\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .md\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .md\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .md\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .md\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .md\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .md\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .md\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .md\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .md\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .md\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .md\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .md\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .md\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .md\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .md\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .md\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .md\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .md\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .md\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .md\:mt-auto {
+    margin-top: auto;
+  }
+
+  .md\:mr-auto {
+    margin-right: auto;
+  }
+
+  .md\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .md\:ml-auto {
+    margin-left: auto;
+  }
+
+  .md\:mt-px {
+    margin-top: 1px;
+  }
+
+  .md\:mr-px {
+    margin-right: 1px;
+  }
+
+  .md\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .md\:ml-px {
+    margin-left: 1px;
+  }
+
+  .md\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .md\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .md\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .md\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .md\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .md\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .md\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .md\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .md\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .md\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .md\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .md\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .md\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .md\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .md\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .md\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .md\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .md\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .md\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .md\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .md\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .md\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .md\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .md\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .md\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .md\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .md\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .md\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .md\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .md\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .md\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .md\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .md\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .md\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .md\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .md\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .md\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .md\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .md\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .md\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .md\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .md\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .md\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .md\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .md\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .md\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .md\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .md\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .md\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .md\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .md\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .md\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .md\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .md\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .md\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .md\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .md\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .md\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .md\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .md\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .md\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .md\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .md\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .md\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .md\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .md\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .md\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .md\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .md\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .md\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .md\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .md\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .md\:max-h-full {
+    max-height: 100%;
+  }
+
+  .md\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .md\:max-w-none {
+    max-width: none;
+  }
+
+  .md\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .md\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .md\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .md\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .md\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .md\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .md\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .md\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .md\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .md\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .md\:max-w-full {
+    max-width: 100%;
+  }
+
+  .md\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .md\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .md\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .md\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .md\:min-h-0 {
+    min-height: 0;
+  }
+
+  .md\:min-h-full {
+    min-height: 100%;
+  }
+
+  .md\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .md\:min-w-0 {
+    min-width: 0;
+  }
+
+  .md\:min-w-full {
+    min-width: 100%;
+  }
+
+  .md\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .md\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .md\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .md\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .md\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .md\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .md\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .md\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .md\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .md\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .md\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .md\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .md\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .md\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .md\:opacity-0 {
+    opacity: 0;
+  }
+
+  .md\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .md\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .md\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .md\:opacity-100 {
+    opacity: 1;
+  }
+
+  .md\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .md\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .md\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .md\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .md\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .md\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .md\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .md\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .md\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .md\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .md\:outline-none {
+    outline: 0;
+  }
+
+  .md\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .md\:overflow-auto {
+    overflow: auto;
+  }
+
+  .md\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .md\:overflow-visible {
+    overflow: visible;
+  }
+
+  .md\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .md\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .md\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .md\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .md\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .md\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .md\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .md\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .md\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .md\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .md\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .md\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .md\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .md\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .md\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .md\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .md\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .md\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .md\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .md\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .md\:p-0 {
+    padding: 0;
+  }
+
+  .md\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .md\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .md\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .md\:p-4 {
+    padding: 1rem;
+  }
+
+  .md\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .md\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .md\:p-8 {
+    padding: 2rem;
+  }
+
+  .md\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .md\:p-12 {
+    padding: 3rem;
+  }
+
+  .md\:p-16 {
+    padding: 4rem;
+  }
+
+  .md\:p-20 {
+    padding: 5rem;
+  }
+
+  .md\:p-24 {
+    padding: 6rem;
+  }
+
+  .md\:p-32 {
+    padding: 8rem;
+  }
+
+  .md\:p-40 {
+    padding: 10rem;
+  }
+
+  .md\:p-48 {
+    padding: 12rem;
+  }
+
+  .md\:p-56 {
+    padding: 14rem;
+  }
+
+  .md\:p-64 {
+    padding: 16rem;
+  }
+
+  .md\:p-px {
+    padding: 1px;
+  }
+
+  .md\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .md\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .md\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .md\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .md\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .md\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .md\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .md\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .md\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .md\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .md\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .md\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .md\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .md\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .md\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .md\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .md\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .md\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .md\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .md\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .md\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .md\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .md\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .md\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .md\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .md\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .md\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .md\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .md\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .md\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .md\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .md\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .md\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .md\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .md\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .md\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .md\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .md\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .md\:pt-0 {
+    padding-top: 0;
+  }
+
+  .md\:pr-0 {
+    padding-right: 0;
+  }
+
+  .md\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .md\:pl-0 {
+    padding-left: 0;
+  }
+
+  .md\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .md\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .md\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .md\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .md\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .md\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .md\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .md\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .md\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .md\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .md\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .md\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .md\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .md\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .md\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .md\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .md\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .md\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .md\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .md\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .md\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .md\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .md\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .md\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .md\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .md\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .md\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .md\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .md\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .md\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .md\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .md\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .md\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .md\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .md\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .md\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .md\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .md\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .md\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .md\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .md\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .md\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .md\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .md\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .md\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .md\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .md\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .md\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .md\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .md\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .md\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .md\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .md\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .md\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .md\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .md\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .md\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .md\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .md\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .md\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .md\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .md\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .md\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .md\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .md\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .md\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .md\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .md\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .md\:pt-px {
+    padding-top: 1px;
+  }
+
+  .md\:pr-px {
+    padding-right: 1px;
+  }
+
+  .md\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .md\:pl-px {
+    padding-left: 1px;
+  }
+
+  .md\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .md\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .md\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .md\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .md\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .md\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .md\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .md\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .md\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .md\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .md\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .md\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .md\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .md\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .md\:static {
+    position: static;
+  }
+
+  .md\:fixed {
+    position: fixed;
+  }
+
+  .md\:absolute {
+    position: absolute;
+  }
+
+  .md\:relative {
+    position: relative;
+  }
+
+  .md\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .md\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .md\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .md\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .md\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .md\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .md\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .md\:top-0 {
+    top: 0;
+  }
+
+  .md\:right-0 {
+    right: 0;
+  }
+
+  .md\:bottom-0 {
+    bottom: 0;
+  }
+
+  .md\:left-0 {
+    left: 0;
+  }
+
+  .md\:top-auto {
+    top: auto;
+  }
+
+  .md\:right-auto {
+    right: auto;
+  }
+
+  .md\:bottom-auto {
+    bottom: auto;
+  }
+
+  .md\:left-auto {
+    left: auto;
+  }
+
+  .md\:resize-none {
+    resize: none;
+  }
+
+  .md\:resize-y {
+    resize: vertical;
+  }
+
+  .md\:resize-x {
+    resize: horizontal;
+  }
+
+  .md\:resize {
+    resize: both;
+  }
+
+  .md\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .md\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .md\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .md\:shadow-none {
+    box-shadow: none;
+  }
+
+  .md\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .md\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .md\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .md\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .md\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .md\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .md\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .md\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .md\:fill-current {
+    fill: currentColor;
+  }
+
+  .md\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .md\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .md\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .md\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .md\:table-auto {
+    table-layout: auto;
+  }
+
+  .md\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .md\:text-left {
+    text-align: left;
+  }
+
+  .md\:text-center {
+    text-align: center;
+  }
+
+  .md\:text-right {
+    text-align: right;
+  }
+
+  .md\:text-justify {
+    text-align: justify;
+  }
+
+  .md\:text-transparent {
+    color: transparent;
+  }
+
+  .md\:text-current {
+    color: currentColor;
+  }
+
+  .md\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .md\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .md\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .md\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .md\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .md\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .md\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .md\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .md\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .md\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .md\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .md\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .md\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .md\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .md\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .md\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .md\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .md\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .md\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .md\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .md\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .md\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .md\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .md\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .md\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .md\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .md\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .md\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .md\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .md\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .md\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .md\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .md\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .md\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .md\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .md\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .md\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .md\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .md\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .md\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .md\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .md\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .md\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .md\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .md\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .md\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .md\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .md\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .md\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .md\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .md\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .md\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .md\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .md\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .md\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .md\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .md\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .md\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .md\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .md\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .md\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .md\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .md\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .md\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .md\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .md\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .md\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .md\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .md\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .md\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .md\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .md\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .md\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .md\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .md\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .md\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .md\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .md\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .md\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .md\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .md\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .md\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .md\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .md\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .md\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .md\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .md\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .md\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .md\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .md\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .md\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .md\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .md\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .md\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .md\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .md\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .md\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .md\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .md\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .md\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .md\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .md\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .md\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .md\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .md\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .md\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .md\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .md\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .md\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .md\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .md\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .md\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .md\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .md\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .md\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .md\:italic {
+    font-style: italic;
+  }
+
+  .md\:not-italic {
+    font-style: normal;
+  }
+
+  .md\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .md\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .md\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .md\:normal-case {
+    text-transform: none;
+  }
+
+  .md\:underline {
+    text-decoration: underline;
+  }
+
+  .md\:line-through {
+    text-decoration: line-through;
+  }
+
+  .md\:no-underline {
+    text-decoration: none;
+  }
+
+  .md\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .md\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .md\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .md\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .md\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .md\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .md\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .md\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .md\:ordinal, .md\:slashed-zero, .md\:lining-nums, .md\:oldstyle-nums, .md\:proportional-nums, .md\:tabular-nums, .md\:diagonal-fractions, .md\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .md\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .md\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .md\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .md\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .md\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .md\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .md\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .md\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .md\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .md\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .md\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .md\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .md\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .md\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .md\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .md\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .md\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .md\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .md\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .md\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .md\:align-top {
+    vertical-align: top;
+  }
+
+  .md\:align-middle {
+    vertical-align: middle;
+  }
+
+  .md\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .md\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .md\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .md\:visible {
+    visibility: visible;
+  }
+
+  .md\:invisible {
+    visibility: hidden;
+  }
+
+  .md\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .md\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .md\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .md\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .md\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .md\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .md\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .md\:break-all {
+    word-break: break-all;
+  }
+
+  .md\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .md\:w-0 {
+    width: 0;
+  }
+
+  .md\:w-1 {
+    width: 0.25rem;
+  }
+
+  .md\:w-2 {
+    width: 0.5rem;
+  }
+
+  .md\:w-3 {
+    width: 0.75rem;
+  }
+
+  .md\:w-4 {
+    width: 1rem;
+  }
+
+  .md\:w-5 {
+    width: 1.25rem;
+  }
+
+  .md\:w-6 {
+    width: 1.5rem;
+  }
+
+  .md\:w-8 {
+    width: 2rem;
+  }
+
+  .md\:w-10 {
+    width: 2.5rem;
+  }
+
+  .md\:w-12 {
+    width: 3rem;
+  }
+
+  .md\:w-16 {
+    width: 4rem;
+  }
+
+  .md\:w-20 {
+    width: 5rem;
+  }
+
+  .md\:w-24 {
+    width: 6rem;
+  }
+
+  .md\:w-32 {
+    width: 8rem;
+  }
+
+  .md\:w-40 {
+    width: 10rem;
+  }
+
+  .md\:w-48 {
+    width: 12rem;
+  }
+
+  .md\:w-56 {
+    width: 14rem;
+  }
+
+  .md\:w-64 {
+    width: 16rem;
+  }
+
+  .md\:w-auto {
+    width: auto;
+  }
+
+  .md\:w-px {
+    width: 1px;
+  }
+
+  .md\:w-1\/2 {
+    width: 50%;
+  }
+
+  .md\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .md\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .md\:w-1\/4 {
+    width: 25%;
+  }
+
+  .md\:w-2\/4 {
+    width: 50%;
+  }
+
+  .md\:w-3\/4 {
+    width: 75%;
+  }
+
+  .md\:w-1\/5 {
+    width: 20%;
+  }
+
+  .md\:w-2\/5 {
+    width: 40%;
+  }
+
+  .md\:w-3\/5 {
+    width: 60%;
+  }
+
+  .md\:w-4\/5 {
+    width: 80%;
+  }
+
+  .md\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .md\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .md\:w-3\/6 {
+    width: 50%;
+  }
+
+  .md\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .md\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .md\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .md\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .md\:w-3\/12 {
+    width: 25%;
+  }
+
+  .md\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .md\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .md\:w-6\/12 {
+    width: 50%;
+  }
+
+  .md\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .md\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .md\:w-9\/12 {
+    width: 75%;
+  }
+
+  .md\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .md\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .md\:w-full {
+    width: 100%;
+  }
+
+  .md\:w-screen {
+    width: 100vw;
+  }
+
+  .md\:z-0 {
+    z-index: 0;
+  }
+
+  .md\:z-10 {
+    z-index: 10;
+  }
+
+  .md\:z-20 {
+    z-index: 20;
+  }
+
+  .md\:z-30 {
+    z-index: 30;
+  }
+
+  .md\:z-40 {
+    z-index: 40;
+  }
+
+  .md\:z-50 {
+    z-index: 50;
+  }
+
+  .md\:z-auto {
+    z-index: auto;
+  }
+
+  .md\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .md\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .md\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .md\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .md\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .md\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .md\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .md\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .md\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .md\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .md\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .md\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .md\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .md\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .md\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .md\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .md\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .md\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .md\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .md\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .md\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .md\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .md\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .md\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .md\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .md\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .md\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .md\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .md\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .md\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .md\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .md\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .md\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .md\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .md\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .md\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .md\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .md\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .md\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .md\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .md\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .md\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .md\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .md\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .md\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .md\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .md\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .md\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .md\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .md\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .md\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .md\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .md\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .md\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .md\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .md\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .md\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .md\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .md\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .md\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .md\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .md\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .md\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .md\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .md\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .md\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .md\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .md\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .md\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .md\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .md\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .md\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .md\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .md\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .md\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .md\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .md\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .md\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .md\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .md\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .md\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .md\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .md\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .md\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .md\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .md\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .md\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .md\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .md\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .md\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .md\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .md\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .md\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .md\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .md\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .md\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .md\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .md\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .md\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .md\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .md\:col-auto {
+    grid-column: auto;
+  }
+
+  .md\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .md\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .md\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .md\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .md\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .md\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .md\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .md\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .md\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .md\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .md\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .md\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .md\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .md\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .md\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .md\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .md\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .md\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .md\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .md\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .md\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .md\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .md\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .md\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .md\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .md\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .md\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .md\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .md\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .md\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .md\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .md\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .md\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .md\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .md\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .md\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .md\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .md\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .md\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .md\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .md\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .md\:row-auto {
+    grid-row: auto;
+  }
+
+  .md\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .md\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .md\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .md\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .md\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .md\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .md\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .md\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .md\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .md\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .md\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .md\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .md\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .md\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .md\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .md\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .md\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .md\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .md\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .md\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .md\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .md\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .md\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .md\:transform-none {
+    transform: none;
+  }
+
+  .md\:origin-center {
+    transform-origin: center;
+  }
+
+  .md\:origin-top {
+    transform-origin: top;
+  }
+
+  .md\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .md\:origin-right {
+    transform-origin: right;
+  }
+
+  .md\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .md\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .md\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .md\:origin-left {
+    transform-origin: left;
+  }
+
+  .md\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .md\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .md\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .md\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .md\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .md\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .md\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .md\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .md\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .md\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .md\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .md\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .md\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .md\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .md\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .md\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .md\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .md\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .md\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .md\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .md\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .md\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .md\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .md\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .md\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .md\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .md\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .md\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .md\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .md\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .md\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .md\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .md\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .md\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .md\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .md\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .md\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .md\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .md\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .md\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .md\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .md\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .md\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .md\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .md\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .md\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .md\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .md\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .md\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .md\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .md\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .md\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .md\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .md\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .md\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .md\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .md\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .md\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .md\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .md\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .md\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .md\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .md\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .md\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .md\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .md\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .md\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .md\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .md\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .md\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .md\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .md\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .md\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .md\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .md\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .md\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .md\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .md\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .md\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .md\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .md\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .md\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .md\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .md\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .md\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .md\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .md\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .md\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .md\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .md\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .md\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .md\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .md\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .md\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .md\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .md\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .md\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .md\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .md\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .md\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .md\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .md\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .md\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .md\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .md\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .md\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .md\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .md\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .md\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .md\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .md\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .md\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .md\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .md\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .md\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .md\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .md\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .md\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .md\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .md\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .md\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .md\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .md\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .md\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .md\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .md\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .md\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .md\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .md\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .md\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .md\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .md\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .md\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .md\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .md\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .md\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .md\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .md\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .md\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .md\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .md\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .md\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .md\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .md\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .md\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .md\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .md\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .md\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .md\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .md\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .md\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .md\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .md\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .md\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .md\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .md\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .md\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .md\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .md\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .md\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .md\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .md\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .md\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .md\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .md\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .md\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .md\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .md\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .md\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .md\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .md\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .md\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .md\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .md\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .md\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .md\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .md\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .md\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .md\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .md\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .md\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .md\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .md\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .md\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .md\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .md\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .md\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .md\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .md\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .md\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .md\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .md\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .md\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .md\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .md\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .md\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .md\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .md\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .md\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .md\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .md\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .md\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .md\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .md\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .md\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .md\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .md\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .md\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .md\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .md\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .md\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .md\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .md\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .md\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .md\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .md\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .md\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .md\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .md\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .md\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .md\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .md\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .md\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .md\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .md\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .md\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .md\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .md\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .md\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .md\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .md\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .md\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .md\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .md\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .md\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .md\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .md\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .md\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .md\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .md\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .md\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .md\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .md\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .md\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .md\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .md\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .md\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .md\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .md\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .md\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .md\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .md\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .md\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .md\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .md\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .md\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .md\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .md\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .md\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .md\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .md\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .md\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .md\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .md\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .md\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .md\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .md\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .md\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .md\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .md\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .md\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .md\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .md\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .md\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .md\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .md\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .md\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .md\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .md\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .md\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .md\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .md\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .md\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .md\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .md\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .md\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .md\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .md\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .md\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .md\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .md\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .md\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .md\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .md\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .md\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .md\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .md\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .md\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .md\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .md\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .md\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .md\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .md\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .md\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .md\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .md\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .md\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .md\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .md\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .md\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .md\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .md\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .md\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .md\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .md\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .md\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .md\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .md\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .md\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .md\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .md\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .md\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .md\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .md\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .md\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .md\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .md\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .md\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .md\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .md\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .md\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .md\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .md\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .md\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .md\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .md\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .md\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .md\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .md\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .md\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .md\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .md\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .md\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .md\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .md\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .md\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .md\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .md\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .md\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .md\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .md\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .md\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .md\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .md\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .md\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .md\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .md\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .md\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .md\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .md\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .md\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .md\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .md\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .md\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .md\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .md\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .md\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .md\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .md\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .md\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .md\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .md\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .md\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .md\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .md\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .md\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .md\:transition-none {
+    transition-property: none;
+  }
+
+  .md\:transition-all {
+    transition-property: all;
+  }
+
+  .md\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .md\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .md\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .md\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .md\:transition-transform {
+    transition-property: transform;
+  }
+
+  .md\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .md\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .md\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .md\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .md\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .md\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .md\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .md\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .md\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .md\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .md\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .md\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .md\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .md\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .md\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .md\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .md\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .md\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .md\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .md\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .md\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .md\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .md\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .md\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .md\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
+
+@media (min-width: 1024px) {
+  .lg\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .lg\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .lg\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .lg\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .lg\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .lg\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .lg\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .lg\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .lg\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .lg\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .lg\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .lg\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .lg\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .lg\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .lg\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .lg\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .lg\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .lg\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .lg\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .lg\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .lg\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .lg\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .lg\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .lg\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .lg\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .lg\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .lg\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .lg\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .lg\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .lg\:bg-local {
+    background-attachment: local;
+  }
+
+  .lg\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .lg\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .lg\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .lg\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .lg\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .lg\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .lg\:bg-current {
+    background-color: currentColor;
+  }
+
+  .lg\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .lg\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .lg\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .lg\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .lg\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .lg\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .lg\:bg-none {
+    background-image: none;
+  }
+
+  .lg\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .lg\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .lg\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .lg\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .lg\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .lg\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .lg\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .lg\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .lg\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .lg\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .lg\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .lg\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .lg\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .lg\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .lg\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .lg\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .lg\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .lg\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .lg\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .lg\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .lg\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .lg\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .lg\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .lg\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .lg\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .lg\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .lg\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .lg\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .lg\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .lg\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .lg\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .lg\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .lg\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .lg\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .lg\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .lg\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .lg\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .lg\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .lg\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .lg\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .lg\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .lg\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .lg\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .lg\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .lg\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .lg\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .lg\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .lg\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .lg\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .lg\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .lg\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .lg\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .lg\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .lg\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .lg\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .lg\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .lg\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .lg\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .lg\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .lg\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .lg\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .lg\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .lg\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .lg\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .lg\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .lg\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .lg\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .lg\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .lg\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .lg\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .lg\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .lg\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .lg\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .lg\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .lg\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .lg\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .lg\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .lg\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .lg\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .lg\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .lg\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .lg\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .lg\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .lg\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .lg\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .lg\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .lg\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .lg\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .lg\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .lg\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .lg\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .lg\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .lg\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .lg\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .lg\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .lg\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .lg\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .lg\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .lg\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .lg\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .lg\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .lg\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .lg\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .lg\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .lg\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .lg\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .lg\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .lg\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .lg\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .lg\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .lg\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .lg\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .lg\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .lg\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .lg\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .lg\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .lg\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .lg\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .lg\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .lg\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .lg\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .lg\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .lg\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .lg\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .lg\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .lg\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .lg\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .lg\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .lg\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .lg\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .lg\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .lg\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .lg\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .lg\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .lg\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .lg\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .lg\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .lg\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .lg\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .lg\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .lg\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .lg\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .lg\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .lg\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .lg\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .lg\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .lg\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .lg\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .lg\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .lg\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .lg\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .lg\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .lg\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .lg\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .lg\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .lg\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .lg\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .lg\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .lg\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .lg\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .lg\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .lg\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .lg\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .lg\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .lg\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .lg\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .lg\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .lg\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .lg\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .lg\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .lg\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .lg\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .lg\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .lg\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .lg\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .lg\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .lg\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .lg\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .lg\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .lg\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .lg\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .lg\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .lg\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .lg\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .lg\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .lg\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .lg\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .lg\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .lg\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .lg\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .lg\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .lg\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .lg\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .lg\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .lg\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .lg\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .lg\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .lg\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .lg\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .lg\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .lg\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .lg\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .lg\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .lg\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .lg\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .lg\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .lg\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .lg\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .lg\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .lg\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .lg\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .lg\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .lg\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .lg\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .lg\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .lg\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .lg\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .lg\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .lg\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .lg\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .lg\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .lg\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .lg\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .lg\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .lg\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .lg\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .lg\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .lg\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .lg\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .lg\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .lg\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .lg\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .lg\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .lg\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .lg\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .lg\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .lg\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .lg\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .lg\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .lg\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .lg\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .lg\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .lg\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .lg\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .lg\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .lg\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .lg\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .lg\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .lg\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .lg\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .lg\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .lg\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .lg\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .lg\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .lg\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .lg\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .lg\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .lg\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .lg\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .lg\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .lg\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .lg\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .lg\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .lg\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .lg\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .lg\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .lg\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .lg\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .lg\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .lg\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .lg\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .lg\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .lg\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .lg\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .lg\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .lg\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .lg\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .lg\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .lg\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .lg\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .lg\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .lg\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .lg\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .lg\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .lg\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .lg\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .lg\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .lg\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .lg\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .lg\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .lg\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .lg\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .lg\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .lg\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .lg\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .lg\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .lg\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .lg\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .lg\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .lg\:bg-center {
+    background-position: center;
+  }
+
+  .lg\:bg-left {
+    background-position: left;
+  }
+
+  .lg\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .lg\:bg-left-top {
+    background-position: left top;
+  }
+
+  .lg\:bg-right {
+    background-position: right;
+  }
+
+  .lg\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .lg\:bg-right-top {
+    background-position: right top;
+  }
+
+  .lg\:bg-top {
+    background-position: top;
+  }
+
+  .lg\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .lg\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .lg\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .lg\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .lg\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .lg\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .lg\:bg-auto {
+    background-size: auto;
+  }
+
+  .lg\:bg-cover {
+    background-size: cover;
+  }
+
+  .lg\:bg-contain {
+    background-size: contain;
+  }
+
+  .lg\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .lg\:border-separate {
+    border-collapse: separate;
+  }
+
+  .lg\:border-transparent {
+    border-color: transparent;
+  }
+
+  .lg\:border-current {
+    border-color: currentColor;
+  }
+
+  .lg\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .lg\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .lg\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .lg\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .lg\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .lg\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .lg\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .lg\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .lg\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .lg\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .lg\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .lg\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .lg\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .lg\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .lg\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .lg\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .lg\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .lg\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .lg\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .lg\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .lg\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .lg\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .lg\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .lg\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .lg\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .lg\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .lg\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .lg\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .lg\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .lg\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .lg\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .lg\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .lg\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .lg\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .lg\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .lg\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .lg\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .lg\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .lg\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .lg\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .lg\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .lg\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .lg\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .lg\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .lg\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .lg\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .lg\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .lg\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .lg\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .lg\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .lg\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .lg\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .lg\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .lg\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .lg\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .lg\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .lg\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .lg\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .lg\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .lg\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .lg\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .lg\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .lg\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .lg\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .lg\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .lg\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .lg\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .lg\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .lg\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .lg\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .lg\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .lg\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .lg\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .lg\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .lg\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .lg\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .lg\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .lg\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .lg\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .lg\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .lg\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .lg\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .lg\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .lg\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .lg\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .lg\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .lg\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .lg\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .lg\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .lg\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .lg\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .lg\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .lg\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .lg\:rounded-none {
+    border-radius: 0;
+  }
+
+  .lg\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .lg\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .lg\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .lg\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .lg\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .lg\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .lg\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .lg\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .lg\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .lg\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .lg\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .lg\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .lg\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .lg\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .lg\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .lg\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .lg\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .lg\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .lg\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .lg\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .lg\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .lg\:border-solid {
+    border-style: solid;
+  }
+
+  .lg\:border-dashed {
+    border-style: dashed;
+  }
+
+  .lg\:border-dotted {
+    border-style: dotted;
+  }
+
+  .lg\:border-double {
+    border-style: double;
+  }
+
+  .lg\:border-none {
+    border-style: none;
+  }
+
+  .lg\:border-0 {
+    border-width: 0;
+  }
+
+  .lg\:border-2 {
+    border-width: 2px;
+  }
+
+  .lg\:border-4 {
+    border-width: 4px;
+  }
+
+  .lg\:border-8 {
+    border-width: 8px;
+  }
+
+  .lg\:border {
+    border-width: 1px;
+  }
+
+  .lg\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .lg\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .lg\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .lg\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .lg\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .lg\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .lg\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .lg\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .lg\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .lg\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .lg\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .lg\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .lg\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .lg\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .lg\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .lg\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .lg\:border-t {
+    border-top-width: 1px;
+  }
+
+  .lg\:border-r {
+    border-right-width: 1px;
+  }
+
+  .lg\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .lg\:border-l {
+    border-left-width: 1px;
+  }
+
+  .lg\:box-border {
+    box-sizing: border-box;
+  }
+
+  .lg\:box-content {
+    box-sizing: content-box;
+  }
+
+  .lg\:cursor-auto {
+    cursor: auto;
+  }
+
+  .lg\:cursor-default {
+    cursor: default;
+  }
+
+  .lg\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .lg\:cursor-wait {
+    cursor: wait;
+  }
+
+  .lg\:cursor-text {
+    cursor: text;
+  }
+
+  .lg\:cursor-move {
+    cursor: move;
+  }
+
+  .lg\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .lg\:block {
+    display: block;
+  }
+
+  .lg\:inline-block {
+    display: inline-block;
+  }
+
+  .lg\:inline {
+    display: inline;
+  }
+
+  .lg\:flex {
+    display: flex;
+  }
+
+  .lg\:inline-flex {
+    display: inline-flex;
+  }
+
+  .lg\:table {
+    display: table;
+  }
+
+  .lg\:table-caption {
+    display: table-caption;
+  }
+
+  .lg\:table-cell {
+    display: table-cell;
+  }
+
+  .lg\:table-column {
+    display: table-column;
+  }
+
+  .lg\:table-column-group {
+    display: table-column-group;
+  }
+
+  .lg\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .lg\:table-header-group {
+    display: table-header-group;
+  }
+
+  .lg\:table-row-group {
+    display: table-row-group;
+  }
+
+  .lg\:table-row {
+    display: table-row;
+  }
+
+  .lg\:flow-root {
+    display: flow-root;
+  }
+
+  .lg\:grid {
+    display: grid;
+  }
+
+  .lg\:inline-grid {
+    display: inline-grid;
+  }
+
+  .lg\:contents {
+    display: contents;
+  }
+
+  .lg\:hidden {
+    display: none;
+  }
+
+  .lg\:flex-row {
+    flex-direction: row;
+  }
+
+  .lg\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .lg\:flex-col {
+    flex-direction: column;
+  }
+
+  .lg\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .lg\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .lg\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .lg\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .lg\:place-items-auto {
+    place-items: auto;
+  }
+
+  .lg\:place-items-start {
+    place-items: start;
+  }
+
+  .lg\:place-items-end {
+    place-items: end;
+  }
+
+  .lg\:place-items-center {
+    place-items: center;
+  }
+
+  .lg\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .lg\:place-content-center {
+    place-content: center;
+  }
+
+  .lg\:place-content-start {
+    place-content: start;
+  }
+
+  .lg\:place-content-end {
+    place-content: end;
+  }
+
+  .lg\:place-content-between {
+    place-content: space-between;
+  }
+
+  .lg\:place-content-around {
+    place-content: space-around;
+  }
+
+  .lg\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .lg\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .lg\:place-self-auto {
+    place-self: auto;
+  }
+
+  .lg\:place-self-start {
+    place-self: start;
+  }
+
+  .lg\:place-self-end {
+    place-self: end;
+  }
+
+  .lg\:place-self-center {
+    place-self: center;
+  }
+
+  .lg\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .lg\:items-start {
+    align-items: flex-start;
+  }
+
+  .lg\:items-end {
+    align-items: flex-end;
+  }
+
+  .lg\:items-center {
+    align-items: center;
+  }
+
+  .lg\:items-baseline {
+    align-items: baseline;
+  }
+
+  .lg\:items-stretch {
+    align-items: stretch;
+  }
+
+  .lg\:content-center {
+    align-content: center;
+  }
+
+  .lg\:content-start {
+    align-content: flex-start;
+  }
+
+  .lg\:content-end {
+    align-content: flex-end;
+  }
+
+  .lg\:content-between {
+    align-content: space-between;
+  }
+
+  .lg\:content-around {
+    align-content: space-around;
+  }
+
+  .lg\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .lg\:self-auto {
+    align-self: auto;
+  }
+
+  .lg\:self-start {
+    align-self: flex-start;
+  }
+
+  .lg\:self-end {
+    align-self: flex-end;
+  }
+
+  .lg\:self-center {
+    align-self: center;
+  }
+
+  .lg\:self-stretch {
+    align-self: stretch;
+  }
+
+  .lg\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .lg\:justify-items-start {
+    justify-items: start;
+  }
+
+  .lg\:justify-items-end {
+    justify-items: end;
+  }
+
+  .lg\:justify-items-center {
+    justify-items: center;
+  }
+
+  .lg\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .lg\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .lg\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .lg\:justify-center {
+    justify-content: center;
+  }
+
+  .lg\:justify-between {
+    justify-content: space-between;
+  }
+
+  .lg\:justify-around {
+    justify-content: space-around;
+  }
+
+  .lg\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .lg\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .lg\:justify-self-start {
+    justify-self: start;
+  }
+
+  .lg\:justify-self-end {
+    justify-self: end;
+  }
+
+  .lg\:justify-self-center {
+    justify-self: center;
+  }
+
+  .lg\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .lg\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .lg\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .lg\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .lg\:flex-none {
+    flex: none;
+  }
+
+  .lg\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .lg\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .lg\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .lg\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .lg\:order-1 {
+    order: 1;
+  }
+
+  .lg\:order-2 {
+    order: 2;
+  }
+
+  .lg\:order-3 {
+    order: 3;
+  }
+
+  .lg\:order-4 {
+    order: 4;
+  }
+
+  .lg\:order-5 {
+    order: 5;
+  }
+
+  .lg\:order-6 {
+    order: 6;
+  }
+
+  .lg\:order-7 {
+    order: 7;
+  }
+
+  .lg\:order-8 {
+    order: 8;
+  }
+
+  .lg\:order-9 {
+    order: 9;
+  }
+
+  .lg\:order-10 {
+    order: 10;
+  }
+
+  .lg\:order-11 {
+    order: 11;
+  }
+
+  .lg\:order-12 {
+    order: 12;
+  }
+
+  .lg\:order-first {
+    order: -9999;
+  }
+
+  .lg\:order-last {
+    order: 9999;
+  }
+
+  .lg\:order-none {
+    order: 0;
+  }
+
+  .lg\:float-right {
+    float: right;
+  }
+
+  .lg\:float-left {
+    float: left;
+  }
+
+  .lg\:float-none {
+    float: none;
+  }
+
+  .lg\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .lg\:clear-left {
+    clear: left;
+  }
+
+  .lg\:clear-right {
+    clear: right;
+  }
+
+  .lg\:clear-both {
+    clear: both;
+  }
+
+  .lg\:clear-none {
+    clear: none;
+  }
+
+  .lg\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .lg\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .lg\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .lg\:font-hairline {
+    font-weight: 100;
+  }
+
+  .lg\:font-thin {
+    font-weight: 200;
+  }
+
+  .lg\:font-light {
+    font-weight: 300;
+  }
+
+  .lg\:font-normal {
+    font-weight: 400;
+  }
+
+  .lg\:font-medium {
+    font-weight: 500;
+  }
+
+  .lg\:font-semibold {
+    font-weight: 600;
+  }
+
+  .lg\:font-bold {
+    font-weight: 700;
+  }
+
+  .lg\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .lg\:font-black {
+    font-weight: 900;
+  }
+
+  .lg\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .lg\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .lg\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .lg\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .lg\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .lg\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .lg\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .lg\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .lg\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .lg\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .lg\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .lg\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .lg\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .lg\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .lg\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .lg\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .lg\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .lg\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .lg\:h-0 {
+    height: 0;
+  }
+
+  .lg\:h-1 {
+    height: 0.25rem;
+  }
+
+  .lg\:h-2 {
+    height: 0.5rem;
+  }
+
+  .lg\:h-3 {
+    height: 0.75rem;
+  }
+
+  .lg\:h-4 {
+    height: 1rem;
+  }
+
+  .lg\:h-5 {
+    height: 1.25rem;
+  }
+
+  .lg\:h-6 {
+    height: 1.5rem;
+  }
+
+  .lg\:h-8 {
+    height: 2rem;
+  }
+
+  .lg\:h-10 {
+    height: 2.5rem;
+  }
+
+  .lg\:h-12 {
+    height: 3rem;
+  }
+
+  .lg\:h-16 {
+    height: 4rem;
+  }
+
+  .lg\:h-20 {
+    height: 5rem;
+  }
+
+  .lg\:h-24 {
+    height: 6rem;
+  }
+
+  .lg\:h-32 {
+    height: 8rem;
+  }
+
+  .lg\:h-40 {
+    height: 10rem;
+  }
+
+  .lg\:h-48 {
+    height: 12rem;
+  }
+
+  .lg\:h-56 {
+    height: 14rem;
+  }
+
+  .lg\:h-64 {
+    height: 16rem;
+  }
+
+  .lg\:h-auto {
+    height: auto;
+  }
+
+  .lg\:h-px {
+    height: 1px;
+  }
+
+  .lg\:h-full {
+    height: 100%;
+  }
+
+  .lg\:h-screen {
+    height: 100vh;
+  }
+
+  .lg\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .lg\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .lg\:text-base {
+    font-size: 1rem;
+  }
+
+  .lg\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .lg\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .lg\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .lg\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .lg\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .lg\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .lg\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .lg\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .lg\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .lg\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .lg\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .lg\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .lg\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .lg\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .lg\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .lg\:leading-none {
+    line-height: 1;
+  }
+
+  .lg\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .lg\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .lg\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .lg\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .lg\:leading-loose {
+    line-height: 2;
+  }
+
+  .lg\:list-inside {
+    list-style-position: inside;
+  }
+
+  .lg\:list-outside {
+    list-style-position: outside;
+  }
+
+  .lg\:list-none {
+    list-style-type: none;
+  }
+
+  .lg\:list-disc {
+    list-style-type: disc;
+  }
+
+  .lg\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .lg\:m-0 {
+    margin: 0;
+  }
+
+  .lg\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .lg\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .lg\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .lg\:m-4 {
+    margin: 1rem;
+  }
+
+  .lg\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .lg\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .lg\:m-8 {
+    margin: 2rem;
+  }
+
+  .lg\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .lg\:m-12 {
+    margin: 3rem;
+  }
+
+  .lg\:m-16 {
+    margin: 4rem;
+  }
+
+  .lg\:m-20 {
+    margin: 5rem;
+  }
+
+  .lg\:m-24 {
+    margin: 6rem;
+  }
+
+  .lg\:m-32 {
+    margin: 8rem;
+  }
+
+  .lg\:m-40 {
+    margin: 10rem;
+  }
+
+  .lg\:m-48 {
+    margin: 12rem;
+  }
+
+  .lg\:m-56 {
+    margin: 14rem;
+  }
+
+  .lg\:m-64 {
+    margin: 16rem;
+  }
+
+  .lg\:m-auto {
+    margin: auto;
+  }
+
+  .lg\:m-px {
+    margin: 1px;
+  }
+
+  .lg\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .lg\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .lg\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .lg\:-m-4 {
+    margin: -1rem;
+  }
+
+  .lg\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .lg\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .lg\:-m-8 {
+    margin: -2rem;
+  }
+
+  .lg\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .lg\:-m-12 {
+    margin: -3rem;
+  }
+
+  .lg\:-m-16 {
+    margin: -4rem;
+  }
+
+  .lg\:-m-20 {
+    margin: -5rem;
+  }
+
+  .lg\:-m-24 {
+    margin: -6rem;
+  }
+
+  .lg\:-m-32 {
+    margin: -8rem;
+  }
+
+  .lg\:-m-40 {
+    margin: -10rem;
+  }
+
+  .lg\:-m-48 {
+    margin: -12rem;
+  }
+
+  .lg\:-m-56 {
+    margin: -14rem;
+  }
+
+  .lg\:-m-64 {
+    margin: -16rem;
+  }
+
+  .lg\:-m-px {
+    margin: -1px;
+  }
+
+  .lg\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .lg\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .lg\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .lg\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .lg\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .lg\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .lg\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .lg\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .lg\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .lg\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .lg\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .lg\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .lg\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .lg\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .lg\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .lg\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .lg\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .lg\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .lg\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .lg\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .lg\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .lg\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .lg\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .lg\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .lg\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .lg\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .lg\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .lg\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .lg\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .lg\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .lg\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .lg\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .lg\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .lg\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .lg\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .lg\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .lg\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .lg\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .lg\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .lg\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .lg\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .lg\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .lg\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .lg\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .lg\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .lg\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .lg\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .lg\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .lg\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .lg\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .lg\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .lg\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .lg\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .lg\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .lg\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .lg\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .lg\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .lg\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .lg\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .lg\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .lg\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .lg\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .lg\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .lg\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .lg\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .lg\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .lg\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .lg\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .lg\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .lg\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .lg\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .lg\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .lg\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .lg\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .lg\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .lg\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .lg\:mt-0 {
+    margin-top: 0;
+  }
+
+  .lg\:mr-0 {
+    margin-right: 0;
+  }
+
+  .lg\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .lg\:ml-0 {
+    margin-left: 0;
+  }
+
+  .lg\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .lg\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .lg\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .lg\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .lg\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .lg\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .lg\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .lg\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .lg\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .lg\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .lg\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .lg\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .lg\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .lg\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .lg\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .lg\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .lg\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .lg\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .lg\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .lg\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .lg\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .lg\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .lg\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .lg\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .lg\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .lg\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .lg\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .lg\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .lg\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .lg\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .lg\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .lg\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .lg\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .lg\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .lg\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .lg\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .lg\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .lg\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .lg\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .lg\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .lg\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .lg\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .lg\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .lg\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .lg\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .lg\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .lg\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .lg\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .lg\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .lg\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .lg\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .lg\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .lg\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .lg\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .lg\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .lg\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .lg\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .lg\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .lg\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .lg\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .lg\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .lg\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .lg\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .lg\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .lg\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .lg\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .lg\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .lg\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .lg\:mt-auto {
+    margin-top: auto;
+  }
+
+  .lg\:mr-auto {
+    margin-right: auto;
+  }
+
+  .lg\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .lg\:ml-auto {
+    margin-left: auto;
+  }
+
+  .lg\:mt-px {
+    margin-top: 1px;
+  }
+
+  .lg\:mr-px {
+    margin-right: 1px;
+  }
+
+  .lg\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .lg\:ml-px {
+    margin-left: 1px;
+  }
+
+  .lg\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .lg\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .lg\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .lg\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .lg\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .lg\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .lg\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .lg\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .lg\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .lg\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .lg\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .lg\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .lg\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .lg\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .lg\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .lg\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .lg\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .lg\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .lg\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .lg\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .lg\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .lg\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .lg\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .lg\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .lg\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .lg\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .lg\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .lg\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .lg\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .lg\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .lg\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .lg\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .lg\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .lg\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .lg\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .lg\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .lg\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .lg\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .lg\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .lg\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .lg\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .lg\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .lg\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .lg\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .lg\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .lg\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .lg\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .lg\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .lg\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .lg\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .lg\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .lg\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .lg\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .lg\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .lg\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .lg\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .lg\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .lg\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .lg\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .lg\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .lg\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .lg\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .lg\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .lg\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .lg\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .lg\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .lg\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .lg\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .lg\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .lg\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .lg\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .lg\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .lg\:max-h-full {
+    max-height: 100%;
+  }
+
+  .lg\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .lg\:max-w-none {
+    max-width: none;
+  }
+
+  .lg\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .lg\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .lg\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .lg\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .lg\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .lg\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .lg\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .lg\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .lg\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .lg\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .lg\:max-w-full {
+    max-width: 100%;
+  }
+
+  .lg\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .lg\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .lg\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .lg\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .lg\:min-h-0 {
+    min-height: 0;
+  }
+
+  .lg\:min-h-full {
+    min-height: 100%;
+  }
+
+  .lg\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .lg\:min-w-0 {
+    min-width: 0;
+  }
+
+  .lg\:min-w-full {
+    min-width: 100%;
+  }
+
+  .lg\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .lg\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .lg\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .lg\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .lg\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .lg\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .lg\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .lg\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .lg\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .lg\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .lg\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .lg\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .lg\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .lg\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .lg\:opacity-0 {
+    opacity: 0;
+  }
+
+  .lg\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .lg\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .lg\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .lg\:opacity-100 {
+    opacity: 1;
+  }
+
+  .lg\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .lg\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .lg\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .lg\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .lg\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .lg\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .lg\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .lg\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .lg\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .lg\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .lg\:outline-none {
+    outline: 0;
+  }
+
+  .lg\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .lg\:overflow-auto {
+    overflow: auto;
+  }
+
+  .lg\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .lg\:overflow-visible {
+    overflow: visible;
+  }
+
+  .lg\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .lg\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .lg\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .lg\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .lg\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .lg\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .lg\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .lg\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .lg\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .lg\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .lg\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .lg\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .lg\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .lg\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .lg\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .lg\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .lg\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .lg\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .lg\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .lg\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .lg\:p-0 {
+    padding: 0;
+  }
+
+  .lg\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .lg\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .lg\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .lg\:p-4 {
+    padding: 1rem;
+  }
+
+  .lg\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .lg\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .lg\:p-8 {
+    padding: 2rem;
+  }
+
+  .lg\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .lg\:p-12 {
+    padding: 3rem;
+  }
+
+  .lg\:p-16 {
+    padding: 4rem;
+  }
+
+  .lg\:p-20 {
+    padding: 5rem;
+  }
+
+  .lg\:p-24 {
+    padding: 6rem;
+  }
+
+  .lg\:p-32 {
+    padding: 8rem;
+  }
+
+  .lg\:p-40 {
+    padding: 10rem;
+  }
+
+  .lg\:p-48 {
+    padding: 12rem;
+  }
+
+  .lg\:p-56 {
+    padding: 14rem;
+  }
+
+  .lg\:p-64 {
+    padding: 16rem;
+  }
+
+  .lg\:p-px {
+    padding: 1px;
+  }
+
+  .lg\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .lg\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .lg\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .lg\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .lg\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .lg\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .lg\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .lg\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .lg\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .lg\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .lg\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .lg\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .lg\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .lg\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .lg\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .lg\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .lg\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .lg\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .lg\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .lg\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .lg\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .lg\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .lg\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .lg\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .lg\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .lg\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .lg\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .lg\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .lg\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .lg\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .lg\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .lg\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .lg\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .lg\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .lg\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .lg\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .lg\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .lg\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .lg\:pt-0 {
+    padding-top: 0;
+  }
+
+  .lg\:pr-0 {
+    padding-right: 0;
+  }
+
+  .lg\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .lg\:pl-0 {
+    padding-left: 0;
+  }
+
+  .lg\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .lg\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .lg\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .lg\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .lg\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .lg\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .lg\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .lg\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .lg\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .lg\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .lg\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .lg\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .lg\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .lg\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .lg\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .lg\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .lg\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .lg\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .lg\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .lg\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .lg\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .lg\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .lg\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .lg\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .lg\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .lg\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .lg\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .lg\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .lg\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .lg\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .lg\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .lg\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .lg\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .lg\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .lg\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .lg\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .lg\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .lg\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .lg\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .lg\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .lg\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .lg\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .lg\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .lg\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .lg\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .lg\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .lg\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .lg\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .lg\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .lg\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .lg\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .lg\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .lg\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .lg\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .lg\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .lg\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .lg\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .lg\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .lg\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .lg\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .lg\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .lg\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .lg\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .lg\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .lg\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .lg\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .lg\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .lg\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .lg\:pt-px {
+    padding-top: 1px;
+  }
+
+  .lg\:pr-px {
+    padding-right: 1px;
+  }
+
+  .lg\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .lg\:pl-px {
+    padding-left: 1px;
+  }
+
+  .lg\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .lg\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .lg\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .lg\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .lg\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .lg\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .lg\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .lg\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .lg\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .lg\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .lg\:static {
+    position: static;
+  }
+
+  .lg\:fixed {
+    position: fixed;
+  }
+
+  .lg\:absolute {
+    position: absolute;
+  }
+
+  .lg\:relative {
+    position: relative;
+  }
+
+  .lg\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .lg\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .lg\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .lg\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .lg\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .lg\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .lg\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .lg\:top-0 {
+    top: 0;
+  }
+
+  .lg\:right-0 {
+    right: 0;
+  }
+
+  .lg\:bottom-0 {
+    bottom: 0;
+  }
+
+  .lg\:left-0 {
+    left: 0;
+  }
+
+  .lg\:top-auto {
+    top: auto;
+  }
+
+  .lg\:right-auto {
+    right: auto;
+  }
+
+  .lg\:bottom-auto {
+    bottom: auto;
+  }
+
+  .lg\:left-auto {
+    left: auto;
+  }
+
+  .lg\:resize-none {
+    resize: none;
+  }
+
+  .lg\:resize-y {
+    resize: vertical;
+  }
+
+  .lg\:resize-x {
+    resize: horizontal;
+  }
+
+  .lg\:resize {
+    resize: both;
+  }
+
+  .lg\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .lg\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .lg\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .lg\:shadow-none {
+    box-shadow: none;
+  }
+
+  .lg\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .lg\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .lg\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .lg\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .lg\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .lg\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .lg\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .lg\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .lg\:fill-current {
+    fill: currentColor;
+  }
+
+  .lg\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .lg\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .lg\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .lg\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .lg\:table-auto {
+    table-layout: auto;
+  }
+
+  .lg\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .lg\:text-left {
+    text-align: left;
+  }
+
+  .lg\:text-center {
+    text-align: center;
+  }
+
+  .lg\:text-right {
+    text-align: right;
+  }
+
+  .lg\:text-justify {
+    text-align: justify;
+  }
+
+  .lg\:text-transparent {
+    color: transparent;
+  }
+
+  .lg\:text-current {
+    color: currentColor;
+  }
+
+  .lg\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .lg\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .lg\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .lg\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .lg\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .lg\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .lg\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .lg\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .lg\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .lg\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .lg\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .lg\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .lg\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .lg\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .lg\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .lg\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .lg\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .lg\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .lg\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .lg\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .lg\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .lg\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .lg\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .lg\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .lg\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .lg\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .lg\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .lg\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .lg\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .lg\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .lg\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .lg\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .lg\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .lg\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .lg\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .lg\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .lg\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .lg\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .lg\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .lg\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .lg\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .lg\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .lg\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .lg\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .lg\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .lg\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .lg\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .lg\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .lg\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .lg\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .lg\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .lg\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .lg\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .lg\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .lg\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .lg\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .lg\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .lg\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .lg\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .lg\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .lg\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .lg\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .lg\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .lg\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .lg\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .lg\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .lg\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .lg\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .lg\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .lg\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .lg\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .lg\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .lg\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .lg\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .lg\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .lg\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .lg\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .lg\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .lg\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .lg\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .lg\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .lg\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .lg\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .lg\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .lg\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .lg\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .lg\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .lg\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .lg\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .lg\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .lg\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .lg\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .lg\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .lg\:italic {
+    font-style: italic;
+  }
+
+  .lg\:not-italic {
+    font-style: normal;
+  }
+
+  .lg\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .lg\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .lg\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .lg\:normal-case {
+    text-transform: none;
+  }
+
+  .lg\:underline {
+    text-decoration: underline;
+  }
+
+  .lg\:line-through {
+    text-decoration: line-through;
+  }
+
+  .lg\:no-underline {
+    text-decoration: none;
+  }
+
+  .lg\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .lg\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .lg\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .lg\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .lg\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .lg\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .lg\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .lg\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .lg\:ordinal, .lg\:slashed-zero, .lg\:lining-nums, .lg\:oldstyle-nums, .lg\:proportional-nums, .lg\:tabular-nums, .lg\:diagonal-fractions, .lg\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .lg\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .lg\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .lg\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .lg\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .lg\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .lg\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .lg\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .lg\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .lg\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .lg\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .lg\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .lg\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .lg\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .lg\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .lg\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .lg\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .lg\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .lg\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .lg\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .lg\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .lg\:align-top {
+    vertical-align: top;
+  }
+
+  .lg\:align-middle {
+    vertical-align: middle;
+  }
+
+  .lg\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .lg\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .lg\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .lg\:visible {
+    visibility: visible;
+  }
+
+  .lg\:invisible {
+    visibility: hidden;
+  }
+
+  .lg\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .lg\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .lg\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .lg\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .lg\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .lg\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .lg\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .lg\:break-all {
+    word-break: break-all;
+  }
+
+  .lg\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .lg\:w-0 {
+    width: 0;
+  }
+
+  .lg\:w-1 {
+    width: 0.25rem;
+  }
+
+  .lg\:w-2 {
+    width: 0.5rem;
+  }
+
+  .lg\:w-3 {
+    width: 0.75rem;
+  }
+
+  .lg\:w-4 {
+    width: 1rem;
+  }
+
+  .lg\:w-5 {
+    width: 1.25rem;
+  }
+
+  .lg\:w-6 {
+    width: 1.5rem;
+  }
+
+  .lg\:w-8 {
+    width: 2rem;
+  }
+
+  .lg\:w-10 {
+    width: 2.5rem;
+  }
+
+  .lg\:w-12 {
+    width: 3rem;
+  }
+
+  .lg\:w-16 {
+    width: 4rem;
+  }
+
+  .lg\:w-20 {
+    width: 5rem;
+  }
+
+  .lg\:w-24 {
+    width: 6rem;
+  }
+
+  .lg\:w-32 {
+    width: 8rem;
+  }
+
+  .lg\:w-40 {
+    width: 10rem;
+  }
+
+  .lg\:w-48 {
+    width: 12rem;
+  }
+
+  .lg\:w-56 {
+    width: 14rem;
+  }
+
+  .lg\:w-64 {
+    width: 16rem;
+  }
+
+  .lg\:w-auto {
+    width: auto;
+  }
+
+  .lg\:w-px {
+    width: 1px;
+  }
+
+  .lg\:w-1\/2 {
+    width: 50%;
+  }
+
+  .lg\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .lg\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .lg\:w-1\/4 {
+    width: 25%;
+  }
+
+  .lg\:w-2\/4 {
+    width: 50%;
+  }
+
+  .lg\:w-3\/4 {
+    width: 75%;
+  }
+
+  .lg\:w-1\/5 {
+    width: 20%;
+  }
+
+  .lg\:w-2\/5 {
+    width: 40%;
+  }
+
+  .lg\:w-3\/5 {
+    width: 60%;
+  }
+
+  .lg\:w-4\/5 {
+    width: 80%;
+  }
+
+  .lg\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .lg\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .lg\:w-3\/6 {
+    width: 50%;
+  }
+
+  .lg\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .lg\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .lg\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .lg\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .lg\:w-3\/12 {
+    width: 25%;
+  }
+
+  .lg\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .lg\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .lg\:w-6\/12 {
+    width: 50%;
+  }
+
+  .lg\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .lg\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .lg\:w-9\/12 {
+    width: 75%;
+  }
+
+  .lg\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .lg\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .lg\:w-full {
+    width: 100%;
+  }
+
+  .lg\:w-screen {
+    width: 100vw;
+  }
+
+  .lg\:z-0 {
+    z-index: 0;
+  }
+
+  .lg\:z-10 {
+    z-index: 10;
+  }
+
+  .lg\:z-20 {
+    z-index: 20;
+  }
+
+  .lg\:z-30 {
+    z-index: 30;
+  }
+
+  .lg\:z-40 {
+    z-index: 40;
+  }
+
+  .lg\:z-50 {
+    z-index: 50;
+  }
+
+  .lg\:z-auto {
+    z-index: auto;
+  }
+
+  .lg\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .lg\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .lg\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .lg\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .lg\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .lg\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .lg\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .lg\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .lg\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .lg\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .lg\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .lg\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .lg\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .lg\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .lg\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .lg\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .lg\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .lg\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .lg\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .lg\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .lg\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .lg\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .lg\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .lg\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .lg\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .lg\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .lg\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .lg\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .lg\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .lg\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .lg\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .lg\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .lg\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .lg\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .lg\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .lg\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .lg\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .lg\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .lg\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .lg\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .lg\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .lg\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .lg\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .lg\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .lg\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .lg\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .lg\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .lg\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .lg\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .lg\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .lg\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .lg\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .lg\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .lg\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .lg\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .lg\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .lg\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .lg\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .lg\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .lg\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .lg\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .lg\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .lg\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .lg\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .lg\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .lg\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .lg\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .lg\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .lg\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .lg\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .lg\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .lg\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .lg\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .lg\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .lg\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .lg\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .lg\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .lg\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .lg\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .lg\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .lg\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .lg\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .lg\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .lg\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .lg\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .lg\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .lg\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .lg\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .lg\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .lg\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .lg\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .lg\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .lg\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .lg\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .lg\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .lg\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .lg\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .lg\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .lg\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .lg\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .lg\:col-auto {
+    grid-column: auto;
+  }
+
+  .lg\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .lg\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .lg\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .lg\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .lg\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .lg\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .lg\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .lg\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .lg\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .lg\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .lg\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .lg\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .lg\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .lg\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .lg\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .lg\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .lg\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .lg\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .lg\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .lg\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .lg\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .lg\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .lg\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .lg\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .lg\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .lg\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .lg\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .lg\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .lg\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .lg\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .lg\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .lg\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .lg\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .lg\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .lg\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .lg\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .lg\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .lg\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .lg\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .lg\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .lg\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .lg\:row-auto {
+    grid-row: auto;
+  }
+
+  .lg\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .lg\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .lg\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .lg\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .lg\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .lg\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .lg\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .lg\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .lg\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .lg\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .lg\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .lg\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .lg\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .lg\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .lg\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .lg\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .lg\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .lg\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .lg\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .lg\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .lg\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .lg\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .lg\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .lg\:transform-none {
+    transform: none;
+  }
+
+  .lg\:origin-center {
+    transform-origin: center;
+  }
+
+  .lg\:origin-top {
+    transform-origin: top;
+  }
+
+  .lg\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .lg\:origin-right {
+    transform-origin: right;
+  }
+
+  .lg\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .lg\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .lg\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .lg\:origin-left {
+    transform-origin: left;
+  }
+
+  .lg\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .lg\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .lg\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .lg\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .lg\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .lg\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .lg\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .lg\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .lg\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .lg\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .lg\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .lg\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .lg\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .lg\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .lg\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .lg\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .lg\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .lg\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .lg\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .lg\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .lg\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .lg\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .lg\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .lg\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .lg\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .lg\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .lg\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .lg\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .lg\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .lg\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .lg\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .lg\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .lg\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .lg\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .lg\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .lg\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .lg\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .lg\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .lg\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .lg\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .lg\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .lg\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .lg\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .lg\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .lg\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .lg\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .lg\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .lg\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .lg\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .lg\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .lg\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .lg\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .lg\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .lg\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .lg\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .lg\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .lg\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .lg\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .lg\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .lg\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .lg\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .lg\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .lg\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .lg\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .lg\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .lg\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .lg\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .lg\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .lg\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .lg\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .lg\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .lg\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .lg\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .lg\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .lg\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .lg\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .lg\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .lg\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .lg\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .lg\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .lg\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .lg\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .lg\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .lg\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .lg\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .lg\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .lg\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .lg\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .lg\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .lg\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .lg\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .lg\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .lg\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .lg\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .lg\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .lg\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .lg\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .lg\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .lg\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .lg\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .lg\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .lg\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .lg\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .lg\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .lg\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .lg\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .lg\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .lg\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .lg\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .lg\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .lg\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .lg\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .lg\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .lg\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .lg\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .lg\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .lg\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .lg\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .lg\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .lg\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .lg\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .lg\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .lg\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .lg\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .lg\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .lg\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .lg\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .lg\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .lg\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .lg\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .lg\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .lg\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .lg\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .lg\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .lg\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .lg\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .lg\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .lg\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .lg\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .lg\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .lg\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .lg\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .lg\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .lg\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .lg\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .lg\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .lg\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .lg\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .lg\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .lg\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .lg\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .lg\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .lg\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .lg\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .lg\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .lg\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .lg\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .lg\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .lg\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .lg\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .lg\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .lg\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .lg\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .lg\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .lg\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .lg\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .lg\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .lg\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .lg\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .lg\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .lg\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .lg\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .lg\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .lg\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .lg\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .lg\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .lg\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .lg\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .lg\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .lg\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .lg\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .lg\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .lg\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .lg\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .lg\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .lg\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .lg\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .lg\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .lg\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .lg\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .lg\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .lg\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .lg\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .lg\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .lg\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .lg\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .lg\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .lg\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .lg\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .lg\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .lg\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .lg\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .lg\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .lg\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .lg\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .lg\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .lg\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .lg\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .lg\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .lg\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .lg\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .lg\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .lg\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .lg\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .lg\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .lg\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .lg\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .lg\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .lg\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .lg\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .lg\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .lg\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .lg\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .lg\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .lg\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .lg\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .lg\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .lg\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .lg\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .lg\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .lg\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .lg\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .lg\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .lg\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .lg\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .lg\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .lg\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .lg\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .lg\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .lg\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .lg\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .lg\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .lg\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .lg\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .lg\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .lg\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .lg\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .lg\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .lg\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .lg\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .lg\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .lg\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .lg\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .lg\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .lg\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .lg\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .lg\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .lg\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .lg\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .lg\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .lg\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .lg\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .lg\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .lg\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .lg\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .lg\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .lg\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .lg\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .lg\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .lg\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .lg\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .lg\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .lg\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .lg\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .lg\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .lg\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .lg\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .lg\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .lg\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .lg\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .lg\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .lg\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .lg\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .lg\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .lg\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .lg\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .lg\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .lg\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .lg\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .lg\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .lg\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .lg\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .lg\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .lg\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .lg\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .lg\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .lg\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .lg\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .lg\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .lg\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .lg\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .lg\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .lg\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .lg\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .lg\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .lg\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .lg\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .lg\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .lg\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .lg\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .lg\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .lg\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .lg\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .lg\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .lg\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .lg\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .lg\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .lg\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .lg\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .lg\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .lg\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .lg\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .lg\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .lg\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .lg\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .lg\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .lg\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .lg\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .lg\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .lg\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .lg\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .lg\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .lg\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .lg\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .lg\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .lg\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .lg\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .lg\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .lg\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .lg\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .lg\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .lg\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .lg\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .lg\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .lg\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .lg\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .lg\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .lg\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .lg\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .lg\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .lg\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .lg\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .lg\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .lg\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .lg\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .lg\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .lg\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .lg\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .lg\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .lg\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .lg\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .lg\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .lg\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .lg\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .lg\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .lg\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .lg\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .lg\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .lg\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .lg\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .lg\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .lg\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .lg\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .lg\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .lg\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .lg\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .lg\:transition-none {
+    transition-property: none;
+  }
+
+  .lg\:transition-all {
+    transition-property: all;
+  }
+
+  .lg\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .lg\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .lg\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .lg\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .lg\:transition-transform {
+    transition-property: transform;
+  }
+
+  .lg\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .lg\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .lg\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .lg\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .lg\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .lg\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .lg\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .lg\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .lg\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .lg\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .lg\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .lg\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .lg\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .lg\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .lg\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .lg\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .lg\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .lg\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .lg\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .lg\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .lg\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .lg\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .lg\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .lg\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .lg\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
+
+@media (min-width: 1280px) {
+  .xl\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .xl\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .xl\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .xl\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .xl\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .xl\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .xl\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .xl\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .xl\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .xl\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .xl\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .xl\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .xl\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .xl\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .xl\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .xl\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .xl\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .xl\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .xl\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .xl\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .xl\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .xl\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .xl\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .xl\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .xl\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .xl\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .xl\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .xl\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .xl\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .xl\:bg-local {
+    background-attachment: local;
+  }
+
+  .xl\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .xl\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .xl\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .xl\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .xl\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .xl\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .xl\:bg-current {
+    background-color: currentColor;
+  }
+
+  .xl\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .xl\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .xl\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .xl\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .xl\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .xl\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .xl\:bg-none {
+    background-image: none;
+  }
+
+  .xl\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .xl\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .xl\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .xl\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .xl\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .xl\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .xl\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .xl\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .xl\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .xl\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .xl\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .xl\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .xl\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .xl\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .xl\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .xl\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .xl\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .xl\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .xl\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .xl\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .xl\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .xl\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .xl\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .xl\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .xl\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .xl\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .xl\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .xl\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .xl\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .xl\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .xl\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .xl\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .xl\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .xl\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .xl\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .xl\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .xl\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .xl\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .xl\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .xl\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .xl\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .xl\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .xl\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .xl\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .xl\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .xl\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .xl\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .xl\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .xl\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .xl\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .xl\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .xl\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .xl\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .xl\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .xl\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .xl\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .xl\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .xl\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .xl\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .xl\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .xl\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .xl\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .xl\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .xl\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .xl\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .xl\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .xl\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .xl\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .xl\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .xl\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .xl\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .xl\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .xl\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .xl\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .xl\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .xl\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .xl\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .xl\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .xl\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .xl\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .xl\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .xl\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .xl\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .xl\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .xl\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .xl\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .xl\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .xl\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .xl\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .xl\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .xl\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .xl\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .xl\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .xl\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .xl\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .xl\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .xl\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .xl\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .xl\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .xl\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .xl\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .xl\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .xl\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .xl\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .xl\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .xl\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .xl\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .xl\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .xl\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .xl\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .xl\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .xl\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .xl\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .xl\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .xl\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .xl\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .xl\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .xl\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .xl\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .xl\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .xl\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .xl\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .xl\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .xl\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .xl\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .xl\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .xl\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .xl\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .xl\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .xl\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .xl\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .xl\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .xl\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .xl\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .xl\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .xl\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .xl\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .xl\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .xl\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .xl\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .xl\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .xl\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .xl\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .xl\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .xl\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .xl\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .xl\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .xl\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .xl\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .xl\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .xl\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .xl\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .xl\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .xl\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .xl\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .xl\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .xl\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .xl\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .xl\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .xl\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .xl\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .xl\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .xl\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .xl\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .xl\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .xl\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .xl\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .xl\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .xl\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .xl\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .xl\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .xl\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .xl\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .xl\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .xl\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .xl\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .xl\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .xl\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .xl\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .xl\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .xl\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .xl\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .xl\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .xl\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .xl\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .xl\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .xl\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .xl\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .xl\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .xl\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .xl\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .xl\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .xl\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .xl\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .xl\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .xl\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .xl\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .xl\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .xl\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .xl\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .xl\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .xl\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .xl\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .xl\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .xl\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .xl\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .xl\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .xl\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .xl\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .xl\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .xl\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .xl\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .xl\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .xl\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .xl\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .xl\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .xl\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .xl\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .xl\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .xl\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .xl\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .xl\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .xl\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .xl\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .xl\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .xl\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .xl\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .xl\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .xl\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .xl\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .xl\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .xl\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .xl\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .xl\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .xl\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .xl\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .xl\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .xl\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .xl\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .xl\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .xl\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .xl\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .xl\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .xl\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .xl\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .xl\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .xl\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .xl\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .xl\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .xl\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .xl\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .xl\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .xl\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .xl\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .xl\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .xl\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .xl\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .xl\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .xl\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .xl\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .xl\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .xl\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .xl\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .xl\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .xl\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .xl\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .xl\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .xl\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .xl\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .xl\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .xl\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .xl\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .xl\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .xl\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .xl\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .xl\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .xl\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .xl\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .xl\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .xl\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .xl\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .xl\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .xl\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .xl\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .xl\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .xl\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .xl\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .xl\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .xl\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .xl\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .xl\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .xl\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .xl\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .xl\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .xl\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .xl\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .xl\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .xl\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .xl\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .xl\:bg-center {
+    background-position: center;
+  }
+
+  .xl\:bg-left {
+    background-position: left;
+  }
+
+  .xl\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .xl\:bg-left-top {
+    background-position: left top;
+  }
+
+  .xl\:bg-right {
+    background-position: right;
+  }
+
+  .xl\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .xl\:bg-right-top {
+    background-position: right top;
+  }
+
+  .xl\:bg-top {
+    background-position: top;
+  }
+
+  .xl\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .xl\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .xl\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .xl\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .xl\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .xl\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .xl\:bg-auto {
+    background-size: auto;
+  }
+
+  .xl\:bg-cover {
+    background-size: cover;
+  }
+
+  .xl\:bg-contain {
+    background-size: contain;
+  }
+
+  .xl\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .xl\:border-separate {
+    border-collapse: separate;
+  }
+
+  .xl\:border-transparent {
+    border-color: transparent;
+  }
+
+  .xl\:border-current {
+    border-color: currentColor;
+  }
+
+  .xl\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .xl\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .xl\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .xl\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .xl\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .xl\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .xl\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .xl\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .xl\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .xl\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .xl\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .xl\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .xl\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .xl\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .xl\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .xl\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .xl\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .xl\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .xl\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .xl\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .xl\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .xl\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .xl\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .xl\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .xl\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .xl\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .xl\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .xl\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .xl\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .xl\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .xl\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .xl\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .xl\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .xl\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .xl\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .xl\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .xl\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .xl\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .xl\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .xl\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .xl\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .xl\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .xl\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .xl\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .xl\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .xl\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .xl\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .xl\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .xl\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .xl\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .xl\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .xl\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .xl\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .xl\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .xl\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .xl\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .xl\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .xl\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .xl\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .xl\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .xl\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .xl\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .xl\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .xl\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .xl\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .xl\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .xl\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .xl\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .xl\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .xl\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .xl\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .xl\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .xl\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .xl\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .xl\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .xl\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .xl\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .xl\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .xl\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .xl\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .xl\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .xl\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .xl\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .xl\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .xl\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .xl\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .xl\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .xl\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .xl\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .xl\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .xl\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .xl\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .xl\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .xl\:rounded-none {
+    border-radius: 0;
+  }
+
+  .xl\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .xl\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .xl\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .xl\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .xl\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .xl\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .xl\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .xl\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .xl\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .xl\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .xl\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .xl\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .xl\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .xl\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .xl\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .xl\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .xl\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .xl\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .xl\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .xl\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .xl\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .xl\:border-solid {
+    border-style: solid;
+  }
+
+  .xl\:border-dashed {
+    border-style: dashed;
+  }
+
+  .xl\:border-dotted {
+    border-style: dotted;
+  }
+
+  .xl\:border-double {
+    border-style: double;
+  }
+
+  .xl\:border-none {
+    border-style: none;
+  }
+
+  .xl\:border-0 {
+    border-width: 0;
+  }
+
+  .xl\:border-2 {
+    border-width: 2px;
+  }
+
+  .xl\:border-4 {
+    border-width: 4px;
+  }
+
+  .xl\:border-8 {
+    border-width: 8px;
+  }
+
+  .xl\:border {
+    border-width: 1px;
+  }
+
+  .xl\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .xl\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .xl\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .xl\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .xl\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .xl\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .xl\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .xl\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .xl\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .xl\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .xl\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .xl\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .xl\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .xl\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .xl\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .xl\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .xl\:border-t {
+    border-top-width: 1px;
+  }
+
+  .xl\:border-r {
+    border-right-width: 1px;
+  }
+
+  .xl\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .xl\:border-l {
+    border-left-width: 1px;
+  }
+
+  .xl\:box-border {
+    box-sizing: border-box;
+  }
+
+  .xl\:box-content {
+    box-sizing: content-box;
+  }
+
+  .xl\:cursor-auto {
+    cursor: auto;
+  }
+
+  .xl\:cursor-default {
+    cursor: default;
+  }
+
+  .xl\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .xl\:cursor-wait {
+    cursor: wait;
+  }
+
+  .xl\:cursor-text {
+    cursor: text;
+  }
+
+  .xl\:cursor-move {
+    cursor: move;
+  }
+
+  .xl\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .xl\:block {
+    display: block;
+  }
+
+  .xl\:inline-block {
+    display: inline-block;
+  }
+
+  .xl\:inline {
+    display: inline;
+  }
+
+  .xl\:flex {
+    display: flex;
+  }
+
+  .xl\:inline-flex {
+    display: inline-flex;
+  }
+
+  .xl\:table {
+    display: table;
+  }
+
+  .xl\:table-caption {
+    display: table-caption;
+  }
+
+  .xl\:table-cell {
+    display: table-cell;
+  }
+
+  .xl\:table-column {
+    display: table-column;
+  }
+
+  .xl\:table-column-group {
+    display: table-column-group;
+  }
+
+  .xl\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .xl\:table-header-group {
+    display: table-header-group;
+  }
+
+  .xl\:table-row-group {
+    display: table-row-group;
+  }
+
+  .xl\:table-row {
+    display: table-row;
+  }
+
+  .xl\:flow-root {
+    display: flow-root;
+  }
+
+  .xl\:grid {
+    display: grid;
+  }
+
+  .xl\:inline-grid {
+    display: inline-grid;
+  }
+
+  .xl\:contents {
+    display: contents;
+  }
+
+  .xl\:hidden {
+    display: none;
+  }
+
+  .xl\:flex-row {
+    flex-direction: row;
+  }
+
+  .xl\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .xl\:flex-col {
+    flex-direction: column;
+  }
+
+  .xl\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .xl\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .xl\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .xl\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .xl\:place-items-auto {
+    place-items: auto;
+  }
+
+  .xl\:place-items-start {
+    place-items: start;
+  }
+
+  .xl\:place-items-end {
+    place-items: end;
+  }
+
+  .xl\:place-items-center {
+    place-items: center;
+  }
+
+  .xl\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .xl\:place-content-center {
+    place-content: center;
+  }
+
+  .xl\:place-content-start {
+    place-content: start;
+  }
+
+  .xl\:place-content-end {
+    place-content: end;
+  }
+
+  .xl\:place-content-between {
+    place-content: space-between;
+  }
+
+  .xl\:place-content-around {
+    place-content: space-around;
+  }
+
+  .xl\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .xl\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .xl\:place-self-auto {
+    place-self: auto;
+  }
+
+  .xl\:place-self-start {
+    place-self: start;
+  }
+
+  .xl\:place-self-end {
+    place-self: end;
+  }
+
+  .xl\:place-self-center {
+    place-self: center;
+  }
+
+  .xl\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .xl\:items-start {
+    align-items: flex-start;
+  }
+
+  .xl\:items-end {
+    align-items: flex-end;
+  }
+
+  .xl\:items-center {
+    align-items: center;
+  }
+
+  .xl\:items-baseline {
+    align-items: baseline;
+  }
+
+  .xl\:items-stretch {
+    align-items: stretch;
+  }
+
+  .xl\:content-center {
+    align-content: center;
+  }
+
+  .xl\:content-start {
+    align-content: flex-start;
+  }
+
+  .xl\:content-end {
+    align-content: flex-end;
+  }
+
+  .xl\:content-between {
+    align-content: space-between;
+  }
+
+  .xl\:content-around {
+    align-content: space-around;
+  }
+
+  .xl\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .xl\:self-auto {
+    align-self: auto;
+  }
+
+  .xl\:self-start {
+    align-self: flex-start;
+  }
+
+  .xl\:self-end {
+    align-self: flex-end;
+  }
+
+  .xl\:self-center {
+    align-self: center;
+  }
+
+  .xl\:self-stretch {
+    align-self: stretch;
+  }
+
+  .xl\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .xl\:justify-items-start {
+    justify-items: start;
+  }
+
+  .xl\:justify-items-end {
+    justify-items: end;
+  }
+
+  .xl\:justify-items-center {
+    justify-items: center;
+  }
+
+  .xl\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .xl\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .xl\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .xl\:justify-center {
+    justify-content: center;
+  }
+
+  .xl\:justify-between {
+    justify-content: space-between;
+  }
+
+  .xl\:justify-around {
+    justify-content: space-around;
+  }
+
+  .xl\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .xl\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .xl\:justify-self-start {
+    justify-self: start;
+  }
+
+  .xl\:justify-self-end {
+    justify-self: end;
+  }
+
+  .xl\:justify-self-center {
+    justify-self: center;
+  }
+
+  .xl\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .xl\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .xl\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .xl\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .xl\:flex-none {
+    flex: none;
+  }
+
+  .xl\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .xl\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .xl\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .xl\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .xl\:order-1 {
+    order: 1;
+  }
+
+  .xl\:order-2 {
+    order: 2;
+  }
+
+  .xl\:order-3 {
+    order: 3;
+  }
+
+  .xl\:order-4 {
+    order: 4;
+  }
+
+  .xl\:order-5 {
+    order: 5;
+  }
+
+  .xl\:order-6 {
+    order: 6;
+  }
+
+  .xl\:order-7 {
+    order: 7;
+  }
+
+  .xl\:order-8 {
+    order: 8;
+  }
+
+  .xl\:order-9 {
+    order: 9;
+  }
+
+  .xl\:order-10 {
+    order: 10;
+  }
+
+  .xl\:order-11 {
+    order: 11;
+  }
+
+  .xl\:order-12 {
+    order: 12;
+  }
+
+  .xl\:order-first {
+    order: -9999;
+  }
+
+  .xl\:order-last {
+    order: 9999;
+  }
+
+  .xl\:order-none {
+    order: 0;
+  }
+
+  .xl\:float-right {
+    float: right;
+  }
+
+  .xl\:float-left {
+    float: left;
+  }
+
+  .xl\:float-none {
+    float: none;
+  }
+
+  .xl\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .xl\:clear-left {
+    clear: left;
+  }
+
+  .xl\:clear-right {
+    clear: right;
+  }
+
+  .xl\:clear-both {
+    clear: both;
+  }
+
+  .xl\:clear-none {
+    clear: none;
+  }
+
+  .xl\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .xl\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .xl\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .xl\:font-hairline {
+    font-weight: 100;
+  }
+
+  .xl\:font-thin {
+    font-weight: 200;
+  }
+
+  .xl\:font-light {
+    font-weight: 300;
+  }
+
+  .xl\:font-normal {
+    font-weight: 400;
+  }
+
+  .xl\:font-medium {
+    font-weight: 500;
+  }
+
+  .xl\:font-semibold {
+    font-weight: 600;
+  }
+
+  .xl\:font-bold {
+    font-weight: 700;
+  }
+
+  .xl\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .xl\:font-black {
+    font-weight: 900;
+  }
+
+  .xl\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .xl\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .xl\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .xl\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .xl\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .xl\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .xl\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .xl\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .xl\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .xl\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .xl\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .xl\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .xl\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .xl\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .xl\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .xl\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .xl\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .xl\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .xl\:h-0 {
+    height: 0;
+  }
+
+  .xl\:h-1 {
+    height: 0.25rem;
+  }
+
+  .xl\:h-2 {
+    height: 0.5rem;
+  }
+
+  .xl\:h-3 {
+    height: 0.75rem;
+  }
+
+  .xl\:h-4 {
+    height: 1rem;
+  }
+
+  .xl\:h-5 {
+    height: 1.25rem;
+  }
+
+  .xl\:h-6 {
+    height: 1.5rem;
+  }
+
+  .xl\:h-8 {
+    height: 2rem;
+  }
+
+  .xl\:h-10 {
+    height: 2.5rem;
+  }
+
+  .xl\:h-12 {
+    height: 3rem;
+  }
+
+  .xl\:h-16 {
+    height: 4rem;
+  }
+
+  .xl\:h-20 {
+    height: 5rem;
+  }
+
+  .xl\:h-24 {
+    height: 6rem;
+  }
+
+  .xl\:h-32 {
+    height: 8rem;
+  }
+
+  .xl\:h-40 {
+    height: 10rem;
+  }
+
+  .xl\:h-48 {
+    height: 12rem;
+  }
+
+  .xl\:h-56 {
+    height: 14rem;
+  }
+
+  .xl\:h-64 {
+    height: 16rem;
+  }
+
+  .xl\:h-auto {
+    height: auto;
+  }
+
+  .xl\:h-px {
+    height: 1px;
+  }
+
+  .xl\:h-full {
+    height: 100%;
+  }
+
+  .xl\:h-screen {
+    height: 100vh;
+  }
+
+  .xl\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .xl\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .xl\:text-base {
+    font-size: 1rem;
+  }
+
+  .xl\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .xl\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .xl\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .xl\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .xl\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .xl\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .xl\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .xl\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .xl\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .xl\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .xl\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .xl\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .xl\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .xl\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .xl\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .xl\:leading-none {
+    line-height: 1;
+  }
+
+  .xl\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .xl\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .xl\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .xl\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .xl\:leading-loose {
+    line-height: 2;
+  }
+
+  .xl\:list-inside {
+    list-style-position: inside;
+  }
+
+  .xl\:list-outside {
+    list-style-position: outside;
+  }
+
+  .xl\:list-none {
+    list-style-type: none;
+  }
+
+  .xl\:list-disc {
+    list-style-type: disc;
+  }
+
+  .xl\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .xl\:m-0 {
+    margin: 0;
+  }
+
+  .xl\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .xl\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .xl\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .xl\:m-4 {
+    margin: 1rem;
+  }
+
+  .xl\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .xl\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .xl\:m-8 {
+    margin: 2rem;
+  }
+
+  .xl\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .xl\:m-12 {
+    margin: 3rem;
+  }
+
+  .xl\:m-16 {
+    margin: 4rem;
+  }
+
+  .xl\:m-20 {
+    margin: 5rem;
+  }
+
+  .xl\:m-24 {
+    margin: 6rem;
+  }
+
+  .xl\:m-32 {
+    margin: 8rem;
+  }
+
+  .xl\:m-40 {
+    margin: 10rem;
+  }
+
+  .xl\:m-48 {
+    margin: 12rem;
+  }
+
+  .xl\:m-56 {
+    margin: 14rem;
+  }
+
+  .xl\:m-64 {
+    margin: 16rem;
+  }
+
+  .xl\:m-auto {
+    margin: auto;
+  }
+
+  .xl\:m-px {
+    margin: 1px;
+  }
+
+  .xl\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .xl\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .xl\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .xl\:-m-4 {
+    margin: -1rem;
+  }
+
+  .xl\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .xl\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .xl\:-m-8 {
+    margin: -2rem;
+  }
+
+  .xl\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .xl\:-m-12 {
+    margin: -3rem;
+  }
+
+  .xl\:-m-16 {
+    margin: -4rem;
+  }
+
+  .xl\:-m-20 {
+    margin: -5rem;
+  }
+
+  .xl\:-m-24 {
+    margin: -6rem;
+  }
+
+  .xl\:-m-32 {
+    margin: -8rem;
+  }
+
+  .xl\:-m-40 {
+    margin: -10rem;
+  }
+
+  .xl\:-m-48 {
+    margin: -12rem;
+  }
+
+  .xl\:-m-56 {
+    margin: -14rem;
+  }
+
+  .xl\:-m-64 {
+    margin: -16rem;
+  }
+
+  .xl\:-m-px {
+    margin: -1px;
+  }
+
+  .xl\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .xl\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .xl\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .xl\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .xl\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .xl\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .xl\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .xl\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .xl\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .xl\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .xl\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .xl\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .xl\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .xl\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .xl\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .xl\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .xl\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .xl\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .xl\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .xl\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .xl\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .xl\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .xl\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .xl\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .xl\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .xl\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .xl\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .xl\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .xl\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .xl\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .xl\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .xl\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .xl\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .xl\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .xl\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .xl\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .xl\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .xl\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .xl\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .xl\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .xl\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .xl\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .xl\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .xl\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .xl\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .xl\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .xl\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .xl\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .xl\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .xl\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .xl\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .xl\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .xl\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .xl\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .xl\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .xl\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .xl\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .xl\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .xl\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .xl\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .xl\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .xl\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .xl\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .xl\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .xl\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .xl\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .xl\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .xl\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .xl\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .xl\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .xl\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .xl\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .xl\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .xl\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .xl\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .xl\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .xl\:mt-0 {
+    margin-top: 0;
+  }
+
+  .xl\:mr-0 {
+    margin-right: 0;
+  }
+
+  .xl\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .xl\:ml-0 {
+    margin-left: 0;
+  }
+
+  .xl\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .xl\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .xl\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .xl\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .xl\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .xl\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .xl\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .xl\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .xl\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .xl\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .xl\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .xl\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .xl\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .xl\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .xl\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .xl\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .xl\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .xl\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .xl\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .xl\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .xl\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .xl\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .xl\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .xl\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .xl\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .xl\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .xl\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .xl\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .xl\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .xl\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .xl\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .xl\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .xl\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .xl\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .xl\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .xl\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .xl\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .xl\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .xl\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .xl\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .xl\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .xl\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .xl\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .xl\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .xl\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .xl\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .xl\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .xl\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .xl\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .xl\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .xl\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .xl\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .xl\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .xl\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .xl\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .xl\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .xl\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .xl\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .xl\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .xl\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .xl\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .xl\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .xl\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .xl\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .xl\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .xl\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .xl\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .xl\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .xl\:mt-auto {
+    margin-top: auto;
+  }
+
+  .xl\:mr-auto {
+    margin-right: auto;
+  }
+
+  .xl\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .xl\:ml-auto {
+    margin-left: auto;
+  }
+
+  .xl\:mt-px {
+    margin-top: 1px;
+  }
+
+  .xl\:mr-px {
+    margin-right: 1px;
+  }
+
+  .xl\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .xl\:ml-px {
+    margin-left: 1px;
+  }
+
+  .xl\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .xl\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .xl\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .xl\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .xl\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .xl\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .xl\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .xl\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .xl\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .xl\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .xl\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .xl\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .xl\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .xl\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .xl\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .xl\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .xl\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .xl\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .xl\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .xl\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .xl\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .xl\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .xl\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .xl\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .xl\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .xl\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .xl\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .xl\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .xl\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .xl\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .xl\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .xl\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .xl\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .xl\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .xl\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .xl\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .xl\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .xl\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .xl\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .xl\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .xl\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .xl\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .xl\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .xl\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .xl\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .xl\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .xl\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .xl\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .xl\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .xl\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .xl\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .xl\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .xl\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .xl\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .xl\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .xl\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .xl\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .xl\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .xl\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .xl\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .xl\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .xl\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .xl\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .xl\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .xl\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .xl\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .xl\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .xl\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .xl\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .xl\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .xl\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .xl\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .xl\:max-h-full {
+    max-height: 100%;
+  }
+
+  .xl\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .xl\:max-w-none {
+    max-width: none;
+  }
+
+  .xl\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .xl\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .xl\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .xl\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .xl\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .xl\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .xl\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .xl\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .xl\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .xl\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .xl\:max-w-full {
+    max-width: 100%;
+  }
+
+  .xl\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .xl\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .xl\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .xl\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .xl\:min-h-0 {
+    min-height: 0;
+  }
+
+  .xl\:min-h-full {
+    min-height: 100%;
+  }
+
+  .xl\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .xl\:min-w-0 {
+    min-width: 0;
+  }
+
+  .xl\:min-w-full {
+    min-width: 100%;
+  }
+
+  .xl\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .xl\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .xl\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .xl\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .xl\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .xl\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .xl\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .xl\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .xl\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .xl\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .xl\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .xl\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .xl\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .xl\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .xl\:opacity-0 {
+    opacity: 0;
+  }
+
+  .xl\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .xl\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .xl\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .xl\:opacity-100 {
+    opacity: 1;
+  }
+
+  .xl\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .xl\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .xl\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .xl\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .xl\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .xl\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .xl\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .xl\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .xl\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .xl\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .xl\:outline-none {
+    outline: 0;
+  }
+
+  .xl\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .xl\:overflow-auto {
+    overflow: auto;
+  }
+
+  .xl\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .xl\:overflow-visible {
+    overflow: visible;
+  }
+
+  .xl\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .xl\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .xl\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .xl\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .xl\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .xl\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .xl\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .xl\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .xl\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .xl\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .xl\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .xl\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .xl\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .xl\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .xl\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .xl\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .xl\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .xl\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .xl\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .xl\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .xl\:p-0 {
+    padding: 0;
+  }
+
+  .xl\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .xl\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .xl\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .xl\:p-4 {
+    padding: 1rem;
+  }
+
+  .xl\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .xl\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .xl\:p-8 {
+    padding: 2rem;
+  }
+
+  .xl\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .xl\:p-12 {
+    padding: 3rem;
+  }
+
+  .xl\:p-16 {
+    padding: 4rem;
+  }
+
+  .xl\:p-20 {
+    padding: 5rem;
+  }
+
+  .xl\:p-24 {
+    padding: 6rem;
+  }
+
+  .xl\:p-32 {
+    padding: 8rem;
+  }
+
+  .xl\:p-40 {
+    padding: 10rem;
+  }
+
+  .xl\:p-48 {
+    padding: 12rem;
+  }
+
+  .xl\:p-56 {
+    padding: 14rem;
+  }
+
+  .xl\:p-64 {
+    padding: 16rem;
+  }
+
+  .xl\:p-px {
+    padding: 1px;
+  }
+
+  .xl\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .xl\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .xl\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .xl\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .xl\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .xl\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .xl\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .xl\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .xl\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .xl\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .xl\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .xl\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .xl\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .xl\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .xl\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .xl\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .xl\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .xl\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .xl\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .xl\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .xl\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .xl\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .xl\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .xl\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .xl\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .xl\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .xl\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .xl\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .xl\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .xl\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .xl\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .xl\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .xl\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .xl\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .xl\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .xl\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .xl\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .xl\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .xl\:pt-0 {
+    padding-top: 0;
+  }
+
+  .xl\:pr-0 {
+    padding-right: 0;
+  }
+
+  .xl\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .xl\:pl-0 {
+    padding-left: 0;
+  }
+
+  .xl\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .xl\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .xl\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .xl\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .xl\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .xl\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .xl\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .xl\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .xl\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .xl\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .xl\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .xl\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .xl\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .xl\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .xl\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .xl\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .xl\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .xl\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .xl\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .xl\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .xl\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .xl\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .xl\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .xl\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .xl\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .xl\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .xl\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .xl\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .xl\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .xl\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .xl\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .xl\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .xl\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .xl\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .xl\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .xl\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .xl\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .xl\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .xl\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .xl\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .xl\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .xl\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .xl\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .xl\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .xl\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .xl\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .xl\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .xl\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .xl\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .xl\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .xl\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .xl\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .xl\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .xl\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .xl\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .xl\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .xl\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .xl\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .xl\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .xl\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .xl\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .xl\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .xl\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .xl\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .xl\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .xl\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .xl\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .xl\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .xl\:pt-px {
+    padding-top: 1px;
+  }
+
+  .xl\:pr-px {
+    padding-right: 1px;
+  }
+
+  .xl\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .xl\:pl-px {
+    padding-left: 1px;
+  }
+
+  .xl\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .xl\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .xl\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .xl\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .xl\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .xl\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .xl\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .xl\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .xl\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .xl\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .xl\:static {
+    position: static;
+  }
+
+  .xl\:fixed {
+    position: fixed;
+  }
+
+  .xl\:absolute {
+    position: absolute;
+  }
+
+  .xl\:relative {
+    position: relative;
+  }
+
+  .xl\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .xl\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .xl\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .xl\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .xl\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .xl\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .xl\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .xl\:top-0 {
+    top: 0;
+  }
+
+  .xl\:right-0 {
+    right: 0;
+  }
+
+  .xl\:bottom-0 {
+    bottom: 0;
+  }
+
+  .xl\:left-0 {
+    left: 0;
+  }
+
+  .xl\:top-auto {
+    top: auto;
+  }
+
+  .xl\:right-auto {
+    right: auto;
+  }
+
+  .xl\:bottom-auto {
+    bottom: auto;
+  }
+
+  .xl\:left-auto {
+    left: auto;
+  }
+
+  .xl\:resize-none {
+    resize: none;
+  }
+
+  .xl\:resize-y {
+    resize: vertical;
+  }
+
+  .xl\:resize-x {
+    resize: horizontal;
+  }
+
+  .xl\:resize {
+    resize: both;
+  }
+
+  .xl\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .xl\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .xl\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .xl\:shadow-none {
+    box-shadow: none;
+  }
+
+  .xl\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .xl\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .xl\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .xl\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .xl\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .xl\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .xl\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .xl\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .xl\:fill-current {
+    fill: currentColor;
+  }
+
+  .xl\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .xl\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .xl\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .xl\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .xl\:table-auto {
+    table-layout: auto;
+  }
+
+  .xl\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .xl\:text-left {
+    text-align: left;
+  }
+
+  .xl\:text-center {
+    text-align: center;
+  }
+
+  .xl\:text-right {
+    text-align: right;
+  }
+
+  .xl\:text-justify {
+    text-align: justify;
+  }
+
+  .xl\:text-transparent {
+    color: transparent;
+  }
+
+  .xl\:text-current {
+    color: currentColor;
+  }
+
+  .xl\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .xl\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .xl\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .xl\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .xl\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .xl\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .xl\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .xl\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .xl\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .xl\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .xl\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .xl\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .xl\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .xl\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .xl\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .xl\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .xl\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .xl\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .xl\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .xl\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .xl\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .xl\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .xl\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .xl\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .xl\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .xl\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .xl\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .xl\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .xl\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .xl\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .xl\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .xl\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .xl\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .xl\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .xl\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .xl\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .xl\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .xl\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .xl\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .xl\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .xl\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .xl\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .xl\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .xl\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .xl\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .xl\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .xl\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .xl\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .xl\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .xl\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .xl\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .xl\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .xl\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .xl\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .xl\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .xl\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .xl\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .xl\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .xl\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .xl\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .xl\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .xl\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .xl\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .xl\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .xl\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .xl\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .xl\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .xl\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .xl\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .xl\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .xl\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .xl\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .xl\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .xl\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .xl\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .xl\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .xl\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .xl\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .xl\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .xl\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .xl\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .xl\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .xl\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .xl\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .xl\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .xl\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .xl\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .xl\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .xl\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .xl\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .xl\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .xl\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .xl\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .xl\:italic {
+    font-style: italic;
+  }
+
+  .xl\:not-italic {
+    font-style: normal;
+  }
+
+  .xl\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .xl\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .xl\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .xl\:normal-case {
+    text-transform: none;
+  }
+
+  .xl\:underline {
+    text-decoration: underline;
+  }
+
+  .xl\:line-through {
+    text-decoration: line-through;
+  }
+
+  .xl\:no-underline {
+    text-decoration: none;
+  }
+
+  .xl\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .xl\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .xl\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .xl\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .xl\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .xl\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .xl\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .xl\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .xl\:ordinal, .xl\:slashed-zero, .xl\:lining-nums, .xl\:oldstyle-nums, .xl\:proportional-nums, .xl\:tabular-nums, .xl\:diagonal-fractions, .xl\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .xl\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .xl\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .xl\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .xl\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .xl\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .xl\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .xl\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .xl\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .xl\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .xl\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .xl\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .xl\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .xl\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .xl\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .xl\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .xl\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .xl\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .xl\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .xl\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .xl\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .xl\:align-top {
+    vertical-align: top;
+  }
+
+  .xl\:align-middle {
+    vertical-align: middle;
+  }
+
+  .xl\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .xl\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .xl\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .xl\:visible {
+    visibility: visible;
+  }
+
+  .xl\:invisible {
+    visibility: hidden;
+  }
+
+  .xl\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .xl\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .xl\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .xl\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .xl\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .xl\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .xl\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .xl\:break-all {
+    word-break: break-all;
+  }
+
+  .xl\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .xl\:w-0 {
+    width: 0;
+  }
+
+  .xl\:w-1 {
+    width: 0.25rem;
+  }
+
+  .xl\:w-2 {
+    width: 0.5rem;
+  }
+
+  .xl\:w-3 {
+    width: 0.75rem;
+  }
+
+  .xl\:w-4 {
+    width: 1rem;
+  }
+
+  .xl\:w-5 {
+    width: 1.25rem;
+  }
+
+  .xl\:w-6 {
+    width: 1.5rem;
+  }
+
+  .xl\:w-8 {
+    width: 2rem;
+  }
+
+  .xl\:w-10 {
+    width: 2.5rem;
+  }
+
+  .xl\:w-12 {
+    width: 3rem;
+  }
+
+  .xl\:w-16 {
+    width: 4rem;
+  }
+
+  .xl\:w-20 {
+    width: 5rem;
+  }
+
+  .xl\:w-24 {
+    width: 6rem;
+  }
+
+  .xl\:w-32 {
+    width: 8rem;
+  }
+
+  .xl\:w-40 {
+    width: 10rem;
+  }
+
+  .xl\:w-48 {
+    width: 12rem;
+  }
+
+  .xl\:w-56 {
+    width: 14rem;
+  }
+
+  .xl\:w-64 {
+    width: 16rem;
+  }
+
+  .xl\:w-auto {
+    width: auto;
+  }
+
+  .xl\:w-px {
+    width: 1px;
+  }
+
+  .xl\:w-1\/2 {
+    width: 50%;
+  }
+
+  .xl\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .xl\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .xl\:w-1\/4 {
+    width: 25%;
+  }
+
+  .xl\:w-2\/4 {
+    width: 50%;
+  }
+
+  .xl\:w-3\/4 {
+    width: 75%;
+  }
+
+  .xl\:w-1\/5 {
+    width: 20%;
+  }
+
+  .xl\:w-2\/5 {
+    width: 40%;
+  }
+
+  .xl\:w-3\/5 {
+    width: 60%;
+  }
+
+  .xl\:w-4\/5 {
+    width: 80%;
+  }
+
+  .xl\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .xl\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .xl\:w-3\/6 {
+    width: 50%;
+  }
+
+  .xl\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .xl\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .xl\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .xl\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .xl\:w-3\/12 {
+    width: 25%;
+  }
+
+  .xl\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .xl\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .xl\:w-6\/12 {
+    width: 50%;
+  }
+
+  .xl\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .xl\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .xl\:w-9\/12 {
+    width: 75%;
+  }
+
+  .xl\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .xl\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .xl\:w-full {
+    width: 100%;
+  }
+
+  .xl\:w-screen {
+    width: 100vw;
+  }
+
+  .xl\:z-0 {
+    z-index: 0;
+  }
+
+  .xl\:z-10 {
+    z-index: 10;
+  }
+
+  .xl\:z-20 {
+    z-index: 20;
+  }
+
+  .xl\:z-30 {
+    z-index: 30;
+  }
+
+  .xl\:z-40 {
+    z-index: 40;
+  }
+
+  .xl\:z-50 {
+    z-index: 50;
+  }
+
+  .xl\:z-auto {
+    z-index: auto;
+  }
+
+  .xl\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .xl\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .xl\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .xl\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .xl\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .xl\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .xl\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .xl\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .xl\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .xl\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .xl\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .xl\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .xl\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .xl\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .xl\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .xl\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .xl\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .xl\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .xl\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .xl\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .xl\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .xl\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .xl\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .xl\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .xl\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .xl\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .xl\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .xl\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .xl\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .xl\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .xl\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .xl\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .xl\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .xl\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .xl\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .xl\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .xl\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .xl\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .xl\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .xl\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .xl\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .xl\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .xl\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .xl\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .xl\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .xl\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .xl\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .xl\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .xl\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .xl\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .xl\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .xl\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .xl\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .xl\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .xl\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .xl\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .xl\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .xl\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .xl\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .xl\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .xl\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .xl\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .xl\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .xl\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .xl\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .xl\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .xl\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .xl\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .xl\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .xl\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .xl\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .xl\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .xl\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .xl\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .xl\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .xl\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .xl\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .xl\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .xl\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .xl\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .xl\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .xl\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .xl\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .xl\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .xl\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .xl\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .xl\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .xl\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .xl\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .xl\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .xl\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .xl\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .xl\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .xl\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .xl\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .xl\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .xl\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .xl\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .xl\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .xl\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .xl\:col-auto {
+    grid-column: auto;
+  }
+
+  .xl\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .xl\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .xl\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .xl\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .xl\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .xl\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .xl\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .xl\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .xl\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .xl\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .xl\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .xl\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .xl\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .xl\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .xl\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .xl\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .xl\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .xl\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .xl\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .xl\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .xl\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .xl\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .xl\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .xl\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .xl\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .xl\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .xl\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .xl\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .xl\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .xl\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .xl\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .xl\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .xl\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .xl\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .xl\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .xl\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .xl\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .xl\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .xl\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .xl\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .xl\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .xl\:row-auto {
+    grid-row: auto;
+  }
+
+  .xl\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .xl\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .xl\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .xl\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .xl\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .xl\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .xl\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .xl\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .xl\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .xl\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .xl\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .xl\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .xl\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .xl\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .xl\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .xl\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .xl\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .xl\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .xl\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .xl\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .xl\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .xl\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .xl\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .xl\:transform-none {
+    transform: none;
+  }
+
+  .xl\:origin-center {
+    transform-origin: center;
+  }
+
+  .xl\:origin-top {
+    transform-origin: top;
+  }
+
+  .xl\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .xl\:origin-right {
+    transform-origin: right;
+  }
+
+  .xl\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .xl\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .xl\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .xl\:origin-left {
+    transform-origin: left;
+  }
+
+  .xl\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .xl\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .xl\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .xl\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .xl\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .xl\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .xl\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .xl\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .xl\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .xl\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .xl\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .xl\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .xl\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .xl\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .xl\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .xl\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .xl\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .xl\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .xl\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .xl\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .xl\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .xl\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .xl\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .xl\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .xl\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .xl\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .xl\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .xl\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .xl\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .xl\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .xl\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .xl\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .xl\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .xl\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .xl\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .xl\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .xl\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .xl\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .xl\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .xl\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .xl\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .xl\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .xl\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .xl\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .xl\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .xl\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .xl\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .xl\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .xl\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .xl\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .xl\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .xl\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .xl\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .xl\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .xl\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .xl\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .xl\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .xl\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .xl\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .xl\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .xl\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .xl\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .xl\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .xl\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .xl\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .xl\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .xl\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .xl\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .xl\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .xl\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .xl\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .xl\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .xl\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .xl\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .xl\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .xl\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .xl\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .xl\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .xl\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .xl\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .xl\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .xl\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .xl\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .xl\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .xl\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .xl\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .xl\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .xl\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .xl\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .xl\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .xl\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .xl\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .xl\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .xl\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .xl\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .xl\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .xl\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .xl\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .xl\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .xl\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .xl\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .xl\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .xl\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .xl\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .xl\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .xl\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .xl\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .xl\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .xl\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .xl\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .xl\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .xl\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .xl\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .xl\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .xl\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .xl\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .xl\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .xl\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .xl\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .xl\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .xl\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .xl\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .xl\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .xl\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .xl\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .xl\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .xl\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .xl\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .xl\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .xl\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .xl\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .xl\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .xl\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .xl\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .xl\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .xl\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .xl\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .xl\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .xl\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .xl\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .xl\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .xl\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .xl\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .xl\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .xl\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .xl\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .xl\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .xl\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .xl\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .xl\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .xl\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .xl\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .xl\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .xl\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .xl\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .xl\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .xl\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .xl\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .xl\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .xl\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .xl\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .xl\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .xl\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .xl\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .xl\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .xl\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .xl\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .xl\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .xl\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .xl\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .xl\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .xl\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .xl\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .xl\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .xl\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .xl\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .xl\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .xl\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .xl\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .xl\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .xl\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .xl\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .xl\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .xl\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .xl\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .xl\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .xl\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .xl\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .xl\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .xl\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .xl\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .xl\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .xl\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .xl\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .xl\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .xl\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .xl\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .xl\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .xl\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .xl\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .xl\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .xl\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .xl\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .xl\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .xl\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .xl\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .xl\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .xl\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .xl\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .xl\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .xl\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .xl\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .xl\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .xl\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .xl\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .xl\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .xl\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .xl\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .xl\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .xl\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .xl\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .xl\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .xl\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .xl\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .xl\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .xl\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .xl\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .xl\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .xl\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .xl\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .xl\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .xl\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .xl\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .xl\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .xl\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .xl\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .xl\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .xl\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .xl\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .xl\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .xl\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .xl\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .xl\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .xl\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .xl\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .xl\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .xl\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .xl\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .xl\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .xl\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .xl\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .xl\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .xl\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .xl\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .xl\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .xl\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .xl\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .xl\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .xl\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .xl\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .xl\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .xl\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .xl\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .xl\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .xl\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .xl\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .xl\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .xl\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .xl\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .xl\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .xl\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .xl\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .xl\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .xl\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .xl\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .xl\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .xl\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .xl\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .xl\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .xl\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .xl\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .xl\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .xl\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .xl\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .xl\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .xl\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .xl\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .xl\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .xl\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .xl\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .xl\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .xl\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .xl\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .xl\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .xl\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .xl\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .xl\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .xl\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .xl\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .xl\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .xl\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .xl\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .xl\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .xl\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .xl\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .xl\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .xl\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .xl\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .xl\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .xl\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .xl\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .xl\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .xl\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .xl\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .xl\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .xl\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .xl\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .xl\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .xl\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .xl\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .xl\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .xl\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .xl\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .xl\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .xl\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .xl\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .xl\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .xl\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .xl\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .xl\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .xl\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .xl\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .xl\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .xl\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .xl\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .xl\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .xl\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .xl\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .xl\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .xl\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .xl\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .xl\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .xl\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .xl\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .xl\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .xl\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .xl\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .xl\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .xl\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .xl\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .xl\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .xl\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .xl\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .xl\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .xl\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .xl\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .xl\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .xl\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .xl\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .xl\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .xl\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .xl\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .xl\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .xl\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .xl\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .xl\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .xl\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .xl\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .xl\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .xl\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .xl\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .xl\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .xl\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .xl\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .xl\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .xl\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .xl\:transition-none {
+    transition-property: none;
+  }
+
+  .xl\:transition-all {
+    transition-property: all;
+  }
+
+  .xl\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .xl\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .xl\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .xl\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .xl\:transition-transform {
+    transition-property: transform;
+  }
+
+  .xl\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .xl\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .xl\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .xl\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .xl\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .xl\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .xl\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .xl\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .xl\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .xl\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .xl\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .xl\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .xl\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .xl\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .xl\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .xl\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .xl\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .xl\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .xl\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .xl\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .xl\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .xl\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .xl\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .xl\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .xl\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
diff --git a/users/wpcarro/website/habit-screens/registry.dat b/users/wpcarro/website/habit-screens/registry.dat
new file mode 100644
index 0000000000..d2671b2cf1
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/registry.dat
Binary files differdiff --git a/users/wpcarro/website/habit-screens/shell.nix b/users/wpcarro/website/habit-screens/shell.nix
new file mode 100644
index 0000000000..afcc0f4d36
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs.elmPackages; [
+    elm
+    elm-format
+    elm-live
+  ];
+}
diff --git a/users/wpcarro/website/habit-screens/src/Habits.elm b/users/wpcarro/website/habit-screens/src/Habits.elm
new file mode 100644
index 0000000000..691adc9394
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/src/Habits.elm
@@ -0,0 +1,463 @@
+module Habits exposing (render)
+
+import Browser
+import Date exposing (Date)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Set exposing (Set)
+import State exposing (HabitType(..))
+import Time exposing (Weekday(..))
+import UI
+import Utils exposing (Strategy(..))
+
+
+morning : List State.Habit
+morning =
+    List.map
+        (\( duration, x ) ->
+            { label = x
+            , habitType = State.Morning
+            , minutesDuration = duration
+            }
+        )
+        [ ( 1, "Make bed" )
+        , ( 2, "Brush teeth" )
+        , ( 30, "Run (15 minutes)" )
+        , ( 10, "Shower" )
+        , ( 10, "Meditate" )
+        ]
+
+
+evening : List State.Habit
+evening =
+    List.map
+        (\( duration, x ) ->
+            { label = x
+            , habitType = State.Evening
+            , minutesDuration = duration
+            }
+        )
+        [ ( 30, "Read" )
+        , ( 1, "Record in habit Journal" )
+        ]
+
+
+monday : List ( Int, String )
+monday =
+    [ ( 90, "Bikram Yoga @ 17:00" )
+    ]
+
+
+tuesday : List ( Int, String )
+tuesday =
+    [ ( 90, "Bikram Yoga @ 18:00" )
+    ]
+
+
+wednesday : List ( Int, String )
+wednesday =
+    [ ( 5, "Shave" )
+    , ( 90, "Bikram Yoga @ 17:00" )
+    ]
+
+
+thursday : List ( Int, String )
+thursday =
+    []
+
+
+friday : List ( Int, String )
+friday =
+    [ ( 60, "Bikram Yoga @ 17:00" )
+    , ( 3, "Take-out trash" )
+    , ( 60, "Shop for groceries" )
+    ]
+
+
+saturday : List ( Int, String )
+saturday =
+    [ ( 60, "Warm Yin Yoga @ 15:00" )
+    ]
+
+
+sunday : List ( Int, String )
+sunday =
+    [ ( 1, "Shampoo" )
+    , ( 5, "Shave" )
+    , ( 1, "Trim nails" )
+    , ( 1, "Combine trash cans" )
+    , ( 10, "Mop tile and wood floors" )
+    , ( 10, "Laundry" )
+    , ( 5, "Vacuum bedroom" )
+    , ( 5, "Clean desk" )
+    ]
+
+
+payday : List State.Habit
+payday =
+    List.map
+        (\( duration, x ) ->
+            { label = x
+            , habitType = State.Payday
+            , minutesDuration = duration
+            }
+        )
+        [ ( 1, "Ensure \"Emergency\" fund has a balance of 1000 GBP" )
+        , ( 1, "Open \"finances_2020\" Google Sheet" )
+        , ( 1, "Settle up with Mimi on TransferWise" )
+        , ( 1, "Adjust GBP:USD exchange rate" )
+        , ( 1, "Adjust \"Stocks (after tax)\" to reflect amount Google sent" )
+        , ( 1, "Add remaining cash to \"Carryover (cash)\"" )
+        , ( 1, "Adjust \"Paycheck\" to reflect amount Google sent" )
+        , ( 5, "In the \"International Xfer\" table, send \"Xfer amount\" from Monzo to USAA" )
+        , ( 10, "Go to an ATM and extract the amount in \"ATM withdrawal\"" )
+        , ( 0, "Await the TransferWise transaction to complete and pay MyFedLoan in USD" )
+        ]
+
+
+firstOfTheMonth : List State.Habit
+firstOfTheMonth =
+    List.map
+        (\( duration, x ) ->
+            { label = x
+            , habitType = State.FirstOfTheMonth
+            , minutesDuration = duration
+            }
+        )
+        [ ( 10, "Create habit template in journal" )
+        , ( 30, "Assess previous month's performance" )
+        , ( 5, "Register for Bikram Yoga classes" )
+        ]
+
+
+firstOfTheYear : List State.Habit
+firstOfTheYear =
+    List.map
+        (\( duration, x ) ->
+            { label = x
+            , habitType = State.FirstOfTheYear
+            , minutesDuration = duration
+            }
+        )
+        [ ( 60, "Write a post mortem for the previous year" )
+        ]
+
+
+habitTypes :
+    { includeMorning : Bool
+    , includeEvening : Bool
+    , date : Date
+    }
+    -> List State.HabitType
+habitTypes { includeMorning, includeEvening, date } =
+    let
+        habitTypePredicates : List ( State.HabitType, Date -> Bool )
+        habitTypePredicates =
+            [ ( Morning, \_ -> includeMorning )
+            , ( DayOfWeek, \_ -> True )
+            , ( Payday, \x -> Date.day x == 25 )
+            , ( FirstOfTheMonth, \x -> Date.day x == 1 )
+            , ( FirstOfTheYear, \x -> Date.day x == 1 && Date.monthNumber x == 1 )
+            , ( Evening, \_ -> includeEvening )
+            ]
+    in
+    habitTypePredicates
+        |> List.filter (\( _, predicate ) -> predicate date)
+        |> List.map (\( habitType, _ ) -> habitType)
+
+
+habitsFor : State.HabitType -> Weekday -> List State.Habit
+habitsFor habitType weekday =
+    case habitType of
+        Morning ->
+            morning
+
+        Evening ->
+            evening
+
+        DayOfWeek ->
+            let
+                toHabit : List ( Int, String ) -> List State.Habit
+                toHabit =
+                    List.map
+                        (\( duration, x ) ->
+                            { label = x
+                            , habitType = State.DayOfWeek
+                            , minutesDuration = duration
+                            }
+                        )
+            in
+            case weekday of
+                Mon ->
+                    toHabit monday
+
+                Tue ->
+                    toHabit tuesday
+
+                Wed ->
+                    toHabit wednesday
+
+                Thu ->
+                    toHabit thursday
+
+                Fri ->
+                    toHabit friday
+
+                Sat ->
+                    toHabit saturday
+
+                Sun ->
+                    toHabit sunday
+
+        Payday ->
+            payday
+
+        FirstOfTheMonth ->
+            firstOfTheMonth
+
+        FirstOfTheYear ->
+            firstOfTheYear
+
+
+weekdayLabelFor : Weekday -> State.WeekdayLabel
+weekdayLabelFor weekday =
+    case weekday of
+        Mon ->
+            "Monday"
+
+        Tue ->
+            "Tuesday"
+
+        Wed ->
+            "Wednesday"
+
+        Thu ->
+            "Thursday"
+
+        Fri ->
+            "Friday"
+
+        Sat ->
+            "Saturday"
+
+        Sun ->
+            "Sunday"
+
+
+timeRemaining : State.WeekdayLabel -> State.CompletedHabits -> List State.Habit -> Int
+timeRemaining weekdayLabel completed habits =
+    habits
+        |> List.indexedMap
+            (\i { label, minutesDuration } ->
+                if Set.member ( weekdayLabel, label ) completed then
+                    0
+
+                else
+                    minutesDuration
+            )
+        |> List.sum
+
+
+render : State.Model -> Html State.Msg
+render { today, visibleDayOfWeek, completed, includeMorning, includeEvening } =
+    case ( today, visibleDayOfWeek ) of
+        ( Just todaysDate, Just visibleWeekday ) ->
+            let
+                todaysWeekday : Weekday
+                todaysWeekday =
+                    Date.weekday todaysDate
+
+                habits : List State.Habit
+                habits =
+                    habitTypes
+                        { includeMorning = includeMorning
+                        , includeEvening = includeEvening
+                        , date = todaysDate
+                        }
+                        |> List.map (\habitType -> habitsFor habitType todaysWeekday)
+                        |> List.concat
+            in
+            div
+                [ Utils.class
+                    [ Always "max-w-xl mx-auto py-6 px-6"
+                    , When (todaysWeekday /= visibleWeekday) "pt-20"
+                    ]
+                ]
+                [ header []
+                    [ if todaysWeekday /= visibleWeekday then
+                        div [ class "text-center w-full bg-blue-600 text-white fixed top-0 left-0 px-3 py-4" ]
+                            [ p [ class "py-2 inline pr-5" ]
+                                [ text "As you are not viewing today's habits, the UI is in read-only mode" ]
+                            , UI.button
+                                [ class "bg-blue-200 px-4 py-2 rounded text-blue-600 text-xs font-bold"
+                                , onClick State.ViewToday
+                                ]
+                                [ text "View Today's Habits" ]
+                            ]
+
+                      else
+                        text ""
+                    , div [ class "flex center" ]
+                        [ UI.button
+                            [ class "w-1/4 text-gray-500"
+                            , onClick State.ViewPrevious
+                            ]
+                            [ text "‹ previous" ]
+                        , h1 [ class "font-bold text-blue-500 text-3xl text-center w-full" ]
+                            [ text (weekdayLabelFor visibleWeekday) ]
+                        , UI.button
+                            [ class "w-1/4 text-gray-500"
+                            , onClick State.ViewNext
+                            ]
+                            [ text "next ›" ]
+                        ]
+                    ]
+                , if todaysWeekday == visibleWeekday then
+                    p [ class "text-center pt-1 pb-4" ]
+                        [ let
+                            t : Int
+                            t =
+                                timeRemaining (weekdayLabelFor todaysWeekday) completed habits
+                          in
+                          if t == 0 then
+                            text "Nothing to do!"
+
+                          else
+                            text
+                                ((habits
+                                    |> timeRemaining (weekdayLabelFor todaysWeekday) completed
+                                    |> String.fromInt
+                                 )
+                                    ++ " minutes remaining"
+                                )
+                        ]
+
+                  else
+                    text ""
+                , if todaysWeekday == visibleWeekday then
+                    div []
+                        [ UI.button
+                            [ onClick
+                                (if Set.size completed == 0 then
+                                    State.DoNothing
+
+                                 else
+                                    State.ClearAll
+                                )
+                            , Utils.class
+                                [ Always "ml-10 px-3"
+                                , If (Set.size completed == 0)
+                                    "text-gray-500 cursor-not-allowed"
+                                    "text-red-500 underline cursor-pointer"
+                                ]
+                            ]
+                            [ let
+                                numCompleted : Int
+                                numCompleted =
+                                    habits
+                                        |> List.indexedMap (\i { label } -> ( i, label ))
+                                        |> List.filter
+                                            (\( i, label ) ->
+                                                Set.member
+                                                    ( weekdayLabelFor todaysWeekday, label )
+                                                    completed
+                                            )
+                                        |> List.length
+                              in
+                              if numCompleted == 0 then
+                                text "Clear"
+
+                              else
+                                text ("Clear (" ++ String.fromInt numCompleted ++ ")")
+                            ]
+                        , UI.button
+                            [ onClick State.ToggleMorning
+                            , Utils.class
+                                [ Always "px-3 underline"
+                                , If includeMorning
+                                    "text-gray-600"
+                                    "text-blue-600"
+                                ]
+                            ]
+                            [ text
+                                (if includeMorning then
+                                    "Hide Morning"
+
+                                 else
+                                    "Show Morning"
+                                )
+                            ]
+                        , UI.button
+                            [ Utils.class
+                                [ Always "px-3 underline"
+                                , If includeEvening
+                                    "text-gray-600"
+                                    "text-blue-600"
+                                ]
+                            , onClick State.ToggleEvening
+                            ]
+                            [ text
+                                (if includeEvening then
+                                    "Hide Evening"
+
+                                 else
+                                    "Show Evening"
+                                )
+                            ]
+                        ]
+
+                  else
+                    text ""
+                , ul [ class "pb-10" ]
+                    (habits
+                        |> List.indexedMap
+                            (\i { label, minutesDuration } ->
+                                let
+                                    isCompleted : Bool
+                                    isCompleted =
+                                        Set.member ( weekdayLabelFor todaysWeekday, label ) completed
+                                in
+                                li [ class "text-xl list-disc ml-6" ]
+                                    [ if todaysWeekday == visibleWeekday then
+                                        UI.button
+                                            [ class "py-5 px-3"
+                                            , onClick
+                                                (State.ToggleHabit
+                                                    (weekdayLabelFor todaysWeekday)
+                                                    label
+                                                )
+                                            ]
+                                            [ span
+                                                [ Utils.class
+                                                    [ Always "text-white pt-1 px-2 rounded"
+                                                    , If isCompleted "bg-gray-400" "bg-blue-500"
+                                                    ]
+                                                ]
+                                                [ text (String.fromInt minutesDuration ++ " mins") ]
+                                            , p
+                                                [ Utils.class
+                                                    [ Always "inline pl-3"
+                                                    , When isCompleted "line-through text-gray-400"
+                                                    ]
+                                                ]
+                                                [ text label ]
+                                            ]
+
+                                      else
+                                        UI.button
+                                            [ class "py-5 px-3 cursor-not-allowed"
+                                            , onClick State.DoNothing
+                                            ]
+                                            [ text label ]
+                                    ]
+                            )
+                    )
+                , footer [ class "bg-white text-sm text-center text-gray-500 fixed bottom-0 left-0 w-full py-4" ]
+                    [ p [] [ text "This app is brought to you by William Carroll." ]
+                    , p [] [ text "Client: Elm; Server: n/a" ]
+                    ]
+                ]
+
+        ( _, _ ) ->
+            p [] [ text "Unable to display habits because we do not know what day of the week it is." ]
diff --git a/users/wpcarro/website/habit-screens/src/Main.elm b/users/wpcarro/website/habit-screens/src/Main.elm
new file mode 100644
index 0000000000..2ddedb9133
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/src/Main.elm
@@ -0,0 +1,29 @@
+module Main exposing (main)
+
+import Browser
+import Habits
+import Html exposing (..)
+import State
+import Time
+
+
+subscriptions : State.Model -> Sub State.Msg
+subscriptions model =
+    -- once per minute
+    Time.every (1000 * 60) (\_ -> State.MaybeAdjustWeekday)
+
+
+view : State.Model -> Html State.Msg
+view model =
+    case model.view of
+        State.Habits ->
+            Habits.render model
+
+
+main =
+    Browser.element
+        { init = \() -> State.init
+        , subscriptions = subscriptions
+        , update = State.update
+        , view = view
+        }
diff --git a/users/wpcarro/website/habit-screens/src/State.elm b/users/wpcarro/website/habit-screens/src/State.elm
new file mode 100644
index 0000000000..ea00a01351
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/src/State.elm
@@ -0,0 +1,195 @@
+module State exposing (..)
+
+import Date exposing (Date)
+import Set exposing (Set)
+import Task
+import Time exposing (Weekday(..))
+
+
+type alias WeekdayLabel =
+    String
+
+
+type alias HabitLabel =
+    String
+
+
+type Msg
+    = DoNothing
+    | SetView View
+    | ReceiveDate Date
+    | ToggleHabit WeekdayLabel HabitLabel
+    | MaybeAdjustWeekday
+    | ViewToday
+    | ViewPrevious
+    | ViewNext
+    | ClearAll
+    | ToggleMorning
+    | ToggleEvening
+
+
+type View
+    = Habits
+
+
+type HabitType
+    = Morning
+    | Evening
+    | DayOfWeek
+    | Payday
+    | FirstOfTheMonth
+    | FirstOfTheYear
+
+
+type alias Habit =
+    { label : HabitLabel
+    , habitType : HabitType
+    , minutesDuration : Int
+    }
+
+
+type alias CompletedHabits =
+    Set ( WeekdayLabel, HabitLabel )
+
+
+type alias Model =
+    { isLoading : Bool
+    , view : View
+    , today : Maybe Date
+    , completed : CompletedHabits
+    , visibleDayOfWeek : Maybe Weekday
+    , includeMorning : Bool
+    , includeEvening : Bool
+    }
+
+
+previousDay : Weekday -> Weekday
+previousDay weekday =
+    case weekday of
+        Mon ->
+            Sun
+
+        Tue ->
+            Mon
+
+        Wed ->
+            Tue
+
+        Thu ->
+            Wed
+
+        Fri ->
+            Thu
+
+        Sat ->
+            Fri
+
+        Sun ->
+            Sat
+
+
+nextDay : Weekday -> Weekday
+nextDay weekday =
+    case weekday of
+        Mon ->
+            Tue
+
+        Tue ->
+            Wed
+
+        Wed ->
+            Thu
+
+        Thu ->
+            Fri
+
+        Fri ->
+            Sat
+
+        Sat ->
+            Sun
+
+        Sun ->
+            Mon
+
+
+{-| The initial state for the application.
+-}
+init : ( Model, Cmd Msg )
+init =
+    ( { isLoading = False
+      , view = Habits
+      , today = Nothing
+      , completed = Set.empty
+      , visibleDayOfWeek = Nothing
+      , includeMorning = True
+      , includeEvening = True
+      }
+    , Date.today |> Task.perform ReceiveDate
+    )
+
+
+{-| Now that we have state, we need a function to change the state.
+-}
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg ({ today, visibleDayOfWeek, completed } as model) =
+    case msg of
+        DoNothing ->
+            ( model, Cmd.none )
+
+        SetView x ->
+            ( { model
+                | view = x
+                , isLoading = True
+              }
+            , Cmd.none
+            )
+
+        ReceiveDate x ->
+            ( { model
+                | today = Just x
+                , visibleDayOfWeek = Just (Date.weekday x)
+              }
+            , Cmd.none
+            )
+
+        ToggleHabit weekdayLabel habitLabel ->
+            ( { model
+                | completed =
+                    if Set.member ( weekdayLabel, habitLabel ) completed then
+                        Set.remove ( weekdayLabel, habitLabel ) completed
+
+                    else
+                        Set.insert ( weekdayLabel, habitLabel ) completed
+              }
+            , Cmd.none
+            )
+
+        MaybeAdjustWeekday ->
+            ( model, Date.today |> Task.perform ReceiveDate )
+
+        ViewToday ->
+            ( { model | visibleDayOfWeek = today |> Maybe.map Date.weekday }, Cmd.none )
+
+        ViewPrevious ->
+            ( { model
+                | visibleDayOfWeek = visibleDayOfWeek |> Maybe.map previousDay
+              }
+            , Cmd.none
+            )
+
+        ViewNext ->
+            ( { model
+                | visibleDayOfWeek = visibleDayOfWeek |> Maybe.map nextDay
+              }
+            , Cmd.none
+            )
+
+        ClearAll ->
+            ( { model | completed = Set.empty }, Cmd.none )
+
+        ToggleMorning ->
+            ( { model | includeMorning = not model.includeMorning }, Cmd.none )
+
+        ToggleEvening ->
+            ( { model | includeEvening = not model.includeEvening }, Cmd.none )
diff --git a/users/wpcarro/website/habit-screens/src/UI.elm b/users/wpcarro/website/habit-screens/src/UI.elm
new file mode 100644
index 0000000000..5b54269135
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/src/UI.elm
@@ -0,0 +1,9 @@
+module UI exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+
+
+button : List (Attribute msg) -> List (Html msg) -> Html msg
+button attrs children =
+    Html.button ([ class "focus:outline-none" ] ++ attrs) children
diff --git a/users/wpcarro/website/habit-screens/src/Utils.elm b/users/wpcarro/website/habit-screens/src/Utils.elm
new file mode 100644
index 0000000000..23b13c224c
--- /dev/null
+++ b/users/wpcarro/website/habit-screens/src/Utils.elm
@@ -0,0 +1,37 @@
+module Utils exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Maybe.Extra
+
+
+type Strategy
+    = Always String
+    | When Bool String
+    | If Bool String String
+
+
+class : List Strategy -> Attribute msg
+class classes =
+    classes
+        |> List.map
+            (\strategy ->
+                case strategy of
+                    Always x ->
+                        Just x
+
+                    When True x ->
+                        Just x
+
+                    When False _ ->
+                        Nothing
+
+                    If True x _ ->
+                        Just x
+
+                    If False _ x ->
+                        Just x
+            )
+        |> Maybe.Extra.values
+        |> String.join " "
+        |> Html.Attributes.class
diff --git a/users/wpcarro/website/sandbox/contentful/.envrc b/users/wpcarro/website/sandbox/contentful/.envrc
new file mode 100644
index 0000000000..e9d0356aaa
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/.envrc
@@ -0,0 +1,5 @@
+source_up
+use_nix
+# TODO(wpcarro): Prefer age-nix solution if possible.
+export CONTENTFUL_SPACE_ID="$(jq -j '.contentful | .spaceId' < $WPCARRO/secrets.json)"
+export CONTENTFUL_ACCESS_TOKEN="$(jq -j '.contentful | .accessToken' < $WPCARRO/secrets.json)"
diff --git a/users/wpcarro/website/sandbox/contentful/.gitignore b/users/wpcarro/website/sandbox/contentful/.gitignore
new file mode 100644
index 0000000000..fdf1c6188a
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/.gitignore
@@ -0,0 +1,2 @@
+.cache
+dist
\ No newline at end of file
diff --git a/users/wpcarro/website/sandbox/contentful/README.md b/users/wpcarro/website/sandbox/contentful/README.md
new file mode 100644
index 0000000000..9bd6fc914b
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/README.md
@@ -0,0 +1,18 @@
+# Contentful
+
+I have not used a CMS in a few years. I learned about Contentful from a
+Gatsby.js tutorial, and I wanted to learn more; I created a Contentful account,
+and I'm experimenting with the data here.
+
+## Developing
+
+```shell
+$ nix-shell
+$ yarn run dev
+```
+
+## Building
+
+```shell
+$ nix-build
+```
diff --git a/users/wpcarro/website/sandbox/contentful/default.nix b/users/wpcarro/website/sandbox/contentful/default.nix
new file mode 100644
index 0000000000..ce7e534b23
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/default.nix
@@ -0,0 +1,24 @@
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "ideal-website";
+  src = builtins.path { path = ./.; name = "contentful"; };
+  buildInputs = with pkgs; [
+    nodejs
+    # Exposes lscpu for parcel.js
+    utillinux
+  ];
+  # parcel.js needs number of CPUs
+  PARCEL_WORKERS = "1";
+  buildPhase = ''
+    export HOME="."
+    npx parcel build index.html
+  '';
+
+  installPhase = ''
+    mv dist $out
+  '';
+
+  # TODO(wpcarro): This doesn't build at all.
+  meta.ci.skip = true;
+}
diff --git a/users/wpcarro/website/sandbox/contentful/package.json b/users/wpcarro/website/sandbox/contentful/package.json
new file mode 100644
index 0000000000..3530bef763
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "tailwindcss",
+  "version": "1.0.0",
+  "main": "index.js",
+  "license": "MIT",
+  "scripts": {
+    "dev": "npx parcel src/index.html & npx tsc --watch --noEmit"
+  },
+  "devDependencies": {
+    "@types/node": "^13.9.3",
+    "parcel-bundler": "^1.12.4",
+    "tailwindcss": "^1.2.0",
+    "typescript": "^3.8.3"
+  },
+  "dependencies": {
+    "@reduxjs/toolkit": "^1.2.5",
+    "@types/react-dom": "^16.9.5",
+    "@types/react-redux": "^7.1.7",
+    "@types/react-router-dom": "^5.1.3",
+    "contentful": "^7.14.0",
+    "react": "^16.13.1",
+    "react-dom": "^16.13.1",
+    "react-redux": "^7.2.0",
+    "react-router-dom": "^5.1.2"
+  }
+}
diff --git a/users/wpcarro/website/sandbox/contentful/postcss.config.js b/users/wpcarro/website/sandbox/contentful/postcss.config.js
new file mode 100644
index 0000000000..a23795075b
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/postcss.config.js
@@ -0,0 +1,5 @@
+const tailwindcss = require("tailwindcss");
+
+module.exports = {
+  plugins: [tailwindcss("./tailwind.config.js")],
+};
diff --git a/users/wpcarro/website/sandbox/contentful/shell.nix b/users/wpcarro/website/sandbox/contentful/shell.nix
new file mode 100644
index 0000000000..a3ae929ef4
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/shell.nix
@@ -0,0 +1,8 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    nodejs
+    yarn
+  ];
+}
diff --git a/users/wpcarro/website/sandbox/contentful/src/App.tsx b/users/wpcarro/website/sandbox/contentful/src/App.tsx
new file mode 100644
index 0000000000..288f033218
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/src/App.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect } from "react";
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { actions, useTypedSelector } from "./store";
+import { Link } from "react-router-dom";
+import { getClient } from "./contentful";
+import type { Book } from "./store";
+
+const App: React.FC = () => {
+  const dispatch = useDispatch();
+  const { isLoading, books } = useTypedSelector((state) => ({
+    isLoading: state.isLoading,
+    books: state.books,
+  }));
+
+  useEffect(() => {
+    async function fetchData() {
+      const entries = await getClient().getEntries();
+      const books = entries.items.map((x) => x.fields) as Book[];
+
+      dispatch(actions.setBooks(books));
+    }
+    fetchData();
+  }, []);
+
+  return (
+    <Router>
+      <Switch>
+        <Route exact path="/">
+          <div className="container mx-auto">
+            <h1 className="py-6 text-2xl">Books</h1>
+            <ul>
+              {books.map((book) => (
+                <li key={book.title} className="py-3">
+                  <p>
+                    <span className="font-bold pr-3">{book.title}</span>
+                    <span className="text-gray-600">{book.author}</span>
+                  </p>
+                </li>
+              ))}
+            </ul>
+          </div>
+        </Route>
+      </Switch>
+    </Router>
+  );
+};
+
+export default App;
diff --git a/users/wpcarro/website/sandbox/contentful/src/contentful.ts b/users/wpcarro/website/sandbox/contentful/src/contentful.ts
new file mode 100644
index 0000000000..02ebc92b68
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/src/contentful.ts
@@ -0,0 +1,27 @@
+import { createClient } from "contentful";
+import type { ContentfulClientApi } from "contentful";
+
+const space = process.env.CONTENTFUL_SPACE_ID;
+const accessToken = process.env.CONTENTFUL_ACCESS_TOKEN;
+
+let client: ContentfulClientApi;
+
+// Idempotent way to get a reference to the Contentful client.
+export const getClient = (): ContentfulClientApi => {
+  if (typeof client !== "undefined") {
+    return client;
+  } else {
+    if (typeof space === "string" && typeof accessToken === "string") {
+      let client = createClient({
+        space,
+        accessToken,
+      });
+
+      return client;
+    } else {
+      throw new Error(
+        "Please set CONTENTFUL_SPACE_ID and CONTENTFUL_ACCESS_TOKEN"
+      );
+    }
+  }
+};
diff --git a/users/wpcarro/website/sandbox/contentful/src/index.css b/users/wpcarro/website/sandbox/contentful/src/index.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/src/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/users/wpcarro/website/sandbox/contentful/src/index.html b/users/wpcarro/website/sandbox/contentful/src/index.html
new file mode 100644
index 0000000000..91752af916
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/src/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="stylesheet" href="./index.css" />
+  </head>
+  <body>
+    <div id="mount"></div>
+    <script src="./index.tsx"></script>
+  </body>
+</html>
diff --git a/users/wpcarro/website/sandbox/contentful/src/index.tsx b/users/wpcarro/website/sandbox/contentful/src/index.tsx
new file mode 100644
index 0000000000..dc28dc4a9c
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/src/index.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+import { Provider } from "react-redux";
+import store from "./store";
+
+ReactDOM.render(
+  <Provider store={store}>
+    <App />
+  </Provider>,
+  document.getElementById("mount")
+);
diff --git a/users/wpcarro/website/sandbox/contentful/src/store.ts b/users/wpcarro/website/sandbox/contentful/src/store.ts
new file mode 100644
index 0000000000..b02053d302
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/src/store.ts
@@ -0,0 +1,36 @@
+import { createSlice, configureStore, PayloadAction } from "@reduxjs/toolkit";
+import { useSelector, TypedUseSelectorHook } from "react-redux";
+
+export interface Book {
+  title: string;
+  author: string;
+  // TODO(wpcarro): Prefer datetime type here.
+  publicationDate: string;
+}
+
+export interface State {
+  isLoading: boolean;
+  books: Book[];
+}
+
+const initialState: State = {
+  isLoading: true,
+  books: [],
+};
+
+export const { actions, reducer } = createSlice({
+  name: "application",
+  initialState,
+  reducers: {
+    toggleIsLoading: (state) => ({ ...state, isLoading: !state.isLoading }),
+    setBooks: (state, action) => ({ ...state, books: action.payload }),
+  },
+});
+
+/**
+ * Defining and consuming this allows us to avoid annotating State in all of our
+ * selectors.
+ */
+export const useTypedSelector: TypedUseSelectorHook<State> = useSelector;
+
+export default configureStore({ reducer });
diff --git a/users/wpcarro/website/sandbox/contentful/tailwind.config.js b/users/wpcarro/website/sandbox/contentful/tailwind.config.js
new file mode 100644
index 0000000000..3da6fa0dc7
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/tailwind.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+  theme: {
+    extend: {},
+  },
+  variants: {},
+  plugins: [],
+};
diff --git a/users/wpcarro/website/sandbox/contentful/tsconfig.json b/users/wpcarro/website/sandbox/contentful/tsconfig.json
new file mode 100644
index 0000000000..fe07ec1da4
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/tsconfig.json
@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react"
+  },
+  "include": ["src/**/*"]
+}
diff --git a/users/wpcarro/website/sandbox/contentful/yarn.lock b/users/wpcarro/website/sandbox/contentful/yarn.lock
new file mode 100644
index 0000000000..45fdea32b7
--- /dev/null
+++ b/users/wpcarro/website/sandbox/contentful/yarn.lock
@@ -0,0 +1,5717 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
+  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
+  dependencies:
+    "@babel/highlight" "^7.8.3"
+
+"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c"
+  integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==
+  dependencies:
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    semver "^5.5.0"
+
+"@babel/core@^7.4.4":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
+  integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helpers" "^7.9.0"
+    "@babel/parser" "^7.9.0"
+    "@babel/template" "^7.8.6"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
+"@babel/generator@^7.4.4", "@babel/generator@^7.9.0":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.3.tgz#7c8b2956c6f68b3ab732bd16305916fbba521d94"
+  integrity sha512-RpxM252EYsz9qLUIq6F7YJyK1sv0wWDBFuztfDGWaQKzHjqDHysxSiRUpA/X9jmfqo+WzkAVKFaUily5h+gDCQ==
+  dependencies:
+    "@babel/types" "^7.9.0"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+
+"@babel/helper-annotate-as-pure@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
+  integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503"
+  integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==
+  dependencies:
+    "@babel/helper-explode-assignable-expression" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-builder-react-jsx-experimental@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.0.tgz#066d80262ade488f9c1b1823ce5db88a4cedaa43"
+  integrity sha512-3xJEiyuYU4Q/Ar9BsHisgdxZsRlsShMe90URZ0e6przL26CCs8NJbDoxH94kKT17PcxlMhsCAwZd90evCo26VQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/types" "^7.9.0"
+
+"@babel/helper-builder-react-jsx@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz#16bf391990b57732700a3278d4d9a81231ea8d32"
+  integrity sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/types" "^7.9.0"
+
+"@babel/helper-compilation-targets@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde"
+  integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==
+  dependencies:
+    "@babel/compat-data" "^7.8.6"
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087"
+  integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-regex" "^7.8.3"
+    regexpu-core "^4.7.0"
+
+"@babel/helper-define-map@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15"
+  integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==
+  dependencies:
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/types" "^7.8.3"
+    lodash "^4.17.13"
+
+"@babel/helper-explode-assignable-expression@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982"
+  integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==
+  dependencies:
+    "@babel/traverse" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-function-name@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca"
+  integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-get-function-arity@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
+  integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-hoist-variables@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134"
+  integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-member-expression-to-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
+  integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-module-imports@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
+  integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-module-transforms@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
+  integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-simple-access" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/template" "^7.8.6"
+    "@babel/types" "^7.9.0"
+    lodash "^4.17.13"
+
+"@babel/helper-optimise-call-expression@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
+  integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
+  integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
+
+"@babel/helper-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965"
+  integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==
+  dependencies:
+    lodash "^4.17.13"
+
+"@babel/helper-remap-async-to-generator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86"
+  integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-wrap-function" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
+  integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/traverse" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
+"@babel/helper-simple-access@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
+  integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-split-export-declaration@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
+  integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-validator-identifier@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed"
+  integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==
+
+"@babel/helper-wrap-function@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610"
+  integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==
+  dependencies:
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helpers@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
+  integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+
+"@babel/highlight@^7.8.3":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
+  integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.4.4", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.3.tgz#043a5fc2ad8b7ea9facddc4e802a1f0f25da7255"
+  integrity sha512-E6SpIDJZ0cZAKoCNk+qSDd0ChfTnpiJN9FfNf3RZ20dzwA2vL2oq5IX1XTVT+4vDmRlta2nGk5HGMMskJAR+4A==
+
+"@babel/plugin-proposal-async-generator-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
+  integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-remap-async-to-generator" "^7.8.3"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+
+"@babel/plugin-proposal-dynamic-import@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054"
+  integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+
+"@babel/plugin-proposal-json-strings@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b"
+  integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2"
+  integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8"
+  integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+
+"@babel/plugin-proposal-object-rest-spread@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f"
+  integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9"
+  integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58"
+  integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d"
+  integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.8"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-async-generators@^7.8.0":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+  integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-dynamic-import@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
+  integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-flow@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.8.3.tgz#f2c883bd61a6316f2c89380ae5122f923ba4527f"
+  integrity sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-json-strings@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-jsx@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94"
+  integrity sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f"
+  integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-object-rest-spread@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+  integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391"
+  integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-arrow-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
+  integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-async-to-generator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086"
+  integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-remap-async-to-generator" "^7.8.3"
+
+"@babel/plugin-transform-block-scoped-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3"
+  integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-block-scoping@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a"
+  integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    lodash "^4.17.13"
+
+"@babel/plugin-transform-classes@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d"
+  integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-define-map" "^7.8.3"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b"
+  integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-destructuring@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b"
+  integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e"
+  integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-duplicate-keys@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1"
+  integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-exponentiation-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7"
+  integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==
+  dependencies:
+    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-flow-strip-types@^7.4.4":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.9.0.tgz#8a3538aa40434e000b8f44a3c5c9ac7229bd2392"
+  integrity sha512-7Qfg0lKQhEHs93FChxVLAvhBshOPQDtJUTVHr/ZwQNRccCm4O9D79r9tVSoV8iNwjP1YgfD+e/fgHcPkN1qEQg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-flow" "^7.8.3"
+
+"@babel/plugin-transform-for-of@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e"
+  integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-function-name@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b"
+  integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==
+  dependencies:
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1"
+  integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-member-expression-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410"
+  integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-modules-amd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4"
+  integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.4.4", "@babel/plugin-transform-modules-commonjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940"
+  integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-simple-access" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90"
+  integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==
+  dependencies:
+    "@babel/helper-hoist-variables" "^7.8.3"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-umd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697"
+  integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c"
+  integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+
+"@babel/plugin-transform-new-target@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43"
+  integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-object-super@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725"
+  integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.3"
+
+"@babel/plugin-transform-parameters@^7.8.7":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a"
+  integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-property-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263"
+  integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-react-jsx@^7.0.0":
+  version "7.9.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.1.tgz#d03af29396a6dc51bfa24eefd8005a9fd381152a"
+  integrity sha512-+xIZ6fPoix7h57CNO/ZeYADchg1tFyX9NDsnmNFFua8e1JNPln156mzS+8AQe1On2X2GLlANHJWHIXbMCqWDkQ==
+  dependencies:
+    "@babel/helper-builder-react-jsx" "^7.9.0"
+    "@babel/helper-builder-react-jsx-experimental" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-jsx" "^7.8.3"
+
+"@babel/plugin-transform-regenerator@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8"
+  integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==
+  dependencies:
+    regenerator-transform "^0.14.2"
+
+"@babel/plugin-transform-reserved-words@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5"
+  integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-shorthand-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8"
+  integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-spread@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8"
+  integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-sticky-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100"
+  integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-regex" "^7.8.3"
+
+"@babel/plugin-transform-template-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80"
+  integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-typeof-symbol@^7.8.4":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412"
+  integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-unicode-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad"
+  integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/preset-env@^7.4.4":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
+  integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==
+  dependencies:
+    "@babel/compat-data" "^7.9.0"
+    "@babel/helper-compilation-targets" "^7.8.7"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-proposal-async-generator-functions" "^7.8.3"
+    "@babel/plugin-proposal-dynamic-import" "^7.8.3"
+    "@babel/plugin-proposal-json-strings" "^7.8.3"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-proposal-numeric-separator" "^7.8.3"
+    "@babel/plugin-proposal-object-rest-spread" "^7.9.0"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-proposal-optional-chaining" "^7.9.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.8.3"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.8.3"
+    "@babel/plugin-transform-arrow-functions" "^7.8.3"
+    "@babel/plugin-transform-async-to-generator" "^7.8.3"
+    "@babel/plugin-transform-block-scoped-functions" "^7.8.3"
+    "@babel/plugin-transform-block-scoping" "^7.8.3"
+    "@babel/plugin-transform-classes" "^7.9.0"
+    "@babel/plugin-transform-computed-properties" "^7.8.3"
+    "@babel/plugin-transform-destructuring" "^7.8.3"
+    "@babel/plugin-transform-dotall-regex" "^7.8.3"
+    "@babel/plugin-transform-duplicate-keys" "^7.8.3"
+    "@babel/plugin-transform-exponentiation-operator" "^7.8.3"
+    "@babel/plugin-transform-for-of" "^7.9.0"
+    "@babel/plugin-transform-function-name" "^7.8.3"
+    "@babel/plugin-transform-literals" "^7.8.3"
+    "@babel/plugin-transform-member-expression-literals" "^7.8.3"
+    "@babel/plugin-transform-modules-amd" "^7.9.0"
+    "@babel/plugin-transform-modules-commonjs" "^7.9.0"
+    "@babel/plugin-transform-modules-systemjs" "^7.9.0"
+    "@babel/plugin-transform-modules-umd" "^7.9.0"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3"
+    "@babel/plugin-transform-new-target" "^7.8.3"
+    "@babel/plugin-transform-object-super" "^7.8.3"
+    "@babel/plugin-transform-parameters" "^7.8.7"
+    "@babel/plugin-transform-property-literals" "^7.8.3"
+    "@babel/plugin-transform-regenerator" "^7.8.7"
+    "@babel/plugin-transform-reserved-words" "^7.8.3"
+    "@babel/plugin-transform-shorthand-properties" "^7.8.3"
+    "@babel/plugin-transform-spread" "^7.8.3"
+    "@babel/plugin-transform-sticky-regex" "^7.8.3"
+    "@babel/plugin-transform-template-literals" "^7.8.3"
+    "@babel/plugin-transform-typeof-symbol" "^7.8.4"
+    "@babel/plugin-transform-unicode-regex" "^7.8.3"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.9.0"
+    browserslist "^4.9.1"
+    core-js-compat "^3.6.2"
+    invariant "^2.2.2"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/preset-modules@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
+  integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
+
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
+  integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/template@^7.4.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
+  integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/parser" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
+"@babel/traverse@^7.4.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892"
+  integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/parser" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.13"
+
+"@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5"
+  integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
+"@iarna/toml@^2.2.0":
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.3.tgz#f060bf6eaafae4d56a7dac618980838b0696e2ab"
+  integrity sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+  integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+  dependencies:
+    call-me-maybe "^1.0.1"
+    glob-to-regexp "^0.3.0"
+
+"@nodelib/fs.stat@^1.1.2":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+  integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+
+"@parcel/fs@^1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-1.11.0.tgz#fb8a2be038c454ad46a50dc0554c1805f13535cd"
+  integrity sha512-86RyEqULbbVoeo8OLcv+LQ1Vq2PKBAvWTU9fCgALxuCTbbs5Ppcvll4Vr+Ko1AnmMzja/k++SzNAwJfeQXVlpA==
+  dependencies:
+    "@parcel/utils" "^1.11.0"
+    mkdirp "^0.5.1"
+    rimraf "^2.6.2"
+
+"@parcel/logger@^1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-1.11.1.tgz#c55b0744bcbe84ebc291155627f0ec406a23e2e6"
+  integrity sha512-9NF3M6UVeP2udOBDILuoEHd8VrF4vQqoWHEafymO1pfSoOMfxrSJZw1MfyAAIUN/IFp9qjcpDCUbDZB+ioVevA==
+  dependencies:
+    "@parcel/workers" "^1.11.0"
+    chalk "^2.1.0"
+    grapheme-breaker "^0.3.2"
+    ora "^2.1.0"
+    strip-ansi "^4.0.0"
+
+"@parcel/utils@^1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-1.11.0.tgz#539e08fff8af3b26eca11302be80b522674b51ea"
+  integrity sha512-cA3p4jTlaMeOtAKR/6AadanOPvKeg8VwgnHhOyfi0yClD0TZS/hi9xu12w4EzA/8NtHu0g6o4RDfcNjqN8l1AQ==
+
+"@parcel/watcher@^1.12.1":
+  version "1.12.1"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-1.12.1.tgz#b98b3df309fcab93451b5583fc38e40826696dad"
+  integrity sha512-od+uCtCxC/KoNQAIE1vWx1YTyKYY+7CTrxBJPRh3cDWw/C0tCtlBMVlrbplscGoEpt6B27KhJDCv82PBxOERNA==
+  dependencies:
+    "@parcel/utils" "^1.11.0"
+    chokidar "^2.1.5"
+
+"@parcel/workers@^1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-1.11.0.tgz#7b8dcf992806f4ad2b6cecf629839c41c2336c59"
+  integrity sha512-USSjRAAQYsZFlv43FUPdD+jEGML5/8oLF0rUzPQTtK4q9kvaXr49F5ZplyLz5lox78cLZ0TxN2bIDQ1xhOkulQ==
+  dependencies:
+    "@parcel/utils" "^1.11.0"
+    physical-cpu-count "^2.0.0"
+
+"@reduxjs/toolkit@^1.2.5":
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.2.5.tgz#149aa62da12a18a67a30495cb63fd897003f2272"
+  integrity sha512-/OWoW5mniUXAomw4+3ZhhWodcs1/SRvK2HKyxLXdW6vKgmJhiBiSHe/huHARlKWujEmGaJrkafx548GE494bCQ==
+  dependencies:
+    immer "^4.0.1"
+    redux "^4.0.0"
+    redux-devtools-extension "^2.13.8"
+    redux-immutable-state-invariant "^2.1.0"
+    redux-thunk "^2.3.0"
+    reselect "^4.0.0"
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+"@types/history@*":
+  version "4.7.5"
+  resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
+  integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==
+
+"@types/hoist-non-react-statics@^3.3.0":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+  integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+  dependencies:
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+
+"@types/node@^13.9.3":
+  version "13.9.3"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.3.tgz#6356df2647de9eac569f9a52eda3480fa9e70b4d"
+  integrity sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==
+
+"@types/prop-types@*":
+  version "15.7.3"
+  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
+  integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
+
+"@types/q@^1.5.1":
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
+  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
+
+"@types/react-dom@^16.9.5":
+  version "16.9.5"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7"
+  integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg==
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-redux@^7.1.7":
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.7.tgz#12a0c529aba660696947384a059c5c6e08185c7a"
+  integrity sha512-U+WrzeFfI83+evZE2dkZ/oF/1vjIYgqrb5dGgedkqVV8HEfDFujNgWCwHL89TDuWKb47U0nTBT6PLGq4IIogWg==
+  dependencies:
+    "@types/hoist-non-react-statics" "^3.3.0"
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+    redux "^4.0.0"
+
+"@types/react-router-dom@^5.1.3":
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196"
+  integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==
+  dependencies:
+    "@types/history" "*"
+    "@types/react" "*"
+    "@types/react-router" "*"
+
+"@types/react-router@*":
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6"
+  integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ==
+  dependencies:
+    "@types/history" "*"
+    "@types/react" "*"
+
+"@types/react@*":
+  version "16.9.25"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.25.tgz#6ae2159b40138c792058a23c3c04fd3db49e929e"
+  integrity sha512-Dlj2V72cfYLPNscIG3/SMUOzhzj7GK3bpSrfefwt2YT9GLynvLCCZjbhyF6VsT0q0+aRACRX03TDJGb7cA0cqg==
+  dependencies:
+    "@types/prop-types" "*"
+    csstype "^2.2.0"
+
+abab@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
+  integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
+
+acorn-globals@^4.3.0:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+  integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+  dependencies:
+    acorn "^6.0.1"
+    acorn-walk "^6.0.1"
+
+acorn-node@^1.6.1:
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+  integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
+  dependencies:
+    acorn "^7.0.0"
+    acorn-walk "^7.0.0"
+    xtend "^4.0.2"
+
+acorn-walk@^6.0.1:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
+  integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+
+acorn-walk@^7.0.0:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
+  integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
+
+acorn@^6.0.1, acorn@^6.0.4:
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
+  integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+
+acorn@^7.0.0, acorn@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
+  integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
+
+ajv@^6.5.5:
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
+  integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+alphanum-sort@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-styles@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+  integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+ansi-to-html@^0.6.4:
+  version "0.6.14"
+  resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8"
+  integrity sha512-7ZslfB1+EnFSDO5Ju+ue5Y6It19DRnZXWv8jrGHgIlPna5Mh4jz7BV5jCbQneXNFurQcKoolaaAjHtgSBfOIuA==
+  dependencies:
+    entities "^1.1.2"
+
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+  integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+  integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+  integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-equal@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+  integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-unique@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+asn1.js@^4.0.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+  integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
+  dependencies:
+    bn.js "^4.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert@^1.1.1:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
+  integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
+  dependencies:
+    object-assign "^4.1.1"
+    util "0.10.3"
+
+assign-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+  integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+async-each@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
+  integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
+
+async-limiter@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+autoprefixer@^9.4.5:
+  version "9.7.4"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378"
+  integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g==
+  dependencies:
+    browserslist "^4.8.3"
+    caniuse-lite "^1.0.30001020"
+    chalk "^2.4.2"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^7.0.26"
+    postcss-value-parser "^4.0.2"
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+
+axios@^0.19.1:
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+  dependencies:
+    follow-redirects "1.5.10"
+
+babel-plugin-dynamic-import-node@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
+  integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
+  dependencies:
+    object.assign "^4.1.0"
+
+babel-runtime@^6.11.6, babel-runtime@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+  integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+  dependencies:
+    core-js "^2.4.0"
+    regenerator-runtime "^0.11.0"
+
+babel-types@^6.15.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+  integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
+  dependencies:
+    babel-runtime "^6.26.0"
+    esutils "^2.0.2"
+    lodash "^4.17.4"
+    to-fast-properties "^1.0.3"
+
+babylon-walk@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/babylon-walk/-/babylon-walk-1.0.2.tgz#3b15a5ddbb482a78b4ce9c01c8ba181702d9d6ce"
+  integrity sha1-OxWl3btIKni0zpwByLoYFwLZ1s4=
+  dependencies:
+    babel-runtime "^6.11.6"
+    babel-types "^6.15.0"
+    lodash.clone "^4.5.0"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base64-js@^1.0.2:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
+  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+
+base@^0.11.1:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+  integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+  dependencies:
+    cache-base "^1.0.1"
+    class-utils "^0.3.5"
+    component-emitter "^1.2.1"
+    define-property "^1.0.0"
+    isobject "^3.0.1"
+    mixin-deep "^1.2.0"
+    pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+  dependencies:
+    tweetnacl "^0.14.3"
+
+binary-extensions@^1.0.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
+  integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+  integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+boolbase@^1.0.0, boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^2.3.1, braces@^2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+  integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+  dependencies:
+    arr-flatten "^1.1.0"
+    array-unique "^0.3.2"
+    extend-shallow "^2.0.1"
+    fill-range "^4.0.0"
+    isobject "^3.0.1"
+    repeat-element "^1.1.2"
+    snapdragon "^0.8.1"
+    snapdragon-node "^2.0.1"
+    split-string "^3.0.2"
+    to-regex "^3.0.1"
+
+brfs@^1.2.0:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3"
+  integrity sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==
+  dependencies:
+    quote-stream "^1.0.1"
+    resolve "^1.1.5"
+    static-module "^2.2.0"
+    through2 "^2.0.0"
+
+brorand@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+  integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browser-process-hrtime@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+  integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+  integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+  dependencies:
+    buffer-xor "^1.0.3"
+    cipher-base "^1.0.0"
+    create-hash "^1.1.0"
+    evp_bytestokey "^1.0.3"
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+  integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+  dependencies:
+    browserify-aes "^1.0.4"
+    browserify-des "^1.0.0"
+    evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
+  integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+  dependencies:
+    cipher-base "^1.0.1"
+    des.js "^1.0.0"
+    inherits "^2.0.1"
+    safe-buffer "^5.1.2"
+
+browserify-rsa@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+  integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+  dependencies:
+    bn.js "^4.1.0"
+    randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+  integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=
+  dependencies:
+    bn.js "^4.1.1"
+    browserify-rsa "^4.0.0"
+    create-hash "^1.1.0"
+    create-hmac "^1.1.2"
+    elliptic "^6.0.0"
+    inherits "^2.0.1"
+    parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+  integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+  dependencies:
+    pako "~1.0.5"
+
+browserslist@^4.0.0, browserslist@^4.1.0, browserslist@^4.8.3, browserslist@^4.9.1:
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad"
+  integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A==
+  dependencies:
+    caniuse-lite "^1.0.30001035"
+    electron-to-chromium "^1.3.380"
+    node-releases "^1.1.52"
+    pkg-up "^3.1.0"
+
+buffer-equal@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
+  integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=
+
+buffer-from@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-xor@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+  integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
+builtin-status-codes@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+  integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bytes@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+cache-base@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+  integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+  dependencies:
+    collection-visit "^1.0.0"
+    component-emitter "^1.2.1"
+    get-value "^2.0.6"
+    has-value "^1.0.0"
+    isobject "^3.0.1"
+    set-value "^2.0.0"
+    to-object-path "^0.3.0"
+    union-value "^1.0.0"
+    unset-value "^1.0.0"
+
+call-me-maybe@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+  integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
+caller-callsite@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
+  integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+  dependencies:
+    callsites "^2.0.0"
+
+caller-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
+  integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+  dependencies:
+    caller-callsite "^2.0.0"
+
+callsites@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+  integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
+camelcase-css@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+  integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035:
+  version "1.0.30001036"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0"
+  integrity sha512-jU8CIFIj2oR7r4W+5AKcsvWNVIb6Q6OZE3UsrXrZBHFtreT4YgTeOJtTucp+zSedEpTi3L5wASSP0LYIE3if6w==
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+  integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+  dependencies:
+    ansi-styles "^2.2.1"
+    escape-string-regexp "^1.0.2"
+    has-ansi "^2.0.0"
+    strip-ansi "^3.0.0"
+    supports-color "^2.0.0"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+  integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chokidar@^2.1.5:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
+  integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
+  dependencies:
+    anymatch "^2.0.0"
+    async-each "^1.0.1"
+    braces "^2.3.2"
+    glob-parent "^3.1.0"
+    inherits "^2.0.3"
+    is-binary-path "^1.0.0"
+    is-glob "^4.0.0"
+    normalize-path "^3.0.0"
+    path-is-absolute "^1.0.0"
+    readdirp "^2.2.1"
+    upath "^1.1.1"
+  optionalDependencies:
+    fsevents "^1.2.7"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+  integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+class-utils@^0.3.5:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+  integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+  dependencies:
+    arr-union "^3.1.0"
+    define-property "^0.2.5"
+    isobject "^3.0.0"
+    static-extend "^0.1.1"
+
+cli-cursor@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+  integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+  dependencies:
+    restore-cursor "^2.0.0"
+
+cli-spinners@^1.1.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a"
+  integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==
+
+cliui@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+  dependencies:
+    string-width "^3.1.0"
+    strip-ansi "^5.2.0"
+    wrap-ansi "^5.1.0"
+
+clone@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
+
+clone@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
+coa@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
+  integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+  dependencies:
+    "@types/q" "^1.5.1"
+    chalk "^2.4.1"
+    q "^1.1.2"
+
+collection-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+  integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+  dependencies:
+    map-visit "^1.0.0"
+    object-visit "^1.0.0"
+
+color-convert@^1.9.0, color-convert@^1.9.1:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.5.2:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
+  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
+color@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
+  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  dependencies:
+    color-convert "^1.9.1"
+    color-string "^1.5.2"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+command-exists@^1.2.6:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.8.tgz#715acefdd1223b9c9b37110a149c6392c2852291"
+  integrity sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw==
+
+commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+component-emitter@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@~1.6.0:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+console-browserify@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
+  integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
+
+constants-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+  integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+contentful-resolve-response@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/contentful-resolve-response/-/contentful-resolve-response-1.1.4.tgz#9eb656876eecb2cd00444f0adf26bd91a5ec1992"
+  integrity sha512-oFq6n6zjbiwD9/7mBa8YHPwvPM0B0D4uOgg1n/rVzpQPhCrzeIixNj6fbJAbDiJt05rZqxiY3K1Db7pPRhRaZw==
+  dependencies:
+    lodash "^4.17.4"
+
+contentful-sdk-core@^6.4.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/contentful-sdk-core/-/contentful-sdk-core-6.4.0.tgz#3b42991ae9084baf1bc5d01c61cb54441f740803"
+  integrity sha512-UvYQ/Wrt5EntlMSBbgqgvKfTBRzf6fIT2p5Wp7bsnA3/KLEiYcYd/2qhUKw4x9nfp+0G8B1s4TpDwxV0oymBiA==
+  dependencies:
+    lodash "^4.17.10"
+    qs "^6.5.2"
+
+contentful@^7.14.0:
+  version "7.14.0"
+  resolved "https://registry.yarnpkg.com/contentful/-/contentful-7.14.0.tgz#3b57287e484b8370adfd654a5196be2c2ffb9afa"
+  integrity sha512-edoiQx0AkmNqnGofmLHGVt84k2S8XuPyw2UOct/Oc3HEW0Z66osMJ4M/XA9GeByCCD5ZC7qotseBRyag/1g0iA==
+  dependencies:
+    axios "^0.19.1"
+    contentful-resolve-response "^1.1.4"
+    contentful-sdk-core "^6.4.0"
+    json-stringify-safe "^5.0.1"
+    lodash "^4.17.11"
+
+convert-source-map@^1.5.1, convert-source-map@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+  dependencies:
+    safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+  integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js-compat@^3.6.2:
+  version "3.6.4"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17"
+  integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==
+  dependencies:
+    browserslist "^4.8.3"
+    semver "7.0.0"
+
+core-js@^2.4.0, core-js@^2.6.5:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
+  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cosmiconfig@^5.0.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
+  integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
+  dependencies:
+    import-fresh "^2.0.0"
+    is-directory "^0.3.1"
+    js-yaml "^3.13.1"
+    parse-json "^4.0.0"
+
+create-ecdh@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
+  integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==
+  dependencies:
+    bn.js "^4.1.0"
+    elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+  integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+  dependencies:
+    cipher-base "^1.0.1"
+    inherits "^2.0.1"
+    md5.js "^1.3.4"
+    ripemd160 "^2.0.1"
+    sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+  integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+  dependencies:
+    cipher-base "^1.0.3"
+    create-hash "^1.1.0"
+    inherits "^2.0.1"
+    ripemd160 "^2.0.0"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
+cross-spawn@^6.0.4:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+crypto-browserify@^3.11.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+  integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+  dependencies:
+    browserify-cipher "^1.0.0"
+    browserify-sign "^4.0.0"
+    create-ecdh "^4.0.0"
+    create-hash "^1.1.0"
+    create-hmac "^1.1.0"
+    diffie-hellman "^5.0.0"
+    inherits "^2.0.1"
+    pbkdf2 "^3.0.3"
+    public-encrypt "^4.0.0"
+    randombytes "^2.0.0"
+    randomfill "^1.0.3"
+
+css-color-names@0.0.4, css-color-names@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+  integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
+
+css-declaration-sorter@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22"
+  integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==
+  dependencies:
+    postcss "^7.0.1"
+    timsort "^0.3.0"
+
+css-modules-loader-core@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz#5908668294a1becd261ae0a4ce21b0b551f21d16"
+  integrity sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=
+  dependencies:
+    icss-replace-symbols "1.1.0"
+    postcss "6.0.1"
+    postcss-modules-extract-imports "1.1.0"
+    postcss-modules-local-by-default "1.2.0"
+    postcss-modules-scope "1.1.0"
+    postcss-modules-values "1.3.0"
+
+css-select-base-adapter@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
+  integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+
+css-select@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
+  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^3.2.1"
+    domutils "^1.7.0"
+    nth-check "^1.0.2"
+
+css-selector-tokenizer@^0.7.0:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz#11e5e27c9a48d90284f22d45061c303d7a25ad87"
+  integrity sha512-yj856NGuAymN6r8bn8/Jl46pR+OC3eEvAhfGYDUe7YPtTPAYrSSw4oAniZ9Y8T5B92hjhwTBLUen0/vKPxf6pw==
+  dependencies:
+    cssesc "^3.0.0"
+    fastparse "^1.1.2"
+    regexpu-core "^4.6.0"
+
+css-tree@1.0.0-alpha.37:
+  version "1.0.0-alpha.37"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
+  integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
+  dependencies:
+    mdn-data "2.0.4"
+    source-map "^0.6.1"
+
+css-unit-converter@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
+  integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
+
+css-what@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1"
+  integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
+  integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+  dependencies:
+    css-declaration-sorter "^4.0.1"
+    cssnano-util-raw-cache "^4.0.1"
+    postcss "^7.0.0"
+    postcss-calc "^7.0.1"
+    postcss-colormin "^4.0.3"
+    postcss-convert-values "^4.0.1"
+    postcss-discard-comments "^4.0.2"
+    postcss-discard-duplicates "^4.0.2"
+    postcss-discard-empty "^4.0.1"
+    postcss-discard-overridden "^4.0.1"
+    postcss-merge-longhand "^4.0.11"
+    postcss-merge-rules "^4.0.3"
+    postcss-minify-font-values "^4.0.2"
+    postcss-minify-gradients "^4.0.2"
+    postcss-minify-params "^4.0.2"
+    postcss-minify-selectors "^4.0.2"
+    postcss-normalize-charset "^4.0.1"
+    postcss-normalize-display-values "^4.0.2"
+    postcss-normalize-positions "^4.0.2"
+    postcss-normalize-repeat-style "^4.0.2"
+    postcss-normalize-string "^4.0.2"
+    postcss-normalize-timing-functions "^4.0.2"
+    postcss-normalize-unicode "^4.0.1"
+    postcss-normalize-url "^4.0.1"
+    postcss-normalize-whitespace "^4.0.2"
+    postcss-ordered-values "^4.1.2"
+    postcss-reduce-initial "^4.0.3"
+    postcss-reduce-transforms "^4.0.2"
+    postcss-svgo "^4.0.2"
+    postcss-unique-selectors "^4.0.1"
+
+cssnano-util-get-arguments@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f"
+  integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=
+
+cssnano-util-get-match@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d"
+  integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=
+
+cssnano-util-raw-cache@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282"
+  integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==
+  dependencies:
+    postcss "^7.0.0"
+
+cssnano-util-same-parent@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
+  integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
+
+cssnano@^4.0.0, cssnano@^4.1.10:
+  version "4.1.10"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
+  integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+  dependencies:
+    cosmiconfig "^5.0.0"
+    cssnano-preset-default "^4.0.7"
+    is-resolvable "^1.0.0"
+    postcss "^7.0.0"
+
+csso@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d"
+  integrity sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==
+  dependencies:
+    css-tree "1.0.0-alpha.37"
+
+cssom@0.3.x, cssom@^0.3.4:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+  integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^1.1.1:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
+  integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==
+  dependencies:
+    cssom "0.3.x"
+
+csstype@^2.2.0:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098"
+  integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+  dependencies:
+    assert-plus "^1.0.0"
+
+data-urls@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+  integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+  dependencies:
+    abab "^2.0.0"
+    whatwg-mimetype "^2.2.0"
+    whatwg-url "^7.0.0"
+
+deasync@^0.1.14:
+  version "0.1.19"
+  resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.19.tgz#e7ea89fcc9ad483367e8a48fe78f508ca86286e8"
+  integrity sha512-oh3MRktfnPlLysCPpBpKZZzb4cUC/p0aA3SyRGp15lN30juJBTo/CiD0d4fR+f1kBtUQoJj1NE9RPNWQ7BQ9Mg==
+  dependencies:
+    bindings "^1.5.0"
+    node-addon-api "^1.7.1"
+
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@=3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
+debug@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+  dependencies:
+    ms "^2.1.1"
+
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+defaults@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+  integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
+  dependencies:
+    clone "^1.0.2"
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+define-property@^0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+  integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+  dependencies:
+    is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+  integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+  dependencies:
+    is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+  integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+  dependencies:
+    is-descriptor "^1.0.2"
+    isobject "^3.0.1"
+
+defined@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+  integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+depd@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+des.js@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
+  integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
+  dependencies:
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+  integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detective@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
+  integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==
+  dependencies:
+    acorn-node "^1.6.1"
+    defined "^1.0.0"
+    minimist "^1.1.1"
+
+diffie-hellman@^5.0.0:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+  integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+  dependencies:
+    bn.js "^4.1.0"
+    miller-rabin "^4.0.0"
+    randombytes "^2.0.0"
+
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+domain-browser@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+  integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+domelementtype@1, domelementtype@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
+  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+
+domexception@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+  integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+  dependencies:
+    webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+  dependencies:
+    domelementtype "1"
+
+domutils@^1.5.1, domutils@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+dot-prop@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
+  integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
+  dependencies:
+    is-obj "^2.0.0"
+
+dotenv-expand@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
+  integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+
+dotenv@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
+  integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
+
+duplexer2@~0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
+  integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
+  dependencies:
+    readable-stream "^2.0.2"
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.380:
+  version "1.3.381"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.381.tgz#952678ff91a5f36175a3832358a6dd2de3bf62b7"
+  integrity sha512-JQBpVUr83l+QOqPQpj2SbOve1bBE4ACpmwcMNqWlZmfib7jccxJ02qFNichDpZ5LS4Zsqc985NIPKegBIZjK8Q==
+
+elliptic@^6.0.0:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
+  integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
+  dependencies:
+    bn.js "^4.4.0"
+    brorand "^1.0.1"
+    hash.js "^1.0.0"
+    hmac-drbg "^1.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.0"
+
+emoji-regex@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+entities@^1.1.1, entities@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
+  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+
+envinfo@^7.3.1:
+  version "7.5.0"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.0.tgz#91410bb6db262fb4f1409bd506e9ff57e91023f4"
+  integrity sha512-jDgnJaF/Btomk+m3PZDTTCb5XIIIX3zYItnCRfF73zVgvinLoRomuhi75Y4su0PtQxWz4v66XnLLckyvyJTOIQ==
+
+error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.2:
+  version "1.17.5"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
+  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^1.11.0, escodegen@^1.11.1:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457"
+  integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+escodegen@~1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
+  integrity sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==
+  dependencies:
+    esprima "^3.1.3"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+esprima@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+  integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
+
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+  integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+events@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
+  integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+  integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+  dependencies:
+    md5.js "^1.3.4"
+    safe-buffer "^5.1.1"
+
+expand-brackets@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+  integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+  dependencies:
+    debug "^2.3.3"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    posix-character-classes "^0.1.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extend-shallow@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+  dependencies:
+    is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+  integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+  dependencies:
+    assign-symbols "^1.0.0"
+    is-extendable "^1.0.1"
+
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+  integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+  dependencies:
+    array-unique "^0.3.2"
+    define-property "^1.0.0"
+    expand-brackets "^2.1.4"
+    extend-shallow "^2.0.1"
+    fragment-cache "^0.2.1"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+falafel@^2.1.0:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.2.4.tgz#b5d86c060c2412a43166243cb1bce44d1abd2819"
+  integrity sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ==
+  dependencies:
+    acorn "^7.1.1"
+    foreach "^2.0.5"
+    isarray "^2.0.1"
+    object-keys "^1.0.6"
+
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+
+fast-glob@^2.2.2:
+  version "2.2.7"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
+  integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
+  dependencies:
+    "@mrmlnc/readdir-enhanced" "^2.2.1"
+    "@nodelib/fs.stat" "^1.1.2"
+    glob-parent "^3.1.0"
+    is-glob "^4.0.0"
+    merge2 "^1.2.3"
+    micromatch "^3.1.10"
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fastparse@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
+  integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
+
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+filesize@^3.6.0:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+  integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+
+fill-range@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+  integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+    to-regex-range "^2.1.0"
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
+
+for-in@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+foreach@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+  integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+  integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+  dependencies:
+    map-cache "^0.2.2"
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+fs-extra@^8.0.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+  version "1.2.12"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c"
+  integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==
+  dependencies:
+    bindings "^1.5.0"
+    nan "^2.12.1"
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+gensync@^1.0.0-beta.1:
+  version "1.0.0-beta.1"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+
+get-caller-file@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-port@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
+  integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=
+
+get-value@^2.0.3, get-value@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+  integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+  dependencies:
+    assert-plus "^1.0.0"
+
+glob-parent@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+  integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+  dependencies:
+    is-glob "^3.1.0"
+    path-dirname "^1.0.0"
+
+glob-to-regexp@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+  integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+
+glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    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"
+
+globals@^11.1.0:
+  version "11.12.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
+  integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+
+grapheme-breaker@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/grapheme-breaker/-/grapheme-breaker-0.3.2.tgz#5b9e6b78c3832452d2ba2bb1cb830f96276410ac"
+  integrity sha1-W55reMODJFLSuiuxy4MPlidkEKw=
+  dependencies:
+    brfs "^1.2.0"
+    unicode-trie "^0.3.1"
+
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+  integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+has-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+  integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-value@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+  integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+  dependencies:
+    get-value "^2.0.3"
+    has-values "^0.1.4"
+    isobject "^2.0.0"
+
+has-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+  integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+  dependencies:
+    get-value "^2.0.6"
+    has-values "^1.0.0"
+    isobject "^3.0.0"
+
+has-values@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+  integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+  integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+has@^1.0.0, has@^1.0.1, has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+hash-base@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+  integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+  integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+  dependencies:
+    inherits "^2.0.3"
+    minimalistic-assert "^1.0.1"
+
+hex-color-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
+  integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+
+history@^4.9.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
+  integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    loose-envify "^1.2.0"
+    resolve-pathname "^3.0.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+    value-equal "^1.0.1"
+
+hmac-drbg@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+  integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+  dependencies:
+    hash.js "^1.0.3"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.1"
+
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+  integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+  dependencies:
+    react-is "^16.7.0"
+
+hsl-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
+  integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=
+
+hsla-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
+  integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+
+html-comment-regex@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
+  integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+
+html-encoding-sniffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+  integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+  dependencies:
+    whatwg-encoding "^1.0.1"
+
+html-tags@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.2.0.tgz#c78de65b5663aa597989dd2b7ab49200d7e4db98"
+  integrity sha1-x43mW1Zjqll5id0rerSSANfk25g=
+
+htmlnano@^0.2.2:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-0.2.5.tgz#134fd9548c7cbe51c8508ce434a3f9488cff1b0b"
+  integrity sha512-X1iPSwXG/iF9bVs+/obt2n6F64uH0ETkA8zp7qFDmLW9/+A6ueHGeb/+qD67T21qUY22owZPMdawljN50ajkqA==
+  dependencies:
+    cssnano "^4.1.10"
+    normalize-html-whitespace "^1.0.0"
+    posthtml "^0.12.0"
+    posthtml-render "^1.1.5"
+    purgecss "^1.4.0"
+    svgo "^1.3.2"
+    terser "^4.3.9"
+    uncss "^0.17.2"
+
+htmlparser2@^3.9.2:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+  dependencies:
+    domelementtype "^1.3.1"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^3.1.1"
+
+http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+  integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+iconv-lite@0.4.24:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+  integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
+
+ieee754@^1.1.4:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+
+immer@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/immer/-/immer-4.0.2.tgz#9ff0fcdf88e06f92618a5978ceecb5884e633559"
+  integrity sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w==
+
+import-fresh@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
+  integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+  dependencies:
+    caller-path "^2.0.0"
+    resolve-from "^3.0.0"
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+  integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+inherits@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+invariant@^2.1.0, invariant@^2.2.2, invariant@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+  integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+  dependencies:
+    loose-envify "^1.0.0"
+
+is-absolute-url@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+  integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+
+is-absolute-url@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-accessor-descriptor@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+  integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+  integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+  dependencies:
+    kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-binary-path@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+  integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+  dependencies:
+    binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.1.4, is-callable@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+
+is-color-stop@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
+  integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=
+  dependencies:
+    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-data-descriptor@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+  integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+  integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+  dependencies:
+    kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-descriptor@^0.1.0:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+  integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+  dependencies:
+    is-accessor-descriptor "^0.1.6"
+    is-data-descriptor "^0.1.4"
+    kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+  integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+  dependencies:
+    is-accessor-descriptor "^1.0.0"
+    is-data-descriptor "^1.0.0"
+    kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+  integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+  dependencies:
+    is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-glob@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+  integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+  dependencies:
+    is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-html@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-html/-/is-html-1.1.0.tgz#e04f1c18d39485111396f9a0273eab51af218464"
+  integrity sha1-4E8cGNOUhRETlvmgJz6rUa8hhGQ=
+  dependencies:
+    html-tags "^1.0.0"
+
+is-number@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+  integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-obj@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
+  integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-regex@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+  dependencies:
+    has "^1.0.3"
+
+is-resolvable@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-svg@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
+  integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
+  dependencies:
+    html-comment-regex "^1.1.0"
+
+is-symbol@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+  dependencies:
+    has-symbols "^1.0.1"
+
+is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-url@^1.2.2:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
+  integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+
+is-windows@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+  integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+isarray@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isarray@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+  dependencies:
+    isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.10.0, js-yaml@^3.13.1:
+  version "3.13.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+  integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^14.1.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-14.1.0.tgz#916463b6094956b0a6c1782c94e380cd30e1981b"
+  integrity sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng==
+  dependencies:
+    abab "^2.0.0"
+    acorn "^6.0.4"
+    acorn-globals "^4.3.0"
+    array-equal "^1.0.0"
+    cssom "^0.3.4"
+    cssstyle "^1.1.1"
+    data-urls "^1.1.0"
+    domexception "^1.0.1"
+    escodegen "^1.11.0"
+    html-encoding-sniffer "^1.0.2"
+    nwsapi "^2.1.3"
+    parse5 "5.1.0"
+    pn "^1.1.0"
+    request "^2.88.0"
+    request-promise-native "^1.0.5"
+    saxes "^3.1.9"
+    symbol-tree "^3.2.2"
+    tough-cookie "^2.5.0"
+    w3c-hr-time "^1.0.1"
+    w3c-xmlserializer "^1.1.2"
+    webidl-conversions "^4.0.2"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^7.0.0"
+    ws "^6.1.2"
+    xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+  integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
+  integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+  integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+  integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levenary@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
+  integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
+  dependencies:
+    leven "^3.1.0"
+
+levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+lodash.clone@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
+  integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.sortby@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash.toarray@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
+  integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+log-symbols@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+  integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
+  dependencies:
+    chalk "^2.0.1"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+magic-string@^0.22.4:
+  version "0.22.5"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
+  integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==
+  dependencies:
+    vlq "^0.2.2"
+
+map-cache@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+  integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+  integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+  dependencies:
+    object-visit "^1.0.0"
+
+md5.js@^1.3.4:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+  integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+    safe-buffer "^5.1.2"
+
+mdn-data@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
+  integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+
+merge-source-map@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f"
+  integrity sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=
+  dependencies:
+    source-map "^0.5.6"
+
+merge2@^1.2.3:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
+  integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
+
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+  integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    braces "^2.3.1"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    extglob "^2.0.4"
+    fragment-cache "^0.2.1"
+    kind-of "^6.0.2"
+    nanomatch "^1.2.9"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.2"
+
+miller-rabin@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+  integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+  dependencies:
+    bn.js "^4.0.0"
+    brorand "^1.0.1"
+
+mime-db@1.43.0:
+  version "1.43.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
+  integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+  version "2.1.26"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
+  integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
+  dependencies:
+    mime-db "1.43.0"
+
+mime@1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mimic-fn@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+  integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+mini-create-react-context@^0.3.0:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
+  integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
+  dependencies:
+    "@babel/runtime" "^7.4.0"
+    gud "^1.0.0"
+    tiny-warning "^1.0.2"
+
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+  integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+mixin-deep@^1.2.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+  dependencies:
+    for-in "^1.0.2"
+    is-extendable "^1.0.1"
+
+mkdirp@^0.5.1, mkdirp@~0.5.1:
+  version "0.5.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512"
+  integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==
+  dependencies:
+    minimist "^1.2.5"
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+  integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+ms@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nan@^2.12.1:
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
+  integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+
+nanomatch@^1.2.9:
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+  integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    fragment-cache "^0.2.1"
+    is-windows "^1.0.2"
+    kind-of "^6.0.2"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-addon-api@^1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.1.tgz#cf813cd69bb8d9100f6bdca6755fc268f54ac492"
+  integrity sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==
+
+node-emoji@^1.8.1:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
+  integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==
+  dependencies:
+    lodash.toarray "^4.4.0"
+
+node-forge@^0.7.1:
+  version "0.7.6"
+  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
+  integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
+
+node-libs-browser@^2.0.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
+  integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
+  dependencies:
+    assert "^1.1.1"
+    browserify-zlib "^0.2.0"
+    buffer "^4.3.0"
+    console-browserify "^1.1.0"
+    constants-browserify "^1.0.0"
+    crypto-browserify "^3.11.0"
+    domain-browser "^1.1.1"
+    events "^3.0.0"
+    https-browserify "^1.0.0"
+    os-browserify "^0.3.0"
+    path-browserify "0.0.1"
+    process "^0.11.10"
+    punycode "^1.2.4"
+    querystring-es3 "^0.2.0"
+    readable-stream "^2.3.3"
+    stream-browserify "^2.0.1"
+    stream-http "^2.7.2"
+    string_decoder "^1.0.0"
+    timers-browserify "^2.0.4"
+    tty-browserify "0.0.0"
+    url "^0.11.0"
+    util "^0.11.0"
+    vm-browserify "^1.0.1"
+
+node-releases@^1.1.52:
+  version "1.1.52"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9"
+  integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ==
+  dependencies:
+    semver "^6.3.0"
+
+normalize-html-whitespace@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-html-whitespace/-/normalize-html-whitespace-1.0.0.tgz#5e3c8e192f1b06c3b9eee4b7e7f28854c7601e34"
+  integrity sha512-9ui7CGtOOlehQu0t/OhhlmDyc71mKVlv+4vF+me4iZLPrNtRL2xoquEdfZxasC/bdQi/Hr3iTrpyRKIG+ocabA==
+
+normalize-path@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+  dependencies:
+    remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+  integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+normalize-url@^3.0.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
+  integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
+
+normalize.css@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+  integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
+
+nth-check@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+  dependencies:
+    boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+  integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
+nwsapi@^2.1.3:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+  integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+  dependencies:
+    copy-descriptor "^0.1.0"
+    define-property "^0.2.5"
+    kind-of "^3.0.3"
+
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-inspect@~1.4.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.4.1.tgz#37ffb10e71adaf3748d05f713b4c9452f402cbc4"
+  integrity sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-visit@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+  integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+  dependencies:
+    isobject "^3.0.0"
+
+object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+object.pick@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+  integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+  dependencies:
+    isobject "^3.0.1"
+
+object.values@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+on-finished@~2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+  dependencies:
+    ee-first "1.1.1"
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+  integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+  dependencies:
+    mimic-fn "^1.0.0"
+
+opn@^5.1.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+  integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+  dependencies:
+    is-wsl "^1.1.0"
+
+optionator@^0.8.1:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+ora@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ora/-/ora-2.1.0.tgz#6caf2830eb924941861ec53a173799e008b51e5b"
+  integrity sha512-hNNlAd3gfv/iPmsNxYoAPLvxg7HuPozww7fFonMZvL84tP6Ox5igfk5j/+a9rtJJwqMgKK+JgWsAQik5o0HTLA==
+  dependencies:
+    chalk "^2.3.1"
+    cli-cursor "^2.1.0"
+    cli-spinners "^1.1.0"
+    log-symbols "^2.2.0"
+    strip-ansi "^4.0.0"
+    wcwidth "^1.0.1"
+
+os-browserify@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+  integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+p-limit@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
+  integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+pako@^0.2.5:
+  version "0.2.9"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+  integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=
+
+pako@~1.0.5:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+parcel-bundler@^1.12.4:
+  version "1.12.4"
+  resolved "https://registry.yarnpkg.com/parcel-bundler/-/parcel-bundler-1.12.4.tgz#31223f4ab4d00323a109fce28d5e46775409a9ee"
+  integrity sha512-G+iZGGiPEXcRzw0fiRxWYCKxdt/F7l9a0xkiU4XbcVRJCSlBnioWEwJMutOCCpoQmaQtjB4RBHDGIHN85AIhLQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/core" "^7.4.4"
+    "@babel/generator" "^7.4.4"
+    "@babel/parser" "^7.4.4"
+    "@babel/plugin-transform-flow-strip-types" "^7.4.4"
+    "@babel/plugin-transform-modules-commonjs" "^7.4.4"
+    "@babel/plugin-transform-react-jsx" "^7.0.0"
+    "@babel/preset-env" "^7.4.4"
+    "@babel/runtime" "^7.4.4"
+    "@babel/template" "^7.4.4"
+    "@babel/traverse" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    "@iarna/toml" "^2.2.0"
+    "@parcel/fs" "^1.11.0"
+    "@parcel/logger" "^1.11.1"
+    "@parcel/utils" "^1.11.0"
+    "@parcel/watcher" "^1.12.1"
+    "@parcel/workers" "^1.11.0"
+    ansi-to-html "^0.6.4"
+    babylon-walk "^1.0.2"
+    browserslist "^4.1.0"
+    chalk "^2.1.0"
+    clone "^2.1.1"
+    command-exists "^1.2.6"
+    commander "^2.11.0"
+    core-js "^2.6.5"
+    cross-spawn "^6.0.4"
+    css-modules-loader-core "^1.1.0"
+    cssnano "^4.0.0"
+    deasync "^0.1.14"
+    dotenv "^5.0.0"
+    dotenv-expand "^5.1.0"
+    envinfo "^7.3.1"
+    fast-glob "^2.2.2"
+    filesize "^3.6.0"
+    get-port "^3.2.0"
+    htmlnano "^0.2.2"
+    is-glob "^4.0.0"
+    is-url "^1.2.2"
+    js-yaml "^3.10.0"
+    json5 "^1.0.1"
+    micromatch "^3.0.4"
+    mkdirp "^0.5.1"
+    node-forge "^0.7.1"
+    node-libs-browser "^2.0.0"
+    opn "^5.1.0"
+    postcss "^7.0.11"
+    postcss-value-parser "^3.3.1"
+    posthtml "^0.11.2"
+    posthtml-parser "^0.4.0"
+    posthtml-render "^1.1.3"
+    resolve "^1.4.0"
+    semver "^5.4.1"
+    serialize-to-js "^3.0.0"
+    serve-static "^1.12.4"
+    source-map "0.6.1"
+    terser "^3.7.3"
+    v8-compile-cache "^2.0.0"
+    ws "^5.1.1"
+
+parse-asn1@^5.0.0:
+  version "5.1.5"
+  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e"
+  integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==
+  dependencies:
+    asn1.js "^4.0.0"
+    browserify-aes "^1.0.0"
+    create-hash "^1.1.0"
+    evp_bytestokey "^1.0.0"
+    pbkdf2 "^3.0.3"
+    safe-buffer "^5.1.1"
+
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
+parse5@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+  integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+
+parseurl@~1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+  integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
+  integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
+
+path-dirname@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+  integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@^1.7.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+  integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+  dependencies:
+    isarray "0.0.1"
+
+pbkdf2@^3.0.3:
+  version "3.0.17"
+  resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
+  integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==
+  dependencies:
+    create-hash "^1.1.2"
+    create-hmac "^1.1.4"
+    ripemd160 "^2.0.1"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+physical-cpu-count@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz#18de2f97e4bf7a9551ad7511942b5496f7aba660"
+  integrity sha1-GN4vl+S/epVRrXURlCtUlverpmA=
+
+pkg-up@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
+  integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
+  dependencies:
+    find-up "^3.0.0"
+
+pn@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+  integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+posix-character-classes@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss-calc@^7.0.1:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.2.tgz#504efcd008ca0273120568b0792b16cdcde8aac1"
+  integrity sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==
+  dependencies:
+    postcss "^7.0.27"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381"
+  integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==
+  dependencies:
+    browserslist "^4.0.0"
+    color "^3.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-convert-values@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
+  integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-discard-comments@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033"
+  integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-duplicates@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb"
+  integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-empty@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765"
+  integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-discard-overridden@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57"
+  integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-functions@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-functions/-/postcss-functions-3.0.0.tgz#0e94d01444700a481de20de4d55fb2640564250e"
+  integrity sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=
+  dependencies:
+    glob "^7.1.2"
+    object-assign "^4.1.1"
+    postcss "^6.0.9"
+    postcss-value-parser "^3.3.0"
+
+postcss-js@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9"
+  integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==
+  dependencies:
+    camelcase-css "^2.0.1"
+    postcss "^7.0.18"
+
+postcss-merge-longhand@^4.0.11:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24"
+  integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==
+  dependencies:
+    css-color-names "0.0.4"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    stylehacks "^4.0.0"
+
+postcss-merge-rules@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650"
+  integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-api "^3.0.0"
+    cssnano-util-same-parent "^4.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+    vendors "^1.0.0"
+
+postcss-minify-font-values@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6"
+  integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-minify-gradients@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471"
+  integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    is-color-stop "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-minify-params@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874"
+  integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    browserslist "^4.0.0"
+    cssnano-util-get-arguments "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8"
+  integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+
+postcss-modules-extract-imports@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz#b614c9720be6816eaee35fb3a5faa1dba6a05ddb"
+  integrity sha1-thTJcgvmgW6u41+zpfqh26agXds=
+  dependencies:
+    postcss "^6.0.1"
+
+postcss-modules-local-by-default@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
+  integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=
+  dependencies:
+    css-selector-tokenizer "^0.7.0"
+    postcss "^6.0.1"
+
+postcss-modules-scope@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
+  integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A=
+  dependencies:
+    css-selector-tokenizer "^0.7.0"
+    postcss "^6.0.1"
+
+postcss-modules-values@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
+  integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=
+  dependencies:
+    icss-replace-symbols "^1.1.0"
+    postcss "^6.0.1"
+
+postcss-nested@^4.1.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.1.tgz#4bc2e5b35e3b1e481ff81e23b700da7f82a8b248"
+  integrity sha512-AMayXX8tS0HCp4O4lolp4ygj9wBn32DJWXvG6gCv+ZvJrEa00GUxJcJEEzMh87BIe6FrWdYkpR2cuyqHKrxmXw==
+  dependencies:
+    postcss "^7.0.21"
+    postcss-selector-parser "^6.0.2"
+
+postcss-normalize-charset@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4"
+  integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==
+  dependencies:
+    postcss "^7.0.0"
+
+postcss-normalize-display-values@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a"
+  integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-positions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f"
+  integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-repeat-style@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c"
+  integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-string@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c"
+  integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==
+  dependencies:
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-timing-functions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9"
+  integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-unicode@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb"
+  integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==
+  dependencies:
+    browserslist "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-url@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1"
+  integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==
+  dependencies:
+    is-absolute-url "^2.0.0"
+    normalize-url "^3.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-normalize-whitespace@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
+  integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
+  dependencies:
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-ordered-values@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee"
+  integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==
+  dependencies:
+    cssnano-util-get-arguments "^4.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-reduce-initial@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df"
+  integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-api "^3.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+
+postcss-reduce-transforms@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29"
+  integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==
+  dependencies:
+    cssnano-util-get-match "^4.0.0"
+    has "^1.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+
+postcss-selector-parser@6.0.2, postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-selector-parser@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270"
+  integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==
+  dependencies:
+    dot-prop "^5.2.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-svgo@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
+  integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+  dependencies:
+    is-svg "^3.0.0"
+    postcss "^7.0.0"
+    postcss-value-parser "^3.0.0"
+    svgo "^1.0.0"
+
+postcss-unique-selectors@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac"
+  integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==
+  dependencies:
+    alphanum-sort "^1.0.0"
+    postcss "^7.0.0"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+  integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
+  integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
+
+postcss@6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.1.tgz#000dbd1f8eef217aa368b9a212c5fc40b2a8f3f2"
+  integrity sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=
+  dependencies:
+    chalk "^1.1.3"
+    source-map "^0.5.6"
+    supports-color "^3.2.3"
+
+postcss@^6.0.1, postcss@^6.0.9:
+  version "6.0.23"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
+  integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
+  dependencies:
+    chalk "^2.4.1"
+    source-map "^0.6.1"
+    supports-color "^5.4.0"
+
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27:
+  version "7.0.27"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
+  integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
+posthtml-parser@^0.4.0, posthtml-parser@^0.4.1:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.4.2.tgz#a132bbdf0cd4bc199d34f322f5c1599385d7c6c1"
+  integrity sha512-BUIorsYJTvS9UhXxPTzupIztOMVNPa/HtAm9KHni9z6qEfiJ1bpOBL5DfUOL9XAc3XkLIEzBzpph+Zbm4AdRAg==
+  dependencies:
+    htmlparser2 "^3.9.2"
+
+posthtml-render@^1.1.3, posthtml-render@^1.1.5:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.0.tgz#3df0c800a8bbb95af583a94748520469477addf4"
+  integrity sha512-dQB+hoAKDtnI94RZm/wxBUH9My8OJcXd0uhWmGh2c7tVtQ85A+OS3yCN3LNbFtPz3bViwBJXAeoi+CBGMXM0DA==
+
+posthtml@^0.11.2:
+  version "0.11.6"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.11.6.tgz#e349d51af7929d0683b9d8c3abd8166beecc90a8"
+  integrity sha512-C2hrAPzmRdpuL3iH0TDdQ6XCc9M7Dcc3zEW5BLerY65G4tWWszwv6nG/ksi6ul5i2mx22ubdljgktXCtNkydkw==
+  dependencies:
+    posthtml-parser "^0.4.1"
+    posthtml-render "^1.1.5"
+
+posthtml@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.12.0.tgz#6e2a2fcd774eaed1a419a95c5cc3a92b676a40a6"
+  integrity sha512-aNUEP/SfKUXAt+ghG51LC5MmafChBZeslVe/SSdfKIgLGUVRE68mrMF4V8XbH07ZifM91tCSuxY3eHIFLlecQw==
+  dependencies:
+    posthtml-parser "^0.4.1"
+    posthtml-render "^1.1.5"
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+pretty-hrtime@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
+  integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
+
+private@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+  integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+prop-types@^15.6.2, prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
+psl@^1.1.28:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
+  integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
+
+public-encrypt@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
+  integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+  dependencies:
+    bn.js "^4.1.0"
+    browserify-rsa "^4.0.0"
+    create-hash "^1.1.0"
+    parse-asn1 "^5.0.0"
+    randombytes "^2.0.1"
+    safe-buffer "^5.1.2"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+purgecss@^1.4.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-1.4.2.tgz#67ab50cb4f5c163fcefde56002467c974e577f41"
+  integrity sha512-hkOreFTgiyMHMmC2BxzdIw5DuC6kxAbP/gGOGd3MEsF3+5m69rIvUEPaxrnoUtfODTFKe9hcXjGwC6jcjoyhOw==
+  dependencies:
+    glob "^7.1.3"
+    postcss "^7.0.14"
+    postcss-selector-parser "^6.0.0"
+    yargs "^14.0.0"
+
+q@^1.1.2:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qs@^6.5.2:
+  version "6.9.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.2.tgz#a27b695006544a04bf0e6c6a7e8120778926d5bd"
+  integrity sha512-2eQ6zajpK7HwqrY1rRtGw5IZvjgtELXzJECaEDuzDFo2jjnIXpJSimzd4qflWZq6bLLi+Zgfj5eDrAzl/lptyg==
+
+qs@~6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring-es3@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+  integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+quote-stream@^1.0.1, quote-stream@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2"
+  integrity sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=
+  dependencies:
+    buffer-equal "0.0.1"
+    minimist "^1.1.3"
+    through2 "^2.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+  integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+  dependencies:
+    randombytes "^2.0.5"
+    safe-buffer "^5.1.0"
+
+range-parser@~1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+react-dom@^16.13.1:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
+  integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    scheduler "^0.19.1"
+
+react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-redux@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
+  integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    hoist-non-react-statics "^3.3.0"
+    loose-envify "^1.4.0"
+    prop-types "^15.7.2"
+    react-is "^16.9.0"
+
+react-router-dom@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
+  integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    history "^4.9.0"
+    loose-envify "^1.3.1"
+    prop-types "^15.6.2"
+    react-router "5.1.2"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+
+react-router@5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
+  integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    history "^4.9.0"
+    hoist-non-react-statics "^3.1.0"
+    loose-envify "^1.3.1"
+    mini-create-react-context "^0.3.0"
+    path-to-regexp "^1.7.0"
+    prop-types "^15.6.2"
+    react-is "^16.6.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+
+react@^16.13.1:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
+  integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+
+readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@^3.1.1:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+readdirp@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+  integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+  dependencies:
+    graceful-fs "^4.1.11"
+    micromatch "^3.1.10"
+    readable-stream "^2.0.2"
+
+reduce-css-calc@^2.1.6:
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
+  integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==
+  dependencies:
+    css-unit-converter "^1.1.1"
+    postcss-value-parser "^3.3.0"
+
+redux-devtools-extension@^2.13.8:
+  version "2.13.8"
+  resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
+  integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
+
+redux-immutable-state-invariant@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1"
+  integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==
+  dependencies:
+    invariant "^2.1.0"
+    json-stringify-safe "^5.0.1"
+
+redux-thunk@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
+  integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
+
+redux@^4.0.0:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+  integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+  dependencies:
+    loose-envify "^1.4.0"
+    symbol-observable "^1.2.0"
+
+regenerate-unicode-properties@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
+  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
+  dependencies:
+    regenerate "^1.4.0"
+
+regenerate@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+  integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+
+regenerator-runtime@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+  integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
+regenerator-runtime@^0.13.4:
+  version "0.13.5"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
+  integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+
+regenerator-transform@^0.14.2:
+  version "0.14.4"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
+  integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==
+  dependencies:
+    "@babel/runtime" "^7.8.4"
+    private "^0.1.8"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+  integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+  dependencies:
+    extend-shallow "^3.0.2"
+    safe-regex "^1.1.0"
+
+regexpu-core@^4.6.0, regexpu-core@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
+  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+  dependencies:
+    regenerate "^1.4.0"
+    regenerate-unicode-properties "^8.2.0"
+    regjsgen "^0.5.1"
+    regjsparser "^0.6.4"
+    unicode-match-property-ecmascript "^1.0.4"
+    unicode-match-property-value-ecmascript "^1.2.0"
+
+regjsgen@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
+  integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
+
+regjsparser@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
+  integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
+  dependencies:
+    jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+  integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+  integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+  dependencies:
+    lodash "^4.17.15"
+
+request-promise-native@^1.0.5:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
+  integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+  dependencies:
+    request-promise-core "1.1.3"
+    stealthy-require "^1.1.1"
+    tough-cookie "^2.3.3"
+
+request@^2.88.0:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+reselect@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+  integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
+resolve-from@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+  integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-pathname@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
+  integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
+
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@^1.1.5, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.4.0:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
+  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+  dependencies:
+    path-parse "^1.0.6"
+
+restore-cursor@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+  integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+  dependencies:
+    onetime "^2.0.0"
+    signal-exit "^3.0.2"
+
+ret@~0.1.10:
+  version "0.1.15"
+  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+  integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rgb-regex@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
+  integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE=
+
+rgba-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
+  integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+
+rimraf@^2.6.2:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+  integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+  integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+  dependencies:
+    ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@~1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+saxes@^3.1.9:
+  version "3.1.11"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
+  integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==
+  dependencies:
+    xmlchars "^2.1.1"
+
+scheduler@^0.19.1:
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
+  integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
+semver@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
+  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
+
+semver@^5.4.1, semver@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+send@0.17.1:
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
+  integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+  dependencies:
+    debug "2.6.9"
+    depd "~1.1.2"
+    destroy "~1.0.4"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "~1.7.2"
+    mime "1.6.0"
+    ms "2.1.1"
+    on-finished "~2.3.0"
+    range-parser "~1.2.1"
+    statuses "~1.5.0"
+
+serialize-to-js@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/serialize-to-js/-/serialize-to-js-3.1.1.tgz#b3e77d0568ee4a60bfe66287f991e104d3a1a4ac"
+  integrity sha512-F+NGU0UHMBO4Q965tjw7rvieNVjlH6Lqi2emq/Lc9LUURYJbiCzmpi4Cy1OOjjVPtxu0c+NE85LU6968Wko5ZA==
+
+serve-static@^1.12.4:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
+  integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.17.1"
+
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^2.0.0, set-value@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+  integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+shallow-copy@~0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
+  integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+signal-exit@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+  dependencies:
+    is-arrayish "^0.3.1"
+
+snapdragon-node@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+  integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+  dependencies:
+    define-property "^1.0.0"
+    isobject "^3.0.0"
+    snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+  integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+  dependencies:
+    kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+  integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+  dependencies:
+    base "^0.11.1"
+    debug "^2.2.0"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    map-cache "^0.2.2"
+    source-map "^0.5.6"
+    source-map-resolve "^0.5.0"
+    use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+  dependencies:
+    atob "^2.1.2"
+    decode-uri-component "^0.2.0"
+    resolve-url "^0.2.1"
+    source-map-url "^0.4.0"
+    urix "^0.1.0"
+
+source-map-support@~0.5.10, source-map-support@~0.5.12:
+  version "0.5.16"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
+  integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+  integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@^0.5.0, source-map@^0.5.6:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+  integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+split-string@^3.0.1, split-string@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+  integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+  dependencies:
+    extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
+    ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
+    jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
+    tweetnacl "~0.14.0"
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+static-eval@^2.0.0:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.5.tgz#f0782e66999c4b3651cda99d9ce59c507d188f71"
+  integrity sha512-nNbV6LbGtMBgv7e9LFkt5JV8RVlRsyJrphfAt9tOtBBW/SfnzZDf2KnS72an8e434A+9e/BmJuTxeGPvrAK7KA==
+  dependencies:
+    escodegen "^1.11.1"
+
+static-extend@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+  integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+  dependencies:
+    define-property "^0.2.5"
+    object-copy "^0.1.0"
+
+static-module@^2.2.0:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/static-module/-/static-module-2.2.5.tgz#bd40abceae33da6b7afb84a0e4329ff8852bfbbf"
+  integrity sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==
+  dependencies:
+    concat-stream "~1.6.0"
+    convert-source-map "^1.5.1"
+    duplexer2 "~0.1.4"
+    escodegen "~1.9.0"
+    falafel "^2.1.0"
+    has "^1.0.1"
+    magic-string "^0.22.4"
+    merge-source-map "1.0.4"
+    object-inspect "~1.4.0"
+    quote-stream "~1.0.2"
+    readable-stream "~2.3.3"
+    shallow-copy "~0.0.1"
+    static-eval "^2.0.0"
+    through2 "~2.0.3"
+
+"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+stream-browserify@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
+  integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
+  dependencies:
+    inherits "~2.0.1"
+    readable-stream "^2.0.2"
+
+stream-http@^2.7.2:
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
+  integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+  dependencies:
+    builtin-status-codes "^3.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.3.6"
+    to-arraybuffer "^1.0.0"
+    xtend "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+  dependencies:
+    emoji-regex "^7.0.1"
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^5.1.0"
+
+string.prototype.trimleft@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
+  integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==
+  dependencies:
+    define-properties "^1.1.3"
+    function-bind "^1.1.1"
+
+string.prototype.trimright@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
+  integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==
+  dependencies:
+    define-properties "^1.1.3"
+    function-bind "^1.1.1"
+
+string_decoder@^1.0.0, string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+  dependencies:
+    ansi-regex "^4.1.0"
+
+stylehacks@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
+  integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==
+  dependencies:
+    browserslist "^4.0.0"
+    postcss "^7.0.0"
+    postcss-selector-parser "^3.0.0"
+
+supports-color@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+  integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+
+supports-color@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+  integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=
+  dependencies:
+    has-flag "^1.0.0"
+
+supports-color@^5.3.0, supports-color@^5.4.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+  integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^1.0.0, svgo@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
+  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
+  dependencies:
+    chalk "^2.4.1"
+    coa "^2.0.2"
+    css-select "^2.0.0"
+    css-select-base-adapter "^0.1.1"
+    css-tree "1.0.0-alpha.37"
+    csso "^4.0.2"
+    js-yaml "^3.13.1"
+    mkdirp "~0.5.1"
+    object.values "^1.1.0"
+    sax "~1.2.4"
+    stable "^0.1.8"
+    unquote "~1.1.1"
+    util.promisify "~1.0.0"
+
+symbol-observable@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+  integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
+symbol-tree@^3.2.2:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+  integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+tailwindcss@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.2.0.tgz#5df317cebac4f3131f275d258a39da1ba3a0f291"
+  integrity sha512-CKvY0ytB3ze5qvynG7qv4XSpQtFNGPbu9pUn8qFdkqgD8Yo/vGss8mhzbqls44YCXTl4G62p3qVZBj45qrd6FQ==
+  dependencies:
+    autoprefixer "^9.4.5"
+    bytes "^3.0.0"
+    chalk "^3.0.0"
+    detective "^5.2.0"
+    fs-extra "^8.0.0"
+    lodash "^4.17.15"
+    node-emoji "^1.8.1"
+    normalize.css "^8.0.1"
+    postcss "^7.0.11"
+    postcss-functions "^3.0.0"
+    postcss-js "^2.0.0"
+    postcss-nested "^4.1.1"
+    postcss-selector-parser "^6.0.0"
+    pretty-hrtime "^1.0.3"
+    reduce-css-calc "^2.1.6"
+    resolve "^1.14.2"
+
+terser@^3.7.3:
+  version "3.17.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
+  integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==
+  dependencies:
+    commander "^2.19.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.10"
+
+terser@^4.3.9:
+  version "4.6.7"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72"
+  integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.12"
+
+through2@^2.0.0, through2@~2.0.3:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+  integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+  dependencies:
+    readable-stream "~2.3.6"
+    xtend "~4.0.1"
+
+timers-browserify@^2.0.4:
+  version "2.0.11"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
+  integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+  dependencies:
+    setimmediate "^1.0.4"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+tiny-inflate@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
+  integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
+tiny-invariant@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
+  integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+
+tiny-warning@^1.0.0, tiny-warning@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
+to-arraybuffer@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+  integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+to-fast-properties@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+  integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
+
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+  integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+  dependencies:
+    kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+  integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+  dependencies:
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+  integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+  dependencies:
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    regex-not "^1.0.2"
+    safe-regex "^1.1.0"
+
+toidentifier@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tr46@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+  integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+  dependencies:
+    punycode "^2.1.0"
+
+tty-browserify@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+  integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+typescript@^3.8.3:
+  version "3.8.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
+  integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
+
+uncss@^0.17.2:
+  version "0.17.3"
+  resolved "https://registry.yarnpkg.com/uncss/-/uncss-0.17.3.tgz#50fc1eb4ed573ffff763458d801cd86e4d69ea11"
+  integrity sha512-ksdDWl81YWvF/X14fOSw4iu8tESDHFIeyKIeDrK6GEVTQvqJc1WlOEXqostNwOCi3qAj++4EaLsdAgPmUbEyog==
+  dependencies:
+    commander "^2.20.0"
+    glob "^7.1.4"
+    is-absolute-url "^3.0.1"
+    is-html "^1.1.0"
+    jsdom "^14.1.0"
+    lodash "^4.17.15"
+    postcss "^7.0.17"
+    postcss-selector-parser "6.0.2"
+    request "^2.88.0"
+
+unicode-canonical-property-names-ecmascript@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
+  integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+
+unicode-match-property-ecmascript@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
+  integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+  dependencies:
+    unicode-canonical-property-names-ecmascript "^1.0.4"
+    unicode-property-aliases-ecmascript "^1.0.4"
+
+unicode-match-property-value-ecmascript@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
+  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
+
+unicode-property-aliases-ecmascript@^1.0.4:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
+  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
+
+unicode-trie@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-0.3.1.tgz#d671dddd89101a08bac37b6a5161010602052085"
+  integrity sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=
+  dependencies:
+    pako "^0.2.5"
+    tiny-inflate "^1.0.0"
+
+union-value@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+  dependencies:
+    arr-union "^3.1.0"
+    get-value "^2.0.6"
+    is-extendable "^0.1.1"
+    set-value "^2.0.1"
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unquote@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
+  integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+
+unset-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+  integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+  dependencies:
+    has-value "^0.3.1"
+    isobject "^3.0.0"
+
+upath@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
+  integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
+
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  dependencies:
+    punycode "^2.1.0"
+
+urix@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+  integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+  integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+use@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
+
+util@0.10.3:
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+  integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+  dependencies:
+    inherits "2.0.1"
+
+util@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
+  integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
+  dependencies:
+    inherits "2.0.3"
+
+uuid@^3.3.2:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+v8-compile-cache@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
+  integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
+
+value-equal@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
+  integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
+
+vendors@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+vlq@^0.2.2:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
+  integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
+
+vm-browserify@^1.0.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+  integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+
+w3c-hr-time@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+  integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+  dependencies:
+    browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
+  integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
+  dependencies:
+    domexception "^1.0.1"
+    webidl-conversions "^4.0.2"
+    xml-name-validator "^3.0.0"
+
+wcwidth@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+  integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
+  dependencies:
+    defaults "^1.0.3"
+
+webidl-conversions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+  integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+  dependencies:
+    iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+  integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wrap-ansi@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+  dependencies:
+    ansi-styles "^3.2.0"
+    string-width "^3.0.0"
+    strip-ansi "^5.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+ws@^5.1.1:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+  integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+  dependencies:
+    async-limiter "~1.0.0"
+
+ws@^6.1.2:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
+  integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+  dependencies:
+    async-limiter "~1.0.0"
+
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+  integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xmlchars@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
+xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+  integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yargs-parser@^15.0.1:
+  version "15.0.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3"
+  integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs@^14.0.0:
+  version "14.2.3"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414"
+  integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==
+  dependencies:
+    cliui "^5.0.0"
+    decamelize "^1.2.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^15.0.1"
diff --git a/users/wpcarro/website/sandbox/covid-uk/default.nix.ignore b/users/wpcarro/website/sandbox/covid-uk/default.nix.ignore
new file mode 100644
index 0000000000..309b1fa64b
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/default.nix.ignore
@@ -0,0 +1,16 @@
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "covid-uk";
+  buildInputs = [];
+  src = builtins.path { path = ./.; name = "covid-uk"; };
+  # TODO(wpcarro): Need to run `yarn install` somehow.
+  # TODO(wpcarro): Need to run `npx tailwindcss build styles.css -o output.css`.
+  buildPhase = ''
+    mkdir -p $out
+    mkdir -p $out/node_modules/chart.js/dist
+    cp $src/node_modules/chart.js/dist/Chart.bundle.min.js $out/node_modules/chart.js/dist
+    cp $src/index.html $src/output.css $out
+  '';
+  dontInstall = true;
+}
diff --git a/users/wpcarro/website/sandbox/covid-uk/index.html b/users/wpcarro/website/sandbox/covid-uk/index.html
new file mode 100644
index 0000000000..15769f7490
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/index.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>COVID-19 UK</title>
+  <link rel="stylesheet"  href="output.css">
+</head>
+<body class="container mx-auto py-10">
+  <div>
+    <h1 class="text-center">COVID-19 in the UK</h1>
+    <p>
+      Up until recently, I used a couple of resources (i.e.
+      <a href="https://multimedia.scmp.com/infographics/news/china/article/3047038/wuhan-virus/index.html">one</a>, 
+      <a href="https://www.worldometers.info/coronavirus/">two</a>) for tracking
+      an updated number of confirmed covid-19 cases.
+    </p>
+    <p>
+      Given the high speed at which the virus is spreading, I was having a
+      difficult time intuiting the shape of this growth. For example if today
+      the total number of confirmed cases for covid-19 in the UK was 500, I
+      could not remember if yesterday it was 450, 400, or 200.
+    </p>
+    <p>
+      Thankfully someone is <a
+      href="https://github.com/pomber/covid19">publishing this data</a> as a
+      timeseries database. I am currently living in London, so I decided to
+      chart the <u>daily number of confirmed covid-19 cases in the UK</u> to
+      better understand what is happening.
+    </p>
+  </div>
+  <canvas id="myChart" class="py-12"></canvas>
+  <script src="./node_modules/chart.js/dist/Chart.bundle.min.js"></script>
+  <script>
+   var timeseries =
+     fetch('https://pomber.github.io/covid19/timeseries.json')
+       .then(res => res.json())
+       .then(createChart);
+
+   function createChart(data) {
+     var uk = data["United Kingdom"];
+     var data = uk.map(x => x["confirmed"]);
+     var labels = uk.map(x => x["date"]);
+
+     var ctx = document.getElementById('myChart').getContext('2d');
+     var myChart = new Chart(ctx, {
+       type: 'line',
+       data: {
+         labels: labels,
+         datasets: [{
+           label: 'Number of confirmed COVID-19 cases in the U.K.',
+           data: data,
+           backgroundColor: 'rgba(255, 0, 100, 0.2)',
+           borderWidth: 3
+         }]
+       },
+       options: {
+         scales: {
+           yAxes: [{
+             ticks: {
+               beginAtZero: true
+             }
+           }]
+         }
+       }
+     });
+   }
+  </script>
+  <div>
+    <h2 class="text-center">Back of the envelope predictions</h2>
+    <p>
+      From what I have read, a population where 60% of its constituents have
+      been infected with covid-19 and have recovered is said to have "herd
+      immunity". Once a population has herd immunity, the rate at which the
+      virus spreads decreases.
+    </p>
+    <p>
+      Roughly 60M people live in the UK; 60% of 60M is around 40M. Before a
+      population reaches "herd immunity", the total number of <em>true
+      covid-19 cases</em> <u>doubles every five days</u>. Therefore in <u>fifty
+      days</u> you might expect the number of true cases to be <u>1000x
+      larger</u> than what it is today.
+    </p>
+    <p>
+      So if you think the total number of <em>true covid-19 cases</em>
+      <u>today</u> is 40,000 then you might expect the rate of growth to slow
+      down in a little less than two months.
+    </p>
+    <p>
+      Thank you for reading.
+    </p>
+  </div>
+  <footer class="pt-5 mb-8 lg:flex">
+    <a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://learn.wpcarro.dev">Learn</a>
+    <a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://blog.wpcarro.dev">Blog</a>
+    <a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://twitter.com/wpcarro">Twitter</a>
+    <a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://github.com/wpcarro">Github</a>
+  </footer>
+</body>
+</html>
diff --git a/users/wpcarro/website/sandbox/covid-uk/package.json b/users/wpcarro/website/sandbox/covid-uk/package.json
new file mode 100644
index 0000000000..939506c8cc
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "covid-uk",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "chart.js": "^2.9.3",
+    "tailwindcss": "^1.2.0"
+  }
+}
diff --git a/users/wpcarro/website/sandbox/covid-uk/shell.nix b/users/wpcarro/website/sandbox/covid-uk/shell.nix
new file mode 100644
index 0000000000..f918c4033e
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/shell.nix
@@ -0,0 +1,8 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    yarn
+    nodejs
+  ];
+}
diff --git a/users/wpcarro/website/sandbox/covid-uk/styles.css b/users/wpcarro/website/sandbox/covid-uk/styles.css
new file mode 100644
index 0000000000..1f48906d8f
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/styles.css
@@ -0,0 +1,28 @@
+@tailwind base;
+
+body {
+  @apply font-mono;
+}
+
+h1 {
+  @apply text-3xl mb-5 font-bold;
+}
+
+h2 {
+  @apply text-2xl mb-5 font-bold;
+}
+
+h3 {
+  @apply text-xl mb-5 font-bold;
+}
+
+a {
+  @apply text-blue-600 underline;
+}
+
+p {
+  @apply mt-2 mb-5;
+}
+
+@tailwind components;
+@tailwind utilities;
diff --git a/users/wpcarro/website/sandbox/covid-uk/tailwind.config.js b/users/wpcarro/website/sandbox/covid-uk/tailwind.config.js
new file mode 100644
index 0000000000..af829e20f9
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/tailwind.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+  theme: {
+    extend: {},
+  },
+  variants: {},
+  plugins: [],
+}
diff --git a/users/wpcarro/website/sandbox/covid-uk/yarn.lock b/users/wpcarro/website/sandbox/covid-uk/yarn.lock
new file mode 100644
index 0000000000..3e66831c9e
--- /dev/null
+++ b/users/wpcarro/website/sandbox/covid-uk/yarn.lock
@@ -0,0 +1,542 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+acorn-node@^1.6.1:
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+  integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
+  dependencies:
+    acorn "^7.0.0"
+    acorn-walk "^7.0.0"
+    xtend "^4.0.2"
+
+acorn-walk@^7.0.0:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
+  integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
+
+acorn@^7.0.0:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
+  integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+autoprefixer@^9.4.5:
+  version "9.7.4"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378"
+  integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g==
+  dependencies:
+    browserslist "^4.8.3"
+    caniuse-lite "^1.0.30001020"
+    chalk "^2.4.2"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^7.0.26"
+    postcss-value-parser "^4.0.2"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+browserslist@^4.8.3:
+  version "4.10.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.10.0.tgz#f179737913eaf0d2b98e4926ac1ca6a15cbcc6a9"
+  integrity sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA==
+  dependencies:
+    caniuse-lite "^1.0.30001035"
+    electron-to-chromium "^1.3.378"
+    node-releases "^1.1.52"
+    pkg-up "^3.1.0"
+
+bytes@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+camelcase-css@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+  integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035:
+  version "1.0.30001035"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz#2bb53b8aa4716b2ed08e088d4dc816a5fe089a1e"
+  integrity sha512-C1ZxgkuA4/bUEdMbU5WrGY4+UhMFFiXrgNAfxiMIqWgFTWfv/xsZCS2xEHT2LMq7xAZfuAnu6mcqyDl0ZR6wLQ==
+
+chalk@^2.4.1, chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+  integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chart.js@^2.9.3:
+  version "2.9.3"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
+  integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
+  dependencies:
+    chartjs-color "^2.1.0"
+    moment "^2.10.2"
+
+chartjs-color-string@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
+  integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
+  dependencies:
+    color-name "^1.0.0"
+
+chartjs-color@^2.1.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
+  integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
+  dependencies:
+    chartjs-color-string "^0.6.0"
+    color-convert "^1.9.3"
+
+color-convert@^1.9.0, color-convert@^1.9.3:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+css-unit-converter@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
+  integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+defined@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+  integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
+
+detective@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
+  integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==
+  dependencies:
+    acorn-node "^1.6.1"
+    defined "^1.0.0"
+    minimist "^1.1.1"
+
+electron-to-chromium@^1.3.378:
+  version "1.3.379"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.379.tgz#81dc5e82a3e72bbb830d93e15bc35eda2bbc910e"
+  integrity sha512-NK9DBBYEBb5f9D7zXI0hiE941gq3wkBeQmXs1ingigA/jnTg5mhwY2Z5egwA+ZI8OLGKCx0h1Cl8/xeuIBuLlg==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+fs-extra@^8.0.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+glob@^7.1.2:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    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"
+
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
+  integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+lodash.toarray@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
+  integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE=
+
+lodash@^4.17.15:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.1.1:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+moment@^2.10.2:
+  version "2.24.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+
+node-emoji@^1.8.1:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
+  integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==
+  dependencies:
+    lodash.toarray "^4.4.0"
+
+node-releases@^1.1.52:
+  version "1.1.52"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9"
+  integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ==
+  dependencies:
+    semver "^6.3.0"
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+  integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+normalize.css@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+  integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
+
+num2fraction@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+  integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
+object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+p-limit@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
+  integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+pkg-up@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
+  integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
+  dependencies:
+    find-up "^3.0.0"
+
+postcss-functions@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-functions/-/postcss-functions-3.0.0.tgz#0e94d01444700a481de20de4d55fb2640564250e"
+  integrity sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=
+  dependencies:
+    glob "^7.1.2"
+    object-assign "^4.1.1"
+    postcss "^6.0.9"
+    postcss-value-parser "^3.3.0"
+
+postcss-js@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9"
+  integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==
+  dependencies:
+    camelcase-css "^2.0.1"
+    postcss "^7.0.18"
+
+postcss-nested@^4.1.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.1.tgz#4bc2e5b35e3b1e481ff81e23b700da7f82a8b248"
+  integrity sha512-AMayXX8tS0HCp4O4lolp4ygj9wBn32DJWXvG6gCv+ZvJrEa00GUxJcJEEzMh87BIe6FrWdYkpR2cuyqHKrxmXw==
+  dependencies:
+    postcss "^7.0.21"
+    postcss-selector-parser "^6.0.2"
+
+postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-value-parser@^3.3.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+  integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
+  integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
+
+postcss@^6.0.9:
+  version "6.0.23"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
+  integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
+  dependencies:
+    chalk "^2.4.1"
+    source-map "^0.6.1"
+    supports-color "^5.4.0"
+
+postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.26:
+  version "7.0.27"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
+  integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
+pretty-hrtime@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
+  integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
+
+reduce-css-calc@^2.1.6:
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
+  integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==
+  dependencies:
+    css-unit-converter "^1.1.1"
+    postcss-value-parser "^3.3.0"
+
+resolve@^1.14.2:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
+  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+  dependencies:
+    path-parse "^1.0.6"
+
+semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+supports-color@^5.3.0, supports-color@^5.4.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+  integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  dependencies:
+    has-flag "^4.0.0"
+
+tailwindcss@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.2.0.tgz#5df317cebac4f3131f275d258a39da1ba3a0f291"
+  integrity sha512-CKvY0ytB3ze5qvynG7qv4XSpQtFNGPbu9pUn8qFdkqgD8Yo/vGss8mhzbqls44YCXTl4G62p3qVZBj45qrd6FQ==
+  dependencies:
+    autoprefixer "^9.4.5"
+    bytes "^3.0.0"
+    chalk "^3.0.0"
+    detective "^5.2.0"
+    fs-extra "^8.0.0"
+    lodash "^4.17.15"
+    node-emoji "^1.8.1"
+    normalize.css "^8.0.1"
+    postcss "^7.0.11"
+    postcss-functions "^3.0.0"
+    postcss-js "^2.0.0"
+    postcss-nested "^4.1.1"
+    postcss-selector-parser "^6.0.0"
+    pretty-hrtime "^1.0.3"
+    reduce-css-calc "^2.1.6"
+    resolve "^1.14.2"
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+xtend@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
diff --git a/users/wpcarro/website/sandbox/default.nix.ignore b/users/wpcarro/website/sandbox/default.nix.ignore
new file mode 100644
index 0000000000..d7b4940a55
--- /dev/null
+++ b/users/wpcarro/website/sandbox/default.nix.ignore
@@ -0,0 +1,13 @@
+{ pkgs, ... }:
+
+pkgs.stdenv.mkDerivation {
+  name = "covid-uk";
+  buildInputs = [];
+  src = builtins.path { path = ./.; name = "sandbox"; };
+  buildPhase = ''
+    mkdir -p $out
+    cp $src/index.html $out
+    cp -r ${depot.users.wpcarro.website.sandbox.covid-uk} $out/covid-uk
+  '';
+  dontInstall = true;
+}
diff --git a/users/wpcarro/website/sandbox/github-issues-service/README.md b/users/wpcarro/website/sandbox/github-issues-service/README.md
new file mode 100644
index 0000000000..2af8600143
--- /dev/null
+++ b/users/wpcarro/website/sandbox/github-issues-service/README.md
@@ -0,0 +1,28 @@
+# Github Issues Service (GIS)
+
+> 'Cause I got issues. But you got 'em too...
+> - [Issues by Julia Michaels][issues]
+
+You have a website and your users want to request features or report bugs. How
+do they do this?
+
+Our robot, GIS, can help you. GIS adds a widget to your website that allows
+users to easily request features and report bugs.
+
+## Getting Started
+
+If Github is hosting your website's source code, you're ready to start using
+GIS. GIS works with public and private repositories.
+
+Let's adopt Github's notion of "issues" to group feature requests and bug
+reports together. When users click the GIS widget to create an issue, GIS
+displays a modal form that the user completes. When the user submits the form,
+GIS creates an issue on your Github repository. Now your team can use all of
+Github's rich issue-tracking tools to manage your issues.
+
+## Installation
+
+To add GIS to your website, register your Github repository with us and we'll
+give you a snippet to add to your website's HTML. It's that simple.
+
+[issues]: https://www.youtube.com/watch?v=9Ke4480MicU
diff --git a/users/wpcarro/website/sandbox/index.html b/users/wpcarro/website/sandbox/index.html
new file mode 100644
index 0000000000..ecd5475af2
--- /dev/null
+++ b/users/wpcarro/website/sandbox/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>sandbox.wpcarro.dev</title>
+  </head>
+  <body>
+    <h1>Projects</h1>
+    <ul>
+      <li>
+        <a href="/covid-uk">COVID-19 in the UK</a>
+      </li>
+    </ul>
+  </body>
+</html>
diff --git a/users/wpcarro/website/sandbox/learnpianochords/.envrc b/users/wpcarro/website/sandbox/learnpianochords/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/website/sandbox/learnpianochords/.gitignore b/users/wpcarro/website/sandbox/learnpianochords/.gitignore
new file mode 100644
index 0000000000..0c1c258f65
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/.gitignore
@@ -0,0 +1,2 @@
+/elm-stuff
+/Main.min.js
diff --git a/users/wpcarro/website/sandbox/learnpianochords/README.md b/users/wpcarro/website/sandbox/learnpianochords/README.md
new file mode 100644
index 0000000000..2527f4b963
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/README.md
@@ -0,0 +1,57 @@
+# Learn Piano Chords (LPC)
+
+Are you a musician looking for a more effective way to improve your craft? Maybe
+you're a music teacher looking to create useful exercises to give your students.
+
+Studying music theory can be a fruitful undertaking, but it can often overwhelm
+or bore students. I think that if practicing is enjoyable, students will
+practice more. Practice doesn't make perfect; *perfect* practice makes perfect.
+Learn Piano Chords is a web app that lowers the barrier to practicing and
+internalizing music theory.
+
+## How does it work?
+
+1. Grab a cell phone or a laptop and your instrument.
+2. Open a web browser and visit the Learn Piano Chords app (URL and app
+   forthcoming).
+6. Set the tempo (i.e. pace) at which you would like to practice.
+4. Set the target duration of your session.
+5. Select the key(s) and chord(s) you would like to practice.
+7. LPC will display chords at various rhythmic intervals during your practice
+   session. It is your job to play these chords in time before the next chord
+   appears.
+
+## Highlights
+
+Here are some useful features of LPC:
+- Tempo: Set the rate at which LPC displays chords.
+- Predefined practice sessions: LPC offers users a few practice sessions to get
+  users started. The goal, however, is to teach users to create their own
+  bespoke practice sessions. LPC aims to foster a community of practitioners who
+  curate and share their practice sessions.
+- Whitelist / blacklist: Construct the set of chords you would like to
+  practice. Let's say you only want to practice triads in the keys of F, C, and
+  G. Would you also like to avoid diminished chords? Or maybe you *only* want to
+  practice major-7th chords for *all* keys. LPC supports all of these scenarios
+  and many others. You can save these chord configurations to reuse them at any
+  time. You can also share chord configurations with other LPC users if you find
+  the practice useful.
+- Inversions: Every chord has inversions. For instance, every triad (i.e. chord
+  composed of three notes) has three inversions: root, second, and third
+  positions. LPC acknowledges all of the positions in which chords may appear
+  and helps you study all, some, or none of these inversions.
+- Harmony: LPC understands basic harmony and can sort the chords you would like
+  to train in various harmonious permutations.
+- Chaos-mode: Feeling confident? Throw the classical notions of harmony to the
+  wayside and use LPC in "chaos-mode" where LPC samples randomly from the Circle
+  of Fifths.
+
+## Developing
+
+If you're interested in contributing, the following will create an environment
+in which you can develop:
+
+```shell
+$ nix-shell
+$ elm-live -- src/Main.elm --output=elm.js
+```
diff --git a/users/wpcarro/website/sandbox/learnpianochords/default.nix b/users/wpcarro/website/sandbox/learnpianochords/default.nix
new file mode 100644
index 0000000000..7cfdf7c451
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/default.nix
@@ -0,0 +1,63 @@
+{ pkgs, ... }:
+
+with pkgs;
+
+let
+  mkDerivation =
+    { srcs ? ./elm-srcs.nix
+    , src
+    , name
+    , srcdir ? "./src"
+    , targets ? [ ]
+    , registryDat ? ./registry.dat
+    , outputJavaScript ? false
+    }:
+    stdenv.mkDerivation {
+      inherit name src;
+
+      buildInputs = [ elmPackages.elm ]
+        ++ lib.optional outputJavaScript nodePackages.uglify-js;
+
+      buildPhase = elmPackages.fetchElmDeps {
+        elmPackages = import srcs;
+        elmVersion = "0.19.1";
+        inherit registryDat;
+      };
+
+      installPhase =
+        let
+          elmfile = module: "${srcdir}/${builtins.replaceStrings ["."] ["/"] module}.elm";
+          extension = if outputJavaScript then "js" else "html";
+        in
+        ''
+          mkdir -p $out/share/doc
+          ${lib.concatStrings (map (module: ''
+            echo "compiling ${elmfile module}"
+            elm make ${elmfile module} --output $out/${module}.${extension} --docs $out/share/doc/${module}.json
+            ${lib.optionalString outputJavaScript ''
+              echo "minifying ${elmfile module}"
+              uglifyjs $out/${module}.${extension} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \
+                  | uglifyjs --mangle --output $out/${module}.min.${extension}
+            ''}
+          '') targets)}
+        '';
+    };
+  mainDotElm = mkDerivation {
+    name = "elm-app-0.1.0";
+    srcs = ./elm-srcs.nix;
+    src = builtins.path { path = ./.; name = "learnpianochords"; };
+    targets = [ "Main" ];
+    srcdir = "./src";
+    outputJavaScript = true;
+  };
+in
+stdenv.mkDerivation {
+  name = "learn-piano-chords";
+  buildInputs = [ ];
+  src = builtins.path { path = ./.; name = "learnpianochords"; };
+  buildPhase = ''
+    mkdir -p $out
+    cp index.html output.css ${mainDotElm}/Main.min.js $out
+  '';
+  dontInstall = true;
+}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/elm-srcs.nix b/users/wpcarro/website/sandbox/learnpianochords/elm-srcs.nix
new file mode 100644
index 0000000000..c62262e683
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/elm-srcs.nix
@@ -0,0 +1,67 @@
+{
+
+  "elm-community/maybe-extra" = {
+    sha256 = "0qslmgswa625d218djd3p62pnqcrz38f5p558mbjl6kc1ss0kzv3";
+    version = "5.2.0";
+  };
+
+  "elm/html" = {
+    sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
+    version = "1.0.0";
+  };
+
+  "elm-community/random-extra" = {
+    sha256 = "1dg2nz77w2cvp16xazbdsxkkw0xc9ycqpkd032faqdyky6gmz9g6";
+    version = "3.1.0";
+  };
+
+  "elm/svg" = {
+    sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
+    version = "1.0.1";
+  };
+
+  "elm/browser" = {
+    sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13";
+    version = "1.0.2";
+  };
+
+  "elm/core" = {
+    sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
+    version = "1.0.5";
+  };
+
+  "elm-community/list-extra" = {
+    sha256 = "1ayv3148drynqnxdfwpjxal8vwzgsjqanjg7yxp6lhdcbkxgd3vd";
+    version = "8.2.3";
+  };
+
+  "elm/random" = {
+    sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
+    version = "1.0.0";
+  };
+
+  "elm/time" = {
+    sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
+    version = "1.0.0";
+  };
+
+  "elm/json" = {
+    sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh";
+    version = "1.1.3";
+  };
+
+  "owanturist/elm-union-find" = {
+    sha256 = "13gm7msnp0gr1lqia5m7m4lhy3m6kvjg37d304whb3psn88wqhj5";
+    version = "1.0.0";
+  };
+
+  "elm/url" = {
+    sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
+    version = "1.0.0";
+  };
+
+  "elm/virtual-dom" = {
+    sha256 = "0q1v5gi4g336bzz1lgwpn5b1639lrn63d8y6k6pimcyismp2i1yg";
+    version = "1.0.2";
+  };
+}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/elm.json b/users/wpcarro/website/sandbox/learnpianochords/elm.json
new file mode 100644
index 0000000000..a95f80408e
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/elm.json
@@ -0,0 +1,30 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/random": "1.0.0",
+            "elm/svg": "1.0.1",
+            "elm/time": "1.0.0",
+            "elm-community/list-extra": "8.2.3",
+            "elm-community/maybe-extra": "5.2.0",
+            "elm-community/random-extra": "3.1.0"
+        },
+        "indirect": {
+            "elm/json": "1.1.3",
+            "elm/url": "1.0.0",
+            "elm/virtual-dom": "1.0.2",
+            "owanturist/elm-union-find": "1.0.0"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/ideas.org b/users/wpcarro/website/sandbox/learnpianochords/ideas.org
new file mode 100644
index 0000000000..4c2372280e
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/ideas.org
@@ -0,0 +1,3 @@
+* Support a frequency table of all of the chords
+* Support using spaced-repetition to help populate the frequency table of chords
+* If doing a frequency table, support left and right hands
diff --git a/users/wpcarro/website/sandbox/learnpianochords/index.css b/users/wpcarro/website/sandbox/learnpianochords/index.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/users/wpcarro/website/sandbox/learnpianochords/index.html b/users/wpcarro/website/sandbox/learnpianochords/index.html
new file mode 100644
index 0000000000..5687c29eb7
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Learn Piano Chords</title>
+    <link rel="stylesheet" href="./output.css" />
+    <script src="./Main.min.js"></script>
+  </head>
+  <body class="font-serif">
+    <div id="mount"></div>
+    <script>
+     Elm.Main.init({node: document.getElementById("mount")});
+    </script>
+  </body>
+</html>
diff --git a/users/wpcarro/website/sandbox/learnpianochords/output.css b/users/wpcarro/website/sandbox/learnpianochords/output.css
new file mode 100644
index 0000000000..b522419aa3
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/output.css
@@ -0,0 +1,103571 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  -webkit-text-decoration: underline dotted;
+          text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}
+
+/**
+ * Manually forked from SUIT CSS Base: https://github.com/suitcss/base
+ * A thin layer on top of normalize.css that provides a starting point more
+ * suitable for web applications.
+ */
+
+/**
+ * Removes the default spacing and border for appropriate elements.
+ */
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+  margin: 0;
+}
+
+button {
+  background-color: transparent;
+  background-image: none;
+}
+
+/**
+ * Work around a Firefox/IE bug where the transparent `button` background
+ * results in a loss of the default `button` focus styles.
+ */
+
+button:focus {
+  outline: 1px dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+}
+
+fieldset {
+  margin: 0;
+  padding: 0;
+}
+
+ol,
+ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+/**
+ * Tailwind custom reset styles
+ */
+
+/**
+ * 1. Use the user's configured `sans` font-family (with Tailwind's default
+ *    sans-serif font stack as a fallback) as a sane default.
+ * 2. Use Tailwind's default "normal" line-height so the user isn't forced
+ *    to override it to ensure consistency even when using the default theme.
+ */
+
+html {
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */
+  line-height: 1.5; /* 2 */
+}
+
+/**
+ * 1. Prevent padding and border from affecting element width.
+ *
+ *    We used to set this in the html element and inherit from
+ *    the parent element for everything else. This caused issues
+ *    in shadow-dom-enhanced elements like <details> where the content
+ *    is wrapped by a div with box-sizing set to `content-box`.
+ *
+ *    https://github.com/mozdevs/cssremedy/issues/4
+ *
+ *
+ * 2. Allow adding a border to an element by just adding a border-width.
+ *
+ *    By default, the way the browser specifies that an element should have no
+ *    border is by setting it's border-style to `none` in the user-agent
+ *    stylesheet.
+ *
+ *    In order to easily add borders to elements by just setting the `border-width`
+ *    property, we change the default border-style for all elements to `solid`, and
+ *    use border-width to hide them instead. This way our `border` utilities only
+ *    need to set the `border-width` property instead of the entire `border`
+ *    shorthand, making our border utilities much more straightforward to compose.
+ *
+ *    https://github.com/tailwindcss/tailwindcss/pull/116
+ */
+
+*,
+::before,
+::after {
+  box-sizing: border-box; /* 1 */
+  border-width: 0; /* 2 */
+  border-style: solid; /* 2 */
+  border-color: #e2e8f0; /* 2 */
+}
+
+/*
+ * Ensure horizontal rules are visible by default
+ */
+
+hr {
+  border-top-width: 1px;
+}
+
+/**
+ * Undo the `border-style: none` reset that Normalize applies to images so that
+ * our `border-{width}` utilities have the expected effect.
+ *
+ * The Normalize reset is unnecessary for us since we default the border-width
+ * to 0 on all elements.
+ *
+ * https://github.com/tailwindcss/tailwindcss/issues/362
+ */
+
+img {
+  border-style: solid;
+}
+
+textarea {
+  resize: vertical;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+  color: #a0aec0;
+}
+
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
+  color: #a0aec0;
+}
+
+input::placeholder,
+textarea::placeholder {
+  color: #a0aec0;
+}
+
+button,
+[role="button"] {
+  cursor: pointer;
+}
+
+table {
+  border-collapse: collapse;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: inherit;
+  font-weight: inherit;
+}
+
+/**
+ * Reset links to optimize for opt-in styling instead of
+ * opt-out.
+ */
+
+a {
+  color: inherit;
+  text-decoration: inherit;
+}
+
+/**
+ * Reset form element properties that are easy to forget to
+ * style explicitly so you don't inadvertently introduce
+ * styles that deviate from your design system. These styles
+ * supplement a partial reset that is already applied by
+ * normalize.css.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  padding: 0;
+  line-height: inherit;
+  color: inherit;
+}
+
+/**
+ * Use the configured 'mono' font family for elements that
+ * are expected to be rendered with a monospace font, falling
+ * back to the system monospace stack if there is no configured
+ * 'mono' font family.
+ */
+
+pre,
+code,
+kbd,
+samp {
+  font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+/**
+ * Make replaced elements `display: block` by default as that's
+ * the behavior you want almost all of the time. Inspired by
+ * CSS Remedy, with `svg` added as well.
+ *
+ * https://github.com/mozdevs/cssremedy/issues/14
+ */
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+  display: block;
+  vertical-align: middle;
+}
+
+/**
+ * Constrain images and videos to the parent width and preserve
+ * their instrinsic aspect ratio.
+ *
+ * https://github.com/mozdevs/cssremedy/issues/14
+ */
+
+img,
+video {
+  max-width: 100%;
+  height: auto;
+}
+
+.container {
+  width: 100%;
+}
+
+@media (min-width: 640px) {
+  .container {
+    max-width: 640px;
+  }
+}
+
+@media (min-width: 768px) {
+  .container {
+    max-width: 768px;
+  }
+}
+
+@media (min-width: 1024px) {
+  .container {
+    max-width: 1024px;
+  }
+}
+
+@media (min-width: 1280px) {
+  .container {
+    max-width: 1280px;
+  }
+}
+
+.space-y-0 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0px * var(--space-y-reverse));
+}
+
+.space-x-0 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0px * var(--space-x-reverse));
+  margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-1 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0.25rem * var(--space-y-reverse));
+}
+
+.space-x-1 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0.25rem * var(--space-x-reverse));
+  margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-2 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0.5rem * var(--space-y-reverse));
+}
+
+.space-x-2 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0.5rem * var(--space-x-reverse));
+  margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-3 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(0.75rem * var(--space-y-reverse));
+}
+
+.space-x-3 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(0.75rem * var(--space-x-reverse));
+  margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-4 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1rem * var(--space-y-reverse));
+}
+
+.space-x-4 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1rem * var(--space-x-reverse));
+  margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-5 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1.25rem * var(--space-y-reverse));
+}
+
+.space-x-5 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1.25rem * var(--space-x-reverse));
+  margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-6 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1.5rem * var(--space-y-reverse));
+}
+
+.space-x-6 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1.5rem * var(--space-x-reverse));
+  margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-8 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(2rem * var(--space-y-reverse));
+}
+
+.space-x-8 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(2rem * var(--space-x-reverse));
+  margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-10 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(2.5rem * var(--space-y-reverse));
+}
+
+.space-x-10 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(2.5rem * var(--space-x-reverse));
+  margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-12 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(3rem * var(--space-y-reverse));
+}
+
+.space-x-12 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(3rem * var(--space-x-reverse));
+  margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-16 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(4rem * var(--space-y-reverse));
+}
+
+.space-x-16 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(4rem * var(--space-x-reverse));
+  margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-20 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(5rem * var(--space-y-reverse));
+}
+
+.space-x-20 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(5rem * var(--space-x-reverse));
+  margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-24 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(6rem * var(--space-y-reverse));
+}
+
+.space-x-24 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(6rem * var(--space-x-reverse));
+  margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-32 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(8rem * var(--space-y-reverse));
+}
+
+.space-x-32 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(8rem * var(--space-x-reverse));
+  margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-40 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(10rem * var(--space-y-reverse));
+}
+
+.space-x-40 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(10rem * var(--space-x-reverse));
+  margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-48 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(12rem * var(--space-y-reverse));
+}
+
+.space-x-48 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(12rem * var(--space-x-reverse));
+  margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-56 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(14rem * var(--space-y-reverse));
+}
+
+.space-x-56 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(14rem * var(--space-x-reverse));
+  margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-64 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(16rem * var(--space-y-reverse));
+}
+
+.space-x-64 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(16rem * var(--space-x-reverse));
+  margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-px > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(1px * var(--space-y-reverse));
+}
+
+.space-x-px > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(1px * var(--space-x-reverse));
+  margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-1 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+}
+
+.-space-x-1 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-0.25rem * var(--space-x-reverse));
+  margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-2 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+}
+
+.-space-x-2 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-0.5rem * var(--space-x-reverse));
+  margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-3 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+}
+
+.-space-x-3 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-0.75rem * var(--space-x-reverse));
+  margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-4 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1rem * var(--space-y-reverse));
+}
+
+.-space-x-4 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1rem * var(--space-x-reverse));
+  margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-5 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+}
+
+.-space-x-5 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1.25rem * var(--space-x-reverse));
+  margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-6 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+}
+
+.-space-x-6 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1.5rem * var(--space-x-reverse));
+  margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-8 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-2rem * var(--space-y-reverse));
+}
+
+.-space-x-8 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-2rem * var(--space-x-reverse));
+  margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-10 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+}
+
+.-space-x-10 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-2.5rem * var(--space-x-reverse));
+  margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-12 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-3rem * var(--space-y-reverse));
+}
+
+.-space-x-12 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-3rem * var(--space-x-reverse));
+  margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-16 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-4rem * var(--space-y-reverse));
+}
+
+.-space-x-16 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-4rem * var(--space-x-reverse));
+  margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-20 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-5rem * var(--space-y-reverse));
+}
+
+.-space-x-20 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-5rem * var(--space-x-reverse));
+  margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-24 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-6rem * var(--space-y-reverse));
+}
+
+.-space-x-24 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-6rem * var(--space-x-reverse));
+  margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-32 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-8rem * var(--space-y-reverse));
+}
+
+.-space-x-32 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-8rem * var(--space-x-reverse));
+  margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-40 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-10rem * var(--space-y-reverse));
+}
+
+.-space-x-40 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-10rem * var(--space-x-reverse));
+  margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-48 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-12rem * var(--space-y-reverse));
+}
+
+.-space-x-48 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-12rem * var(--space-x-reverse));
+  margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-56 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-14rem * var(--space-y-reverse));
+}
+
+.-space-x-56 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-14rem * var(--space-x-reverse));
+  margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-64 > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-16rem * var(--space-y-reverse));
+}
+
+.-space-x-64 > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-16rem * var(--space-x-reverse));
+  margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+}
+
+.-space-y-px > :not(template) ~ :not(template) {
+  --space-y-reverse: 0;
+  margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+  margin-bottom: calc(-1px * var(--space-y-reverse));
+}
+
+.-space-x-px > :not(template) ~ :not(template) {
+  --space-x-reverse: 0;
+  margin-right: calc(-1px * var(--space-x-reverse));
+  margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+}
+
+.space-y-reverse > :not(template) ~ :not(template) {
+  --space-y-reverse: 1;
+}
+
+.space-x-reverse > :not(template) ~ :not(template) {
+  --space-x-reverse: 1;
+}
+
+.divide-y-0 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(0px * var(--divide-y-reverse));
+}
+
+.divide-x-0 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(0px * var(--divide-x-reverse));
+  border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-2 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(2px * var(--divide-y-reverse));
+}
+
+.divide-x-2 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(2px * var(--divide-x-reverse));
+  border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-4 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(4px * var(--divide-y-reverse));
+}
+
+.divide-x-4 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(4px * var(--divide-x-reverse));
+  border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-8 > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(8px * var(--divide-y-reverse));
+}
+
+.divide-x-8 > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(8px * var(--divide-x-reverse));
+  border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y > :not(template) ~ :not(template) {
+  --divide-y-reverse: 0;
+  border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+  border-bottom-width: calc(1px * var(--divide-y-reverse));
+}
+
+.divide-x > :not(template) ~ :not(template) {
+  --divide-x-reverse: 0;
+  border-right-width: calc(1px * var(--divide-x-reverse));
+  border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+}
+
+.divide-y-reverse > :not(template) ~ :not(template) {
+  --divide-y-reverse: 1;
+}
+
+.divide-x-reverse > :not(template) ~ :not(template) {
+  --divide-x-reverse: 1;
+}
+
+.divide-transparent > :not(template) ~ :not(template) {
+  border-color: transparent;
+}
+
+.divide-current > :not(template) ~ :not(template) {
+  border-color: currentColor;
+}
+
+.divide-black > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--divide-opacity));
+}
+
+.divide-white > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--divide-opacity));
+}
+
+.divide-gray-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--divide-opacity));
+}
+
+.divide-gray-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--divide-opacity));
+}
+
+.divide-gray-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--divide-opacity));
+}
+
+.divide-gray-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--divide-opacity));
+}
+
+.divide-gray-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--divide-opacity));
+}
+
+.divide-gray-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--divide-opacity));
+}
+
+.divide-gray-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--divide-opacity));
+}
+
+.divide-gray-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--divide-opacity));
+}
+
+.divide-gray-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--divide-opacity));
+}
+
+.divide-red-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--divide-opacity));
+}
+
+.divide-red-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--divide-opacity));
+}
+
+.divide-red-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--divide-opacity));
+}
+
+.divide-red-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--divide-opacity));
+}
+
+.divide-red-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--divide-opacity));
+}
+
+.divide-red-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--divide-opacity));
+}
+
+.divide-red-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--divide-opacity));
+}
+
+.divide-red-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--divide-opacity));
+}
+
+.divide-red-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--divide-opacity));
+}
+
+.divide-orange-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--divide-opacity));
+}
+
+.divide-orange-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--divide-opacity));
+}
+
+.divide-orange-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--divide-opacity));
+}
+
+.divide-orange-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--divide-opacity));
+}
+
+.divide-orange-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--divide-opacity));
+}
+
+.divide-orange-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--divide-opacity));
+}
+
+.divide-orange-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--divide-opacity));
+}
+
+.divide-orange-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--divide-opacity));
+}
+
+.divide-orange-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--divide-opacity));
+}
+
+.divide-yellow-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--divide-opacity));
+}
+
+.divide-yellow-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--divide-opacity));
+}
+
+.divide-yellow-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--divide-opacity));
+}
+
+.divide-yellow-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--divide-opacity));
+}
+
+.divide-yellow-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--divide-opacity));
+}
+
+.divide-yellow-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--divide-opacity));
+}
+
+.divide-yellow-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--divide-opacity));
+}
+
+.divide-yellow-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--divide-opacity));
+}
+
+.divide-yellow-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--divide-opacity));
+}
+
+.divide-green-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--divide-opacity));
+}
+
+.divide-green-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--divide-opacity));
+}
+
+.divide-green-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--divide-opacity));
+}
+
+.divide-green-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--divide-opacity));
+}
+
+.divide-green-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--divide-opacity));
+}
+
+.divide-green-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--divide-opacity));
+}
+
+.divide-green-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--divide-opacity));
+}
+
+.divide-green-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--divide-opacity));
+}
+
+.divide-green-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--divide-opacity));
+}
+
+.divide-teal-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--divide-opacity));
+}
+
+.divide-teal-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--divide-opacity));
+}
+
+.divide-teal-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--divide-opacity));
+}
+
+.divide-teal-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--divide-opacity));
+}
+
+.divide-teal-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--divide-opacity));
+}
+
+.divide-teal-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--divide-opacity));
+}
+
+.divide-teal-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--divide-opacity));
+}
+
+.divide-teal-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--divide-opacity));
+}
+
+.divide-teal-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--divide-opacity));
+}
+
+.divide-blue-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--divide-opacity));
+}
+
+.divide-blue-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--divide-opacity));
+}
+
+.divide-blue-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--divide-opacity));
+}
+
+.divide-blue-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--divide-opacity));
+}
+
+.divide-blue-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--divide-opacity));
+}
+
+.divide-blue-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--divide-opacity));
+}
+
+.divide-blue-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--divide-opacity));
+}
+
+.divide-blue-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--divide-opacity));
+}
+
+.divide-blue-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--divide-opacity));
+}
+
+.divide-indigo-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--divide-opacity));
+}
+
+.divide-indigo-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--divide-opacity));
+}
+
+.divide-indigo-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--divide-opacity));
+}
+
+.divide-indigo-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--divide-opacity));
+}
+
+.divide-indigo-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--divide-opacity));
+}
+
+.divide-indigo-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--divide-opacity));
+}
+
+.divide-indigo-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--divide-opacity));
+}
+
+.divide-indigo-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--divide-opacity));
+}
+
+.divide-indigo-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--divide-opacity));
+}
+
+.divide-purple-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--divide-opacity));
+}
+
+.divide-purple-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--divide-opacity));
+}
+
+.divide-purple-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--divide-opacity));
+}
+
+.divide-purple-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--divide-opacity));
+}
+
+.divide-purple-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--divide-opacity));
+}
+
+.divide-purple-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--divide-opacity));
+}
+
+.divide-purple-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--divide-opacity));
+}
+
+.divide-purple-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--divide-opacity));
+}
+
+.divide-purple-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--divide-opacity));
+}
+
+.divide-pink-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--divide-opacity));
+}
+
+.divide-pink-200 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--divide-opacity));
+}
+
+.divide-pink-300 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--divide-opacity));
+}
+
+.divide-pink-400 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--divide-opacity));
+}
+
+.divide-pink-500 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--divide-opacity));
+}
+
+.divide-pink-600 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--divide-opacity));
+}
+
+.divide-pink-700 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--divide-opacity));
+}
+
+.divide-pink-800 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--divide-opacity));
+}
+
+.divide-pink-900 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--divide-opacity));
+}
+
+.divide-solid > :not(template) ~ :not(template) {
+  border-style: solid;
+}
+
+.divide-dashed > :not(template) ~ :not(template) {
+  border-style: dashed;
+}
+
+.divide-dotted > :not(template) ~ :not(template) {
+  border-style: dotted;
+}
+
+.divide-double > :not(template) ~ :not(template) {
+  border-style: double;
+}
+
+.divide-none > :not(template) ~ :not(template) {
+  border-style: none;
+}
+
+.divide-opacity-0 > :not(template) ~ :not(template) {
+  --divide-opacity: 0;
+}
+
+.divide-opacity-25 > :not(template) ~ :not(template) {
+  --divide-opacity: 0.25;
+}
+
+.divide-opacity-50 > :not(template) ~ :not(template) {
+  --divide-opacity: 0.5;
+}
+
+.divide-opacity-75 > :not(template) ~ :not(template) {
+  --divide-opacity: 0.75;
+}
+
+.divide-opacity-100 > :not(template) ~ :not(template) {
+  --divide-opacity: 1;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border-width: 0;
+}
+
+.not-sr-only {
+  position: static;
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
+  white-space: normal;
+}
+
+.focus\:sr-only:focus {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border-width: 0;
+}
+
+.focus\:not-sr-only:focus {
+  position: static;
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
+  white-space: normal;
+}
+
+.appearance-none {
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+}
+
+.bg-fixed {
+  background-attachment: fixed;
+}
+
+.bg-local {
+  background-attachment: local;
+}
+
+.bg-scroll {
+  background-attachment: scroll;
+}
+
+.bg-clip-border {
+  background-clip: border-box;
+}
+
+.bg-clip-padding {
+  background-clip: padding-box;
+}
+
+.bg-clip-content {
+  background-clip: content-box;
+}
+
+.bg-clip-text {
+  -webkit-background-clip: text;
+          background-clip: text;
+}
+
+.bg-transparent {
+  background-color: transparent;
+}
+
+.bg-current {
+  background-color: currentColor;
+}
+
+.bg-black {
+  --bg-opacity: 1;
+  background-color: #000;
+  background-color: rgba(0, 0, 0, var(--bg-opacity));
+}
+
+.bg-white {
+  --bg-opacity: 1;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, var(--bg-opacity));
+}
+
+.bg-gray-100 {
+  --bg-opacity: 1;
+  background-color: #f7fafc;
+  background-color: rgba(247, 250, 252, var(--bg-opacity));
+}
+
+.bg-gray-200 {
+  --bg-opacity: 1;
+  background-color: #edf2f7;
+  background-color: rgba(237, 242, 247, var(--bg-opacity));
+}
+
+.bg-gray-300 {
+  --bg-opacity: 1;
+  background-color: #e2e8f0;
+  background-color: rgba(226, 232, 240, var(--bg-opacity));
+}
+
+.bg-gray-400 {
+  --bg-opacity: 1;
+  background-color: #cbd5e0;
+  background-color: rgba(203, 213, 224, var(--bg-opacity));
+}
+
+.bg-gray-500 {
+  --bg-opacity: 1;
+  background-color: #a0aec0;
+  background-color: rgba(160, 174, 192, var(--bg-opacity));
+}
+
+.bg-gray-600 {
+  --bg-opacity: 1;
+  background-color: #718096;
+  background-color: rgba(113, 128, 150, var(--bg-opacity));
+}
+
+.bg-gray-700 {
+  --bg-opacity: 1;
+  background-color: #4a5568;
+  background-color: rgba(74, 85, 104, var(--bg-opacity));
+}
+
+.bg-gray-800 {
+  --bg-opacity: 1;
+  background-color: #2d3748;
+  background-color: rgba(45, 55, 72, var(--bg-opacity));
+}
+
+.bg-gray-900 {
+  --bg-opacity: 1;
+  background-color: #1a202c;
+  background-color: rgba(26, 32, 44, var(--bg-opacity));
+}
+
+.bg-red-100 {
+  --bg-opacity: 1;
+  background-color: #fff5f5;
+  background-color: rgba(255, 245, 245, var(--bg-opacity));
+}
+
+.bg-red-200 {
+  --bg-opacity: 1;
+  background-color: #fed7d7;
+  background-color: rgba(254, 215, 215, var(--bg-opacity));
+}
+
+.bg-red-300 {
+  --bg-opacity: 1;
+  background-color: #feb2b2;
+  background-color: rgba(254, 178, 178, var(--bg-opacity));
+}
+
+.bg-red-400 {
+  --bg-opacity: 1;
+  background-color: #fc8181;
+  background-color: rgba(252, 129, 129, var(--bg-opacity));
+}
+
+.bg-red-500 {
+  --bg-opacity: 1;
+  background-color: #f56565;
+  background-color: rgba(245, 101, 101, var(--bg-opacity));
+}
+
+.bg-red-600 {
+  --bg-opacity: 1;
+  background-color: #e53e3e;
+  background-color: rgba(229, 62, 62, var(--bg-opacity));
+}
+
+.bg-red-700 {
+  --bg-opacity: 1;
+  background-color: #c53030;
+  background-color: rgba(197, 48, 48, var(--bg-opacity));
+}
+
+.bg-red-800 {
+  --bg-opacity: 1;
+  background-color: #9b2c2c;
+  background-color: rgba(155, 44, 44, var(--bg-opacity));
+}
+
+.bg-red-900 {
+  --bg-opacity: 1;
+  background-color: #742a2a;
+  background-color: rgba(116, 42, 42, var(--bg-opacity));
+}
+
+.bg-orange-100 {
+  --bg-opacity: 1;
+  background-color: #fffaf0;
+  background-color: rgba(255, 250, 240, var(--bg-opacity));
+}
+
+.bg-orange-200 {
+  --bg-opacity: 1;
+  background-color: #feebc8;
+  background-color: rgba(254, 235, 200, var(--bg-opacity));
+}
+
+.bg-orange-300 {
+  --bg-opacity: 1;
+  background-color: #fbd38d;
+  background-color: rgba(251, 211, 141, var(--bg-opacity));
+}
+
+.bg-orange-400 {
+  --bg-opacity: 1;
+  background-color: #f6ad55;
+  background-color: rgba(246, 173, 85, var(--bg-opacity));
+}
+
+.bg-orange-500 {
+  --bg-opacity: 1;
+  background-color: #ed8936;
+  background-color: rgba(237, 137, 54, var(--bg-opacity));
+}
+
+.bg-orange-600 {
+  --bg-opacity: 1;
+  background-color: #dd6b20;
+  background-color: rgba(221, 107, 32, var(--bg-opacity));
+}
+
+.bg-orange-700 {
+  --bg-opacity: 1;
+  background-color: #c05621;
+  background-color: rgba(192, 86, 33, var(--bg-opacity));
+}
+
+.bg-orange-800 {
+  --bg-opacity: 1;
+  background-color: #9c4221;
+  background-color: rgba(156, 66, 33, var(--bg-opacity));
+}
+
+.bg-orange-900 {
+  --bg-opacity: 1;
+  background-color: #7b341e;
+  background-color: rgba(123, 52, 30, var(--bg-opacity));
+}
+
+.bg-yellow-100 {
+  --bg-opacity: 1;
+  background-color: #fffff0;
+  background-color: rgba(255, 255, 240, var(--bg-opacity));
+}
+
+.bg-yellow-200 {
+  --bg-opacity: 1;
+  background-color: #fefcbf;
+  background-color: rgba(254, 252, 191, var(--bg-opacity));
+}
+
+.bg-yellow-300 {
+  --bg-opacity: 1;
+  background-color: #faf089;
+  background-color: rgba(250, 240, 137, var(--bg-opacity));
+}
+
+.bg-yellow-400 {
+  --bg-opacity: 1;
+  background-color: #f6e05e;
+  background-color: rgba(246, 224, 94, var(--bg-opacity));
+}
+
+.bg-yellow-500 {
+  --bg-opacity: 1;
+  background-color: #ecc94b;
+  background-color: rgba(236, 201, 75, var(--bg-opacity));
+}
+
+.bg-yellow-600 {
+  --bg-opacity: 1;
+  background-color: #d69e2e;
+  background-color: rgba(214, 158, 46, var(--bg-opacity));
+}
+
+.bg-yellow-700 {
+  --bg-opacity: 1;
+  background-color: #b7791f;
+  background-color: rgba(183, 121, 31, var(--bg-opacity));
+}
+
+.bg-yellow-800 {
+  --bg-opacity: 1;
+  background-color: #975a16;
+  background-color: rgba(151, 90, 22, var(--bg-opacity));
+}
+
+.bg-yellow-900 {
+  --bg-opacity: 1;
+  background-color: #744210;
+  background-color: rgba(116, 66, 16, var(--bg-opacity));
+}
+
+.bg-green-100 {
+  --bg-opacity: 1;
+  background-color: #f0fff4;
+  background-color: rgba(240, 255, 244, var(--bg-opacity));
+}
+
+.bg-green-200 {
+  --bg-opacity: 1;
+  background-color: #c6f6d5;
+  background-color: rgba(198, 246, 213, var(--bg-opacity));
+}
+
+.bg-green-300 {
+  --bg-opacity: 1;
+  background-color: #9ae6b4;
+  background-color: rgba(154, 230, 180, var(--bg-opacity));
+}
+
+.bg-green-400 {
+  --bg-opacity: 1;
+  background-color: #68d391;
+  background-color: rgba(104, 211, 145, var(--bg-opacity));
+}
+
+.bg-green-500 {
+  --bg-opacity: 1;
+  background-color: #48bb78;
+  background-color: rgba(72, 187, 120, var(--bg-opacity));
+}
+
+.bg-green-600 {
+  --bg-opacity: 1;
+  background-color: #38a169;
+  background-color: rgba(56, 161, 105, var(--bg-opacity));
+}
+
+.bg-green-700 {
+  --bg-opacity: 1;
+  background-color: #2f855a;
+  background-color: rgba(47, 133, 90, var(--bg-opacity));
+}
+
+.bg-green-800 {
+  --bg-opacity: 1;
+  background-color: #276749;
+  background-color: rgba(39, 103, 73, var(--bg-opacity));
+}
+
+.bg-green-900 {
+  --bg-opacity: 1;
+  background-color: #22543d;
+  background-color: rgba(34, 84, 61, var(--bg-opacity));
+}
+
+.bg-teal-100 {
+  --bg-opacity: 1;
+  background-color: #e6fffa;
+  background-color: rgba(230, 255, 250, var(--bg-opacity));
+}
+
+.bg-teal-200 {
+  --bg-opacity: 1;
+  background-color: #b2f5ea;
+  background-color: rgba(178, 245, 234, var(--bg-opacity));
+}
+
+.bg-teal-300 {
+  --bg-opacity: 1;
+  background-color: #81e6d9;
+  background-color: rgba(129, 230, 217, var(--bg-opacity));
+}
+
+.bg-teal-400 {
+  --bg-opacity: 1;
+  background-color: #4fd1c5;
+  background-color: rgba(79, 209, 197, var(--bg-opacity));
+}
+
+.bg-teal-500 {
+  --bg-opacity: 1;
+  background-color: #38b2ac;
+  background-color: rgba(56, 178, 172, var(--bg-opacity));
+}
+
+.bg-teal-600 {
+  --bg-opacity: 1;
+  background-color: #319795;
+  background-color: rgba(49, 151, 149, var(--bg-opacity));
+}
+
+.bg-teal-700 {
+  --bg-opacity: 1;
+  background-color: #2c7a7b;
+  background-color: rgba(44, 122, 123, var(--bg-opacity));
+}
+
+.bg-teal-800 {
+  --bg-opacity: 1;
+  background-color: #285e61;
+  background-color: rgba(40, 94, 97, var(--bg-opacity));
+}
+
+.bg-teal-900 {
+  --bg-opacity: 1;
+  background-color: #234e52;
+  background-color: rgba(35, 78, 82, var(--bg-opacity));
+}
+
+.bg-blue-100 {
+  --bg-opacity: 1;
+  background-color: #ebf8ff;
+  background-color: rgba(235, 248, 255, var(--bg-opacity));
+}
+
+.bg-blue-200 {
+  --bg-opacity: 1;
+  background-color: #bee3f8;
+  background-color: rgba(190, 227, 248, var(--bg-opacity));
+}
+
+.bg-blue-300 {
+  --bg-opacity: 1;
+  background-color: #90cdf4;
+  background-color: rgba(144, 205, 244, var(--bg-opacity));
+}
+
+.bg-blue-400 {
+  --bg-opacity: 1;
+  background-color: #63b3ed;
+  background-color: rgba(99, 179, 237, var(--bg-opacity));
+}
+
+.bg-blue-500 {
+  --bg-opacity: 1;
+  background-color: #4299e1;
+  background-color: rgba(66, 153, 225, var(--bg-opacity));
+}
+
+.bg-blue-600 {
+  --bg-opacity: 1;
+  background-color: #3182ce;
+  background-color: rgba(49, 130, 206, var(--bg-opacity));
+}
+
+.bg-blue-700 {
+  --bg-opacity: 1;
+  background-color: #2b6cb0;
+  background-color: rgba(43, 108, 176, var(--bg-opacity));
+}
+
+.bg-blue-800 {
+  --bg-opacity: 1;
+  background-color: #2c5282;
+  background-color: rgba(44, 82, 130, var(--bg-opacity));
+}
+
+.bg-blue-900 {
+  --bg-opacity: 1;
+  background-color: #2a4365;
+  background-color: rgba(42, 67, 101, var(--bg-opacity));
+}
+
+.bg-indigo-100 {
+  --bg-opacity: 1;
+  background-color: #ebf4ff;
+  background-color: rgba(235, 244, 255, var(--bg-opacity));
+}
+
+.bg-indigo-200 {
+  --bg-opacity: 1;
+  background-color: #c3dafe;
+  background-color: rgba(195, 218, 254, var(--bg-opacity));
+}
+
+.bg-indigo-300 {
+  --bg-opacity: 1;
+  background-color: #a3bffa;
+  background-color: rgba(163, 191, 250, var(--bg-opacity));
+}
+
+.bg-indigo-400 {
+  --bg-opacity: 1;
+  background-color: #7f9cf5;
+  background-color: rgba(127, 156, 245, var(--bg-opacity));
+}
+
+.bg-indigo-500 {
+  --bg-opacity: 1;
+  background-color: #667eea;
+  background-color: rgba(102, 126, 234, var(--bg-opacity));
+}
+
+.bg-indigo-600 {
+  --bg-opacity: 1;
+  background-color: #5a67d8;
+  background-color: rgba(90, 103, 216, var(--bg-opacity));
+}
+
+.bg-indigo-700 {
+  --bg-opacity: 1;
+  background-color: #4c51bf;
+  background-color: rgba(76, 81, 191, var(--bg-opacity));
+}
+
+.bg-indigo-800 {
+  --bg-opacity: 1;
+  background-color: #434190;
+  background-color: rgba(67, 65, 144, var(--bg-opacity));
+}
+
+.bg-indigo-900 {
+  --bg-opacity: 1;
+  background-color: #3c366b;
+  background-color: rgba(60, 54, 107, var(--bg-opacity));
+}
+
+.bg-purple-100 {
+  --bg-opacity: 1;
+  background-color: #faf5ff;
+  background-color: rgba(250, 245, 255, var(--bg-opacity));
+}
+
+.bg-purple-200 {
+  --bg-opacity: 1;
+  background-color: #e9d8fd;
+  background-color: rgba(233, 216, 253, var(--bg-opacity));
+}
+
+.bg-purple-300 {
+  --bg-opacity: 1;
+  background-color: #d6bcfa;
+  background-color: rgba(214, 188, 250, var(--bg-opacity));
+}
+
+.bg-purple-400 {
+  --bg-opacity: 1;
+  background-color: #b794f4;
+  background-color: rgba(183, 148, 244, var(--bg-opacity));
+}
+
+.bg-purple-500 {
+  --bg-opacity: 1;
+  background-color: #9f7aea;
+  background-color: rgba(159, 122, 234, var(--bg-opacity));
+}
+
+.bg-purple-600 {
+  --bg-opacity: 1;
+  background-color: #805ad5;
+  background-color: rgba(128, 90, 213, var(--bg-opacity));
+}
+
+.bg-purple-700 {
+  --bg-opacity: 1;
+  background-color: #6b46c1;
+  background-color: rgba(107, 70, 193, var(--bg-opacity));
+}
+
+.bg-purple-800 {
+  --bg-opacity: 1;
+  background-color: #553c9a;
+  background-color: rgba(85, 60, 154, var(--bg-opacity));
+}
+
+.bg-purple-900 {
+  --bg-opacity: 1;
+  background-color: #44337a;
+  background-color: rgba(68, 51, 122, var(--bg-opacity));
+}
+
+.bg-pink-100 {
+  --bg-opacity: 1;
+  background-color: #fff5f7;
+  background-color: rgba(255, 245, 247, var(--bg-opacity));
+}
+
+.bg-pink-200 {
+  --bg-opacity: 1;
+  background-color: #fed7e2;
+  background-color: rgba(254, 215, 226, var(--bg-opacity));
+}
+
+.bg-pink-300 {
+  --bg-opacity: 1;
+  background-color: #fbb6ce;
+  background-color: rgba(251, 182, 206, var(--bg-opacity));
+}
+
+.bg-pink-400 {
+  --bg-opacity: 1;
+  background-color: #f687b3;
+  background-color: rgba(246, 135, 179, var(--bg-opacity));
+}
+
+.bg-pink-500 {
+  --bg-opacity: 1;
+  background-color: #ed64a6;
+  background-color: rgba(237, 100, 166, var(--bg-opacity));
+}
+
+.bg-pink-600 {
+  --bg-opacity: 1;
+  background-color: #d53f8c;
+  background-color: rgba(213, 63, 140, var(--bg-opacity));
+}
+
+.bg-pink-700 {
+  --bg-opacity: 1;
+  background-color: #b83280;
+  background-color: rgba(184, 50, 128, var(--bg-opacity));
+}
+
+.bg-pink-800 {
+  --bg-opacity: 1;
+  background-color: #97266d;
+  background-color: rgba(151, 38, 109, var(--bg-opacity));
+}
+
+.bg-pink-900 {
+  --bg-opacity: 1;
+  background-color: #702459;
+  background-color: rgba(112, 36, 89, var(--bg-opacity));
+}
+
+.hover\:bg-transparent:hover {
+  background-color: transparent;
+}
+
+.hover\:bg-current:hover {
+  background-color: currentColor;
+}
+
+.hover\:bg-black:hover {
+  --bg-opacity: 1;
+  background-color: #000;
+  background-color: rgba(0, 0, 0, var(--bg-opacity));
+}
+
+.hover\:bg-white:hover {
+  --bg-opacity: 1;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, var(--bg-opacity));
+}
+
+.hover\:bg-gray-100:hover {
+  --bg-opacity: 1;
+  background-color: #f7fafc;
+  background-color: rgba(247, 250, 252, var(--bg-opacity));
+}
+
+.hover\:bg-gray-200:hover {
+  --bg-opacity: 1;
+  background-color: #edf2f7;
+  background-color: rgba(237, 242, 247, var(--bg-opacity));
+}
+
+.hover\:bg-gray-300:hover {
+  --bg-opacity: 1;
+  background-color: #e2e8f0;
+  background-color: rgba(226, 232, 240, var(--bg-opacity));
+}
+
+.hover\:bg-gray-400:hover {
+  --bg-opacity: 1;
+  background-color: #cbd5e0;
+  background-color: rgba(203, 213, 224, var(--bg-opacity));
+}
+
+.hover\:bg-gray-500:hover {
+  --bg-opacity: 1;
+  background-color: #a0aec0;
+  background-color: rgba(160, 174, 192, var(--bg-opacity));
+}
+
+.hover\:bg-gray-600:hover {
+  --bg-opacity: 1;
+  background-color: #718096;
+  background-color: rgba(113, 128, 150, var(--bg-opacity));
+}
+
+.hover\:bg-gray-700:hover {
+  --bg-opacity: 1;
+  background-color: #4a5568;
+  background-color: rgba(74, 85, 104, var(--bg-opacity));
+}
+
+.hover\:bg-gray-800:hover {
+  --bg-opacity: 1;
+  background-color: #2d3748;
+  background-color: rgba(45, 55, 72, var(--bg-opacity));
+}
+
+.hover\:bg-gray-900:hover {
+  --bg-opacity: 1;
+  background-color: #1a202c;
+  background-color: rgba(26, 32, 44, var(--bg-opacity));
+}
+
+.hover\:bg-red-100:hover {
+  --bg-opacity: 1;
+  background-color: #fff5f5;
+  background-color: rgba(255, 245, 245, var(--bg-opacity));
+}
+
+.hover\:bg-red-200:hover {
+  --bg-opacity: 1;
+  background-color: #fed7d7;
+  background-color: rgba(254, 215, 215, var(--bg-opacity));
+}
+
+.hover\:bg-red-300:hover {
+  --bg-opacity: 1;
+  background-color: #feb2b2;
+  background-color: rgba(254, 178, 178, var(--bg-opacity));
+}
+
+.hover\:bg-red-400:hover {
+  --bg-opacity: 1;
+  background-color: #fc8181;
+  background-color: rgba(252, 129, 129, var(--bg-opacity));
+}
+
+.hover\:bg-red-500:hover {
+  --bg-opacity: 1;
+  background-color: #f56565;
+  background-color: rgba(245, 101, 101, var(--bg-opacity));
+}
+
+.hover\:bg-red-600:hover {
+  --bg-opacity: 1;
+  background-color: #e53e3e;
+  background-color: rgba(229, 62, 62, var(--bg-opacity));
+}
+
+.hover\:bg-red-700:hover {
+  --bg-opacity: 1;
+  background-color: #c53030;
+  background-color: rgba(197, 48, 48, var(--bg-opacity));
+}
+
+.hover\:bg-red-800:hover {
+  --bg-opacity: 1;
+  background-color: #9b2c2c;
+  background-color: rgba(155, 44, 44, var(--bg-opacity));
+}
+
+.hover\:bg-red-900:hover {
+  --bg-opacity: 1;
+  background-color: #742a2a;
+  background-color: rgba(116, 42, 42, var(--bg-opacity));
+}
+
+.hover\:bg-orange-100:hover {
+  --bg-opacity: 1;
+  background-color: #fffaf0;
+  background-color: rgba(255, 250, 240, var(--bg-opacity));
+}
+
+.hover\:bg-orange-200:hover {
+  --bg-opacity: 1;
+  background-color: #feebc8;
+  background-color: rgba(254, 235, 200, var(--bg-opacity));
+}
+
+.hover\:bg-orange-300:hover {
+  --bg-opacity: 1;
+  background-color: #fbd38d;
+  background-color: rgba(251, 211, 141, var(--bg-opacity));
+}
+
+.hover\:bg-orange-400:hover {
+  --bg-opacity: 1;
+  background-color: #f6ad55;
+  background-color: rgba(246, 173, 85, var(--bg-opacity));
+}
+
+.hover\:bg-orange-500:hover {
+  --bg-opacity: 1;
+  background-color: #ed8936;
+  background-color: rgba(237, 137, 54, var(--bg-opacity));
+}
+
+.hover\:bg-orange-600:hover {
+  --bg-opacity: 1;
+  background-color: #dd6b20;
+  background-color: rgba(221, 107, 32, var(--bg-opacity));
+}
+
+.hover\:bg-orange-700:hover {
+  --bg-opacity: 1;
+  background-color: #c05621;
+  background-color: rgba(192, 86, 33, var(--bg-opacity));
+}
+
+.hover\:bg-orange-800:hover {
+  --bg-opacity: 1;
+  background-color: #9c4221;
+  background-color: rgba(156, 66, 33, var(--bg-opacity));
+}
+
+.hover\:bg-orange-900:hover {
+  --bg-opacity: 1;
+  background-color: #7b341e;
+  background-color: rgba(123, 52, 30, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-100:hover {
+  --bg-opacity: 1;
+  background-color: #fffff0;
+  background-color: rgba(255, 255, 240, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-200:hover {
+  --bg-opacity: 1;
+  background-color: #fefcbf;
+  background-color: rgba(254, 252, 191, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-300:hover {
+  --bg-opacity: 1;
+  background-color: #faf089;
+  background-color: rgba(250, 240, 137, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-400:hover {
+  --bg-opacity: 1;
+  background-color: #f6e05e;
+  background-color: rgba(246, 224, 94, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-500:hover {
+  --bg-opacity: 1;
+  background-color: #ecc94b;
+  background-color: rgba(236, 201, 75, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-600:hover {
+  --bg-opacity: 1;
+  background-color: #d69e2e;
+  background-color: rgba(214, 158, 46, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-700:hover {
+  --bg-opacity: 1;
+  background-color: #b7791f;
+  background-color: rgba(183, 121, 31, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-800:hover {
+  --bg-opacity: 1;
+  background-color: #975a16;
+  background-color: rgba(151, 90, 22, var(--bg-opacity));
+}
+
+.hover\:bg-yellow-900:hover {
+  --bg-opacity: 1;
+  background-color: #744210;
+  background-color: rgba(116, 66, 16, var(--bg-opacity));
+}
+
+.hover\:bg-green-100:hover {
+  --bg-opacity: 1;
+  background-color: #f0fff4;
+  background-color: rgba(240, 255, 244, var(--bg-opacity));
+}
+
+.hover\:bg-green-200:hover {
+  --bg-opacity: 1;
+  background-color: #c6f6d5;
+  background-color: rgba(198, 246, 213, var(--bg-opacity));
+}
+
+.hover\:bg-green-300:hover {
+  --bg-opacity: 1;
+  background-color: #9ae6b4;
+  background-color: rgba(154, 230, 180, var(--bg-opacity));
+}
+
+.hover\:bg-green-400:hover {
+  --bg-opacity: 1;
+  background-color: #68d391;
+  background-color: rgba(104, 211, 145, var(--bg-opacity));
+}
+
+.hover\:bg-green-500:hover {
+  --bg-opacity: 1;
+  background-color: #48bb78;
+  background-color: rgba(72, 187, 120, var(--bg-opacity));
+}
+
+.hover\:bg-green-600:hover {
+  --bg-opacity: 1;
+  background-color: #38a169;
+  background-color: rgba(56, 161, 105, var(--bg-opacity));
+}
+
+.hover\:bg-green-700:hover {
+  --bg-opacity: 1;
+  background-color: #2f855a;
+  background-color: rgba(47, 133, 90, var(--bg-opacity));
+}
+
+.hover\:bg-green-800:hover {
+  --bg-opacity: 1;
+  background-color: #276749;
+  background-color: rgba(39, 103, 73, var(--bg-opacity));
+}
+
+.hover\:bg-green-900:hover {
+  --bg-opacity: 1;
+  background-color: #22543d;
+  background-color: rgba(34, 84, 61, var(--bg-opacity));
+}
+
+.hover\:bg-teal-100:hover {
+  --bg-opacity: 1;
+  background-color: #e6fffa;
+  background-color: rgba(230, 255, 250, var(--bg-opacity));
+}
+
+.hover\:bg-teal-200:hover {
+  --bg-opacity: 1;
+  background-color: #b2f5ea;
+  background-color: rgba(178, 245, 234, var(--bg-opacity));
+}
+
+.hover\:bg-teal-300:hover {
+  --bg-opacity: 1;
+  background-color: #81e6d9;
+  background-color: rgba(129, 230, 217, var(--bg-opacity));
+}
+
+.hover\:bg-teal-400:hover {
+  --bg-opacity: 1;
+  background-color: #4fd1c5;
+  background-color: rgba(79, 209, 197, var(--bg-opacity));
+}
+
+.hover\:bg-teal-500:hover {
+  --bg-opacity: 1;
+  background-color: #38b2ac;
+  background-color: rgba(56, 178, 172, var(--bg-opacity));
+}
+
+.hover\:bg-teal-600:hover {
+  --bg-opacity: 1;
+  background-color: #319795;
+  background-color: rgba(49, 151, 149, var(--bg-opacity));
+}
+
+.hover\:bg-teal-700:hover {
+  --bg-opacity: 1;
+  background-color: #2c7a7b;
+  background-color: rgba(44, 122, 123, var(--bg-opacity));
+}
+
+.hover\:bg-teal-800:hover {
+  --bg-opacity: 1;
+  background-color: #285e61;
+  background-color: rgba(40, 94, 97, var(--bg-opacity));
+}
+
+.hover\:bg-teal-900:hover {
+  --bg-opacity: 1;
+  background-color: #234e52;
+  background-color: rgba(35, 78, 82, var(--bg-opacity));
+}
+
+.hover\:bg-blue-100:hover {
+  --bg-opacity: 1;
+  background-color: #ebf8ff;
+  background-color: rgba(235, 248, 255, var(--bg-opacity));
+}
+
+.hover\:bg-blue-200:hover {
+  --bg-opacity: 1;
+  background-color: #bee3f8;
+  background-color: rgba(190, 227, 248, var(--bg-opacity));
+}
+
+.hover\:bg-blue-300:hover {
+  --bg-opacity: 1;
+  background-color: #90cdf4;
+  background-color: rgba(144, 205, 244, var(--bg-opacity));
+}
+
+.hover\:bg-blue-400:hover {
+  --bg-opacity: 1;
+  background-color: #63b3ed;
+  background-color: rgba(99, 179, 237, var(--bg-opacity));
+}
+
+.hover\:bg-blue-500:hover {
+  --bg-opacity: 1;
+  background-color: #4299e1;
+  background-color: rgba(66, 153, 225, var(--bg-opacity));
+}
+
+.hover\:bg-blue-600:hover {
+  --bg-opacity: 1;
+  background-color: #3182ce;
+  background-color: rgba(49, 130, 206, var(--bg-opacity));
+}
+
+.hover\:bg-blue-700:hover {
+  --bg-opacity: 1;
+  background-color: #2b6cb0;
+  background-color: rgba(43, 108, 176, var(--bg-opacity));
+}
+
+.hover\:bg-blue-800:hover {
+  --bg-opacity: 1;
+  background-color: #2c5282;
+  background-color: rgba(44, 82, 130, var(--bg-opacity));
+}
+
+.hover\:bg-blue-900:hover {
+  --bg-opacity: 1;
+  background-color: #2a4365;
+  background-color: rgba(42, 67, 101, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-100:hover {
+  --bg-opacity: 1;
+  background-color: #ebf4ff;
+  background-color: rgba(235, 244, 255, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-200:hover {
+  --bg-opacity: 1;
+  background-color: #c3dafe;
+  background-color: rgba(195, 218, 254, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-300:hover {
+  --bg-opacity: 1;
+  background-color: #a3bffa;
+  background-color: rgba(163, 191, 250, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-400:hover {
+  --bg-opacity: 1;
+  background-color: #7f9cf5;
+  background-color: rgba(127, 156, 245, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-500:hover {
+  --bg-opacity: 1;
+  background-color: #667eea;
+  background-color: rgba(102, 126, 234, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-600:hover {
+  --bg-opacity: 1;
+  background-color: #5a67d8;
+  background-color: rgba(90, 103, 216, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-700:hover {
+  --bg-opacity: 1;
+  background-color: #4c51bf;
+  background-color: rgba(76, 81, 191, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-800:hover {
+  --bg-opacity: 1;
+  background-color: #434190;
+  background-color: rgba(67, 65, 144, var(--bg-opacity));
+}
+
+.hover\:bg-indigo-900:hover {
+  --bg-opacity: 1;
+  background-color: #3c366b;
+  background-color: rgba(60, 54, 107, var(--bg-opacity));
+}
+
+.hover\:bg-purple-100:hover {
+  --bg-opacity: 1;
+  background-color: #faf5ff;
+  background-color: rgba(250, 245, 255, var(--bg-opacity));
+}
+
+.hover\:bg-purple-200:hover {
+  --bg-opacity: 1;
+  background-color: #e9d8fd;
+  background-color: rgba(233, 216, 253, var(--bg-opacity));
+}
+
+.hover\:bg-purple-300:hover {
+  --bg-opacity: 1;
+  background-color: #d6bcfa;
+  background-color: rgba(214, 188, 250, var(--bg-opacity));
+}
+
+.hover\:bg-purple-400:hover {
+  --bg-opacity: 1;
+  background-color: #b794f4;
+  background-color: rgba(183, 148, 244, var(--bg-opacity));
+}
+
+.hover\:bg-purple-500:hover {
+  --bg-opacity: 1;
+  background-color: #9f7aea;
+  background-color: rgba(159, 122, 234, var(--bg-opacity));
+}
+
+.hover\:bg-purple-600:hover {
+  --bg-opacity: 1;
+  background-color: #805ad5;
+  background-color: rgba(128, 90, 213, var(--bg-opacity));
+}
+
+.hover\:bg-purple-700:hover {
+  --bg-opacity: 1;
+  background-color: #6b46c1;
+  background-color: rgba(107, 70, 193, var(--bg-opacity));
+}
+
+.hover\:bg-purple-800:hover {
+  --bg-opacity: 1;
+  background-color: #553c9a;
+  background-color: rgba(85, 60, 154, var(--bg-opacity));
+}
+
+.hover\:bg-purple-900:hover {
+  --bg-opacity: 1;
+  background-color: #44337a;
+  background-color: rgba(68, 51, 122, var(--bg-opacity));
+}
+
+.hover\:bg-pink-100:hover {
+  --bg-opacity: 1;
+  background-color: #fff5f7;
+  background-color: rgba(255, 245, 247, var(--bg-opacity));
+}
+
+.hover\:bg-pink-200:hover {
+  --bg-opacity: 1;
+  background-color: #fed7e2;
+  background-color: rgba(254, 215, 226, var(--bg-opacity));
+}
+
+.hover\:bg-pink-300:hover {
+  --bg-opacity: 1;
+  background-color: #fbb6ce;
+  background-color: rgba(251, 182, 206, var(--bg-opacity));
+}
+
+.hover\:bg-pink-400:hover {
+  --bg-opacity: 1;
+  background-color: #f687b3;
+  background-color: rgba(246, 135, 179, var(--bg-opacity));
+}
+
+.hover\:bg-pink-500:hover {
+  --bg-opacity: 1;
+  background-color: #ed64a6;
+  background-color: rgba(237, 100, 166, var(--bg-opacity));
+}
+
+.hover\:bg-pink-600:hover {
+  --bg-opacity: 1;
+  background-color: #d53f8c;
+  background-color: rgba(213, 63, 140, var(--bg-opacity));
+}
+
+.hover\:bg-pink-700:hover {
+  --bg-opacity: 1;
+  background-color: #b83280;
+  background-color: rgba(184, 50, 128, var(--bg-opacity));
+}
+
+.hover\:bg-pink-800:hover {
+  --bg-opacity: 1;
+  background-color: #97266d;
+  background-color: rgba(151, 38, 109, var(--bg-opacity));
+}
+
+.hover\:bg-pink-900:hover {
+  --bg-opacity: 1;
+  background-color: #702459;
+  background-color: rgba(112, 36, 89, var(--bg-opacity));
+}
+
+.focus\:bg-transparent:focus {
+  background-color: transparent;
+}
+
+.focus\:bg-current:focus {
+  background-color: currentColor;
+}
+
+.focus\:bg-black:focus {
+  --bg-opacity: 1;
+  background-color: #000;
+  background-color: rgba(0, 0, 0, var(--bg-opacity));
+}
+
+.focus\:bg-white:focus {
+  --bg-opacity: 1;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, var(--bg-opacity));
+}
+
+.focus\:bg-gray-100:focus {
+  --bg-opacity: 1;
+  background-color: #f7fafc;
+  background-color: rgba(247, 250, 252, var(--bg-opacity));
+}
+
+.focus\:bg-gray-200:focus {
+  --bg-opacity: 1;
+  background-color: #edf2f7;
+  background-color: rgba(237, 242, 247, var(--bg-opacity));
+}
+
+.focus\:bg-gray-300:focus {
+  --bg-opacity: 1;
+  background-color: #e2e8f0;
+  background-color: rgba(226, 232, 240, var(--bg-opacity));
+}
+
+.focus\:bg-gray-400:focus {
+  --bg-opacity: 1;
+  background-color: #cbd5e0;
+  background-color: rgba(203, 213, 224, var(--bg-opacity));
+}
+
+.focus\:bg-gray-500:focus {
+  --bg-opacity: 1;
+  background-color: #a0aec0;
+  background-color: rgba(160, 174, 192, var(--bg-opacity));
+}
+
+.focus\:bg-gray-600:focus {
+  --bg-opacity: 1;
+  background-color: #718096;
+  background-color: rgba(113, 128, 150, var(--bg-opacity));
+}
+
+.focus\:bg-gray-700:focus {
+  --bg-opacity: 1;
+  background-color: #4a5568;
+  background-color: rgba(74, 85, 104, var(--bg-opacity));
+}
+
+.focus\:bg-gray-800:focus {
+  --bg-opacity: 1;
+  background-color: #2d3748;
+  background-color: rgba(45, 55, 72, var(--bg-opacity));
+}
+
+.focus\:bg-gray-900:focus {
+  --bg-opacity: 1;
+  background-color: #1a202c;
+  background-color: rgba(26, 32, 44, var(--bg-opacity));
+}
+
+.focus\:bg-red-100:focus {
+  --bg-opacity: 1;
+  background-color: #fff5f5;
+  background-color: rgba(255, 245, 245, var(--bg-opacity));
+}
+
+.focus\:bg-red-200:focus {
+  --bg-opacity: 1;
+  background-color: #fed7d7;
+  background-color: rgba(254, 215, 215, var(--bg-opacity));
+}
+
+.focus\:bg-red-300:focus {
+  --bg-opacity: 1;
+  background-color: #feb2b2;
+  background-color: rgba(254, 178, 178, var(--bg-opacity));
+}
+
+.focus\:bg-red-400:focus {
+  --bg-opacity: 1;
+  background-color: #fc8181;
+  background-color: rgba(252, 129, 129, var(--bg-opacity));
+}
+
+.focus\:bg-red-500:focus {
+  --bg-opacity: 1;
+  background-color: #f56565;
+  background-color: rgba(245, 101, 101, var(--bg-opacity));
+}
+
+.focus\:bg-red-600:focus {
+  --bg-opacity: 1;
+  background-color: #e53e3e;
+  background-color: rgba(229, 62, 62, var(--bg-opacity));
+}
+
+.focus\:bg-red-700:focus {
+  --bg-opacity: 1;
+  background-color: #c53030;
+  background-color: rgba(197, 48, 48, var(--bg-opacity));
+}
+
+.focus\:bg-red-800:focus {
+  --bg-opacity: 1;
+  background-color: #9b2c2c;
+  background-color: rgba(155, 44, 44, var(--bg-opacity));
+}
+
+.focus\:bg-red-900:focus {
+  --bg-opacity: 1;
+  background-color: #742a2a;
+  background-color: rgba(116, 42, 42, var(--bg-opacity));
+}
+
+.focus\:bg-orange-100:focus {
+  --bg-opacity: 1;
+  background-color: #fffaf0;
+  background-color: rgba(255, 250, 240, var(--bg-opacity));
+}
+
+.focus\:bg-orange-200:focus {
+  --bg-opacity: 1;
+  background-color: #feebc8;
+  background-color: rgba(254, 235, 200, var(--bg-opacity));
+}
+
+.focus\:bg-orange-300:focus {
+  --bg-opacity: 1;
+  background-color: #fbd38d;
+  background-color: rgba(251, 211, 141, var(--bg-opacity));
+}
+
+.focus\:bg-orange-400:focus {
+  --bg-opacity: 1;
+  background-color: #f6ad55;
+  background-color: rgba(246, 173, 85, var(--bg-opacity));
+}
+
+.focus\:bg-orange-500:focus {
+  --bg-opacity: 1;
+  background-color: #ed8936;
+  background-color: rgba(237, 137, 54, var(--bg-opacity));
+}
+
+.focus\:bg-orange-600:focus {
+  --bg-opacity: 1;
+  background-color: #dd6b20;
+  background-color: rgba(221, 107, 32, var(--bg-opacity));
+}
+
+.focus\:bg-orange-700:focus {
+  --bg-opacity: 1;
+  background-color: #c05621;
+  background-color: rgba(192, 86, 33, var(--bg-opacity));
+}
+
+.focus\:bg-orange-800:focus {
+  --bg-opacity: 1;
+  background-color: #9c4221;
+  background-color: rgba(156, 66, 33, var(--bg-opacity));
+}
+
+.focus\:bg-orange-900:focus {
+  --bg-opacity: 1;
+  background-color: #7b341e;
+  background-color: rgba(123, 52, 30, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-100:focus {
+  --bg-opacity: 1;
+  background-color: #fffff0;
+  background-color: rgba(255, 255, 240, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-200:focus {
+  --bg-opacity: 1;
+  background-color: #fefcbf;
+  background-color: rgba(254, 252, 191, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-300:focus {
+  --bg-opacity: 1;
+  background-color: #faf089;
+  background-color: rgba(250, 240, 137, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-400:focus {
+  --bg-opacity: 1;
+  background-color: #f6e05e;
+  background-color: rgba(246, 224, 94, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-500:focus {
+  --bg-opacity: 1;
+  background-color: #ecc94b;
+  background-color: rgba(236, 201, 75, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-600:focus {
+  --bg-opacity: 1;
+  background-color: #d69e2e;
+  background-color: rgba(214, 158, 46, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-700:focus {
+  --bg-opacity: 1;
+  background-color: #b7791f;
+  background-color: rgba(183, 121, 31, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-800:focus {
+  --bg-opacity: 1;
+  background-color: #975a16;
+  background-color: rgba(151, 90, 22, var(--bg-opacity));
+}
+
+.focus\:bg-yellow-900:focus {
+  --bg-opacity: 1;
+  background-color: #744210;
+  background-color: rgba(116, 66, 16, var(--bg-opacity));
+}
+
+.focus\:bg-green-100:focus {
+  --bg-opacity: 1;
+  background-color: #f0fff4;
+  background-color: rgba(240, 255, 244, var(--bg-opacity));
+}
+
+.focus\:bg-green-200:focus {
+  --bg-opacity: 1;
+  background-color: #c6f6d5;
+  background-color: rgba(198, 246, 213, var(--bg-opacity));
+}
+
+.focus\:bg-green-300:focus {
+  --bg-opacity: 1;
+  background-color: #9ae6b4;
+  background-color: rgba(154, 230, 180, var(--bg-opacity));
+}
+
+.focus\:bg-green-400:focus {
+  --bg-opacity: 1;
+  background-color: #68d391;
+  background-color: rgba(104, 211, 145, var(--bg-opacity));
+}
+
+.focus\:bg-green-500:focus {
+  --bg-opacity: 1;
+  background-color: #48bb78;
+  background-color: rgba(72, 187, 120, var(--bg-opacity));
+}
+
+.focus\:bg-green-600:focus {
+  --bg-opacity: 1;
+  background-color: #38a169;
+  background-color: rgba(56, 161, 105, var(--bg-opacity));
+}
+
+.focus\:bg-green-700:focus {
+  --bg-opacity: 1;
+  background-color: #2f855a;
+  background-color: rgba(47, 133, 90, var(--bg-opacity));
+}
+
+.focus\:bg-green-800:focus {
+  --bg-opacity: 1;
+  background-color: #276749;
+  background-color: rgba(39, 103, 73, var(--bg-opacity));
+}
+
+.focus\:bg-green-900:focus {
+  --bg-opacity: 1;
+  background-color: #22543d;
+  background-color: rgba(34, 84, 61, var(--bg-opacity));
+}
+
+.focus\:bg-teal-100:focus {
+  --bg-opacity: 1;
+  background-color: #e6fffa;
+  background-color: rgba(230, 255, 250, var(--bg-opacity));
+}
+
+.focus\:bg-teal-200:focus {
+  --bg-opacity: 1;
+  background-color: #b2f5ea;
+  background-color: rgba(178, 245, 234, var(--bg-opacity));
+}
+
+.focus\:bg-teal-300:focus {
+  --bg-opacity: 1;
+  background-color: #81e6d9;
+  background-color: rgba(129, 230, 217, var(--bg-opacity));
+}
+
+.focus\:bg-teal-400:focus {
+  --bg-opacity: 1;
+  background-color: #4fd1c5;
+  background-color: rgba(79, 209, 197, var(--bg-opacity));
+}
+
+.focus\:bg-teal-500:focus {
+  --bg-opacity: 1;
+  background-color: #38b2ac;
+  background-color: rgba(56, 178, 172, var(--bg-opacity));
+}
+
+.focus\:bg-teal-600:focus {
+  --bg-opacity: 1;
+  background-color: #319795;
+  background-color: rgba(49, 151, 149, var(--bg-opacity));
+}
+
+.focus\:bg-teal-700:focus {
+  --bg-opacity: 1;
+  background-color: #2c7a7b;
+  background-color: rgba(44, 122, 123, var(--bg-opacity));
+}
+
+.focus\:bg-teal-800:focus {
+  --bg-opacity: 1;
+  background-color: #285e61;
+  background-color: rgba(40, 94, 97, var(--bg-opacity));
+}
+
+.focus\:bg-teal-900:focus {
+  --bg-opacity: 1;
+  background-color: #234e52;
+  background-color: rgba(35, 78, 82, var(--bg-opacity));
+}
+
+.focus\:bg-blue-100:focus {
+  --bg-opacity: 1;
+  background-color: #ebf8ff;
+  background-color: rgba(235, 248, 255, var(--bg-opacity));
+}
+
+.focus\:bg-blue-200:focus {
+  --bg-opacity: 1;
+  background-color: #bee3f8;
+  background-color: rgba(190, 227, 248, var(--bg-opacity));
+}
+
+.focus\:bg-blue-300:focus {
+  --bg-opacity: 1;
+  background-color: #90cdf4;
+  background-color: rgba(144, 205, 244, var(--bg-opacity));
+}
+
+.focus\:bg-blue-400:focus {
+  --bg-opacity: 1;
+  background-color: #63b3ed;
+  background-color: rgba(99, 179, 237, var(--bg-opacity));
+}
+
+.focus\:bg-blue-500:focus {
+  --bg-opacity: 1;
+  background-color: #4299e1;
+  background-color: rgba(66, 153, 225, var(--bg-opacity));
+}
+
+.focus\:bg-blue-600:focus {
+  --bg-opacity: 1;
+  background-color: #3182ce;
+  background-color: rgba(49, 130, 206, var(--bg-opacity));
+}
+
+.focus\:bg-blue-700:focus {
+  --bg-opacity: 1;
+  background-color: #2b6cb0;
+  background-color: rgba(43, 108, 176, var(--bg-opacity));
+}
+
+.focus\:bg-blue-800:focus {
+  --bg-opacity: 1;
+  background-color: #2c5282;
+  background-color: rgba(44, 82, 130, var(--bg-opacity));
+}
+
+.focus\:bg-blue-900:focus {
+  --bg-opacity: 1;
+  background-color: #2a4365;
+  background-color: rgba(42, 67, 101, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-100:focus {
+  --bg-opacity: 1;
+  background-color: #ebf4ff;
+  background-color: rgba(235, 244, 255, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-200:focus {
+  --bg-opacity: 1;
+  background-color: #c3dafe;
+  background-color: rgba(195, 218, 254, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-300:focus {
+  --bg-opacity: 1;
+  background-color: #a3bffa;
+  background-color: rgba(163, 191, 250, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-400:focus {
+  --bg-opacity: 1;
+  background-color: #7f9cf5;
+  background-color: rgba(127, 156, 245, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-500:focus {
+  --bg-opacity: 1;
+  background-color: #667eea;
+  background-color: rgba(102, 126, 234, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-600:focus {
+  --bg-opacity: 1;
+  background-color: #5a67d8;
+  background-color: rgba(90, 103, 216, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-700:focus {
+  --bg-opacity: 1;
+  background-color: #4c51bf;
+  background-color: rgba(76, 81, 191, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-800:focus {
+  --bg-opacity: 1;
+  background-color: #434190;
+  background-color: rgba(67, 65, 144, var(--bg-opacity));
+}
+
+.focus\:bg-indigo-900:focus {
+  --bg-opacity: 1;
+  background-color: #3c366b;
+  background-color: rgba(60, 54, 107, var(--bg-opacity));
+}
+
+.focus\:bg-purple-100:focus {
+  --bg-opacity: 1;
+  background-color: #faf5ff;
+  background-color: rgba(250, 245, 255, var(--bg-opacity));
+}
+
+.focus\:bg-purple-200:focus {
+  --bg-opacity: 1;
+  background-color: #e9d8fd;
+  background-color: rgba(233, 216, 253, var(--bg-opacity));
+}
+
+.focus\:bg-purple-300:focus {
+  --bg-opacity: 1;
+  background-color: #d6bcfa;
+  background-color: rgba(214, 188, 250, var(--bg-opacity));
+}
+
+.focus\:bg-purple-400:focus {
+  --bg-opacity: 1;
+  background-color: #b794f4;
+  background-color: rgba(183, 148, 244, var(--bg-opacity));
+}
+
+.focus\:bg-purple-500:focus {
+  --bg-opacity: 1;
+  background-color: #9f7aea;
+  background-color: rgba(159, 122, 234, var(--bg-opacity));
+}
+
+.focus\:bg-purple-600:focus {
+  --bg-opacity: 1;
+  background-color: #805ad5;
+  background-color: rgba(128, 90, 213, var(--bg-opacity));
+}
+
+.focus\:bg-purple-700:focus {
+  --bg-opacity: 1;
+  background-color: #6b46c1;
+  background-color: rgba(107, 70, 193, var(--bg-opacity));
+}
+
+.focus\:bg-purple-800:focus {
+  --bg-opacity: 1;
+  background-color: #553c9a;
+  background-color: rgba(85, 60, 154, var(--bg-opacity));
+}
+
+.focus\:bg-purple-900:focus {
+  --bg-opacity: 1;
+  background-color: #44337a;
+  background-color: rgba(68, 51, 122, var(--bg-opacity));
+}
+
+.focus\:bg-pink-100:focus {
+  --bg-opacity: 1;
+  background-color: #fff5f7;
+  background-color: rgba(255, 245, 247, var(--bg-opacity));
+}
+
+.focus\:bg-pink-200:focus {
+  --bg-opacity: 1;
+  background-color: #fed7e2;
+  background-color: rgba(254, 215, 226, var(--bg-opacity));
+}
+
+.focus\:bg-pink-300:focus {
+  --bg-opacity: 1;
+  background-color: #fbb6ce;
+  background-color: rgba(251, 182, 206, var(--bg-opacity));
+}
+
+.focus\:bg-pink-400:focus {
+  --bg-opacity: 1;
+  background-color: #f687b3;
+  background-color: rgba(246, 135, 179, var(--bg-opacity));
+}
+
+.focus\:bg-pink-500:focus {
+  --bg-opacity: 1;
+  background-color: #ed64a6;
+  background-color: rgba(237, 100, 166, var(--bg-opacity));
+}
+
+.focus\:bg-pink-600:focus {
+  --bg-opacity: 1;
+  background-color: #d53f8c;
+  background-color: rgba(213, 63, 140, var(--bg-opacity));
+}
+
+.focus\:bg-pink-700:focus {
+  --bg-opacity: 1;
+  background-color: #b83280;
+  background-color: rgba(184, 50, 128, var(--bg-opacity));
+}
+
+.focus\:bg-pink-800:focus {
+  --bg-opacity: 1;
+  background-color: #97266d;
+  background-color: rgba(151, 38, 109, var(--bg-opacity));
+}
+
+.focus\:bg-pink-900:focus {
+  --bg-opacity: 1;
+  background-color: #702459;
+  background-color: rgba(112, 36, 89, var(--bg-opacity));
+}
+
+.bg-none {
+  background-image: none;
+}
+
+.bg-gradient-to-t {
+  background-image: linear-gradient(to top, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-tr {
+  background-image: linear-gradient(to top right, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-r {
+  background-image: linear-gradient(to right, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-br {
+  background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-b {
+  background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-bl {
+  background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-l {
+  background-image: linear-gradient(to left, var(--gradient-color-stops));
+}
+
+.bg-gradient-to-tl {
+  background-image: linear-gradient(to top left, var(--gradient-color-stops));
+}
+
+.from-transparent {
+  --gradient-from-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.from-current {
+  --gradient-from-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.from-black {
+  --gradient-from-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.from-white {
+  --gradient-from-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.from-gray-100 {
+  --gradient-from-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.from-gray-200 {
+  --gradient-from-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.from-gray-300 {
+  --gradient-from-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.from-gray-400 {
+  --gradient-from-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.from-gray-500 {
+  --gradient-from-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.from-gray-600 {
+  --gradient-from-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.from-gray-700 {
+  --gradient-from-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.from-gray-800 {
+  --gradient-from-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.from-gray-900 {
+  --gradient-from-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.from-red-100 {
+  --gradient-from-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.from-red-200 {
+  --gradient-from-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.from-red-300 {
+  --gradient-from-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.from-red-400 {
+  --gradient-from-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.from-red-500 {
+  --gradient-from-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.from-red-600 {
+  --gradient-from-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.from-red-700 {
+  --gradient-from-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.from-red-800 {
+  --gradient-from-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.from-red-900 {
+  --gradient-from-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.from-orange-100 {
+  --gradient-from-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.from-orange-200 {
+  --gradient-from-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.from-orange-300 {
+  --gradient-from-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.from-orange-400 {
+  --gradient-from-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.from-orange-500 {
+  --gradient-from-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.from-orange-600 {
+  --gradient-from-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.from-orange-700 {
+  --gradient-from-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.from-orange-800 {
+  --gradient-from-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.from-orange-900 {
+  --gradient-from-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.from-yellow-100 {
+  --gradient-from-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.from-yellow-200 {
+  --gradient-from-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.from-yellow-300 {
+  --gradient-from-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.from-yellow-400 {
+  --gradient-from-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.from-yellow-500 {
+  --gradient-from-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.from-yellow-600 {
+  --gradient-from-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.from-yellow-700 {
+  --gradient-from-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.from-yellow-800 {
+  --gradient-from-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.from-yellow-900 {
+  --gradient-from-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.from-green-100 {
+  --gradient-from-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.from-green-200 {
+  --gradient-from-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.from-green-300 {
+  --gradient-from-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.from-green-400 {
+  --gradient-from-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.from-green-500 {
+  --gradient-from-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.from-green-600 {
+  --gradient-from-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.from-green-700 {
+  --gradient-from-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.from-green-800 {
+  --gradient-from-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.from-green-900 {
+  --gradient-from-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.from-teal-100 {
+  --gradient-from-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.from-teal-200 {
+  --gradient-from-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.from-teal-300 {
+  --gradient-from-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.from-teal-400 {
+  --gradient-from-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.from-teal-500 {
+  --gradient-from-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.from-teal-600 {
+  --gradient-from-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.from-teal-700 {
+  --gradient-from-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.from-teal-800 {
+  --gradient-from-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.from-teal-900 {
+  --gradient-from-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.from-blue-100 {
+  --gradient-from-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.from-blue-200 {
+  --gradient-from-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.from-blue-300 {
+  --gradient-from-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.from-blue-400 {
+  --gradient-from-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.from-blue-500 {
+  --gradient-from-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.from-blue-600 {
+  --gradient-from-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.from-blue-700 {
+  --gradient-from-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.from-blue-800 {
+  --gradient-from-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.from-blue-900 {
+  --gradient-from-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.from-indigo-100 {
+  --gradient-from-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.from-indigo-200 {
+  --gradient-from-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.from-indigo-300 {
+  --gradient-from-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.from-indigo-400 {
+  --gradient-from-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.from-indigo-500 {
+  --gradient-from-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.from-indigo-600 {
+  --gradient-from-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.from-indigo-700 {
+  --gradient-from-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.from-indigo-800 {
+  --gradient-from-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.from-indigo-900 {
+  --gradient-from-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.from-purple-100 {
+  --gradient-from-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.from-purple-200 {
+  --gradient-from-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.from-purple-300 {
+  --gradient-from-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.from-purple-400 {
+  --gradient-from-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.from-purple-500 {
+  --gradient-from-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.from-purple-600 {
+  --gradient-from-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.from-purple-700 {
+  --gradient-from-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.from-purple-800 {
+  --gradient-from-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.from-purple-900 {
+  --gradient-from-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.from-pink-100 {
+  --gradient-from-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.from-pink-200 {
+  --gradient-from-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.from-pink-300 {
+  --gradient-from-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.from-pink-400 {
+  --gradient-from-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.from-pink-500 {
+  --gradient-from-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.from-pink-600 {
+  --gradient-from-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.from-pink-700 {
+  --gradient-from-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.from-pink-800 {
+  --gradient-from-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.from-pink-900 {
+  --gradient-from-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.via-transparent {
+  --gradient-via-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.via-current {
+  --gradient-via-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.via-black {
+  --gradient-via-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.via-white {
+  --gradient-via-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.via-gray-100 {
+  --gradient-via-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.via-gray-200 {
+  --gradient-via-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.via-gray-300 {
+  --gradient-via-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.via-gray-400 {
+  --gradient-via-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.via-gray-500 {
+  --gradient-via-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.via-gray-600 {
+  --gradient-via-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.via-gray-700 {
+  --gradient-via-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.via-gray-800 {
+  --gradient-via-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.via-gray-900 {
+  --gradient-via-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.via-red-100 {
+  --gradient-via-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.via-red-200 {
+  --gradient-via-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.via-red-300 {
+  --gradient-via-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.via-red-400 {
+  --gradient-via-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.via-red-500 {
+  --gradient-via-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.via-red-600 {
+  --gradient-via-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.via-red-700 {
+  --gradient-via-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.via-red-800 {
+  --gradient-via-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.via-red-900 {
+  --gradient-via-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.via-orange-100 {
+  --gradient-via-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.via-orange-200 {
+  --gradient-via-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.via-orange-300 {
+  --gradient-via-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.via-orange-400 {
+  --gradient-via-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.via-orange-500 {
+  --gradient-via-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.via-orange-600 {
+  --gradient-via-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.via-orange-700 {
+  --gradient-via-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.via-orange-800 {
+  --gradient-via-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.via-orange-900 {
+  --gradient-via-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.via-yellow-100 {
+  --gradient-via-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.via-yellow-200 {
+  --gradient-via-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.via-yellow-300 {
+  --gradient-via-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.via-yellow-400 {
+  --gradient-via-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.via-yellow-500 {
+  --gradient-via-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.via-yellow-600 {
+  --gradient-via-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.via-yellow-700 {
+  --gradient-via-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.via-yellow-800 {
+  --gradient-via-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.via-yellow-900 {
+  --gradient-via-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.via-green-100 {
+  --gradient-via-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.via-green-200 {
+  --gradient-via-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.via-green-300 {
+  --gradient-via-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.via-green-400 {
+  --gradient-via-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.via-green-500 {
+  --gradient-via-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.via-green-600 {
+  --gradient-via-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.via-green-700 {
+  --gradient-via-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.via-green-800 {
+  --gradient-via-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.via-green-900 {
+  --gradient-via-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.via-teal-100 {
+  --gradient-via-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.via-teal-200 {
+  --gradient-via-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.via-teal-300 {
+  --gradient-via-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.via-teal-400 {
+  --gradient-via-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.via-teal-500 {
+  --gradient-via-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.via-teal-600 {
+  --gradient-via-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.via-teal-700 {
+  --gradient-via-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.via-teal-800 {
+  --gradient-via-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.via-teal-900 {
+  --gradient-via-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.via-blue-100 {
+  --gradient-via-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.via-blue-200 {
+  --gradient-via-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.via-blue-300 {
+  --gradient-via-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.via-blue-400 {
+  --gradient-via-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.via-blue-500 {
+  --gradient-via-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.via-blue-600 {
+  --gradient-via-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.via-blue-700 {
+  --gradient-via-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.via-blue-800 {
+  --gradient-via-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.via-blue-900 {
+  --gradient-via-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.via-indigo-100 {
+  --gradient-via-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.via-indigo-200 {
+  --gradient-via-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.via-indigo-300 {
+  --gradient-via-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.via-indigo-400 {
+  --gradient-via-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.via-indigo-500 {
+  --gradient-via-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.via-indigo-600 {
+  --gradient-via-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.via-indigo-700 {
+  --gradient-via-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.via-indigo-800 {
+  --gradient-via-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.via-indigo-900 {
+  --gradient-via-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.via-purple-100 {
+  --gradient-via-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.via-purple-200 {
+  --gradient-via-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.via-purple-300 {
+  --gradient-via-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.via-purple-400 {
+  --gradient-via-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.via-purple-500 {
+  --gradient-via-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.via-purple-600 {
+  --gradient-via-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.via-purple-700 {
+  --gradient-via-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.via-purple-800 {
+  --gradient-via-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.via-purple-900 {
+  --gradient-via-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.via-pink-100 {
+  --gradient-via-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.via-pink-200 {
+  --gradient-via-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.via-pink-300 {
+  --gradient-via-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.via-pink-400 {
+  --gradient-via-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.via-pink-500 {
+  --gradient-via-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.via-pink-600 {
+  --gradient-via-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.via-pink-700 {
+  --gradient-via-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.via-pink-800 {
+  --gradient-via-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.via-pink-900 {
+  --gradient-via-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.to-transparent {
+  --gradient-to-color: transparent;
+}
+
+.to-current {
+  --gradient-to-color: currentColor;
+}
+
+.to-black {
+  --gradient-to-color: #000;
+}
+
+.to-white {
+  --gradient-to-color: #fff;
+}
+
+.to-gray-100 {
+  --gradient-to-color: #f7fafc;
+}
+
+.to-gray-200 {
+  --gradient-to-color: #edf2f7;
+}
+
+.to-gray-300 {
+  --gradient-to-color: #e2e8f0;
+}
+
+.to-gray-400 {
+  --gradient-to-color: #cbd5e0;
+}
+
+.to-gray-500 {
+  --gradient-to-color: #a0aec0;
+}
+
+.to-gray-600 {
+  --gradient-to-color: #718096;
+}
+
+.to-gray-700 {
+  --gradient-to-color: #4a5568;
+}
+
+.to-gray-800 {
+  --gradient-to-color: #2d3748;
+}
+
+.to-gray-900 {
+  --gradient-to-color: #1a202c;
+}
+
+.to-red-100 {
+  --gradient-to-color: #fff5f5;
+}
+
+.to-red-200 {
+  --gradient-to-color: #fed7d7;
+}
+
+.to-red-300 {
+  --gradient-to-color: #feb2b2;
+}
+
+.to-red-400 {
+  --gradient-to-color: #fc8181;
+}
+
+.to-red-500 {
+  --gradient-to-color: #f56565;
+}
+
+.to-red-600 {
+  --gradient-to-color: #e53e3e;
+}
+
+.to-red-700 {
+  --gradient-to-color: #c53030;
+}
+
+.to-red-800 {
+  --gradient-to-color: #9b2c2c;
+}
+
+.to-red-900 {
+  --gradient-to-color: #742a2a;
+}
+
+.to-orange-100 {
+  --gradient-to-color: #fffaf0;
+}
+
+.to-orange-200 {
+  --gradient-to-color: #feebc8;
+}
+
+.to-orange-300 {
+  --gradient-to-color: #fbd38d;
+}
+
+.to-orange-400 {
+  --gradient-to-color: #f6ad55;
+}
+
+.to-orange-500 {
+  --gradient-to-color: #ed8936;
+}
+
+.to-orange-600 {
+  --gradient-to-color: #dd6b20;
+}
+
+.to-orange-700 {
+  --gradient-to-color: #c05621;
+}
+
+.to-orange-800 {
+  --gradient-to-color: #9c4221;
+}
+
+.to-orange-900 {
+  --gradient-to-color: #7b341e;
+}
+
+.to-yellow-100 {
+  --gradient-to-color: #fffff0;
+}
+
+.to-yellow-200 {
+  --gradient-to-color: #fefcbf;
+}
+
+.to-yellow-300 {
+  --gradient-to-color: #faf089;
+}
+
+.to-yellow-400 {
+  --gradient-to-color: #f6e05e;
+}
+
+.to-yellow-500 {
+  --gradient-to-color: #ecc94b;
+}
+
+.to-yellow-600 {
+  --gradient-to-color: #d69e2e;
+}
+
+.to-yellow-700 {
+  --gradient-to-color: #b7791f;
+}
+
+.to-yellow-800 {
+  --gradient-to-color: #975a16;
+}
+
+.to-yellow-900 {
+  --gradient-to-color: #744210;
+}
+
+.to-green-100 {
+  --gradient-to-color: #f0fff4;
+}
+
+.to-green-200 {
+  --gradient-to-color: #c6f6d5;
+}
+
+.to-green-300 {
+  --gradient-to-color: #9ae6b4;
+}
+
+.to-green-400 {
+  --gradient-to-color: #68d391;
+}
+
+.to-green-500 {
+  --gradient-to-color: #48bb78;
+}
+
+.to-green-600 {
+  --gradient-to-color: #38a169;
+}
+
+.to-green-700 {
+  --gradient-to-color: #2f855a;
+}
+
+.to-green-800 {
+  --gradient-to-color: #276749;
+}
+
+.to-green-900 {
+  --gradient-to-color: #22543d;
+}
+
+.to-teal-100 {
+  --gradient-to-color: #e6fffa;
+}
+
+.to-teal-200 {
+  --gradient-to-color: #b2f5ea;
+}
+
+.to-teal-300 {
+  --gradient-to-color: #81e6d9;
+}
+
+.to-teal-400 {
+  --gradient-to-color: #4fd1c5;
+}
+
+.to-teal-500 {
+  --gradient-to-color: #38b2ac;
+}
+
+.to-teal-600 {
+  --gradient-to-color: #319795;
+}
+
+.to-teal-700 {
+  --gradient-to-color: #2c7a7b;
+}
+
+.to-teal-800 {
+  --gradient-to-color: #285e61;
+}
+
+.to-teal-900 {
+  --gradient-to-color: #234e52;
+}
+
+.to-blue-100 {
+  --gradient-to-color: #ebf8ff;
+}
+
+.to-blue-200 {
+  --gradient-to-color: #bee3f8;
+}
+
+.to-blue-300 {
+  --gradient-to-color: #90cdf4;
+}
+
+.to-blue-400 {
+  --gradient-to-color: #63b3ed;
+}
+
+.to-blue-500 {
+  --gradient-to-color: #4299e1;
+}
+
+.to-blue-600 {
+  --gradient-to-color: #3182ce;
+}
+
+.to-blue-700 {
+  --gradient-to-color: #2b6cb0;
+}
+
+.to-blue-800 {
+  --gradient-to-color: #2c5282;
+}
+
+.to-blue-900 {
+  --gradient-to-color: #2a4365;
+}
+
+.to-indigo-100 {
+  --gradient-to-color: #ebf4ff;
+}
+
+.to-indigo-200 {
+  --gradient-to-color: #c3dafe;
+}
+
+.to-indigo-300 {
+  --gradient-to-color: #a3bffa;
+}
+
+.to-indigo-400 {
+  --gradient-to-color: #7f9cf5;
+}
+
+.to-indigo-500 {
+  --gradient-to-color: #667eea;
+}
+
+.to-indigo-600 {
+  --gradient-to-color: #5a67d8;
+}
+
+.to-indigo-700 {
+  --gradient-to-color: #4c51bf;
+}
+
+.to-indigo-800 {
+  --gradient-to-color: #434190;
+}
+
+.to-indigo-900 {
+  --gradient-to-color: #3c366b;
+}
+
+.to-purple-100 {
+  --gradient-to-color: #faf5ff;
+}
+
+.to-purple-200 {
+  --gradient-to-color: #e9d8fd;
+}
+
+.to-purple-300 {
+  --gradient-to-color: #d6bcfa;
+}
+
+.to-purple-400 {
+  --gradient-to-color: #b794f4;
+}
+
+.to-purple-500 {
+  --gradient-to-color: #9f7aea;
+}
+
+.to-purple-600 {
+  --gradient-to-color: #805ad5;
+}
+
+.to-purple-700 {
+  --gradient-to-color: #6b46c1;
+}
+
+.to-purple-800 {
+  --gradient-to-color: #553c9a;
+}
+
+.to-purple-900 {
+  --gradient-to-color: #44337a;
+}
+
+.to-pink-100 {
+  --gradient-to-color: #fff5f7;
+}
+
+.to-pink-200 {
+  --gradient-to-color: #fed7e2;
+}
+
+.to-pink-300 {
+  --gradient-to-color: #fbb6ce;
+}
+
+.to-pink-400 {
+  --gradient-to-color: #f687b3;
+}
+
+.to-pink-500 {
+  --gradient-to-color: #ed64a6;
+}
+
+.to-pink-600 {
+  --gradient-to-color: #d53f8c;
+}
+
+.to-pink-700 {
+  --gradient-to-color: #b83280;
+}
+
+.to-pink-800 {
+  --gradient-to-color: #97266d;
+}
+
+.to-pink-900 {
+  --gradient-to-color: #702459;
+}
+
+.hover\:from-transparent:hover {
+  --gradient-from-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:from-current:hover {
+  --gradient-from-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:from-black:hover {
+  --gradient-from-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:from-white:hover {
+  --gradient-from-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:from-gray-100:hover {
+  --gradient-from-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.hover\:from-gray-200:hover {
+  --gradient-from-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.hover\:from-gray-300:hover {
+  --gradient-from-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.hover\:from-gray-400:hover {
+  --gradient-from-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.hover\:from-gray-500:hover {
+  --gradient-from-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.hover\:from-gray-600:hover {
+  --gradient-from-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.hover\:from-gray-700:hover {
+  --gradient-from-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.hover\:from-gray-800:hover {
+  --gradient-from-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.hover\:from-gray-900:hover {
+  --gradient-from-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.hover\:from-red-100:hover {
+  --gradient-from-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.hover\:from-red-200:hover {
+  --gradient-from-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.hover\:from-red-300:hover {
+  --gradient-from-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.hover\:from-red-400:hover {
+  --gradient-from-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.hover\:from-red-500:hover {
+  --gradient-from-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.hover\:from-red-600:hover {
+  --gradient-from-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.hover\:from-red-700:hover {
+  --gradient-from-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.hover\:from-red-800:hover {
+  --gradient-from-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.hover\:from-red-900:hover {
+  --gradient-from-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.hover\:from-orange-100:hover {
+  --gradient-from-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.hover\:from-orange-200:hover {
+  --gradient-from-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.hover\:from-orange-300:hover {
+  --gradient-from-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.hover\:from-orange-400:hover {
+  --gradient-from-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.hover\:from-orange-500:hover {
+  --gradient-from-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.hover\:from-orange-600:hover {
+  --gradient-from-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.hover\:from-orange-700:hover {
+  --gradient-from-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.hover\:from-orange-800:hover {
+  --gradient-from-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.hover\:from-orange-900:hover {
+  --gradient-from-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.hover\:from-yellow-100:hover {
+  --gradient-from-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.hover\:from-yellow-200:hover {
+  --gradient-from-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.hover\:from-yellow-300:hover {
+  --gradient-from-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.hover\:from-yellow-400:hover {
+  --gradient-from-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.hover\:from-yellow-500:hover {
+  --gradient-from-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.hover\:from-yellow-600:hover {
+  --gradient-from-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.hover\:from-yellow-700:hover {
+  --gradient-from-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.hover\:from-yellow-800:hover {
+  --gradient-from-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.hover\:from-yellow-900:hover {
+  --gradient-from-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.hover\:from-green-100:hover {
+  --gradient-from-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.hover\:from-green-200:hover {
+  --gradient-from-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.hover\:from-green-300:hover {
+  --gradient-from-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.hover\:from-green-400:hover {
+  --gradient-from-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.hover\:from-green-500:hover {
+  --gradient-from-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.hover\:from-green-600:hover {
+  --gradient-from-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.hover\:from-green-700:hover {
+  --gradient-from-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.hover\:from-green-800:hover {
+  --gradient-from-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.hover\:from-green-900:hover {
+  --gradient-from-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.hover\:from-teal-100:hover {
+  --gradient-from-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.hover\:from-teal-200:hover {
+  --gradient-from-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.hover\:from-teal-300:hover {
+  --gradient-from-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.hover\:from-teal-400:hover {
+  --gradient-from-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.hover\:from-teal-500:hover {
+  --gradient-from-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.hover\:from-teal-600:hover {
+  --gradient-from-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.hover\:from-teal-700:hover {
+  --gradient-from-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.hover\:from-teal-800:hover {
+  --gradient-from-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.hover\:from-teal-900:hover {
+  --gradient-from-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.hover\:from-blue-100:hover {
+  --gradient-from-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.hover\:from-blue-200:hover {
+  --gradient-from-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.hover\:from-blue-300:hover {
+  --gradient-from-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.hover\:from-blue-400:hover {
+  --gradient-from-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.hover\:from-blue-500:hover {
+  --gradient-from-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.hover\:from-blue-600:hover {
+  --gradient-from-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.hover\:from-blue-700:hover {
+  --gradient-from-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.hover\:from-blue-800:hover {
+  --gradient-from-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.hover\:from-blue-900:hover {
+  --gradient-from-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.hover\:from-indigo-100:hover {
+  --gradient-from-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.hover\:from-indigo-200:hover {
+  --gradient-from-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.hover\:from-indigo-300:hover {
+  --gradient-from-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.hover\:from-indigo-400:hover {
+  --gradient-from-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.hover\:from-indigo-500:hover {
+  --gradient-from-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.hover\:from-indigo-600:hover {
+  --gradient-from-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.hover\:from-indigo-700:hover {
+  --gradient-from-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.hover\:from-indigo-800:hover {
+  --gradient-from-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.hover\:from-indigo-900:hover {
+  --gradient-from-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.hover\:from-purple-100:hover {
+  --gradient-from-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.hover\:from-purple-200:hover {
+  --gradient-from-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.hover\:from-purple-300:hover {
+  --gradient-from-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.hover\:from-purple-400:hover {
+  --gradient-from-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.hover\:from-purple-500:hover {
+  --gradient-from-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.hover\:from-purple-600:hover {
+  --gradient-from-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.hover\:from-purple-700:hover {
+  --gradient-from-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.hover\:from-purple-800:hover {
+  --gradient-from-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.hover\:from-purple-900:hover {
+  --gradient-from-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.hover\:from-pink-100:hover {
+  --gradient-from-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.hover\:from-pink-200:hover {
+  --gradient-from-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.hover\:from-pink-300:hover {
+  --gradient-from-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.hover\:from-pink-400:hover {
+  --gradient-from-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.hover\:from-pink-500:hover {
+  --gradient-from-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.hover\:from-pink-600:hover {
+  --gradient-from-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.hover\:from-pink-700:hover {
+  --gradient-from-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.hover\:from-pink-800:hover {
+  --gradient-from-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.hover\:from-pink-900:hover {
+  --gradient-from-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.hover\:via-transparent:hover {
+  --gradient-via-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:via-current:hover {
+  --gradient-via-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:via-black:hover {
+  --gradient-via-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.hover\:via-white:hover {
+  --gradient-via-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.hover\:via-gray-100:hover {
+  --gradient-via-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.hover\:via-gray-200:hover {
+  --gradient-via-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.hover\:via-gray-300:hover {
+  --gradient-via-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.hover\:via-gray-400:hover {
+  --gradient-via-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.hover\:via-gray-500:hover {
+  --gradient-via-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.hover\:via-gray-600:hover {
+  --gradient-via-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.hover\:via-gray-700:hover {
+  --gradient-via-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.hover\:via-gray-800:hover {
+  --gradient-via-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.hover\:via-gray-900:hover {
+  --gradient-via-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.hover\:via-red-100:hover {
+  --gradient-via-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.hover\:via-red-200:hover {
+  --gradient-via-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.hover\:via-red-300:hover {
+  --gradient-via-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.hover\:via-red-400:hover {
+  --gradient-via-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.hover\:via-red-500:hover {
+  --gradient-via-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.hover\:via-red-600:hover {
+  --gradient-via-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.hover\:via-red-700:hover {
+  --gradient-via-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.hover\:via-red-800:hover {
+  --gradient-via-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.hover\:via-red-900:hover {
+  --gradient-via-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.hover\:via-orange-100:hover {
+  --gradient-via-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.hover\:via-orange-200:hover {
+  --gradient-via-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.hover\:via-orange-300:hover {
+  --gradient-via-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.hover\:via-orange-400:hover {
+  --gradient-via-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.hover\:via-orange-500:hover {
+  --gradient-via-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.hover\:via-orange-600:hover {
+  --gradient-via-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.hover\:via-orange-700:hover {
+  --gradient-via-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.hover\:via-orange-800:hover {
+  --gradient-via-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.hover\:via-orange-900:hover {
+  --gradient-via-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.hover\:via-yellow-100:hover {
+  --gradient-via-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.hover\:via-yellow-200:hover {
+  --gradient-via-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.hover\:via-yellow-300:hover {
+  --gradient-via-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.hover\:via-yellow-400:hover {
+  --gradient-via-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.hover\:via-yellow-500:hover {
+  --gradient-via-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.hover\:via-yellow-600:hover {
+  --gradient-via-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.hover\:via-yellow-700:hover {
+  --gradient-via-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.hover\:via-yellow-800:hover {
+  --gradient-via-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.hover\:via-yellow-900:hover {
+  --gradient-via-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.hover\:via-green-100:hover {
+  --gradient-via-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.hover\:via-green-200:hover {
+  --gradient-via-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.hover\:via-green-300:hover {
+  --gradient-via-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.hover\:via-green-400:hover {
+  --gradient-via-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.hover\:via-green-500:hover {
+  --gradient-via-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.hover\:via-green-600:hover {
+  --gradient-via-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.hover\:via-green-700:hover {
+  --gradient-via-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.hover\:via-green-800:hover {
+  --gradient-via-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.hover\:via-green-900:hover {
+  --gradient-via-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.hover\:via-teal-100:hover {
+  --gradient-via-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.hover\:via-teal-200:hover {
+  --gradient-via-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.hover\:via-teal-300:hover {
+  --gradient-via-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.hover\:via-teal-400:hover {
+  --gradient-via-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.hover\:via-teal-500:hover {
+  --gradient-via-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.hover\:via-teal-600:hover {
+  --gradient-via-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.hover\:via-teal-700:hover {
+  --gradient-via-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.hover\:via-teal-800:hover {
+  --gradient-via-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.hover\:via-teal-900:hover {
+  --gradient-via-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.hover\:via-blue-100:hover {
+  --gradient-via-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.hover\:via-blue-200:hover {
+  --gradient-via-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.hover\:via-blue-300:hover {
+  --gradient-via-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.hover\:via-blue-400:hover {
+  --gradient-via-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.hover\:via-blue-500:hover {
+  --gradient-via-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.hover\:via-blue-600:hover {
+  --gradient-via-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.hover\:via-blue-700:hover {
+  --gradient-via-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.hover\:via-blue-800:hover {
+  --gradient-via-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.hover\:via-blue-900:hover {
+  --gradient-via-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.hover\:via-indigo-100:hover {
+  --gradient-via-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.hover\:via-indigo-200:hover {
+  --gradient-via-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.hover\:via-indigo-300:hover {
+  --gradient-via-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.hover\:via-indigo-400:hover {
+  --gradient-via-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.hover\:via-indigo-500:hover {
+  --gradient-via-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.hover\:via-indigo-600:hover {
+  --gradient-via-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.hover\:via-indigo-700:hover {
+  --gradient-via-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.hover\:via-indigo-800:hover {
+  --gradient-via-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.hover\:via-indigo-900:hover {
+  --gradient-via-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.hover\:via-purple-100:hover {
+  --gradient-via-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.hover\:via-purple-200:hover {
+  --gradient-via-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.hover\:via-purple-300:hover {
+  --gradient-via-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.hover\:via-purple-400:hover {
+  --gradient-via-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.hover\:via-purple-500:hover {
+  --gradient-via-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.hover\:via-purple-600:hover {
+  --gradient-via-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.hover\:via-purple-700:hover {
+  --gradient-via-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.hover\:via-purple-800:hover {
+  --gradient-via-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.hover\:via-purple-900:hover {
+  --gradient-via-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.hover\:via-pink-100:hover {
+  --gradient-via-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.hover\:via-pink-200:hover {
+  --gradient-via-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.hover\:via-pink-300:hover {
+  --gradient-via-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.hover\:via-pink-400:hover {
+  --gradient-via-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.hover\:via-pink-500:hover {
+  --gradient-via-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.hover\:via-pink-600:hover {
+  --gradient-via-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.hover\:via-pink-700:hover {
+  --gradient-via-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.hover\:via-pink-800:hover {
+  --gradient-via-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.hover\:via-pink-900:hover {
+  --gradient-via-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.hover\:to-transparent:hover {
+  --gradient-to-color: transparent;
+}
+
+.hover\:to-current:hover {
+  --gradient-to-color: currentColor;
+}
+
+.hover\:to-black:hover {
+  --gradient-to-color: #000;
+}
+
+.hover\:to-white:hover {
+  --gradient-to-color: #fff;
+}
+
+.hover\:to-gray-100:hover {
+  --gradient-to-color: #f7fafc;
+}
+
+.hover\:to-gray-200:hover {
+  --gradient-to-color: #edf2f7;
+}
+
+.hover\:to-gray-300:hover {
+  --gradient-to-color: #e2e8f0;
+}
+
+.hover\:to-gray-400:hover {
+  --gradient-to-color: #cbd5e0;
+}
+
+.hover\:to-gray-500:hover {
+  --gradient-to-color: #a0aec0;
+}
+
+.hover\:to-gray-600:hover {
+  --gradient-to-color: #718096;
+}
+
+.hover\:to-gray-700:hover {
+  --gradient-to-color: #4a5568;
+}
+
+.hover\:to-gray-800:hover {
+  --gradient-to-color: #2d3748;
+}
+
+.hover\:to-gray-900:hover {
+  --gradient-to-color: #1a202c;
+}
+
+.hover\:to-red-100:hover {
+  --gradient-to-color: #fff5f5;
+}
+
+.hover\:to-red-200:hover {
+  --gradient-to-color: #fed7d7;
+}
+
+.hover\:to-red-300:hover {
+  --gradient-to-color: #feb2b2;
+}
+
+.hover\:to-red-400:hover {
+  --gradient-to-color: #fc8181;
+}
+
+.hover\:to-red-500:hover {
+  --gradient-to-color: #f56565;
+}
+
+.hover\:to-red-600:hover {
+  --gradient-to-color: #e53e3e;
+}
+
+.hover\:to-red-700:hover {
+  --gradient-to-color: #c53030;
+}
+
+.hover\:to-red-800:hover {
+  --gradient-to-color: #9b2c2c;
+}
+
+.hover\:to-red-900:hover {
+  --gradient-to-color: #742a2a;
+}
+
+.hover\:to-orange-100:hover {
+  --gradient-to-color: #fffaf0;
+}
+
+.hover\:to-orange-200:hover {
+  --gradient-to-color: #feebc8;
+}
+
+.hover\:to-orange-300:hover {
+  --gradient-to-color: #fbd38d;
+}
+
+.hover\:to-orange-400:hover {
+  --gradient-to-color: #f6ad55;
+}
+
+.hover\:to-orange-500:hover {
+  --gradient-to-color: #ed8936;
+}
+
+.hover\:to-orange-600:hover {
+  --gradient-to-color: #dd6b20;
+}
+
+.hover\:to-orange-700:hover {
+  --gradient-to-color: #c05621;
+}
+
+.hover\:to-orange-800:hover {
+  --gradient-to-color: #9c4221;
+}
+
+.hover\:to-orange-900:hover {
+  --gradient-to-color: #7b341e;
+}
+
+.hover\:to-yellow-100:hover {
+  --gradient-to-color: #fffff0;
+}
+
+.hover\:to-yellow-200:hover {
+  --gradient-to-color: #fefcbf;
+}
+
+.hover\:to-yellow-300:hover {
+  --gradient-to-color: #faf089;
+}
+
+.hover\:to-yellow-400:hover {
+  --gradient-to-color: #f6e05e;
+}
+
+.hover\:to-yellow-500:hover {
+  --gradient-to-color: #ecc94b;
+}
+
+.hover\:to-yellow-600:hover {
+  --gradient-to-color: #d69e2e;
+}
+
+.hover\:to-yellow-700:hover {
+  --gradient-to-color: #b7791f;
+}
+
+.hover\:to-yellow-800:hover {
+  --gradient-to-color: #975a16;
+}
+
+.hover\:to-yellow-900:hover {
+  --gradient-to-color: #744210;
+}
+
+.hover\:to-green-100:hover {
+  --gradient-to-color: #f0fff4;
+}
+
+.hover\:to-green-200:hover {
+  --gradient-to-color: #c6f6d5;
+}
+
+.hover\:to-green-300:hover {
+  --gradient-to-color: #9ae6b4;
+}
+
+.hover\:to-green-400:hover {
+  --gradient-to-color: #68d391;
+}
+
+.hover\:to-green-500:hover {
+  --gradient-to-color: #48bb78;
+}
+
+.hover\:to-green-600:hover {
+  --gradient-to-color: #38a169;
+}
+
+.hover\:to-green-700:hover {
+  --gradient-to-color: #2f855a;
+}
+
+.hover\:to-green-800:hover {
+  --gradient-to-color: #276749;
+}
+
+.hover\:to-green-900:hover {
+  --gradient-to-color: #22543d;
+}
+
+.hover\:to-teal-100:hover {
+  --gradient-to-color: #e6fffa;
+}
+
+.hover\:to-teal-200:hover {
+  --gradient-to-color: #b2f5ea;
+}
+
+.hover\:to-teal-300:hover {
+  --gradient-to-color: #81e6d9;
+}
+
+.hover\:to-teal-400:hover {
+  --gradient-to-color: #4fd1c5;
+}
+
+.hover\:to-teal-500:hover {
+  --gradient-to-color: #38b2ac;
+}
+
+.hover\:to-teal-600:hover {
+  --gradient-to-color: #319795;
+}
+
+.hover\:to-teal-700:hover {
+  --gradient-to-color: #2c7a7b;
+}
+
+.hover\:to-teal-800:hover {
+  --gradient-to-color: #285e61;
+}
+
+.hover\:to-teal-900:hover {
+  --gradient-to-color: #234e52;
+}
+
+.hover\:to-blue-100:hover {
+  --gradient-to-color: #ebf8ff;
+}
+
+.hover\:to-blue-200:hover {
+  --gradient-to-color: #bee3f8;
+}
+
+.hover\:to-blue-300:hover {
+  --gradient-to-color: #90cdf4;
+}
+
+.hover\:to-blue-400:hover {
+  --gradient-to-color: #63b3ed;
+}
+
+.hover\:to-blue-500:hover {
+  --gradient-to-color: #4299e1;
+}
+
+.hover\:to-blue-600:hover {
+  --gradient-to-color: #3182ce;
+}
+
+.hover\:to-blue-700:hover {
+  --gradient-to-color: #2b6cb0;
+}
+
+.hover\:to-blue-800:hover {
+  --gradient-to-color: #2c5282;
+}
+
+.hover\:to-blue-900:hover {
+  --gradient-to-color: #2a4365;
+}
+
+.hover\:to-indigo-100:hover {
+  --gradient-to-color: #ebf4ff;
+}
+
+.hover\:to-indigo-200:hover {
+  --gradient-to-color: #c3dafe;
+}
+
+.hover\:to-indigo-300:hover {
+  --gradient-to-color: #a3bffa;
+}
+
+.hover\:to-indigo-400:hover {
+  --gradient-to-color: #7f9cf5;
+}
+
+.hover\:to-indigo-500:hover {
+  --gradient-to-color: #667eea;
+}
+
+.hover\:to-indigo-600:hover {
+  --gradient-to-color: #5a67d8;
+}
+
+.hover\:to-indigo-700:hover {
+  --gradient-to-color: #4c51bf;
+}
+
+.hover\:to-indigo-800:hover {
+  --gradient-to-color: #434190;
+}
+
+.hover\:to-indigo-900:hover {
+  --gradient-to-color: #3c366b;
+}
+
+.hover\:to-purple-100:hover {
+  --gradient-to-color: #faf5ff;
+}
+
+.hover\:to-purple-200:hover {
+  --gradient-to-color: #e9d8fd;
+}
+
+.hover\:to-purple-300:hover {
+  --gradient-to-color: #d6bcfa;
+}
+
+.hover\:to-purple-400:hover {
+  --gradient-to-color: #b794f4;
+}
+
+.hover\:to-purple-500:hover {
+  --gradient-to-color: #9f7aea;
+}
+
+.hover\:to-purple-600:hover {
+  --gradient-to-color: #805ad5;
+}
+
+.hover\:to-purple-700:hover {
+  --gradient-to-color: #6b46c1;
+}
+
+.hover\:to-purple-800:hover {
+  --gradient-to-color: #553c9a;
+}
+
+.hover\:to-purple-900:hover {
+  --gradient-to-color: #44337a;
+}
+
+.hover\:to-pink-100:hover {
+  --gradient-to-color: #fff5f7;
+}
+
+.hover\:to-pink-200:hover {
+  --gradient-to-color: #fed7e2;
+}
+
+.hover\:to-pink-300:hover {
+  --gradient-to-color: #fbb6ce;
+}
+
+.hover\:to-pink-400:hover {
+  --gradient-to-color: #f687b3;
+}
+
+.hover\:to-pink-500:hover {
+  --gradient-to-color: #ed64a6;
+}
+
+.hover\:to-pink-600:hover {
+  --gradient-to-color: #d53f8c;
+}
+
+.hover\:to-pink-700:hover {
+  --gradient-to-color: #b83280;
+}
+
+.hover\:to-pink-800:hover {
+  --gradient-to-color: #97266d;
+}
+
+.hover\:to-pink-900:hover {
+  --gradient-to-color: #702459;
+}
+
+.focus\:from-transparent:focus {
+  --gradient-from-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:from-current:focus {
+  --gradient-from-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:from-black:focus {
+  --gradient-from-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:from-white:focus {
+  --gradient-from-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:from-gray-100:focus {
+  --gradient-from-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.focus\:from-gray-200:focus {
+  --gradient-from-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.focus\:from-gray-300:focus {
+  --gradient-from-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.focus\:from-gray-400:focus {
+  --gradient-from-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.focus\:from-gray-500:focus {
+  --gradient-from-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.focus\:from-gray-600:focus {
+  --gradient-from-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.focus\:from-gray-700:focus {
+  --gradient-from-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.focus\:from-gray-800:focus {
+  --gradient-from-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.focus\:from-gray-900:focus {
+  --gradient-from-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.focus\:from-red-100:focus {
+  --gradient-from-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.focus\:from-red-200:focus {
+  --gradient-from-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.focus\:from-red-300:focus {
+  --gradient-from-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.focus\:from-red-400:focus {
+  --gradient-from-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.focus\:from-red-500:focus {
+  --gradient-from-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.focus\:from-red-600:focus {
+  --gradient-from-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.focus\:from-red-700:focus {
+  --gradient-from-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.focus\:from-red-800:focus {
+  --gradient-from-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.focus\:from-red-900:focus {
+  --gradient-from-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.focus\:from-orange-100:focus {
+  --gradient-from-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.focus\:from-orange-200:focus {
+  --gradient-from-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.focus\:from-orange-300:focus {
+  --gradient-from-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.focus\:from-orange-400:focus {
+  --gradient-from-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.focus\:from-orange-500:focus {
+  --gradient-from-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.focus\:from-orange-600:focus {
+  --gradient-from-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.focus\:from-orange-700:focus {
+  --gradient-from-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.focus\:from-orange-800:focus {
+  --gradient-from-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.focus\:from-orange-900:focus {
+  --gradient-from-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.focus\:from-yellow-100:focus {
+  --gradient-from-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.focus\:from-yellow-200:focus {
+  --gradient-from-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.focus\:from-yellow-300:focus {
+  --gradient-from-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.focus\:from-yellow-400:focus {
+  --gradient-from-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.focus\:from-yellow-500:focus {
+  --gradient-from-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.focus\:from-yellow-600:focus {
+  --gradient-from-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.focus\:from-yellow-700:focus {
+  --gradient-from-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.focus\:from-yellow-800:focus {
+  --gradient-from-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.focus\:from-yellow-900:focus {
+  --gradient-from-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.focus\:from-green-100:focus {
+  --gradient-from-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.focus\:from-green-200:focus {
+  --gradient-from-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.focus\:from-green-300:focus {
+  --gradient-from-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.focus\:from-green-400:focus {
+  --gradient-from-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.focus\:from-green-500:focus {
+  --gradient-from-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.focus\:from-green-600:focus {
+  --gradient-from-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.focus\:from-green-700:focus {
+  --gradient-from-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.focus\:from-green-800:focus {
+  --gradient-from-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.focus\:from-green-900:focus {
+  --gradient-from-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.focus\:from-teal-100:focus {
+  --gradient-from-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.focus\:from-teal-200:focus {
+  --gradient-from-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.focus\:from-teal-300:focus {
+  --gradient-from-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.focus\:from-teal-400:focus {
+  --gradient-from-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.focus\:from-teal-500:focus {
+  --gradient-from-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.focus\:from-teal-600:focus {
+  --gradient-from-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.focus\:from-teal-700:focus {
+  --gradient-from-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.focus\:from-teal-800:focus {
+  --gradient-from-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.focus\:from-teal-900:focus {
+  --gradient-from-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.focus\:from-blue-100:focus {
+  --gradient-from-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.focus\:from-blue-200:focus {
+  --gradient-from-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.focus\:from-blue-300:focus {
+  --gradient-from-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.focus\:from-blue-400:focus {
+  --gradient-from-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.focus\:from-blue-500:focus {
+  --gradient-from-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.focus\:from-blue-600:focus {
+  --gradient-from-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.focus\:from-blue-700:focus {
+  --gradient-from-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.focus\:from-blue-800:focus {
+  --gradient-from-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.focus\:from-blue-900:focus {
+  --gradient-from-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.focus\:from-indigo-100:focus {
+  --gradient-from-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.focus\:from-indigo-200:focus {
+  --gradient-from-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.focus\:from-indigo-300:focus {
+  --gradient-from-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.focus\:from-indigo-400:focus {
+  --gradient-from-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.focus\:from-indigo-500:focus {
+  --gradient-from-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.focus\:from-indigo-600:focus {
+  --gradient-from-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.focus\:from-indigo-700:focus {
+  --gradient-from-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.focus\:from-indigo-800:focus {
+  --gradient-from-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.focus\:from-indigo-900:focus {
+  --gradient-from-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.focus\:from-purple-100:focus {
+  --gradient-from-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.focus\:from-purple-200:focus {
+  --gradient-from-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.focus\:from-purple-300:focus {
+  --gradient-from-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.focus\:from-purple-400:focus {
+  --gradient-from-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.focus\:from-purple-500:focus {
+  --gradient-from-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.focus\:from-purple-600:focus {
+  --gradient-from-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.focus\:from-purple-700:focus {
+  --gradient-from-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.focus\:from-purple-800:focus {
+  --gradient-from-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.focus\:from-purple-900:focus {
+  --gradient-from-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.focus\:from-pink-100:focus {
+  --gradient-from-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.focus\:from-pink-200:focus {
+  --gradient-from-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.focus\:from-pink-300:focus {
+  --gradient-from-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.focus\:from-pink-400:focus {
+  --gradient-from-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.focus\:from-pink-500:focus {
+  --gradient-from-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.focus\:from-pink-600:focus {
+  --gradient-from-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.focus\:from-pink-700:focus {
+  --gradient-from-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.focus\:from-pink-800:focus {
+  --gradient-from-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.focus\:from-pink-900:focus {
+  --gradient-from-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.focus\:via-transparent:focus {
+  --gradient-via-color: transparent;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:via-current:focus {
+  --gradient-via-color: currentColor;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:via-black:focus {
+  --gradient-via-color: #000;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+}
+
+.focus\:via-white:focus {
+  --gradient-via-color: #fff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+}
+
+.focus\:via-gray-100:focus {
+  --gradient-via-color: #f7fafc;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+}
+
+.focus\:via-gray-200:focus {
+  --gradient-via-color: #edf2f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+}
+
+.focus\:via-gray-300:focus {
+  --gradient-via-color: #e2e8f0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+}
+
+.focus\:via-gray-400:focus {
+  --gradient-via-color: #cbd5e0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+}
+
+.focus\:via-gray-500:focus {
+  --gradient-via-color: #a0aec0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+}
+
+.focus\:via-gray-600:focus {
+  --gradient-via-color: #718096;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+}
+
+.focus\:via-gray-700:focus {
+  --gradient-via-color: #4a5568;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+}
+
+.focus\:via-gray-800:focus {
+  --gradient-via-color: #2d3748;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+}
+
+.focus\:via-gray-900:focus {
+  --gradient-via-color: #1a202c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+}
+
+.focus\:via-red-100:focus {
+  --gradient-via-color: #fff5f5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+}
+
+.focus\:via-red-200:focus {
+  --gradient-via-color: #fed7d7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+}
+
+.focus\:via-red-300:focus {
+  --gradient-via-color: #feb2b2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+}
+
+.focus\:via-red-400:focus {
+  --gradient-via-color: #fc8181;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+}
+
+.focus\:via-red-500:focus {
+  --gradient-via-color: #f56565;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+}
+
+.focus\:via-red-600:focus {
+  --gradient-via-color: #e53e3e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+}
+
+.focus\:via-red-700:focus {
+  --gradient-via-color: #c53030;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+}
+
+.focus\:via-red-800:focus {
+  --gradient-via-color: #9b2c2c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+}
+
+.focus\:via-red-900:focus {
+  --gradient-via-color: #742a2a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+}
+
+.focus\:via-orange-100:focus {
+  --gradient-via-color: #fffaf0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+}
+
+.focus\:via-orange-200:focus {
+  --gradient-via-color: #feebc8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+}
+
+.focus\:via-orange-300:focus {
+  --gradient-via-color: #fbd38d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+}
+
+.focus\:via-orange-400:focus {
+  --gradient-via-color: #f6ad55;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+}
+
+.focus\:via-orange-500:focus {
+  --gradient-via-color: #ed8936;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+}
+
+.focus\:via-orange-600:focus {
+  --gradient-via-color: #dd6b20;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+}
+
+.focus\:via-orange-700:focus {
+  --gradient-via-color: #c05621;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+}
+
+.focus\:via-orange-800:focus {
+  --gradient-via-color: #9c4221;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+}
+
+.focus\:via-orange-900:focus {
+  --gradient-via-color: #7b341e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+}
+
+.focus\:via-yellow-100:focus {
+  --gradient-via-color: #fffff0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+}
+
+.focus\:via-yellow-200:focus {
+  --gradient-via-color: #fefcbf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+}
+
+.focus\:via-yellow-300:focus {
+  --gradient-via-color: #faf089;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+}
+
+.focus\:via-yellow-400:focus {
+  --gradient-via-color: #f6e05e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+}
+
+.focus\:via-yellow-500:focus {
+  --gradient-via-color: #ecc94b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+}
+
+.focus\:via-yellow-600:focus {
+  --gradient-via-color: #d69e2e;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+}
+
+.focus\:via-yellow-700:focus {
+  --gradient-via-color: #b7791f;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+}
+
+.focus\:via-yellow-800:focus {
+  --gradient-via-color: #975a16;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+}
+
+.focus\:via-yellow-900:focus {
+  --gradient-via-color: #744210;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+}
+
+.focus\:via-green-100:focus {
+  --gradient-via-color: #f0fff4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+}
+
+.focus\:via-green-200:focus {
+  --gradient-via-color: #c6f6d5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+}
+
+.focus\:via-green-300:focus {
+  --gradient-via-color: #9ae6b4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+}
+
+.focus\:via-green-400:focus {
+  --gradient-via-color: #68d391;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+}
+
+.focus\:via-green-500:focus {
+  --gradient-via-color: #48bb78;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+}
+
+.focus\:via-green-600:focus {
+  --gradient-via-color: #38a169;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+}
+
+.focus\:via-green-700:focus {
+  --gradient-via-color: #2f855a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+}
+
+.focus\:via-green-800:focus {
+  --gradient-via-color: #276749;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+}
+
+.focus\:via-green-900:focus {
+  --gradient-via-color: #22543d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+}
+
+.focus\:via-teal-100:focus {
+  --gradient-via-color: #e6fffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+}
+
+.focus\:via-teal-200:focus {
+  --gradient-via-color: #b2f5ea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+}
+
+.focus\:via-teal-300:focus {
+  --gradient-via-color: #81e6d9;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+}
+
+.focus\:via-teal-400:focus {
+  --gradient-via-color: #4fd1c5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+}
+
+.focus\:via-teal-500:focus {
+  --gradient-via-color: #38b2ac;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+}
+
+.focus\:via-teal-600:focus {
+  --gradient-via-color: #319795;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+}
+
+.focus\:via-teal-700:focus {
+  --gradient-via-color: #2c7a7b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+}
+
+.focus\:via-teal-800:focus {
+  --gradient-via-color: #285e61;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+}
+
+.focus\:via-teal-900:focus {
+  --gradient-via-color: #234e52;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+}
+
+.focus\:via-blue-100:focus {
+  --gradient-via-color: #ebf8ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+}
+
+.focus\:via-blue-200:focus {
+  --gradient-via-color: #bee3f8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+}
+
+.focus\:via-blue-300:focus {
+  --gradient-via-color: #90cdf4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+}
+
+.focus\:via-blue-400:focus {
+  --gradient-via-color: #63b3ed;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+}
+
+.focus\:via-blue-500:focus {
+  --gradient-via-color: #4299e1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+}
+
+.focus\:via-blue-600:focus {
+  --gradient-via-color: #3182ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+}
+
+.focus\:via-blue-700:focus {
+  --gradient-via-color: #2b6cb0;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+}
+
+.focus\:via-blue-800:focus {
+  --gradient-via-color: #2c5282;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+}
+
+.focus\:via-blue-900:focus {
+  --gradient-via-color: #2a4365;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+}
+
+.focus\:via-indigo-100:focus {
+  --gradient-via-color: #ebf4ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+}
+
+.focus\:via-indigo-200:focus {
+  --gradient-via-color: #c3dafe;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+}
+
+.focus\:via-indigo-300:focus {
+  --gradient-via-color: #a3bffa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+}
+
+.focus\:via-indigo-400:focus {
+  --gradient-via-color: #7f9cf5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+}
+
+.focus\:via-indigo-500:focus {
+  --gradient-via-color: #667eea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+}
+
+.focus\:via-indigo-600:focus {
+  --gradient-via-color: #5a67d8;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+}
+
+.focus\:via-indigo-700:focus {
+  --gradient-via-color: #4c51bf;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+}
+
+.focus\:via-indigo-800:focus {
+  --gradient-via-color: #434190;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+}
+
+.focus\:via-indigo-900:focus {
+  --gradient-via-color: #3c366b;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+}
+
+.focus\:via-purple-100:focus {
+  --gradient-via-color: #faf5ff;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+}
+
+.focus\:via-purple-200:focus {
+  --gradient-via-color: #e9d8fd;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+}
+
+.focus\:via-purple-300:focus {
+  --gradient-via-color: #d6bcfa;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+}
+
+.focus\:via-purple-400:focus {
+  --gradient-via-color: #b794f4;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+}
+
+.focus\:via-purple-500:focus {
+  --gradient-via-color: #9f7aea;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+}
+
+.focus\:via-purple-600:focus {
+  --gradient-via-color: #805ad5;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+}
+
+.focus\:via-purple-700:focus {
+  --gradient-via-color: #6b46c1;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+}
+
+.focus\:via-purple-800:focus {
+  --gradient-via-color: #553c9a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+}
+
+.focus\:via-purple-900:focus {
+  --gradient-via-color: #44337a;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+}
+
+.focus\:via-pink-100:focus {
+  --gradient-via-color: #fff5f7;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+}
+
+.focus\:via-pink-200:focus {
+  --gradient-via-color: #fed7e2;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+}
+
+.focus\:via-pink-300:focus {
+  --gradient-via-color: #fbb6ce;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+}
+
+.focus\:via-pink-400:focus {
+  --gradient-via-color: #f687b3;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+}
+
+.focus\:via-pink-500:focus {
+  --gradient-via-color: #ed64a6;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+}
+
+.focus\:via-pink-600:focus {
+  --gradient-via-color: #d53f8c;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+}
+
+.focus\:via-pink-700:focus {
+  --gradient-via-color: #b83280;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+}
+
+.focus\:via-pink-800:focus {
+  --gradient-via-color: #97266d;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+}
+
+.focus\:via-pink-900:focus {
+  --gradient-via-color: #702459;
+  --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+}
+
+.focus\:to-transparent:focus {
+  --gradient-to-color: transparent;
+}
+
+.focus\:to-current:focus {
+  --gradient-to-color: currentColor;
+}
+
+.focus\:to-black:focus {
+  --gradient-to-color: #000;
+}
+
+.focus\:to-white:focus {
+  --gradient-to-color: #fff;
+}
+
+.focus\:to-gray-100:focus {
+  --gradient-to-color: #f7fafc;
+}
+
+.focus\:to-gray-200:focus {
+  --gradient-to-color: #edf2f7;
+}
+
+.focus\:to-gray-300:focus {
+  --gradient-to-color: #e2e8f0;
+}
+
+.focus\:to-gray-400:focus {
+  --gradient-to-color: #cbd5e0;
+}
+
+.focus\:to-gray-500:focus {
+  --gradient-to-color: #a0aec0;
+}
+
+.focus\:to-gray-600:focus {
+  --gradient-to-color: #718096;
+}
+
+.focus\:to-gray-700:focus {
+  --gradient-to-color: #4a5568;
+}
+
+.focus\:to-gray-800:focus {
+  --gradient-to-color: #2d3748;
+}
+
+.focus\:to-gray-900:focus {
+  --gradient-to-color: #1a202c;
+}
+
+.focus\:to-red-100:focus {
+  --gradient-to-color: #fff5f5;
+}
+
+.focus\:to-red-200:focus {
+  --gradient-to-color: #fed7d7;
+}
+
+.focus\:to-red-300:focus {
+  --gradient-to-color: #feb2b2;
+}
+
+.focus\:to-red-400:focus {
+  --gradient-to-color: #fc8181;
+}
+
+.focus\:to-red-500:focus {
+  --gradient-to-color: #f56565;
+}
+
+.focus\:to-red-600:focus {
+  --gradient-to-color: #e53e3e;
+}
+
+.focus\:to-red-700:focus {
+  --gradient-to-color: #c53030;
+}
+
+.focus\:to-red-800:focus {
+  --gradient-to-color: #9b2c2c;
+}
+
+.focus\:to-red-900:focus {
+  --gradient-to-color: #742a2a;
+}
+
+.focus\:to-orange-100:focus {
+  --gradient-to-color: #fffaf0;
+}
+
+.focus\:to-orange-200:focus {
+  --gradient-to-color: #feebc8;
+}
+
+.focus\:to-orange-300:focus {
+  --gradient-to-color: #fbd38d;
+}
+
+.focus\:to-orange-400:focus {
+  --gradient-to-color: #f6ad55;
+}
+
+.focus\:to-orange-500:focus {
+  --gradient-to-color: #ed8936;
+}
+
+.focus\:to-orange-600:focus {
+  --gradient-to-color: #dd6b20;
+}
+
+.focus\:to-orange-700:focus {
+  --gradient-to-color: #c05621;
+}
+
+.focus\:to-orange-800:focus {
+  --gradient-to-color: #9c4221;
+}
+
+.focus\:to-orange-900:focus {
+  --gradient-to-color: #7b341e;
+}
+
+.focus\:to-yellow-100:focus {
+  --gradient-to-color: #fffff0;
+}
+
+.focus\:to-yellow-200:focus {
+  --gradient-to-color: #fefcbf;
+}
+
+.focus\:to-yellow-300:focus {
+  --gradient-to-color: #faf089;
+}
+
+.focus\:to-yellow-400:focus {
+  --gradient-to-color: #f6e05e;
+}
+
+.focus\:to-yellow-500:focus {
+  --gradient-to-color: #ecc94b;
+}
+
+.focus\:to-yellow-600:focus {
+  --gradient-to-color: #d69e2e;
+}
+
+.focus\:to-yellow-700:focus {
+  --gradient-to-color: #b7791f;
+}
+
+.focus\:to-yellow-800:focus {
+  --gradient-to-color: #975a16;
+}
+
+.focus\:to-yellow-900:focus {
+  --gradient-to-color: #744210;
+}
+
+.focus\:to-green-100:focus {
+  --gradient-to-color: #f0fff4;
+}
+
+.focus\:to-green-200:focus {
+  --gradient-to-color: #c6f6d5;
+}
+
+.focus\:to-green-300:focus {
+  --gradient-to-color: #9ae6b4;
+}
+
+.focus\:to-green-400:focus {
+  --gradient-to-color: #68d391;
+}
+
+.focus\:to-green-500:focus {
+  --gradient-to-color: #48bb78;
+}
+
+.focus\:to-green-600:focus {
+  --gradient-to-color: #38a169;
+}
+
+.focus\:to-green-700:focus {
+  --gradient-to-color: #2f855a;
+}
+
+.focus\:to-green-800:focus {
+  --gradient-to-color: #276749;
+}
+
+.focus\:to-green-900:focus {
+  --gradient-to-color: #22543d;
+}
+
+.focus\:to-teal-100:focus {
+  --gradient-to-color: #e6fffa;
+}
+
+.focus\:to-teal-200:focus {
+  --gradient-to-color: #b2f5ea;
+}
+
+.focus\:to-teal-300:focus {
+  --gradient-to-color: #81e6d9;
+}
+
+.focus\:to-teal-400:focus {
+  --gradient-to-color: #4fd1c5;
+}
+
+.focus\:to-teal-500:focus {
+  --gradient-to-color: #38b2ac;
+}
+
+.focus\:to-teal-600:focus {
+  --gradient-to-color: #319795;
+}
+
+.focus\:to-teal-700:focus {
+  --gradient-to-color: #2c7a7b;
+}
+
+.focus\:to-teal-800:focus {
+  --gradient-to-color: #285e61;
+}
+
+.focus\:to-teal-900:focus {
+  --gradient-to-color: #234e52;
+}
+
+.focus\:to-blue-100:focus {
+  --gradient-to-color: #ebf8ff;
+}
+
+.focus\:to-blue-200:focus {
+  --gradient-to-color: #bee3f8;
+}
+
+.focus\:to-blue-300:focus {
+  --gradient-to-color: #90cdf4;
+}
+
+.focus\:to-blue-400:focus {
+  --gradient-to-color: #63b3ed;
+}
+
+.focus\:to-blue-500:focus {
+  --gradient-to-color: #4299e1;
+}
+
+.focus\:to-blue-600:focus {
+  --gradient-to-color: #3182ce;
+}
+
+.focus\:to-blue-700:focus {
+  --gradient-to-color: #2b6cb0;
+}
+
+.focus\:to-blue-800:focus {
+  --gradient-to-color: #2c5282;
+}
+
+.focus\:to-blue-900:focus {
+  --gradient-to-color: #2a4365;
+}
+
+.focus\:to-indigo-100:focus {
+  --gradient-to-color: #ebf4ff;
+}
+
+.focus\:to-indigo-200:focus {
+  --gradient-to-color: #c3dafe;
+}
+
+.focus\:to-indigo-300:focus {
+  --gradient-to-color: #a3bffa;
+}
+
+.focus\:to-indigo-400:focus {
+  --gradient-to-color: #7f9cf5;
+}
+
+.focus\:to-indigo-500:focus {
+  --gradient-to-color: #667eea;
+}
+
+.focus\:to-indigo-600:focus {
+  --gradient-to-color: #5a67d8;
+}
+
+.focus\:to-indigo-700:focus {
+  --gradient-to-color: #4c51bf;
+}
+
+.focus\:to-indigo-800:focus {
+  --gradient-to-color: #434190;
+}
+
+.focus\:to-indigo-900:focus {
+  --gradient-to-color: #3c366b;
+}
+
+.focus\:to-purple-100:focus {
+  --gradient-to-color: #faf5ff;
+}
+
+.focus\:to-purple-200:focus {
+  --gradient-to-color: #e9d8fd;
+}
+
+.focus\:to-purple-300:focus {
+  --gradient-to-color: #d6bcfa;
+}
+
+.focus\:to-purple-400:focus {
+  --gradient-to-color: #b794f4;
+}
+
+.focus\:to-purple-500:focus {
+  --gradient-to-color: #9f7aea;
+}
+
+.focus\:to-purple-600:focus {
+  --gradient-to-color: #805ad5;
+}
+
+.focus\:to-purple-700:focus {
+  --gradient-to-color: #6b46c1;
+}
+
+.focus\:to-purple-800:focus {
+  --gradient-to-color: #553c9a;
+}
+
+.focus\:to-purple-900:focus {
+  --gradient-to-color: #44337a;
+}
+
+.focus\:to-pink-100:focus {
+  --gradient-to-color: #fff5f7;
+}
+
+.focus\:to-pink-200:focus {
+  --gradient-to-color: #fed7e2;
+}
+
+.focus\:to-pink-300:focus {
+  --gradient-to-color: #fbb6ce;
+}
+
+.focus\:to-pink-400:focus {
+  --gradient-to-color: #f687b3;
+}
+
+.focus\:to-pink-500:focus {
+  --gradient-to-color: #ed64a6;
+}
+
+.focus\:to-pink-600:focus {
+  --gradient-to-color: #d53f8c;
+}
+
+.focus\:to-pink-700:focus {
+  --gradient-to-color: #b83280;
+}
+
+.focus\:to-pink-800:focus {
+  --gradient-to-color: #97266d;
+}
+
+.focus\:to-pink-900:focus {
+  --gradient-to-color: #702459;
+}
+
+.bg-opacity-0 {
+  --bg-opacity: 0;
+}
+
+.bg-opacity-25 {
+  --bg-opacity: 0.25;
+}
+
+.bg-opacity-50 {
+  --bg-opacity: 0.5;
+}
+
+.bg-opacity-75 {
+  --bg-opacity: 0.75;
+}
+
+.bg-opacity-100 {
+  --bg-opacity: 1;
+}
+
+.hover\:bg-opacity-0:hover {
+  --bg-opacity: 0;
+}
+
+.hover\:bg-opacity-25:hover {
+  --bg-opacity: 0.25;
+}
+
+.hover\:bg-opacity-50:hover {
+  --bg-opacity: 0.5;
+}
+
+.hover\:bg-opacity-75:hover {
+  --bg-opacity: 0.75;
+}
+
+.hover\:bg-opacity-100:hover {
+  --bg-opacity: 1;
+}
+
+.focus\:bg-opacity-0:focus {
+  --bg-opacity: 0;
+}
+
+.focus\:bg-opacity-25:focus {
+  --bg-opacity: 0.25;
+}
+
+.focus\:bg-opacity-50:focus {
+  --bg-opacity: 0.5;
+}
+
+.focus\:bg-opacity-75:focus {
+  --bg-opacity: 0.75;
+}
+
+.focus\:bg-opacity-100:focus {
+  --bg-opacity: 1;
+}
+
+.bg-bottom {
+  background-position: bottom;
+}
+
+.bg-center {
+  background-position: center;
+}
+
+.bg-left {
+  background-position: left;
+}
+
+.bg-left-bottom {
+  background-position: left bottom;
+}
+
+.bg-left-top {
+  background-position: left top;
+}
+
+.bg-right {
+  background-position: right;
+}
+
+.bg-right-bottom {
+  background-position: right bottom;
+}
+
+.bg-right-top {
+  background-position: right top;
+}
+
+.bg-top {
+  background-position: top;
+}
+
+.bg-repeat {
+  background-repeat: repeat;
+}
+
+.bg-no-repeat {
+  background-repeat: no-repeat;
+}
+
+.bg-repeat-x {
+  background-repeat: repeat-x;
+}
+
+.bg-repeat-y {
+  background-repeat: repeat-y;
+}
+
+.bg-repeat-round {
+  background-repeat: round;
+}
+
+.bg-repeat-space {
+  background-repeat: space;
+}
+
+.bg-auto {
+  background-size: auto;
+}
+
+.bg-cover {
+  background-size: cover;
+}
+
+.bg-contain {
+  background-size: contain;
+}
+
+.border-collapse {
+  border-collapse: collapse;
+}
+
+.border-separate {
+  border-collapse: separate;
+}
+
+.border-transparent {
+  border-color: transparent;
+}
+
+.border-current {
+  border-color: currentColor;
+}
+
+.border-black {
+  --border-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--border-opacity));
+}
+
+.border-white {
+  --border-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--border-opacity));
+}
+
+.border-gray-100 {
+  --border-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--border-opacity));
+}
+
+.border-gray-200 {
+  --border-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--border-opacity));
+}
+
+.border-gray-300 {
+  --border-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--border-opacity));
+}
+
+.border-gray-400 {
+  --border-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--border-opacity));
+}
+
+.border-gray-500 {
+  --border-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--border-opacity));
+}
+
+.border-gray-600 {
+  --border-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--border-opacity));
+}
+
+.border-gray-700 {
+  --border-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--border-opacity));
+}
+
+.border-gray-800 {
+  --border-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--border-opacity));
+}
+
+.border-gray-900 {
+  --border-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--border-opacity));
+}
+
+.border-red-100 {
+  --border-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--border-opacity));
+}
+
+.border-red-200 {
+  --border-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--border-opacity));
+}
+
+.border-red-300 {
+  --border-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--border-opacity));
+}
+
+.border-red-400 {
+  --border-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--border-opacity));
+}
+
+.border-red-500 {
+  --border-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--border-opacity));
+}
+
+.border-red-600 {
+  --border-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--border-opacity));
+}
+
+.border-red-700 {
+  --border-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--border-opacity));
+}
+
+.border-red-800 {
+  --border-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--border-opacity));
+}
+
+.border-red-900 {
+  --border-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--border-opacity));
+}
+
+.border-orange-100 {
+  --border-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--border-opacity));
+}
+
+.border-orange-200 {
+  --border-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--border-opacity));
+}
+
+.border-orange-300 {
+  --border-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--border-opacity));
+}
+
+.border-orange-400 {
+  --border-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--border-opacity));
+}
+
+.border-orange-500 {
+  --border-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--border-opacity));
+}
+
+.border-orange-600 {
+  --border-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--border-opacity));
+}
+
+.border-orange-700 {
+  --border-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--border-opacity));
+}
+
+.border-orange-800 {
+  --border-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--border-opacity));
+}
+
+.border-orange-900 {
+  --border-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--border-opacity));
+}
+
+.border-yellow-100 {
+  --border-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--border-opacity));
+}
+
+.border-yellow-200 {
+  --border-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--border-opacity));
+}
+
+.border-yellow-300 {
+  --border-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--border-opacity));
+}
+
+.border-yellow-400 {
+  --border-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--border-opacity));
+}
+
+.border-yellow-500 {
+  --border-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--border-opacity));
+}
+
+.border-yellow-600 {
+  --border-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--border-opacity));
+}
+
+.border-yellow-700 {
+  --border-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--border-opacity));
+}
+
+.border-yellow-800 {
+  --border-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--border-opacity));
+}
+
+.border-yellow-900 {
+  --border-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--border-opacity));
+}
+
+.border-green-100 {
+  --border-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--border-opacity));
+}
+
+.border-green-200 {
+  --border-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--border-opacity));
+}
+
+.border-green-300 {
+  --border-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--border-opacity));
+}
+
+.border-green-400 {
+  --border-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--border-opacity));
+}
+
+.border-green-500 {
+  --border-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--border-opacity));
+}
+
+.border-green-600 {
+  --border-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--border-opacity));
+}
+
+.border-green-700 {
+  --border-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--border-opacity));
+}
+
+.border-green-800 {
+  --border-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--border-opacity));
+}
+
+.border-green-900 {
+  --border-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--border-opacity));
+}
+
+.border-teal-100 {
+  --border-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--border-opacity));
+}
+
+.border-teal-200 {
+  --border-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--border-opacity));
+}
+
+.border-teal-300 {
+  --border-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--border-opacity));
+}
+
+.border-teal-400 {
+  --border-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--border-opacity));
+}
+
+.border-teal-500 {
+  --border-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--border-opacity));
+}
+
+.border-teal-600 {
+  --border-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--border-opacity));
+}
+
+.border-teal-700 {
+  --border-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--border-opacity));
+}
+
+.border-teal-800 {
+  --border-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--border-opacity));
+}
+
+.border-teal-900 {
+  --border-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--border-opacity));
+}
+
+.border-blue-100 {
+  --border-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--border-opacity));
+}
+
+.border-blue-200 {
+  --border-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--border-opacity));
+}
+
+.border-blue-300 {
+  --border-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--border-opacity));
+}
+
+.border-blue-400 {
+  --border-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--border-opacity));
+}
+
+.border-blue-500 {
+  --border-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--border-opacity));
+}
+
+.border-blue-600 {
+  --border-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--border-opacity));
+}
+
+.border-blue-700 {
+  --border-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--border-opacity));
+}
+
+.border-blue-800 {
+  --border-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--border-opacity));
+}
+
+.border-blue-900 {
+  --border-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--border-opacity));
+}
+
+.border-indigo-100 {
+  --border-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--border-opacity));
+}
+
+.border-indigo-200 {
+  --border-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--border-opacity));
+}
+
+.border-indigo-300 {
+  --border-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--border-opacity));
+}
+
+.border-indigo-400 {
+  --border-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--border-opacity));
+}
+
+.border-indigo-500 {
+  --border-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--border-opacity));
+}
+
+.border-indigo-600 {
+  --border-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--border-opacity));
+}
+
+.border-indigo-700 {
+  --border-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--border-opacity));
+}
+
+.border-indigo-800 {
+  --border-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--border-opacity));
+}
+
+.border-indigo-900 {
+  --border-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--border-opacity));
+}
+
+.border-purple-100 {
+  --border-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--border-opacity));
+}
+
+.border-purple-200 {
+  --border-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--border-opacity));
+}
+
+.border-purple-300 {
+  --border-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--border-opacity));
+}
+
+.border-purple-400 {
+  --border-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--border-opacity));
+}
+
+.border-purple-500 {
+  --border-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--border-opacity));
+}
+
+.border-purple-600 {
+  --border-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--border-opacity));
+}
+
+.border-purple-700 {
+  --border-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--border-opacity));
+}
+
+.border-purple-800 {
+  --border-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--border-opacity));
+}
+
+.border-purple-900 {
+  --border-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--border-opacity));
+}
+
+.border-pink-100 {
+  --border-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--border-opacity));
+}
+
+.border-pink-200 {
+  --border-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--border-opacity));
+}
+
+.border-pink-300 {
+  --border-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--border-opacity));
+}
+
+.border-pink-400 {
+  --border-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--border-opacity));
+}
+
+.border-pink-500 {
+  --border-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--border-opacity));
+}
+
+.border-pink-600 {
+  --border-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--border-opacity));
+}
+
+.border-pink-700 {
+  --border-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--border-opacity));
+}
+
+.border-pink-800 {
+  --border-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--border-opacity));
+}
+
+.border-pink-900 {
+  --border-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--border-opacity));
+}
+
+.hover\:border-transparent:hover {
+  border-color: transparent;
+}
+
+.hover\:border-current:hover {
+  border-color: currentColor;
+}
+
+.hover\:border-black:hover {
+  --border-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--border-opacity));
+}
+
+.hover\:border-white:hover {
+  --border-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--border-opacity));
+}
+
+.hover\:border-gray-100:hover {
+  --border-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--border-opacity));
+}
+
+.hover\:border-gray-200:hover {
+  --border-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--border-opacity));
+}
+
+.hover\:border-gray-300:hover {
+  --border-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--border-opacity));
+}
+
+.hover\:border-gray-400:hover {
+  --border-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--border-opacity));
+}
+
+.hover\:border-gray-500:hover {
+  --border-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--border-opacity));
+}
+
+.hover\:border-gray-600:hover {
+  --border-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--border-opacity));
+}
+
+.hover\:border-gray-700:hover {
+  --border-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--border-opacity));
+}
+
+.hover\:border-gray-800:hover {
+  --border-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--border-opacity));
+}
+
+.hover\:border-gray-900:hover {
+  --border-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--border-opacity));
+}
+
+.hover\:border-red-100:hover {
+  --border-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--border-opacity));
+}
+
+.hover\:border-red-200:hover {
+  --border-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--border-opacity));
+}
+
+.hover\:border-red-300:hover {
+  --border-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--border-opacity));
+}
+
+.hover\:border-red-400:hover {
+  --border-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--border-opacity));
+}
+
+.hover\:border-red-500:hover {
+  --border-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--border-opacity));
+}
+
+.hover\:border-red-600:hover {
+  --border-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--border-opacity));
+}
+
+.hover\:border-red-700:hover {
+  --border-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--border-opacity));
+}
+
+.hover\:border-red-800:hover {
+  --border-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--border-opacity));
+}
+
+.hover\:border-red-900:hover {
+  --border-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--border-opacity));
+}
+
+.hover\:border-orange-100:hover {
+  --border-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--border-opacity));
+}
+
+.hover\:border-orange-200:hover {
+  --border-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--border-opacity));
+}
+
+.hover\:border-orange-300:hover {
+  --border-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--border-opacity));
+}
+
+.hover\:border-orange-400:hover {
+  --border-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--border-opacity));
+}
+
+.hover\:border-orange-500:hover {
+  --border-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--border-opacity));
+}
+
+.hover\:border-orange-600:hover {
+  --border-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--border-opacity));
+}
+
+.hover\:border-orange-700:hover {
+  --border-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--border-opacity));
+}
+
+.hover\:border-orange-800:hover {
+  --border-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--border-opacity));
+}
+
+.hover\:border-orange-900:hover {
+  --border-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--border-opacity));
+}
+
+.hover\:border-yellow-100:hover {
+  --border-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--border-opacity));
+}
+
+.hover\:border-yellow-200:hover {
+  --border-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--border-opacity));
+}
+
+.hover\:border-yellow-300:hover {
+  --border-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--border-opacity));
+}
+
+.hover\:border-yellow-400:hover {
+  --border-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--border-opacity));
+}
+
+.hover\:border-yellow-500:hover {
+  --border-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--border-opacity));
+}
+
+.hover\:border-yellow-600:hover {
+  --border-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--border-opacity));
+}
+
+.hover\:border-yellow-700:hover {
+  --border-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--border-opacity));
+}
+
+.hover\:border-yellow-800:hover {
+  --border-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--border-opacity));
+}
+
+.hover\:border-yellow-900:hover {
+  --border-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--border-opacity));
+}
+
+.hover\:border-green-100:hover {
+  --border-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--border-opacity));
+}
+
+.hover\:border-green-200:hover {
+  --border-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--border-opacity));
+}
+
+.hover\:border-green-300:hover {
+  --border-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--border-opacity));
+}
+
+.hover\:border-green-400:hover {
+  --border-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--border-opacity));
+}
+
+.hover\:border-green-500:hover {
+  --border-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--border-opacity));
+}
+
+.hover\:border-green-600:hover {
+  --border-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--border-opacity));
+}
+
+.hover\:border-green-700:hover {
+  --border-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--border-opacity));
+}
+
+.hover\:border-green-800:hover {
+  --border-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--border-opacity));
+}
+
+.hover\:border-green-900:hover {
+  --border-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--border-opacity));
+}
+
+.hover\:border-teal-100:hover {
+  --border-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--border-opacity));
+}
+
+.hover\:border-teal-200:hover {
+  --border-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--border-opacity));
+}
+
+.hover\:border-teal-300:hover {
+  --border-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--border-opacity));
+}
+
+.hover\:border-teal-400:hover {
+  --border-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--border-opacity));
+}
+
+.hover\:border-teal-500:hover {
+  --border-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--border-opacity));
+}
+
+.hover\:border-teal-600:hover {
+  --border-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--border-opacity));
+}
+
+.hover\:border-teal-700:hover {
+  --border-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--border-opacity));
+}
+
+.hover\:border-teal-800:hover {
+  --border-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--border-opacity));
+}
+
+.hover\:border-teal-900:hover {
+  --border-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--border-opacity));
+}
+
+.hover\:border-blue-100:hover {
+  --border-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--border-opacity));
+}
+
+.hover\:border-blue-200:hover {
+  --border-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--border-opacity));
+}
+
+.hover\:border-blue-300:hover {
+  --border-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--border-opacity));
+}
+
+.hover\:border-blue-400:hover {
+  --border-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--border-opacity));
+}
+
+.hover\:border-blue-500:hover {
+  --border-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--border-opacity));
+}
+
+.hover\:border-blue-600:hover {
+  --border-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--border-opacity));
+}
+
+.hover\:border-blue-700:hover {
+  --border-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--border-opacity));
+}
+
+.hover\:border-blue-800:hover {
+  --border-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--border-opacity));
+}
+
+.hover\:border-blue-900:hover {
+  --border-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--border-opacity));
+}
+
+.hover\:border-indigo-100:hover {
+  --border-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--border-opacity));
+}
+
+.hover\:border-indigo-200:hover {
+  --border-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--border-opacity));
+}
+
+.hover\:border-indigo-300:hover {
+  --border-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--border-opacity));
+}
+
+.hover\:border-indigo-400:hover {
+  --border-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--border-opacity));
+}
+
+.hover\:border-indigo-500:hover {
+  --border-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--border-opacity));
+}
+
+.hover\:border-indigo-600:hover {
+  --border-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--border-opacity));
+}
+
+.hover\:border-indigo-700:hover {
+  --border-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--border-opacity));
+}
+
+.hover\:border-indigo-800:hover {
+  --border-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--border-opacity));
+}
+
+.hover\:border-indigo-900:hover {
+  --border-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--border-opacity));
+}
+
+.hover\:border-purple-100:hover {
+  --border-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--border-opacity));
+}
+
+.hover\:border-purple-200:hover {
+  --border-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--border-opacity));
+}
+
+.hover\:border-purple-300:hover {
+  --border-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--border-opacity));
+}
+
+.hover\:border-purple-400:hover {
+  --border-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--border-opacity));
+}
+
+.hover\:border-purple-500:hover {
+  --border-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--border-opacity));
+}
+
+.hover\:border-purple-600:hover {
+  --border-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--border-opacity));
+}
+
+.hover\:border-purple-700:hover {
+  --border-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--border-opacity));
+}
+
+.hover\:border-purple-800:hover {
+  --border-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--border-opacity));
+}
+
+.hover\:border-purple-900:hover {
+  --border-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--border-opacity));
+}
+
+.hover\:border-pink-100:hover {
+  --border-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--border-opacity));
+}
+
+.hover\:border-pink-200:hover {
+  --border-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--border-opacity));
+}
+
+.hover\:border-pink-300:hover {
+  --border-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--border-opacity));
+}
+
+.hover\:border-pink-400:hover {
+  --border-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--border-opacity));
+}
+
+.hover\:border-pink-500:hover {
+  --border-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--border-opacity));
+}
+
+.hover\:border-pink-600:hover {
+  --border-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--border-opacity));
+}
+
+.hover\:border-pink-700:hover {
+  --border-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--border-opacity));
+}
+
+.hover\:border-pink-800:hover {
+  --border-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--border-opacity));
+}
+
+.hover\:border-pink-900:hover {
+  --border-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--border-opacity));
+}
+
+.focus\:border-transparent:focus {
+  border-color: transparent;
+}
+
+.focus\:border-current:focus {
+  border-color: currentColor;
+}
+
+.focus\:border-black:focus {
+  --border-opacity: 1;
+  border-color: #000;
+  border-color: rgba(0, 0, 0, var(--border-opacity));
+}
+
+.focus\:border-white:focus {
+  --border-opacity: 1;
+  border-color: #fff;
+  border-color: rgba(255, 255, 255, var(--border-opacity));
+}
+
+.focus\:border-gray-100:focus {
+  --border-opacity: 1;
+  border-color: #f7fafc;
+  border-color: rgba(247, 250, 252, var(--border-opacity));
+}
+
+.focus\:border-gray-200:focus {
+  --border-opacity: 1;
+  border-color: #edf2f7;
+  border-color: rgba(237, 242, 247, var(--border-opacity));
+}
+
+.focus\:border-gray-300:focus {
+  --border-opacity: 1;
+  border-color: #e2e8f0;
+  border-color: rgba(226, 232, 240, var(--border-opacity));
+}
+
+.focus\:border-gray-400:focus {
+  --border-opacity: 1;
+  border-color: #cbd5e0;
+  border-color: rgba(203, 213, 224, var(--border-opacity));
+}
+
+.focus\:border-gray-500:focus {
+  --border-opacity: 1;
+  border-color: #a0aec0;
+  border-color: rgba(160, 174, 192, var(--border-opacity));
+}
+
+.focus\:border-gray-600:focus {
+  --border-opacity: 1;
+  border-color: #718096;
+  border-color: rgba(113, 128, 150, var(--border-opacity));
+}
+
+.focus\:border-gray-700:focus {
+  --border-opacity: 1;
+  border-color: #4a5568;
+  border-color: rgba(74, 85, 104, var(--border-opacity));
+}
+
+.focus\:border-gray-800:focus {
+  --border-opacity: 1;
+  border-color: #2d3748;
+  border-color: rgba(45, 55, 72, var(--border-opacity));
+}
+
+.focus\:border-gray-900:focus {
+  --border-opacity: 1;
+  border-color: #1a202c;
+  border-color: rgba(26, 32, 44, var(--border-opacity));
+}
+
+.focus\:border-red-100:focus {
+  --border-opacity: 1;
+  border-color: #fff5f5;
+  border-color: rgba(255, 245, 245, var(--border-opacity));
+}
+
+.focus\:border-red-200:focus {
+  --border-opacity: 1;
+  border-color: #fed7d7;
+  border-color: rgba(254, 215, 215, var(--border-opacity));
+}
+
+.focus\:border-red-300:focus {
+  --border-opacity: 1;
+  border-color: #feb2b2;
+  border-color: rgba(254, 178, 178, var(--border-opacity));
+}
+
+.focus\:border-red-400:focus {
+  --border-opacity: 1;
+  border-color: #fc8181;
+  border-color: rgba(252, 129, 129, var(--border-opacity));
+}
+
+.focus\:border-red-500:focus {
+  --border-opacity: 1;
+  border-color: #f56565;
+  border-color: rgba(245, 101, 101, var(--border-opacity));
+}
+
+.focus\:border-red-600:focus {
+  --border-opacity: 1;
+  border-color: #e53e3e;
+  border-color: rgba(229, 62, 62, var(--border-opacity));
+}
+
+.focus\:border-red-700:focus {
+  --border-opacity: 1;
+  border-color: #c53030;
+  border-color: rgba(197, 48, 48, var(--border-opacity));
+}
+
+.focus\:border-red-800:focus {
+  --border-opacity: 1;
+  border-color: #9b2c2c;
+  border-color: rgba(155, 44, 44, var(--border-opacity));
+}
+
+.focus\:border-red-900:focus {
+  --border-opacity: 1;
+  border-color: #742a2a;
+  border-color: rgba(116, 42, 42, var(--border-opacity));
+}
+
+.focus\:border-orange-100:focus {
+  --border-opacity: 1;
+  border-color: #fffaf0;
+  border-color: rgba(255, 250, 240, var(--border-opacity));
+}
+
+.focus\:border-orange-200:focus {
+  --border-opacity: 1;
+  border-color: #feebc8;
+  border-color: rgba(254, 235, 200, var(--border-opacity));
+}
+
+.focus\:border-orange-300:focus {
+  --border-opacity: 1;
+  border-color: #fbd38d;
+  border-color: rgba(251, 211, 141, var(--border-opacity));
+}
+
+.focus\:border-orange-400:focus {
+  --border-opacity: 1;
+  border-color: #f6ad55;
+  border-color: rgba(246, 173, 85, var(--border-opacity));
+}
+
+.focus\:border-orange-500:focus {
+  --border-opacity: 1;
+  border-color: #ed8936;
+  border-color: rgba(237, 137, 54, var(--border-opacity));
+}
+
+.focus\:border-orange-600:focus {
+  --border-opacity: 1;
+  border-color: #dd6b20;
+  border-color: rgba(221, 107, 32, var(--border-opacity));
+}
+
+.focus\:border-orange-700:focus {
+  --border-opacity: 1;
+  border-color: #c05621;
+  border-color: rgba(192, 86, 33, var(--border-opacity));
+}
+
+.focus\:border-orange-800:focus {
+  --border-opacity: 1;
+  border-color: #9c4221;
+  border-color: rgba(156, 66, 33, var(--border-opacity));
+}
+
+.focus\:border-orange-900:focus {
+  --border-opacity: 1;
+  border-color: #7b341e;
+  border-color: rgba(123, 52, 30, var(--border-opacity));
+}
+
+.focus\:border-yellow-100:focus {
+  --border-opacity: 1;
+  border-color: #fffff0;
+  border-color: rgba(255, 255, 240, var(--border-opacity));
+}
+
+.focus\:border-yellow-200:focus {
+  --border-opacity: 1;
+  border-color: #fefcbf;
+  border-color: rgba(254, 252, 191, var(--border-opacity));
+}
+
+.focus\:border-yellow-300:focus {
+  --border-opacity: 1;
+  border-color: #faf089;
+  border-color: rgba(250, 240, 137, var(--border-opacity));
+}
+
+.focus\:border-yellow-400:focus {
+  --border-opacity: 1;
+  border-color: #f6e05e;
+  border-color: rgba(246, 224, 94, var(--border-opacity));
+}
+
+.focus\:border-yellow-500:focus {
+  --border-opacity: 1;
+  border-color: #ecc94b;
+  border-color: rgba(236, 201, 75, var(--border-opacity));
+}
+
+.focus\:border-yellow-600:focus {
+  --border-opacity: 1;
+  border-color: #d69e2e;
+  border-color: rgba(214, 158, 46, var(--border-opacity));
+}
+
+.focus\:border-yellow-700:focus {
+  --border-opacity: 1;
+  border-color: #b7791f;
+  border-color: rgba(183, 121, 31, var(--border-opacity));
+}
+
+.focus\:border-yellow-800:focus {
+  --border-opacity: 1;
+  border-color: #975a16;
+  border-color: rgba(151, 90, 22, var(--border-opacity));
+}
+
+.focus\:border-yellow-900:focus {
+  --border-opacity: 1;
+  border-color: #744210;
+  border-color: rgba(116, 66, 16, var(--border-opacity));
+}
+
+.focus\:border-green-100:focus {
+  --border-opacity: 1;
+  border-color: #f0fff4;
+  border-color: rgba(240, 255, 244, var(--border-opacity));
+}
+
+.focus\:border-green-200:focus {
+  --border-opacity: 1;
+  border-color: #c6f6d5;
+  border-color: rgba(198, 246, 213, var(--border-opacity));
+}
+
+.focus\:border-green-300:focus {
+  --border-opacity: 1;
+  border-color: #9ae6b4;
+  border-color: rgba(154, 230, 180, var(--border-opacity));
+}
+
+.focus\:border-green-400:focus {
+  --border-opacity: 1;
+  border-color: #68d391;
+  border-color: rgba(104, 211, 145, var(--border-opacity));
+}
+
+.focus\:border-green-500:focus {
+  --border-opacity: 1;
+  border-color: #48bb78;
+  border-color: rgba(72, 187, 120, var(--border-opacity));
+}
+
+.focus\:border-green-600:focus {
+  --border-opacity: 1;
+  border-color: #38a169;
+  border-color: rgba(56, 161, 105, var(--border-opacity));
+}
+
+.focus\:border-green-700:focus {
+  --border-opacity: 1;
+  border-color: #2f855a;
+  border-color: rgba(47, 133, 90, var(--border-opacity));
+}
+
+.focus\:border-green-800:focus {
+  --border-opacity: 1;
+  border-color: #276749;
+  border-color: rgba(39, 103, 73, var(--border-opacity));
+}
+
+.focus\:border-green-900:focus {
+  --border-opacity: 1;
+  border-color: #22543d;
+  border-color: rgba(34, 84, 61, var(--border-opacity));
+}
+
+.focus\:border-teal-100:focus {
+  --border-opacity: 1;
+  border-color: #e6fffa;
+  border-color: rgba(230, 255, 250, var(--border-opacity));
+}
+
+.focus\:border-teal-200:focus {
+  --border-opacity: 1;
+  border-color: #b2f5ea;
+  border-color: rgba(178, 245, 234, var(--border-opacity));
+}
+
+.focus\:border-teal-300:focus {
+  --border-opacity: 1;
+  border-color: #81e6d9;
+  border-color: rgba(129, 230, 217, var(--border-opacity));
+}
+
+.focus\:border-teal-400:focus {
+  --border-opacity: 1;
+  border-color: #4fd1c5;
+  border-color: rgba(79, 209, 197, var(--border-opacity));
+}
+
+.focus\:border-teal-500:focus {
+  --border-opacity: 1;
+  border-color: #38b2ac;
+  border-color: rgba(56, 178, 172, var(--border-opacity));
+}
+
+.focus\:border-teal-600:focus {
+  --border-opacity: 1;
+  border-color: #319795;
+  border-color: rgba(49, 151, 149, var(--border-opacity));
+}
+
+.focus\:border-teal-700:focus {
+  --border-opacity: 1;
+  border-color: #2c7a7b;
+  border-color: rgba(44, 122, 123, var(--border-opacity));
+}
+
+.focus\:border-teal-800:focus {
+  --border-opacity: 1;
+  border-color: #285e61;
+  border-color: rgba(40, 94, 97, var(--border-opacity));
+}
+
+.focus\:border-teal-900:focus {
+  --border-opacity: 1;
+  border-color: #234e52;
+  border-color: rgba(35, 78, 82, var(--border-opacity));
+}
+
+.focus\:border-blue-100:focus {
+  --border-opacity: 1;
+  border-color: #ebf8ff;
+  border-color: rgba(235, 248, 255, var(--border-opacity));
+}
+
+.focus\:border-blue-200:focus {
+  --border-opacity: 1;
+  border-color: #bee3f8;
+  border-color: rgba(190, 227, 248, var(--border-opacity));
+}
+
+.focus\:border-blue-300:focus {
+  --border-opacity: 1;
+  border-color: #90cdf4;
+  border-color: rgba(144, 205, 244, var(--border-opacity));
+}
+
+.focus\:border-blue-400:focus {
+  --border-opacity: 1;
+  border-color: #63b3ed;
+  border-color: rgba(99, 179, 237, var(--border-opacity));
+}
+
+.focus\:border-blue-500:focus {
+  --border-opacity: 1;
+  border-color: #4299e1;
+  border-color: rgba(66, 153, 225, var(--border-opacity));
+}
+
+.focus\:border-blue-600:focus {
+  --border-opacity: 1;
+  border-color: #3182ce;
+  border-color: rgba(49, 130, 206, var(--border-opacity));
+}
+
+.focus\:border-blue-700:focus {
+  --border-opacity: 1;
+  border-color: #2b6cb0;
+  border-color: rgba(43, 108, 176, var(--border-opacity));
+}
+
+.focus\:border-blue-800:focus {
+  --border-opacity: 1;
+  border-color: #2c5282;
+  border-color: rgba(44, 82, 130, var(--border-opacity));
+}
+
+.focus\:border-blue-900:focus {
+  --border-opacity: 1;
+  border-color: #2a4365;
+  border-color: rgba(42, 67, 101, var(--border-opacity));
+}
+
+.focus\:border-indigo-100:focus {
+  --border-opacity: 1;
+  border-color: #ebf4ff;
+  border-color: rgba(235, 244, 255, var(--border-opacity));
+}
+
+.focus\:border-indigo-200:focus {
+  --border-opacity: 1;
+  border-color: #c3dafe;
+  border-color: rgba(195, 218, 254, var(--border-opacity));
+}
+
+.focus\:border-indigo-300:focus {
+  --border-opacity: 1;
+  border-color: #a3bffa;
+  border-color: rgba(163, 191, 250, var(--border-opacity));
+}
+
+.focus\:border-indigo-400:focus {
+  --border-opacity: 1;
+  border-color: #7f9cf5;
+  border-color: rgba(127, 156, 245, var(--border-opacity));
+}
+
+.focus\:border-indigo-500:focus {
+  --border-opacity: 1;
+  border-color: #667eea;
+  border-color: rgba(102, 126, 234, var(--border-opacity));
+}
+
+.focus\:border-indigo-600:focus {
+  --border-opacity: 1;
+  border-color: #5a67d8;
+  border-color: rgba(90, 103, 216, var(--border-opacity));
+}
+
+.focus\:border-indigo-700:focus {
+  --border-opacity: 1;
+  border-color: #4c51bf;
+  border-color: rgba(76, 81, 191, var(--border-opacity));
+}
+
+.focus\:border-indigo-800:focus {
+  --border-opacity: 1;
+  border-color: #434190;
+  border-color: rgba(67, 65, 144, var(--border-opacity));
+}
+
+.focus\:border-indigo-900:focus {
+  --border-opacity: 1;
+  border-color: #3c366b;
+  border-color: rgba(60, 54, 107, var(--border-opacity));
+}
+
+.focus\:border-purple-100:focus {
+  --border-opacity: 1;
+  border-color: #faf5ff;
+  border-color: rgba(250, 245, 255, var(--border-opacity));
+}
+
+.focus\:border-purple-200:focus {
+  --border-opacity: 1;
+  border-color: #e9d8fd;
+  border-color: rgba(233, 216, 253, var(--border-opacity));
+}
+
+.focus\:border-purple-300:focus {
+  --border-opacity: 1;
+  border-color: #d6bcfa;
+  border-color: rgba(214, 188, 250, var(--border-opacity));
+}
+
+.focus\:border-purple-400:focus {
+  --border-opacity: 1;
+  border-color: #b794f4;
+  border-color: rgba(183, 148, 244, var(--border-opacity));
+}
+
+.focus\:border-purple-500:focus {
+  --border-opacity: 1;
+  border-color: #9f7aea;
+  border-color: rgba(159, 122, 234, var(--border-opacity));
+}
+
+.focus\:border-purple-600:focus {
+  --border-opacity: 1;
+  border-color: #805ad5;
+  border-color: rgba(128, 90, 213, var(--border-opacity));
+}
+
+.focus\:border-purple-700:focus {
+  --border-opacity: 1;
+  border-color: #6b46c1;
+  border-color: rgba(107, 70, 193, var(--border-opacity));
+}
+
+.focus\:border-purple-800:focus {
+  --border-opacity: 1;
+  border-color: #553c9a;
+  border-color: rgba(85, 60, 154, var(--border-opacity));
+}
+
+.focus\:border-purple-900:focus {
+  --border-opacity: 1;
+  border-color: #44337a;
+  border-color: rgba(68, 51, 122, var(--border-opacity));
+}
+
+.focus\:border-pink-100:focus {
+  --border-opacity: 1;
+  border-color: #fff5f7;
+  border-color: rgba(255, 245, 247, var(--border-opacity));
+}
+
+.focus\:border-pink-200:focus {
+  --border-opacity: 1;
+  border-color: #fed7e2;
+  border-color: rgba(254, 215, 226, var(--border-opacity));
+}
+
+.focus\:border-pink-300:focus {
+  --border-opacity: 1;
+  border-color: #fbb6ce;
+  border-color: rgba(251, 182, 206, var(--border-opacity));
+}
+
+.focus\:border-pink-400:focus {
+  --border-opacity: 1;
+  border-color: #f687b3;
+  border-color: rgba(246, 135, 179, var(--border-opacity));
+}
+
+.focus\:border-pink-500:focus {
+  --border-opacity: 1;
+  border-color: #ed64a6;
+  border-color: rgba(237, 100, 166, var(--border-opacity));
+}
+
+.focus\:border-pink-600:focus {
+  --border-opacity: 1;
+  border-color: #d53f8c;
+  border-color: rgba(213, 63, 140, var(--border-opacity));
+}
+
+.focus\:border-pink-700:focus {
+  --border-opacity: 1;
+  border-color: #b83280;
+  border-color: rgba(184, 50, 128, var(--border-opacity));
+}
+
+.focus\:border-pink-800:focus {
+  --border-opacity: 1;
+  border-color: #97266d;
+  border-color: rgba(151, 38, 109, var(--border-opacity));
+}
+
+.focus\:border-pink-900:focus {
+  --border-opacity: 1;
+  border-color: #702459;
+  border-color: rgba(112, 36, 89, var(--border-opacity));
+}
+
+.border-opacity-0 {
+  --border-opacity: 0;
+}
+
+.border-opacity-25 {
+  --border-opacity: 0.25;
+}
+
+.border-opacity-50 {
+  --border-opacity: 0.5;
+}
+
+.border-opacity-75 {
+  --border-opacity: 0.75;
+}
+
+.border-opacity-100 {
+  --border-opacity: 1;
+}
+
+.hover\:border-opacity-0:hover {
+  --border-opacity: 0;
+}
+
+.hover\:border-opacity-25:hover {
+  --border-opacity: 0.25;
+}
+
+.hover\:border-opacity-50:hover {
+  --border-opacity: 0.5;
+}
+
+.hover\:border-opacity-75:hover {
+  --border-opacity: 0.75;
+}
+
+.hover\:border-opacity-100:hover {
+  --border-opacity: 1;
+}
+
+.focus\:border-opacity-0:focus {
+  --border-opacity: 0;
+}
+
+.focus\:border-opacity-25:focus {
+  --border-opacity: 0.25;
+}
+
+.focus\:border-opacity-50:focus {
+  --border-opacity: 0.5;
+}
+
+.focus\:border-opacity-75:focus {
+  --border-opacity: 0.75;
+}
+
+.focus\:border-opacity-100:focus {
+  --border-opacity: 1;
+}
+
+.rounded-none {
+  border-radius: 0;
+}
+
+.rounded-sm {
+  border-radius: 0.125rem;
+}
+
+.rounded {
+  border-radius: 0.25rem;
+}
+
+.rounded-md {
+  border-radius: 0.375rem;
+}
+
+.rounded-lg {
+  border-radius: 0.5rem;
+}
+
+.rounded-full {
+  border-radius: 9999px;
+}
+
+.rounded-t-none {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.rounded-r-none {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.rounded-b-none {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.rounded-l-none {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.rounded-t-sm {
+  border-top-left-radius: 0.125rem;
+  border-top-right-radius: 0.125rem;
+}
+
+.rounded-r-sm {
+  border-top-right-radius: 0.125rem;
+  border-bottom-right-radius: 0.125rem;
+}
+
+.rounded-b-sm {
+  border-bottom-right-radius: 0.125rem;
+  border-bottom-left-radius: 0.125rem;
+}
+
+.rounded-l-sm {
+  border-top-left-radius: 0.125rem;
+  border-bottom-left-radius: 0.125rem;
+}
+
+.rounded-t {
+  border-top-left-radius: 0.25rem;
+  border-top-right-radius: 0.25rem;
+}
+
+.rounded-r {
+  border-top-right-radius: 0.25rem;
+  border-bottom-right-radius: 0.25rem;
+}
+
+.rounded-b {
+  border-bottom-right-radius: 0.25rem;
+  border-bottom-left-radius: 0.25rem;
+}
+
+.rounded-l {
+  border-top-left-radius: 0.25rem;
+  border-bottom-left-radius: 0.25rem;
+}
+
+.rounded-t-md {
+  border-top-left-radius: 0.375rem;
+  border-top-right-radius: 0.375rem;
+}
+
+.rounded-r-md {
+  border-top-right-radius: 0.375rem;
+  border-bottom-right-radius: 0.375rem;
+}
+
+.rounded-b-md {
+  border-bottom-right-radius: 0.375rem;
+  border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-l-md {
+  border-top-left-radius: 0.375rem;
+  border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-t-lg {
+  border-top-left-radius: 0.5rem;
+  border-top-right-radius: 0.5rem;
+}
+
+.rounded-r-lg {
+  border-top-right-radius: 0.5rem;
+  border-bottom-right-radius: 0.5rem;
+}
+
+.rounded-b-lg {
+  border-bottom-right-radius: 0.5rem;
+  border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-l-lg {
+  border-top-left-radius: 0.5rem;
+  border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-t-full {
+  border-top-left-radius: 9999px;
+  border-top-right-radius: 9999px;
+}
+
+.rounded-r-full {
+  border-top-right-radius: 9999px;
+  border-bottom-right-radius: 9999px;
+}
+
+.rounded-b-full {
+  border-bottom-right-radius: 9999px;
+  border-bottom-left-radius: 9999px;
+}
+
+.rounded-l-full {
+  border-top-left-radius: 9999px;
+  border-bottom-left-radius: 9999px;
+}
+
+.rounded-tl-none {
+  border-top-left-radius: 0;
+}
+
+.rounded-tr-none {
+  border-top-right-radius: 0;
+}
+
+.rounded-br-none {
+  border-bottom-right-radius: 0;
+}
+
+.rounded-bl-none {
+  border-bottom-left-radius: 0;
+}
+
+.rounded-tl-sm {
+  border-top-left-radius: 0.125rem;
+}
+
+.rounded-tr-sm {
+  border-top-right-radius: 0.125rem;
+}
+
+.rounded-br-sm {
+  border-bottom-right-radius: 0.125rem;
+}
+
+.rounded-bl-sm {
+  border-bottom-left-radius: 0.125rem;
+}
+
+.rounded-tl {
+  border-top-left-radius: 0.25rem;
+}
+
+.rounded-tr {
+  border-top-right-radius: 0.25rem;
+}
+
+.rounded-br {
+  border-bottom-right-radius: 0.25rem;
+}
+
+.rounded-bl {
+  border-bottom-left-radius: 0.25rem;
+}
+
+.rounded-tl-md {
+  border-top-left-radius: 0.375rem;
+}
+
+.rounded-tr-md {
+  border-top-right-radius: 0.375rem;
+}
+
+.rounded-br-md {
+  border-bottom-right-radius: 0.375rem;
+}
+
+.rounded-bl-md {
+  border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-tl-lg {
+  border-top-left-radius: 0.5rem;
+}
+
+.rounded-tr-lg {
+  border-top-right-radius: 0.5rem;
+}
+
+.rounded-br-lg {
+  border-bottom-right-radius: 0.5rem;
+}
+
+.rounded-bl-lg {
+  border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-tl-full {
+  border-top-left-radius: 9999px;
+}
+
+.rounded-tr-full {
+  border-top-right-radius: 9999px;
+}
+
+.rounded-br-full {
+  border-bottom-right-radius: 9999px;
+}
+
+.rounded-bl-full {
+  border-bottom-left-radius: 9999px;
+}
+
+.border-solid {
+  border-style: solid;
+}
+
+.border-dashed {
+  border-style: dashed;
+}
+
+.border-dotted {
+  border-style: dotted;
+}
+
+.border-double {
+  border-style: double;
+}
+
+.border-none {
+  border-style: none;
+}
+
+.border-0 {
+  border-width: 0;
+}
+
+.border-2 {
+  border-width: 2px;
+}
+
+.border-4 {
+  border-width: 4px;
+}
+
+.border-8 {
+  border-width: 8px;
+}
+
+.border {
+  border-width: 1px;
+}
+
+.border-t-0 {
+  border-top-width: 0;
+}
+
+.border-r-0 {
+  border-right-width: 0;
+}
+
+.border-b-0 {
+  border-bottom-width: 0;
+}
+
+.border-l-0 {
+  border-left-width: 0;
+}
+
+.border-t-2 {
+  border-top-width: 2px;
+}
+
+.border-r-2 {
+  border-right-width: 2px;
+}
+
+.border-b-2 {
+  border-bottom-width: 2px;
+}
+
+.border-l-2 {
+  border-left-width: 2px;
+}
+
+.border-t-4 {
+  border-top-width: 4px;
+}
+
+.border-r-4 {
+  border-right-width: 4px;
+}
+
+.border-b-4 {
+  border-bottom-width: 4px;
+}
+
+.border-l-4 {
+  border-left-width: 4px;
+}
+
+.border-t-8 {
+  border-top-width: 8px;
+}
+
+.border-r-8 {
+  border-right-width: 8px;
+}
+
+.border-b-8 {
+  border-bottom-width: 8px;
+}
+
+.border-l-8 {
+  border-left-width: 8px;
+}
+
+.border-t {
+  border-top-width: 1px;
+}
+
+.border-r {
+  border-right-width: 1px;
+}
+
+.border-b {
+  border-bottom-width: 1px;
+}
+
+.border-l {
+  border-left-width: 1px;
+}
+
+.box-border {
+  box-sizing: border-box;
+}
+
+.box-content {
+  box-sizing: content-box;
+}
+
+.cursor-auto {
+  cursor: auto;
+}
+
+.cursor-default {
+  cursor: default;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.cursor-wait {
+  cursor: wait;
+}
+
+.cursor-text {
+  cursor: text;
+}
+
+.cursor-move {
+  cursor: move;
+}
+
+.cursor-not-allowed {
+  cursor: not-allowed;
+}
+
+.block {
+  display: block;
+}
+
+.inline-block {
+  display: inline-block;
+}
+
+.inline {
+  display: inline;
+}
+
+.flex {
+  display: flex;
+}
+
+.inline-flex {
+  display: inline-flex;
+}
+
+.table {
+  display: table;
+}
+
+.table-caption {
+  display: table-caption;
+}
+
+.table-cell {
+  display: table-cell;
+}
+
+.table-column {
+  display: table-column;
+}
+
+.table-column-group {
+  display: table-column-group;
+}
+
+.table-footer-group {
+  display: table-footer-group;
+}
+
+.table-header-group {
+  display: table-header-group;
+}
+
+.table-row-group {
+  display: table-row-group;
+}
+
+.table-row {
+  display: table-row;
+}
+
+.flow-root {
+  display: flow-root;
+}
+
+.grid {
+  display: grid;
+}
+
+.inline-grid {
+  display: inline-grid;
+}
+
+.contents {
+  display: contents;
+}
+
+.hidden {
+  display: none;
+}
+
+.flex-row {
+  flex-direction: row;
+}
+
+.flex-row-reverse {
+  flex-direction: row-reverse;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.flex-col-reverse {
+  flex-direction: column-reverse;
+}
+
+.flex-wrap {
+  flex-wrap: wrap;
+}
+
+.flex-wrap-reverse {
+  flex-wrap: wrap-reverse;
+}
+
+.flex-no-wrap {
+  flex-wrap: nowrap;
+}
+
+.place-items-auto {
+  place-items: auto;
+}
+
+.place-items-start {
+  place-items: start;
+}
+
+.place-items-end {
+  place-items: end;
+}
+
+.place-items-center {
+  place-items: center;
+}
+
+.place-items-stretch {
+  place-items: stretch;
+}
+
+.place-content-center {
+  place-content: center;
+}
+
+.place-content-start {
+  place-content: start;
+}
+
+.place-content-end {
+  place-content: end;
+}
+
+.place-content-between {
+  place-content: space-between;
+}
+
+.place-content-around {
+  place-content: space-around;
+}
+
+.place-content-evenly {
+  place-content: space-evenly;
+}
+
+.place-content-stretch {
+  place-content: stretch;
+}
+
+.place-self-auto {
+  place-self: auto;
+}
+
+.place-self-start {
+  place-self: start;
+}
+
+.place-self-end {
+  place-self: end;
+}
+
+.place-self-center {
+  place-self: center;
+}
+
+.place-self-stretch {
+  place-self: stretch;
+}
+
+.items-start {
+  align-items: flex-start;
+}
+
+.items-end {
+  align-items: flex-end;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.items-baseline {
+  align-items: baseline;
+}
+
+.items-stretch {
+  align-items: stretch;
+}
+
+.content-center {
+  align-content: center;
+}
+
+.content-start {
+  align-content: flex-start;
+}
+
+.content-end {
+  align-content: flex-end;
+}
+
+.content-between {
+  align-content: space-between;
+}
+
+.content-around {
+  align-content: space-around;
+}
+
+.content-evenly {
+  align-content: space-evenly;
+}
+
+.self-auto {
+  align-self: auto;
+}
+
+.self-start {
+  align-self: flex-start;
+}
+
+.self-end {
+  align-self: flex-end;
+}
+
+.self-center {
+  align-self: center;
+}
+
+.self-stretch {
+  align-self: stretch;
+}
+
+.justify-items-auto {
+  justify-items: auto;
+}
+
+.justify-items-start {
+  justify-items: start;
+}
+
+.justify-items-end {
+  justify-items: end;
+}
+
+.justify-items-center {
+  justify-items: center;
+}
+
+.justify-items-stretch {
+  justify-items: stretch;
+}
+
+.justify-start {
+  justify-content: flex-start;
+}
+
+.justify-end {
+  justify-content: flex-end;
+}
+
+.justify-center {
+  justify-content: center;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.justify-around {
+  justify-content: space-around;
+}
+
+.justify-evenly {
+  justify-content: space-evenly;
+}
+
+.justify-self-auto {
+  justify-self: auto;
+}
+
+.justify-self-start {
+  justify-self: start;
+}
+
+.justify-self-end {
+  justify-self: end;
+}
+
+.justify-self-center {
+  justify-self: center;
+}
+
+.justify-self-stretch {
+  justify-self: stretch;
+}
+
+.flex-1 {
+  flex: 1 1 0%;
+}
+
+.flex-auto {
+  flex: 1 1 auto;
+}
+
+.flex-initial {
+  flex: 0 1 auto;
+}
+
+.flex-none {
+  flex: none;
+}
+
+.flex-grow-0 {
+  flex-grow: 0;
+}
+
+.flex-grow {
+  flex-grow: 1;
+}
+
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+
+.flex-shrink {
+  flex-shrink: 1;
+}
+
+.order-1 {
+  order: 1;
+}
+
+.order-2 {
+  order: 2;
+}
+
+.order-3 {
+  order: 3;
+}
+
+.order-4 {
+  order: 4;
+}
+
+.order-5 {
+  order: 5;
+}
+
+.order-6 {
+  order: 6;
+}
+
+.order-7 {
+  order: 7;
+}
+
+.order-8 {
+  order: 8;
+}
+
+.order-9 {
+  order: 9;
+}
+
+.order-10 {
+  order: 10;
+}
+
+.order-11 {
+  order: 11;
+}
+
+.order-12 {
+  order: 12;
+}
+
+.order-first {
+  order: -9999;
+}
+
+.order-last {
+  order: 9999;
+}
+
+.order-none {
+  order: 0;
+}
+
+.float-right {
+  float: right;
+}
+
+.float-left {
+  float: left;
+}
+
+.float-none {
+  float: none;
+}
+
+.clearfix:after {
+  content: "";
+  display: table;
+  clear: both;
+}
+
+.clear-left {
+  clear: left;
+}
+
+.clear-right {
+  clear: right;
+}
+
+.clear-both {
+  clear: both;
+}
+
+.clear-none {
+  clear: none;
+}
+
+.font-sans {
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+.font-serif {
+  font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+}
+
+.font-mono {
+  font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+.font-hairline {
+  font-weight: 100;
+}
+
+.font-thin {
+  font-weight: 200;
+}
+
+.font-light {
+  font-weight: 300;
+}
+
+.font-normal {
+  font-weight: 400;
+}
+
+.font-medium {
+  font-weight: 500;
+}
+
+.font-semibold {
+  font-weight: 600;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+
+.font-extrabold {
+  font-weight: 800;
+}
+
+.font-black {
+  font-weight: 900;
+}
+
+.hover\:font-hairline:hover {
+  font-weight: 100;
+}
+
+.hover\:font-thin:hover {
+  font-weight: 200;
+}
+
+.hover\:font-light:hover {
+  font-weight: 300;
+}
+
+.hover\:font-normal:hover {
+  font-weight: 400;
+}
+
+.hover\:font-medium:hover {
+  font-weight: 500;
+}
+
+.hover\:font-semibold:hover {
+  font-weight: 600;
+}
+
+.hover\:font-bold:hover {
+  font-weight: 700;
+}
+
+.hover\:font-extrabold:hover {
+  font-weight: 800;
+}
+
+.hover\:font-black:hover {
+  font-weight: 900;
+}
+
+.focus\:font-hairline:focus {
+  font-weight: 100;
+}
+
+.focus\:font-thin:focus {
+  font-weight: 200;
+}
+
+.focus\:font-light:focus {
+  font-weight: 300;
+}
+
+.focus\:font-normal:focus {
+  font-weight: 400;
+}
+
+.focus\:font-medium:focus {
+  font-weight: 500;
+}
+
+.focus\:font-semibold:focus {
+  font-weight: 600;
+}
+
+.focus\:font-bold:focus {
+  font-weight: 700;
+}
+
+.focus\:font-extrabold:focus {
+  font-weight: 800;
+}
+
+.focus\:font-black:focus {
+  font-weight: 900;
+}
+
+.h-0 {
+  height: 0;
+}
+
+.h-1 {
+  height: 0.25rem;
+}
+
+.h-2 {
+  height: 0.5rem;
+}
+
+.h-3 {
+  height: 0.75rem;
+}
+
+.h-4 {
+  height: 1rem;
+}
+
+.h-5 {
+  height: 1.25rem;
+}
+
+.h-6 {
+  height: 1.5rem;
+}
+
+.h-8 {
+  height: 2rem;
+}
+
+.h-10 {
+  height: 2.5rem;
+}
+
+.h-12 {
+  height: 3rem;
+}
+
+.h-16 {
+  height: 4rem;
+}
+
+.h-20 {
+  height: 5rem;
+}
+
+.h-24 {
+  height: 6rem;
+}
+
+.h-32 {
+  height: 8rem;
+}
+
+.h-40 {
+  height: 10rem;
+}
+
+.h-48 {
+  height: 12rem;
+}
+
+.h-56 {
+  height: 14rem;
+}
+
+.h-64 {
+  height: 16rem;
+}
+
+.h-auto {
+  height: auto;
+}
+
+.h-px {
+  height: 1px;
+}
+
+.h-full {
+  height: 100%;
+}
+
+.h-screen {
+  height: 100vh;
+}
+
+.text-xs {
+  font-size: 0.75rem;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+}
+
+.text-base {
+  font-size: 1rem;
+}
+
+.text-lg {
+  font-size: 1.125rem;
+}
+
+.text-xl {
+  font-size: 1.25rem;
+}
+
+.text-2xl {
+  font-size: 1.5rem;
+}
+
+.text-3xl {
+  font-size: 1.875rem;
+}
+
+.text-4xl {
+  font-size: 2.25rem;
+}
+
+.text-5xl {
+  font-size: 3rem;
+}
+
+.text-6xl {
+  font-size: 4rem;
+}
+
+.leading-3 {
+  line-height: .75rem;
+}
+
+.leading-4 {
+  line-height: 1rem;
+}
+
+.leading-5 {
+  line-height: 1.25rem;
+}
+
+.leading-6 {
+  line-height: 1.5rem;
+}
+
+.leading-7 {
+  line-height: 1.75rem;
+}
+
+.leading-8 {
+  line-height: 2rem;
+}
+
+.leading-9 {
+  line-height: 2.25rem;
+}
+
+.leading-10 {
+  line-height: 2.5rem;
+}
+
+.leading-none {
+  line-height: 1;
+}
+
+.leading-tight {
+  line-height: 1.25;
+}
+
+.leading-snug {
+  line-height: 1.375;
+}
+
+.leading-normal {
+  line-height: 1.5;
+}
+
+.leading-relaxed {
+  line-height: 1.625;
+}
+
+.leading-loose {
+  line-height: 2;
+}
+
+.list-inside {
+  list-style-position: inside;
+}
+
+.list-outside {
+  list-style-position: outside;
+}
+
+.list-none {
+  list-style-type: none;
+}
+
+.list-disc {
+  list-style-type: disc;
+}
+
+.list-decimal {
+  list-style-type: decimal;
+}
+
+.m-0 {
+  margin: 0;
+}
+
+.m-1 {
+  margin: 0.25rem;
+}
+
+.m-2 {
+  margin: 0.5rem;
+}
+
+.m-3 {
+  margin: 0.75rem;
+}
+
+.m-4 {
+  margin: 1rem;
+}
+
+.m-5 {
+  margin: 1.25rem;
+}
+
+.m-6 {
+  margin: 1.5rem;
+}
+
+.m-8 {
+  margin: 2rem;
+}
+
+.m-10 {
+  margin: 2.5rem;
+}
+
+.m-12 {
+  margin: 3rem;
+}
+
+.m-16 {
+  margin: 4rem;
+}
+
+.m-20 {
+  margin: 5rem;
+}
+
+.m-24 {
+  margin: 6rem;
+}
+
+.m-32 {
+  margin: 8rem;
+}
+
+.m-40 {
+  margin: 10rem;
+}
+
+.m-48 {
+  margin: 12rem;
+}
+
+.m-56 {
+  margin: 14rem;
+}
+
+.m-64 {
+  margin: 16rem;
+}
+
+.m-auto {
+  margin: auto;
+}
+
+.m-px {
+  margin: 1px;
+}
+
+.-m-1 {
+  margin: -0.25rem;
+}
+
+.-m-2 {
+  margin: -0.5rem;
+}
+
+.-m-3 {
+  margin: -0.75rem;
+}
+
+.-m-4 {
+  margin: -1rem;
+}
+
+.-m-5 {
+  margin: -1.25rem;
+}
+
+.-m-6 {
+  margin: -1.5rem;
+}
+
+.-m-8 {
+  margin: -2rem;
+}
+
+.-m-10 {
+  margin: -2.5rem;
+}
+
+.-m-12 {
+  margin: -3rem;
+}
+
+.-m-16 {
+  margin: -4rem;
+}
+
+.-m-20 {
+  margin: -5rem;
+}
+
+.-m-24 {
+  margin: -6rem;
+}
+
+.-m-32 {
+  margin: -8rem;
+}
+
+.-m-40 {
+  margin: -10rem;
+}
+
+.-m-48 {
+  margin: -12rem;
+}
+
+.-m-56 {
+  margin: -14rem;
+}
+
+.-m-64 {
+  margin: -16rem;
+}
+
+.-m-px {
+  margin: -1px;
+}
+
+.my-0 {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.mx-0 {
+  margin-left: 0;
+  margin-right: 0;
+}
+
+.my-1 {
+  margin-top: 0.25rem;
+  margin-bottom: 0.25rem;
+}
+
+.mx-1 {
+  margin-left: 0.25rem;
+  margin-right: 0.25rem;
+}
+
+.my-2 {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+
+.mx-2 {
+  margin-left: 0.5rem;
+  margin-right: 0.5rem;
+}
+
+.my-3 {
+  margin-top: 0.75rem;
+  margin-bottom: 0.75rem;
+}
+
+.mx-3 {
+  margin-left: 0.75rem;
+  margin-right: 0.75rem;
+}
+
+.my-4 {
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+}
+
+.mx-4 {
+  margin-left: 1rem;
+  margin-right: 1rem;
+}
+
+.my-5 {
+  margin-top: 1.25rem;
+  margin-bottom: 1.25rem;
+}
+
+.mx-5 {
+  margin-left: 1.25rem;
+  margin-right: 1.25rem;
+}
+
+.my-6 {
+  margin-top: 1.5rem;
+  margin-bottom: 1.5rem;
+}
+
+.mx-6 {
+  margin-left: 1.5rem;
+  margin-right: 1.5rem;
+}
+
+.my-8 {
+  margin-top: 2rem;
+  margin-bottom: 2rem;
+}
+
+.mx-8 {
+  margin-left: 2rem;
+  margin-right: 2rem;
+}
+
+.my-10 {
+  margin-top: 2.5rem;
+  margin-bottom: 2.5rem;
+}
+
+.mx-10 {
+  margin-left: 2.5rem;
+  margin-right: 2.5rem;
+}
+
+.my-12 {
+  margin-top: 3rem;
+  margin-bottom: 3rem;
+}
+
+.mx-12 {
+  margin-left: 3rem;
+  margin-right: 3rem;
+}
+
+.my-16 {
+  margin-top: 4rem;
+  margin-bottom: 4rem;
+}
+
+.mx-16 {
+  margin-left: 4rem;
+  margin-right: 4rem;
+}
+
+.my-20 {
+  margin-top: 5rem;
+  margin-bottom: 5rem;
+}
+
+.mx-20 {
+  margin-left: 5rem;
+  margin-right: 5rem;
+}
+
+.my-24 {
+  margin-top: 6rem;
+  margin-bottom: 6rem;
+}
+
+.mx-24 {
+  margin-left: 6rem;
+  margin-right: 6rem;
+}
+
+.my-32 {
+  margin-top: 8rem;
+  margin-bottom: 8rem;
+}
+
+.mx-32 {
+  margin-left: 8rem;
+  margin-right: 8rem;
+}
+
+.my-40 {
+  margin-top: 10rem;
+  margin-bottom: 10rem;
+}
+
+.mx-40 {
+  margin-left: 10rem;
+  margin-right: 10rem;
+}
+
+.my-48 {
+  margin-top: 12rem;
+  margin-bottom: 12rem;
+}
+
+.mx-48 {
+  margin-left: 12rem;
+  margin-right: 12rem;
+}
+
+.my-56 {
+  margin-top: 14rem;
+  margin-bottom: 14rem;
+}
+
+.mx-56 {
+  margin-left: 14rem;
+  margin-right: 14rem;
+}
+
+.my-64 {
+  margin-top: 16rem;
+  margin-bottom: 16rem;
+}
+
+.mx-64 {
+  margin-left: 16rem;
+  margin-right: 16rem;
+}
+
+.my-auto {
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
+.mx-auto {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.my-px {
+  margin-top: 1px;
+  margin-bottom: 1px;
+}
+
+.mx-px {
+  margin-left: 1px;
+  margin-right: 1px;
+}
+
+.-my-1 {
+  margin-top: -0.25rem;
+  margin-bottom: -0.25rem;
+}
+
+.-mx-1 {
+  margin-left: -0.25rem;
+  margin-right: -0.25rem;
+}
+
+.-my-2 {
+  margin-top: -0.5rem;
+  margin-bottom: -0.5rem;
+}
+
+.-mx-2 {
+  margin-left: -0.5rem;
+  margin-right: -0.5rem;
+}
+
+.-my-3 {
+  margin-top: -0.75rem;
+  margin-bottom: -0.75rem;
+}
+
+.-mx-3 {
+  margin-left: -0.75rem;
+  margin-right: -0.75rem;
+}
+
+.-my-4 {
+  margin-top: -1rem;
+  margin-bottom: -1rem;
+}
+
+.-mx-4 {
+  margin-left: -1rem;
+  margin-right: -1rem;
+}
+
+.-my-5 {
+  margin-top: -1.25rem;
+  margin-bottom: -1.25rem;
+}
+
+.-mx-5 {
+  margin-left: -1.25rem;
+  margin-right: -1.25rem;
+}
+
+.-my-6 {
+  margin-top: -1.5rem;
+  margin-bottom: -1.5rem;
+}
+
+.-mx-6 {
+  margin-left: -1.5rem;
+  margin-right: -1.5rem;
+}
+
+.-my-8 {
+  margin-top: -2rem;
+  margin-bottom: -2rem;
+}
+
+.-mx-8 {
+  margin-left: -2rem;
+  margin-right: -2rem;
+}
+
+.-my-10 {
+  margin-top: -2.5rem;
+  margin-bottom: -2.5rem;
+}
+
+.-mx-10 {
+  margin-left: -2.5rem;
+  margin-right: -2.5rem;
+}
+
+.-my-12 {
+  margin-top: -3rem;
+  margin-bottom: -3rem;
+}
+
+.-mx-12 {
+  margin-left: -3rem;
+  margin-right: -3rem;
+}
+
+.-my-16 {
+  margin-top: -4rem;
+  margin-bottom: -4rem;
+}
+
+.-mx-16 {
+  margin-left: -4rem;
+  margin-right: -4rem;
+}
+
+.-my-20 {
+  margin-top: -5rem;
+  margin-bottom: -5rem;
+}
+
+.-mx-20 {
+  margin-left: -5rem;
+  margin-right: -5rem;
+}
+
+.-my-24 {
+  margin-top: -6rem;
+  margin-bottom: -6rem;
+}
+
+.-mx-24 {
+  margin-left: -6rem;
+  margin-right: -6rem;
+}
+
+.-my-32 {
+  margin-top: -8rem;
+  margin-bottom: -8rem;
+}
+
+.-mx-32 {
+  margin-left: -8rem;
+  margin-right: -8rem;
+}
+
+.-my-40 {
+  margin-top: -10rem;
+  margin-bottom: -10rem;
+}
+
+.-mx-40 {
+  margin-left: -10rem;
+  margin-right: -10rem;
+}
+
+.-my-48 {
+  margin-top: -12rem;
+  margin-bottom: -12rem;
+}
+
+.-mx-48 {
+  margin-left: -12rem;
+  margin-right: -12rem;
+}
+
+.-my-56 {
+  margin-top: -14rem;
+  margin-bottom: -14rem;
+}
+
+.-mx-56 {
+  margin-left: -14rem;
+  margin-right: -14rem;
+}
+
+.-my-64 {
+  margin-top: -16rem;
+  margin-bottom: -16rem;
+}
+
+.-mx-64 {
+  margin-left: -16rem;
+  margin-right: -16rem;
+}
+
+.-my-px {
+  margin-top: -1px;
+  margin-bottom: -1px;
+}
+
+.-mx-px {
+  margin-left: -1px;
+  margin-right: -1px;
+}
+
+.mt-0 {
+  margin-top: 0;
+}
+
+.mr-0 {
+  margin-right: 0;
+}
+
+.mb-0 {
+  margin-bottom: 0;
+}
+
+.ml-0 {
+  margin-left: 0;
+}
+
+.mt-1 {
+  margin-top: 0.25rem;
+}
+
+.mr-1 {
+  margin-right: 0.25rem;
+}
+
+.mb-1 {
+  margin-bottom: 0.25rem;
+}
+
+.ml-1 {
+  margin-left: 0.25rem;
+}
+
+.mt-2 {
+  margin-top: 0.5rem;
+}
+
+.mr-2 {
+  margin-right: 0.5rem;
+}
+
+.mb-2 {
+  margin-bottom: 0.5rem;
+}
+
+.ml-2 {
+  margin-left: 0.5rem;
+}
+
+.mt-3 {
+  margin-top: 0.75rem;
+}
+
+.mr-3 {
+  margin-right: 0.75rem;
+}
+
+.mb-3 {
+  margin-bottom: 0.75rem;
+}
+
+.ml-3 {
+  margin-left: 0.75rem;
+}
+
+.mt-4 {
+  margin-top: 1rem;
+}
+
+.mr-4 {
+  margin-right: 1rem;
+}
+
+.mb-4 {
+  margin-bottom: 1rem;
+}
+
+.ml-4 {
+  margin-left: 1rem;
+}
+
+.mt-5 {
+  margin-top: 1.25rem;
+}
+
+.mr-5 {
+  margin-right: 1.25rem;
+}
+
+.mb-5 {
+  margin-bottom: 1.25rem;
+}
+
+.ml-5 {
+  margin-left: 1.25rem;
+}
+
+.mt-6 {
+  margin-top: 1.5rem;
+}
+
+.mr-6 {
+  margin-right: 1.5rem;
+}
+
+.mb-6 {
+  margin-bottom: 1.5rem;
+}
+
+.ml-6 {
+  margin-left: 1.5rem;
+}
+
+.mt-8 {
+  margin-top: 2rem;
+}
+
+.mr-8 {
+  margin-right: 2rem;
+}
+
+.mb-8 {
+  margin-bottom: 2rem;
+}
+
+.ml-8 {
+  margin-left: 2rem;
+}
+
+.mt-10 {
+  margin-top: 2.5rem;
+}
+
+.mr-10 {
+  margin-right: 2.5rem;
+}
+
+.mb-10 {
+  margin-bottom: 2.5rem;
+}
+
+.ml-10 {
+  margin-left: 2.5rem;
+}
+
+.mt-12 {
+  margin-top: 3rem;
+}
+
+.mr-12 {
+  margin-right: 3rem;
+}
+
+.mb-12 {
+  margin-bottom: 3rem;
+}
+
+.ml-12 {
+  margin-left: 3rem;
+}
+
+.mt-16 {
+  margin-top: 4rem;
+}
+
+.mr-16 {
+  margin-right: 4rem;
+}
+
+.mb-16 {
+  margin-bottom: 4rem;
+}
+
+.ml-16 {
+  margin-left: 4rem;
+}
+
+.mt-20 {
+  margin-top: 5rem;
+}
+
+.mr-20 {
+  margin-right: 5rem;
+}
+
+.mb-20 {
+  margin-bottom: 5rem;
+}
+
+.ml-20 {
+  margin-left: 5rem;
+}
+
+.mt-24 {
+  margin-top: 6rem;
+}
+
+.mr-24 {
+  margin-right: 6rem;
+}
+
+.mb-24 {
+  margin-bottom: 6rem;
+}
+
+.ml-24 {
+  margin-left: 6rem;
+}
+
+.mt-32 {
+  margin-top: 8rem;
+}
+
+.mr-32 {
+  margin-right: 8rem;
+}
+
+.mb-32 {
+  margin-bottom: 8rem;
+}
+
+.ml-32 {
+  margin-left: 8rem;
+}
+
+.mt-40 {
+  margin-top: 10rem;
+}
+
+.mr-40 {
+  margin-right: 10rem;
+}
+
+.mb-40 {
+  margin-bottom: 10rem;
+}
+
+.ml-40 {
+  margin-left: 10rem;
+}
+
+.mt-48 {
+  margin-top: 12rem;
+}
+
+.mr-48 {
+  margin-right: 12rem;
+}
+
+.mb-48 {
+  margin-bottom: 12rem;
+}
+
+.ml-48 {
+  margin-left: 12rem;
+}
+
+.mt-56 {
+  margin-top: 14rem;
+}
+
+.mr-56 {
+  margin-right: 14rem;
+}
+
+.mb-56 {
+  margin-bottom: 14rem;
+}
+
+.ml-56 {
+  margin-left: 14rem;
+}
+
+.mt-64 {
+  margin-top: 16rem;
+}
+
+.mr-64 {
+  margin-right: 16rem;
+}
+
+.mb-64 {
+  margin-bottom: 16rem;
+}
+
+.ml-64 {
+  margin-left: 16rem;
+}
+
+.mt-auto {
+  margin-top: auto;
+}
+
+.mr-auto {
+  margin-right: auto;
+}
+
+.mb-auto {
+  margin-bottom: auto;
+}
+
+.ml-auto {
+  margin-left: auto;
+}
+
+.mt-px {
+  margin-top: 1px;
+}
+
+.mr-px {
+  margin-right: 1px;
+}
+
+.mb-px {
+  margin-bottom: 1px;
+}
+
+.ml-px {
+  margin-left: 1px;
+}
+
+.-mt-1 {
+  margin-top: -0.25rem;
+}
+
+.-mr-1 {
+  margin-right: -0.25rem;
+}
+
+.-mb-1 {
+  margin-bottom: -0.25rem;
+}
+
+.-ml-1 {
+  margin-left: -0.25rem;
+}
+
+.-mt-2 {
+  margin-top: -0.5rem;
+}
+
+.-mr-2 {
+  margin-right: -0.5rem;
+}
+
+.-mb-2 {
+  margin-bottom: -0.5rem;
+}
+
+.-ml-2 {
+  margin-left: -0.5rem;
+}
+
+.-mt-3 {
+  margin-top: -0.75rem;
+}
+
+.-mr-3 {
+  margin-right: -0.75rem;
+}
+
+.-mb-3 {
+  margin-bottom: -0.75rem;
+}
+
+.-ml-3 {
+  margin-left: -0.75rem;
+}
+
+.-mt-4 {
+  margin-top: -1rem;
+}
+
+.-mr-4 {
+  margin-right: -1rem;
+}
+
+.-mb-4 {
+  margin-bottom: -1rem;
+}
+
+.-ml-4 {
+  margin-left: -1rem;
+}
+
+.-mt-5 {
+  margin-top: -1.25rem;
+}
+
+.-mr-5 {
+  margin-right: -1.25rem;
+}
+
+.-mb-5 {
+  margin-bottom: -1.25rem;
+}
+
+.-ml-5 {
+  margin-left: -1.25rem;
+}
+
+.-mt-6 {
+  margin-top: -1.5rem;
+}
+
+.-mr-6 {
+  margin-right: -1.5rem;
+}
+
+.-mb-6 {
+  margin-bottom: -1.5rem;
+}
+
+.-ml-6 {
+  margin-left: -1.5rem;
+}
+
+.-mt-8 {
+  margin-top: -2rem;
+}
+
+.-mr-8 {
+  margin-right: -2rem;
+}
+
+.-mb-8 {
+  margin-bottom: -2rem;
+}
+
+.-ml-8 {
+  margin-left: -2rem;
+}
+
+.-mt-10 {
+  margin-top: -2.5rem;
+}
+
+.-mr-10 {
+  margin-right: -2.5rem;
+}
+
+.-mb-10 {
+  margin-bottom: -2.5rem;
+}
+
+.-ml-10 {
+  margin-left: -2.5rem;
+}
+
+.-mt-12 {
+  margin-top: -3rem;
+}
+
+.-mr-12 {
+  margin-right: -3rem;
+}
+
+.-mb-12 {
+  margin-bottom: -3rem;
+}
+
+.-ml-12 {
+  margin-left: -3rem;
+}
+
+.-mt-16 {
+  margin-top: -4rem;
+}
+
+.-mr-16 {
+  margin-right: -4rem;
+}
+
+.-mb-16 {
+  margin-bottom: -4rem;
+}
+
+.-ml-16 {
+  margin-left: -4rem;
+}
+
+.-mt-20 {
+  margin-top: -5rem;
+}
+
+.-mr-20 {
+  margin-right: -5rem;
+}
+
+.-mb-20 {
+  margin-bottom: -5rem;
+}
+
+.-ml-20 {
+  margin-left: -5rem;
+}
+
+.-mt-24 {
+  margin-top: -6rem;
+}
+
+.-mr-24 {
+  margin-right: -6rem;
+}
+
+.-mb-24 {
+  margin-bottom: -6rem;
+}
+
+.-ml-24 {
+  margin-left: -6rem;
+}
+
+.-mt-32 {
+  margin-top: -8rem;
+}
+
+.-mr-32 {
+  margin-right: -8rem;
+}
+
+.-mb-32 {
+  margin-bottom: -8rem;
+}
+
+.-ml-32 {
+  margin-left: -8rem;
+}
+
+.-mt-40 {
+  margin-top: -10rem;
+}
+
+.-mr-40 {
+  margin-right: -10rem;
+}
+
+.-mb-40 {
+  margin-bottom: -10rem;
+}
+
+.-ml-40 {
+  margin-left: -10rem;
+}
+
+.-mt-48 {
+  margin-top: -12rem;
+}
+
+.-mr-48 {
+  margin-right: -12rem;
+}
+
+.-mb-48 {
+  margin-bottom: -12rem;
+}
+
+.-ml-48 {
+  margin-left: -12rem;
+}
+
+.-mt-56 {
+  margin-top: -14rem;
+}
+
+.-mr-56 {
+  margin-right: -14rem;
+}
+
+.-mb-56 {
+  margin-bottom: -14rem;
+}
+
+.-ml-56 {
+  margin-left: -14rem;
+}
+
+.-mt-64 {
+  margin-top: -16rem;
+}
+
+.-mr-64 {
+  margin-right: -16rem;
+}
+
+.-mb-64 {
+  margin-bottom: -16rem;
+}
+
+.-ml-64 {
+  margin-left: -16rem;
+}
+
+.-mt-px {
+  margin-top: -1px;
+}
+
+.-mr-px {
+  margin-right: -1px;
+}
+
+.-mb-px {
+  margin-bottom: -1px;
+}
+
+.-ml-px {
+  margin-left: -1px;
+}
+
+.max-h-full {
+  max-height: 100%;
+}
+
+.max-h-screen {
+  max-height: 100vh;
+}
+
+.max-w-none {
+  max-width: none;
+}
+
+.max-w-xs {
+  max-width: 20rem;
+}
+
+.max-w-sm {
+  max-width: 24rem;
+}
+
+.max-w-md {
+  max-width: 28rem;
+}
+
+.max-w-lg {
+  max-width: 32rem;
+}
+
+.max-w-xl {
+  max-width: 36rem;
+}
+
+.max-w-2xl {
+  max-width: 42rem;
+}
+
+.max-w-3xl {
+  max-width: 48rem;
+}
+
+.max-w-4xl {
+  max-width: 56rem;
+}
+
+.max-w-5xl {
+  max-width: 64rem;
+}
+
+.max-w-6xl {
+  max-width: 72rem;
+}
+
+.max-w-full {
+  max-width: 100%;
+}
+
+.max-w-screen-sm {
+  max-width: 640px;
+}
+
+.max-w-screen-md {
+  max-width: 768px;
+}
+
+.max-w-screen-lg {
+  max-width: 1024px;
+}
+
+.max-w-screen-xl {
+  max-width: 1280px;
+}
+
+.min-h-0 {
+  min-height: 0;
+}
+
+.min-h-full {
+  min-height: 100%;
+}
+
+.min-h-screen {
+  min-height: 100vh;
+}
+
+.min-w-0 {
+  min-width: 0;
+}
+
+.min-w-full {
+  min-width: 100%;
+}
+
+.object-contain {
+  -o-object-fit: contain;
+     object-fit: contain;
+}
+
+.object-cover {
+  -o-object-fit: cover;
+     object-fit: cover;
+}
+
+.object-fill {
+  -o-object-fit: fill;
+     object-fit: fill;
+}
+
+.object-none {
+  -o-object-fit: none;
+     object-fit: none;
+}
+
+.object-scale-down {
+  -o-object-fit: scale-down;
+     object-fit: scale-down;
+}
+
+.object-bottom {
+  -o-object-position: bottom;
+     object-position: bottom;
+}
+
+.object-center {
+  -o-object-position: center;
+     object-position: center;
+}
+
+.object-left {
+  -o-object-position: left;
+     object-position: left;
+}
+
+.object-left-bottom {
+  -o-object-position: left bottom;
+     object-position: left bottom;
+}
+
+.object-left-top {
+  -o-object-position: left top;
+     object-position: left top;
+}
+
+.object-right {
+  -o-object-position: right;
+     object-position: right;
+}
+
+.object-right-bottom {
+  -o-object-position: right bottom;
+     object-position: right bottom;
+}
+
+.object-right-top {
+  -o-object-position: right top;
+     object-position: right top;
+}
+
+.object-top {
+  -o-object-position: top;
+     object-position: top;
+}
+
+.opacity-0 {
+  opacity: 0;
+}
+
+.opacity-25 {
+  opacity: 0.25;
+}
+
+.opacity-50 {
+  opacity: 0.5;
+}
+
+.opacity-75 {
+  opacity: 0.75;
+}
+
+.opacity-100 {
+  opacity: 1;
+}
+
+.hover\:opacity-0:hover {
+  opacity: 0;
+}
+
+.hover\:opacity-25:hover {
+  opacity: 0.25;
+}
+
+.hover\:opacity-50:hover {
+  opacity: 0.5;
+}
+
+.hover\:opacity-75:hover {
+  opacity: 0.75;
+}
+
+.hover\:opacity-100:hover {
+  opacity: 1;
+}
+
+.focus\:opacity-0:focus {
+  opacity: 0;
+}
+
+.focus\:opacity-25:focus {
+  opacity: 0.25;
+}
+
+.focus\:opacity-50:focus {
+  opacity: 0.5;
+}
+
+.focus\:opacity-75:focus {
+  opacity: 0.75;
+}
+
+.focus\:opacity-100:focus {
+  opacity: 1;
+}
+
+.outline-none {
+  outline: 0;
+}
+
+.focus\:outline-none:focus {
+  outline: 0;
+}
+
+.overflow-auto {
+  overflow: auto;
+}
+
+.overflow-hidden {
+  overflow: hidden;
+}
+
+.overflow-visible {
+  overflow: visible;
+}
+
+.overflow-scroll {
+  overflow: scroll;
+}
+
+.overflow-x-auto {
+  overflow-x: auto;
+}
+
+.overflow-y-auto {
+  overflow-y: auto;
+}
+
+.overflow-x-hidden {
+  overflow-x: hidden;
+}
+
+.overflow-y-hidden {
+  overflow-y: hidden;
+}
+
+.overflow-x-visible {
+  overflow-x: visible;
+}
+
+.overflow-y-visible {
+  overflow-y: visible;
+}
+
+.overflow-x-scroll {
+  overflow-x: scroll;
+}
+
+.overflow-y-scroll {
+  overflow-y: scroll;
+}
+
+.scrolling-touch {
+  -webkit-overflow-scrolling: touch;
+}
+
+.scrolling-auto {
+  -webkit-overflow-scrolling: auto;
+}
+
+.overscroll-auto {
+  -ms-scroll-chaining: chained;
+      overscroll-behavior: auto;
+}
+
+.overscroll-contain {
+  -ms-scroll-chaining: none;
+      overscroll-behavior: contain;
+}
+
+.overscroll-none {
+  -ms-scroll-chaining: none;
+      overscroll-behavior: none;
+}
+
+.overscroll-y-auto {
+  overscroll-behavior-y: auto;
+}
+
+.overscroll-y-contain {
+  overscroll-behavior-y: contain;
+}
+
+.overscroll-y-none {
+  overscroll-behavior-y: none;
+}
+
+.overscroll-x-auto {
+  overscroll-behavior-x: auto;
+}
+
+.overscroll-x-contain {
+  overscroll-behavior-x: contain;
+}
+
+.overscroll-x-none {
+  overscroll-behavior-x: none;
+}
+
+.p-0 {
+  padding: 0;
+}
+
+.p-1 {
+  padding: 0.25rem;
+}
+
+.p-2 {
+  padding: 0.5rem;
+}
+
+.p-3 {
+  padding: 0.75rem;
+}
+
+.p-4 {
+  padding: 1rem;
+}
+
+.p-5 {
+  padding: 1.25rem;
+}
+
+.p-6 {
+  padding: 1.5rem;
+}
+
+.p-8 {
+  padding: 2rem;
+}
+
+.p-10 {
+  padding: 2.5rem;
+}
+
+.p-12 {
+  padding: 3rem;
+}
+
+.p-16 {
+  padding: 4rem;
+}
+
+.p-20 {
+  padding: 5rem;
+}
+
+.p-24 {
+  padding: 6rem;
+}
+
+.p-32 {
+  padding: 8rem;
+}
+
+.p-40 {
+  padding: 10rem;
+}
+
+.p-48 {
+  padding: 12rem;
+}
+
+.p-56 {
+  padding: 14rem;
+}
+
+.p-64 {
+  padding: 16rem;
+}
+
+.p-px {
+  padding: 1px;
+}
+
+.py-0 {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.px-0 {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.py-1 {
+  padding-top: 0.25rem;
+  padding-bottom: 0.25rem;
+}
+
+.px-1 {
+  padding-left: 0.25rem;
+  padding-right: 0.25rem;
+}
+
+.py-2 {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.px-2 {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+
+.py-3 {
+  padding-top: 0.75rem;
+  padding-bottom: 0.75rem;
+}
+
+.px-3 {
+  padding-left: 0.75rem;
+  padding-right: 0.75rem;
+}
+
+.py-4 {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.px-4 {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+
+.py-5 {
+  padding-top: 1.25rem;
+  padding-bottom: 1.25rem;
+}
+
+.px-5 {
+  padding-left: 1.25rem;
+  padding-right: 1.25rem;
+}
+
+.py-6 {
+  padding-top: 1.5rem;
+  padding-bottom: 1.5rem;
+}
+
+.px-6 {
+  padding-left: 1.5rem;
+  padding-right: 1.5rem;
+}
+
+.py-8 {
+  padding-top: 2rem;
+  padding-bottom: 2rem;
+}
+
+.px-8 {
+  padding-left: 2rem;
+  padding-right: 2rem;
+}
+
+.py-10 {
+  padding-top: 2.5rem;
+  padding-bottom: 2.5rem;
+}
+
+.px-10 {
+  padding-left: 2.5rem;
+  padding-right: 2.5rem;
+}
+
+.py-12 {
+  padding-top: 3rem;
+  padding-bottom: 3rem;
+}
+
+.px-12 {
+  padding-left: 3rem;
+  padding-right: 3rem;
+}
+
+.py-16 {
+  padding-top: 4rem;
+  padding-bottom: 4rem;
+}
+
+.px-16 {
+  padding-left: 4rem;
+  padding-right: 4rem;
+}
+
+.py-20 {
+  padding-top: 5rem;
+  padding-bottom: 5rem;
+}
+
+.px-20 {
+  padding-left: 5rem;
+  padding-right: 5rem;
+}
+
+.py-24 {
+  padding-top: 6rem;
+  padding-bottom: 6rem;
+}
+
+.px-24 {
+  padding-left: 6rem;
+  padding-right: 6rem;
+}
+
+.py-32 {
+  padding-top: 8rem;
+  padding-bottom: 8rem;
+}
+
+.px-32 {
+  padding-left: 8rem;
+  padding-right: 8rem;
+}
+
+.py-40 {
+  padding-top: 10rem;
+  padding-bottom: 10rem;
+}
+
+.px-40 {
+  padding-left: 10rem;
+  padding-right: 10rem;
+}
+
+.py-48 {
+  padding-top: 12rem;
+  padding-bottom: 12rem;
+}
+
+.px-48 {
+  padding-left: 12rem;
+  padding-right: 12rem;
+}
+
+.py-56 {
+  padding-top: 14rem;
+  padding-bottom: 14rem;
+}
+
+.px-56 {
+  padding-left: 14rem;
+  padding-right: 14rem;
+}
+
+.py-64 {
+  padding-top: 16rem;
+  padding-bottom: 16rem;
+}
+
+.px-64 {
+  padding-left: 16rem;
+  padding-right: 16rem;
+}
+
+.py-px {
+  padding-top: 1px;
+  padding-bottom: 1px;
+}
+
+.px-px {
+  padding-left: 1px;
+  padding-right: 1px;
+}
+
+.pt-0 {
+  padding-top: 0;
+}
+
+.pr-0 {
+  padding-right: 0;
+}
+
+.pb-0 {
+  padding-bottom: 0;
+}
+
+.pl-0 {
+  padding-left: 0;
+}
+
+.pt-1 {
+  padding-top: 0.25rem;
+}
+
+.pr-1 {
+  padding-right: 0.25rem;
+}
+
+.pb-1 {
+  padding-bottom: 0.25rem;
+}
+
+.pl-1 {
+  padding-left: 0.25rem;
+}
+
+.pt-2 {
+  padding-top: 0.5rem;
+}
+
+.pr-2 {
+  padding-right: 0.5rem;
+}
+
+.pb-2 {
+  padding-bottom: 0.5rem;
+}
+
+.pl-2 {
+  padding-left: 0.5rem;
+}
+
+.pt-3 {
+  padding-top: 0.75rem;
+}
+
+.pr-3 {
+  padding-right: 0.75rem;
+}
+
+.pb-3 {
+  padding-bottom: 0.75rem;
+}
+
+.pl-3 {
+  padding-left: 0.75rem;
+}
+
+.pt-4 {
+  padding-top: 1rem;
+}
+
+.pr-4 {
+  padding-right: 1rem;
+}
+
+.pb-4 {
+  padding-bottom: 1rem;
+}
+
+.pl-4 {
+  padding-left: 1rem;
+}
+
+.pt-5 {
+  padding-top: 1.25rem;
+}
+
+.pr-5 {
+  padding-right: 1.25rem;
+}
+
+.pb-5 {
+  padding-bottom: 1.25rem;
+}
+
+.pl-5 {
+  padding-left: 1.25rem;
+}
+
+.pt-6 {
+  padding-top: 1.5rem;
+}
+
+.pr-6 {
+  padding-right: 1.5rem;
+}
+
+.pb-6 {
+  padding-bottom: 1.5rem;
+}
+
+.pl-6 {
+  padding-left: 1.5rem;
+}
+
+.pt-8 {
+  padding-top: 2rem;
+}
+
+.pr-8 {
+  padding-right: 2rem;
+}
+
+.pb-8 {
+  padding-bottom: 2rem;
+}
+
+.pl-8 {
+  padding-left: 2rem;
+}
+
+.pt-10 {
+  padding-top: 2.5rem;
+}
+
+.pr-10 {
+  padding-right: 2.5rem;
+}
+
+.pb-10 {
+  padding-bottom: 2.5rem;
+}
+
+.pl-10 {
+  padding-left: 2.5rem;
+}
+
+.pt-12 {
+  padding-top: 3rem;
+}
+
+.pr-12 {
+  padding-right: 3rem;
+}
+
+.pb-12 {
+  padding-bottom: 3rem;
+}
+
+.pl-12 {
+  padding-left: 3rem;
+}
+
+.pt-16 {
+  padding-top: 4rem;
+}
+
+.pr-16 {
+  padding-right: 4rem;
+}
+
+.pb-16 {
+  padding-bottom: 4rem;
+}
+
+.pl-16 {
+  padding-left: 4rem;
+}
+
+.pt-20 {
+  padding-top: 5rem;
+}
+
+.pr-20 {
+  padding-right: 5rem;
+}
+
+.pb-20 {
+  padding-bottom: 5rem;
+}
+
+.pl-20 {
+  padding-left: 5rem;
+}
+
+.pt-24 {
+  padding-top: 6rem;
+}
+
+.pr-24 {
+  padding-right: 6rem;
+}
+
+.pb-24 {
+  padding-bottom: 6rem;
+}
+
+.pl-24 {
+  padding-left: 6rem;
+}
+
+.pt-32 {
+  padding-top: 8rem;
+}
+
+.pr-32 {
+  padding-right: 8rem;
+}
+
+.pb-32 {
+  padding-bottom: 8rem;
+}
+
+.pl-32 {
+  padding-left: 8rem;
+}
+
+.pt-40 {
+  padding-top: 10rem;
+}
+
+.pr-40 {
+  padding-right: 10rem;
+}
+
+.pb-40 {
+  padding-bottom: 10rem;
+}
+
+.pl-40 {
+  padding-left: 10rem;
+}
+
+.pt-48 {
+  padding-top: 12rem;
+}
+
+.pr-48 {
+  padding-right: 12rem;
+}
+
+.pb-48 {
+  padding-bottom: 12rem;
+}
+
+.pl-48 {
+  padding-left: 12rem;
+}
+
+.pt-56 {
+  padding-top: 14rem;
+}
+
+.pr-56 {
+  padding-right: 14rem;
+}
+
+.pb-56 {
+  padding-bottom: 14rem;
+}
+
+.pl-56 {
+  padding-left: 14rem;
+}
+
+.pt-64 {
+  padding-top: 16rem;
+}
+
+.pr-64 {
+  padding-right: 16rem;
+}
+
+.pb-64 {
+  padding-bottom: 16rem;
+}
+
+.pl-64 {
+  padding-left: 16rem;
+}
+
+.pt-px {
+  padding-top: 1px;
+}
+
+.pr-px {
+  padding-right: 1px;
+}
+
+.pb-px {
+  padding-bottom: 1px;
+}
+
+.pl-px {
+  padding-left: 1px;
+}
+
+.placeholder-transparent::-moz-placeholder {
+  color: transparent;
+}
+
+.placeholder-transparent:-ms-input-placeholder {
+  color: transparent;
+}
+
+.placeholder-transparent::placeholder {
+  color: transparent;
+}
+
+.placeholder-current::-moz-placeholder {
+  color: currentColor;
+}
+
+.placeholder-current:-ms-input-placeholder {
+  color: currentColor;
+}
+
+.placeholder-current::placeholder {
+  color: currentColor;
+}
+
+.placeholder-black::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.placeholder-black:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.placeholder-black::placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.placeholder-white::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.placeholder-white:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.placeholder-white::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.placeholder-gray-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.placeholder-gray-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.placeholder-gray-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.placeholder-gray-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.placeholder-gray-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.placeholder-gray-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.placeholder-gray-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.placeholder-gray-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.placeholder-gray-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.placeholder-gray-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.placeholder-gray-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.placeholder-gray-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.placeholder-gray-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.placeholder-gray-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.placeholder-gray-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.placeholder-gray-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.placeholder-gray-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.placeholder-gray-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.placeholder-gray-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.placeholder-gray-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.placeholder-gray-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.placeholder-gray-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.placeholder-gray-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.placeholder-gray-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.placeholder-gray-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.placeholder-gray-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.placeholder-gray-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.placeholder-red-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.placeholder-red-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.placeholder-red-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.placeholder-red-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.placeholder-red-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.placeholder-red-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.placeholder-red-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.placeholder-red-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.placeholder-red-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.placeholder-red-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.placeholder-red-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.placeholder-red-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.placeholder-red-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.placeholder-red-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.placeholder-red-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.placeholder-red-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.placeholder-red-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.placeholder-red-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.placeholder-red-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.placeholder-red-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.placeholder-red-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.placeholder-red-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.placeholder-red-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.placeholder-red-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.placeholder-orange-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.placeholder-orange-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.placeholder-orange-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.placeholder-orange-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.placeholder-orange-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.placeholder-orange-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.placeholder-orange-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.placeholder-orange-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.placeholder-orange-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.placeholder-orange-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.placeholder-orange-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.placeholder-orange-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.placeholder-orange-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.placeholder-orange-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.placeholder-orange-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.placeholder-orange-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.placeholder-orange-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.placeholder-orange-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.placeholder-orange-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.placeholder-orange-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.placeholder-orange-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.placeholder-orange-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.placeholder-yellow-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.placeholder-green-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.placeholder-green-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.placeholder-green-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.placeholder-green-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.placeholder-green-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.placeholder-green-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.placeholder-green-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.placeholder-green-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.placeholder-green-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.placeholder-green-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.placeholder-green-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.placeholder-green-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.placeholder-green-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.placeholder-green-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.placeholder-green-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.placeholder-green-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.placeholder-green-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.placeholder-green-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.placeholder-green-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.placeholder-green-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.placeholder-green-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.placeholder-green-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.placeholder-green-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.placeholder-green-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.placeholder-green-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.placeholder-green-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.placeholder-green-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.placeholder-teal-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.placeholder-teal-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.placeholder-teal-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.placeholder-teal-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.placeholder-teal-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.placeholder-teal-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.placeholder-teal-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.placeholder-teal-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.placeholder-teal-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.placeholder-teal-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.placeholder-teal-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.placeholder-teal-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.placeholder-teal-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.placeholder-teal-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.placeholder-teal-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.placeholder-teal-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.placeholder-teal-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.placeholder-teal-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.placeholder-teal-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.placeholder-teal-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.placeholder-teal-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.placeholder-teal-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.placeholder-teal-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.placeholder-teal-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.placeholder-teal-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.placeholder-teal-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.placeholder-teal-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.placeholder-blue-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.placeholder-blue-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.placeholder-blue-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.placeholder-blue-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.placeholder-blue-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.placeholder-blue-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.placeholder-blue-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.placeholder-blue-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.placeholder-blue-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.placeholder-blue-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.placeholder-blue-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.placeholder-blue-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.placeholder-blue-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.placeholder-blue-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.placeholder-blue-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.placeholder-blue-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.placeholder-blue-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.placeholder-blue-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.placeholder-blue-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.placeholder-blue-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.placeholder-blue-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.placeholder-blue-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.placeholder-blue-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.placeholder-blue-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.placeholder-blue-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.placeholder-blue-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.placeholder-blue-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.placeholder-indigo-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.placeholder-purple-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.placeholder-purple-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.placeholder-purple-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.placeholder-purple-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.placeholder-purple-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.placeholder-purple-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.placeholder-purple-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.placeholder-purple-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.placeholder-purple-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.placeholder-purple-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.placeholder-purple-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.placeholder-purple-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.placeholder-purple-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.placeholder-purple-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.placeholder-purple-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.placeholder-purple-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.placeholder-purple-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.placeholder-purple-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.placeholder-purple-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.placeholder-purple-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.placeholder-purple-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.placeholder-purple-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.placeholder-purple-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.placeholder-purple-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.placeholder-purple-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.placeholder-purple-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.placeholder-purple-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.placeholder-pink-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.placeholder-pink-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.placeholder-pink-100::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.placeholder-pink-200::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.placeholder-pink-200:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.placeholder-pink-200::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.placeholder-pink-300::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.placeholder-pink-300:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.placeholder-pink-300::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.placeholder-pink-400::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.placeholder-pink-400:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.placeholder-pink-400::placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.placeholder-pink-500::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.placeholder-pink-500:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.placeholder-pink-500::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.placeholder-pink-600::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.placeholder-pink-600:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.placeholder-pink-600::placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.placeholder-pink-700::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.placeholder-pink-700:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.placeholder-pink-700::placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.placeholder-pink-800::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.placeholder-pink-800:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.placeholder-pink-800::placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.placeholder-pink-900::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.placeholder-pink-900:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.placeholder-pink-900::placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-transparent:focus::-moz-placeholder {
+  color: transparent;
+}
+
+.focus\:placeholder-transparent:focus:-ms-input-placeholder {
+  color: transparent;
+}
+
+.focus\:placeholder-transparent:focus::placeholder {
+  color: transparent;
+}
+
+.focus\:placeholder-current:focus::-moz-placeholder {
+  color: currentColor;
+}
+
+.focus\:placeholder-current:focus:-ms-input-placeholder {
+  color: currentColor;
+}
+
+.focus\:placeholder-current:focus::placeholder {
+  color: currentColor;
+}
+
+.focus\:placeholder-black:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-black:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-black:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-white:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-white:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-white:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-gray-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-red-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-orange-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-yellow-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-green-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-teal-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-blue-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-indigo-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-purple-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-100:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-200:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-200:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-300:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-300:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-400:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-400:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-500:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-500:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-600:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-600:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-700:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-700:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-800:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-800:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-900:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.focus\:placeholder-pink-900:focus::placeholder {
+  --placeholder-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--placeholder-opacity));
+}
+
+.placeholder-opacity-0::-moz-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.placeholder-opacity-0:-ms-input-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.placeholder-opacity-0::placeholder {
+  --placeholder-opacity: 0;
+}
+
+.placeholder-opacity-25::-moz-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.placeholder-opacity-25:-ms-input-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.placeholder-opacity-25::placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.placeholder-opacity-50::-moz-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.placeholder-opacity-50:-ms-input-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.placeholder-opacity-50::placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.placeholder-opacity-75::-moz-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.placeholder-opacity-75:-ms-input-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.placeholder-opacity-75::placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.placeholder-opacity-100::-moz-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.placeholder-opacity-100:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.placeholder-opacity-100::placeholder {
+  --placeholder-opacity: 1;
+}
+
+.focus\:placeholder-opacity-0:focus::-moz-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0;
+}
+
+.focus\:placeholder-opacity-0:focus::placeholder {
+  --placeholder-opacity: 0;
+}
+
+.focus\:placeholder-opacity-25:focus::-moz-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.focus\:placeholder-opacity-25:focus::placeholder {
+  --placeholder-opacity: 0.25;
+}
+
+.focus\:placeholder-opacity-50:focus::-moz-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.focus\:placeholder-opacity-50:focus::placeholder {
+  --placeholder-opacity: 0.5;
+}
+
+.focus\:placeholder-opacity-75:focus::-moz-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.focus\:placeholder-opacity-75:focus::placeholder {
+  --placeholder-opacity: 0.75;
+}
+
+.focus\:placeholder-opacity-100:focus::-moz-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+  --placeholder-opacity: 1;
+}
+
+.focus\:placeholder-opacity-100:focus::placeholder {
+  --placeholder-opacity: 1;
+}
+
+.pointer-events-none {
+  pointer-events: none;
+}
+
+.pointer-events-auto {
+  pointer-events: auto;
+}
+
+.static {
+  position: static;
+}
+
+.fixed {
+  position: fixed;
+}
+
+.absolute {
+  position: absolute;
+}
+
+.relative {
+  position: relative;
+}
+
+.sticky {
+  position: -webkit-sticky;
+  position: sticky;
+}
+
+.inset-0 {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.inset-auto {
+  top: auto;
+  right: auto;
+  bottom: auto;
+  left: auto;
+}
+
+.inset-y-0 {
+  top: 0;
+  bottom: 0;
+}
+
+.inset-x-0 {
+  right: 0;
+  left: 0;
+}
+
+.inset-y-auto {
+  top: auto;
+  bottom: auto;
+}
+
+.inset-x-auto {
+  right: auto;
+  left: auto;
+}
+
+.top-0 {
+  top: 0;
+}
+
+.right-0 {
+  right: 0;
+}
+
+.bottom-0 {
+  bottom: 0;
+}
+
+.left-0 {
+  left: 0;
+}
+
+.top-auto {
+  top: auto;
+}
+
+.right-auto {
+  right: auto;
+}
+
+.bottom-auto {
+  bottom: auto;
+}
+
+.left-auto {
+  left: auto;
+}
+
+.resize-none {
+  resize: none;
+}
+
+.resize-y {
+  resize: vertical;
+}
+
+.resize-x {
+  resize: horizontal;
+}
+
+.resize {
+  resize: both;
+}
+
+.shadow-xs {
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.shadow-sm {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.shadow {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.shadow-md {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.shadow-lg {
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.shadow-xl {
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.shadow-2xl {
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.shadow-inner {
+  box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+}
+
+.shadow-outline {
+  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+}
+
+.shadow-none {
+  box-shadow: none;
+}
+
+.hover\:shadow-xs:hover {
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.hover\:shadow-sm:hover {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.hover\:shadow:hover {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.hover\:shadow-md:hover {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.hover\:shadow-lg:hover {
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.hover\:shadow-xl:hover {
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.hover\:shadow-2xl:hover {
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.hover\:shadow-inner:hover {
+  box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+}
+
+.hover\:shadow-outline:hover {
+  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+}
+
+.hover\:shadow-none:hover {
+  box-shadow: none;
+}
+
+.focus\:shadow-xs:focus {
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.focus\:shadow-sm:focus {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.focus\:shadow:focus {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.focus\:shadow-md:focus {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.focus\:shadow-lg:focus {
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.focus\:shadow-xl:focus {
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.focus\:shadow-2xl:focus {
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.focus\:shadow-inner:focus {
+  box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+}
+
+.focus\:shadow-outline:focus {
+  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+}
+
+.focus\:shadow-none:focus {
+  box-shadow: none;
+}
+
+.fill-current {
+  fill: currentColor;
+}
+
+.stroke-current {
+  stroke: currentColor;
+}
+
+.stroke-0 {
+  stroke-width: 0;
+}
+
+.stroke-1 {
+  stroke-width: 1;
+}
+
+.stroke-2 {
+  stroke-width: 2;
+}
+
+.table-auto {
+  table-layout: auto;
+}
+
+.table-fixed {
+  table-layout: fixed;
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.text-justify {
+  text-align: justify;
+}
+
+.text-transparent {
+  color: transparent;
+}
+
+.text-current {
+  color: currentColor;
+}
+
+.text-black {
+  --text-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--text-opacity));
+}
+
+.text-white {
+  --text-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--text-opacity));
+}
+
+.text-gray-100 {
+  --text-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--text-opacity));
+}
+
+.text-gray-200 {
+  --text-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--text-opacity));
+}
+
+.text-gray-300 {
+  --text-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--text-opacity));
+}
+
+.text-gray-400 {
+  --text-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--text-opacity));
+}
+
+.text-gray-500 {
+  --text-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--text-opacity));
+}
+
+.text-gray-600 {
+  --text-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--text-opacity));
+}
+
+.text-gray-700 {
+  --text-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--text-opacity));
+}
+
+.text-gray-800 {
+  --text-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--text-opacity));
+}
+
+.text-gray-900 {
+  --text-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--text-opacity));
+}
+
+.text-red-100 {
+  --text-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--text-opacity));
+}
+
+.text-red-200 {
+  --text-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--text-opacity));
+}
+
+.text-red-300 {
+  --text-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--text-opacity));
+}
+
+.text-red-400 {
+  --text-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--text-opacity));
+}
+
+.text-red-500 {
+  --text-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--text-opacity));
+}
+
+.text-red-600 {
+  --text-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--text-opacity));
+}
+
+.text-red-700 {
+  --text-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--text-opacity));
+}
+
+.text-red-800 {
+  --text-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--text-opacity));
+}
+
+.text-red-900 {
+  --text-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--text-opacity));
+}
+
+.text-orange-100 {
+  --text-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--text-opacity));
+}
+
+.text-orange-200 {
+  --text-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--text-opacity));
+}
+
+.text-orange-300 {
+  --text-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--text-opacity));
+}
+
+.text-orange-400 {
+  --text-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--text-opacity));
+}
+
+.text-orange-500 {
+  --text-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--text-opacity));
+}
+
+.text-orange-600 {
+  --text-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--text-opacity));
+}
+
+.text-orange-700 {
+  --text-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--text-opacity));
+}
+
+.text-orange-800 {
+  --text-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--text-opacity));
+}
+
+.text-orange-900 {
+  --text-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--text-opacity));
+}
+
+.text-yellow-100 {
+  --text-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--text-opacity));
+}
+
+.text-yellow-200 {
+  --text-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--text-opacity));
+}
+
+.text-yellow-300 {
+  --text-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--text-opacity));
+}
+
+.text-yellow-400 {
+  --text-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--text-opacity));
+}
+
+.text-yellow-500 {
+  --text-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--text-opacity));
+}
+
+.text-yellow-600 {
+  --text-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--text-opacity));
+}
+
+.text-yellow-700 {
+  --text-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--text-opacity));
+}
+
+.text-yellow-800 {
+  --text-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--text-opacity));
+}
+
+.text-yellow-900 {
+  --text-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--text-opacity));
+}
+
+.text-green-100 {
+  --text-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--text-opacity));
+}
+
+.text-green-200 {
+  --text-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--text-opacity));
+}
+
+.text-green-300 {
+  --text-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--text-opacity));
+}
+
+.text-green-400 {
+  --text-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--text-opacity));
+}
+
+.text-green-500 {
+  --text-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--text-opacity));
+}
+
+.text-green-600 {
+  --text-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--text-opacity));
+}
+
+.text-green-700 {
+  --text-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--text-opacity));
+}
+
+.text-green-800 {
+  --text-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--text-opacity));
+}
+
+.text-green-900 {
+  --text-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--text-opacity));
+}
+
+.text-teal-100 {
+  --text-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--text-opacity));
+}
+
+.text-teal-200 {
+  --text-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--text-opacity));
+}
+
+.text-teal-300 {
+  --text-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--text-opacity));
+}
+
+.text-teal-400 {
+  --text-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--text-opacity));
+}
+
+.text-teal-500 {
+  --text-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--text-opacity));
+}
+
+.text-teal-600 {
+  --text-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--text-opacity));
+}
+
+.text-teal-700 {
+  --text-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--text-opacity));
+}
+
+.text-teal-800 {
+  --text-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--text-opacity));
+}
+
+.text-teal-900 {
+  --text-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--text-opacity));
+}
+
+.text-blue-100 {
+  --text-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--text-opacity));
+}
+
+.text-blue-200 {
+  --text-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--text-opacity));
+}
+
+.text-blue-300 {
+  --text-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--text-opacity));
+}
+
+.text-blue-400 {
+  --text-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--text-opacity));
+}
+
+.text-blue-500 {
+  --text-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--text-opacity));
+}
+
+.text-blue-600 {
+  --text-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--text-opacity));
+}
+
+.text-blue-700 {
+  --text-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--text-opacity));
+}
+
+.text-blue-800 {
+  --text-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--text-opacity));
+}
+
+.text-blue-900 {
+  --text-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--text-opacity));
+}
+
+.text-indigo-100 {
+  --text-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--text-opacity));
+}
+
+.text-indigo-200 {
+  --text-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--text-opacity));
+}
+
+.text-indigo-300 {
+  --text-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--text-opacity));
+}
+
+.text-indigo-400 {
+  --text-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--text-opacity));
+}
+
+.text-indigo-500 {
+  --text-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--text-opacity));
+}
+
+.text-indigo-600 {
+  --text-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--text-opacity));
+}
+
+.text-indigo-700 {
+  --text-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--text-opacity));
+}
+
+.text-indigo-800 {
+  --text-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--text-opacity));
+}
+
+.text-indigo-900 {
+  --text-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--text-opacity));
+}
+
+.text-purple-100 {
+  --text-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--text-opacity));
+}
+
+.text-purple-200 {
+  --text-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--text-opacity));
+}
+
+.text-purple-300 {
+  --text-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--text-opacity));
+}
+
+.text-purple-400 {
+  --text-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--text-opacity));
+}
+
+.text-purple-500 {
+  --text-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--text-opacity));
+}
+
+.text-purple-600 {
+  --text-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--text-opacity));
+}
+
+.text-purple-700 {
+  --text-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--text-opacity));
+}
+
+.text-purple-800 {
+  --text-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--text-opacity));
+}
+
+.text-purple-900 {
+  --text-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--text-opacity));
+}
+
+.text-pink-100 {
+  --text-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--text-opacity));
+}
+
+.text-pink-200 {
+  --text-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--text-opacity));
+}
+
+.text-pink-300 {
+  --text-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--text-opacity));
+}
+
+.text-pink-400 {
+  --text-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--text-opacity));
+}
+
+.text-pink-500 {
+  --text-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--text-opacity));
+}
+
+.text-pink-600 {
+  --text-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--text-opacity));
+}
+
+.text-pink-700 {
+  --text-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--text-opacity));
+}
+
+.text-pink-800 {
+  --text-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--text-opacity));
+}
+
+.text-pink-900 {
+  --text-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--text-opacity));
+}
+
+.hover\:text-transparent:hover {
+  color: transparent;
+}
+
+.hover\:text-current:hover {
+  color: currentColor;
+}
+
+.hover\:text-black:hover {
+  --text-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--text-opacity));
+}
+
+.hover\:text-white:hover {
+  --text-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--text-opacity));
+}
+
+.hover\:text-gray-100:hover {
+  --text-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--text-opacity));
+}
+
+.hover\:text-gray-200:hover {
+  --text-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--text-opacity));
+}
+
+.hover\:text-gray-300:hover {
+  --text-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--text-opacity));
+}
+
+.hover\:text-gray-400:hover {
+  --text-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--text-opacity));
+}
+
+.hover\:text-gray-500:hover {
+  --text-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--text-opacity));
+}
+
+.hover\:text-gray-600:hover {
+  --text-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--text-opacity));
+}
+
+.hover\:text-gray-700:hover {
+  --text-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--text-opacity));
+}
+
+.hover\:text-gray-800:hover {
+  --text-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--text-opacity));
+}
+
+.hover\:text-gray-900:hover {
+  --text-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--text-opacity));
+}
+
+.hover\:text-red-100:hover {
+  --text-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--text-opacity));
+}
+
+.hover\:text-red-200:hover {
+  --text-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--text-opacity));
+}
+
+.hover\:text-red-300:hover {
+  --text-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--text-opacity));
+}
+
+.hover\:text-red-400:hover {
+  --text-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--text-opacity));
+}
+
+.hover\:text-red-500:hover {
+  --text-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--text-opacity));
+}
+
+.hover\:text-red-600:hover {
+  --text-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--text-opacity));
+}
+
+.hover\:text-red-700:hover {
+  --text-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--text-opacity));
+}
+
+.hover\:text-red-800:hover {
+  --text-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--text-opacity));
+}
+
+.hover\:text-red-900:hover {
+  --text-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--text-opacity));
+}
+
+.hover\:text-orange-100:hover {
+  --text-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--text-opacity));
+}
+
+.hover\:text-orange-200:hover {
+  --text-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--text-opacity));
+}
+
+.hover\:text-orange-300:hover {
+  --text-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--text-opacity));
+}
+
+.hover\:text-orange-400:hover {
+  --text-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--text-opacity));
+}
+
+.hover\:text-orange-500:hover {
+  --text-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--text-opacity));
+}
+
+.hover\:text-orange-600:hover {
+  --text-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--text-opacity));
+}
+
+.hover\:text-orange-700:hover {
+  --text-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--text-opacity));
+}
+
+.hover\:text-orange-800:hover {
+  --text-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--text-opacity));
+}
+
+.hover\:text-orange-900:hover {
+  --text-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--text-opacity));
+}
+
+.hover\:text-yellow-100:hover {
+  --text-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--text-opacity));
+}
+
+.hover\:text-yellow-200:hover {
+  --text-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--text-opacity));
+}
+
+.hover\:text-yellow-300:hover {
+  --text-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--text-opacity));
+}
+
+.hover\:text-yellow-400:hover {
+  --text-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--text-opacity));
+}
+
+.hover\:text-yellow-500:hover {
+  --text-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--text-opacity));
+}
+
+.hover\:text-yellow-600:hover {
+  --text-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--text-opacity));
+}
+
+.hover\:text-yellow-700:hover {
+  --text-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--text-opacity));
+}
+
+.hover\:text-yellow-800:hover {
+  --text-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--text-opacity));
+}
+
+.hover\:text-yellow-900:hover {
+  --text-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--text-opacity));
+}
+
+.hover\:text-green-100:hover {
+  --text-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--text-opacity));
+}
+
+.hover\:text-green-200:hover {
+  --text-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--text-opacity));
+}
+
+.hover\:text-green-300:hover {
+  --text-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--text-opacity));
+}
+
+.hover\:text-green-400:hover {
+  --text-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--text-opacity));
+}
+
+.hover\:text-green-500:hover {
+  --text-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--text-opacity));
+}
+
+.hover\:text-green-600:hover {
+  --text-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--text-opacity));
+}
+
+.hover\:text-green-700:hover {
+  --text-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--text-opacity));
+}
+
+.hover\:text-green-800:hover {
+  --text-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--text-opacity));
+}
+
+.hover\:text-green-900:hover {
+  --text-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--text-opacity));
+}
+
+.hover\:text-teal-100:hover {
+  --text-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--text-opacity));
+}
+
+.hover\:text-teal-200:hover {
+  --text-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--text-opacity));
+}
+
+.hover\:text-teal-300:hover {
+  --text-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--text-opacity));
+}
+
+.hover\:text-teal-400:hover {
+  --text-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--text-opacity));
+}
+
+.hover\:text-teal-500:hover {
+  --text-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--text-opacity));
+}
+
+.hover\:text-teal-600:hover {
+  --text-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--text-opacity));
+}
+
+.hover\:text-teal-700:hover {
+  --text-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--text-opacity));
+}
+
+.hover\:text-teal-800:hover {
+  --text-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--text-opacity));
+}
+
+.hover\:text-teal-900:hover {
+  --text-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--text-opacity));
+}
+
+.hover\:text-blue-100:hover {
+  --text-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--text-opacity));
+}
+
+.hover\:text-blue-200:hover {
+  --text-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--text-opacity));
+}
+
+.hover\:text-blue-300:hover {
+  --text-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--text-opacity));
+}
+
+.hover\:text-blue-400:hover {
+  --text-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--text-opacity));
+}
+
+.hover\:text-blue-500:hover {
+  --text-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--text-opacity));
+}
+
+.hover\:text-blue-600:hover {
+  --text-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--text-opacity));
+}
+
+.hover\:text-blue-700:hover {
+  --text-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--text-opacity));
+}
+
+.hover\:text-blue-800:hover {
+  --text-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--text-opacity));
+}
+
+.hover\:text-blue-900:hover {
+  --text-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--text-opacity));
+}
+
+.hover\:text-indigo-100:hover {
+  --text-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--text-opacity));
+}
+
+.hover\:text-indigo-200:hover {
+  --text-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--text-opacity));
+}
+
+.hover\:text-indigo-300:hover {
+  --text-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--text-opacity));
+}
+
+.hover\:text-indigo-400:hover {
+  --text-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--text-opacity));
+}
+
+.hover\:text-indigo-500:hover {
+  --text-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--text-opacity));
+}
+
+.hover\:text-indigo-600:hover {
+  --text-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--text-opacity));
+}
+
+.hover\:text-indigo-700:hover {
+  --text-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--text-opacity));
+}
+
+.hover\:text-indigo-800:hover {
+  --text-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--text-opacity));
+}
+
+.hover\:text-indigo-900:hover {
+  --text-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--text-opacity));
+}
+
+.hover\:text-purple-100:hover {
+  --text-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--text-opacity));
+}
+
+.hover\:text-purple-200:hover {
+  --text-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--text-opacity));
+}
+
+.hover\:text-purple-300:hover {
+  --text-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--text-opacity));
+}
+
+.hover\:text-purple-400:hover {
+  --text-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--text-opacity));
+}
+
+.hover\:text-purple-500:hover {
+  --text-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--text-opacity));
+}
+
+.hover\:text-purple-600:hover {
+  --text-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--text-opacity));
+}
+
+.hover\:text-purple-700:hover {
+  --text-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--text-opacity));
+}
+
+.hover\:text-purple-800:hover {
+  --text-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--text-opacity));
+}
+
+.hover\:text-purple-900:hover {
+  --text-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--text-opacity));
+}
+
+.hover\:text-pink-100:hover {
+  --text-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--text-opacity));
+}
+
+.hover\:text-pink-200:hover {
+  --text-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--text-opacity));
+}
+
+.hover\:text-pink-300:hover {
+  --text-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--text-opacity));
+}
+
+.hover\:text-pink-400:hover {
+  --text-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--text-opacity));
+}
+
+.hover\:text-pink-500:hover {
+  --text-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--text-opacity));
+}
+
+.hover\:text-pink-600:hover {
+  --text-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--text-opacity));
+}
+
+.hover\:text-pink-700:hover {
+  --text-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--text-opacity));
+}
+
+.hover\:text-pink-800:hover {
+  --text-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--text-opacity));
+}
+
+.hover\:text-pink-900:hover {
+  --text-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--text-opacity));
+}
+
+.focus\:text-transparent:focus {
+  color: transparent;
+}
+
+.focus\:text-current:focus {
+  color: currentColor;
+}
+
+.focus\:text-black:focus {
+  --text-opacity: 1;
+  color: #000;
+  color: rgba(0, 0, 0, var(--text-opacity));
+}
+
+.focus\:text-white:focus {
+  --text-opacity: 1;
+  color: #fff;
+  color: rgba(255, 255, 255, var(--text-opacity));
+}
+
+.focus\:text-gray-100:focus {
+  --text-opacity: 1;
+  color: #f7fafc;
+  color: rgba(247, 250, 252, var(--text-opacity));
+}
+
+.focus\:text-gray-200:focus {
+  --text-opacity: 1;
+  color: #edf2f7;
+  color: rgba(237, 242, 247, var(--text-opacity));
+}
+
+.focus\:text-gray-300:focus {
+  --text-opacity: 1;
+  color: #e2e8f0;
+  color: rgba(226, 232, 240, var(--text-opacity));
+}
+
+.focus\:text-gray-400:focus {
+  --text-opacity: 1;
+  color: #cbd5e0;
+  color: rgba(203, 213, 224, var(--text-opacity));
+}
+
+.focus\:text-gray-500:focus {
+  --text-opacity: 1;
+  color: #a0aec0;
+  color: rgba(160, 174, 192, var(--text-opacity));
+}
+
+.focus\:text-gray-600:focus {
+  --text-opacity: 1;
+  color: #718096;
+  color: rgba(113, 128, 150, var(--text-opacity));
+}
+
+.focus\:text-gray-700:focus {
+  --text-opacity: 1;
+  color: #4a5568;
+  color: rgba(74, 85, 104, var(--text-opacity));
+}
+
+.focus\:text-gray-800:focus {
+  --text-opacity: 1;
+  color: #2d3748;
+  color: rgba(45, 55, 72, var(--text-opacity));
+}
+
+.focus\:text-gray-900:focus {
+  --text-opacity: 1;
+  color: #1a202c;
+  color: rgba(26, 32, 44, var(--text-opacity));
+}
+
+.focus\:text-red-100:focus {
+  --text-opacity: 1;
+  color: #fff5f5;
+  color: rgba(255, 245, 245, var(--text-opacity));
+}
+
+.focus\:text-red-200:focus {
+  --text-opacity: 1;
+  color: #fed7d7;
+  color: rgba(254, 215, 215, var(--text-opacity));
+}
+
+.focus\:text-red-300:focus {
+  --text-opacity: 1;
+  color: #feb2b2;
+  color: rgba(254, 178, 178, var(--text-opacity));
+}
+
+.focus\:text-red-400:focus {
+  --text-opacity: 1;
+  color: #fc8181;
+  color: rgba(252, 129, 129, var(--text-opacity));
+}
+
+.focus\:text-red-500:focus {
+  --text-opacity: 1;
+  color: #f56565;
+  color: rgba(245, 101, 101, var(--text-opacity));
+}
+
+.focus\:text-red-600:focus {
+  --text-opacity: 1;
+  color: #e53e3e;
+  color: rgba(229, 62, 62, var(--text-opacity));
+}
+
+.focus\:text-red-700:focus {
+  --text-opacity: 1;
+  color: #c53030;
+  color: rgba(197, 48, 48, var(--text-opacity));
+}
+
+.focus\:text-red-800:focus {
+  --text-opacity: 1;
+  color: #9b2c2c;
+  color: rgba(155, 44, 44, var(--text-opacity));
+}
+
+.focus\:text-red-900:focus {
+  --text-opacity: 1;
+  color: #742a2a;
+  color: rgba(116, 42, 42, var(--text-opacity));
+}
+
+.focus\:text-orange-100:focus {
+  --text-opacity: 1;
+  color: #fffaf0;
+  color: rgba(255, 250, 240, var(--text-opacity));
+}
+
+.focus\:text-orange-200:focus {
+  --text-opacity: 1;
+  color: #feebc8;
+  color: rgba(254, 235, 200, var(--text-opacity));
+}
+
+.focus\:text-orange-300:focus {
+  --text-opacity: 1;
+  color: #fbd38d;
+  color: rgba(251, 211, 141, var(--text-opacity));
+}
+
+.focus\:text-orange-400:focus {
+  --text-opacity: 1;
+  color: #f6ad55;
+  color: rgba(246, 173, 85, var(--text-opacity));
+}
+
+.focus\:text-orange-500:focus {
+  --text-opacity: 1;
+  color: #ed8936;
+  color: rgba(237, 137, 54, var(--text-opacity));
+}
+
+.focus\:text-orange-600:focus {
+  --text-opacity: 1;
+  color: #dd6b20;
+  color: rgba(221, 107, 32, var(--text-opacity));
+}
+
+.focus\:text-orange-700:focus {
+  --text-opacity: 1;
+  color: #c05621;
+  color: rgba(192, 86, 33, var(--text-opacity));
+}
+
+.focus\:text-orange-800:focus {
+  --text-opacity: 1;
+  color: #9c4221;
+  color: rgba(156, 66, 33, var(--text-opacity));
+}
+
+.focus\:text-orange-900:focus {
+  --text-opacity: 1;
+  color: #7b341e;
+  color: rgba(123, 52, 30, var(--text-opacity));
+}
+
+.focus\:text-yellow-100:focus {
+  --text-opacity: 1;
+  color: #fffff0;
+  color: rgba(255, 255, 240, var(--text-opacity));
+}
+
+.focus\:text-yellow-200:focus {
+  --text-opacity: 1;
+  color: #fefcbf;
+  color: rgba(254, 252, 191, var(--text-opacity));
+}
+
+.focus\:text-yellow-300:focus {
+  --text-opacity: 1;
+  color: #faf089;
+  color: rgba(250, 240, 137, var(--text-opacity));
+}
+
+.focus\:text-yellow-400:focus {
+  --text-opacity: 1;
+  color: #f6e05e;
+  color: rgba(246, 224, 94, var(--text-opacity));
+}
+
+.focus\:text-yellow-500:focus {
+  --text-opacity: 1;
+  color: #ecc94b;
+  color: rgba(236, 201, 75, var(--text-opacity));
+}
+
+.focus\:text-yellow-600:focus {
+  --text-opacity: 1;
+  color: #d69e2e;
+  color: rgba(214, 158, 46, var(--text-opacity));
+}
+
+.focus\:text-yellow-700:focus {
+  --text-opacity: 1;
+  color: #b7791f;
+  color: rgba(183, 121, 31, var(--text-opacity));
+}
+
+.focus\:text-yellow-800:focus {
+  --text-opacity: 1;
+  color: #975a16;
+  color: rgba(151, 90, 22, var(--text-opacity));
+}
+
+.focus\:text-yellow-900:focus {
+  --text-opacity: 1;
+  color: #744210;
+  color: rgba(116, 66, 16, var(--text-opacity));
+}
+
+.focus\:text-green-100:focus {
+  --text-opacity: 1;
+  color: #f0fff4;
+  color: rgba(240, 255, 244, var(--text-opacity));
+}
+
+.focus\:text-green-200:focus {
+  --text-opacity: 1;
+  color: #c6f6d5;
+  color: rgba(198, 246, 213, var(--text-opacity));
+}
+
+.focus\:text-green-300:focus {
+  --text-opacity: 1;
+  color: #9ae6b4;
+  color: rgba(154, 230, 180, var(--text-opacity));
+}
+
+.focus\:text-green-400:focus {
+  --text-opacity: 1;
+  color: #68d391;
+  color: rgba(104, 211, 145, var(--text-opacity));
+}
+
+.focus\:text-green-500:focus {
+  --text-opacity: 1;
+  color: #48bb78;
+  color: rgba(72, 187, 120, var(--text-opacity));
+}
+
+.focus\:text-green-600:focus {
+  --text-opacity: 1;
+  color: #38a169;
+  color: rgba(56, 161, 105, var(--text-opacity));
+}
+
+.focus\:text-green-700:focus {
+  --text-opacity: 1;
+  color: #2f855a;
+  color: rgba(47, 133, 90, var(--text-opacity));
+}
+
+.focus\:text-green-800:focus {
+  --text-opacity: 1;
+  color: #276749;
+  color: rgba(39, 103, 73, var(--text-opacity));
+}
+
+.focus\:text-green-900:focus {
+  --text-opacity: 1;
+  color: #22543d;
+  color: rgba(34, 84, 61, var(--text-opacity));
+}
+
+.focus\:text-teal-100:focus {
+  --text-opacity: 1;
+  color: #e6fffa;
+  color: rgba(230, 255, 250, var(--text-opacity));
+}
+
+.focus\:text-teal-200:focus {
+  --text-opacity: 1;
+  color: #b2f5ea;
+  color: rgba(178, 245, 234, var(--text-opacity));
+}
+
+.focus\:text-teal-300:focus {
+  --text-opacity: 1;
+  color: #81e6d9;
+  color: rgba(129, 230, 217, var(--text-opacity));
+}
+
+.focus\:text-teal-400:focus {
+  --text-opacity: 1;
+  color: #4fd1c5;
+  color: rgba(79, 209, 197, var(--text-opacity));
+}
+
+.focus\:text-teal-500:focus {
+  --text-opacity: 1;
+  color: #38b2ac;
+  color: rgba(56, 178, 172, var(--text-opacity));
+}
+
+.focus\:text-teal-600:focus {
+  --text-opacity: 1;
+  color: #319795;
+  color: rgba(49, 151, 149, var(--text-opacity));
+}
+
+.focus\:text-teal-700:focus {
+  --text-opacity: 1;
+  color: #2c7a7b;
+  color: rgba(44, 122, 123, var(--text-opacity));
+}
+
+.focus\:text-teal-800:focus {
+  --text-opacity: 1;
+  color: #285e61;
+  color: rgba(40, 94, 97, var(--text-opacity));
+}
+
+.focus\:text-teal-900:focus {
+  --text-opacity: 1;
+  color: #234e52;
+  color: rgba(35, 78, 82, var(--text-opacity));
+}
+
+.focus\:text-blue-100:focus {
+  --text-opacity: 1;
+  color: #ebf8ff;
+  color: rgba(235, 248, 255, var(--text-opacity));
+}
+
+.focus\:text-blue-200:focus {
+  --text-opacity: 1;
+  color: #bee3f8;
+  color: rgba(190, 227, 248, var(--text-opacity));
+}
+
+.focus\:text-blue-300:focus {
+  --text-opacity: 1;
+  color: #90cdf4;
+  color: rgba(144, 205, 244, var(--text-opacity));
+}
+
+.focus\:text-blue-400:focus {
+  --text-opacity: 1;
+  color: #63b3ed;
+  color: rgba(99, 179, 237, var(--text-opacity));
+}
+
+.focus\:text-blue-500:focus {
+  --text-opacity: 1;
+  color: #4299e1;
+  color: rgba(66, 153, 225, var(--text-opacity));
+}
+
+.focus\:text-blue-600:focus {
+  --text-opacity: 1;
+  color: #3182ce;
+  color: rgba(49, 130, 206, var(--text-opacity));
+}
+
+.focus\:text-blue-700:focus {
+  --text-opacity: 1;
+  color: #2b6cb0;
+  color: rgba(43, 108, 176, var(--text-opacity));
+}
+
+.focus\:text-blue-800:focus {
+  --text-opacity: 1;
+  color: #2c5282;
+  color: rgba(44, 82, 130, var(--text-opacity));
+}
+
+.focus\:text-blue-900:focus {
+  --text-opacity: 1;
+  color: #2a4365;
+  color: rgba(42, 67, 101, var(--text-opacity));
+}
+
+.focus\:text-indigo-100:focus {
+  --text-opacity: 1;
+  color: #ebf4ff;
+  color: rgba(235, 244, 255, var(--text-opacity));
+}
+
+.focus\:text-indigo-200:focus {
+  --text-opacity: 1;
+  color: #c3dafe;
+  color: rgba(195, 218, 254, var(--text-opacity));
+}
+
+.focus\:text-indigo-300:focus {
+  --text-opacity: 1;
+  color: #a3bffa;
+  color: rgba(163, 191, 250, var(--text-opacity));
+}
+
+.focus\:text-indigo-400:focus {
+  --text-opacity: 1;
+  color: #7f9cf5;
+  color: rgba(127, 156, 245, var(--text-opacity));
+}
+
+.focus\:text-indigo-500:focus {
+  --text-opacity: 1;
+  color: #667eea;
+  color: rgba(102, 126, 234, var(--text-opacity));
+}
+
+.focus\:text-indigo-600:focus {
+  --text-opacity: 1;
+  color: #5a67d8;
+  color: rgba(90, 103, 216, var(--text-opacity));
+}
+
+.focus\:text-indigo-700:focus {
+  --text-opacity: 1;
+  color: #4c51bf;
+  color: rgba(76, 81, 191, var(--text-opacity));
+}
+
+.focus\:text-indigo-800:focus {
+  --text-opacity: 1;
+  color: #434190;
+  color: rgba(67, 65, 144, var(--text-opacity));
+}
+
+.focus\:text-indigo-900:focus {
+  --text-opacity: 1;
+  color: #3c366b;
+  color: rgba(60, 54, 107, var(--text-opacity));
+}
+
+.focus\:text-purple-100:focus {
+  --text-opacity: 1;
+  color: #faf5ff;
+  color: rgba(250, 245, 255, var(--text-opacity));
+}
+
+.focus\:text-purple-200:focus {
+  --text-opacity: 1;
+  color: #e9d8fd;
+  color: rgba(233, 216, 253, var(--text-opacity));
+}
+
+.focus\:text-purple-300:focus {
+  --text-opacity: 1;
+  color: #d6bcfa;
+  color: rgba(214, 188, 250, var(--text-opacity));
+}
+
+.focus\:text-purple-400:focus {
+  --text-opacity: 1;
+  color: #b794f4;
+  color: rgba(183, 148, 244, var(--text-opacity));
+}
+
+.focus\:text-purple-500:focus {
+  --text-opacity: 1;
+  color: #9f7aea;
+  color: rgba(159, 122, 234, var(--text-opacity));
+}
+
+.focus\:text-purple-600:focus {
+  --text-opacity: 1;
+  color: #805ad5;
+  color: rgba(128, 90, 213, var(--text-opacity));
+}
+
+.focus\:text-purple-700:focus {
+  --text-opacity: 1;
+  color: #6b46c1;
+  color: rgba(107, 70, 193, var(--text-opacity));
+}
+
+.focus\:text-purple-800:focus {
+  --text-opacity: 1;
+  color: #553c9a;
+  color: rgba(85, 60, 154, var(--text-opacity));
+}
+
+.focus\:text-purple-900:focus {
+  --text-opacity: 1;
+  color: #44337a;
+  color: rgba(68, 51, 122, var(--text-opacity));
+}
+
+.focus\:text-pink-100:focus {
+  --text-opacity: 1;
+  color: #fff5f7;
+  color: rgba(255, 245, 247, var(--text-opacity));
+}
+
+.focus\:text-pink-200:focus {
+  --text-opacity: 1;
+  color: #fed7e2;
+  color: rgba(254, 215, 226, var(--text-opacity));
+}
+
+.focus\:text-pink-300:focus {
+  --text-opacity: 1;
+  color: #fbb6ce;
+  color: rgba(251, 182, 206, var(--text-opacity));
+}
+
+.focus\:text-pink-400:focus {
+  --text-opacity: 1;
+  color: #f687b3;
+  color: rgba(246, 135, 179, var(--text-opacity));
+}
+
+.focus\:text-pink-500:focus {
+  --text-opacity: 1;
+  color: #ed64a6;
+  color: rgba(237, 100, 166, var(--text-opacity));
+}
+
+.focus\:text-pink-600:focus {
+  --text-opacity: 1;
+  color: #d53f8c;
+  color: rgba(213, 63, 140, var(--text-opacity));
+}
+
+.focus\:text-pink-700:focus {
+  --text-opacity: 1;
+  color: #b83280;
+  color: rgba(184, 50, 128, var(--text-opacity));
+}
+
+.focus\:text-pink-800:focus {
+  --text-opacity: 1;
+  color: #97266d;
+  color: rgba(151, 38, 109, var(--text-opacity));
+}
+
+.focus\:text-pink-900:focus {
+  --text-opacity: 1;
+  color: #702459;
+  color: rgba(112, 36, 89, var(--text-opacity));
+}
+
+.text-opacity-0 {
+  --text-opacity: 0;
+}
+
+.text-opacity-25 {
+  --text-opacity: 0.25;
+}
+
+.text-opacity-50 {
+  --text-opacity: 0.5;
+}
+
+.text-opacity-75 {
+  --text-opacity: 0.75;
+}
+
+.text-opacity-100 {
+  --text-opacity: 1;
+}
+
+.hover\:text-opacity-0:hover {
+  --text-opacity: 0;
+}
+
+.hover\:text-opacity-25:hover {
+  --text-opacity: 0.25;
+}
+
+.hover\:text-opacity-50:hover {
+  --text-opacity: 0.5;
+}
+
+.hover\:text-opacity-75:hover {
+  --text-opacity: 0.75;
+}
+
+.hover\:text-opacity-100:hover {
+  --text-opacity: 1;
+}
+
+.focus\:text-opacity-0:focus {
+  --text-opacity: 0;
+}
+
+.focus\:text-opacity-25:focus {
+  --text-opacity: 0.25;
+}
+
+.focus\:text-opacity-50:focus {
+  --text-opacity: 0.5;
+}
+
+.focus\:text-opacity-75:focus {
+  --text-opacity: 0.75;
+}
+
+.focus\:text-opacity-100:focus {
+  --text-opacity: 1;
+}
+
+.italic {
+  font-style: italic;
+}
+
+.not-italic {
+  font-style: normal;
+}
+
+.uppercase {
+  text-transform: uppercase;
+}
+
+.lowercase {
+  text-transform: lowercase;
+}
+
+.capitalize {
+  text-transform: capitalize;
+}
+
+.normal-case {
+  text-transform: none;
+}
+
+.underline {
+  text-decoration: underline;
+}
+
+.line-through {
+  text-decoration: line-through;
+}
+
+.no-underline {
+  text-decoration: none;
+}
+
+.hover\:underline:hover {
+  text-decoration: underline;
+}
+
+.hover\:line-through:hover {
+  text-decoration: line-through;
+}
+
+.hover\:no-underline:hover {
+  text-decoration: none;
+}
+
+.focus\:underline:focus {
+  text-decoration: underline;
+}
+
+.focus\:line-through:focus {
+  text-decoration: line-through;
+}
+
+.focus\:no-underline:focus {
+  text-decoration: none;
+}
+
+.antialiased {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.subpixel-antialiased {
+  -webkit-font-smoothing: auto;
+  -moz-osx-font-smoothing: auto;
+}
+
+.ordinal, .slashed-zero, .lining-nums, .oldstyle-nums, .proportional-nums, .tabular-nums, .diagonal-fractions, .stacked-fractions {
+  --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+  --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+  font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+}
+
+.normal-nums {
+  font-variant-numeric: normal;
+}
+
+.ordinal {
+  --font-variant-numeric-ordinal: ordinal;
+}
+
+.slashed-zero {
+  --font-variant-numeric-slashed-zero: slashed-zero;
+}
+
+.lining-nums {
+  --font-variant-numeric-figure: lining-nums;
+}
+
+.oldstyle-nums {
+  --font-variant-numeric-figure: oldstyle-nums;
+}
+
+.proportional-nums {
+  --font-variant-numeric-spacing: proportional-nums;
+}
+
+.tabular-nums {
+  --font-variant-numeric-spacing: tabular-nums;
+}
+
+.diagonal-fractions {
+  --font-variant-numeric-fraction: diagonal-fractions;
+}
+
+.stacked-fractions {
+  --font-variant-numeric-fraction: stacked-fractions;
+}
+
+.tracking-tighter {
+  letter-spacing: -0.05em;
+}
+
+.tracking-tight {
+  letter-spacing: -0.025em;
+}
+
+.tracking-normal {
+  letter-spacing: 0;
+}
+
+.tracking-wide {
+  letter-spacing: 0.025em;
+}
+
+.tracking-wider {
+  letter-spacing: 0.05em;
+}
+
+.tracking-widest {
+  letter-spacing: 0.1em;
+}
+
+.select-none {
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+}
+
+.select-text {
+  -webkit-user-select: text;
+     -moz-user-select: text;
+      -ms-user-select: text;
+          user-select: text;
+}
+
+.select-all {
+  -webkit-user-select: all;
+     -moz-user-select: all;
+      -ms-user-select: all;
+          user-select: all;
+}
+
+.select-auto {
+  -webkit-user-select: auto;
+     -moz-user-select: auto;
+      -ms-user-select: auto;
+          user-select: auto;
+}
+
+.align-baseline {
+  vertical-align: baseline;
+}
+
+.align-top {
+  vertical-align: top;
+}
+
+.align-middle {
+  vertical-align: middle;
+}
+
+.align-bottom {
+  vertical-align: bottom;
+}
+
+.align-text-top {
+  vertical-align: text-top;
+}
+
+.align-text-bottom {
+  vertical-align: text-bottom;
+}
+
+.visible {
+  visibility: visible;
+}
+
+.invisible {
+  visibility: hidden;
+}
+
+.whitespace-normal {
+  white-space: normal;
+}
+
+.whitespace-no-wrap {
+  white-space: nowrap;
+}
+
+.whitespace-pre {
+  white-space: pre;
+}
+
+.whitespace-pre-line {
+  white-space: pre-line;
+}
+
+.whitespace-pre-wrap {
+  white-space: pre-wrap;
+}
+
+.break-normal {
+  overflow-wrap: normal;
+  word-break: normal;
+}
+
+.break-words {
+  overflow-wrap: break-word;
+}
+
+.break-all {
+  word-break: break-all;
+}
+
+.truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.w-0 {
+  width: 0;
+}
+
+.w-1 {
+  width: 0.25rem;
+}
+
+.w-2 {
+  width: 0.5rem;
+}
+
+.w-3 {
+  width: 0.75rem;
+}
+
+.w-4 {
+  width: 1rem;
+}
+
+.w-5 {
+  width: 1.25rem;
+}
+
+.w-6 {
+  width: 1.5rem;
+}
+
+.w-8 {
+  width: 2rem;
+}
+
+.w-10 {
+  width: 2.5rem;
+}
+
+.w-12 {
+  width: 3rem;
+}
+
+.w-16 {
+  width: 4rem;
+}
+
+.w-20 {
+  width: 5rem;
+}
+
+.w-24 {
+  width: 6rem;
+}
+
+.w-32 {
+  width: 8rem;
+}
+
+.w-40 {
+  width: 10rem;
+}
+
+.w-48 {
+  width: 12rem;
+}
+
+.w-56 {
+  width: 14rem;
+}
+
+.w-64 {
+  width: 16rem;
+}
+
+.w-auto {
+  width: auto;
+}
+
+.w-px {
+  width: 1px;
+}
+
+.w-1\/2 {
+  width: 50%;
+}
+
+.w-1\/3 {
+  width: 33.333333%;
+}
+
+.w-2\/3 {
+  width: 66.666667%;
+}
+
+.w-1\/4 {
+  width: 25%;
+}
+
+.w-2\/4 {
+  width: 50%;
+}
+
+.w-3\/4 {
+  width: 75%;
+}
+
+.w-1\/5 {
+  width: 20%;
+}
+
+.w-2\/5 {
+  width: 40%;
+}
+
+.w-3\/5 {
+  width: 60%;
+}
+
+.w-4\/5 {
+  width: 80%;
+}
+
+.w-1\/6 {
+  width: 16.666667%;
+}
+
+.w-2\/6 {
+  width: 33.333333%;
+}
+
+.w-3\/6 {
+  width: 50%;
+}
+
+.w-4\/6 {
+  width: 66.666667%;
+}
+
+.w-5\/6 {
+  width: 83.333333%;
+}
+
+.w-1\/12 {
+  width: 8.333333%;
+}
+
+.w-2\/12 {
+  width: 16.666667%;
+}
+
+.w-3\/12 {
+  width: 25%;
+}
+
+.w-4\/12 {
+  width: 33.333333%;
+}
+
+.w-5\/12 {
+  width: 41.666667%;
+}
+
+.w-6\/12 {
+  width: 50%;
+}
+
+.w-7\/12 {
+  width: 58.333333%;
+}
+
+.w-8\/12 {
+  width: 66.666667%;
+}
+
+.w-9\/12 {
+  width: 75%;
+}
+
+.w-10\/12 {
+  width: 83.333333%;
+}
+
+.w-11\/12 {
+  width: 91.666667%;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.w-screen {
+  width: 100vw;
+}
+
+.z-0 {
+  z-index: 0;
+}
+
+.z-10 {
+  z-index: 10;
+}
+
+.z-20 {
+  z-index: 20;
+}
+
+.z-30 {
+  z-index: 30;
+}
+
+.z-40 {
+  z-index: 40;
+}
+
+.z-50 {
+  z-index: 50;
+}
+
+.z-auto {
+  z-index: auto;
+}
+
+.gap-0 {
+  grid-gap: 0;
+  gap: 0;
+}
+
+.gap-1 {
+  grid-gap: 0.25rem;
+  gap: 0.25rem;
+}
+
+.gap-2 {
+  grid-gap: 0.5rem;
+  gap: 0.5rem;
+}
+
+.gap-3 {
+  grid-gap: 0.75rem;
+  gap: 0.75rem;
+}
+
+.gap-4 {
+  grid-gap: 1rem;
+  gap: 1rem;
+}
+
+.gap-5 {
+  grid-gap: 1.25rem;
+  gap: 1.25rem;
+}
+
+.gap-6 {
+  grid-gap: 1.5rem;
+  gap: 1.5rem;
+}
+
+.gap-8 {
+  grid-gap: 2rem;
+  gap: 2rem;
+}
+
+.gap-10 {
+  grid-gap: 2.5rem;
+  gap: 2.5rem;
+}
+
+.gap-12 {
+  grid-gap: 3rem;
+  gap: 3rem;
+}
+
+.gap-16 {
+  grid-gap: 4rem;
+  gap: 4rem;
+}
+
+.gap-20 {
+  grid-gap: 5rem;
+  gap: 5rem;
+}
+
+.gap-24 {
+  grid-gap: 6rem;
+  gap: 6rem;
+}
+
+.gap-32 {
+  grid-gap: 8rem;
+  gap: 8rem;
+}
+
+.gap-40 {
+  grid-gap: 10rem;
+  gap: 10rem;
+}
+
+.gap-48 {
+  grid-gap: 12rem;
+  gap: 12rem;
+}
+
+.gap-56 {
+  grid-gap: 14rem;
+  gap: 14rem;
+}
+
+.gap-64 {
+  grid-gap: 16rem;
+  gap: 16rem;
+}
+
+.gap-px {
+  grid-gap: 1px;
+  gap: 1px;
+}
+
+.col-gap-0 {
+  grid-column-gap: 0;
+  -moz-column-gap: 0;
+       column-gap: 0;
+}
+
+.col-gap-1 {
+  grid-column-gap: 0.25rem;
+  -moz-column-gap: 0.25rem;
+       column-gap: 0.25rem;
+}
+
+.col-gap-2 {
+  grid-column-gap: 0.5rem;
+  -moz-column-gap: 0.5rem;
+       column-gap: 0.5rem;
+}
+
+.col-gap-3 {
+  grid-column-gap: 0.75rem;
+  -moz-column-gap: 0.75rem;
+       column-gap: 0.75rem;
+}
+
+.col-gap-4 {
+  grid-column-gap: 1rem;
+  -moz-column-gap: 1rem;
+       column-gap: 1rem;
+}
+
+.col-gap-5 {
+  grid-column-gap: 1.25rem;
+  -moz-column-gap: 1.25rem;
+       column-gap: 1.25rem;
+}
+
+.col-gap-6 {
+  grid-column-gap: 1.5rem;
+  -moz-column-gap: 1.5rem;
+       column-gap: 1.5rem;
+}
+
+.col-gap-8 {
+  grid-column-gap: 2rem;
+  -moz-column-gap: 2rem;
+       column-gap: 2rem;
+}
+
+.col-gap-10 {
+  grid-column-gap: 2.5rem;
+  -moz-column-gap: 2.5rem;
+       column-gap: 2.5rem;
+}
+
+.col-gap-12 {
+  grid-column-gap: 3rem;
+  -moz-column-gap: 3rem;
+       column-gap: 3rem;
+}
+
+.col-gap-16 {
+  grid-column-gap: 4rem;
+  -moz-column-gap: 4rem;
+       column-gap: 4rem;
+}
+
+.col-gap-20 {
+  grid-column-gap: 5rem;
+  -moz-column-gap: 5rem;
+       column-gap: 5rem;
+}
+
+.col-gap-24 {
+  grid-column-gap: 6rem;
+  -moz-column-gap: 6rem;
+       column-gap: 6rem;
+}
+
+.col-gap-32 {
+  grid-column-gap: 8rem;
+  -moz-column-gap: 8rem;
+       column-gap: 8rem;
+}
+
+.col-gap-40 {
+  grid-column-gap: 10rem;
+  -moz-column-gap: 10rem;
+       column-gap: 10rem;
+}
+
+.col-gap-48 {
+  grid-column-gap: 12rem;
+  -moz-column-gap: 12rem;
+       column-gap: 12rem;
+}
+
+.col-gap-56 {
+  grid-column-gap: 14rem;
+  -moz-column-gap: 14rem;
+       column-gap: 14rem;
+}
+
+.col-gap-64 {
+  grid-column-gap: 16rem;
+  -moz-column-gap: 16rem;
+       column-gap: 16rem;
+}
+
+.col-gap-px {
+  grid-column-gap: 1px;
+  -moz-column-gap: 1px;
+       column-gap: 1px;
+}
+
+.gap-x-0 {
+  grid-column-gap: 0;
+  -moz-column-gap: 0;
+       column-gap: 0;
+}
+
+.gap-x-1 {
+  grid-column-gap: 0.25rem;
+  -moz-column-gap: 0.25rem;
+       column-gap: 0.25rem;
+}
+
+.gap-x-2 {
+  grid-column-gap: 0.5rem;
+  -moz-column-gap: 0.5rem;
+       column-gap: 0.5rem;
+}
+
+.gap-x-3 {
+  grid-column-gap: 0.75rem;
+  -moz-column-gap: 0.75rem;
+       column-gap: 0.75rem;
+}
+
+.gap-x-4 {
+  grid-column-gap: 1rem;
+  -moz-column-gap: 1rem;
+       column-gap: 1rem;
+}
+
+.gap-x-5 {
+  grid-column-gap: 1.25rem;
+  -moz-column-gap: 1.25rem;
+       column-gap: 1.25rem;
+}
+
+.gap-x-6 {
+  grid-column-gap: 1.5rem;
+  -moz-column-gap: 1.5rem;
+       column-gap: 1.5rem;
+}
+
+.gap-x-8 {
+  grid-column-gap: 2rem;
+  -moz-column-gap: 2rem;
+       column-gap: 2rem;
+}
+
+.gap-x-10 {
+  grid-column-gap: 2.5rem;
+  -moz-column-gap: 2.5rem;
+       column-gap: 2.5rem;
+}
+
+.gap-x-12 {
+  grid-column-gap: 3rem;
+  -moz-column-gap: 3rem;
+       column-gap: 3rem;
+}
+
+.gap-x-16 {
+  grid-column-gap: 4rem;
+  -moz-column-gap: 4rem;
+       column-gap: 4rem;
+}
+
+.gap-x-20 {
+  grid-column-gap: 5rem;
+  -moz-column-gap: 5rem;
+       column-gap: 5rem;
+}
+
+.gap-x-24 {
+  grid-column-gap: 6rem;
+  -moz-column-gap: 6rem;
+       column-gap: 6rem;
+}
+
+.gap-x-32 {
+  grid-column-gap: 8rem;
+  -moz-column-gap: 8rem;
+       column-gap: 8rem;
+}
+
+.gap-x-40 {
+  grid-column-gap: 10rem;
+  -moz-column-gap: 10rem;
+       column-gap: 10rem;
+}
+
+.gap-x-48 {
+  grid-column-gap: 12rem;
+  -moz-column-gap: 12rem;
+       column-gap: 12rem;
+}
+
+.gap-x-56 {
+  grid-column-gap: 14rem;
+  -moz-column-gap: 14rem;
+       column-gap: 14rem;
+}
+
+.gap-x-64 {
+  grid-column-gap: 16rem;
+  -moz-column-gap: 16rem;
+       column-gap: 16rem;
+}
+
+.gap-x-px {
+  grid-column-gap: 1px;
+  -moz-column-gap: 1px;
+       column-gap: 1px;
+}
+
+.row-gap-0 {
+  grid-row-gap: 0;
+  row-gap: 0;
+}
+
+.row-gap-1 {
+  grid-row-gap: 0.25rem;
+  row-gap: 0.25rem;
+}
+
+.row-gap-2 {
+  grid-row-gap: 0.5rem;
+  row-gap: 0.5rem;
+}
+
+.row-gap-3 {
+  grid-row-gap: 0.75rem;
+  row-gap: 0.75rem;
+}
+
+.row-gap-4 {
+  grid-row-gap: 1rem;
+  row-gap: 1rem;
+}
+
+.row-gap-5 {
+  grid-row-gap: 1.25rem;
+  row-gap: 1.25rem;
+}
+
+.row-gap-6 {
+  grid-row-gap: 1.5rem;
+  row-gap: 1.5rem;
+}
+
+.row-gap-8 {
+  grid-row-gap: 2rem;
+  row-gap: 2rem;
+}
+
+.row-gap-10 {
+  grid-row-gap: 2.5rem;
+  row-gap: 2.5rem;
+}
+
+.row-gap-12 {
+  grid-row-gap: 3rem;
+  row-gap: 3rem;
+}
+
+.row-gap-16 {
+  grid-row-gap: 4rem;
+  row-gap: 4rem;
+}
+
+.row-gap-20 {
+  grid-row-gap: 5rem;
+  row-gap: 5rem;
+}
+
+.row-gap-24 {
+  grid-row-gap: 6rem;
+  row-gap: 6rem;
+}
+
+.row-gap-32 {
+  grid-row-gap: 8rem;
+  row-gap: 8rem;
+}
+
+.row-gap-40 {
+  grid-row-gap: 10rem;
+  row-gap: 10rem;
+}
+
+.row-gap-48 {
+  grid-row-gap: 12rem;
+  row-gap: 12rem;
+}
+
+.row-gap-56 {
+  grid-row-gap: 14rem;
+  row-gap: 14rem;
+}
+
+.row-gap-64 {
+  grid-row-gap: 16rem;
+  row-gap: 16rem;
+}
+
+.row-gap-px {
+  grid-row-gap: 1px;
+  row-gap: 1px;
+}
+
+.gap-y-0 {
+  grid-row-gap: 0;
+  row-gap: 0;
+}
+
+.gap-y-1 {
+  grid-row-gap: 0.25rem;
+  row-gap: 0.25rem;
+}
+
+.gap-y-2 {
+  grid-row-gap: 0.5rem;
+  row-gap: 0.5rem;
+}
+
+.gap-y-3 {
+  grid-row-gap: 0.75rem;
+  row-gap: 0.75rem;
+}
+
+.gap-y-4 {
+  grid-row-gap: 1rem;
+  row-gap: 1rem;
+}
+
+.gap-y-5 {
+  grid-row-gap: 1.25rem;
+  row-gap: 1.25rem;
+}
+
+.gap-y-6 {
+  grid-row-gap: 1.5rem;
+  row-gap: 1.5rem;
+}
+
+.gap-y-8 {
+  grid-row-gap: 2rem;
+  row-gap: 2rem;
+}
+
+.gap-y-10 {
+  grid-row-gap: 2.5rem;
+  row-gap: 2.5rem;
+}
+
+.gap-y-12 {
+  grid-row-gap: 3rem;
+  row-gap: 3rem;
+}
+
+.gap-y-16 {
+  grid-row-gap: 4rem;
+  row-gap: 4rem;
+}
+
+.gap-y-20 {
+  grid-row-gap: 5rem;
+  row-gap: 5rem;
+}
+
+.gap-y-24 {
+  grid-row-gap: 6rem;
+  row-gap: 6rem;
+}
+
+.gap-y-32 {
+  grid-row-gap: 8rem;
+  row-gap: 8rem;
+}
+
+.gap-y-40 {
+  grid-row-gap: 10rem;
+  row-gap: 10rem;
+}
+
+.gap-y-48 {
+  grid-row-gap: 12rem;
+  row-gap: 12rem;
+}
+
+.gap-y-56 {
+  grid-row-gap: 14rem;
+  row-gap: 14rem;
+}
+
+.gap-y-64 {
+  grid-row-gap: 16rem;
+  row-gap: 16rem;
+}
+
+.gap-y-px {
+  grid-row-gap: 1px;
+  row-gap: 1px;
+}
+
+.grid-flow-row {
+  grid-auto-flow: row;
+}
+
+.grid-flow-col {
+  grid-auto-flow: column;
+}
+
+.grid-flow-row-dense {
+  grid-auto-flow: row dense;
+}
+
+.grid-flow-col-dense {
+  grid-auto-flow: column dense;
+}
+
+.grid-cols-1 {
+  grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+.grid-cols-2 {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.grid-cols-3 {
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.grid-cols-4 {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.grid-cols-5 {
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+}
+
+.grid-cols-6 {
+  grid-template-columns: repeat(6, minmax(0, 1fr));
+}
+
+.grid-cols-7 {
+  grid-template-columns: repeat(7, minmax(0, 1fr));
+}
+
+.grid-cols-8 {
+  grid-template-columns: repeat(8, minmax(0, 1fr));
+}
+
+.grid-cols-9 {
+  grid-template-columns: repeat(9, minmax(0, 1fr));
+}
+
+.grid-cols-10 {
+  grid-template-columns: repeat(10, minmax(0, 1fr));
+}
+
+.grid-cols-11 {
+  grid-template-columns: repeat(11, minmax(0, 1fr));
+}
+
+.grid-cols-12 {
+  grid-template-columns: repeat(12, minmax(0, 1fr));
+}
+
+.grid-cols-none {
+  grid-template-columns: none;
+}
+
+.col-auto {
+  grid-column: auto;
+}
+
+.col-span-1 {
+  grid-column: span 1 / span 1;
+}
+
+.col-span-2 {
+  grid-column: span 2 / span 2;
+}
+
+.col-span-3 {
+  grid-column: span 3 / span 3;
+}
+
+.col-span-4 {
+  grid-column: span 4 / span 4;
+}
+
+.col-span-5 {
+  grid-column: span 5 / span 5;
+}
+
+.col-span-6 {
+  grid-column: span 6 / span 6;
+}
+
+.col-span-7 {
+  grid-column: span 7 / span 7;
+}
+
+.col-span-8 {
+  grid-column: span 8 / span 8;
+}
+
+.col-span-9 {
+  grid-column: span 9 / span 9;
+}
+
+.col-span-10 {
+  grid-column: span 10 / span 10;
+}
+
+.col-span-11 {
+  grid-column: span 11 / span 11;
+}
+
+.col-span-12 {
+  grid-column: span 12 / span 12;
+}
+
+.col-start-1 {
+  grid-column-start: 1;
+}
+
+.col-start-2 {
+  grid-column-start: 2;
+}
+
+.col-start-3 {
+  grid-column-start: 3;
+}
+
+.col-start-4 {
+  grid-column-start: 4;
+}
+
+.col-start-5 {
+  grid-column-start: 5;
+}
+
+.col-start-6 {
+  grid-column-start: 6;
+}
+
+.col-start-7 {
+  grid-column-start: 7;
+}
+
+.col-start-8 {
+  grid-column-start: 8;
+}
+
+.col-start-9 {
+  grid-column-start: 9;
+}
+
+.col-start-10 {
+  grid-column-start: 10;
+}
+
+.col-start-11 {
+  grid-column-start: 11;
+}
+
+.col-start-12 {
+  grid-column-start: 12;
+}
+
+.col-start-13 {
+  grid-column-start: 13;
+}
+
+.col-start-auto {
+  grid-column-start: auto;
+}
+
+.col-end-1 {
+  grid-column-end: 1;
+}
+
+.col-end-2 {
+  grid-column-end: 2;
+}
+
+.col-end-3 {
+  grid-column-end: 3;
+}
+
+.col-end-4 {
+  grid-column-end: 4;
+}
+
+.col-end-5 {
+  grid-column-end: 5;
+}
+
+.col-end-6 {
+  grid-column-end: 6;
+}
+
+.col-end-7 {
+  grid-column-end: 7;
+}
+
+.col-end-8 {
+  grid-column-end: 8;
+}
+
+.col-end-9 {
+  grid-column-end: 9;
+}
+
+.col-end-10 {
+  grid-column-end: 10;
+}
+
+.col-end-11 {
+  grid-column-end: 11;
+}
+
+.col-end-12 {
+  grid-column-end: 12;
+}
+
+.col-end-13 {
+  grid-column-end: 13;
+}
+
+.col-end-auto {
+  grid-column-end: auto;
+}
+
+.grid-rows-1 {
+  grid-template-rows: repeat(1, minmax(0, 1fr));
+}
+
+.grid-rows-2 {
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+}
+
+.grid-rows-3 {
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+
+.grid-rows-4 {
+  grid-template-rows: repeat(4, minmax(0, 1fr));
+}
+
+.grid-rows-5 {
+  grid-template-rows: repeat(5, minmax(0, 1fr));
+}
+
+.grid-rows-6 {
+  grid-template-rows: repeat(6, minmax(0, 1fr));
+}
+
+.grid-rows-none {
+  grid-template-rows: none;
+}
+
+.row-auto {
+  grid-row: auto;
+}
+
+.row-span-1 {
+  grid-row: span 1 / span 1;
+}
+
+.row-span-2 {
+  grid-row: span 2 / span 2;
+}
+
+.row-span-3 {
+  grid-row: span 3 / span 3;
+}
+
+.row-span-4 {
+  grid-row: span 4 / span 4;
+}
+
+.row-span-5 {
+  grid-row: span 5 / span 5;
+}
+
+.row-span-6 {
+  grid-row: span 6 / span 6;
+}
+
+.row-start-1 {
+  grid-row-start: 1;
+}
+
+.row-start-2 {
+  grid-row-start: 2;
+}
+
+.row-start-3 {
+  grid-row-start: 3;
+}
+
+.row-start-4 {
+  grid-row-start: 4;
+}
+
+.row-start-5 {
+  grid-row-start: 5;
+}
+
+.row-start-6 {
+  grid-row-start: 6;
+}
+
+.row-start-7 {
+  grid-row-start: 7;
+}
+
+.row-start-auto {
+  grid-row-start: auto;
+}
+
+.row-end-1 {
+  grid-row-end: 1;
+}
+
+.row-end-2 {
+  grid-row-end: 2;
+}
+
+.row-end-3 {
+  grid-row-end: 3;
+}
+
+.row-end-4 {
+  grid-row-end: 4;
+}
+
+.row-end-5 {
+  grid-row-end: 5;
+}
+
+.row-end-6 {
+  grid-row-end: 6;
+}
+
+.row-end-7 {
+  grid-row-end: 7;
+}
+
+.row-end-auto {
+  grid-row-end: auto;
+}
+
+.transform {
+  --transform-translate-x: 0;
+  --transform-translate-y: 0;
+  --transform-rotate: 0;
+  --transform-skew-x: 0;
+  --transform-skew-y: 0;
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+  transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+}
+
+.transform-none {
+  transform: none;
+}
+
+.origin-center {
+  transform-origin: center;
+}
+
+.origin-top {
+  transform-origin: top;
+}
+
+.origin-top-right {
+  transform-origin: top right;
+}
+
+.origin-right {
+  transform-origin: right;
+}
+
+.origin-bottom-right {
+  transform-origin: bottom right;
+}
+
+.origin-bottom {
+  transform-origin: bottom;
+}
+
+.origin-bottom-left {
+  transform-origin: bottom left;
+}
+
+.origin-left {
+  transform-origin: left;
+}
+
+.origin-top-left {
+  transform-origin: top left;
+}
+
+.scale-0 {
+  --transform-scale-x: 0;
+  --transform-scale-y: 0;
+}
+
+.scale-50 {
+  --transform-scale-x: .5;
+  --transform-scale-y: .5;
+}
+
+.scale-75 {
+  --transform-scale-x: .75;
+  --transform-scale-y: .75;
+}
+
+.scale-90 {
+  --transform-scale-x: .9;
+  --transform-scale-y: .9;
+}
+
+.scale-95 {
+  --transform-scale-x: .95;
+  --transform-scale-y: .95;
+}
+
+.scale-100 {
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+}
+
+.scale-105 {
+  --transform-scale-x: 1.05;
+  --transform-scale-y: 1.05;
+}
+
+.scale-110 {
+  --transform-scale-x: 1.1;
+  --transform-scale-y: 1.1;
+}
+
+.scale-125 {
+  --transform-scale-x: 1.25;
+  --transform-scale-y: 1.25;
+}
+
+.scale-150 {
+  --transform-scale-x: 1.5;
+  --transform-scale-y: 1.5;
+}
+
+.scale-x-0 {
+  --transform-scale-x: 0;
+}
+
+.scale-x-50 {
+  --transform-scale-x: .5;
+}
+
+.scale-x-75 {
+  --transform-scale-x: .75;
+}
+
+.scale-x-90 {
+  --transform-scale-x: .9;
+}
+
+.scale-x-95 {
+  --transform-scale-x: .95;
+}
+
+.scale-x-100 {
+  --transform-scale-x: 1;
+}
+
+.scale-x-105 {
+  --transform-scale-x: 1.05;
+}
+
+.scale-x-110 {
+  --transform-scale-x: 1.1;
+}
+
+.scale-x-125 {
+  --transform-scale-x: 1.25;
+}
+
+.scale-x-150 {
+  --transform-scale-x: 1.5;
+}
+
+.scale-y-0 {
+  --transform-scale-y: 0;
+}
+
+.scale-y-50 {
+  --transform-scale-y: .5;
+}
+
+.scale-y-75 {
+  --transform-scale-y: .75;
+}
+
+.scale-y-90 {
+  --transform-scale-y: .9;
+}
+
+.scale-y-95 {
+  --transform-scale-y: .95;
+}
+
+.scale-y-100 {
+  --transform-scale-y: 1;
+}
+
+.scale-y-105 {
+  --transform-scale-y: 1.05;
+}
+
+.scale-y-110 {
+  --transform-scale-y: 1.1;
+}
+
+.scale-y-125 {
+  --transform-scale-y: 1.25;
+}
+
+.scale-y-150 {
+  --transform-scale-y: 1.5;
+}
+
+.hover\:scale-0:hover {
+  --transform-scale-x: 0;
+  --transform-scale-y: 0;
+}
+
+.hover\:scale-50:hover {
+  --transform-scale-x: .5;
+  --transform-scale-y: .5;
+}
+
+.hover\:scale-75:hover {
+  --transform-scale-x: .75;
+  --transform-scale-y: .75;
+}
+
+.hover\:scale-90:hover {
+  --transform-scale-x: .9;
+  --transform-scale-y: .9;
+}
+
+.hover\:scale-95:hover {
+  --transform-scale-x: .95;
+  --transform-scale-y: .95;
+}
+
+.hover\:scale-100:hover {
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+}
+
+.hover\:scale-105:hover {
+  --transform-scale-x: 1.05;
+  --transform-scale-y: 1.05;
+}
+
+.hover\:scale-110:hover {
+  --transform-scale-x: 1.1;
+  --transform-scale-y: 1.1;
+}
+
+.hover\:scale-125:hover {
+  --transform-scale-x: 1.25;
+  --transform-scale-y: 1.25;
+}
+
+.hover\:scale-150:hover {
+  --transform-scale-x: 1.5;
+  --transform-scale-y: 1.5;
+}
+
+.hover\:scale-x-0:hover {
+  --transform-scale-x: 0;
+}
+
+.hover\:scale-x-50:hover {
+  --transform-scale-x: .5;
+}
+
+.hover\:scale-x-75:hover {
+  --transform-scale-x: .75;
+}
+
+.hover\:scale-x-90:hover {
+  --transform-scale-x: .9;
+}
+
+.hover\:scale-x-95:hover {
+  --transform-scale-x: .95;
+}
+
+.hover\:scale-x-100:hover {
+  --transform-scale-x: 1;
+}
+
+.hover\:scale-x-105:hover {
+  --transform-scale-x: 1.05;
+}
+
+.hover\:scale-x-110:hover {
+  --transform-scale-x: 1.1;
+}
+
+.hover\:scale-x-125:hover {
+  --transform-scale-x: 1.25;
+}
+
+.hover\:scale-x-150:hover {
+  --transform-scale-x: 1.5;
+}
+
+.hover\:scale-y-0:hover {
+  --transform-scale-y: 0;
+}
+
+.hover\:scale-y-50:hover {
+  --transform-scale-y: .5;
+}
+
+.hover\:scale-y-75:hover {
+  --transform-scale-y: .75;
+}
+
+.hover\:scale-y-90:hover {
+  --transform-scale-y: .9;
+}
+
+.hover\:scale-y-95:hover {
+  --transform-scale-y: .95;
+}
+
+.hover\:scale-y-100:hover {
+  --transform-scale-y: 1;
+}
+
+.hover\:scale-y-105:hover {
+  --transform-scale-y: 1.05;
+}
+
+.hover\:scale-y-110:hover {
+  --transform-scale-y: 1.1;
+}
+
+.hover\:scale-y-125:hover {
+  --transform-scale-y: 1.25;
+}
+
+.hover\:scale-y-150:hover {
+  --transform-scale-y: 1.5;
+}
+
+.focus\:scale-0:focus {
+  --transform-scale-x: 0;
+  --transform-scale-y: 0;
+}
+
+.focus\:scale-50:focus {
+  --transform-scale-x: .5;
+  --transform-scale-y: .5;
+}
+
+.focus\:scale-75:focus {
+  --transform-scale-x: .75;
+  --transform-scale-y: .75;
+}
+
+.focus\:scale-90:focus {
+  --transform-scale-x: .9;
+  --transform-scale-y: .9;
+}
+
+.focus\:scale-95:focus {
+  --transform-scale-x: .95;
+  --transform-scale-y: .95;
+}
+
+.focus\:scale-100:focus {
+  --transform-scale-x: 1;
+  --transform-scale-y: 1;
+}
+
+.focus\:scale-105:focus {
+  --transform-scale-x: 1.05;
+  --transform-scale-y: 1.05;
+}
+
+.focus\:scale-110:focus {
+  --transform-scale-x: 1.1;
+  --transform-scale-y: 1.1;
+}
+
+.focus\:scale-125:focus {
+  --transform-scale-x: 1.25;
+  --transform-scale-y: 1.25;
+}
+
+.focus\:scale-150:focus {
+  --transform-scale-x: 1.5;
+  --transform-scale-y: 1.5;
+}
+
+.focus\:scale-x-0:focus {
+  --transform-scale-x: 0;
+}
+
+.focus\:scale-x-50:focus {
+  --transform-scale-x: .5;
+}
+
+.focus\:scale-x-75:focus {
+  --transform-scale-x: .75;
+}
+
+.focus\:scale-x-90:focus {
+  --transform-scale-x: .9;
+}
+
+.focus\:scale-x-95:focus {
+  --transform-scale-x: .95;
+}
+
+.focus\:scale-x-100:focus {
+  --transform-scale-x: 1;
+}
+
+.focus\:scale-x-105:focus {
+  --transform-scale-x: 1.05;
+}
+
+.focus\:scale-x-110:focus {
+  --transform-scale-x: 1.1;
+}
+
+.focus\:scale-x-125:focus {
+  --transform-scale-x: 1.25;
+}
+
+.focus\:scale-x-150:focus {
+  --transform-scale-x: 1.5;
+}
+
+.focus\:scale-y-0:focus {
+  --transform-scale-y: 0;
+}
+
+.focus\:scale-y-50:focus {
+  --transform-scale-y: .5;
+}
+
+.focus\:scale-y-75:focus {
+  --transform-scale-y: .75;
+}
+
+.focus\:scale-y-90:focus {
+  --transform-scale-y: .9;
+}
+
+.focus\:scale-y-95:focus {
+  --transform-scale-y: .95;
+}
+
+.focus\:scale-y-100:focus {
+  --transform-scale-y: 1;
+}
+
+.focus\:scale-y-105:focus {
+  --transform-scale-y: 1.05;
+}
+
+.focus\:scale-y-110:focus {
+  --transform-scale-y: 1.1;
+}
+
+.focus\:scale-y-125:focus {
+  --transform-scale-y: 1.25;
+}
+
+.focus\:scale-y-150:focus {
+  --transform-scale-y: 1.5;
+}
+
+.rotate-0 {
+  --transform-rotate: 0;
+}
+
+.rotate-45 {
+  --transform-rotate: 45deg;
+}
+
+.rotate-90 {
+  --transform-rotate: 90deg;
+}
+
+.rotate-180 {
+  --transform-rotate: 180deg;
+}
+
+.-rotate-180 {
+  --transform-rotate: -180deg;
+}
+
+.-rotate-90 {
+  --transform-rotate: -90deg;
+}
+
+.-rotate-45 {
+  --transform-rotate: -45deg;
+}
+
+.hover\:rotate-0:hover {
+  --transform-rotate: 0;
+}
+
+.hover\:rotate-45:hover {
+  --transform-rotate: 45deg;
+}
+
+.hover\:rotate-90:hover {
+  --transform-rotate: 90deg;
+}
+
+.hover\:rotate-180:hover {
+  --transform-rotate: 180deg;
+}
+
+.hover\:-rotate-180:hover {
+  --transform-rotate: -180deg;
+}
+
+.hover\:-rotate-90:hover {
+  --transform-rotate: -90deg;
+}
+
+.hover\:-rotate-45:hover {
+  --transform-rotate: -45deg;
+}
+
+.focus\:rotate-0:focus {
+  --transform-rotate: 0;
+}
+
+.focus\:rotate-45:focus {
+  --transform-rotate: 45deg;
+}
+
+.focus\:rotate-90:focus {
+  --transform-rotate: 90deg;
+}
+
+.focus\:rotate-180:focus {
+  --transform-rotate: 180deg;
+}
+
+.focus\:-rotate-180:focus {
+  --transform-rotate: -180deg;
+}
+
+.focus\:-rotate-90:focus {
+  --transform-rotate: -90deg;
+}
+
+.focus\:-rotate-45:focus {
+  --transform-rotate: -45deg;
+}
+
+.translate-x-0 {
+  --transform-translate-x: 0;
+}
+
+.translate-x-1 {
+  --transform-translate-x: 0.25rem;
+}
+
+.translate-x-2 {
+  --transform-translate-x: 0.5rem;
+}
+
+.translate-x-3 {
+  --transform-translate-x: 0.75rem;
+}
+
+.translate-x-4 {
+  --transform-translate-x: 1rem;
+}
+
+.translate-x-5 {
+  --transform-translate-x: 1.25rem;
+}
+
+.translate-x-6 {
+  --transform-translate-x: 1.5rem;
+}
+
+.translate-x-8 {
+  --transform-translate-x: 2rem;
+}
+
+.translate-x-10 {
+  --transform-translate-x: 2.5rem;
+}
+
+.translate-x-12 {
+  --transform-translate-x: 3rem;
+}
+
+.translate-x-16 {
+  --transform-translate-x: 4rem;
+}
+
+.translate-x-20 {
+  --transform-translate-x: 5rem;
+}
+
+.translate-x-24 {
+  --transform-translate-x: 6rem;
+}
+
+.translate-x-32 {
+  --transform-translate-x: 8rem;
+}
+
+.translate-x-40 {
+  --transform-translate-x: 10rem;
+}
+
+.translate-x-48 {
+  --transform-translate-x: 12rem;
+}
+
+.translate-x-56 {
+  --transform-translate-x: 14rem;
+}
+
+.translate-x-64 {
+  --transform-translate-x: 16rem;
+}
+
+.translate-x-px {
+  --transform-translate-x: 1px;
+}
+
+.-translate-x-1 {
+  --transform-translate-x: -0.25rem;
+}
+
+.-translate-x-2 {
+  --transform-translate-x: -0.5rem;
+}
+
+.-translate-x-3 {
+  --transform-translate-x: -0.75rem;
+}
+
+.-translate-x-4 {
+  --transform-translate-x: -1rem;
+}
+
+.-translate-x-5 {
+  --transform-translate-x: -1.25rem;
+}
+
+.-translate-x-6 {
+  --transform-translate-x: -1.5rem;
+}
+
+.-translate-x-8 {
+  --transform-translate-x: -2rem;
+}
+
+.-translate-x-10 {
+  --transform-translate-x: -2.5rem;
+}
+
+.-translate-x-12 {
+  --transform-translate-x: -3rem;
+}
+
+.-translate-x-16 {
+  --transform-translate-x: -4rem;
+}
+
+.-translate-x-20 {
+  --transform-translate-x: -5rem;
+}
+
+.-translate-x-24 {
+  --transform-translate-x: -6rem;
+}
+
+.-translate-x-32 {
+  --transform-translate-x: -8rem;
+}
+
+.-translate-x-40 {
+  --transform-translate-x: -10rem;
+}
+
+.-translate-x-48 {
+  --transform-translate-x: -12rem;
+}
+
+.-translate-x-56 {
+  --transform-translate-x: -14rem;
+}
+
+.-translate-x-64 {
+  --transform-translate-x: -16rem;
+}
+
+.-translate-x-px {
+  --transform-translate-x: -1px;
+}
+
+.-translate-x-full {
+  --transform-translate-x: -100%;
+}
+
+.-translate-x-1\/2 {
+  --transform-translate-x: -50%;
+}
+
+.translate-x-1\/2 {
+  --transform-translate-x: 50%;
+}
+
+.translate-x-full {
+  --transform-translate-x: 100%;
+}
+
+.translate-y-0 {
+  --transform-translate-y: 0;
+}
+
+.translate-y-1 {
+  --transform-translate-y: 0.25rem;
+}
+
+.translate-y-2 {
+  --transform-translate-y: 0.5rem;
+}
+
+.translate-y-3 {
+  --transform-translate-y: 0.75rem;
+}
+
+.translate-y-4 {
+  --transform-translate-y: 1rem;
+}
+
+.translate-y-5 {
+  --transform-translate-y: 1.25rem;
+}
+
+.translate-y-6 {
+  --transform-translate-y: 1.5rem;
+}
+
+.translate-y-8 {
+  --transform-translate-y: 2rem;
+}
+
+.translate-y-10 {
+  --transform-translate-y: 2.5rem;
+}
+
+.translate-y-12 {
+  --transform-translate-y: 3rem;
+}
+
+.translate-y-16 {
+  --transform-translate-y: 4rem;
+}
+
+.translate-y-20 {
+  --transform-translate-y: 5rem;
+}
+
+.translate-y-24 {
+  --transform-translate-y: 6rem;
+}
+
+.translate-y-32 {
+  --transform-translate-y: 8rem;
+}
+
+.translate-y-40 {
+  --transform-translate-y: 10rem;
+}
+
+.translate-y-48 {
+  --transform-translate-y: 12rem;
+}
+
+.translate-y-56 {
+  --transform-translate-y: 14rem;
+}
+
+.translate-y-64 {
+  --transform-translate-y: 16rem;
+}
+
+.translate-y-px {
+  --transform-translate-y: 1px;
+}
+
+.-translate-y-1 {
+  --transform-translate-y: -0.25rem;
+}
+
+.-translate-y-2 {
+  --transform-translate-y: -0.5rem;
+}
+
+.-translate-y-3 {
+  --transform-translate-y: -0.75rem;
+}
+
+.-translate-y-4 {
+  --transform-translate-y: -1rem;
+}
+
+.-translate-y-5 {
+  --transform-translate-y: -1.25rem;
+}
+
+.-translate-y-6 {
+  --transform-translate-y: -1.5rem;
+}
+
+.-translate-y-8 {
+  --transform-translate-y: -2rem;
+}
+
+.-translate-y-10 {
+  --transform-translate-y: -2.5rem;
+}
+
+.-translate-y-12 {
+  --transform-translate-y: -3rem;
+}
+
+.-translate-y-16 {
+  --transform-translate-y: -4rem;
+}
+
+.-translate-y-20 {
+  --transform-translate-y: -5rem;
+}
+
+.-translate-y-24 {
+  --transform-translate-y: -6rem;
+}
+
+.-translate-y-32 {
+  --transform-translate-y: -8rem;
+}
+
+.-translate-y-40 {
+  --transform-translate-y: -10rem;
+}
+
+.-translate-y-48 {
+  --transform-translate-y: -12rem;
+}
+
+.-translate-y-56 {
+  --transform-translate-y: -14rem;
+}
+
+.-translate-y-64 {
+  --transform-translate-y: -16rem;
+}
+
+.-translate-y-px {
+  --transform-translate-y: -1px;
+}
+
+.-translate-y-full {
+  --transform-translate-y: -100%;
+}
+
+.-translate-y-1\/2 {
+  --transform-translate-y: -50%;
+}
+
+.translate-y-1\/2 {
+  --transform-translate-y: 50%;
+}
+
+.translate-y-full {
+  --transform-translate-y: 100%;
+}
+
+.hover\:translate-x-0:hover {
+  --transform-translate-x: 0;
+}
+
+.hover\:translate-x-1:hover {
+  --transform-translate-x: 0.25rem;
+}
+
+.hover\:translate-x-2:hover {
+  --transform-translate-x: 0.5rem;
+}
+
+.hover\:translate-x-3:hover {
+  --transform-translate-x: 0.75rem;
+}
+
+.hover\:translate-x-4:hover {
+  --transform-translate-x: 1rem;
+}
+
+.hover\:translate-x-5:hover {
+  --transform-translate-x: 1.25rem;
+}
+
+.hover\:translate-x-6:hover {
+  --transform-translate-x: 1.5rem;
+}
+
+.hover\:translate-x-8:hover {
+  --transform-translate-x: 2rem;
+}
+
+.hover\:translate-x-10:hover {
+  --transform-translate-x: 2.5rem;
+}
+
+.hover\:translate-x-12:hover {
+  --transform-translate-x: 3rem;
+}
+
+.hover\:translate-x-16:hover {
+  --transform-translate-x: 4rem;
+}
+
+.hover\:translate-x-20:hover {
+  --transform-translate-x: 5rem;
+}
+
+.hover\:translate-x-24:hover {
+  --transform-translate-x: 6rem;
+}
+
+.hover\:translate-x-32:hover {
+  --transform-translate-x: 8rem;
+}
+
+.hover\:translate-x-40:hover {
+  --transform-translate-x: 10rem;
+}
+
+.hover\:translate-x-48:hover {
+  --transform-translate-x: 12rem;
+}
+
+.hover\:translate-x-56:hover {
+  --transform-translate-x: 14rem;
+}
+
+.hover\:translate-x-64:hover {
+  --transform-translate-x: 16rem;
+}
+
+.hover\:translate-x-px:hover {
+  --transform-translate-x: 1px;
+}
+
+.hover\:-translate-x-1:hover {
+  --transform-translate-x: -0.25rem;
+}
+
+.hover\:-translate-x-2:hover {
+  --transform-translate-x: -0.5rem;
+}
+
+.hover\:-translate-x-3:hover {
+  --transform-translate-x: -0.75rem;
+}
+
+.hover\:-translate-x-4:hover {
+  --transform-translate-x: -1rem;
+}
+
+.hover\:-translate-x-5:hover {
+  --transform-translate-x: -1.25rem;
+}
+
+.hover\:-translate-x-6:hover {
+  --transform-translate-x: -1.5rem;
+}
+
+.hover\:-translate-x-8:hover {
+  --transform-translate-x: -2rem;
+}
+
+.hover\:-translate-x-10:hover {
+  --transform-translate-x: -2.5rem;
+}
+
+.hover\:-translate-x-12:hover {
+  --transform-translate-x: -3rem;
+}
+
+.hover\:-translate-x-16:hover {
+  --transform-translate-x: -4rem;
+}
+
+.hover\:-translate-x-20:hover {
+  --transform-translate-x: -5rem;
+}
+
+.hover\:-translate-x-24:hover {
+  --transform-translate-x: -6rem;
+}
+
+.hover\:-translate-x-32:hover {
+  --transform-translate-x: -8rem;
+}
+
+.hover\:-translate-x-40:hover {
+  --transform-translate-x: -10rem;
+}
+
+.hover\:-translate-x-48:hover {
+  --transform-translate-x: -12rem;
+}
+
+.hover\:-translate-x-56:hover {
+  --transform-translate-x: -14rem;
+}
+
+.hover\:-translate-x-64:hover {
+  --transform-translate-x: -16rem;
+}
+
+.hover\:-translate-x-px:hover {
+  --transform-translate-x: -1px;
+}
+
+.hover\:-translate-x-full:hover {
+  --transform-translate-x: -100%;
+}
+
+.hover\:-translate-x-1\/2:hover {
+  --transform-translate-x: -50%;
+}
+
+.hover\:translate-x-1\/2:hover {
+  --transform-translate-x: 50%;
+}
+
+.hover\:translate-x-full:hover {
+  --transform-translate-x: 100%;
+}
+
+.hover\:translate-y-0:hover {
+  --transform-translate-y: 0;
+}
+
+.hover\:translate-y-1:hover {
+  --transform-translate-y: 0.25rem;
+}
+
+.hover\:translate-y-2:hover {
+  --transform-translate-y: 0.5rem;
+}
+
+.hover\:translate-y-3:hover {
+  --transform-translate-y: 0.75rem;
+}
+
+.hover\:translate-y-4:hover {
+  --transform-translate-y: 1rem;
+}
+
+.hover\:translate-y-5:hover {
+  --transform-translate-y: 1.25rem;
+}
+
+.hover\:translate-y-6:hover {
+  --transform-translate-y: 1.5rem;
+}
+
+.hover\:translate-y-8:hover {
+  --transform-translate-y: 2rem;
+}
+
+.hover\:translate-y-10:hover {
+  --transform-translate-y: 2.5rem;
+}
+
+.hover\:translate-y-12:hover {
+  --transform-translate-y: 3rem;
+}
+
+.hover\:translate-y-16:hover {
+  --transform-translate-y: 4rem;
+}
+
+.hover\:translate-y-20:hover {
+  --transform-translate-y: 5rem;
+}
+
+.hover\:translate-y-24:hover {
+  --transform-translate-y: 6rem;
+}
+
+.hover\:translate-y-32:hover {
+  --transform-translate-y: 8rem;
+}
+
+.hover\:translate-y-40:hover {
+  --transform-translate-y: 10rem;
+}
+
+.hover\:translate-y-48:hover {
+  --transform-translate-y: 12rem;
+}
+
+.hover\:translate-y-56:hover {
+  --transform-translate-y: 14rem;
+}
+
+.hover\:translate-y-64:hover {
+  --transform-translate-y: 16rem;
+}
+
+.hover\:translate-y-px:hover {
+  --transform-translate-y: 1px;
+}
+
+.hover\:-translate-y-1:hover {
+  --transform-translate-y: -0.25rem;
+}
+
+.hover\:-translate-y-2:hover {
+  --transform-translate-y: -0.5rem;
+}
+
+.hover\:-translate-y-3:hover {
+  --transform-translate-y: -0.75rem;
+}
+
+.hover\:-translate-y-4:hover {
+  --transform-translate-y: -1rem;
+}
+
+.hover\:-translate-y-5:hover {
+  --transform-translate-y: -1.25rem;
+}
+
+.hover\:-translate-y-6:hover {
+  --transform-translate-y: -1.5rem;
+}
+
+.hover\:-translate-y-8:hover {
+  --transform-translate-y: -2rem;
+}
+
+.hover\:-translate-y-10:hover {
+  --transform-translate-y: -2.5rem;
+}
+
+.hover\:-translate-y-12:hover {
+  --transform-translate-y: -3rem;
+}
+
+.hover\:-translate-y-16:hover {
+  --transform-translate-y: -4rem;
+}
+
+.hover\:-translate-y-20:hover {
+  --transform-translate-y: -5rem;
+}
+
+.hover\:-translate-y-24:hover {
+  --transform-translate-y: -6rem;
+}
+
+.hover\:-translate-y-32:hover {
+  --transform-translate-y: -8rem;
+}
+
+.hover\:-translate-y-40:hover {
+  --transform-translate-y: -10rem;
+}
+
+.hover\:-translate-y-48:hover {
+  --transform-translate-y: -12rem;
+}
+
+.hover\:-translate-y-56:hover {
+  --transform-translate-y: -14rem;
+}
+
+.hover\:-translate-y-64:hover {
+  --transform-translate-y: -16rem;
+}
+
+.hover\:-translate-y-px:hover {
+  --transform-translate-y: -1px;
+}
+
+.hover\:-translate-y-full:hover {
+  --transform-translate-y: -100%;
+}
+
+.hover\:-translate-y-1\/2:hover {
+  --transform-translate-y: -50%;
+}
+
+.hover\:translate-y-1\/2:hover {
+  --transform-translate-y: 50%;
+}
+
+.hover\:translate-y-full:hover {
+  --transform-translate-y: 100%;
+}
+
+.focus\:translate-x-0:focus {
+  --transform-translate-x: 0;
+}
+
+.focus\:translate-x-1:focus {
+  --transform-translate-x: 0.25rem;
+}
+
+.focus\:translate-x-2:focus {
+  --transform-translate-x: 0.5rem;
+}
+
+.focus\:translate-x-3:focus {
+  --transform-translate-x: 0.75rem;
+}
+
+.focus\:translate-x-4:focus {
+  --transform-translate-x: 1rem;
+}
+
+.focus\:translate-x-5:focus {
+  --transform-translate-x: 1.25rem;
+}
+
+.focus\:translate-x-6:focus {
+  --transform-translate-x: 1.5rem;
+}
+
+.focus\:translate-x-8:focus {
+  --transform-translate-x: 2rem;
+}
+
+.focus\:translate-x-10:focus {
+  --transform-translate-x: 2.5rem;
+}
+
+.focus\:translate-x-12:focus {
+  --transform-translate-x: 3rem;
+}
+
+.focus\:translate-x-16:focus {
+  --transform-translate-x: 4rem;
+}
+
+.focus\:translate-x-20:focus {
+  --transform-translate-x: 5rem;
+}
+
+.focus\:translate-x-24:focus {
+  --transform-translate-x: 6rem;
+}
+
+.focus\:translate-x-32:focus {
+  --transform-translate-x: 8rem;
+}
+
+.focus\:translate-x-40:focus {
+  --transform-translate-x: 10rem;
+}
+
+.focus\:translate-x-48:focus {
+  --transform-translate-x: 12rem;
+}
+
+.focus\:translate-x-56:focus {
+  --transform-translate-x: 14rem;
+}
+
+.focus\:translate-x-64:focus {
+  --transform-translate-x: 16rem;
+}
+
+.focus\:translate-x-px:focus {
+  --transform-translate-x: 1px;
+}
+
+.focus\:-translate-x-1:focus {
+  --transform-translate-x: -0.25rem;
+}
+
+.focus\:-translate-x-2:focus {
+  --transform-translate-x: -0.5rem;
+}
+
+.focus\:-translate-x-3:focus {
+  --transform-translate-x: -0.75rem;
+}
+
+.focus\:-translate-x-4:focus {
+  --transform-translate-x: -1rem;
+}
+
+.focus\:-translate-x-5:focus {
+  --transform-translate-x: -1.25rem;
+}
+
+.focus\:-translate-x-6:focus {
+  --transform-translate-x: -1.5rem;
+}
+
+.focus\:-translate-x-8:focus {
+  --transform-translate-x: -2rem;
+}
+
+.focus\:-translate-x-10:focus {
+  --transform-translate-x: -2.5rem;
+}
+
+.focus\:-translate-x-12:focus {
+  --transform-translate-x: -3rem;
+}
+
+.focus\:-translate-x-16:focus {
+  --transform-translate-x: -4rem;
+}
+
+.focus\:-translate-x-20:focus {
+  --transform-translate-x: -5rem;
+}
+
+.focus\:-translate-x-24:focus {
+  --transform-translate-x: -6rem;
+}
+
+.focus\:-translate-x-32:focus {
+  --transform-translate-x: -8rem;
+}
+
+.focus\:-translate-x-40:focus {
+  --transform-translate-x: -10rem;
+}
+
+.focus\:-translate-x-48:focus {
+  --transform-translate-x: -12rem;
+}
+
+.focus\:-translate-x-56:focus {
+  --transform-translate-x: -14rem;
+}
+
+.focus\:-translate-x-64:focus {
+  --transform-translate-x: -16rem;
+}
+
+.focus\:-translate-x-px:focus {
+  --transform-translate-x: -1px;
+}
+
+.focus\:-translate-x-full:focus {
+  --transform-translate-x: -100%;
+}
+
+.focus\:-translate-x-1\/2:focus {
+  --transform-translate-x: -50%;
+}
+
+.focus\:translate-x-1\/2:focus {
+  --transform-translate-x: 50%;
+}
+
+.focus\:translate-x-full:focus {
+  --transform-translate-x: 100%;
+}
+
+.focus\:translate-y-0:focus {
+  --transform-translate-y: 0;
+}
+
+.focus\:translate-y-1:focus {
+  --transform-translate-y: 0.25rem;
+}
+
+.focus\:translate-y-2:focus {
+  --transform-translate-y: 0.5rem;
+}
+
+.focus\:translate-y-3:focus {
+  --transform-translate-y: 0.75rem;
+}
+
+.focus\:translate-y-4:focus {
+  --transform-translate-y: 1rem;
+}
+
+.focus\:translate-y-5:focus {
+  --transform-translate-y: 1.25rem;
+}
+
+.focus\:translate-y-6:focus {
+  --transform-translate-y: 1.5rem;
+}
+
+.focus\:translate-y-8:focus {
+  --transform-translate-y: 2rem;
+}
+
+.focus\:translate-y-10:focus {
+  --transform-translate-y: 2.5rem;
+}
+
+.focus\:translate-y-12:focus {
+  --transform-translate-y: 3rem;
+}
+
+.focus\:translate-y-16:focus {
+  --transform-translate-y: 4rem;
+}
+
+.focus\:translate-y-20:focus {
+  --transform-translate-y: 5rem;
+}
+
+.focus\:translate-y-24:focus {
+  --transform-translate-y: 6rem;
+}
+
+.focus\:translate-y-32:focus {
+  --transform-translate-y: 8rem;
+}
+
+.focus\:translate-y-40:focus {
+  --transform-translate-y: 10rem;
+}
+
+.focus\:translate-y-48:focus {
+  --transform-translate-y: 12rem;
+}
+
+.focus\:translate-y-56:focus {
+  --transform-translate-y: 14rem;
+}
+
+.focus\:translate-y-64:focus {
+  --transform-translate-y: 16rem;
+}
+
+.focus\:translate-y-px:focus {
+  --transform-translate-y: 1px;
+}
+
+.focus\:-translate-y-1:focus {
+  --transform-translate-y: -0.25rem;
+}
+
+.focus\:-translate-y-2:focus {
+  --transform-translate-y: -0.5rem;
+}
+
+.focus\:-translate-y-3:focus {
+  --transform-translate-y: -0.75rem;
+}
+
+.focus\:-translate-y-4:focus {
+  --transform-translate-y: -1rem;
+}
+
+.focus\:-translate-y-5:focus {
+  --transform-translate-y: -1.25rem;
+}
+
+.focus\:-translate-y-6:focus {
+  --transform-translate-y: -1.5rem;
+}
+
+.focus\:-translate-y-8:focus {
+  --transform-translate-y: -2rem;
+}
+
+.focus\:-translate-y-10:focus {
+  --transform-translate-y: -2.5rem;
+}
+
+.focus\:-translate-y-12:focus {
+  --transform-translate-y: -3rem;
+}
+
+.focus\:-translate-y-16:focus {
+  --transform-translate-y: -4rem;
+}
+
+.focus\:-translate-y-20:focus {
+  --transform-translate-y: -5rem;
+}
+
+.focus\:-translate-y-24:focus {
+  --transform-translate-y: -6rem;
+}
+
+.focus\:-translate-y-32:focus {
+  --transform-translate-y: -8rem;
+}
+
+.focus\:-translate-y-40:focus {
+  --transform-translate-y: -10rem;
+}
+
+.focus\:-translate-y-48:focus {
+  --transform-translate-y: -12rem;
+}
+
+.focus\:-translate-y-56:focus {
+  --transform-translate-y: -14rem;
+}
+
+.focus\:-translate-y-64:focus {
+  --transform-translate-y: -16rem;
+}
+
+.focus\:-translate-y-px:focus {
+  --transform-translate-y: -1px;
+}
+
+.focus\:-translate-y-full:focus {
+  --transform-translate-y: -100%;
+}
+
+.focus\:-translate-y-1\/2:focus {
+  --transform-translate-y: -50%;
+}
+
+.focus\:translate-y-1\/2:focus {
+  --transform-translate-y: 50%;
+}
+
+.focus\:translate-y-full:focus {
+  --transform-translate-y: 100%;
+}
+
+.skew-x-0 {
+  --transform-skew-x: 0;
+}
+
+.skew-x-3 {
+  --transform-skew-x: 3deg;
+}
+
+.skew-x-6 {
+  --transform-skew-x: 6deg;
+}
+
+.skew-x-12 {
+  --transform-skew-x: 12deg;
+}
+
+.-skew-x-12 {
+  --transform-skew-x: -12deg;
+}
+
+.-skew-x-6 {
+  --transform-skew-x: -6deg;
+}
+
+.-skew-x-3 {
+  --transform-skew-x: -3deg;
+}
+
+.skew-y-0 {
+  --transform-skew-y: 0;
+}
+
+.skew-y-3 {
+  --transform-skew-y: 3deg;
+}
+
+.skew-y-6 {
+  --transform-skew-y: 6deg;
+}
+
+.skew-y-12 {
+  --transform-skew-y: 12deg;
+}
+
+.-skew-y-12 {
+  --transform-skew-y: -12deg;
+}
+
+.-skew-y-6 {
+  --transform-skew-y: -6deg;
+}
+
+.-skew-y-3 {
+  --transform-skew-y: -3deg;
+}
+
+.hover\:skew-x-0:hover {
+  --transform-skew-x: 0;
+}
+
+.hover\:skew-x-3:hover {
+  --transform-skew-x: 3deg;
+}
+
+.hover\:skew-x-6:hover {
+  --transform-skew-x: 6deg;
+}
+
+.hover\:skew-x-12:hover {
+  --transform-skew-x: 12deg;
+}
+
+.hover\:-skew-x-12:hover {
+  --transform-skew-x: -12deg;
+}
+
+.hover\:-skew-x-6:hover {
+  --transform-skew-x: -6deg;
+}
+
+.hover\:-skew-x-3:hover {
+  --transform-skew-x: -3deg;
+}
+
+.hover\:skew-y-0:hover {
+  --transform-skew-y: 0;
+}
+
+.hover\:skew-y-3:hover {
+  --transform-skew-y: 3deg;
+}
+
+.hover\:skew-y-6:hover {
+  --transform-skew-y: 6deg;
+}
+
+.hover\:skew-y-12:hover {
+  --transform-skew-y: 12deg;
+}
+
+.hover\:-skew-y-12:hover {
+  --transform-skew-y: -12deg;
+}
+
+.hover\:-skew-y-6:hover {
+  --transform-skew-y: -6deg;
+}
+
+.hover\:-skew-y-3:hover {
+  --transform-skew-y: -3deg;
+}
+
+.focus\:skew-x-0:focus {
+  --transform-skew-x: 0;
+}
+
+.focus\:skew-x-3:focus {
+  --transform-skew-x: 3deg;
+}
+
+.focus\:skew-x-6:focus {
+  --transform-skew-x: 6deg;
+}
+
+.focus\:skew-x-12:focus {
+  --transform-skew-x: 12deg;
+}
+
+.focus\:-skew-x-12:focus {
+  --transform-skew-x: -12deg;
+}
+
+.focus\:-skew-x-6:focus {
+  --transform-skew-x: -6deg;
+}
+
+.focus\:-skew-x-3:focus {
+  --transform-skew-x: -3deg;
+}
+
+.focus\:skew-y-0:focus {
+  --transform-skew-y: 0;
+}
+
+.focus\:skew-y-3:focus {
+  --transform-skew-y: 3deg;
+}
+
+.focus\:skew-y-6:focus {
+  --transform-skew-y: 6deg;
+}
+
+.focus\:skew-y-12:focus {
+  --transform-skew-y: 12deg;
+}
+
+.focus\:-skew-y-12:focus {
+  --transform-skew-y: -12deg;
+}
+
+.focus\:-skew-y-6:focus {
+  --transform-skew-y: -6deg;
+}
+
+.focus\:-skew-y-3:focus {
+  --transform-skew-y: -3deg;
+}
+
+.transition-none {
+  transition-property: none;
+}
+
+.transition-all {
+  transition-property: all;
+}
+
+.transition {
+  transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+}
+
+.transition-colors {
+  transition-property: background-color, border-color, color, fill, stroke;
+}
+
+.transition-opacity {
+  transition-property: opacity;
+}
+
+.transition-shadow {
+  transition-property: box-shadow;
+}
+
+.transition-transform {
+  transition-property: transform;
+}
+
+.ease-linear {
+  transition-timing-function: linear;
+}
+
+.ease-in {
+  transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+}
+
+.ease-out {
+  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+}
+
+.ease-in-out {
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.duration-75 {
+  transition-duration: 75ms;
+}
+
+.duration-100 {
+  transition-duration: 100ms;
+}
+
+.duration-150 {
+  transition-duration: 150ms;
+}
+
+.duration-200 {
+  transition-duration: 200ms;
+}
+
+.duration-300 {
+  transition-duration: 300ms;
+}
+
+.duration-500 {
+  transition-duration: 500ms;
+}
+
+.duration-700 {
+  transition-duration: 700ms;
+}
+
+.duration-1000 {
+  transition-duration: 1000ms;
+}
+
+.delay-75 {
+  transition-delay: 75ms;
+}
+
+.delay-100 {
+  transition-delay: 100ms;
+}
+
+.delay-150 {
+  transition-delay: 150ms;
+}
+
+.delay-200 {
+  transition-delay: 200ms;
+}
+
+.delay-300 {
+  transition-delay: 300ms;
+}
+
+.delay-500 {
+  transition-delay: 500ms;
+}
+
+.delay-700 {
+  transition-delay: 700ms;
+}
+
+.delay-1000 {
+  transition-delay: 1000ms;
+}
+
+@-webkit-keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@-webkit-keyframes ping {
+  75%, 100% {
+    transform: scale(2);
+    opacity: 0;
+  }
+}
+
+@keyframes ping {
+  75%, 100% {
+    transform: scale(2);
+    opacity: 0;
+  }
+}
+
+@-webkit-keyframes pulse {
+  50% {
+    opacity: .5;
+  }
+}
+
+@keyframes pulse {
+  50% {
+    opacity: .5;
+  }
+}
+
+@-webkit-keyframes bounce {
+  0%, 100% {
+    transform: translateY(-25%);
+    -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
+            animation-timing-function: cubic-bezier(0.8,0,1,1);
+  }
+
+  50% {
+    transform: none;
+    -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
+            animation-timing-function: cubic-bezier(0,0,0.2,1);
+  }
+}
+
+@keyframes bounce {
+  0%, 100% {
+    transform: translateY(-25%);
+    -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
+            animation-timing-function: cubic-bezier(0.8,0,1,1);
+  }
+
+  50% {
+    transform: none;
+    -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
+            animation-timing-function: cubic-bezier(0,0,0.2,1);
+  }
+}
+
+.animate-none {
+  -webkit-animation: none;
+          animation: none;
+}
+
+.animate-spin {
+  -webkit-animation: spin 1s linear infinite;
+          animation: spin 1s linear infinite;
+}
+
+.animate-ping {
+  -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+          animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+}
+
+.animate-pulse {
+  -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+          animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+.animate-bounce {
+  -webkit-animation: bounce 1s infinite;
+          animation: bounce 1s infinite;
+}
+
+@media (min-width: 640px) {
+  .sm\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .sm\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .sm\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .sm\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .sm\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .sm\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .sm\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .sm\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .sm\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .sm\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .sm\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .sm\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .sm\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .sm\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .sm\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .sm\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .sm\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .sm\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .sm\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .sm\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .sm\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .sm\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .sm\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .sm\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .sm\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .sm\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .sm\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .sm\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .sm\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .sm\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .sm\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .sm\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .sm\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .sm\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .sm\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .sm\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .sm\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .sm\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .sm\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .sm\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .sm\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .sm\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .sm\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .sm\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .sm\:bg-local {
+    background-attachment: local;
+  }
+
+  .sm\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .sm\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .sm\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .sm\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .sm\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .sm\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .sm\:bg-current {
+    background-color: currentColor;
+  }
+
+  .sm\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .sm\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .sm\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .sm\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .sm\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .sm\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .sm\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .sm\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .sm\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .sm\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .sm\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .sm\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .sm\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .sm\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .sm\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .sm\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .sm\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .sm\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .sm\:bg-none {
+    background-image: none;
+  }
+
+  .sm\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .sm\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .sm\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .sm\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .sm\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .sm\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .sm\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .sm\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .sm\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .sm\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .sm\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .sm\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .sm\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .sm\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .sm\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .sm\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .sm\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .sm\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .sm\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .sm\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .sm\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .sm\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .sm\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .sm\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .sm\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .sm\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .sm\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .sm\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .sm\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .sm\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .sm\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .sm\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .sm\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .sm\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .sm\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .sm\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .sm\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .sm\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .sm\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .sm\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .sm\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .sm\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .sm\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .sm\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .sm\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .sm\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .sm\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .sm\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .sm\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .sm\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .sm\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .sm\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .sm\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .sm\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .sm\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .sm\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .sm\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .sm\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .sm\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .sm\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .sm\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .sm\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .sm\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .sm\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .sm\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .sm\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .sm\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .sm\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .sm\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .sm\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .sm\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .sm\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .sm\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .sm\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .sm\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .sm\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .sm\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .sm\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .sm\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .sm\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .sm\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .sm\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .sm\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .sm\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .sm\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .sm\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .sm\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .sm\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .sm\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .sm\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .sm\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .sm\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .sm\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .sm\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .sm\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .sm\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .sm\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .sm\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .sm\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .sm\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .sm\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .sm\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .sm\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .sm\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .sm\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .sm\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .sm\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .sm\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .sm\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .sm\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .sm\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .sm\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .sm\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .sm\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .sm\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .sm\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .sm\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .sm\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .sm\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .sm\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .sm\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .sm\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .sm\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .sm\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .sm\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .sm\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .sm\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .sm\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .sm\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .sm\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .sm\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .sm\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .sm\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .sm\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .sm\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .sm\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .sm\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .sm\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .sm\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .sm\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .sm\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .sm\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .sm\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .sm\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .sm\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .sm\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .sm\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .sm\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .sm\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .sm\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .sm\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .sm\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .sm\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .sm\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .sm\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .sm\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .sm\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .sm\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .sm\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .sm\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .sm\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .sm\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .sm\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .sm\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .sm\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .sm\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .sm\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .sm\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .sm\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .sm\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .sm\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .sm\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .sm\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .sm\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .sm\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .sm\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .sm\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .sm\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .sm\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .sm\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .sm\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .sm\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .sm\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .sm\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .sm\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .sm\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .sm\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .sm\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .sm\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .sm\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .sm\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .sm\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .sm\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .sm\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .sm\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .sm\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .sm\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .sm\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .sm\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .sm\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .sm\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .sm\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .sm\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .sm\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .sm\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .sm\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .sm\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .sm\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .sm\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .sm\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .sm\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .sm\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .sm\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .sm\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .sm\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .sm\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .sm\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .sm\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .sm\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .sm\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .sm\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .sm\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .sm\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .sm\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .sm\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .sm\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .sm\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .sm\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .sm\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .sm\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .sm\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .sm\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .sm\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .sm\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .sm\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .sm\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .sm\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .sm\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .sm\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .sm\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .sm\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .sm\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .sm\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .sm\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .sm\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .sm\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .sm\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .sm\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .sm\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .sm\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .sm\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .sm\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .sm\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .sm\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .sm\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .sm\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .sm\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .sm\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .sm\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .sm\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .sm\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .sm\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .sm\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .sm\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .sm\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .sm\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .sm\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .sm\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .sm\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .sm\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .sm\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .sm\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .sm\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .sm\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .sm\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .sm\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .sm\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .sm\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .sm\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .sm\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .sm\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .sm\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .sm\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .sm\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .sm\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .sm\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .sm\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .sm\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .sm\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .sm\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .sm\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .sm\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .sm\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .sm\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .sm\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .sm\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .sm\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .sm\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .sm\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .sm\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .sm\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .sm\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .sm\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .sm\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .sm\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .sm\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .sm\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .sm\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .sm\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .sm\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .sm\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .sm\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .sm\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .sm\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .sm\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .sm\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .sm\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .sm\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .sm\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .sm\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .sm\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .sm\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .sm\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .sm\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .sm\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .sm\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .sm\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .sm\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .sm\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .sm\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .sm\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .sm\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .sm\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .sm\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .sm\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .sm\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .sm\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .sm\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .sm\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .sm\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .sm\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .sm\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .sm\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .sm\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .sm\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .sm\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .sm\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .sm\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .sm\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .sm\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .sm\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .sm\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .sm\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .sm\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .sm\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .sm\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .sm\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .sm\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .sm\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .sm\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .sm\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .sm\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .sm\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .sm\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .sm\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .sm\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .sm\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .sm\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .sm\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .sm\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .sm\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .sm\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .sm\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .sm\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .sm\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .sm\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .sm\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .sm\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .sm\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .sm\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .sm\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .sm\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .sm\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .sm\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .sm\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .sm\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .sm\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .sm\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .sm\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .sm\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .sm\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .sm\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .sm\:bg-center {
+    background-position: center;
+  }
+
+  .sm\:bg-left {
+    background-position: left;
+  }
+
+  .sm\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .sm\:bg-left-top {
+    background-position: left top;
+  }
+
+  .sm\:bg-right {
+    background-position: right;
+  }
+
+  .sm\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .sm\:bg-right-top {
+    background-position: right top;
+  }
+
+  .sm\:bg-top {
+    background-position: top;
+  }
+
+  .sm\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .sm\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .sm\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .sm\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .sm\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .sm\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .sm\:bg-auto {
+    background-size: auto;
+  }
+
+  .sm\:bg-cover {
+    background-size: cover;
+  }
+
+  .sm\:bg-contain {
+    background-size: contain;
+  }
+
+  .sm\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .sm\:border-separate {
+    border-collapse: separate;
+  }
+
+  .sm\:border-transparent {
+    border-color: transparent;
+  }
+
+  .sm\:border-current {
+    border-color: currentColor;
+  }
+
+  .sm\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .sm\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .sm\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .sm\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .sm\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .sm\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .sm\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .sm\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .sm\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .sm\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .sm\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .sm\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .sm\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .sm\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .sm\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .sm\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .sm\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .sm\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .sm\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .sm\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .sm\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .sm\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .sm\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .sm\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .sm\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .sm\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .sm\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .sm\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .sm\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .sm\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .sm\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .sm\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .sm\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .sm\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .sm\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .sm\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .sm\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .sm\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .sm\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .sm\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .sm\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .sm\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .sm\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .sm\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .sm\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .sm\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .sm\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .sm\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .sm\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .sm\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .sm\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .sm\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .sm\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .sm\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .sm\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .sm\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .sm\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .sm\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .sm\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .sm\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .sm\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .sm\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .sm\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .sm\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .sm\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .sm\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .sm\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .sm\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .sm\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .sm\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .sm\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .sm\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .sm\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .sm\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .sm\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .sm\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .sm\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .sm\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .sm\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .sm\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .sm\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .sm\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .sm\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .sm\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .sm\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .sm\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .sm\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .sm\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .sm\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .sm\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .sm\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .sm\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .sm\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .sm\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .sm\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .sm\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .sm\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .sm\:rounded-none {
+    border-radius: 0;
+  }
+
+  .sm\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .sm\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .sm\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .sm\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .sm\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .sm\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .sm\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .sm\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .sm\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .sm\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .sm\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .sm\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .sm\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .sm\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .sm\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .sm\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .sm\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .sm\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .sm\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .sm\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .sm\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .sm\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .sm\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .sm\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .sm\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .sm\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .sm\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .sm\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .sm\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .sm\:border-solid {
+    border-style: solid;
+  }
+
+  .sm\:border-dashed {
+    border-style: dashed;
+  }
+
+  .sm\:border-dotted {
+    border-style: dotted;
+  }
+
+  .sm\:border-double {
+    border-style: double;
+  }
+
+  .sm\:border-none {
+    border-style: none;
+  }
+
+  .sm\:border-0 {
+    border-width: 0;
+  }
+
+  .sm\:border-2 {
+    border-width: 2px;
+  }
+
+  .sm\:border-4 {
+    border-width: 4px;
+  }
+
+  .sm\:border-8 {
+    border-width: 8px;
+  }
+
+  .sm\:border {
+    border-width: 1px;
+  }
+
+  .sm\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .sm\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .sm\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .sm\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .sm\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .sm\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .sm\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .sm\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .sm\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .sm\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .sm\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .sm\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .sm\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .sm\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .sm\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .sm\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .sm\:border-t {
+    border-top-width: 1px;
+  }
+
+  .sm\:border-r {
+    border-right-width: 1px;
+  }
+
+  .sm\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .sm\:border-l {
+    border-left-width: 1px;
+  }
+
+  .sm\:box-border {
+    box-sizing: border-box;
+  }
+
+  .sm\:box-content {
+    box-sizing: content-box;
+  }
+
+  .sm\:cursor-auto {
+    cursor: auto;
+  }
+
+  .sm\:cursor-default {
+    cursor: default;
+  }
+
+  .sm\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .sm\:cursor-wait {
+    cursor: wait;
+  }
+
+  .sm\:cursor-text {
+    cursor: text;
+  }
+
+  .sm\:cursor-move {
+    cursor: move;
+  }
+
+  .sm\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .sm\:block {
+    display: block;
+  }
+
+  .sm\:inline-block {
+    display: inline-block;
+  }
+
+  .sm\:inline {
+    display: inline;
+  }
+
+  .sm\:flex {
+    display: flex;
+  }
+
+  .sm\:inline-flex {
+    display: inline-flex;
+  }
+
+  .sm\:table {
+    display: table;
+  }
+
+  .sm\:table-caption {
+    display: table-caption;
+  }
+
+  .sm\:table-cell {
+    display: table-cell;
+  }
+
+  .sm\:table-column {
+    display: table-column;
+  }
+
+  .sm\:table-column-group {
+    display: table-column-group;
+  }
+
+  .sm\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .sm\:table-header-group {
+    display: table-header-group;
+  }
+
+  .sm\:table-row-group {
+    display: table-row-group;
+  }
+
+  .sm\:table-row {
+    display: table-row;
+  }
+
+  .sm\:flow-root {
+    display: flow-root;
+  }
+
+  .sm\:grid {
+    display: grid;
+  }
+
+  .sm\:inline-grid {
+    display: inline-grid;
+  }
+
+  .sm\:contents {
+    display: contents;
+  }
+
+  .sm\:hidden {
+    display: none;
+  }
+
+  .sm\:flex-row {
+    flex-direction: row;
+  }
+
+  .sm\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .sm\:flex-col {
+    flex-direction: column;
+  }
+
+  .sm\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .sm\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .sm\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .sm\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .sm\:place-items-auto {
+    place-items: auto;
+  }
+
+  .sm\:place-items-start {
+    place-items: start;
+  }
+
+  .sm\:place-items-end {
+    place-items: end;
+  }
+
+  .sm\:place-items-center {
+    place-items: center;
+  }
+
+  .sm\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .sm\:place-content-center {
+    place-content: center;
+  }
+
+  .sm\:place-content-start {
+    place-content: start;
+  }
+
+  .sm\:place-content-end {
+    place-content: end;
+  }
+
+  .sm\:place-content-between {
+    place-content: space-between;
+  }
+
+  .sm\:place-content-around {
+    place-content: space-around;
+  }
+
+  .sm\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .sm\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .sm\:place-self-auto {
+    place-self: auto;
+  }
+
+  .sm\:place-self-start {
+    place-self: start;
+  }
+
+  .sm\:place-self-end {
+    place-self: end;
+  }
+
+  .sm\:place-self-center {
+    place-self: center;
+  }
+
+  .sm\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .sm\:items-start {
+    align-items: flex-start;
+  }
+
+  .sm\:items-end {
+    align-items: flex-end;
+  }
+
+  .sm\:items-center {
+    align-items: center;
+  }
+
+  .sm\:items-baseline {
+    align-items: baseline;
+  }
+
+  .sm\:items-stretch {
+    align-items: stretch;
+  }
+
+  .sm\:content-center {
+    align-content: center;
+  }
+
+  .sm\:content-start {
+    align-content: flex-start;
+  }
+
+  .sm\:content-end {
+    align-content: flex-end;
+  }
+
+  .sm\:content-between {
+    align-content: space-between;
+  }
+
+  .sm\:content-around {
+    align-content: space-around;
+  }
+
+  .sm\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .sm\:self-auto {
+    align-self: auto;
+  }
+
+  .sm\:self-start {
+    align-self: flex-start;
+  }
+
+  .sm\:self-end {
+    align-self: flex-end;
+  }
+
+  .sm\:self-center {
+    align-self: center;
+  }
+
+  .sm\:self-stretch {
+    align-self: stretch;
+  }
+
+  .sm\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .sm\:justify-items-start {
+    justify-items: start;
+  }
+
+  .sm\:justify-items-end {
+    justify-items: end;
+  }
+
+  .sm\:justify-items-center {
+    justify-items: center;
+  }
+
+  .sm\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .sm\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .sm\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .sm\:justify-center {
+    justify-content: center;
+  }
+
+  .sm\:justify-between {
+    justify-content: space-between;
+  }
+
+  .sm\:justify-around {
+    justify-content: space-around;
+  }
+
+  .sm\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .sm\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .sm\:justify-self-start {
+    justify-self: start;
+  }
+
+  .sm\:justify-self-end {
+    justify-self: end;
+  }
+
+  .sm\:justify-self-center {
+    justify-self: center;
+  }
+
+  .sm\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .sm\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .sm\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .sm\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .sm\:flex-none {
+    flex: none;
+  }
+
+  .sm\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .sm\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .sm\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .sm\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .sm\:order-1 {
+    order: 1;
+  }
+
+  .sm\:order-2 {
+    order: 2;
+  }
+
+  .sm\:order-3 {
+    order: 3;
+  }
+
+  .sm\:order-4 {
+    order: 4;
+  }
+
+  .sm\:order-5 {
+    order: 5;
+  }
+
+  .sm\:order-6 {
+    order: 6;
+  }
+
+  .sm\:order-7 {
+    order: 7;
+  }
+
+  .sm\:order-8 {
+    order: 8;
+  }
+
+  .sm\:order-9 {
+    order: 9;
+  }
+
+  .sm\:order-10 {
+    order: 10;
+  }
+
+  .sm\:order-11 {
+    order: 11;
+  }
+
+  .sm\:order-12 {
+    order: 12;
+  }
+
+  .sm\:order-first {
+    order: -9999;
+  }
+
+  .sm\:order-last {
+    order: 9999;
+  }
+
+  .sm\:order-none {
+    order: 0;
+  }
+
+  .sm\:float-right {
+    float: right;
+  }
+
+  .sm\:float-left {
+    float: left;
+  }
+
+  .sm\:float-none {
+    float: none;
+  }
+
+  .sm\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .sm\:clear-left {
+    clear: left;
+  }
+
+  .sm\:clear-right {
+    clear: right;
+  }
+
+  .sm\:clear-both {
+    clear: both;
+  }
+
+  .sm\:clear-none {
+    clear: none;
+  }
+
+  .sm\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .sm\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .sm\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .sm\:font-hairline {
+    font-weight: 100;
+  }
+
+  .sm\:font-thin {
+    font-weight: 200;
+  }
+
+  .sm\:font-light {
+    font-weight: 300;
+  }
+
+  .sm\:font-normal {
+    font-weight: 400;
+  }
+
+  .sm\:font-medium {
+    font-weight: 500;
+  }
+
+  .sm\:font-semibold {
+    font-weight: 600;
+  }
+
+  .sm\:font-bold {
+    font-weight: 700;
+  }
+
+  .sm\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .sm\:font-black {
+    font-weight: 900;
+  }
+
+  .sm\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .sm\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .sm\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .sm\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .sm\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .sm\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .sm\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .sm\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .sm\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .sm\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .sm\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .sm\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .sm\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .sm\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .sm\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .sm\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .sm\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .sm\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .sm\:h-0 {
+    height: 0;
+  }
+
+  .sm\:h-1 {
+    height: 0.25rem;
+  }
+
+  .sm\:h-2 {
+    height: 0.5rem;
+  }
+
+  .sm\:h-3 {
+    height: 0.75rem;
+  }
+
+  .sm\:h-4 {
+    height: 1rem;
+  }
+
+  .sm\:h-5 {
+    height: 1.25rem;
+  }
+
+  .sm\:h-6 {
+    height: 1.5rem;
+  }
+
+  .sm\:h-8 {
+    height: 2rem;
+  }
+
+  .sm\:h-10 {
+    height: 2.5rem;
+  }
+
+  .sm\:h-12 {
+    height: 3rem;
+  }
+
+  .sm\:h-16 {
+    height: 4rem;
+  }
+
+  .sm\:h-20 {
+    height: 5rem;
+  }
+
+  .sm\:h-24 {
+    height: 6rem;
+  }
+
+  .sm\:h-32 {
+    height: 8rem;
+  }
+
+  .sm\:h-40 {
+    height: 10rem;
+  }
+
+  .sm\:h-48 {
+    height: 12rem;
+  }
+
+  .sm\:h-56 {
+    height: 14rem;
+  }
+
+  .sm\:h-64 {
+    height: 16rem;
+  }
+
+  .sm\:h-auto {
+    height: auto;
+  }
+
+  .sm\:h-px {
+    height: 1px;
+  }
+
+  .sm\:h-full {
+    height: 100%;
+  }
+
+  .sm\:h-screen {
+    height: 100vh;
+  }
+
+  .sm\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .sm\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .sm\:text-base {
+    font-size: 1rem;
+  }
+
+  .sm\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .sm\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .sm\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .sm\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .sm\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .sm\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .sm\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .sm\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .sm\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .sm\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .sm\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .sm\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .sm\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .sm\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .sm\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .sm\:leading-none {
+    line-height: 1;
+  }
+
+  .sm\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .sm\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .sm\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .sm\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .sm\:leading-loose {
+    line-height: 2;
+  }
+
+  .sm\:list-inside {
+    list-style-position: inside;
+  }
+
+  .sm\:list-outside {
+    list-style-position: outside;
+  }
+
+  .sm\:list-none {
+    list-style-type: none;
+  }
+
+  .sm\:list-disc {
+    list-style-type: disc;
+  }
+
+  .sm\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .sm\:m-0 {
+    margin: 0;
+  }
+
+  .sm\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .sm\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .sm\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .sm\:m-4 {
+    margin: 1rem;
+  }
+
+  .sm\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .sm\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .sm\:m-8 {
+    margin: 2rem;
+  }
+
+  .sm\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .sm\:m-12 {
+    margin: 3rem;
+  }
+
+  .sm\:m-16 {
+    margin: 4rem;
+  }
+
+  .sm\:m-20 {
+    margin: 5rem;
+  }
+
+  .sm\:m-24 {
+    margin: 6rem;
+  }
+
+  .sm\:m-32 {
+    margin: 8rem;
+  }
+
+  .sm\:m-40 {
+    margin: 10rem;
+  }
+
+  .sm\:m-48 {
+    margin: 12rem;
+  }
+
+  .sm\:m-56 {
+    margin: 14rem;
+  }
+
+  .sm\:m-64 {
+    margin: 16rem;
+  }
+
+  .sm\:m-auto {
+    margin: auto;
+  }
+
+  .sm\:m-px {
+    margin: 1px;
+  }
+
+  .sm\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .sm\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .sm\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .sm\:-m-4 {
+    margin: -1rem;
+  }
+
+  .sm\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .sm\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .sm\:-m-8 {
+    margin: -2rem;
+  }
+
+  .sm\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .sm\:-m-12 {
+    margin: -3rem;
+  }
+
+  .sm\:-m-16 {
+    margin: -4rem;
+  }
+
+  .sm\:-m-20 {
+    margin: -5rem;
+  }
+
+  .sm\:-m-24 {
+    margin: -6rem;
+  }
+
+  .sm\:-m-32 {
+    margin: -8rem;
+  }
+
+  .sm\:-m-40 {
+    margin: -10rem;
+  }
+
+  .sm\:-m-48 {
+    margin: -12rem;
+  }
+
+  .sm\:-m-56 {
+    margin: -14rem;
+  }
+
+  .sm\:-m-64 {
+    margin: -16rem;
+  }
+
+  .sm\:-m-px {
+    margin: -1px;
+  }
+
+  .sm\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .sm\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .sm\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .sm\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .sm\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .sm\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .sm\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .sm\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .sm\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .sm\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .sm\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .sm\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .sm\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .sm\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .sm\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .sm\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .sm\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .sm\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .sm\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .sm\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .sm\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .sm\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .sm\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .sm\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .sm\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .sm\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .sm\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .sm\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .sm\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .sm\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .sm\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .sm\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .sm\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .sm\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .sm\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .sm\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .sm\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .sm\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .sm\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .sm\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .sm\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .sm\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .sm\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .sm\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .sm\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .sm\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .sm\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .sm\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .sm\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .sm\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .sm\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .sm\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .sm\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .sm\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .sm\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .sm\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .sm\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .sm\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .sm\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .sm\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .sm\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .sm\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .sm\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .sm\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .sm\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .sm\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .sm\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .sm\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .sm\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .sm\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .sm\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .sm\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .sm\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .sm\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .sm\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .sm\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .sm\:mt-0 {
+    margin-top: 0;
+  }
+
+  .sm\:mr-0 {
+    margin-right: 0;
+  }
+
+  .sm\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .sm\:ml-0 {
+    margin-left: 0;
+  }
+
+  .sm\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .sm\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .sm\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .sm\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .sm\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .sm\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .sm\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .sm\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .sm\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .sm\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .sm\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .sm\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .sm\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .sm\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .sm\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .sm\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .sm\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .sm\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .sm\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .sm\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .sm\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .sm\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .sm\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .sm\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .sm\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .sm\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .sm\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .sm\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .sm\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .sm\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .sm\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .sm\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .sm\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .sm\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .sm\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .sm\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .sm\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .sm\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .sm\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .sm\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .sm\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .sm\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .sm\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .sm\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .sm\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .sm\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .sm\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .sm\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .sm\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .sm\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .sm\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .sm\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .sm\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .sm\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .sm\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .sm\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .sm\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .sm\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .sm\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .sm\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .sm\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .sm\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .sm\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .sm\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .sm\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .sm\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .sm\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .sm\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .sm\:mt-auto {
+    margin-top: auto;
+  }
+
+  .sm\:mr-auto {
+    margin-right: auto;
+  }
+
+  .sm\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .sm\:ml-auto {
+    margin-left: auto;
+  }
+
+  .sm\:mt-px {
+    margin-top: 1px;
+  }
+
+  .sm\:mr-px {
+    margin-right: 1px;
+  }
+
+  .sm\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .sm\:ml-px {
+    margin-left: 1px;
+  }
+
+  .sm\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .sm\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .sm\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .sm\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .sm\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .sm\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .sm\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .sm\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .sm\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .sm\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .sm\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .sm\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .sm\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .sm\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .sm\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .sm\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .sm\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .sm\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .sm\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .sm\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .sm\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .sm\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .sm\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .sm\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .sm\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .sm\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .sm\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .sm\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .sm\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .sm\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .sm\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .sm\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .sm\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .sm\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .sm\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .sm\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .sm\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .sm\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .sm\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .sm\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .sm\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .sm\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .sm\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .sm\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .sm\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .sm\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .sm\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .sm\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .sm\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .sm\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .sm\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .sm\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .sm\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .sm\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .sm\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .sm\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .sm\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .sm\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .sm\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .sm\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .sm\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .sm\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .sm\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .sm\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .sm\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .sm\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .sm\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .sm\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .sm\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .sm\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .sm\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .sm\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .sm\:max-h-full {
+    max-height: 100%;
+  }
+
+  .sm\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .sm\:max-w-none {
+    max-width: none;
+  }
+
+  .sm\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .sm\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .sm\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .sm\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .sm\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .sm\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .sm\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .sm\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .sm\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .sm\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .sm\:max-w-full {
+    max-width: 100%;
+  }
+
+  .sm\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .sm\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .sm\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .sm\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .sm\:min-h-0 {
+    min-height: 0;
+  }
+
+  .sm\:min-h-full {
+    min-height: 100%;
+  }
+
+  .sm\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .sm\:min-w-0 {
+    min-width: 0;
+  }
+
+  .sm\:min-w-full {
+    min-width: 100%;
+  }
+
+  .sm\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .sm\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .sm\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .sm\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .sm\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .sm\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .sm\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .sm\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .sm\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .sm\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .sm\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .sm\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .sm\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .sm\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .sm\:opacity-0 {
+    opacity: 0;
+  }
+
+  .sm\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .sm\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .sm\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .sm\:opacity-100 {
+    opacity: 1;
+  }
+
+  .sm\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .sm\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .sm\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .sm\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .sm\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .sm\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .sm\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .sm\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .sm\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .sm\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .sm\:outline-none {
+    outline: 0;
+  }
+
+  .sm\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .sm\:overflow-auto {
+    overflow: auto;
+  }
+
+  .sm\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .sm\:overflow-visible {
+    overflow: visible;
+  }
+
+  .sm\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .sm\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .sm\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .sm\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .sm\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .sm\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .sm\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .sm\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .sm\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .sm\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .sm\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .sm\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .sm\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .sm\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .sm\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .sm\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .sm\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .sm\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .sm\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .sm\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .sm\:p-0 {
+    padding: 0;
+  }
+
+  .sm\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .sm\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .sm\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .sm\:p-4 {
+    padding: 1rem;
+  }
+
+  .sm\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .sm\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .sm\:p-8 {
+    padding: 2rem;
+  }
+
+  .sm\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .sm\:p-12 {
+    padding: 3rem;
+  }
+
+  .sm\:p-16 {
+    padding: 4rem;
+  }
+
+  .sm\:p-20 {
+    padding: 5rem;
+  }
+
+  .sm\:p-24 {
+    padding: 6rem;
+  }
+
+  .sm\:p-32 {
+    padding: 8rem;
+  }
+
+  .sm\:p-40 {
+    padding: 10rem;
+  }
+
+  .sm\:p-48 {
+    padding: 12rem;
+  }
+
+  .sm\:p-56 {
+    padding: 14rem;
+  }
+
+  .sm\:p-64 {
+    padding: 16rem;
+  }
+
+  .sm\:p-px {
+    padding: 1px;
+  }
+
+  .sm\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .sm\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .sm\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .sm\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .sm\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .sm\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .sm\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .sm\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .sm\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .sm\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .sm\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .sm\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .sm\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .sm\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .sm\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .sm\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .sm\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .sm\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .sm\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .sm\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .sm\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .sm\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .sm\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .sm\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .sm\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .sm\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .sm\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .sm\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .sm\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .sm\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .sm\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .sm\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .sm\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .sm\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .sm\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .sm\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .sm\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .sm\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .sm\:pt-0 {
+    padding-top: 0;
+  }
+
+  .sm\:pr-0 {
+    padding-right: 0;
+  }
+
+  .sm\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .sm\:pl-0 {
+    padding-left: 0;
+  }
+
+  .sm\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .sm\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .sm\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .sm\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .sm\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .sm\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .sm\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .sm\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .sm\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .sm\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .sm\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .sm\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .sm\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .sm\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .sm\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .sm\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .sm\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .sm\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .sm\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .sm\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .sm\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .sm\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .sm\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .sm\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .sm\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .sm\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .sm\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .sm\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .sm\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .sm\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .sm\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .sm\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .sm\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .sm\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .sm\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .sm\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .sm\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .sm\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .sm\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .sm\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .sm\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .sm\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .sm\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .sm\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .sm\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .sm\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .sm\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .sm\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .sm\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .sm\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .sm\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .sm\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .sm\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .sm\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .sm\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .sm\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .sm\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .sm\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .sm\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .sm\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .sm\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .sm\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .sm\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .sm\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .sm\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .sm\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .sm\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .sm\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .sm\:pt-px {
+    padding-top: 1px;
+  }
+
+  .sm\:pr-px {
+    padding-right: 1px;
+  }
+
+  .sm\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .sm\:pl-px {
+    padding-left: 1px;
+  }
+
+  .sm\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .sm\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .sm\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .sm\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .sm\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .sm\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .sm\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .sm\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .sm\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .sm\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .sm\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .sm\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .sm\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .sm\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .sm\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .sm\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .sm\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .sm\:static {
+    position: static;
+  }
+
+  .sm\:fixed {
+    position: fixed;
+  }
+
+  .sm\:absolute {
+    position: absolute;
+  }
+
+  .sm\:relative {
+    position: relative;
+  }
+
+  .sm\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .sm\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .sm\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .sm\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .sm\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .sm\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .sm\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .sm\:top-0 {
+    top: 0;
+  }
+
+  .sm\:right-0 {
+    right: 0;
+  }
+
+  .sm\:bottom-0 {
+    bottom: 0;
+  }
+
+  .sm\:left-0 {
+    left: 0;
+  }
+
+  .sm\:top-auto {
+    top: auto;
+  }
+
+  .sm\:right-auto {
+    right: auto;
+  }
+
+  .sm\:bottom-auto {
+    bottom: auto;
+  }
+
+  .sm\:left-auto {
+    left: auto;
+  }
+
+  .sm\:resize-none {
+    resize: none;
+  }
+
+  .sm\:resize-y {
+    resize: vertical;
+  }
+
+  .sm\:resize-x {
+    resize: horizontal;
+  }
+
+  .sm\:resize {
+    resize: both;
+  }
+
+  .sm\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .sm\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .sm\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .sm\:shadow-none {
+    box-shadow: none;
+  }
+
+  .sm\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .sm\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .sm\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .sm\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .sm\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .sm\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .sm\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .sm\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .sm\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .sm\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .sm\:fill-current {
+    fill: currentColor;
+  }
+
+  .sm\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .sm\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .sm\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .sm\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .sm\:table-auto {
+    table-layout: auto;
+  }
+
+  .sm\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .sm\:text-left {
+    text-align: left;
+  }
+
+  .sm\:text-center {
+    text-align: center;
+  }
+
+  .sm\:text-right {
+    text-align: right;
+  }
+
+  .sm\:text-justify {
+    text-align: justify;
+  }
+
+  .sm\:text-transparent {
+    color: transparent;
+  }
+
+  .sm\:text-current {
+    color: currentColor;
+  }
+
+  .sm\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .sm\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .sm\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .sm\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .sm\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .sm\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .sm\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .sm\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .sm\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .sm\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .sm\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .sm\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .sm\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .sm\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .sm\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .sm\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .sm\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .sm\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .sm\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .sm\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .sm\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .sm\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .sm\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .sm\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .sm\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .sm\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .sm\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .sm\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .sm\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .sm\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .sm\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .sm\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .sm\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .sm\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .sm\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .sm\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .sm\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .sm\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .sm\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .sm\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .sm\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .sm\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .sm\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .sm\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .sm\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .sm\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .sm\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .sm\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .sm\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .sm\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .sm\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .sm\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .sm\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .sm\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .sm\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .sm\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .sm\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .sm\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .sm\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .sm\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .sm\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .sm\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .sm\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .sm\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .sm\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .sm\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .sm\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .sm\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .sm\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .sm\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .sm\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .sm\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .sm\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .sm\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .sm\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .sm\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .sm\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .sm\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .sm\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .sm\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .sm\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .sm\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .sm\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .sm\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .sm\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .sm\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .sm\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .sm\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .sm\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .sm\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .sm\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .sm\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .sm\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .sm\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .sm\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .sm\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .sm\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .sm\:italic {
+    font-style: italic;
+  }
+
+  .sm\:not-italic {
+    font-style: normal;
+  }
+
+  .sm\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .sm\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .sm\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .sm\:normal-case {
+    text-transform: none;
+  }
+
+  .sm\:underline {
+    text-decoration: underline;
+  }
+
+  .sm\:line-through {
+    text-decoration: line-through;
+  }
+
+  .sm\:no-underline {
+    text-decoration: none;
+  }
+
+  .sm\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .sm\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .sm\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .sm\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .sm\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .sm\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .sm\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .sm\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .sm\:ordinal, .sm\:slashed-zero, .sm\:lining-nums, .sm\:oldstyle-nums, .sm\:proportional-nums, .sm\:tabular-nums, .sm\:diagonal-fractions, .sm\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .sm\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .sm\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .sm\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .sm\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .sm\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .sm\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .sm\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .sm\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .sm\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .sm\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .sm\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .sm\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .sm\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .sm\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .sm\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .sm\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .sm\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .sm\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .sm\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .sm\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .sm\:align-top {
+    vertical-align: top;
+  }
+
+  .sm\:align-middle {
+    vertical-align: middle;
+  }
+
+  .sm\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .sm\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .sm\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .sm\:visible {
+    visibility: visible;
+  }
+
+  .sm\:invisible {
+    visibility: hidden;
+  }
+
+  .sm\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .sm\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .sm\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .sm\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .sm\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .sm\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .sm\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .sm\:break-all {
+    word-break: break-all;
+  }
+
+  .sm\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .sm\:w-0 {
+    width: 0;
+  }
+
+  .sm\:w-1 {
+    width: 0.25rem;
+  }
+
+  .sm\:w-2 {
+    width: 0.5rem;
+  }
+
+  .sm\:w-3 {
+    width: 0.75rem;
+  }
+
+  .sm\:w-4 {
+    width: 1rem;
+  }
+
+  .sm\:w-5 {
+    width: 1.25rem;
+  }
+
+  .sm\:w-6 {
+    width: 1.5rem;
+  }
+
+  .sm\:w-8 {
+    width: 2rem;
+  }
+
+  .sm\:w-10 {
+    width: 2.5rem;
+  }
+
+  .sm\:w-12 {
+    width: 3rem;
+  }
+
+  .sm\:w-16 {
+    width: 4rem;
+  }
+
+  .sm\:w-20 {
+    width: 5rem;
+  }
+
+  .sm\:w-24 {
+    width: 6rem;
+  }
+
+  .sm\:w-32 {
+    width: 8rem;
+  }
+
+  .sm\:w-40 {
+    width: 10rem;
+  }
+
+  .sm\:w-48 {
+    width: 12rem;
+  }
+
+  .sm\:w-56 {
+    width: 14rem;
+  }
+
+  .sm\:w-64 {
+    width: 16rem;
+  }
+
+  .sm\:w-auto {
+    width: auto;
+  }
+
+  .sm\:w-px {
+    width: 1px;
+  }
+
+  .sm\:w-1\/2 {
+    width: 50%;
+  }
+
+  .sm\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .sm\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .sm\:w-1\/4 {
+    width: 25%;
+  }
+
+  .sm\:w-2\/4 {
+    width: 50%;
+  }
+
+  .sm\:w-3\/4 {
+    width: 75%;
+  }
+
+  .sm\:w-1\/5 {
+    width: 20%;
+  }
+
+  .sm\:w-2\/5 {
+    width: 40%;
+  }
+
+  .sm\:w-3\/5 {
+    width: 60%;
+  }
+
+  .sm\:w-4\/5 {
+    width: 80%;
+  }
+
+  .sm\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .sm\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .sm\:w-3\/6 {
+    width: 50%;
+  }
+
+  .sm\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .sm\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .sm\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .sm\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .sm\:w-3\/12 {
+    width: 25%;
+  }
+
+  .sm\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .sm\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .sm\:w-6\/12 {
+    width: 50%;
+  }
+
+  .sm\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .sm\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .sm\:w-9\/12 {
+    width: 75%;
+  }
+
+  .sm\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .sm\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .sm\:w-full {
+    width: 100%;
+  }
+
+  .sm\:w-screen {
+    width: 100vw;
+  }
+
+  .sm\:z-0 {
+    z-index: 0;
+  }
+
+  .sm\:z-10 {
+    z-index: 10;
+  }
+
+  .sm\:z-20 {
+    z-index: 20;
+  }
+
+  .sm\:z-30 {
+    z-index: 30;
+  }
+
+  .sm\:z-40 {
+    z-index: 40;
+  }
+
+  .sm\:z-50 {
+    z-index: 50;
+  }
+
+  .sm\:z-auto {
+    z-index: auto;
+  }
+
+  .sm\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .sm\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .sm\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .sm\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .sm\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .sm\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .sm\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .sm\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .sm\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .sm\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .sm\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .sm\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .sm\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .sm\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .sm\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .sm\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .sm\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .sm\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .sm\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .sm\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .sm\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .sm\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .sm\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .sm\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .sm\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .sm\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .sm\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .sm\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .sm\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .sm\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .sm\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .sm\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .sm\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .sm\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .sm\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .sm\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .sm\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .sm\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .sm\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .sm\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .sm\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .sm\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .sm\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .sm\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .sm\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .sm\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .sm\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .sm\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .sm\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .sm\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .sm\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .sm\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .sm\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .sm\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .sm\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .sm\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .sm\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .sm\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .sm\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .sm\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .sm\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .sm\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .sm\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .sm\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .sm\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .sm\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .sm\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .sm\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .sm\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .sm\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .sm\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .sm\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .sm\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .sm\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .sm\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .sm\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .sm\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .sm\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .sm\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .sm\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .sm\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .sm\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .sm\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .sm\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .sm\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .sm\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .sm\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .sm\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .sm\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .sm\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .sm\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .sm\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .sm\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .sm\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .sm\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .sm\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .sm\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .sm\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .sm\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .sm\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .sm\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .sm\:col-auto {
+    grid-column: auto;
+  }
+
+  .sm\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .sm\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .sm\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .sm\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .sm\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .sm\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .sm\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .sm\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .sm\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .sm\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .sm\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .sm\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .sm\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .sm\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .sm\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .sm\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .sm\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .sm\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .sm\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .sm\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .sm\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .sm\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .sm\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .sm\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .sm\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .sm\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .sm\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .sm\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .sm\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .sm\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .sm\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .sm\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .sm\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .sm\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .sm\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .sm\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .sm\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .sm\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .sm\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .sm\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .sm\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .sm\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .sm\:row-auto {
+    grid-row: auto;
+  }
+
+  .sm\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .sm\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .sm\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .sm\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .sm\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .sm\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .sm\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .sm\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .sm\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .sm\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .sm\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .sm\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .sm\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .sm\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .sm\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .sm\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .sm\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .sm\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .sm\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .sm\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .sm\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .sm\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .sm\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .sm\:transform-none {
+    transform: none;
+  }
+
+  .sm\:origin-center {
+    transform-origin: center;
+  }
+
+  .sm\:origin-top {
+    transform-origin: top;
+  }
+
+  .sm\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .sm\:origin-right {
+    transform-origin: right;
+  }
+
+  .sm\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .sm\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .sm\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .sm\:origin-left {
+    transform-origin: left;
+  }
+
+  .sm\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .sm\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .sm\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .sm\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .sm\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .sm\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .sm\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .sm\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .sm\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .sm\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .sm\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .sm\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .sm\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .sm\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .sm\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .sm\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .sm\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .sm\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .sm\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .sm\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .sm\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .sm\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .sm\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .sm\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .sm\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .sm\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .sm\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .sm\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .sm\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .sm\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .sm\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .sm\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .sm\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .sm\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .sm\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .sm\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .sm\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .sm\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .sm\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .sm\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .sm\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .sm\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .sm\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .sm\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .sm\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .sm\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .sm\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .sm\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .sm\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .sm\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .sm\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .sm\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .sm\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .sm\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .sm\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .sm\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .sm\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .sm\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .sm\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .sm\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .sm\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .sm\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .sm\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .sm\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .sm\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .sm\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .sm\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .sm\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .sm\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .sm\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .sm\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .sm\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .sm\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .sm\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .sm\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .sm\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .sm\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .sm\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .sm\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .sm\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .sm\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .sm\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .sm\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .sm\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .sm\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .sm\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .sm\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .sm\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .sm\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .sm\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .sm\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .sm\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .sm\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .sm\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .sm\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .sm\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .sm\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .sm\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .sm\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .sm\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .sm\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .sm\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .sm\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .sm\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .sm\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .sm\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .sm\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .sm\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .sm\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .sm\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .sm\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .sm\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .sm\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .sm\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .sm\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .sm\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .sm\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .sm\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .sm\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .sm\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .sm\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .sm\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .sm\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .sm\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .sm\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .sm\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .sm\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .sm\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .sm\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .sm\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .sm\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .sm\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .sm\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .sm\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .sm\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .sm\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .sm\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .sm\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .sm\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .sm\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .sm\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .sm\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .sm\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .sm\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .sm\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .sm\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .sm\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .sm\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .sm\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .sm\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .sm\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .sm\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .sm\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .sm\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .sm\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .sm\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .sm\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .sm\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .sm\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .sm\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .sm\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .sm\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .sm\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .sm\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .sm\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .sm\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .sm\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .sm\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .sm\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .sm\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .sm\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .sm\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .sm\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .sm\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .sm\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .sm\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .sm\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .sm\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .sm\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .sm\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .sm\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .sm\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .sm\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .sm\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .sm\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .sm\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .sm\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .sm\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .sm\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .sm\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .sm\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .sm\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .sm\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .sm\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .sm\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .sm\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .sm\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .sm\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .sm\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .sm\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .sm\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .sm\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .sm\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .sm\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .sm\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .sm\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .sm\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .sm\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .sm\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .sm\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .sm\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .sm\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .sm\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .sm\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .sm\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .sm\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .sm\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .sm\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .sm\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .sm\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .sm\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .sm\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .sm\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .sm\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .sm\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .sm\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .sm\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .sm\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .sm\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .sm\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .sm\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .sm\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .sm\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .sm\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .sm\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .sm\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .sm\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .sm\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .sm\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .sm\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .sm\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .sm\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .sm\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .sm\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .sm\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .sm\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .sm\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .sm\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .sm\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .sm\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .sm\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .sm\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .sm\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .sm\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .sm\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .sm\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .sm\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .sm\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .sm\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .sm\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .sm\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .sm\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .sm\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .sm\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .sm\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .sm\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .sm\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .sm\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .sm\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .sm\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .sm\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .sm\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .sm\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .sm\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .sm\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .sm\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .sm\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .sm\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .sm\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .sm\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .sm\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .sm\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .sm\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .sm\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .sm\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .sm\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .sm\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .sm\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .sm\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .sm\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .sm\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .sm\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .sm\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .sm\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .sm\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .sm\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .sm\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .sm\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .sm\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .sm\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .sm\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .sm\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .sm\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .sm\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .sm\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .sm\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .sm\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .sm\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .sm\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .sm\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .sm\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .sm\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .sm\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .sm\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .sm\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .sm\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .sm\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .sm\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .sm\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .sm\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .sm\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .sm\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .sm\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .sm\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .sm\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .sm\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .sm\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .sm\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .sm\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .sm\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .sm\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .sm\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .sm\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .sm\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .sm\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .sm\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .sm\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .sm\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .sm\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .sm\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .sm\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .sm\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .sm\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .sm\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .sm\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .sm\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .sm\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .sm\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .sm\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .sm\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .sm\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .sm\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .sm\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .sm\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .sm\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .sm\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .sm\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .sm\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .sm\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .sm\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .sm\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .sm\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .sm\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .sm\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .sm\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .sm\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .sm\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .sm\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .sm\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .sm\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .sm\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .sm\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .sm\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .sm\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .sm\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .sm\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .sm\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .sm\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .sm\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .sm\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .sm\:transition-none {
+    transition-property: none;
+  }
+
+  .sm\:transition-all {
+    transition-property: all;
+  }
+
+  .sm\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .sm\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .sm\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .sm\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .sm\:transition-transform {
+    transition-property: transform;
+  }
+
+  .sm\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .sm\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .sm\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .sm\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .sm\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .sm\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .sm\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .sm\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .sm\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .sm\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .sm\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .sm\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .sm\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .sm\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .sm\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .sm\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .sm\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .sm\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .sm\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .sm\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .sm\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .sm\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .sm\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .sm\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .sm\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
+
+@media (min-width: 768px) {
+  .md\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .md\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .md\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .md\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .md\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .md\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .md\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .md\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .md\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .md\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .md\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .md\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .md\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .md\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .md\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .md\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .md\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .md\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .md\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .md\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .md\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .md\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .md\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .md\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .md\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .md\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .md\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .md\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .md\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .md\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .md\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .md\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .md\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .md\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .md\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .md\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .md\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .md\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .md\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .md\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .md\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .md\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .md\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .md\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .md\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .md\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .md\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .md\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .md\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .md\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .md\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .md\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .md\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .md\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .md\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .md\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .md\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .md\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .md\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .md\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .md\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .md\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .md\:bg-local {
+    background-attachment: local;
+  }
+
+  .md\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .md\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .md\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .md\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .md\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .md\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .md\:bg-current {
+    background-color: currentColor;
+  }
+
+  .md\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .md\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .md\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .md\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .md\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .md\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .md\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .md\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .md\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .md\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .md\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .md\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .md\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .md\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .md\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .md\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .md\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .md\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .md\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .md\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .md\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .md\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .md\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .md\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .md\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .md\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .md\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .md\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .md\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .md\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .md\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .md\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .md\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .md\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .md\:bg-none {
+    background-image: none;
+  }
+
+  .md\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .md\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .md\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .md\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .md\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .md\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .md\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .md\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .md\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .md\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .md\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .md\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .md\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .md\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .md\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .md\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .md\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .md\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .md\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .md\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .md\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .md\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .md\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .md\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .md\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .md\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .md\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .md\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .md\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .md\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .md\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .md\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .md\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .md\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .md\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .md\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .md\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .md\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .md\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .md\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .md\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .md\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .md\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .md\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .md\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .md\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .md\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .md\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .md\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .md\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .md\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .md\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .md\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .md\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .md\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .md\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .md\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .md\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .md\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .md\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .md\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .md\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .md\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .md\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .md\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .md\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .md\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .md\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .md\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .md\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .md\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .md\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .md\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .md\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .md\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .md\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .md\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .md\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .md\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .md\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .md\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .md\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .md\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .md\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .md\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .md\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .md\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .md\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .md\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .md\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .md\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .md\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .md\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .md\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .md\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .md\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .md\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .md\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .md\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .md\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .md\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .md\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .md\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .md\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .md\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .md\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .md\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .md\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .md\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .md\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .md\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .md\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .md\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .md\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .md\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .md\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .md\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .md\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .md\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .md\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .md\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .md\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .md\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .md\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .md\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .md\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .md\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .md\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .md\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .md\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .md\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .md\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .md\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .md\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .md\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .md\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .md\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .md\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .md\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .md\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .md\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .md\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .md\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .md\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .md\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .md\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .md\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .md\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .md\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .md\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .md\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .md\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .md\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .md\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .md\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .md\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .md\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .md\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .md\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .md\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .md\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .md\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .md\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .md\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .md\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .md\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .md\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .md\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .md\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .md\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .md\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .md\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .md\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .md\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .md\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .md\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .md\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .md\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .md\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .md\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .md\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .md\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .md\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .md\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .md\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .md\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .md\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .md\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .md\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .md\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .md\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .md\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .md\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .md\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .md\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .md\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .md\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .md\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .md\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .md\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .md\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .md\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .md\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .md\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .md\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .md\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .md\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .md\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .md\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .md\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .md\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .md\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .md\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .md\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .md\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .md\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .md\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .md\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .md\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .md\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .md\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .md\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .md\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .md\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .md\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .md\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .md\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .md\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .md\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .md\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .md\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .md\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .md\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .md\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .md\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .md\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .md\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .md\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .md\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .md\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .md\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .md\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .md\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .md\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .md\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .md\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .md\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .md\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .md\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .md\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .md\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .md\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .md\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .md\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .md\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .md\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .md\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .md\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .md\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .md\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .md\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .md\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .md\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .md\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .md\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .md\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .md\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .md\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .md\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .md\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .md\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .md\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .md\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .md\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .md\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .md\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .md\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .md\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .md\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .md\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .md\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .md\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .md\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .md\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .md\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .md\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .md\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .md\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .md\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .md\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .md\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .md\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .md\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .md\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .md\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .md\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .md\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .md\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .md\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .md\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .md\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .md\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .md\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .md\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .md\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .md\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .md\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .md\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .md\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .md\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .md\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .md\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .md\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .md\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .md\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .md\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .md\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .md\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .md\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .md\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .md\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .md\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .md\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .md\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .md\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .md\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .md\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .md\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .md\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .md\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .md\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .md\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .md\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .md\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .md\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .md\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .md\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .md\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .md\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .md\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .md\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .md\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .md\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .md\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .md\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .md\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .md\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .md\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .md\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .md\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .md\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .md\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .md\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .md\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .md\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .md\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .md\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .md\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .md\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .md\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .md\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .md\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .md\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .md\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .md\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .md\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .md\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .md\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .md\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .md\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .md\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .md\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .md\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .md\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .md\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .md\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .md\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .md\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .md\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .md\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .md\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .md\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .md\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .md\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .md\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .md\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .md\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .md\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .md\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .md\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .md\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .md\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .md\:bg-center {
+    background-position: center;
+  }
+
+  .md\:bg-left {
+    background-position: left;
+  }
+
+  .md\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .md\:bg-left-top {
+    background-position: left top;
+  }
+
+  .md\:bg-right {
+    background-position: right;
+  }
+
+  .md\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .md\:bg-right-top {
+    background-position: right top;
+  }
+
+  .md\:bg-top {
+    background-position: top;
+  }
+
+  .md\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .md\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .md\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .md\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .md\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .md\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .md\:bg-auto {
+    background-size: auto;
+  }
+
+  .md\:bg-cover {
+    background-size: cover;
+  }
+
+  .md\:bg-contain {
+    background-size: contain;
+  }
+
+  .md\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .md\:border-separate {
+    border-collapse: separate;
+  }
+
+  .md\:border-transparent {
+    border-color: transparent;
+  }
+
+  .md\:border-current {
+    border-color: currentColor;
+  }
+
+  .md\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .md\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .md\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .md\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .md\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .md\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .md\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .md\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .md\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .md\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .md\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .md\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .md\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .md\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .md\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .md\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .md\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .md\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .md\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .md\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .md\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .md\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .md\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .md\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .md\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .md\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .md\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .md\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .md\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .md\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .md\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .md\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .md\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .md\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .md\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .md\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .md\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .md\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .md\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .md\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .md\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .md\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .md\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .md\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .md\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .md\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .md\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .md\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .md\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .md\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .md\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .md\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .md\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .md\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .md\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .md\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .md\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .md\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .md\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .md\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .md\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .md\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .md\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .md\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .md\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .md\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .md\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .md\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .md\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .md\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .md\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .md\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .md\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .md\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .md\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .md\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .md\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .md\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .md\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .md\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .md\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .md\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .md\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .md\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .md\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .md\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .md\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .md\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .md\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .md\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .md\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .md\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .md\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .md\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .md\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .md\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .md\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .md\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .md\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .md\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .md\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .md\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .md\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .md\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .md\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .md\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .md\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .md\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .md\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .md\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .md\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .md\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .md\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .md\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .md\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .md\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .md\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .md\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .md\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .md\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .md\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .md\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .md\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .md\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .md\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .md\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .md\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .md\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .md\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .md\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .md\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .md\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .md\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .md\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .md\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .md\:rounded-none {
+    border-radius: 0;
+  }
+
+  .md\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .md\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .md\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .md\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .md\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .md\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .md\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .md\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .md\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .md\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .md\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .md\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .md\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .md\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .md\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .md\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .md\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .md\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .md\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .md\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .md\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .md\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .md\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .md\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .md\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .md\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .md\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .md\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .md\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .md\:border-solid {
+    border-style: solid;
+  }
+
+  .md\:border-dashed {
+    border-style: dashed;
+  }
+
+  .md\:border-dotted {
+    border-style: dotted;
+  }
+
+  .md\:border-double {
+    border-style: double;
+  }
+
+  .md\:border-none {
+    border-style: none;
+  }
+
+  .md\:border-0 {
+    border-width: 0;
+  }
+
+  .md\:border-2 {
+    border-width: 2px;
+  }
+
+  .md\:border-4 {
+    border-width: 4px;
+  }
+
+  .md\:border-8 {
+    border-width: 8px;
+  }
+
+  .md\:border {
+    border-width: 1px;
+  }
+
+  .md\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .md\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .md\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .md\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .md\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .md\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .md\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .md\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .md\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .md\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .md\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .md\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .md\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .md\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .md\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .md\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .md\:border-t {
+    border-top-width: 1px;
+  }
+
+  .md\:border-r {
+    border-right-width: 1px;
+  }
+
+  .md\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .md\:border-l {
+    border-left-width: 1px;
+  }
+
+  .md\:box-border {
+    box-sizing: border-box;
+  }
+
+  .md\:box-content {
+    box-sizing: content-box;
+  }
+
+  .md\:cursor-auto {
+    cursor: auto;
+  }
+
+  .md\:cursor-default {
+    cursor: default;
+  }
+
+  .md\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .md\:cursor-wait {
+    cursor: wait;
+  }
+
+  .md\:cursor-text {
+    cursor: text;
+  }
+
+  .md\:cursor-move {
+    cursor: move;
+  }
+
+  .md\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .md\:block {
+    display: block;
+  }
+
+  .md\:inline-block {
+    display: inline-block;
+  }
+
+  .md\:inline {
+    display: inline;
+  }
+
+  .md\:flex {
+    display: flex;
+  }
+
+  .md\:inline-flex {
+    display: inline-flex;
+  }
+
+  .md\:table {
+    display: table;
+  }
+
+  .md\:table-caption {
+    display: table-caption;
+  }
+
+  .md\:table-cell {
+    display: table-cell;
+  }
+
+  .md\:table-column {
+    display: table-column;
+  }
+
+  .md\:table-column-group {
+    display: table-column-group;
+  }
+
+  .md\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .md\:table-header-group {
+    display: table-header-group;
+  }
+
+  .md\:table-row-group {
+    display: table-row-group;
+  }
+
+  .md\:table-row {
+    display: table-row;
+  }
+
+  .md\:flow-root {
+    display: flow-root;
+  }
+
+  .md\:grid {
+    display: grid;
+  }
+
+  .md\:inline-grid {
+    display: inline-grid;
+  }
+
+  .md\:contents {
+    display: contents;
+  }
+
+  .md\:hidden {
+    display: none;
+  }
+
+  .md\:flex-row {
+    flex-direction: row;
+  }
+
+  .md\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .md\:flex-col {
+    flex-direction: column;
+  }
+
+  .md\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .md\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .md\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .md\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .md\:place-items-auto {
+    place-items: auto;
+  }
+
+  .md\:place-items-start {
+    place-items: start;
+  }
+
+  .md\:place-items-end {
+    place-items: end;
+  }
+
+  .md\:place-items-center {
+    place-items: center;
+  }
+
+  .md\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .md\:place-content-center {
+    place-content: center;
+  }
+
+  .md\:place-content-start {
+    place-content: start;
+  }
+
+  .md\:place-content-end {
+    place-content: end;
+  }
+
+  .md\:place-content-between {
+    place-content: space-between;
+  }
+
+  .md\:place-content-around {
+    place-content: space-around;
+  }
+
+  .md\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .md\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .md\:place-self-auto {
+    place-self: auto;
+  }
+
+  .md\:place-self-start {
+    place-self: start;
+  }
+
+  .md\:place-self-end {
+    place-self: end;
+  }
+
+  .md\:place-self-center {
+    place-self: center;
+  }
+
+  .md\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .md\:items-start {
+    align-items: flex-start;
+  }
+
+  .md\:items-end {
+    align-items: flex-end;
+  }
+
+  .md\:items-center {
+    align-items: center;
+  }
+
+  .md\:items-baseline {
+    align-items: baseline;
+  }
+
+  .md\:items-stretch {
+    align-items: stretch;
+  }
+
+  .md\:content-center {
+    align-content: center;
+  }
+
+  .md\:content-start {
+    align-content: flex-start;
+  }
+
+  .md\:content-end {
+    align-content: flex-end;
+  }
+
+  .md\:content-between {
+    align-content: space-between;
+  }
+
+  .md\:content-around {
+    align-content: space-around;
+  }
+
+  .md\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .md\:self-auto {
+    align-self: auto;
+  }
+
+  .md\:self-start {
+    align-self: flex-start;
+  }
+
+  .md\:self-end {
+    align-self: flex-end;
+  }
+
+  .md\:self-center {
+    align-self: center;
+  }
+
+  .md\:self-stretch {
+    align-self: stretch;
+  }
+
+  .md\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .md\:justify-items-start {
+    justify-items: start;
+  }
+
+  .md\:justify-items-end {
+    justify-items: end;
+  }
+
+  .md\:justify-items-center {
+    justify-items: center;
+  }
+
+  .md\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .md\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .md\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .md\:justify-center {
+    justify-content: center;
+  }
+
+  .md\:justify-between {
+    justify-content: space-between;
+  }
+
+  .md\:justify-around {
+    justify-content: space-around;
+  }
+
+  .md\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .md\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .md\:justify-self-start {
+    justify-self: start;
+  }
+
+  .md\:justify-self-end {
+    justify-self: end;
+  }
+
+  .md\:justify-self-center {
+    justify-self: center;
+  }
+
+  .md\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .md\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .md\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .md\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .md\:flex-none {
+    flex: none;
+  }
+
+  .md\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .md\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .md\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .md\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .md\:order-1 {
+    order: 1;
+  }
+
+  .md\:order-2 {
+    order: 2;
+  }
+
+  .md\:order-3 {
+    order: 3;
+  }
+
+  .md\:order-4 {
+    order: 4;
+  }
+
+  .md\:order-5 {
+    order: 5;
+  }
+
+  .md\:order-6 {
+    order: 6;
+  }
+
+  .md\:order-7 {
+    order: 7;
+  }
+
+  .md\:order-8 {
+    order: 8;
+  }
+
+  .md\:order-9 {
+    order: 9;
+  }
+
+  .md\:order-10 {
+    order: 10;
+  }
+
+  .md\:order-11 {
+    order: 11;
+  }
+
+  .md\:order-12 {
+    order: 12;
+  }
+
+  .md\:order-first {
+    order: -9999;
+  }
+
+  .md\:order-last {
+    order: 9999;
+  }
+
+  .md\:order-none {
+    order: 0;
+  }
+
+  .md\:float-right {
+    float: right;
+  }
+
+  .md\:float-left {
+    float: left;
+  }
+
+  .md\:float-none {
+    float: none;
+  }
+
+  .md\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .md\:clear-left {
+    clear: left;
+  }
+
+  .md\:clear-right {
+    clear: right;
+  }
+
+  .md\:clear-both {
+    clear: both;
+  }
+
+  .md\:clear-none {
+    clear: none;
+  }
+
+  .md\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .md\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .md\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .md\:font-hairline {
+    font-weight: 100;
+  }
+
+  .md\:font-thin {
+    font-weight: 200;
+  }
+
+  .md\:font-light {
+    font-weight: 300;
+  }
+
+  .md\:font-normal {
+    font-weight: 400;
+  }
+
+  .md\:font-medium {
+    font-weight: 500;
+  }
+
+  .md\:font-semibold {
+    font-weight: 600;
+  }
+
+  .md\:font-bold {
+    font-weight: 700;
+  }
+
+  .md\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .md\:font-black {
+    font-weight: 900;
+  }
+
+  .md\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .md\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .md\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .md\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .md\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .md\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .md\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .md\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .md\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .md\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .md\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .md\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .md\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .md\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .md\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .md\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .md\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .md\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .md\:h-0 {
+    height: 0;
+  }
+
+  .md\:h-1 {
+    height: 0.25rem;
+  }
+
+  .md\:h-2 {
+    height: 0.5rem;
+  }
+
+  .md\:h-3 {
+    height: 0.75rem;
+  }
+
+  .md\:h-4 {
+    height: 1rem;
+  }
+
+  .md\:h-5 {
+    height: 1.25rem;
+  }
+
+  .md\:h-6 {
+    height: 1.5rem;
+  }
+
+  .md\:h-8 {
+    height: 2rem;
+  }
+
+  .md\:h-10 {
+    height: 2.5rem;
+  }
+
+  .md\:h-12 {
+    height: 3rem;
+  }
+
+  .md\:h-16 {
+    height: 4rem;
+  }
+
+  .md\:h-20 {
+    height: 5rem;
+  }
+
+  .md\:h-24 {
+    height: 6rem;
+  }
+
+  .md\:h-32 {
+    height: 8rem;
+  }
+
+  .md\:h-40 {
+    height: 10rem;
+  }
+
+  .md\:h-48 {
+    height: 12rem;
+  }
+
+  .md\:h-56 {
+    height: 14rem;
+  }
+
+  .md\:h-64 {
+    height: 16rem;
+  }
+
+  .md\:h-auto {
+    height: auto;
+  }
+
+  .md\:h-px {
+    height: 1px;
+  }
+
+  .md\:h-full {
+    height: 100%;
+  }
+
+  .md\:h-screen {
+    height: 100vh;
+  }
+
+  .md\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .md\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .md\:text-base {
+    font-size: 1rem;
+  }
+
+  .md\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .md\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .md\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .md\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .md\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .md\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .md\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .md\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .md\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .md\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .md\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .md\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .md\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .md\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .md\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .md\:leading-none {
+    line-height: 1;
+  }
+
+  .md\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .md\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .md\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .md\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .md\:leading-loose {
+    line-height: 2;
+  }
+
+  .md\:list-inside {
+    list-style-position: inside;
+  }
+
+  .md\:list-outside {
+    list-style-position: outside;
+  }
+
+  .md\:list-none {
+    list-style-type: none;
+  }
+
+  .md\:list-disc {
+    list-style-type: disc;
+  }
+
+  .md\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .md\:m-0 {
+    margin: 0;
+  }
+
+  .md\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .md\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .md\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .md\:m-4 {
+    margin: 1rem;
+  }
+
+  .md\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .md\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .md\:m-8 {
+    margin: 2rem;
+  }
+
+  .md\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .md\:m-12 {
+    margin: 3rem;
+  }
+
+  .md\:m-16 {
+    margin: 4rem;
+  }
+
+  .md\:m-20 {
+    margin: 5rem;
+  }
+
+  .md\:m-24 {
+    margin: 6rem;
+  }
+
+  .md\:m-32 {
+    margin: 8rem;
+  }
+
+  .md\:m-40 {
+    margin: 10rem;
+  }
+
+  .md\:m-48 {
+    margin: 12rem;
+  }
+
+  .md\:m-56 {
+    margin: 14rem;
+  }
+
+  .md\:m-64 {
+    margin: 16rem;
+  }
+
+  .md\:m-auto {
+    margin: auto;
+  }
+
+  .md\:m-px {
+    margin: 1px;
+  }
+
+  .md\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .md\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .md\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .md\:-m-4 {
+    margin: -1rem;
+  }
+
+  .md\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .md\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .md\:-m-8 {
+    margin: -2rem;
+  }
+
+  .md\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .md\:-m-12 {
+    margin: -3rem;
+  }
+
+  .md\:-m-16 {
+    margin: -4rem;
+  }
+
+  .md\:-m-20 {
+    margin: -5rem;
+  }
+
+  .md\:-m-24 {
+    margin: -6rem;
+  }
+
+  .md\:-m-32 {
+    margin: -8rem;
+  }
+
+  .md\:-m-40 {
+    margin: -10rem;
+  }
+
+  .md\:-m-48 {
+    margin: -12rem;
+  }
+
+  .md\:-m-56 {
+    margin: -14rem;
+  }
+
+  .md\:-m-64 {
+    margin: -16rem;
+  }
+
+  .md\:-m-px {
+    margin: -1px;
+  }
+
+  .md\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .md\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .md\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .md\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .md\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .md\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .md\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .md\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .md\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .md\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .md\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .md\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .md\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .md\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .md\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .md\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .md\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .md\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .md\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .md\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .md\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .md\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .md\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .md\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .md\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .md\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .md\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .md\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .md\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .md\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .md\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .md\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .md\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .md\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .md\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .md\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .md\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .md\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .md\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .md\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .md\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .md\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .md\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .md\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .md\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .md\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .md\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .md\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .md\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .md\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .md\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .md\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .md\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .md\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .md\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .md\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .md\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .md\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .md\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .md\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .md\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .md\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .md\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .md\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .md\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .md\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .md\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .md\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .md\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .md\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .md\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .md\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .md\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .md\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .md\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .md\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .md\:mt-0 {
+    margin-top: 0;
+  }
+
+  .md\:mr-0 {
+    margin-right: 0;
+  }
+
+  .md\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .md\:ml-0 {
+    margin-left: 0;
+  }
+
+  .md\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .md\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .md\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .md\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .md\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .md\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .md\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .md\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .md\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .md\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .md\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .md\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .md\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .md\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .md\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .md\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .md\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .md\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .md\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .md\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .md\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .md\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .md\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .md\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .md\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .md\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .md\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .md\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .md\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .md\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .md\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .md\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .md\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .md\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .md\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .md\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .md\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .md\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .md\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .md\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .md\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .md\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .md\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .md\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .md\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .md\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .md\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .md\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .md\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .md\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .md\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .md\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .md\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .md\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .md\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .md\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .md\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .md\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .md\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .md\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .md\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .md\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .md\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .md\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .md\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .md\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .md\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .md\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .md\:mt-auto {
+    margin-top: auto;
+  }
+
+  .md\:mr-auto {
+    margin-right: auto;
+  }
+
+  .md\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .md\:ml-auto {
+    margin-left: auto;
+  }
+
+  .md\:mt-px {
+    margin-top: 1px;
+  }
+
+  .md\:mr-px {
+    margin-right: 1px;
+  }
+
+  .md\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .md\:ml-px {
+    margin-left: 1px;
+  }
+
+  .md\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .md\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .md\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .md\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .md\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .md\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .md\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .md\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .md\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .md\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .md\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .md\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .md\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .md\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .md\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .md\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .md\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .md\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .md\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .md\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .md\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .md\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .md\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .md\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .md\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .md\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .md\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .md\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .md\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .md\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .md\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .md\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .md\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .md\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .md\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .md\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .md\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .md\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .md\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .md\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .md\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .md\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .md\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .md\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .md\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .md\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .md\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .md\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .md\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .md\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .md\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .md\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .md\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .md\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .md\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .md\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .md\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .md\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .md\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .md\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .md\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .md\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .md\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .md\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .md\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .md\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .md\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .md\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .md\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .md\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .md\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .md\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .md\:max-h-full {
+    max-height: 100%;
+  }
+
+  .md\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .md\:max-w-none {
+    max-width: none;
+  }
+
+  .md\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .md\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .md\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .md\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .md\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .md\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .md\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .md\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .md\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .md\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .md\:max-w-full {
+    max-width: 100%;
+  }
+
+  .md\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .md\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .md\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .md\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .md\:min-h-0 {
+    min-height: 0;
+  }
+
+  .md\:min-h-full {
+    min-height: 100%;
+  }
+
+  .md\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .md\:min-w-0 {
+    min-width: 0;
+  }
+
+  .md\:min-w-full {
+    min-width: 100%;
+  }
+
+  .md\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .md\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .md\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .md\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .md\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .md\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .md\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .md\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .md\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .md\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .md\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .md\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .md\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .md\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .md\:opacity-0 {
+    opacity: 0;
+  }
+
+  .md\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .md\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .md\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .md\:opacity-100 {
+    opacity: 1;
+  }
+
+  .md\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .md\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .md\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .md\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .md\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .md\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .md\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .md\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .md\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .md\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .md\:outline-none {
+    outline: 0;
+  }
+
+  .md\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .md\:overflow-auto {
+    overflow: auto;
+  }
+
+  .md\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .md\:overflow-visible {
+    overflow: visible;
+  }
+
+  .md\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .md\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .md\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .md\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .md\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .md\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .md\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .md\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .md\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .md\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .md\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .md\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .md\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .md\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .md\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .md\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .md\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .md\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .md\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .md\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .md\:p-0 {
+    padding: 0;
+  }
+
+  .md\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .md\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .md\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .md\:p-4 {
+    padding: 1rem;
+  }
+
+  .md\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .md\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .md\:p-8 {
+    padding: 2rem;
+  }
+
+  .md\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .md\:p-12 {
+    padding: 3rem;
+  }
+
+  .md\:p-16 {
+    padding: 4rem;
+  }
+
+  .md\:p-20 {
+    padding: 5rem;
+  }
+
+  .md\:p-24 {
+    padding: 6rem;
+  }
+
+  .md\:p-32 {
+    padding: 8rem;
+  }
+
+  .md\:p-40 {
+    padding: 10rem;
+  }
+
+  .md\:p-48 {
+    padding: 12rem;
+  }
+
+  .md\:p-56 {
+    padding: 14rem;
+  }
+
+  .md\:p-64 {
+    padding: 16rem;
+  }
+
+  .md\:p-px {
+    padding: 1px;
+  }
+
+  .md\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .md\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .md\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .md\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .md\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .md\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .md\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .md\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .md\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .md\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .md\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .md\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .md\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .md\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .md\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .md\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .md\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .md\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .md\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .md\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .md\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .md\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .md\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .md\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .md\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .md\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .md\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .md\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .md\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .md\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .md\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .md\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .md\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .md\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .md\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .md\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .md\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .md\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .md\:pt-0 {
+    padding-top: 0;
+  }
+
+  .md\:pr-0 {
+    padding-right: 0;
+  }
+
+  .md\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .md\:pl-0 {
+    padding-left: 0;
+  }
+
+  .md\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .md\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .md\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .md\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .md\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .md\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .md\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .md\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .md\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .md\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .md\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .md\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .md\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .md\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .md\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .md\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .md\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .md\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .md\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .md\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .md\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .md\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .md\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .md\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .md\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .md\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .md\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .md\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .md\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .md\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .md\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .md\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .md\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .md\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .md\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .md\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .md\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .md\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .md\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .md\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .md\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .md\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .md\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .md\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .md\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .md\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .md\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .md\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .md\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .md\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .md\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .md\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .md\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .md\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .md\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .md\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .md\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .md\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .md\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .md\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .md\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .md\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .md\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .md\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .md\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .md\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .md\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .md\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .md\:pt-px {
+    padding-top: 1px;
+  }
+
+  .md\:pr-px {
+    padding-right: 1px;
+  }
+
+  .md\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .md\:pl-px {
+    padding-left: 1px;
+  }
+
+  .md\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .md\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .md\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .md\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .md\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .md\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .md\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .md\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .md\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .md\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .md\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .md\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .md\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .md\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .md\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .md\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .md\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .md\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .md\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .md\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .md\:static {
+    position: static;
+  }
+
+  .md\:fixed {
+    position: fixed;
+  }
+
+  .md\:absolute {
+    position: absolute;
+  }
+
+  .md\:relative {
+    position: relative;
+  }
+
+  .md\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .md\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .md\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .md\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .md\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .md\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .md\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .md\:top-0 {
+    top: 0;
+  }
+
+  .md\:right-0 {
+    right: 0;
+  }
+
+  .md\:bottom-0 {
+    bottom: 0;
+  }
+
+  .md\:left-0 {
+    left: 0;
+  }
+
+  .md\:top-auto {
+    top: auto;
+  }
+
+  .md\:right-auto {
+    right: auto;
+  }
+
+  .md\:bottom-auto {
+    bottom: auto;
+  }
+
+  .md\:left-auto {
+    left: auto;
+  }
+
+  .md\:resize-none {
+    resize: none;
+  }
+
+  .md\:resize-y {
+    resize: vertical;
+  }
+
+  .md\:resize-x {
+    resize: horizontal;
+  }
+
+  .md\:resize {
+    resize: both;
+  }
+
+  .md\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .md\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .md\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .md\:shadow-none {
+    box-shadow: none;
+  }
+
+  .md\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .md\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .md\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .md\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .md\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .md\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .md\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .md\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .md\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .md\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .md\:fill-current {
+    fill: currentColor;
+  }
+
+  .md\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .md\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .md\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .md\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .md\:table-auto {
+    table-layout: auto;
+  }
+
+  .md\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .md\:text-left {
+    text-align: left;
+  }
+
+  .md\:text-center {
+    text-align: center;
+  }
+
+  .md\:text-right {
+    text-align: right;
+  }
+
+  .md\:text-justify {
+    text-align: justify;
+  }
+
+  .md\:text-transparent {
+    color: transparent;
+  }
+
+  .md\:text-current {
+    color: currentColor;
+  }
+
+  .md\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .md\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .md\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .md\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .md\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .md\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .md\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .md\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .md\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .md\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .md\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .md\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .md\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .md\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .md\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .md\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .md\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .md\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .md\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .md\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .md\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .md\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .md\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .md\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .md\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .md\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .md\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .md\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .md\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .md\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .md\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .md\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .md\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .md\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .md\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .md\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .md\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .md\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .md\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .md\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .md\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .md\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .md\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .md\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .md\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .md\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .md\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .md\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .md\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .md\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .md\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .md\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .md\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .md\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .md\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .md\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .md\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .md\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .md\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .md\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .md\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .md\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .md\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .md\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .md\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .md\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .md\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .md\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .md\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .md\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .md\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .md\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .md\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .md\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .md\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .md\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .md\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .md\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .md\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .md\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .md\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .md\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .md\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .md\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .md\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .md\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .md\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .md\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .md\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .md\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .md\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .md\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .md\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .md\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .md\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .md\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .md\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .md\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .md\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .md\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .md\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .md\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .md\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .md\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .md\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .md\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .md\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .md\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .md\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .md\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .md\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .md\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .md\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .md\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .md\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .md\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .md\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .md\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .md\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .md\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .md\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .md\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .md\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .md\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .md\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .md\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .md\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .md\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .md\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .md\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .md\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .md\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .md\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .md\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .md\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .md\:italic {
+    font-style: italic;
+  }
+
+  .md\:not-italic {
+    font-style: normal;
+  }
+
+  .md\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .md\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .md\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .md\:normal-case {
+    text-transform: none;
+  }
+
+  .md\:underline {
+    text-decoration: underline;
+  }
+
+  .md\:line-through {
+    text-decoration: line-through;
+  }
+
+  .md\:no-underline {
+    text-decoration: none;
+  }
+
+  .md\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .md\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .md\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .md\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .md\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .md\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .md\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .md\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .md\:ordinal, .md\:slashed-zero, .md\:lining-nums, .md\:oldstyle-nums, .md\:proportional-nums, .md\:tabular-nums, .md\:diagonal-fractions, .md\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .md\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .md\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .md\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .md\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .md\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .md\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .md\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .md\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .md\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .md\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .md\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .md\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .md\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .md\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .md\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .md\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .md\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .md\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .md\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .md\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .md\:align-top {
+    vertical-align: top;
+  }
+
+  .md\:align-middle {
+    vertical-align: middle;
+  }
+
+  .md\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .md\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .md\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .md\:visible {
+    visibility: visible;
+  }
+
+  .md\:invisible {
+    visibility: hidden;
+  }
+
+  .md\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .md\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .md\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .md\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .md\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .md\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .md\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .md\:break-all {
+    word-break: break-all;
+  }
+
+  .md\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .md\:w-0 {
+    width: 0;
+  }
+
+  .md\:w-1 {
+    width: 0.25rem;
+  }
+
+  .md\:w-2 {
+    width: 0.5rem;
+  }
+
+  .md\:w-3 {
+    width: 0.75rem;
+  }
+
+  .md\:w-4 {
+    width: 1rem;
+  }
+
+  .md\:w-5 {
+    width: 1.25rem;
+  }
+
+  .md\:w-6 {
+    width: 1.5rem;
+  }
+
+  .md\:w-8 {
+    width: 2rem;
+  }
+
+  .md\:w-10 {
+    width: 2.5rem;
+  }
+
+  .md\:w-12 {
+    width: 3rem;
+  }
+
+  .md\:w-16 {
+    width: 4rem;
+  }
+
+  .md\:w-20 {
+    width: 5rem;
+  }
+
+  .md\:w-24 {
+    width: 6rem;
+  }
+
+  .md\:w-32 {
+    width: 8rem;
+  }
+
+  .md\:w-40 {
+    width: 10rem;
+  }
+
+  .md\:w-48 {
+    width: 12rem;
+  }
+
+  .md\:w-56 {
+    width: 14rem;
+  }
+
+  .md\:w-64 {
+    width: 16rem;
+  }
+
+  .md\:w-auto {
+    width: auto;
+  }
+
+  .md\:w-px {
+    width: 1px;
+  }
+
+  .md\:w-1\/2 {
+    width: 50%;
+  }
+
+  .md\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .md\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .md\:w-1\/4 {
+    width: 25%;
+  }
+
+  .md\:w-2\/4 {
+    width: 50%;
+  }
+
+  .md\:w-3\/4 {
+    width: 75%;
+  }
+
+  .md\:w-1\/5 {
+    width: 20%;
+  }
+
+  .md\:w-2\/5 {
+    width: 40%;
+  }
+
+  .md\:w-3\/5 {
+    width: 60%;
+  }
+
+  .md\:w-4\/5 {
+    width: 80%;
+  }
+
+  .md\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .md\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .md\:w-3\/6 {
+    width: 50%;
+  }
+
+  .md\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .md\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .md\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .md\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .md\:w-3\/12 {
+    width: 25%;
+  }
+
+  .md\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .md\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .md\:w-6\/12 {
+    width: 50%;
+  }
+
+  .md\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .md\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .md\:w-9\/12 {
+    width: 75%;
+  }
+
+  .md\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .md\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .md\:w-full {
+    width: 100%;
+  }
+
+  .md\:w-screen {
+    width: 100vw;
+  }
+
+  .md\:z-0 {
+    z-index: 0;
+  }
+
+  .md\:z-10 {
+    z-index: 10;
+  }
+
+  .md\:z-20 {
+    z-index: 20;
+  }
+
+  .md\:z-30 {
+    z-index: 30;
+  }
+
+  .md\:z-40 {
+    z-index: 40;
+  }
+
+  .md\:z-50 {
+    z-index: 50;
+  }
+
+  .md\:z-auto {
+    z-index: auto;
+  }
+
+  .md\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .md\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .md\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .md\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .md\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .md\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .md\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .md\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .md\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .md\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .md\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .md\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .md\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .md\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .md\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .md\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .md\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .md\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .md\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .md\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .md\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .md\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .md\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .md\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .md\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .md\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .md\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .md\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .md\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .md\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .md\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .md\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .md\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .md\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .md\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .md\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .md\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .md\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .md\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .md\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .md\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .md\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .md\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .md\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .md\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .md\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .md\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .md\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .md\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .md\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .md\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .md\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .md\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .md\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .md\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .md\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .md\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .md\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .md\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .md\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .md\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .md\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .md\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .md\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .md\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .md\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .md\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .md\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .md\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .md\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .md\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .md\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .md\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .md\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .md\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .md\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .md\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .md\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .md\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .md\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .md\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .md\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .md\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .md\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .md\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .md\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .md\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .md\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .md\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .md\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .md\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .md\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .md\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .md\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .md\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .md\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .md\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .md\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .md\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .md\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .md\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .md\:col-auto {
+    grid-column: auto;
+  }
+
+  .md\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .md\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .md\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .md\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .md\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .md\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .md\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .md\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .md\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .md\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .md\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .md\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .md\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .md\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .md\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .md\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .md\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .md\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .md\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .md\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .md\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .md\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .md\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .md\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .md\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .md\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .md\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .md\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .md\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .md\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .md\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .md\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .md\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .md\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .md\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .md\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .md\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .md\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .md\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .md\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .md\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .md\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .md\:row-auto {
+    grid-row: auto;
+  }
+
+  .md\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .md\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .md\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .md\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .md\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .md\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .md\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .md\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .md\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .md\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .md\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .md\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .md\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .md\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .md\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .md\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .md\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .md\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .md\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .md\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .md\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .md\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .md\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .md\:transform-none {
+    transform: none;
+  }
+
+  .md\:origin-center {
+    transform-origin: center;
+  }
+
+  .md\:origin-top {
+    transform-origin: top;
+  }
+
+  .md\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .md\:origin-right {
+    transform-origin: right;
+  }
+
+  .md\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .md\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .md\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .md\:origin-left {
+    transform-origin: left;
+  }
+
+  .md\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .md\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .md\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .md\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .md\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .md\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .md\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .md\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .md\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .md\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .md\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .md\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .md\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .md\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .md\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .md\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .md\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .md\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .md\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .md\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .md\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .md\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .md\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .md\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .md\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .md\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .md\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .md\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .md\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .md\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .md\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .md\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .md\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .md\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .md\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .md\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .md\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .md\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .md\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .md\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .md\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .md\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .md\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .md\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .md\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .md\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .md\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .md\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .md\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .md\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .md\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .md\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .md\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .md\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .md\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .md\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .md\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .md\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .md\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .md\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .md\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .md\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .md\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .md\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .md\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .md\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .md\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .md\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .md\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .md\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .md\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .md\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .md\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .md\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .md\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .md\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .md\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .md\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .md\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .md\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .md\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .md\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .md\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .md\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .md\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .md\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .md\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .md\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .md\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .md\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .md\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .md\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .md\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .md\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .md\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .md\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .md\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .md\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .md\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .md\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .md\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .md\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .md\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .md\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .md\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .md\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .md\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .md\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .md\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .md\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .md\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .md\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .md\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .md\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .md\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .md\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .md\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .md\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .md\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .md\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .md\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .md\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .md\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .md\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .md\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .md\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .md\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .md\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .md\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .md\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .md\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .md\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .md\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .md\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .md\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .md\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .md\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .md\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .md\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .md\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .md\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .md\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .md\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .md\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .md\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .md\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .md\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .md\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .md\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .md\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .md\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .md\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .md\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .md\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .md\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .md\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .md\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .md\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .md\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .md\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .md\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .md\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .md\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .md\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .md\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .md\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .md\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .md\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .md\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .md\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .md\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .md\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .md\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .md\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .md\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .md\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .md\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .md\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .md\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .md\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .md\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .md\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .md\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .md\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .md\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .md\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .md\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .md\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .md\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .md\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .md\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .md\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .md\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .md\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .md\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .md\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .md\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .md\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .md\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .md\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .md\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .md\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .md\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .md\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .md\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .md\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .md\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .md\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .md\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .md\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .md\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .md\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .md\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .md\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .md\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .md\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .md\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .md\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .md\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .md\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .md\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .md\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .md\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .md\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .md\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .md\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .md\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .md\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .md\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .md\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .md\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .md\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .md\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .md\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .md\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .md\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .md\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .md\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .md\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .md\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .md\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .md\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .md\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .md\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .md\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .md\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .md\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .md\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .md\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .md\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .md\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .md\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .md\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .md\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .md\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .md\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .md\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .md\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .md\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .md\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .md\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .md\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .md\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .md\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .md\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .md\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .md\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .md\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .md\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .md\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .md\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .md\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .md\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .md\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .md\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .md\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .md\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .md\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .md\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .md\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .md\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .md\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .md\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .md\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .md\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .md\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .md\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .md\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .md\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .md\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .md\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .md\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .md\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .md\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .md\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .md\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .md\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .md\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .md\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .md\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .md\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .md\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .md\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .md\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .md\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .md\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .md\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .md\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .md\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .md\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .md\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .md\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .md\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .md\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .md\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .md\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .md\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .md\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .md\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .md\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .md\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .md\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .md\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .md\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .md\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .md\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .md\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .md\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .md\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .md\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .md\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .md\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .md\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .md\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .md\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .md\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .md\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .md\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .md\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .md\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .md\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .md\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .md\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .md\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .md\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .md\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .md\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .md\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .md\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .md\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .md\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .md\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .md\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .md\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .md\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .md\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .md\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .md\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .md\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .md\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .md\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .md\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .md\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .md\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .md\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .md\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .md\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .md\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .md\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .md\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .md\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .md\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .md\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .md\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .md\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .md\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .md\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .md\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .md\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .md\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .md\:transition-none {
+    transition-property: none;
+  }
+
+  .md\:transition-all {
+    transition-property: all;
+  }
+
+  .md\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .md\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .md\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .md\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .md\:transition-transform {
+    transition-property: transform;
+  }
+
+  .md\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .md\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .md\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .md\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .md\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .md\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .md\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .md\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .md\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .md\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .md\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .md\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .md\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .md\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .md\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .md\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .md\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .md\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .md\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .md\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .md\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .md\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .md\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .md\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .md\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
+
+@media (min-width: 1024px) {
+  .lg\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .lg\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .lg\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .lg\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .lg\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .lg\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .lg\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .lg\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .lg\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .lg\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .lg\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .lg\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .lg\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .lg\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .lg\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .lg\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .lg\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .lg\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .lg\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .lg\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .lg\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .lg\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .lg\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .lg\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .lg\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .lg\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .lg\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .lg\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .lg\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .lg\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .lg\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .lg\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .lg\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .lg\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .lg\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .lg\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .lg\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .lg\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .lg\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .lg\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .lg\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .lg\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .lg\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .lg\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .lg\:bg-local {
+    background-attachment: local;
+  }
+
+  .lg\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .lg\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .lg\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .lg\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .lg\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .lg\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .lg\:bg-current {
+    background-color: currentColor;
+  }
+
+  .lg\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .lg\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .lg\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .lg\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .lg\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .lg\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .lg\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .lg\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .lg\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .lg\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .lg\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .lg\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .lg\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .lg\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .lg\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .lg\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .lg\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .lg\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .lg\:bg-none {
+    background-image: none;
+  }
+
+  .lg\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .lg\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .lg\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .lg\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .lg\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .lg\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .lg\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .lg\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .lg\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .lg\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .lg\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .lg\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .lg\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .lg\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .lg\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .lg\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .lg\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .lg\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .lg\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .lg\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .lg\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .lg\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .lg\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .lg\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .lg\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .lg\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .lg\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .lg\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .lg\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .lg\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .lg\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .lg\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .lg\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .lg\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .lg\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .lg\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .lg\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .lg\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .lg\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .lg\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .lg\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .lg\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .lg\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .lg\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .lg\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .lg\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .lg\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .lg\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .lg\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .lg\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .lg\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .lg\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .lg\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .lg\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .lg\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .lg\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .lg\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .lg\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .lg\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .lg\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .lg\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .lg\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .lg\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .lg\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .lg\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .lg\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .lg\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .lg\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .lg\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .lg\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .lg\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .lg\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .lg\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .lg\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .lg\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .lg\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .lg\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .lg\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .lg\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .lg\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .lg\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .lg\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .lg\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .lg\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .lg\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .lg\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .lg\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .lg\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .lg\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .lg\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .lg\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .lg\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .lg\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .lg\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .lg\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .lg\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .lg\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .lg\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .lg\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .lg\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .lg\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .lg\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .lg\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .lg\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .lg\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .lg\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .lg\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .lg\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .lg\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .lg\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .lg\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .lg\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .lg\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .lg\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .lg\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .lg\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .lg\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .lg\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .lg\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .lg\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .lg\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .lg\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .lg\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .lg\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .lg\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .lg\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .lg\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .lg\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .lg\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .lg\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .lg\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .lg\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .lg\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .lg\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .lg\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .lg\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .lg\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .lg\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .lg\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .lg\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .lg\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .lg\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .lg\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .lg\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .lg\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .lg\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .lg\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .lg\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .lg\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .lg\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .lg\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .lg\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .lg\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .lg\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .lg\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .lg\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .lg\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .lg\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .lg\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .lg\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .lg\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .lg\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .lg\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .lg\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .lg\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .lg\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .lg\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .lg\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .lg\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .lg\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .lg\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .lg\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .lg\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .lg\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .lg\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .lg\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .lg\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .lg\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .lg\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .lg\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .lg\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .lg\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .lg\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .lg\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .lg\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .lg\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .lg\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .lg\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .lg\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .lg\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .lg\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .lg\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .lg\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .lg\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .lg\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .lg\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .lg\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .lg\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .lg\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .lg\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .lg\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .lg\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .lg\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .lg\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .lg\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .lg\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .lg\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .lg\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .lg\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .lg\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .lg\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .lg\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .lg\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .lg\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .lg\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .lg\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .lg\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .lg\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .lg\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .lg\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .lg\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .lg\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .lg\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .lg\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .lg\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .lg\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .lg\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .lg\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .lg\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .lg\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .lg\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .lg\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .lg\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .lg\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .lg\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .lg\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .lg\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .lg\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .lg\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .lg\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .lg\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .lg\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .lg\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .lg\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .lg\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .lg\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .lg\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .lg\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .lg\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .lg\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .lg\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .lg\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .lg\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .lg\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .lg\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .lg\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .lg\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .lg\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .lg\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .lg\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .lg\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .lg\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .lg\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .lg\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .lg\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .lg\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .lg\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .lg\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .lg\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .lg\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .lg\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .lg\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .lg\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .lg\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .lg\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .lg\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .lg\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .lg\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .lg\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .lg\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .lg\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .lg\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .lg\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .lg\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .lg\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .lg\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .lg\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .lg\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .lg\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .lg\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .lg\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .lg\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .lg\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .lg\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .lg\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .lg\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .lg\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .lg\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .lg\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .lg\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .lg\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .lg\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .lg\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .lg\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .lg\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .lg\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .lg\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .lg\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .lg\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .lg\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .lg\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .lg\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .lg\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .lg\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .lg\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .lg\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .lg\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .lg\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .lg\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .lg\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .lg\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .lg\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .lg\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .lg\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .lg\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .lg\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .lg\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .lg\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .lg\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .lg\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .lg\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .lg\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .lg\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .lg\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .lg\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .lg\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .lg\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .lg\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .lg\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .lg\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .lg\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .lg\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .lg\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .lg\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .lg\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .lg\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .lg\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .lg\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .lg\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .lg\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .lg\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .lg\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .lg\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .lg\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .lg\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .lg\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .lg\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .lg\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .lg\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .lg\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .lg\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .lg\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .lg\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .lg\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .lg\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .lg\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .lg\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .lg\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .lg\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .lg\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .lg\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .lg\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .lg\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .lg\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .lg\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .lg\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .lg\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .lg\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .lg\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .lg\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .lg\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .lg\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .lg\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .lg\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .lg\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .lg\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .lg\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .lg\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .lg\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .lg\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .lg\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .lg\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .lg\:bg-center {
+    background-position: center;
+  }
+
+  .lg\:bg-left {
+    background-position: left;
+  }
+
+  .lg\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .lg\:bg-left-top {
+    background-position: left top;
+  }
+
+  .lg\:bg-right {
+    background-position: right;
+  }
+
+  .lg\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .lg\:bg-right-top {
+    background-position: right top;
+  }
+
+  .lg\:bg-top {
+    background-position: top;
+  }
+
+  .lg\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .lg\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .lg\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .lg\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .lg\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .lg\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .lg\:bg-auto {
+    background-size: auto;
+  }
+
+  .lg\:bg-cover {
+    background-size: cover;
+  }
+
+  .lg\:bg-contain {
+    background-size: contain;
+  }
+
+  .lg\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .lg\:border-separate {
+    border-collapse: separate;
+  }
+
+  .lg\:border-transparent {
+    border-color: transparent;
+  }
+
+  .lg\:border-current {
+    border-color: currentColor;
+  }
+
+  .lg\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .lg\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .lg\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .lg\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .lg\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .lg\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .lg\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .lg\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .lg\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .lg\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .lg\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .lg\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .lg\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .lg\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .lg\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .lg\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .lg\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .lg\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .lg\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .lg\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .lg\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .lg\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .lg\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .lg\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .lg\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .lg\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .lg\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .lg\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .lg\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .lg\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .lg\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .lg\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .lg\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .lg\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .lg\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .lg\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .lg\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .lg\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .lg\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .lg\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .lg\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .lg\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .lg\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .lg\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .lg\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .lg\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .lg\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .lg\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .lg\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .lg\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .lg\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .lg\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .lg\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .lg\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .lg\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .lg\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .lg\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .lg\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .lg\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .lg\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .lg\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .lg\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .lg\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .lg\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .lg\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .lg\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .lg\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .lg\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .lg\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .lg\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .lg\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .lg\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .lg\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .lg\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .lg\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .lg\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .lg\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .lg\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .lg\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .lg\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .lg\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .lg\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .lg\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .lg\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .lg\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .lg\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .lg\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .lg\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .lg\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .lg\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .lg\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .lg\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .lg\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .lg\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .lg\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .lg\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .lg\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .lg\:rounded-none {
+    border-radius: 0;
+  }
+
+  .lg\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .lg\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .lg\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .lg\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .lg\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .lg\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .lg\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .lg\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .lg\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .lg\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .lg\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .lg\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .lg\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .lg\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .lg\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .lg\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .lg\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .lg\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .lg\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .lg\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .lg\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .lg\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .lg\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .lg\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .lg\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .lg\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .lg\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .lg\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .lg\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .lg\:border-solid {
+    border-style: solid;
+  }
+
+  .lg\:border-dashed {
+    border-style: dashed;
+  }
+
+  .lg\:border-dotted {
+    border-style: dotted;
+  }
+
+  .lg\:border-double {
+    border-style: double;
+  }
+
+  .lg\:border-none {
+    border-style: none;
+  }
+
+  .lg\:border-0 {
+    border-width: 0;
+  }
+
+  .lg\:border-2 {
+    border-width: 2px;
+  }
+
+  .lg\:border-4 {
+    border-width: 4px;
+  }
+
+  .lg\:border-8 {
+    border-width: 8px;
+  }
+
+  .lg\:border {
+    border-width: 1px;
+  }
+
+  .lg\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .lg\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .lg\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .lg\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .lg\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .lg\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .lg\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .lg\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .lg\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .lg\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .lg\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .lg\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .lg\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .lg\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .lg\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .lg\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .lg\:border-t {
+    border-top-width: 1px;
+  }
+
+  .lg\:border-r {
+    border-right-width: 1px;
+  }
+
+  .lg\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .lg\:border-l {
+    border-left-width: 1px;
+  }
+
+  .lg\:box-border {
+    box-sizing: border-box;
+  }
+
+  .lg\:box-content {
+    box-sizing: content-box;
+  }
+
+  .lg\:cursor-auto {
+    cursor: auto;
+  }
+
+  .lg\:cursor-default {
+    cursor: default;
+  }
+
+  .lg\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .lg\:cursor-wait {
+    cursor: wait;
+  }
+
+  .lg\:cursor-text {
+    cursor: text;
+  }
+
+  .lg\:cursor-move {
+    cursor: move;
+  }
+
+  .lg\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .lg\:block {
+    display: block;
+  }
+
+  .lg\:inline-block {
+    display: inline-block;
+  }
+
+  .lg\:inline {
+    display: inline;
+  }
+
+  .lg\:flex {
+    display: flex;
+  }
+
+  .lg\:inline-flex {
+    display: inline-flex;
+  }
+
+  .lg\:table {
+    display: table;
+  }
+
+  .lg\:table-caption {
+    display: table-caption;
+  }
+
+  .lg\:table-cell {
+    display: table-cell;
+  }
+
+  .lg\:table-column {
+    display: table-column;
+  }
+
+  .lg\:table-column-group {
+    display: table-column-group;
+  }
+
+  .lg\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .lg\:table-header-group {
+    display: table-header-group;
+  }
+
+  .lg\:table-row-group {
+    display: table-row-group;
+  }
+
+  .lg\:table-row {
+    display: table-row;
+  }
+
+  .lg\:flow-root {
+    display: flow-root;
+  }
+
+  .lg\:grid {
+    display: grid;
+  }
+
+  .lg\:inline-grid {
+    display: inline-grid;
+  }
+
+  .lg\:contents {
+    display: contents;
+  }
+
+  .lg\:hidden {
+    display: none;
+  }
+
+  .lg\:flex-row {
+    flex-direction: row;
+  }
+
+  .lg\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .lg\:flex-col {
+    flex-direction: column;
+  }
+
+  .lg\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .lg\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .lg\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .lg\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .lg\:place-items-auto {
+    place-items: auto;
+  }
+
+  .lg\:place-items-start {
+    place-items: start;
+  }
+
+  .lg\:place-items-end {
+    place-items: end;
+  }
+
+  .lg\:place-items-center {
+    place-items: center;
+  }
+
+  .lg\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .lg\:place-content-center {
+    place-content: center;
+  }
+
+  .lg\:place-content-start {
+    place-content: start;
+  }
+
+  .lg\:place-content-end {
+    place-content: end;
+  }
+
+  .lg\:place-content-between {
+    place-content: space-between;
+  }
+
+  .lg\:place-content-around {
+    place-content: space-around;
+  }
+
+  .lg\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .lg\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .lg\:place-self-auto {
+    place-self: auto;
+  }
+
+  .lg\:place-self-start {
+    place-self: start;
+  }
+
+  .lg\:place-self-end {
+    place-self: end;
+  }
+
+  .lg\:place-self-center {
+    place-self: center;
+  }
+
+  .lg\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .lg\:items-start {
+    align-items: flex-start;
+  }
+
+  .lg\:items-end {
+    align-items: flex-end;
+  }
+
+  .lg\:items-center {
+    align-items: center;
+  }
+
+  .lg\:items-baseline {
+    align-items: baseline;
+  }
+
+  .lg\:items-stretch {
+    align-items: stretch;
+  }
+
+  .lg\:content-center {
+    align-content: center;
+  }
+
+  .lg\:content-start {
+    align-content: flex-start;
+  }
+
+  .lg\:content-end {
+    align-content: flex-end;
+  }
+
+  .lg\:content-between {
+    align-content: space-between;
+  }
+
+  .lg\:content-around {
+    align-content: space-around;
+  }
+
+  .lg\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .lg\:self-auto {
+    align-self: auto;
+  }
+
+  .lg\:self-start {
+    align-self: flex-start;
+  }
+
+  .lg\:self-end {
+    align-self: flex-end;
+  }
+
+  .lg\:self-center {
+    align-self: center;
+  }
+
+  .lg\:self-stretch {
+    align-self: stretch;
+  }
+
+  .lg\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .lg\:justify-items-start {
+    justify-items: start;
+  }
+
+  .lg\:justify-items-end {
+    justify-items: end;
+  }
+
+  .lg\:justify-items-center {
+    justify-items: center;
+  }
+
+  .lg\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .lg\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .lg\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .lg\:justify-center {
+    justify-content: center;
+  }
+
+  .lg\:justify-between {
+    justify-content: space-between;
+  }
+
+  .lg\:justify-around {
+    justify-content: space-around;
+  }
+
+  .lg\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .lg\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .lg\:justify-self-start {
+    justify-self: start;
+  }
+
+  .lg\:justify-self-end {
+    justify-self: end;
+  }
+
+  .lg\:justify-self-center {
+    justify-self: center;
+  }
+
+  .lg\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .lg\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .lg\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .lg\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .lg\:flex-none {
+    flex: none;
+  }
+
+  .lg\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .lg\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .lg\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .lg\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .lg\:order-1 {
+    order: 1;
+  }
+
+  .lg\:order-2 {
+    order: 2;
+  }
+
+  .lg\:order-3 {
+    order: 3;
+  }
+
+  .lg\:order-4 {
+    order: 4;
+  }
+
+  .lg\:order-5 {
+    order: 5;
+  }
+
+  .lg\:order-6 {
+    order: 6;
+  }
+
+  .lg\:order-7 {
+    order: 7;
+  }
+
+  .lg\:order-8 {
+    order: 8;
+  }
+
+  .lg\:order-9 {
+    order: 9;
+  }
+
+  .lg\:order-10 {
+    order: 10;
+  }
+
+  .lg\:order-11 {
+    order: 11;
+  }
+
+  .lg\:order-12 {
+    order: 12;
+  }
+
+  .lg\:order-first {
+    order: -9999;
+  }
+
+  .lg\:order-last {
+    order: 9999;
+  }
+
+  .lg\:order-none {
+    order: 0;
+  }
+
+  .lg\:float-right {
+    float: right;
+  }
+
+  .lg\:float-left {
+    float: left;
+  }
+
+  .lg\:float-none {
+    float: none;
+  }
+
+  .lg\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .lg\:clear-left {
+    clear: left;
+  }
+
+  .lg\:clear-right {
+    clear: right;
+  }
+
+  .lg\:clear-both {
+    clear: both;
+  }
+
+  .lg\:clear-none {
+    clear: none;
+  }
+
+  .lg\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .lg\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .lg\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .lg\:font-hairline {
+    font-weight: 100;
+  }
+
+  .lg\:font-thin {
+    font-weight: 200;
+  }
+
+  .lg\:font-light {
+    font-weight: 300;
+  }
+
+  .lg\:font-normal {
+    font-weight: 400;
+  }
+
+  .lg\:font-medium {
+    font-weight: 500;
+  }
+
+  .lg\:font-semibold {
+    font-weight: 600;
+  }
+
+  .lg\:font-bold {
+    font-weight: 700;
+  }
+
+  .lg\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .lg\:font-black {
+    font-weight: 900;
+  }
+
+  .lg\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .lg\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .lg\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .lg\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .lg\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .lg\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .lg\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .lg\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .lg\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .lg\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .lg\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .lg\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .lg\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .lg\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .lg\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .lg\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .lg\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .lg\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .lg\:h-0 {
+    height: 0;
+  }
+
+  .lg\:h-1 {
+    height: 0.25rem;
+  }
+
+  .lg\:h-2 {
+    height: 0.5rem;
+  }
+
+  .lg\:h-3 {
+    height: 0.75rem;
+  }
+
+  .lg\:h-4 {
+    height: 1rem;
+  }
+
+  .lg\:h-5 {
+    height: 1.25rem;
+  }
+
+  .lg\:h-6 {
+    height: 1.5rem;
+  }
+
+  .lg\:h-8 {
+    height: 2rem;
+  }
+
+  .lg\:h-10 {
+    height: 2.5rem;
+  }
+
+  .lg\:h-12 {
+    height: 3rem;
+  }
+
+  .lg\:h-16 {
+    height: 4rem;
+  }
+
+  .lg\:h-20 {
+    height: 5rem;
+  }
+
+  .lg\:h-24 {
+    height: 6rem;
+  }
+
+  .lg\:h-32 {
+    height: 8rem;
+  }
+
+  .lg\:h-40 {
+    height: 10rem;
+  }
+
+  .lg\:h-48 {
+    height: 12rem;
+  }
+
+  .lg\:h-56 {
+    height: 14rem;
+  }
+
+  .lg\:h-64 {
+    height: 16rem;
+  }
+
+  .lg\:h-auto {
+    height: auto;
+  }
+
+  .lg\:h-px {
+    height: 1px;
+  }
+
+  .lg\:h-full {
+    height: 100%;
+  }
+
+  .lg\:h-screen {
+    height: 100vh;
+  }
+
+  .lg\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .lg\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .lg\:text-base {
+    font-size: 1rem;
+  }
+
+  .lg\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .lg\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .lg\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .lg\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .lg\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .lg\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .lg\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .lg\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .lg\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .lg\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .lg\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .lg\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .lg\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .lg\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .lg\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .lg\:leading-none {
+    line-height: 1;
+  }
+
+  .lg\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .lg\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .lg\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .lg\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .lg\:leading-loose {
+    line-height: 2;
+  }
+
+  .lg\:list-inside {
+    list-style-position: inside;
+  }
+
+  .lg\:list-outside {
+    list-style-position: outside;
+  }
+
+  .lg\:list-none {
+    list-style-type: none;
+  }
+
+  .lg\:list-disc {
+    list-style-type: disc;
+  }
+
+  .lg\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .lg\:m-0 {
+    margin: 0;
+  }
+
+  .lg\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .lg\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .lg\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .lg\:m-4 {
+    margin: 1rem;
+  }
+
+  .lg\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .lg\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .lg\:m-8 {
+    margin: 2rem;
+  }
+
+  .lg\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .lg\:m-12 {
+    margin: 3rem;
+  }
+
+  .lg\:m-16 {
+    margin: 4rem;
+  }
+
+  .lg\:m-20 {
+    margin: 5rem;
+  }
+
+  .lg\:m-24 {
+    margin: 6rem;
+  }
+
+  .lg\:m-32 {
+    margin: 8rem;
+  }
+
+  .lg\:m-40 {
+    margin: 10rem;
+  }
+
+  .lg\:m-48 {
+    margin: 12rem;
+  }
+
+  .lg\:m-56 {
+    margin: 14rem;
+  }
+
+  .lg\:m-64 {
+    margin: 16rem;
+  }
+
+  .lg\:m-auto {
+    margin: auto;
+  }
+
+  .lg\:m-px {
+    margin: 1px;
+  }
+
+  .lg\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .lg\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .lg\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .lg\:-m-4 {
+    margin: -1rem;
+  }
+
+  .lg\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .lg\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .lg\:-m-8 {
+    margin: -2rem;
+  }
+
+  .lg\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .lg\:-m-12 {
+    margin: -3rem;
+  }
+
+  .lg\:-m-16 {
+    margin: -4rem;
+  }
+
+  .lg\:-m-20 {
+    margin: -5rem;
+  }
+
+  .lg\:-m-24 {
+    margin: -6rem;
+  }
+
+  .lg\:-m-32 {
+    margin: -8rem;
+  }
+
+  .lg\:-m-40 {
+    margin: -10rem;
+  }
+
+  .lg\:-m-48 {
+    margin: -12rem;
+  }
+
+  .lg\:-m-56 {
+    margin: -14rem;
+  }
+
+  .lg\:-m-64 {
+    margin: -16rem;
+  }
+
+  .lg\:-m-px {
+    margin: -1px;
+  }
+
+  .lg\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .lg\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .lg\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .lg\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .lg\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .lg\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .lg\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .lg\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .lg\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .lg\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .lg\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .lg\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .lg\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .lg\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .lg\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .lg\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .lg\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .lg\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .lg\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .lg\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .lg\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .lg\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .lg\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .lg\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .lg\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .lg\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .lg\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .lg\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .lg\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .lg\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .lg\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .lg\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .lg\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .lg\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .lg\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .lg\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .lg\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .lg\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .lg\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .lg\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .lg\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .lg\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .lg\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .lg\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .lg\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .lg\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .lg\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .lg\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .lg\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .lg\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .lg\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .lg\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .lg\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .lg\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .lg\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .lg\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .lg\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .lg\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .lg\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .lg\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .lg\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .lg\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .lg\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .lg\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .lg\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .lg\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .lg\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .lg\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .lg\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .lg\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .lg\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .lg\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .lg\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .lg\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .lg\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .lg\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .lg\:mt-0 {
+    margin-top: 0;
+  }
+
+  .lg\:mr-0 {
+    margin-right: 0;
+  }
+
+  .lg\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .lg\:ml-0 {
+    margin-left: 0;
+  }
+
+  .lg\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .lg\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .lg\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .lg\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .lg\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .lg\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .lg\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .lg\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .lg\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .lg\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .lg\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .lg\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .lg\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .lg\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .lg\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .lg\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .lg\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .lg\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .lg\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .lg\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .lg\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .lg\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .lg\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .lg\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .lg\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .lg\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .lg\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .lg\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .lg\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .lg\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .lg\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .lg\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .lg\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .lg\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .lg\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .lg\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .lg\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .lg\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .lg\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .lg\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .lg\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .lg\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .lg\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .lg\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .lg\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .lg\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .lg\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .lg\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .lg\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .lg\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .lg\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .lg\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .lg\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .lg\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .lg\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .lg\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .lg\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .lg\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .lg\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .lg\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .lg\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .lg\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .lg\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .lg\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .lg\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .lg\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .lg\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .lg\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .lg\:mt-auto {
+    margin-top: auto;
+  }
+
+  .lg\:mr-auto {
+    margin-right: auto;
+  }
+
+  .lg\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .lg\:ml-auto {
+    margin-left: auto;
+  }
+
+  .lg\:mt-px {
+    margin-top: 1px;
+  }
+
+  .lg\:mr-px {
+    margin-right: 1px;
+  }
+
+  .lg\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .lg\:ml-px {
+    margin-left: 1px;
+  }
+
+  .lg\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .lg\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .lg\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .lg\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .lg\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .lg\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .lg\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .lg\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .lg\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .lg\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .lg\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .lg\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .lg\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .lg\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .lg\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .lg\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .lg\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .lg\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .lg\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .lg\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .lg\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .lg\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .lg\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .lg\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .lg\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .lg\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .lg\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .lg\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .lg\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .lg\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .lg\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .lg\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .lg\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .lg\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .lg\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .lg\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .lg\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .lg\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .lg\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .lg\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .lg\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .lg\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .lg\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .lg\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .lg\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .lg\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .lg\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .lg\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .lg\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .lg\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .lg\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .lg\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .lg\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .lg\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .lg\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .lg\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .lg\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .lg\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .lg\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .lg\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .lg\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .lg\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .lg\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .lg\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .lg\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .lg\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .lg\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .lg\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .lg\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .lg\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .lg\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .lg\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .lg\:max-h-full {
+    max-height: 100%;
+  }
+
+  .lg\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .lg\:max-w-none {
+    max-width: none;
+  }
+
+  .lg\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .lg\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .lg\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .lg\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .lg\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .lg\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .lg\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .lg\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .lg\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .lg\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .lg\:max-w-full {
+    max-width: 100%;
+  }
+
+  .lg\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .lg\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .lg\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .lg\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .lg\:min-h-0 {
+    min-height: 0;
+  }
+
+  .lg\:min-h-full {
+    min-height: 100%;
+  }
+
+  .lg\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .lg\:min-w-0 {
+    min-width: 0;
+  }
+
+  .lg\:min-w-full {
+    min-width: 100%;
+  }
+
+  .lg\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .lg\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .lg\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .lg\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .lg\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .lg\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .lg\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .lg\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .lg\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .lg\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .lg\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .lg\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .lg\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .lg\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .lg\:opacity-0 {
+    opacity: 0;
+  }
+
+  .lg\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .lg\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .lg\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .lg\:opacity-100 {
+    opacity: 1;
+  }
+
+  .lg\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .lg\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .lg\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .lg\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .lg\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .lg\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .lg\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .lg\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .lg\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .lg\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .lg\:outline-none {
+    outline: 0;
+  }
+
+  .lg\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .lg\:overflow-auto {
+    overflow: auto;
+  }
+
+  .lg\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .lg\:overflow-visible {
+    overflow: visible;
+  }
+
+  .lg\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .lg\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .lg\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .lg\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .lg\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .lg\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .lg\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .lg\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .lg\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .lg\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .lg\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .lg\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .lg\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .lg\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .lg\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .lg\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .lg\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .lg\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .lg\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .lg\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .lg\:p-0 {
+    padding: 0;
+  }
+
+  .lg\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .lg\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .lg\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .lg\:p-4 {
+    padding: 1rem;
+  }
+
+  .lg\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .lg\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .lg\:p-8 {
+    padding: 2rem;
+  }
+
+  .lg\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .lg\:p-12 {
+    padding: 3rem;
+  }
+
+  .lg\:p-16 {
+    padding: 4rem;
+  }
+
+  .lg\:p-20 {
+    padding: 5rem;
+  }
+
+  .lg\:p-24 {
+    padding: 6rem;
+  }
+
+  .lg\:p-32 {
+    padding: 8rem;
+  }
+
+  .lg\:p-40 {
+    padding: 10rem;
+  }
+
+  .lg\:p-48 {
+    padding: 12rem;
+  }
+
+  .lg\:p-56 {
+    padding: 14rem;
+  }
+
+  .lg\:p-64 {
+    padding: 16rem;
+  }
+
+  .lg\:p-px {
+    padding: 1px;
+  }
+
+  .lg\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .lg\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .lg\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .lg\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .lg\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .lg\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .lg\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .lg\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .lg\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .lg\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .lg\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .lg\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .lg\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .lg\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .lg\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .lg\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .lg\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .lg\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .lg\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .lg\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .lg\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .lg\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .lg\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .lg\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .lg\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .lg\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .lg\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .lg\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .lg\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .lg\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .lg\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .lg\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .lg\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .lg\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .lg\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .lg\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .lg\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .lg\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .lg\:pt-0 {
+    padding-top: 0;
+  }
+
+  .lg\:pr-0 {
+    padding-right: 0;
+  }
+
+  .lg\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .lg\:pl-0 {
+    padding-left: 0;
+  }
+
+  .lg\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .lg\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .lg\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .lg\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .lg\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .lg\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .lg\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .lg\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .lg\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .lg\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .lg\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .lg\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .lg\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .lg\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .lg\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .lg\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .lg\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .lg\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .lg\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .lg\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .lg\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .lg\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .lg\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .lg\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .lg\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .lg\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .lg\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .lg\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .lg\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .lg\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .lg\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .lg\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .lg\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .lg\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .lg\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .lg\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .lg\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .lg\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .lg\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .lg\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .lg\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .lg\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .lg\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .lg\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .lg\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .lg\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .lg\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .lg\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .lg\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .lg\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .lg\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .lg\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .lg\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .lg\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .lg\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .lg\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .lg\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .lg\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .lg\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .lg\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .lg\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .lg\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .lg\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .lg\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .lg\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .lg\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .lg\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .lg\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .lg\:pt-px {
+    padding-top: 1px;
+  }
+
+  .lg\:pr-px {
+    padding-right: 1px;
+  }
+
+  .lg\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .lg\:pl-px {
+    padding-left: 1px;
+  }
+
+  .lg\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .lg\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .lg\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .lg\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .lg\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .lg\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .lg\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .lg\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .lg\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .lg\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .lg\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .lg\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .lg\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .lg\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .lg\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .lg\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .lg\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .lg\:static {
+    position: static;
+  }
+
+  .lg\:fixed {
+    position: fixed;
+  }
+
+  .lg\:absolute {
+    position: absolute;
+  }
+
+  .lg\:relative {
+    position: relative;
+  }
+
+  .lg\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .lg\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .lg\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .lg\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .lg\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .lg\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .lg\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .lg\:top-0 {
+    top: 0;
+  }
+
+  .lg\:right-0 {
+    right: 0;
+  }
+
+  .lg\:bottom-0 {
+    bottom: 0;
+  }
+
+  .lg\:left-0 {
+    left: 0;
+  }
+
+  .lg\:top-auto {
+    top: auto;
+  }
+
+  .lg\:right-auto {
+    right: auto;
+  }
+
+  .lg\:bottom-auto {
+    bottom: auto;
+  }
+
+  .lg\:left-auto {
+    left: auto;
+  }
+
+  .lg\:resize-none {
+    resize: none;
+  }
+
+  .lg\:resize-y {
+    resize: vertical;
+  }
+
+  .lg\:resize-x {
+    resize: horizontal;
+  }
+
+  .lg\:resize {
+    resize: both;
+  }
+
+  .lg\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .lg\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .lg\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .lg\:shadow-none {
+    box-shadow: none;
+  }
+
+  .lg\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .lg\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .lg\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .lg\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .lg\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .lg\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .lg\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .lg\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .lg\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .lg\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .lg\:fill-current {
+    fill: currentColor;
+  }
+
+  .lg\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .lg\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .lg\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .lg\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .lg\:table-auto {
+    table-layout: auto;
+  }
+
+  .lg\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .lg\:text-left {
+    text-align: left;
+  }
+
+  .lg\:text-center {
+    text-align: center;
+  }
+
+  .lg\:text-right {
+    text-align: right;
+  }
+
+  .lg\:text-justify {
+    text-align: justify;
+  }
+
+  .lg\:text-transparent {
+    color: transparent;
+  }
+
+  .lg\:text-current {
+    color: currentColor;
+  }
+
+  .lg\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .lg\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .lg\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .lg\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .lg\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .lg\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .lg\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .lg\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .lg\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .lg\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .lg\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .lg\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .lg\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .lg\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .lg\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .lg\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .lg\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .lg\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .lg\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .lg\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .lg\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .lg\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .lg\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .lg\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .lg\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .lg\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .lg\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .lg\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .lg\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .lg\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .lg\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .lg\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .lg\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .lg\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .lg\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .lg\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .lg\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .lg\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .lg\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .lg\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .lg\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .lg\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .lg\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .lg\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .lg\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .lg\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .lg\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .lg\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .lg\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .lg\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .lg\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .lg\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .lg\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .lg\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .lg\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .lg\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .lg\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .lg\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .lg\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .lg\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .lg\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .lg\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .lg\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .lg\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .lg\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .lg\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .lg\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .lg\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .lg\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .lg\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .lg\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .lg\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .lg\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .lg\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .lg\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .lg\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .lg\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .lg\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .lg\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .lg\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .lg\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .lg\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .lg\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .lg\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .lg\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .lg\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .lg\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .lg\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .lg\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .lg\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .lg\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .lg\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .lg\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .lg\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .lg\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .lg\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .lg\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .lg\:italic {
+    font-style: italic;
+  }
+
+  .lg\:not-italic {
+    font-style: normal;
+  }
+
+  .lg\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .lg\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .lg\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .lg\:normal-case {
+    text-transform: none;
+  }
+
+  .lg\:underline {
+    text-decoration: underline;
+  }
+
+  .lg\:line-through {
+    text-decoration: line-through;
+  }
+
+  .lg\:no-underline {
+    text-decoration: none;
+  }
+
+  .lg\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .lg\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .lg\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .lg\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .lg\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .lg\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .lg\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .lg\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .lg\:ordinal, .lg\:slashed-zero, .lg\:lining-nums, .lg\:oldstyle-nums, .lg\:proportional-nums, .lg\:tabular-nums, .lg\:diagonal-fractions, .lg\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .lg\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .lg\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .lg\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .lg\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .lg\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .lg\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .lg\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .lg\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .lg\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .lg\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .lg\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .lg\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .lg\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .lg\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .lg\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .lg\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .lg\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .lg\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .lg\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .lg\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .lg\:align-top {
+    vertical-align: top;
+  }
+
+  .lg\:align-middle {
+    vertical-align: middle;
+  }
+
+  .lg\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .lg\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .lg\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .lg\:visible {
+    visibility: visible;
+  }
+
+  .lg\:invisible {
+    visibility: hidden;
+  }
+
+  .lg\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .lg\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .lg\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .lg\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .lg\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .lg\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .lg\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .lg\:break-all {
+    word-break: break-all;
+  }
+
+  .lg\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .lg\:w-0 {
+    width: 0;
+  }
+
+  .lg\:w-1 {
+    width: 0.25rem;
+  }
+
+  .lg\:w-2 {
+    width: 0.5rem;
+  }
+
+  .lg\:w-3 {
+    width: 0.75rem;
+  }
+
+  .lg\:w-4 {
+    width: 1rem;
+  }
+
+  .lg\:w-5 {
+    width: 1.25rem;
+  }
+
+  .lg\:w-6 {
+    width: 1.5rem;
+  }
+
+  .lg\:w-8 {
+    width: 2rem;
+  }
+
+  .lg\:w-10 {
+    width: 2.5rem;
+  }
+
+  .lg\:w-12 {
+    width: 3rem;
+  }
+
+  .lg\:w-16 {
+    width: 4rem;
+  }
+
+  .lg\:w-20 {
+    width: 5rem;
+  }
+
+  .lg\:w-24 {
+    width: 6rem;
+  }
+
+  .lg\:w-32 {
+    width: 8rem;
+  }
+
+  .lg\:w-40 {
+    width: 10rem;
+  }
+
+  .lg\:w-48 {
+    width: 12rem;
+  }
+
+  .lg\:w-56 {
+    width: 14rem;
+  }
+
+  .lg\:w-64 {
+    width: 16rem;
+  }
+
+  .lg\:w-auto {
+    width: auto;
+  }
+
+  .lg\:w-px {
+    width: 1px;
+  }
+
+  .lg\:w-1\/2 {
+    width: 50%;
+  }
+
+  .lg\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .lg\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .lg\:w-1\/4 {
+    width: 25%;
+  }
+
+  .lg\:w-2\/4 {
+    width: 50%;
+  }
+
+  .lg\:w-3\/4 {
+    width: 75%;
+  }
+
+  .lg\:w-1\/5 {
+    width: 20%;
+  }
+
+  .lg\:w-2\/5 {
+    width: 40%;
+  }
+
+  .lg\:w-3\/5 {
+    width: 60%;
+  }
+
+  .lg\:w-4\/5 {
+    width: 80%;
+  }
+
+  .lg\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .lg\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .lg\:w-3\/6 {
+    width: 50%;
+  }
+
+  .lg\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .lg\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .lg\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .lg\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .lg\:w-3\/12 {
+    width: 25%;
+  }
+
+  .lg\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .lg\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .lg\:w-6\/12 {
+    width: 50%;
+  }
+
+  .lg\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .lg\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .lg\:w-9\/12 {
+    width: 75%;
+  }
+
+  .lg\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .lg\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .lg\:w-full {
+    width: 100%;
+  }
+
+  .lg\:w-screen {
+    width: 100vw;
+  }
+
+  .lg\:z-0 {
+    z-index: 0;
+  }
+
+  .lg\:z-10 {
+    z-index: 10;
+  }
+
+  .lg\:z-20 {
+    z-index: 20;
+  }
+
+  .lg\:z-30 {
+    z-index: 30;
+  }
+
+  .lg\:z-40 {
+    z-index: 40;
+  }
+
+  .lg\:z-50 {
+    z-index: 50;
+  }
+
+  .lg\:z-auto {
+    z-index: auto;
+  }
+
+  .lg\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .lg\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .lg\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .lg\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .lg\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .lg\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .lg\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .lg\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .lg\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .lg\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .lg\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .lg\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .lg\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .lg\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .lg\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .lg\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .lg\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .lg\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .lg\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .lg\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .lg\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .lg\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .lg\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .lg\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .lg\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .lg\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .lg\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .lg\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .lg\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .lg\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .lg\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .lg\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .lg\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .lg\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .lg\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .lg\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .lg\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .lg\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .lg\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .lg\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .lg\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .lg\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .lg\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .lg\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .lg\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .lg\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .lg\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .lg\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .lg\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .lg\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .lg\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .lg\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .lg\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .lg\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .lg\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .lg\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .lg\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .lg\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .lg\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .lg\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .lg\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .lg\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .lg\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .lg\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .lg\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .lg\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .lg\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .lg\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .lg\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .lg\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .lg\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .lg\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .lg\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .lg\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .lg\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .lg\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .lg\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .lg\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .lg\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .lg\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .lg\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .lg\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .lg\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .lg\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .lg\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .lg\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .lg\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .lg\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .lg\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .lg\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .lg\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .lg\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .lg\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .lg\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .lg\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .lg\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .lg\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .lg\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .lg\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .lg\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .lg\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .lg\:col-auto {
+    grid-column: auto;
+  }
+
+  .lg\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .lg\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .lg\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .lg\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .lg\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .lg\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .lg\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .lg\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .lg\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .lg\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .lg\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .lg\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .lg\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .lg\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .lg\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .lg\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .lg\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .lg\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .lg\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .lg\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .lg\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .lg\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .lg\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .lg\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .lg\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .lg\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .lg\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .lg\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .lg\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .lg\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .lg\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .lg\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .lg\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .lg\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .lg\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .lg\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .lg\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .lg\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .lg\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .lg\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .lg\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .lg\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .lg\:row-auto {
+    grid-row: auto;
+  }
+
+  .lg\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .lg\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .lg\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .lg\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .lg\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .lg\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .lg\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .lg\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .lg\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .lg\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .lg\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .lg\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .lg\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .lg\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .lg\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .lg\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .lg\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .lg\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .lg\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .lg\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .lg\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .lg\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .lg\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .lg\:transform-none {
+    transform: none;
+  }
+
+  .lg\:origin-center {
+    transform-origin: center;
+  }
+
+  .lg\:origin-top {
+    transform-origin: top;
+  }
+
+  .lg\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .lg\:origin-right {
+    transform-origin: right;
+  }
+
+  .lg\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .lg\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .lg\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .lg\:origin-left {
+    transform-origin: left;
+  }
+
+  .lg\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .lg\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .lg\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .lg\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .lg\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .lg\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .lg\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .lg\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .lg\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .lg\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .lg\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .lg\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .lg\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .lg\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .lg\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .lg\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .lg\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .lg\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .lg\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .lg\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .lg\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .lg\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .lg\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .lg\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .lg\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .lg\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .lg\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .lg\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .lg\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .lg\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .lg\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .lg\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .lg\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .lg\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .lg\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .lg\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .lg\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .lg\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .lg\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .lg\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .lg\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .lg\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .lg\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .lg\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .lg\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .lg\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .lg\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .lg\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .lg\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .lg\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .lg\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .lg\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .lg\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .lg\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .lg\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .lg\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .lg\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .lg\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .lg\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .lg\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .lg\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .lg\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .lg\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .lg\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .lg\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .lg\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .lg\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .lg\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .lg\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .lg\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .lg\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .lg\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .lg\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .lg\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .lg\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .lg\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .lg\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .lg\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .lg\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .lg\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .lg\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .lg\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .lg\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .lg\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .lg\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .lg\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .lg\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .lg\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .lg\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .lg\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .lg\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .lg\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .lg\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .lg\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .lg\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .lg\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .lg\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .lg\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .lg\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .lg\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .lg\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .lg\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .lg\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .lg\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .lg\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .lg\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .lg\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .lg\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .lg\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .lg\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .lg\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .lg\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .lg\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .lg\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .lg\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .lg\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .lg\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .lg\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .lg\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .lg\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .lg\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .lg\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .lg\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .lg\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .lg\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .lg\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .lg\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .lg\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .lg\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .lg\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .lg\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .lg\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .lg\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .lg\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .lg\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .lg\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .lg\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .lg\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .lg\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .lg\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .lg\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .lg\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .lg\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .lg\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .lg\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .lg\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .lg\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .lg\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .lg\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .lg\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .lg\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .lg\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .lg\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .lg\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .lg\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .lg\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .lg\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .lg\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .lg\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .lg\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .lg\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .lg\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .lg\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .lg\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .lg\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .lg\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .lg\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .lg\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .lg\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .lg\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .lg\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .lg\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .lg\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .lg\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .lg\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .lg\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .lg\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .lg\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .lg\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .lg\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .lg\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .lg\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .lg\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .lg\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .lg\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .lg\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .lg\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .lg\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .lg\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .lg\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .lg\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .lg\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .lg\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .lg\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .lg\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .lg\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .lg\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .lg\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .lg\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .lg\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .lg\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .lg\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .lg\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .lg\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .lg\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .lg\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .lg\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .lg\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .lg\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .lg\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .lg\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .lg\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .lg\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .lg\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .lg\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .lg\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .lg\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .lg\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .lg\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .lg\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .lg\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .lg\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .lg\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .lg\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .lg\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .lg\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .lg\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .lg\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .lg\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .lg\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .lg\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .lg\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .lg\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .lg\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .lg\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .lg\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .lg\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .lg\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .lg\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .lg\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .lg\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .lg\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .lg\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .lg\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .lg\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .lg\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .lg\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .lg\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .lg\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .lg\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .lg\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .lg\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .lg\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .lg\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .lg\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .lg\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .lg\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .lg\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .lg\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .lg\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .lg\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .lg\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .lg\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .lg\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .lg\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .lg\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .lg\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .lg\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .lg\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .lg\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .lg\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .lg\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .lg\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .lg\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .lg\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .lg\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .lg\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .lg\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .lg\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .lg\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .lg\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .lg\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .lg\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .lg\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .lg\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .lg\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .lg\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .lg\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .lg\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .lg\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .lg\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .lg\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .lg\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .lg\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .lg\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .lg\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .lg\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .lg\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .lg\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .lg\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .lg\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .lg\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .lg\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .lg\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .lg\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .lg\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .lg\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .lg\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .lg\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .lg\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .lg\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .lg\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .lg\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .lg\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .lg\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .lg\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .lg\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .lg\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .lg\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .lg\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .lg\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .lg\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .lg\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .lg\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .lg\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .lg\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .lg\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .lg\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .lg\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .lg\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .lg\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .lg\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .lg\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .lg\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .lg\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .lg\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .lg\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .lg\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .lg\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .lg\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .lg\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .lg\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .lg\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .lg\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .lg\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .lg\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .lg\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .lg\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .lg\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .lg\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .lg\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .lg\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .lg\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .lg\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .lg\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .lg\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .lg\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .lg\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .lg\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .lg\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .lg\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .lg\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .lg\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .lg\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .lg\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .lg\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .lg\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .lg\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .lg\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .lg\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .lg\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .lg\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .lg\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .lg\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .lg\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .lg\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .lg\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .lg\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .lg\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .lg\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .lg\:transition-none {
+    transition-property: none;
+  }
+
+  .lg\:transition-all {
+    transition-property: all;
+  }
+
+  .lg\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .lg\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .lg\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .lg\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .lg\:transition-transform {
+    transition-property: transform;
+  }
+
+  .lg\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .lg\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .lg\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .lg\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .lg\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .lg\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .lg\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .lg\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .lg\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .lg\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .lg\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .lg\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .lg\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .lg\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .lg\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .lg\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .lg\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .lg\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .lg\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .lg\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .lg\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .lg\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .lg\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .lg\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .lg\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
+
+@media (min-width: 1280px) {
+  .xl\:container {
+    width: 100%;
+  }
+
+  @media (min-width: 640px) {
+    .xl\:container {
+      max-width: 640px;
+    }
+  }
+
+  @media (min-width: 768px) {
+    .xl\:container {
+      max-width: 768px;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .xl\:container {
+      max-width: 1024px;
+    }
+  }
+
+  @media (min-width: 1280px) {
+    .xl\:container {
+      max-width: 1280px;
+    }
+  }
+
+  .xl\:space-y-0 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0px * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-0 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0px * var(--space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.25rem * var(--space-x-reverse));
+    margin-left: calc(0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(0.75rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(0.75rem * var(--space-x-reverse));
+    margin-left: calc(0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1rem * var(--space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.25rem * var(--space-x-reverse));
+    margin-left: calc(1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1.5rem * var(--space-x-reverse));
+    margin-left: calc(1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2rem * var(--space-x-reverse));
+    margin-left: calc(2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(2.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(2.5rem * var(--space-x-reverse));
+    margin-left: calc(2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(3rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(3rem * var(--space-x-reverse));
+    margin-left: calc(3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(4rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(4rem * var(--space-x-reverse));
+    margin-left: calc(4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(5rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(5rem * var(--space-x-reverse));
+    margin-left: calc(5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(6rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(6rem * var(--space-x-reverse));
+    margin-left: calc(6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(8rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(8rem * var(--space-x-reverse));
+    margin-left: calc(8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(10rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(10rem * var(--space-x-reverse));
+    margin-left: calc(10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(12rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(12rem * var(--space-x-reverse));
+    margin-left: calc(12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(14rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(14rem * var(--space-x-reverse));
+    margin-left: calc(14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(16rem * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(16rem * var(--space-x-reverse));
+    margin-left: calc(16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(1px * var(--space-y-reverse));
+  }
+
+  .xl\:space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(1px * var(--space-x-reverse));
+    margin-left: calc(1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-1 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-1 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.25rem * var(--space-x-reverse));
+    margin-left: calc(-0.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-2 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-2 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.5rem * var(--space-x-reverse));
+    margin-left: calc(-0.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-3 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-0.75rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-0.75rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-3 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-0.75rem * var(--space-x-reverse));
+    margin-left: calc(-0.75rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-4 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-4 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1rem * var(--space-x-reverse));
+    margin-left: calc(-1rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-5 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.25rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.25rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-5 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.25rem * var(--space-x-reverse));
+    margin-left: calc(-1.25rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-6 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-6 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1.5rem * var(--space-x-reverse));
+    margin-left: calc(-1.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-8 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-8 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2rem * var(--space-x-reverse));
+    margin-left: calc(-2rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-10 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-2.5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-2.5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-10 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-2.5rem * var(--space-x-reverse));
+    margin-left: calc(-2.5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-12 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-3rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-3rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-12 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-3rem * var(--space-x-reverse));
+    margin-left: calc(-3rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-16 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-4rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-4rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-16 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-4rem * var(--space-x-reverse));
+    margin-left: calc(-4rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-20 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-5rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-5rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-20 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-5rem * var(--space-x-reverse));
+    margin-left: calc(-5rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-24 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-6rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-6rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-24 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-6rem * var(--space-x-reverse));
+    margin-left: calc(-6rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-32 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-8rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-8rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-32 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-8rem * var(--space-x-reverse));
+    margin-left: calc(-8rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-40 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-10rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-10rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-40 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-10rem * var(--space-x-reverse));
+    margin-left: calc(-10rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-48 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-12rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-12rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-48 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-12rem * var(--space-x-reverse));
+    margin-left: calc(-12rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-56 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-14rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-14rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-56 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-14rem * var(--space-x-reverse));
+    margin-left: calc(-14rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-64 > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-16rem * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-16rem * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-64 > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-16rem * var(--space-x-reverse));
+    margin-left: calc(-16rem * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:-space-y-px > :not(template) ~ :not(template) {
+    --space-y-reverse: 0;
+    margin-top: calc(-1px * calc(1 - var(--space-y-reverse)));
+    margin-bottom: calc(-1px * var(--space-y-reverse));
+  }
+
+  .xl\:-space-x-px > :not(template) ~ :not(template) {
+    --space-x-reverse: 0;
+    margin-right: calc(-1px * var(--space-x-reverse));
+    margin-left: calc(-1px * calc(1 - var(--space-x-reverse)));
+  }
+
+  .xl\:space-y-reverse > :not(template) ~ :not(template) {
+    --space-y-reverse: 1;
+  }
+
+  .xl\:space-x-reverse > :not(template) ~ :not(template) {
+    --space-x-reverse: 1;
+  }
+
+  .xl\:divide-y-0 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(0px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(0px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-0 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(0px * var(--divide-x-reverse));
+    border-left-width: calc(0px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-2 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(2px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(2px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-2 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(2px * var(--divide-x-reverse));
+    border-left-width: calc(2px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-4 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(4px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(4px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-4 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(4px * var(--divide-x-reverse));
+    border-left-width: calc(4px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-8 > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(8px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(8px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x-8 > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(8px * var(--divide-x-reverse));
+    border-left-width: calc(8px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y > :not(template) ~ :not(template) {
+    --divide-y-reverse: 0;
+    border-top-width: calc(1px * calc(1 - var(--divide-y-reverse)));
+    border-bottom-width: calc(1px * var(--divide-y-reverse));
+  }
+
+  .xl\:divide-x > :not(template) ~ :not(template) {
+    --divide-x-reverse: 0;
+    border-right-width: calc(1px * var(--divide-x-reverse));
+    border-left-width: calc(1px * calc(1 - var(--divide-x-reverse)));
+  }
+
+  .xl\:divide-y-reverse > :not(template) ~ :not(template) {
+    --divide-y-reverse: 1;
+  }
+
+  .xl\:divide-x-reverse > :not(template) ~ :not(template) {
+    --divide-x-reverse: 1;
+  }
+
+  .xl\:divide-transparent > :not(template) ~ :not(template) {
+    border-color: transparent;
+  }
+
+  .xl\:divide-current > :not(template) ~ :not(template) {
+    border-color: currentColor;
+  }
+
+  .xl\:divide-black > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--divide-opacity));
+  }
+
+  .xl\:divide-white > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--divide-opacity));
+  }
+
+  .xl\:divide-gray-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--divide-opacity));
+  }
+
+  .xl\:divide-red-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--divide-opacity));
+  }
+
+  .xl\:divide-orange-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--divide-opacity));
+  }
+
+  .xl\:divide-yellow-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--divide-opacity));
+  }
+
+  .xl\:divide-green-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--divide-opacity));
+  }
+
+  .xl\:divide-teal-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--divide-opacity));
+  }
+
+  .xl\:divide-blue-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--divide-opacity));
+  }
+
+  .xl\:divide-indigo-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--divide-opacity));
+  }
+
+  .xl\:divide-purple-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-200 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-300 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-400 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-500 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-600 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-700 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-800 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--divide-opacity));
+  }
+
+  .xl\:divide-pink-900 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--divide-opacity));
+  }
+
+  .xl\:divide-solid > :not(template) ~ :not(template) {
+    border-style: solid;
+  }
+
+  .xl\:divide-dashed > :not(template) ~ :not(template) {
+    border-style: dashed;
+  }
+
+  .xl\:divide-dotted > :not(template) ~ :not(template) {
+    border-style: dotted;
+  }
+
+  .xl\:divide-double > :not(template) ~ :not(template) {
+    border-style: double;
+  }
+
+  .xl\:divide-none > :not(template) ~ :not(template) {
+    border-style: none;
+  }
+
+  .xl\:divide-opacity-0 > :not(template) ~ :not(template) {
+    --divide-opacity: 0;
+  }
+
+  .xl\:divide-opacity-25 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.25;
+  }
+
+  .xl\:divide-opacity-50 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.5;
+  }
+
+  .xl\:divide-opacity-75 > :not(template) ~ :not(template) {
+    --divide-opacity: 0.75;
+  }
+
+  .xl\:divide-opacity-100 > :not(template) ~ :not(template) {
+    --divide-opacity: 1;
+  }
+
+  .xl\:sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .xl\:not-sr-only {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .xl\:focus\:sr-only:focus {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border-width: 0;
+  }
+
+  .xl\:focus\:not-sr-only:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+    white-space: normal;
+  }
+
+  .xl\:appearance-none {
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none;
+  }
+
+  .xl\:bg-fixed {
+    background-attachment: fixed;
+  }
+
+  .xl\:bg-local {
+    background-attachment: local;
+  }
+
+  .xl\:bg-scroll {
+    background-attachment: scroll;
+  }
+
+  .xl\:bg-clip-border {
+    background-clip: border-box;
+  }
+
+  .xl\:bg-clip-padding {
+    background-clip: padding-box;
+  }
+
+  .xl\:bg-clip-content {
+    background-clip: content-box;
+  }
+
+  .xl\:bg-clip-text {
+    -webkit-background-clip: text;
+            background-clip: text;
+  }
+
+  .xl\:bg-transparent {
+    background-color: transparent;
+  }
+
+  .xl\:bg-current {
+    background-color: currentColor;
+  }
+
+  .xl\:bg-black {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .xl\:bg-white {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-100 {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-200 {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-300 {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-400 {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-500 {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-600 {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-700 {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-800 {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .xl\:bg-gray-900 {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-200 {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-300 {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-400 {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-500 {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-600 {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-700 {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-800 {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .xl\:bg-red-900 {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-100 {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-200 {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-300 {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-400 {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-500 {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-600 {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-700 {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-800 {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .xl\:bg-orange-900 {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-100 {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-200 {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-300 {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-400 {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-500 {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-600 {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-700 {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-800 {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .xl\:bg-yellow-900 {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-100 {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-200 {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-300 {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-400 {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-500 {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-600 {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-700 {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-800 {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .xl\:bg-green-900 {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-100 {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-200 {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-300 {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-400 {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-500 {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-600 {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-700 {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-800 {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .xl\:bg-teal-900 {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-100 {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-200 {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-300 {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-400 {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-500 {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-600 {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-700 {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-800 {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .xl\:bg-blue-900 {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-100 {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-200 {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-300 {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-400 {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-500 {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-600 {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-700 {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-800 {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .xl\:bg-indigo-900 {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-100 {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-200 {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-300 {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-400 {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-500 {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-600 {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-700 {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-800 {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .xl\:bg-purple-900 {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-100 {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-200 {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-300 {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-400 {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-500 {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-600 {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-700 {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-800 {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .xl\:bg-pink-900 {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-transparent:hover {
+    background-color: transparent;
+  }
+
+  .xl\:hover\:bg-current:hover {
+    background-color: currentColor;
+  }
+
+  .xl\:hover\:bg-black:hover {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-white:hover {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-100:hover {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-200:hover {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-300:hover {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-400:hover {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-500:hover {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-600:hover {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-700:hover {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-800:hover {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-gray-900:hover {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-300:hover {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-400:hover {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-500:hover {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-600:hover {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-700:hover {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-800:hover {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-red-900:hover {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-200:hover {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-600:hover {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-700:hover {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-800:hover {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-orange-900:hover {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-100:hover {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-200:hover {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-300:hover {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-400:hover {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-500:hover {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-600:hover {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-700:hover {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-800:hover {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-yellow-900:hover {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-100:hover {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-200:hover {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-300:hover {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-400:hover {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-500:hover {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-600:hover {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-700:hover {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-800:hover {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-green-900:hover {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-100:hover {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-200:hover {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-300:hover {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-400:hover {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-500:hover {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-600:hover {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-700:hover {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-800:hover {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-teal-900:hover {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-200:hover {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-300:hover {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-400:hover {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-500:hover {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-600:hover {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-700:hover {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-800:hover {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-blue-900:hover {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-100:hover {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-200:hover {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-300:hover {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-400:hover {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-500:hover {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-600:hover {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-700:hover {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-800:hover {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-indigo-900:hover {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-100:hover {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-200:hover {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-300:hover {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-400:hover {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-500:hover {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-600:hover {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-700:hover {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-800:hover {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-purple-900:hover {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-100:hover {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-200:hover {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-300:hover {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-400:hover {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-500:hover {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-600:hover {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-700:hover {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-800:hover {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .xl\:hover\:bg-pink-900:hover {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-transparent:focus {
+    background-color: transparent;
+  }
+
+  .xl\:focus\:bg-current:focus {
+    background-color: currentColor;
+  }
+
+  .xl\:focus\:bg-black:focus {
+    --bg-opacity: 1;
+    background-color: #000;
+    background-color: rgba(0, 0, 0, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-white:focus {
+    --bg-opacity: 1;
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-100:focus {
+    --bg-opacity: 1;
+    background-color: #f7fafc;
+    background-color: rgba(247, 250, 252, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-200:focus {
+    --bg-opacity: 1;
+    background-color: #edf2f7;
+    background-color: rgba(237, 242, 247, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-300:focus {
+    --bg-opacity: 1;
+    background-color: #e2e8f0;
+    background-color: rgba(226, 232, 240, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-400:focus {
+    --bg-opacity: 1;
+    background-color: #cbd5e0;
+    background-color: rgba(203, 213, 224, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-500:focus {
+    --bg-opacity: 1;
+    background-color: #a0aec0;
+    background-color: rgba(160, 174, 192, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-600:focus {
+    --bg-opacity: 1;
+    background-color: #718096;
+    background-color: rgba(113, 128, 150, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-700:focus {
+    --bg-opacity: 1;
+    background-color: #4a5568;
+    background-color: rgba(74, 85, 104, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-800:focus {
+    --bg-opacity: 1;
+    background-color: #2d3748;
+    background-color: rgba(45, 55, 72, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-gray-900:focus {
+    --bg-opacity: 1;
+    background-color: #1a202c;
+    background-color: rgba(26, 32, 44, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f5;
+    background-color: rgba(255, 245, 245, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7d7;
+    background-color: rgba(254, 215, 215, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-300:focus {
+    --bg-opacity: 1;
+    background-color: #feb2b2;
+    background-color: rgba(254, 178, 178, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-400:focus {
+    --bg-opacity: 1;
+    background-color: #fc8181;
+    background-color: rgba(252, 129, 129, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-500:focus {
+    --bg-opacity: 1;
+    background-color: #f56565;
+    background-color: rgba(245, 101, 101, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-600:focus {
+    --bg-opacity: 1;
+    background-color: #e53e3e;
+    background-color: rgba(229, 62, 62, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-700:focus {
+    --bg-opacity: 1;
+    background-color: #c53030;
+    background-color: rgba(197, 48, 48, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-800:focus {
+    --bg-opacity: 1;
+    background-color: #9b2c2c;
+    background-color: rgba(155, 44, 44, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-red-900:focus {
+    --bg-opacity: 1;
+    background-color: #742a2a;
+    background-color: rgba(116, 42, 42, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffaf0;
+    background-color: rgba(255, 250, 240, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-200:focus {
+    --bg-opacity: 1;
+    background-color: #feebc8;
+    background-color: rgba(254, 235, 200, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbd38d;
+    background-color: rgba(251, 211, 141, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6ad55;
+    background-color: rgba(246, 173, 85, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed8936;
+    background-color: rgba(237, 137, 54, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-600:focus {
+    --bg-opacity: 1;
+    background-color: #dd6b20;
+    background-color: rgba(221, 107, 32, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-700:focus {
+    --bg-opacity: 1;
+    background-color: #c05621;
+    background-color: rgba(192, 86, 33, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-800:focus {
+    --bg-opacity: 1;
+    background-color: #9c4221;
+    background-color: rgba(156, 66, 33, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-orange-900:focus {
+    --bg-opacity: 1;
+    background-color: #7b341e;
+    background-color: rgba(123, 52, 30, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-100:focus {
+    --bg-opacity: 1;
+    background-color: #fffff0;
+    background-color: rgba(255, 255, 240, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-200:focus {
+    --bg-opacity: 1;
+    background-color: #fefcbf;
+    background-color: rgba(254, 252, 191, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-300:focus {
+    --bg-opacity: 1;
+    background-color: #faf089;
+    background-color: rgba(250, 240, 137, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-400:focus {
+    --bg-opacity: 1;
+    background-color: #f6e05e;
+    background-color: rgba(246, 224, 94, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-500:focus {
+    --bg-opacity: 1;
+    background-color: #ecc94b;
+    background-color: rgba(236, 201, 75, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-600:focus {
+    --bg-opacity: 1;
+    background-color: #d69e2e;
+    background-color: rgba(214, 158, 46, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-700:focus {
+    --bg-opacity: 1;
+    background-color: #b7791f;
+    background-color: rgba(183, 121, 31, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-800:focus {
+    --bg-opacity: 1;
+    background-color: #975a16;
+    background-color: rgba(151, 90, 22, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-yellow-900:focus {
+    --bg-opacity: 1;
+    background-color: #744210;
+    background-color: rgba(116, 66, 16, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-100:focus {
+    --bg-opacity: 1;
+    background-color: #f0fff4;
+    background-color: rgba(240, 255, 244, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-200:focus {
+    --bg-opacity: 1;
+    background-color: #c6f6d5;
+    background-color: rgba(198, 246, 213, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-300:focus {
+    --bg-opacity: 1;
+    background-color: #9ae6b4;
+    background-color: rgba(154, 230, 180, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-400:focus {
+    --bg-opacity: 1;
+    background-color: #68d391;
+    background-color: rgba(104, 211, 145, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-500:focus {
+    --bg-opacity: 1;
+    background-color: #48bb78;
+    background-color: rgba(72, 187, 120, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-600:focus {
+    --bg-opacity: 1;
+    background-color: #38a169;
+    background-color: rgba(56, 161, 105, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-700:focus {
+    --bg-opacity: 1;
+    background-color: #2f855a;
+    background-color: rgba(47, 133, 90, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-800:focus {
+    --bg-opacity: 1;
+    background-color: #276749;
+    background-color: rgba(39, 103, 73, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-green-900:focus {
+    --bg-opacity: 1;
+    background-color: #22543d;
+    background-color: rgba(34, 84, 61, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-100:focus {
+    --bg-opacity: 1;
+    background-color: #e6fffa;
+    background-color: rgba(230, 255, 250, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-200:focus {
+    --bg-opacity: 1;
+    background-color: #b2f5ea;
+    background-color: rgba(178, 245, 234, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-300:focus {
+    --bg-opacity: 1;
+    background-color: #81e6d9;
+    background-color: rgba(129, 230, 217, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-400:focus {
+    --bg-opacity: 1;
+    background-color: #4fd1c5;
+    background-color: rgba(79, 209, 197, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-500:focus {
+    --bg-opacity: 1;
+    background-color: #38b2ac;
+    background-color: rgba(56, 178, 172, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-600:focus {
+    --bg-opacity: 1;
+    background-color: #319795;
+    background-color: rgba(49, 151, 149, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-700:focus {
+    --bg-opacity: 1;
+    background-color: #2c7a7b;
+    background-color: rgba(44, 122, 123, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-800:focus {
+    --bg-opacity: 1;
+    background-color: #285e61;
+    background-color: rgba(40, 94, 97, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-teal-900:focus {
+    --bg-opacity: 1;
+    background-color: #234e52;
+    background-color: rgba(35, 78, 82, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf8ff;
+    background-color: rgba(235, 248, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-200:focus {
+    --bg-opacity: 1;
+    background-color: #bee3f8;
+    background-color: rgba(190, 227, 248, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-300:focus {
+    --bg-opacity: 1;
+    background-color: #90cdf4;
+    background-color: rgba(144, 205, 244, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-400:focus {
+    --bg-opacity: 1;
+    background-color: #63b3ed;
+    background-color: rgba(99, 179, 237, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-500:focus {
+    --bg-opacity: 1;
+    background-color: #4299e1;
+    background-color: rgba(66, 153, 225, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-600:focus {
+    --bg-opacity: 1;
+    background-color: #3182ce;
+    background-color: rgba(49, 130, 206, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-700:focus {
+    --bg-opacity: 1;
+    background-color: #2b6cb0;
+    background-color: rgba(43, 108, 176, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-800:focus {
+    --bg-opacity: 1;
+    background-color: #2c5282;
+    background-color: rgba(44, 82, 130, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-blue-900:focus {
+    --bg-opacity: 1;
+    background-color: #2a4365;
+    background-color: rgba(42, 67, 101, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-100:focus {
+    --bg-opacity: 1;
+    background-color: #ebf4ff;
+    background-color: rgba(235, 244, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-200:focus {
+    --bg-opacity: 1;
+    background-color: #c3dafe;
+    background-color: rgba(195, 218, 254, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-300:focus {
+    --bg-opacity: 1;
+    background-color: #a3bffa;
+    background-color: rgba(163, 191, 250, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-400:focus {
+    --bg-opacity: 1;
+    background-color: #7f9cf5;
+    background-color: rgba(127, 156, 245, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-500:focus {
+    --bg-opacity: 1;
+    background-color: #667eea;
+    background-color: rgba(102, 126, 234, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-600:focus {
+    --bg-opacity: 1;
+    background-color: #5a67d8;
+    background-color: rgba(90, 103, 216, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-700:focus {
+    --bg-opacity: 1;
+    background-color: #4c51bf;
+    background-color: rgba(76, 81, 191, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-800:focus {
+    --bg-opacity: 1;
+    background-color: #434190;
+    background-color: rgba(67, 65, 144, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-indigo-900:focus {
+    --bg-opacity: 1;
+    background-color: #3c366b;
+    background-color: rgba(60, 54, 107, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-100:focus {
+    --bg-opacity: 1;
+    background-color: #faf5ff;
+    background-color: rgba(250, 245, 255, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-200:focus {
+    --bg-opacity: 1;
+    background-color: #e9d8fd;
+    background-color: rgba(233, 216, 253, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-300:focus {
+    --bg-opacity: 1;
+    background-color: #d6bcfa;
+    background-color: rgba(214, 188, 250, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-400:focus {
+    --bg-opacity: 1;
+    background-color: #b794f4;
+    background-color: rgba(183, 148, 244, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-500:focus {
+    --bg-opacity: 1;
+    background-color: #9f7aea;
+    background-color: rgba(159, 122, 234, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-600:focus {
+    --bg-opacity: 1;
+    background-color: #805ad5;
+    background-color: rgba(128, 90, 213, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-700:focus {
+    --bg-opacity: 1;
+    background-color: #6b46c1;
+    background-color: rgba(107, 70, 193, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-800:focus {
+    --bg-opacity: 1;
+    background-color: #553c9a;
+    background-color: rgba(85, 60, 154, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-purple-900:focus {
+    --bg-opacity: 1;
+    background-color: #44337a;
+    background-color: rgba(68, 51, 122, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-100:focus {
+    --bg-opacity: 1;
+    background-color: #fff5f7;
+    background-color: rgba(255, 245, 247, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-200:focus {
+    --bg-opacity: 1;
+    background-color: #fed7e2;
+    background-color: rgba(254, 215, 226, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-300:focus {
+    --bg-opacity: 1;
+    background-color: #fbb6ce;
+    background-color: rgba(251, 182, 206, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-400:focus {
+    --bg-opacity: 1;
+    background-color: #f687b3;
+    background-color: rgba(246, 135, 179, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-500:focus {
+    --bg-opacity: 1;
+    background-color: #ed64a6;
+    background-color: rgba(237, 100, 166, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-600:focus {
+    --bg-opacity: 1;
+    background-color: #d53f8c;
+    background-color: rgba(213, 63, 140, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-700:focus {
+    --bg-opacity: 1;
+    background-color: #b83280;
+    background-color: rgba(184, 50, 128, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-800:focus {
+    --bg-opacity: 1;
+    background-color: #97266d;
+    background-color: rgba(151, 38, 109, var(--bg-opacity));
+  }
+
+  .xl\:focus\:bg-pink-900:focus {
+    --bg-opacity: 1;
+    background-color: #702459;
+    background-color: rgba(112, 36, 89, var(--bg-opacity));
+  }
+
+  .xl\:bg-none {
+    background-image: none;
+  }
+
+  .xl\:bg-gradient-to-t {
+    background-image: linear-gradient(to top, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-tr {
+    background-image: linear-gradient(to top right, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-r {
+    background-image: linear-gradient(to right, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-br {
+    background-image: linear-gradient(to bottom right, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-b {
+    background-image: linear-gradient(to bottom, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-bl {
+    background-image: linear-gradient(to bottom left, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-l {
+    background-image: linear-gradient(to left, var(--gradient-color-stops));
+  }
+
+  .xl\:bg-gradient-to-tl {
+    background-image: linear-gradient(to top left, var(--gradient-color-stops));
+  }
+
+  .xl\:from-transparent {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:from-current {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:from-black {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:from-white {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:from-gray-100 {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:from-gray-200 {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:from-gray-300 {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:from-gray-400 {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:from-gray-500 {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:from-gray-600 {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:from-gray-700 {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:from-gray-800 {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:from-gray-900 {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:from-red-100 {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:from-red-200 {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:from-red-300 {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:from-red-400 {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:from-red-500 {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:from-red-600 {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:from-red-700 {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:from-red-800 {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:from-red-900 {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:from-orange-100 {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:from-orange-200 {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:from-orange-300 {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:from-orange-400 {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:from-orange-500 {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:from-orange-600 {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:from-orange-700 {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:from-orange-800 {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:from-orange-900 {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:from-yellow-100 {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:from-yellow-200 {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:from-yellow-300 {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:from-yellow-400 {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:from-yellow-500 {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:from-yellow-600 {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:from-yellow-700 {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:from-yellow-800 {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:from-yellow-900 {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:from-green-100 {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:from-green-200 {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:from-green-300 {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:from-green-400 {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:from-green-500 {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:from-green-600 {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:from-green-700 {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:from-green-800 {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:from-green-900 {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:from-teal-100 {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:from-teal-200 {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:from-teal-300 {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:from-teal-400 {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:from-teal-500 {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:from-teal-600 {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:from-teal-700 {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:from-teal-800 {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:from-teal-900 {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:from-blue-100 {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:from-blue-200 {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:from-blue-300 {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:from-blue-400 {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:from-blue-500 {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:from-blue-600 {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:from-blue-700 {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:from-blue-800 {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:from-blue-900 {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:from-indigo-100 {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:from-indigo-200 {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:from-indigo-300 {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:from-indigo-400 {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:from-indigo-500 {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:from-indigo-600 {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:from-indigo-700 {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:from-indigo-800 {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:from-indigo-900 {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:from-purple-100 {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:from-purple-200 {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:from-purple-300 {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:from-purple-400 {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:from-purple-500 {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:from-purple-600 {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:from-purple-700 {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:from-purple-800 {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:from-purple-900 {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:from-pink-100 {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:from-pink-200 {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:from-pink-300 {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:from-pink-400 {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:from-pink-500 {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:from-pink-600 {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:from-pink-700 {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:from-pink-800 {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:from-pink-900 {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:via-transparent {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:via-current {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:via-black {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:via-white {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:via-gray-100 {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:via-gray-200 {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:via-gray-300 {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:via-gray-400 {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:via-gray-500 {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:via-gray-600 {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:via-gray-700 {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:via-gray-800 {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:via-gray-900 {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:via-red-100 {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:via-red-200 {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:via-red-300 {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:via-red-400 {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:via-red-500 {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:via-red-600 {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:via-red-700 {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:via-red-800 {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:via-red-900 {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:via-orange-100 {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:via-orange-200 {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:via-orange-300 {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:via-orange-400 {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:via-orange-500 {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:via-orange-600 {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:via-orange-700 {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:via-orange-800 {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:via-orange-900 {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:via-yellow-100 {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:via-yellow-200 {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:via-yellow-300 {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:via-yellow-400 {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:via-yellow-500 {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:via-yellow-600 {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:via-yellow-700 {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:via-yellow-800 {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:via-yellow-900 {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:via-green-100 {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:via-green-200 {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:via-green-300 {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:via-green-400 {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:via-green-500 {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:via-green-600 {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:via-green-700 {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:via-green-800 {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:via-green-900 {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:via-teal-100 {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:via-teal-200 {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:via-teal-300 {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:via-teal-400 {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:via-teal-500 {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:via-teal-600 {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:via-teal-700 {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:via-teal-800 {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:via-teal-900 {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:via-blue-100 {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:via-blue-200 {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:via-blue-300 {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:via-blue-400 {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:via-blue-500 {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:via-blue-600 {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:via-blue-700 {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:via-blue-800 {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:via-blue-900 {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:via-indigo-100 {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:via-indigo-200 {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:via-indigo-300 {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:via-indigo-400 {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:via-indigo-500 {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:via-indigo-600 {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:via-indigo-700 {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:via-indigo-800 {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:via-indigo-900 {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:via-purple-100 {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:via-purple-200 {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:via-purple-300 {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:via-purple-400 {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:via-purple-500 {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:via-purple-600 {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:via-purple-700 {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:via-purple-800 {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:via-purple-900 {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:via-pink-100 {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:via-pink-200 {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:via-pink-300 {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:via-pink-400 {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:via-pink-500 {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:via-pink-600 {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:via-pink-700 {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:via-pink-800 {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:via-pink-900 {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:to-transparent {
+    --gradient-to-color: transparent;
+  }
+
+  .xl\:to-current {
+    --gradient-to-color: currentColor;
+  }
+
+  .xl\:to-black {
+    --gradient-to-color: #000;
+  }
+
+  .xl\:to-white {
+    --gradient-to-color: #fff;
+  }
+
+  .xl\:to-gray-100 {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .xl\:to-gray-200 {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .xl\:to-gray-300 {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .xl\:to-gray-400 {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .xl\:to-gray-500 {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .xl\:to-gray-600 {
+    --gradient-to-color: #718096;
+  }
+
+  .xl\:to-gray-700 {
+    --gradient-to-color: #4a5568;
+  }
+
+  .xl\:to-gray-800 {
+    --gradient-to-color: #2d3748;
+  }
+
+  .xl\:to-gray-900 {
+    --gradient-to-color: #1a202c;
+  }
+
+  .xl\:to-red-100 {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .xl\:to-red-200 {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .xl\:to-red-300 {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .xl\:to-red-400 {
+    --gradient-to-color: #fc8181;
+  }
+
+  .xl\:to-red-500 {
+    --gradient-to-color: #f56565;
+  }
+
+  .xl\:to-red-600 {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .xl\:to-red-700 {
+    --gradient-to-color: #c53030;
+  }
+
+  .xl\:to-red-800 {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .xl\:to-red-900 {
+    --gradient-to-color: #742a2a;
+  }
+
+  .xl\:to-orange-100 {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .xl\:to-orange-200 {
+    --gradient-to-color: #feebc8;
+  }
+
+  .xl\:to-orange-300 {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .xl\:to-orange-400 {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .xl\:to-orange-500 {
+    --gradient-to-color: #ed8936;
+  }
+
+  .xl\:to-orange-600 {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .xl\:to-orange-700 {
+    --gradient-to-color: #c05621;
+  }
+
+  .xl\:to-orange-800 {
+    --gradient-to-color: #9c4221;
+  }
+
+  .xl\:to-orange-900 {
+    --gradient-to-color: #7b341e;
+  }
+
+  .xl\:to-yellow-100 {
+    --gradient-to-color: #fffff0;
+  }
+
+  .xl\:to-yellow-200 {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .xl\:to-yellow-300 {
+    --gradient-to-color: #faf089;
+  }
+
+  .xl\:to-yellow-400 {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .xl\:to-yellow-500 {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .xl\:to-yellow-600 {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .xl\:to-yellow-700 {
+    --gradient-to-color: #b7791f;
+  }
+
+  .xl\:to-yellow-800 {
+    --gradient-to-color: #975a16;
+  }
+
+  .xl\:to-yellow-900 {
+    --gradient-to-color: #744210;
+  }
+
+  .xl\:to-green-100 {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .xl\:to-green-200 {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .xl\:to-green-300 {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .xl\:to-green-400 {
+    --gradient-to-color: #68d391;
+  }
+
+  .xl\:to-green-500 {
+    --gradient-to-color: #48bb78;
+  }
+
+  .xl\:to-green-600 {
+    --gradient-to-color: #38a169;
+  }
+
+  .xl\:to-green-700 {
+    --gradient-to-color: #2f855a;
+  }
+
+  .xl\:to-green-800 {
+    --gradient-to-color: #276749;
+  }
+
+  .xl\:to-green-900 {
+    --gradient-to-color: #22543d;
+  }
+
+  .xl\:to-teal-100 {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .xl\:to-teal-200 {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .xl\:to-teal-300 {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .xl\:to-teal-400 {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .xl\:to-teal-500 {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .xl\:to-teal-600 {
+    --gradient-to-color: #319795;
+  }
+
+  .xl\:to-teal-700 {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .xl\:to-teal-800 {
+    --gradient-to-color: #285e61;
+  }
+
+  .xl\:to-teal-900 {
+    --gradient-to-color: #234e52;
+  }
+
+  .xl\:to-blue-100 {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .xl\:to-blue-200 {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .xl\:to-blue-300 {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .xl\:to-blue-400 {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .xl\:to-blue-500 {
+    --gradient-to-color: #4299e1;
+  }
+
+  .xl\:to-blue-600 {
+    --gradient-to-color: #3182ce;
+  }
+
+  .xl\:to-blue-700 {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .xl\:to-blue-800 {
+    --gradient-to-color: #2c5282;
+  }
+
+  .xl\:to-blue-900 {
+    --gradient-to-color: #2a4365;
+  }
+
+  .xl\:to-indigo-100 {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .xl\:to-indigo-200 {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .xl\:to-indigo-300 {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .xl\:to-indigo-400 {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .xl\:to-indigo-500 {
+    --gradient-to-color: #667eea;
+  }
+
+  .xl\:to-indigo-600 {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .xl\:to-indigo-700 {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .xl\:to-indigo-800 {
+    --gradient-to-color: #434190;
+  }
+
+  .xl\:to-indigo-900 {
+    --gradient-to-color: #3c366b;
+  }
+
+  .xl\:to-purple-100 {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .xl\:to-purple-200 {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .xl\:to-purple-300 {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .xl\:to-purple-400 {
+    --gradient-to-color: #b794f4;
+  }
+
+  .xl\:to-purple-500 {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .xl\:to-purple-600 {
+    --gradient-to-color: #805ad5;
+  }
+
+  .xl\:to-purple-700 {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .xl\:to-purple-800 {
+    --gradient-to-color: #553c9a;
+  }
+
+  .xl\:to-purple-900 {
+    --gradient-to-color: #44337a;
+  }
+
+  .xl\:to-pink-100 {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .xl\:to-pink-200 {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .xl\:to-pink-300 {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .xl\:to-pink-400 {
+    --gradient-to-color: #f687b3;
+  }
+
+  .xl\:to-pink-500 {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .xl\:to-pink-600 {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .xl\:to-pink-700 {
+    --gradient-to-color: #b83280;
+  }
+
+  .xl\:to-pink-800 {
+    --gradient-to-color: #97266d;
+  }
+
+  .xl\:to-pink-900 {
+    --gradient-to-color: #702459;
+  }
+
+  .xl\:hover\:from-transparent:hover {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:from-current:hover {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:from-black:hover {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:from-white:hover {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:from-gray-100:hover {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:hover\:from-gray-200:hover {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:hover\:from-gray-300:hover {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:hover\:from-gray-400:hover {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:hover\:from-gray-500:hover {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:hover\:from-gray-600:hover {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:hover\:from-gray-700:hover {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:hover\:from-gray-800:hover {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:hover\:from-gray-900:hover {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:hover\:from-red-100:hover {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:hover\:from-red-200:hover {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:hover\:from-red-300:hover {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:hover\:from-red-400:hover {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:hover\:from-red-500:hover {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:hover\:from-red-600:hover {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:hover\:from-red-700:hover {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:hover\:from-red-800:hover {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:hover\:from-red-900:hover {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:hover\:from-orange-100:hover {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:hover\:from-orange-200:hover {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:hover\:from-orange-300:hover {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:hover\:from-orange-400:hover {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:hover\:from-orange-500:hover {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:hover\:from-orange-600:hover {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:hover\:from-orange-700:hover {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:hover\:from-orange-800:hover {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:hover\:from-orange-900:hover {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:hover\:from-yellow-100:hover {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:hover\:from-yellow-200:hover {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:hover\:from-yellow-300:hover {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:hover\:from-yellow-400:hover {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:hover\:from-yellow-500:hover {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:hover\:from-yellow-600:hover {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:hover\:from-yellow-700:hover {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:hover\:from-yellow-800:hover {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:hover\:from-yellow-900:hover {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:hover\:from-green-100:hover {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:hover\:from-green-200:hover {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:hover\:from-green-300:hover {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:hover\:from-green-400:hover {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:hover\:from-green-500:hover {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:hover\:from-green-600:hover {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:hover\:from-green-700:hover {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:hover\:from-green-800:hover {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:hover\:from-green-900:hover {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:hover\:from-teal-100:hover {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:hover\:from-teal-200:hover {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:hover\:from-teal-300:hover {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:hover\:from-teal-400:hover {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:hover\:from-teal-500:hover {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:hover\:from-teal-600:hover {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:hover\:from-teal-700:hover {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:hover\:from-teal-800:hover {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:hover\:from-teal-900:hover {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:hover\:from-blue-100:hover {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:hover\:from-blue-200:hover {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:hover\:from-blue-300:hover {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:hover\:from-blue-400:hover {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:hover\:from-blue-500:hover {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:hover\:from-blue-600:hover {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:hover\:from-blue-700:hover {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:hover\:from-blue-800:hover {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:hover\:from-blue-900:hover {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:hover\:from-indigo-100:hover {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:hover\:from-indigo-200:hover {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:hover\:from-indigo-300:hover {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:hover\:from-indigo-400:hover {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:hover\:from-indigo-500:hover {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:hover\:from-indigo-600:hover {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:hover\:from-indigo-700:hover {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:hover\:from-indigo-800:hover {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:hover\:from-indigo-900:hover {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:hover\:from-purple-100:hover {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:hover\:from-purple-200:hover {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:hover\:from-purple-300:hover {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:hover\:from-purple-400:hover {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:hover\:from-purple-500:hover {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:hover\:from-purple-600:hover {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:hover\:from-purple-700:hover {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:hover\:from-purple-800:hover {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:hover\:from-purple-900:hover {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:hover\:from-pink-100:hover {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:hover\:from-pink-200:hover {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:hover\:from-pink-300:hover {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:hover\:from-pink-400:hover {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:hover\:from-pink-500:hover {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:hover\:from-pink-600:hover {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:hover\:from-pink-700:hover {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:hover\:from-pink-800:hover {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:hover\:from-pink-900:hover {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:hover\:via-transparent:hover {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:via-current:hover {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:via-black:hover {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:hover\:via-white:hover {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:hover\:via-gray-100:hover {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:hover\:via-gray-200:hover {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:hover\:via-gray-300:hover {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:hover\:via-gray-400:hover {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:hover\:via-gray-500:hover {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:hover\:via-gray-600:hover {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:hover\:via-gray-700:hover {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:hover\:via-gray-800:hover {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:hover\:via-gray-900:hover {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:hover\:via-red-100:hover {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:hover\:via-red-200:hover {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:hover\:via-red-300:hover {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:hover\:via-red-400:hover {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:hover\:via-red-500:hover {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:hover\:via-red-600:hover {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:hover\:via-red-700:hover {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:hover\:via-red-800:hover {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:hover\:via-red-900:hover {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:hover\:via-orange-100:hover {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:hover\:via-orange-200:hover {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:hover\:via-orange-300:hover {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:hover\:via-orange-400:hover {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:hover\:via-orange-500:hover {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:hover\:via-orange-600:hover {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:hover\:via-orange-700:hover {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:hover\:via-orange-800:hover {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:hover\:via-orange-900:hover {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:hover\:via-yellow-100:hover {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:hover\:via-yellow-200:hover {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:hover\:via-yellow-300:hover {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:hover\:via-yellow-400:hover {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:hover\:via-yellow-500:hover {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:hover\:via-yellow-600:hover {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:hover\:via-yellow-700:hover {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:hover\:via-yellow-800:hover {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:hover\:via-yellow-900:hover {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:hover\:via-green-100:hover {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:hover\:via-green-200:hover {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:hover\:via-green-300:hover {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:hover\:via-green-400:hover {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:hover\:via-green-500:hover {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:hover\:via-green-600:hover {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:hover\:via-green-700:hover {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:hover\:via-green-800:hover {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:hover\:via-green-900:hover {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:hover\:via-teal-100:hover {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:hover\:via-teal-200:hover {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:hover\:via-teal-300:hover {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:hover\:via-teal-400:hover {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:hover\:via-teal-500:hover {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:hover\:via-teal-600:hover {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:hover\:via-teal-700:hover {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:hover\:via-teal-800:hover {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:hover\:via-teal-900:hover {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:hover\:via-blue-100:hover {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:hover\:via-blue-200:hover {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:hover\:via-blue-300:hover {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:hover\:via-blue-400:hover {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:hover\:via-blue-500:hover {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:hover\:via-blue-600:hover {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:hover\:via-blue-700:hover {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:hover\:via-blue-800:hover {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:hover\:via-blue-900:hover {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:hover\:via-indigo-100:hover {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:hover\:via-indigo-200:hover {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:hover\:via-indigo-300:hover {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:hover\:via-indigo-400:hover {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:hover\:via-indigo-500:hover {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:hover\:via-indigo-600:hover {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:hover\:via-indigo-700:hover {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:hover\:via-indigo-800:hover {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:hover\:via-indigo-900:hover {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:hover\:via-purple-100:hover {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:hover\:via-purple-200:hover {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:hover\:via-purple-300:hover {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:hover\:via-purple-400:hover {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:hover\:via-purple-500:hover {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:hover\:via-purple-600:hover {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:hover\:via-purple-700:hover {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:hover\:via-purple-800:hover {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:hover\:via-purple-900:hover {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:hover\:via-pink-100:hover {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:hover\:via-pink-200:hover {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:hover\:via-pink-300:hover {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:hover\:via-pink-400:hover {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:hover\:via-pink-500:hover {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:hover\:via-pink-600:hover {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:hover\:via-pink-700:hover {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:hover\:via-pink-800:hover {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:hover\:via-pink-900:hover {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:hover\:to-transparent:hover {
+    --gradient-to-color: transparent;
+  }
+
+  .xl\:hover\:to-current:hover {
+    --gradient-to-color: currentColor;
+  }
+
+  .xl\:hover\:to-black:hover {
+    --gradient-to-color: #000;
+  }
+
+  .xl\:hover\:to-white:hover {
+    --gradient-to-color: #fff;
+  }
+
+  .xl\:hover\:to-gray-100:hover {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .xl\:hover\:to-gray-200:hover {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .xl\:hover\:to-gray-300:hover {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .xl\:hover\:to-gray-400:hover {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .xl\:hover\:to-gray-500:hover {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .xl\:hover\:to-gray-600:hover {
+    --gradient-to-color: #718096;
+  }
+
+  .xl\:hover\:to-gray-700:hover {
+    --gradient-to-color: #4a5568;
+  }
+
+  .xl\:hover\:to-gray-800:hover {
+    --gradient-to-color: #2d3748;
+  }
+
+  .xl\:hover\:to-gray-900:hover {
+    --gradient-to-color: #1a202c;
+  }
+
+  .xl\:hover\:to-red-100:hover {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .xl\:hover\:to-red-200:hover {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .xl\:hover\:to-red-300:hover {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .xl\:hover\:to-red-400:hover {
+    --gradient-to-color: #fc8181;
+  }
+
+  .xl\:hover\:to-red-500:hover {
+    --gradient-to-color: #f56565;
+  }
+
+  .xl\:hover\:to-red-600:hover {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .xl\:hover\:to-red-700:hover {
+    --gradient-to-color: #c53030;
+  }
+
+  .xl\:hover\:to-red-800:hover {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .xl\:hover\:to-red-900:hover {
+    --gradient-to-color: #742a2a;
+  }
+
+  .xl\:hover\:to-orange-100:hover {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .xl\:hover\:to-orange-200:hover {
+    --gradient-to-color: #feebc8;
+  }
+
+  .xl\:hover\:to-orange-300:hover {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .xl\:hover\:to-orange-400:hover {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .xl\:hover\:to-orange-500:hover {
+    --gradient-to-color: #ed8936;
+  }
+
+  .xl\:hover\:to-orange-600:hover {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .xl\:hover\:to-orange-700:hover {
+    --gradient-to-color: #c05621;
+  }
+
+  .xl\:hover\:to-orange-800:hover {
+    --gradient-to-color: #9c4221;
+  }
+
+  .xl\:hover\:to-orange-900:hover {
+    --gradient-to-color: #7b341e;
+  }
+
+  .xl\:hover\:to-yellow-100:hover {
+    --gradient-to-color: #fffff0;
+  }
+
+  .xl\:hover\:to-yellow-200:hover {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .xl\:hover\:to-yellow-300:hover {
+    --gradient-to-color: #faf089;
+  }
+
+  .xl\:hover\:to-yellow-400:hover {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .xl\:hover\:to-yellow-500:hover {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .xl\:hover\:to-yellow-600:hover {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .xl\:hover\:to-yellow-700:hover {
+    --gradient-to-color: #b7791f;
+  }
+
+  .xl\:hover\:to-yellow-800:hover {
+    --gradient-to-color: #975a16;
+  }
+
+  .xl\:hover\:to-yellow-900:hover {
+    --gradient-to-color: #744210;
+  }
+
+  .xl\:hover\:to-green-100:hover {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .xl\:hover\:to-green-200:hover {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .xl\:hover\:to-green-300:hover {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .xl\:hover\:to-green-400:hover {
+    --gradient-to-color: #68d391;
+  }
+
+  .xl\:hover\:to-green-500:hover {
+    --gradient-to-color: #48bb78;
+  }
+
+  .xl\:hover\:to-green-600:hover {
+    --gradient-to-color: #38a169;
+  }
+
+  .xl\:hover\:to-green-700:hover {
+    --gradient-to-color: #2f855a;
+  }
+
+  .xl\:hover\:to-green-800:hover {
+    --gradient-to-color: #276749;
+  }
+
+  .xl\:hover\:to-green-900:hover {
+    --gradient-to-color: #22543d;
+  }
+
+  .xl\:hover\:to-teal-100:hover {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .xl\:hover\:to-teal-200:hover {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .xl\:hover\:to-teal-300:hover {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .xl\:hover\:to-teal-400:hover {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .xl\:hover\:to-teal-500:hover {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .xl\:hover\:to-teal-600:hover {
+    --gradient-to-color: #319795;
+  }
+
+  .xl\:hover\:to-teal-700:hover {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .xl\:hover\:to-teal-800:hover {
+    --gradient-to-color: #285e61;
+  }
+
+  .xl\:hover\:to-teal-900:hover {
+    --gradient-to-color: #234e52;
+  }
+
+  .xl\:hover\:to-blue-100:hover {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .xl\:hover\:to-blue-200:hover {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .xl\:hover\:to-blue-300:hover {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .xl\:hover\:to-blue-400:hover {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .xl\:hover\:to-blue-500:hover {
+    --gradient-to-color: #4299e1;
+  }
+
+  .xl\:hover\:to-blue-600:hover {
+    --gradient-to-color: #3182ce;
+  }
+
+  .xl\:hover\:to-blue-700:hover {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .xl\:hover\:to-blue-800:hover {
+    --gradient-to-color: #2c5282;
+  }
+
+  .xl\:hover\:to-blue-900:hover {
+    --gradient-to-color: #2a4365;
+  }
+
+  .xl\:hover\:to-indigo-100:hover {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .xl\:hover\:to-indigo-200:hover {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .xl\:hover\:to-indigo-300:hover {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .xl\:hover\:to-indigo-400:hover {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .xl\:hover\:to-indigo-500:hover {
+    --gradient-to-color: #667eea;
+  }
+
+  .xl\:hover\:to-indigo-600:hover {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .xl\:hover\:to-indigo-700:hover {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .xl\:hover\:to-indigo-800:hover {
+    --gradient-to-color: #434190;
+  }
+
+  .xl\:hover\:to-indigo-900:hover {
+    --gradient-to-color: #3c366b;
+  }
+
+  .xl\:hover\:to-purple-100:hover {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .xl\:hover\:to-purple-200:hover {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .xl\:hover\:to-purple-300:hover {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .xl\:hover\:to-purple-400:hover {
+    --gradient-to-color: #b794f4;
+  }
+
+  .xl\:hover\:to-purple-500:hover {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .xl\:hover\:to-purple-600:hover {
+    --gradient-to-color: #805ad5;
+  }
+
+  .xl\:hover\:to-purple-700:hover {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .xl\:hover\:to-purple-800:hover {
+    --gradient-to-color: #553c9a;
+  }
+
+  .xl\:hover\:to-purple-900:hover {
+    --gradient-to-color: #44337a;
+  }
+
+  .xl\:hover\:to-pink-100:hover {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .xl\:hover\:to-pink-200:hover {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .xl\:hover\:to-pink-300:hover {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .xl\:hover\:to-pink-400:hover {
+    --gradient-to-color: #f687b3;
+  }
+
+  .xl\:hover\:to-pink-500:hover {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .xl\:hover\:to-pink-600:hover {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .xl\:hover\:to-pink-700:hover {
+    --gradient-to-color: #b83280;
+  }
+
+  .xl\:hover\:to-pink-800:hover {
+    --gradient-to-color: #97266d;
+  }
+
+  .xl\:hover\:to-pink-900:hover {
+    --gradient-to-color: #702459;
+  }
+
+  .xl\:focus\:from-transparent:focus {
+    --gradient-from-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:from-current:focus {
+    --gradient-from-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:from-black:focus {
+    --gradient-from-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:from-white:focus {
+    --gradient-from-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:from-gray-100:focus {
+    --gradient-from-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:focus\:from-gray-200:focus {
+    --gradient-from-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:focus\:from-gray-300:focus {
+    --gradient-from-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:focus\:from-gray-400:focus {
+    --gradient-from-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:focus\:from-gray-500:focus {
+    --gradient-from-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:focus\:from-gray-600:focus {
+    --gradient-from-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:focus\:from-gray-700:focus {
+    --gradient-from-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:focus\:from-gray-800:focus {
+    --gradient-from-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:focus\:from-gray-900:focus {
+    --gradient-from-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:focus\:from-red-100:focus {
+    --gradient-from-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:focus\:from-red-200:focus {
+    --gradient-from-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:focus\:from-red-300:focus {
+    --gradient-from-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:focus\:from-red-400:focus {
+    --gradient-from-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:focus\:from-red-500:focus {
+    --gradient-from-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:focus\:from-red-600:focus {
+    --gradient-from-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:focus\:from-red-700:focus {
+    --gradient-from-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:focus\:from-red-800:focus {
+    --gradient-from-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:focus\:from-red-900:focus {
+    --gradient-from-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:focus\:from-orange-100:focus {
+    --gradient-from-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:focus\:from-orange-200:focus {
+    --gradient-from-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:focus\:from-orange-300:focus {
+    --gradient-from-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:focus\:from-orange-400:focus {
+    --gradient-from-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:focus\:from-orange-500:focus {
+    --gradient-from-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:focus\:from-orange-600:focus {
+    --gradient-from-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:focus\:from-orange-700:focus {
+    --gradient-from-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:focus\:from-orange-800:focus {
+    --gradient-from-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:focus\:from-orange-900:focus {
+    --gradient-from-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:focus\:from-yellow-100:focus {
+    --gradient-from-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:focus\:from-yellow-200:focus {
+    --gradient-from-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:focus\:from-yellow-300:focus {
+    --gradient-from-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:focus\:from-yellow-400:focus {
+    --gradient-from-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:focus\:from-yellow-500:focus {
+    --gradient-from-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:focus\:from-yellow-600:focus {
+    --gradient-from-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:focus\:from-yellow-700:focus {
+    --gradient-from-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:focus\:from-yellow-800:focus {
+    --gradient-from-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:focus\:from-yellow-900:focus {
+    --gradient-from-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:focus\:from-green-100:focus {
+    --gradient-from-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:focus\:from-green-200:focus {
+    --gradient-from-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:focus\:from-green-300:focus {
+    --gradient-from-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:focus\:from-green-400:focus {
+    --gradient-from-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:focus\:from-green-500:focus {
+    --gradient-from-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:focus\:from-green-600:focus {
+    --gradient-from-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:focus\:from-green-700:focus {
+    --gradient-from-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:focus\:from-green-800:focus {
+    --gradient-from-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:focus\:from-green-900:focus {
+    --gradient-from-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:focus\:from-teal-100:focus {
+    --gradient-from-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:focus\:from-teal-200:focus {
+    --gradient-from-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:focus\:from-teal-300:focus {
+    --gradient-from-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:focus\:from-teal-400:focus {
+    --gradient-from-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:focus\:from-teal-500:focus {
+    --gradient-from-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:focus\:from-teal-600:focus {
+    --gradient-from-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:focus\:from-teal-700:focus {
+    --gradient-from-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:focus\:from-teal-800:focus {
+    --gradient-from-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:focus\:from-teal-900:focus {
+    --gradient-from-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:focus\:from-blue-100:focus {
+    --gradient-from-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:focus\:from-blue-200:focus {
+    --gradient-from-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:focus\:from-blue-300:focus {
+    --gradient-from-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:focus\:from-blue-400:focus {
+    --gradient-from-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:focus\:from-blue-500:focus {
+    --gradient-from-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:focus\:from-blue-600:focus {
+    --gradient-from-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:focus\:from-blue-700:focus {
+    --gradient-from-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:focus\:from-blue-800:focus {
+    --gradient-from-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:focus\:from-blue-900:focus {
+    --gradient-from-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:focus\:from-indigo-100:focus {
+    --gradient-from-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:focus\:from-indigo-200:focus {
+    --gradient-from-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:focus\:from-indigo-300:focus {
+    --gradient-from-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:focus\:from-indigo-400:focus {
+    --gradient-from-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:focus\:from-indigo-500:focus {
+    --gradient-from-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:focus\:from-indigo-600:focus {
+    --gradient-from-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:focus\:from-indigo-700:focus {
+    --gradient-from-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:focus\:from-indigo-800:focus {
+    --gradient-from-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:focus\:from-indigo-900:focus {
+    --gradient-from-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:focus\:from-purple-100:focus {
+    --gradient-from-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:focus\:from-purple-200:focus {
+    --gradient-from-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:focus\:from-purple-300:focus {
+    --gradient-from-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:focus\:from-purple-400:focus {
+    --gradient-from-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:focus\:from-purple-500:focus {
+    --gradient-from-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:focus\:from-purple-600:focus {
+    --gradient-from-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:focus\:from-purple-700:focus {
+    --gradient-from-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:focus\:from-purple-800:focus {
+    --gradient-from-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:focus\:from-purple-900:focus {
+    --gradient-from-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:focus\:from-pink-100:focus {
+    --gradient-from-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:focus\:from-pink-200:focus {
+    --gradient-from-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:focus\:from-pink-300:focus {
+    --gradient-from-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:focus\:from-pink-400:focus {
+    --gradient-from-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:focus\:from-pink-500:focus {
+    --gradient-from-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:focus\:from-pink-600:focus {
+    --gradient-from-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:focus\:from-pink-700:focus {
+    --gradient-from-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:focus\:from-pink-800:focus {
+    --gradient-from-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:focus\:from-pink-900:focus {
+    --gradient-from-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:focus\:via-transparent:focus {
+    --gradient-via-color: transparent;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:via-current:focus {
+    --gradient-via-color: currentColor;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:via-black:focus {
+    --gradient-via-color: #000;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(0, 0, 0, 0));
+  }
+
+  .xl\:focus\:via-white:focus {
+    --gradient-via-color: #fff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 255, 0));
+  }
+
+  .xl\:focus\:via-gray-100:focus {
+    --gradient-via-color: #f7fafc;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(247, 250, 252, 0));
+  }
+
+  .xl\:focus\:via-gray-200:focus {
+    --gradient-via-color: #edf2f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 242, 247, 0));
+  }
+
+  .xl\:focus\:via-gray-300:focus {
+    --gradient-via-color: #e2e8f0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(226, 232, 240, 0));
+  }
+
+  .xl\:focus\:via-gray-400:focus {
+    --gradient-via-color: #cbd5e0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(203, 213, 224, 0));
+  }
+
+  .xl\:focus\:via-gray-500:focus {
+    --gradient-via-color: #a0aec0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(160, 174, 192, 0));
+  }
+
+  .xl\:focus\:via-gray-600:focus {
+    --gradient-via-color: #718096;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(113, 128, 150, 0));
+  }
+
+  .xl\:focus\:via-gray-700:focus {
+    --gradient-via-color: #4a5568;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(74, 85, 104, 0));
+  }
+
+  .xl\:focus\:via-gray-800:focus {
+    --gradient-via-color: #2d3748;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(45, 55, 72, 0));
+  }
+
+  .xl\:focus\:via-gray-900:focus {
+    --gradient-via-color: #1a202c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(26, 32, 44, 0));
+  }
+
+  .xl\:focus\:via-red-100:focus {
+    --gradient-via-color: #fff5f5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 245, 0));
+  }
+
+  .xl\:focus\:via-red-200:focus {
+    --gradient-via-color: #fed7d7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 215, 0));
+  }
+
+  .xl\:focus\:via-red-300:focus {
+    --gradient-via-color: #feb2b2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 178, 178, 0));
+  }
+
+  .xl\:focus\:via-red-400:focus {
+    --gradient-via-color: #fc8181;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(252, 129, 129, 0));
+  }
+
+  .xl\:focus\:via-red-500:focus {
+    --gradient-via-color: #f56565;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(245, 101, 101, 0));
+  }
+
+  .xl\:focus\:via-red-600:focus {
+    --gradient-via-color: #e53e3e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(229, 62, 62, 0));
+  }
+
+  .xl\:focus\:via-red-700:focus {
+    --gradient-via-color: #c53030;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(197, 48, 48, 0));
+  }
+
+  .xl\:focus\:via-red-800:focus {
+    --gradient-via-color: #9b2c2c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(155, 44, 44, 0));
+  }
+
+  .xl\:focus\:via-red-900:focus {
+    --gradient-via-color: #742a2a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 42, 42, 0));
+  }
+
+  .xl\:focus\:via-orange-100:focus {
+    --gradient-via-color: #fffaf0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 250, 240, 0));
+  }
+
+  .xl\:focus\:via-orange-200:focus {
+    --gradient-via-color: #feebc8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 235, 200, 0));
+  }
+
+  .xl\:focus\:via-orange-300:focus {
+    --gradient-via-color: #fbd38d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 211, 141, 0));
+  }
+
+  .xl\:focus\:via-orange-400:focus {
+    --gradient-via-color: #f6ad55;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 173, 85, 0));
+  }
+
+  .xl\:focus\:via-orange-500:focus {
+    --gradient-via-color: #ed8936;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 137, 54, 0));
+  }
+
+  .xl\:focus\:via-orange-600:focus {
+    --gradient-via-color: #dd6b20;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(221, 107, 32, 0));
+  }
+
+  .xl\:focus\:via-orange-700:focus {
+    --gradient-via-color: #c05621;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(192, 86, 33, 0));
+  }
+
+  .xl\:focus\:via-orange-800:focus {
+    --gradient-via-color: #9c4221;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(156, 66, 33, 0));
+  }
+
+  .xl\:focus\:via-orange-900:focus {
+    --gradient-via-color: #7b341e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(123, 52, 30, 0));
+  }
+
+  .xl\:focus\:via-yellow-100:focus {
+    --gradient-via-color: #fffff0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 255, 240, 0));
+  }
+
+  .xl\:focus\:via-yellow-200:focus {
+    --gradient-via-color: #fefcbf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 252, 191, 0));
+  }
+
+  .xl\:focus\:via-yellow-300:focus {
+    --gradient-via-color: #faf089;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 240, 137, 0));
+  }
+
+  .xl\:focus\:via-yellow-400:focus {
+    --gradient-via-color: #f6e05e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 224, 94, 0));
+  }
+
+  .xl\:focus\:via-yellow-500:focus {
+    --gradient-via-color: #ecc94b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(236, 201, 75, 0));
+  }
+
+  .xl\:focus\:via-yellow-600:focus {
+    --gradient-via-color: #d69e2e;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 158, 46, 0));
+  }
+
+  .xl\:focus\:via-yellow-700:focus {
+    --gradient-via-color: #b7791f;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 121, 31, 0));
+  }
+
+  .xl\:focus\:via-yellow-800:focus {
+    --gradient-via-color: #975a16;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 90, 22, 0));
+  }
+
+  .xl\:focus\:via-yellow-900:focus {
+    --gradient-via-color: #744210;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(116, 66, 16, 0));
+  }
+
+  .xl\:focus\:via-green-100:focus {
+    --gradient-via-color: #f0fff4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(240, 255, 244, 0));
+  }
+
+  .xl\:focus\:via-green-200:focus {
+    --gradient-via-color: #c6f6d5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(198, 246, 213, 0));
+  }
+
+  .xl\:focus\:via-green-300:focus {
+    --gradient-via-color: #9ae6b4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(154, 230, 180, 0));
+  }
+
+  .xl\:focus\:via-green-400:focus {
+    --gradient-via-color: #68d391;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(104, 211, 145, 0));
+  }
+
+  .xl\:focus\:via-green-500:focus {
+    --gradient-via-color: #48bb78;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(72, 187, 120, 0));
+  }
+
+  .xl\:focus\:via-green-600:focus {
+    --gradient-via-color: #38a169;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 161, 105, 0));
+  }
+
+  .xl\:focus\:via-green-700:focus {
+    --gradient-via-color: #2f855a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(47, 133, 90, 0));
+  }
+
+  .xl\:focus\:via-green-800:focus {
+    --gradient-via-color: #276749;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(39, 103, 73, 0));
+  }
+
+  .xl\:focus\:via-green-900:focus {
+    --gradient-via-color: #22543d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(34, 84, 61, 0));
+  }
+
+  .xl\:focus\:via-teal-100:focus {
+    --gradient-via-color: #e6fffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(230, 255, 250, 0));
+  }
+
+  .xl\:focus\:via-teal-200:focus {
+    --gradient-via-color: #b2f5ea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(178, 245, 234, 0));
+  }
+
+  .xl\:focus\:via-teal-300:focus {
+    --gradient-via-color: #81e6d9;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(129, 230, 217, 0));
+  }
+
+  .xl\:focus\:via-teal-400:focus {
+    --gradient-via-color: #4fd1c5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(79, 209, 197, 0));
+  }
+
+  .xl\:focus\:via-teal-500:focus {
+    --gradient-via-color: #38b2ac;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(56, 178, 172, 0));
+  }
+
+  .xl\:focus\:via-teal-600:focus {
+    --gradient-via-color: #319795;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 151, 149, 0));
+  }
+
+  .xl\:focus\:via-teal-700:focus {
+    --gradient-via-color: #2c7a7b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 122, 123, 0));
+  }
+
+  .xl\:focus\:via-teal-800:focus {
+    --gradient-via-color: #285e61;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(40, 94, 97, 0));
+  }
+
+  .xl\:focus\:via-teal-900:focus {
+    --gradient-via-color: #234e52;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(35, 78, 82, 0));
+  }
+
+  .xl\:focus\:via-blue-100:focus {
+    --gradient-via-color: #ebf8ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 248, 255, 0));
+  }
+
+  .xl\:focus\:via-blue-200:focus {
+    --gradient-via-color: #bee3f8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(190, 227, 248, 0));
+  }
+
+  .xl\:focus\:via-blue-300:focus {
+    --gradient-via-color: #90cdf4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(144, 205, 244, 0));
+  }
+
+  .xl\:focus\:via-blue-400:focus {
+    --gradient-via-color: #63b3ed;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(99, 179, 237, 0));
+  }
+
+  .xl\:focus\:via-blue-500:focus {
+    --gradient-via-color: #4299e1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(66, 153, 225, 0));
+  }
+
+  .xl\:focus\:via-blue-600:focus {
+    --gradient-via-color: #3182ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(49, 130, 206, 0));
+  }
+
+  .xl\:focus\:via-blue-700:focus {
+    --gradient-via-color: #2b6cb0;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(43, 108, 176, 0));
+  }
+
+  .xl\:focus\:via-blue-800:focus {
+    --gradient-via-color: #2c5282;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(44, 82, 130, 0));
+  }
+
+  .xl\:focus\:via-blue-900:focus {
+    --gradient-via-color: #2a4365;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(42, 67, 101, 0));
+  }
+
+  .xl\:focus\:via-indigo-100:focus {
+    --gradient-via-color: #ebf4ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(235, 244, 255, 0));
+  }
+
+  .xl\:focus\:via-indigo-200:focus {
+    --gradient-via-color: #c3dafe;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(195, 218, 254, 0));
+  }
+
+  .xl\:focus\:via-indigo-300:focus {
+    --gradient-via-color: #a3bffa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(163, 191, 250, 0));
+  }
+
+  .xl\:focus\:via-indigo-400:focus {
+    --gradient-via-color: #7f9cf5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(127, 156, 245, 0));
+  }
+
+  .xl\:focus\:via-indigo-500:focus {
+    --gradient-via-color: #667eea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(102, 126, 234, 0));
+  }
+
+  .xl\:focus\:via-indigo-600:focus {
+    --gradient-via-color: #5a67d8;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(90, 103, 216, 0));
+  }
+
+  .xl\:focus\:via-indigo-700:focus {
+    --gradient-via-color: #4c51bf;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(76, 81, 191, 0));
+  }
+
+  .xl\:focus\:via-indigo-800:focus {
+    --gradient-via-color: #434190;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(67, 65, 144, 0));
+  }
+
+  .xl\:focus\:via-indigo-900:focus {
+    --gradient-via-color: #3c366b;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(60, 54, 107, 0));
+  }
+
+  .xl\:focus\:via-purple-100:focus {
+    --gradient-via-color: #faf5ff;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(250, 245, 255, 0));
+  }
+
+  .xl\:focus\:via-purple-200:focus {
+    --gradient-via-color: #e9d8fd;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(233, 216, 253, 0));
+  }
+
+  .xl\:focus\:via-purple-300:focus {
+    --gradient-via-color: #d6bcfa;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(214, 188, 250, 0));
+  }
+
+  .xl\:focus\:via-purple-400:focus {
+    --gradient-via-color: #b794f4;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(183, 148, 244, 0));
+  }
+
+  .xl\:focus\:via-purple-500:focus {
+    --gradient-via-color: #9f7aea;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(159, 122, 234, 0));
+  }
+
+  .xl\:focus\:via-purple-600:focus {
+    --gradient-via-color: #805ad5;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(128, 90, 213, 0));
+  }
+
+  .xl\:focus\:via-purple-700:focus {
+    --gradient-via-color: #6b46c1;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(107, 70, 193, 0));
+  }
+
+  .xl\:focus\:via-purple-800:focus {
+    --gradient-via-color: #553c9a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(85, 60, 154, 0));
+  }
+
+  .xl\:focus\:via-purple-900:focus {
+    --gradient-via-color: #44337a;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(68, 51, 122, 0));
+  }
+
+  .xl\:focus\:via-pink-100:focus {
+    --gradient-via-color: #fff5f7;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(255, 245, 247, 0));
+  }
+
+  .xl\:focus\:via-pink-200:focus {
+    --gradient-via-color: #fed7e2;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(254, 215, 226, 0));
+  }
+
+  .xl\:focus\:via-pink-300:focus {
+    --gradient-via-color: #fbb6ce;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(251, 182, 206, 0));
+  }
+
+  .xl\:focus\:via-pink-400:focus {
+    --gradient-via-color: #f687b3;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(246, 135, 179, 0));
+  }
+
+  .xl\:focus\:via-pink-500:focus {
+    --gradient-via-color: #ed64a6;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(237, 100, 166, 0));
+  }
+
+  .xl\:focus\:via-pink-600:focus {
+    --gradient-via-color: #d53f8c;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(213, 63, 140, 0));
+  }
+
+  .xl\:focus\:via-pink-700:focus {
+    --gradient-via-color: #b83280;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(184, 50, 128, 0));
+  }
+
+  .xl\:focus\:via-pink-800:focus {
+    --gradient-via-color: #97266d;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(151, 38, 109, 0));
+  }
+
+  .xl\:focus\:via-pink-900:focus {
+    --gradient-via-color: #702459;
+    --gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(112, 36, 89, 0));
+  }
+
+  .xl\:focus\:to-transparent:focus {
+    --gradient-to-color: transparent;
+  }
+
+  .xl\:focus\:to-current:focus {
+    --gradient-to-color: currentColor;
+  }
+
+  .xl\:focus\:to-black:focus {
+    --gradient-to-color: #000;
+  }
+
+  .xl\:focus\:to-white:focus {
+    --gradient-to-color: #fff;
+  }
+
+  .xl\:focus\:to-gray-100:focus {
+    --gradient-to-color: #f7fafc;
+  }
+
+  .xl\:focus\:to-gray-200:focus {
+    --gradient-to-color: #edf2f7;
+  }
+
+  .xl\:focus\:to-gray-300:focus {
+    --gradient-to-color: #e2e8f0;
+  }
+
+  .xl\:focus\:to-gray-400:focus {
+    --gradient-to-color: #cbd5e0;
+  }
+
+  .xl\:focus\:to-gray-500:focus {
+    --gradient-to-color: #a0aec0;
+  }
+
+  .xl\:focus\:to-gray-600:focus {
+    --gradient-to-color: #718096;
+  }
+
+  .xl\:focus\:to-gray-700:focus {
+    --gradient-to-color: #4a5568;
+  }
+
+  .xl\:focus\:to-gray-800:focus {
+    --gradient-to-color: #2d3748;
+  }
+
+  .xl\:focus\:to-gray-900:focus {
+    --gradient-to-color: #1a202c;
+  }
+
+  .xl\:focus\:to-red-100:focus {
+    --gradient-to-color: #fff5f5;
+  }
+
+  .xl\:focus\:to-red-200:focus {
+    --gradient-to-color: #fed7d7;
+  }
+
+  .xl\:focus\:to-red-300:focus {
+    --gradient-to-color: #feb2b2;
+  }
+
+  .xl\:focus\:to-red-400:focus {
+    --gradient-to-color: #fc8181;
+  }
+
+  .xl\:focus\:to-red-500:focus {
+    --gradient-to-color: #f56565;
+  }
+
+  .xl\:focus\:to-red-600:focus {
+    --gradient-to-color: #e53e3e;
+  }
+
+  .xl\:focus\:to-red-700:focus {
+    --gradient-to-color: #c53030;
+  }
+
+  .xl\:focus\:to-red-800:focus {
+    --gradient-to-color: #9b2c2c;
+  }
+
+  .xl\:focus\:to-red-900:focus {
+    --gradient-to-color: #742a2a;
+  }
+
+  .xl\:focus\:to-orange-100:focus {
+    --gradient-to-color: #fffaf0;
+  }
+
+  .xl\:focus\:to-orange-200:focus {
+    --gradient-to-color: #feebc8;
+  }
+
+  .xl\:focus\:to-orange-300:focus {
+    --gradient-to-color: #fbd38d;
+  }
+
+  .xl\:focus\:to-orange-400:focus {
+    --gradient-to-color: #f6ad55;
+  }
+
+  .xl\:focus\:to-orange-500:focus {
+    --gradient-to-color: #ed8936;
+  }
+
+  .xl\:focus\:to-orange-600:focus {
+    --gradient-to-color: #dd6b20;
+  }
+
+  .xl\:focus\:to-orange-700:focus {
+    --gradient-to-color: #c05621;
+  }
+
+  .xl\:focus\:to-orange-800:focus {
+    --gradient-to-color: #9c4221;
+  }
+
+  .xl\:focus\:to-orange-900:focus {
+    --gradient-to-color: #7b341e;
+  }
+
+  .xl\:focus\:to-yellow-100:focus {
+    --gradient-to-color: #fffff0;
+  }
+
+  .xl\:focus\:to-yellow-200:focus {
+    --gradient-to-color: #fefcbf;
+  }
+
+  .xl\:focus\:to-yellow-300:focus {
+    --gradient-to-color: #faf089;
+  }
+
+  .xl\:focus\:to-yellow-400:focus {
+    --gradient-to-color: #f6e05e;
+  }
+
+  .xl\:focus\:to-yellow-500:focus {
+    --gradient-to-color: #ecc94b;
+  }
+
+  .xl\:focus\:to-yellow-600:focus {
+    --gradient-to-color: #d69e2e;
+  }
+
+  .xl\:focus\:to-yellow-700:focus {
+    --gradient-to-color: #b7791f;
+  }
+
+  .xl\:focus\:to-yellow-800:focus {
+    --gradient-to-color: #975a16;
+  }
+
+  .xl\:focus\:to-yellow-900:focus {
+    --gradient-to-color: #744210;
+  }
+
+  .xl\:focus\:to-green-100:focus {
+    --gradient-to-color: #f0fff4;
+  }
+
+  .xl\:focus\:to-green-200:focus {
+    --gradient-to-color: #c6f6d5;
+  }
+
+  .xl\:focus\:to-green-300:focus {
+    --gradient-to-color: #9ae6b4;
+  }
+
+  .xl\:focus\:to-green-400:focus {
+    --gradient-to-color: #68d391;
+  }
+
+  .xl\:focus\:to-green-500:focus {
+    --gradient-to-color: #48bb78;
+  }
+
+  .xl\:focus\:to-green-600:focus {
+    --gradient-to-color: #38a169;
+  }
+
+  .xl\:focus\:to-green-700:focus {
+    --gradient-to-color: #2f855a;
+  }
+
+  .xl\:focus\:to-green-800:focus {
+    --gradient-to-color: #276749;
+  }
+
+  .xl\:focus\:to-green-900:focus {
+    --gradient-to-color: #22543d;
+  }
+
+  .xl\:focus\:to-teal-100:focus {
+    --gradient-to-color: #e6fffa;
+  }
+
+  .xl\:focus\:to-teal-200:focus {
+    --gradient-to-color: #b2f5ea;
+  }
+
+  .xl\:focus\:to-teal-300:focus {
+    --gradient-to-color: #81e6d9;
+  }
+
+  .xl\:focus\:to-teal-400:focus {
+    --gradient-to-color: #4fd1c5;
+  }
+
+  .xl\:focus\:to-teal-500:focus {
+    --gradient-to-color: #38b2ac;
+  }
+
+  .xl\:focus\:to-teal-600:focus {
+    --gradient-to-color: #319795;
+  }
+
+  .xl\:focus\:to-teal-700:focus {
+    --gradient-to-color: #2c7a7b;
+  }
+
+  .xl\:focus\:to-teal-800:focus {
+    --gradient-to-color: #285e61;
+  }
+
+  .xl\:focus\:to-teal-900:focus {
+    --gradient-to-color: #234e52;
+  }
+
+  .xl\:focus\:to-blue-100:focus {
+    --gradient-to-color: #ebf8ff;
+  }
+
+  .xl\:focus\:to-blue-200:focus {
+    --gradient-to-color: #bee3f8;
+  }
+
+  .xl\:focus\:to-blue-300:focus {
+    --gradient-to-color: #90cdf4;
+  }
+
+  .xl\:focus\:to-blue-400:focus {
+    --gradient-to-color: #63b3ed;
+  }
+
+  .xl\:focus\:to-blue-500:focus {
+    --gradient-to-color: #4299e1;
+  }
+
+  .xl\:focus\:to-blue-600:focus {
+    --gradient-to-color: #3182ce;
+  }
+
+  .xl\:focus\:to-blue-700:focus {
+    --gradient-to-color: #2b6cb0;
+  }
+
+  .xl\:focus\:to-blue-800:focus {
+    --gradient-to-color: #2c5282;
+  }
+
+  .xl\:focus\:to-blue-900:focus {
+    --gradient-to-color: #2a4365;
+  }
+
+  .xl\:focus\:to-indigo-100:focus {
+    --gradient-to-color: #ebf4ff;
+  }
+
+  .xl\:focus\:to-indigo-200:focus {
+    --gradient-to-color: #c3dafe;
+  }
+
+  .xl\:focus\:to-indigo-300:focus {
+    --gradient-to-color: #a3bffa;
+  }
+
+  .xl\:focus\:to-indigo-400:focus {
+    --gradient-to-color: #7f9cf5;
+  }
+
+  .xl\:focus\:to-indigo-500:focus {
+    --gradient-to-color: #667eea;
+  }
+
+  .xl\:focus\:to-indigo-600:focus {
+    --gradient-to-color: #5a67d8;
+  }
+
+  .xl\:focus\:to-indigo-700:focus {
+    --gradient-to-color: #4c51bf;
+  }
+
+  .xl\:focus\:to-indigo-800:focus {
+    --gradient-to-color: #434190;
+  }
+
+  .xl\:focus\:to-indigo-900:focus {
+    --gradient-to-color: #3c366b;
+  }
+
+  .xl\:focus\:to-purple-100:focus {
+    --gradient-to-color: #faf5ff;
+  }
+
+  .xl\:focus\:to-purple-200:focus {
+    --gradient-to-color: #e9d8fd;
+  }
+
+  .xl\:focus\:to-purple-300:focus {
+    --gradient-to-color: #d6bcfa;
+  }
+
+  .xl\:focus\:to-purple-400:focus {
+    --gradient-to-color: #b794f4;
+  }
+
+  .xl\:focus\:to-purple-500:focus {
+    --gradient-to-color: #9f7aea;
+  }
+
+  .xl\:focus\:to-purple-600:focus {
+    --gradient-to-color: #805ad5;
+  }
+
+  .xl\:focus\:to-purple-700:focus {
+    --gradient-to-color: #6b46c1;
+  }
+
+  .xl\:focus\:to-purple-800:focus {
+    --gradient-to-color: #553c9a;
+  }
+
+  .xl\:focus\:to-purple-900:focus {
+    --gradient-to-color: #44337a;
+  }
+
+  .xl\:focus\:to-pink-100:focus {
+    --gradient-to-color: #fff5f7;
+  }
+
+  .xl\:focus\:to-pink-200:focus {
+    --gradient-to-color: #fed7e2;
+  }
+
+  .xl\:focus\:to-pink-300:focus {
+    --gradient-to-color: #fbb6ce;
+  }
+
+  .xl\:focus\:to-pink-400:focus {
+    --gradient-to-color: #f687b3;
+  }
+
+  .xl\:focus\:to-pink-500:focus {
+    --gradient-to-color: #ed64a6;
+  }
+
+  .xl\:focus\:to-pink-600:focus {
+    --gradient-to-color: #d53f8c;
+  }
+
+  .xl\:focus\:to-pink-700:focus {
+    --gradient-to-color: #b83280;
+  }
+
+  .xl\:focus\:to-pink-800:focus {
+    --gradient-to-color: #97266d;
+  }
+
+  .xl\:focus\:to-pink-900:focus {
+    --gradient-to-color: #702459;
+  }
+
+  .xl\:bg-opacity-0 {
+    --bg-opacity: 0;
+  }
+
+  .xl\:bg-opacity-25 {
+    --bg-opacity: 0.25;
+  }
+
+  .xl\:bg-opacity-50 {
+    --bg-opacity: 0.5;
+  }
+
+  .xl\:bg-opacity-75 {
+    --bg-opacity: 0.75;
+  }
+
+  .xl\:bg-opacity-100 {
+    --bg-opacity: 1;
+  }
+
+  .xl\:hover\:bg-opacity-0:hover {
+    --bg-opacity: 0;
+  }
+
+  .xl\:hover\:bg-opacity-25:hover {
+    --bg-opacity: 0.25;
+  }
+
+  .xl\:hover\:bg-opacity-50:hover {
+    --bg-opacity: 0.5;
+  }
+
+  .xl\:hover\:bg-opacity-75:hover {
+    --bg-opacity: 0.75;
+  }
+
+  .xl\:hover\:bg-opacity-100:hover {
+    --bg-opacity: 1;
+  }
+
+  .xl\:focus\:bg-opacity-0:focus {
+    --bg-opacity: 0;
+  }
+
+  .xl\:focus\:bg-opacity-25:focus {
+    --bg-opacity: 0.25;
+  }
+
+  .xl\:focus\:bg-opacity-50:focus {
+    --bg-opacity: 0.5;
+  }
+
+  .xl\:focus\:bg-opacity-75:focus {
+    --bg-opacity: 0.75;
+  }
+
+  .xl\:focus\:bg-opacity-100:focus {
+    --bg-opacity: 1;
+  }
+
+  .xl\:bg-bottom {
+    background-position: bottom;
+  }
+
+  .xl\:bg-center {
+    background-position: center;
+  }
+
+  .xl\:bg-left {
+    background-position: left;
+  }
+
+  .xl\:bg-left-bottom {
+    background-position: left bottom;
+  }
+
+  .xl\:bg-left-top {
+    background-position: left top;
+  }
+
+  .xl\:bg-right {
+    background-position: right;
+  }
+
+  .xl\:bg-right-bottom {
+    background-position: right bottom;
+  }
+
+  .xl\:bg-right-top {
+    background-position: right top;
+  }
+
+  .xl\:bg-top {
+    background-position: top;
+  }
+
+  .xl\:bg-repeat {
+    background-repeat: repeat;
+  }
+
+  .xl\:bg-no-repeat {
+    background-repeat: no-repeat;
+  }
+
+  .xl\:bg-repeat-x {
+    background-repeat: repeat-x;
+  }
+
+  .xl\:bg-repeat-y {
+    background-repeat: repeat-y;
+  }
+
+  .xl\:bg-repeat-round {
+    background-repeat: round;
+  }
+
+  .xl\:bg-repeat-space {
+    background-repeat: space;
+  }
+
+  .xl\:bg-auto {
+    background-size: auto;
+  }
+
+  .xl\:bg-cover {
+    background-size: cover;
+  }
+
+  .xl\:bg-contain {
+    background-size: contain;
+  }
+
+  .xl\:border-collapse {
+    border-collapse: collapse;
+  }
+
+  .xl\:border-separate {
+    border-collapse: separate;
+  }
+
+  .xl\:border-transparent {
+    border-color: transparent;
+  }
+
+  .xl\:border-current {
+    border-color: currentColor;
+  }
+
+  .xl\:border-black {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .xl\:border-white {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .xl\:border-gray-100 {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .xl\:border-gray-200 {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .xl\:border-gray-300 {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .xl\:border-gray-400 {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .xl\:border-gray-500 {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .xl\:border-gray-600 {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .xl\:border-gray-700 {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .xl\:border-gray-800 {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .xl\:border-gray-900 {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .xl\:border-red-100 {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .xl\:border-red-200 {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .xl\:border-red-300 {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .xl\:border-red-400 {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .xl\:border-red-500 {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .xl\:border-red-600 {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .xl\:border-red-700 {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .xl\:border-red-800 {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .xl\:border-red-900 {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .xl\:border-orange-100 {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .xl\:border-orange-200 {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .xl\:border-orange-300 {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .xl\:border-orange-400 {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .xl\:border-orange-500 {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .xl\:border-orange-600 {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .xl\:border-orange-700 {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .xl\:border-orange-800 {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .xl\:border-orange-900 {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-100 {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-200 {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-300 {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-400 {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-500 {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-600 {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-700 {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-800 {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .xl\:border-yellow-900 {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .xl\:border-green-100 {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .xl\:border-green-200 {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .xl\:border-green-300 {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .xl\:border-green-400 {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .xl\:border-green-500 {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .xl\:border-green-600 {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .xl\:border-green-700 {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .xl\:border-green-800 {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .xl\:border-green-900 {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .xl\:border-teal-100 {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .xl\:border-teal-200 {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .xl\:border-teal-300 {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .xl\:border-teal-400 {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .xl\:border-teal-500 {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .xl\:border-teal-600 {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .xl\:border-teal-700 {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .xl\:border-teal-800 {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .xl\:border-teal-900 {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .xl\:border-blue-100 {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .xl\:border-blue-200 {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .xl\:border-blue-300 {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .xl\:border-blue-400 {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .xl\:border-blue-500 {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .xl\:border-blue-600 {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .xl\:border-blue-700 {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .xl\:border-blue-800 {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .xl\:border-blue-900 {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-100 {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-200 {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-300 {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-400 {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-500 {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-600 {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-700 {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-800 {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .xl\:border-indigo-900 {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .xl\:border-purple-100 {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .xl\:border-purple-200 {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .xl\:border-purple-300 {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .xl\:border-purple-400 {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .xl\:border-purple-500 {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .xl\:border-purple-600 {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .xl\:border-purple-700 {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .xl\:border-purple-800 {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .xl\:border-purple-900 {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .xl\:border-pink-100 {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .xl\:border-pink-200 {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .xl\:border-pink-300 {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .xl\:border-pink-400 {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .xl\:border-pink-500 {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .xl\:border-pink-600 {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .xl\:border-pink-700 {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .xl\:border-pink-800 {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .xl\:border-pink-900 {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-transparent:hover {
+    border-color: transparent;
+  }
+
+  .xl\:hover\:border-current:hover {
+    border-color: currentColor;
+  }
+
+  .xl\:hover\:border-black:hover {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-white:hover {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-100:hover {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-200:hover {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-300:hover {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-400:hover {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-500:hover {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-600:hover {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-700:hover {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-800:hover {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-gray-900:hover {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-300:hover {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-400:hover {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-500:hover {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-600:hover {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-700:hover {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-800:hover {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-red-900:hover {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-100:hover {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-200:hover {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-300:hover {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-400:hover {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-500:hover {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-600:hover {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-700:hover {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-800:hover {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-orange-900:hover {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-100:hover {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-200:hover {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-300:hover {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-400:hover {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-500:hover {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-600:hover {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-700:hover {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-800:hover {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-yellow-900:hover {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-100:hover {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-200:hover {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-300:hover {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-400:hover {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-500:hover {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-600:hover {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-700:hover {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-800:hover {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-green-900:hover {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-100:hover {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-200:hover {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-300:hover {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-400:hover {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-500:hover {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-600:hover {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-700:hover {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-800:hover {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-teal-900:hover {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-200:hover {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-300:hover {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-400:hover {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-500:hover {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-600:hover {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-700:hover {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-800:hover {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-blue-900:hover {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-100:hover {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-200:hover {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-300:hover {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-400:hover {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-500:hover {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-600:hover {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-700:hover {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-800:hover {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-indigo-900:hover {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-100:hover {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-200:hover {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-300:hover {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-400:hover {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-500:hover {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-600:hover {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-700:hover {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-800:hover {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-purple-900:hover {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-100:hover {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-200:hover {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-300:hover {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-400:hover {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-500:hover {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-600:hover {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-700:hover {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-800:hover {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .xl\:hover\:border-pink-900:hover {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-transparent:focus {
+    border-color: transparent;
+  }
+
+  .xl\:focus\:border-current:focus {
+    border-color: currentColor;
+  }
+
+  .xl\:focus\:border-black:focus {
+    --border-opacity: 1;
+    border-color: #000;
+    border-color: rgba(0, 0, 0, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-white:focus {
+    --border-opacity: 1;
+    border-color: #fff;
+    border-color: rgba(255, 255, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-100:focus {
+    --border-opacity: 1;
+    border-color: #f7fafc;
+    border-color: rgba(247, 250, 252, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-200:focus {
+    --border-opacity: 1;
+    border-color: #edf2f7;
+    border-color: rgba(237, 242, 247, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-300:focus {
+    --border-opacity: 1;
+    border-color: #e2e8f0;
+    border-color: rgba(226, 232, 240, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-400:focus {
+    --border-opacity: 1;
+    border-color: #cbd5e0;
+    border-color: rgba(203, 213, 224, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-500:focus {
+    --border-opacity: 1;
+    border-color: #a0aec0;
+    border-color: rgba(160, 174, 192, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-600:focus {
+    --border-opacity: 1;
+    border-color: #718096;
+    border-color: rgba(113, 128, 150, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-700:focus {
+    --border-opacity: 1;
+    border-color: #4a5568;
+    border-color: rgba(74, 85, 104, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-800:focus {
+    --border-opacity: 1;
+    border-color: #2d3748;
+    border-color: rgba(45, 55, 72, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-gray-900:focus {
+    --border-opacity: 1;
+    border-color: #1a202c;
+    border-color: rgba(26, 32, 44, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f5;
+    border-color: rgba(255, 245, 245, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7d7;
+    border-color: rgba(254, 215, 215, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-300:focus {
+    --border-opacity: 1;
+    border-color: #feb2b2;
+    border-color: rgba(254, 178, 178, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-400:focus {
+    --border-opacity: 1;
+    border-color: #fc8181;
+    border-color: rgba(252, 129, 129, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-500:focus {
+    --border-opacity: 1;
+    border-color: #f56565;
+    border-color: rgba(245, 101, 101, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-600:focus {
+    --border-opacity: 1;
+    border-color: #e53e3e;
+    border-color: rgba(229, 62, 62, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-700:focus {
+    --border-opacity: 1;
+    border-color: #c53030;
+    border-color: rgba(197, 48, 48, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-800:focus {
+    --border-opacity: 1;
+    border-color: #9b2c2c;
+    border-color: rgba(155, 44, 44, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-red-900:focus {
+    --border-opacity: 1;
+    border-color: #742a2a;
+    border-color: rgba(116, 42, 42, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-100:focus {
+    --border-opacity: 1;
+    border-color: #fffaf0;
+    border-color: rgba(255, 250, 240, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-200:focus {
+    --border-opacity: 1;
+    border-color: #feebc8;
+    border-color: rgba(254, 235, 200, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-300:focus {
+    --border-opacity: 1;
+    border-color: #fbd38d;
+    border-color: rgba(251, 211, 141, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-400:focus {
+    --border-opacity: 1;
+    border-color: #f6ad55;
+    border-color: rgba(246, 173, 85, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-500:focus {
+    --border-opacity: 1;
+    border-color: #ed8936;
+    border-color: rgba(237, 137, 54, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-600:focus {
+    --border-opacity: 1;
+    border-color: #dd6b20;
+    border-color: rgba(221, 107, 32, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-700:focus {
+    --border-opacity: 1;
+    border-color: #c05621;
+    border-color: rgba(192, 86, 33, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-800:focus {
+    --border-opacity: 1;
+    border-color: #9c4221;
+    border-color: rgba(156, 66, 33, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-orange-900:focus {
+    --border-opacity: 1;
+    border-color: #7b341e;
+    border-color: rgba(123, 52, 30, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-100:focus {
+    --border-opacity: 1;
+    border-color: #fffff0;
+    border-color: rgba(255, 255, 240, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-200:focus {
+    --border-opacity: 1;
+    border-color: #fefcbf;
+    border-color: rgba(254, 252, 191, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-300:focus {
+    --border-opacity: 1;
+    border-color: #faf089;
+    border-color: rgba(250, 240, 137, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-400:focus {
+    --border-opacity: 1;
+    border-color: #f6e05e;
+    border-color: rgba(246, 224, 94, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-500:focus {
+    --border-opacity: 1;
+    border-color: #ecc94b;
+    border-color: rgba(236, 201, 75, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-600:focus {
+    --border-opacity: 1;
+    border-color: #d69e2e;
+    border-color: rgba(214, 158, 46, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-700:focus {
+    --border-opacity: 1;
+    border-color: #b7791f;
+    border-color: rgba(183, 121, 31, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-800:focus {
+    --border-opacity: 1;
+    border-color: #975a16;
+    border-color: rgba(151, 90, 22, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-yellow-900:focus {
+    --border-opacity: 1;
+    border-color: #744210;
+    border-color: rgba(116, 66, 16, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-100:focus {
+    --border-opacity: 1;
+    border-color: #f0fff4;
+    border-color: rgba(240, 255, 244, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-200:focus {
+    --border-opacity: 1;
+    border-color: #c6f6d5;
+    border-color: rgba(198, 246, 213, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-300:focus {
+    --border-opacity: 1;
+    border-color: #9ae6b4;
+    border-color: rgba(154, 230, 180, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-400:focus {
+    --border-opacity: 1;
+    border-color: #68d391;
+    border-color: rgba(104, 211, 145, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-500:focus {
+    --border-opacity: 1;
+    border-color: #48bb78;
+    border-color: rgba(72, 187, 120, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-600:focus {
+    --border-opacity: 1;
+    border-color: #38a169;
+    border-color: rgba(56, 161, 105, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-700:focus {
+    --border-opacity: 1;
+    border-color: #2f855a;
+    border-color: rgba(47, 133, 90, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-800:focus {
+    --border-opacity: 1;
+    border-color: #276749;
+    border-color: rgba(39, 103, 73, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-green-900:focus {
+    --border-opacity: 1;
+    border-color: #22543d;
+    border-color: rgba(34, 84, 61, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-100:focus {
+    --border-opacity: 1;
+    border-color: #e6fffa;
+    border-color: rgba(230, 255, 250, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-200:focus {
+    --border-opacity: 1;
+    border-color: #b2f5ea;
+    border-color: rgba(178, 245, 234, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-300:focus {
+    --border-opacity: 1;
+    border-color: #81e6d9;
+    border-color: rgba(129, 230, 217, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-400:focus {
+    --border-opacity: 1;
+    border-color: #4fd1c5;
+    border-color: rgba(79, 209, 197, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-500:focus {
+    --border-opacity: 1;
+    border-color: #38b2ac;
+    border-color: rgba(56, 178, 172, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-600:focus {
+    --border-opacity: 1;
+    border-color: #319795;
+    border-color: rgba(49, 151, 149, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-700:focus {
+    --border-opacity: 1;
+    border-color: #2c7a7b;
+    border-color: rgba(44, 122, 123, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-800:focus {
+    --border-opacity: 1;
+    border-color: #285e61;
+    border-color: rgba(40, 94, 97, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-teal-900:focus {
+    --border-opacity: 1;
+    border-color: #234e52;
+    border-color: rgba(35, 78, 82, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf8ff;
+    border-color: rgba(235, 248, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-200:focus {
+    --border-opacity: 1;
+    border-color: #bee3f8;
+    border-color: rgba(190, 227, 248, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-300:focus {
+    --border-opacity: 1;
+    border-color: #90cdf4;
+    border-color: rgba(144, 205, 244, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-400:focus {
+    --border-opacity: 1;
+    border-color: #63b3ed;
+    border-color: rgba(99, 179, 237, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-500:focus {
+    --border-opacity: 1;
+    border-color: #4299e1;
+    border-color: rgba(66, 153, 225, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-600:focus {
+    --border-opacity: 1;
+    border-color: #3182ce;
+    border-color: rgba(49, 130, 206, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-700:focus {
+    --border-opacity: 1;
+    border-color: #2b6cb0;
+    border-color: rgba(43, 108, 176, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-800:focus {
+    --border-opacity: 1;
+    border-color: #2c5282;
+    border-color: rgba(44, 82, 130, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-blue-900:focus {
+    --border-opacity: 1;
+    border-color: #2a4365;
+    border-color: rgba(42, 67, 101, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-100:focus {
+    --border-opacity: 1;
+    border-color: #ebf4ff;
+    border-color: rgba(235, 244, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-200:focus {
+    --border-opacity: 1;
+    border-color: #c3dafe;
+    border-color: rgba(195, 218, 254, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-300:focus {
+    --border-opacity: 1;
+    border-color: #a3bffa;
+    border-color: rgba(163, 191, 250, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-400:focus {
+    --border-opacity: 1;
+    border-color: #7f9cf5;
+    border-color: rgba(127, 156, 245, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-500:focus {
+    --border-opacity: 1;
+    border-color: #667eea;
+    border-color: rgba(102, 126, 234, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-600:focus {
+    --border-opacity: 1;
+    border-color: #5a67d8;
+    border-color: rgba(90, 103, 216, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-700:focus {
+    --border-opacity: 1;
+    border-color: #4c51bf;
+    border-color: rgba(76, 81, 191, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-800:focus {
+    --border-opacity: 1;
+    border-color: #434190;
+    border-color: rgba(67, 65, 144, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-indigo-900:focus {
+    --border-opacity: 1;
+    border-color: #3c366b;
+    border-color: rgba(60, 54, 107, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-100:focus {
+    --border-opacity: 1;
+    border-color: #faf5ff;
+    border-color: rgba(250, 245, 255, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-200:focus {
+    --border-opacity: 1;
+    border-color: #e9d8fd;
+    border-color: rgba(233, 216, 253, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-300:focus {
+    --border-opacity: 1;
+    border-color: #d6bcfa;
+    border-color: rgba(214, 188, 250, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-400:focus {
+    --border-opacity: 1;
+    border-color: #b794f4;
+    border-color: rgba(183, 148, 244, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-500:focus {
+    --border-opacity: 1;
+    border-color: #9f7aea;
+    border-color: rgba(159, 122, 234, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-600:focus {
+    --border-opacity: 1;
+    border-color: #805ad5;
+    border-color: rgba(128, 90, 213, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-700:focus {
+    --border-opacity: 1;
+    border-color: #6b46c1;
+    border-color: rgba(107, 70, 193, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-800:focus {
+    --border-opacity: 1;
+    border-color: #553c9a;
+    border-color: rgba(85, 60, 154, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-purple-900:focus {
+    --border-opacity: 1;
+    border-color: #44337a;
+    border-color: rgba(68, 51, 122, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-100:focus {
+    --border-opacity: 1;
+    border-color: #fff5f7;
+    border-color: rgba(255, 245, 247, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-200:focus {
+    --border-opacity: 1;
+    border-color: #fed7e2;
+    border-color: rgba(254, 215, 226, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-300:focus {
+    --border-opacity: 1;
+    border-color: #fbb6ce;
+    border-color: rgba(251, 182, 206, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-400:focus {
+    --border-opacity: 1;
+    border-color: #f687b3;
+    border-color: rgba(246, 135, 179, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-500:focus {
+    --border-opacity: 1;
+    border-color: #ed64a6;
+    border-color: rgba(237, 100, 166, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-600:focus {
+    --border-opacity: 1;
+    border-color: #d53f8c;
+    border-color: rgba(213, 63, 140, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-700:focus {
+    --border-opacity: 1;
+    border-color: #b83280;
+    border-color: rgba(184, 50, 128, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-800:focus {
+    --border-opacity: 1;
+    border-color: #97266d;
+    border-color: rgba(151, 38, 109, var(--border-opacity));
+  }
+
+  .xl\:focus\:border-pink-900:focus {
+    --border-opacity: 1;
+    border-color: #702459;
+    border-color: rgba(112, 36, 89, var(--border-opacity));
+  }
+
+  .xl\:border-opacity-0 {
+    --border-opacity: 0;
+  }
+
+  .xl\:border-opacity-25 {
+    --border-opacity: 0.25;
+  }
+
+  .xl\:border-opacity-50 {
+    --border-opacity: 0.5;
+  }
+
+  .xl\:border-opacity-75 {
+    --border-opacity: 0.75;
+  }
+
+  .xl\:border-opacity-100 {
+    --border-opacity: 1;
+  }
+
+  .xl\:hover\:border-opacity-0:hover {
+    --border-opacity: 0;
+  }
+
+  .xl\:hover\:border-opacity-25:hover {
+    --border-opacity: 0.25;
+  }
+
+  .xl\:hover\:border-opacity-50:hover {
+    --border-opacity: 0.5;
+  }
+
+  .xl\:hover\:border-opacity-75:hover {
+    --border-opacity: 0.75;
+  }
+
+  .xl\:hover\:border-opacity-100:hover {
+    --border-opacity: 1;
+  }
+
+  .xl\:focus\:border-opacity-0:focus {
+    --border-opacity: 0;
+  }
+
+  .xl\:focus\:border-opacity-25:focus {
+    --border-opacity: 0.25;
+  }
+
+  .xl\:focus\:border-opacity-50:focus {
+    --border-opacity: 0.5;
+  }
+
+  .xl\:focus\:border-opacity-75:focus {
+    --border-opacity: 0.75;
+  }
+
+  .xl\:focus\:border-opacity-100:focus {
+    --border-opacity: 1;
+  }
+
+  .xl\:rounded-none {
+    border-radius: 0;
+  }
+
+  .xl\:rounded-sm {
+    border-radius: 0.125rem;
+  }
+
+  .xl\:rounded {
+    border-radius: 0.25rem;
+  }
+
+  .xl\:rounded-md {
+    border-radius: 0.375rem;
+  }
+
+  .xl\:rounded-lg {
+    border-radius: 0.5rem;
+  }
+
+  .xl\:rounded-full {
+    border-radius: 9999px;
+  }
+
+  .xl\:rounded-t-none {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .xl\:rounded-r-none {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .xl\:rounded-b-none {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .xl\:rounded-l-none {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .xl\:rounded-t-sm {
+    border-top-left-radius: 0.125rem;
+    border-top-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-r-sm {
+    border-top-right-radius: 0.125rem;
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-b-sm {
+    border-bottom-right-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-l-sm {
+    border-top-left-radius: 0.125rem;
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-t {
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-r {
+    border-top-right-radius: 0.25rem;
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-b {
+    border-bottom-right-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-l {
+    border-top-left-radius: 0.25rem;
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-t-md {
+    border-top-left-radius: 0.375rem;
+    border-top-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-r-md {
+    border-top-right-radius: 0.375rem;
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-b-md {
+    border-bottom-right-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-l-md {
+    border-top-left-radius: 0.375rem;
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-t-lg {
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-r-lg {
+    border-top-right-radius: 0.5rem;
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-b-lg {
+    border-bottom-right-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-l-lg {
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-t-full {
+    border-top-left-radius: 9999px;
+    border-top-right-radius: 9999px;
+  }
+
+  .xl\:rounded-r-full {
+    border-top-right-radius: 9999px;
+    border-bottom-right-radius: 9999px;
+  }
+
+  .xl\:rounded-b-full {
+    border-bottom-right-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .xl\:rounded-l-full {
+    border-top-left-radius: 9999px;
+    border-bottom-left-radius: 9999px;
+  }
+
+  .xl\:rounded-tl-none {
+    border-top-left-radius: 0;
+  }
+
+  .xl\:rounded-tr-none {
+    border-top-right-radius: 0;
+  }
+
+  .xl\:rounded-br-none {
+    border-bottom-right-radius: 0;
+  }
+
+  .xl\:rounded-bl-none {
+    border-bottom-left-radius: 0;
+  }
+
+  .xl\:rounded-tl-sm {
+    border-top-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-tr-sm {
+    border-top-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-br-sm {
+    border-bottom-right-radius: 0.125rem;
+  }
+
+  .xl\:rounded-bl-sm {
+    border-bottom-left-radius: 0.125rem;
+  }
+
+  .xl\:rounded-tl {
+    border-top-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-tr {
+    border-top-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-br {
+    border-bottom-right-radius: 0.25rem;
+  }
+
+  .xl\:rounded-bl {
+    border-bottom-left-radius: 0.25rem;
+  }
+
+  .xl\:rounded-tl-md {
+    border-top-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-tr-md {
+    border-top-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-br-md {
+    border-bottom-right-radius: 0.375rem;
+  }
+
+  .xl\:rounded-bl-md {
+    border-bottom-left-radius: 0.375rem;
+  }
+
+  .xl\:rounded-tl-lg {
+    border-top-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-tr-lg {
+    border-top-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-br-lg {
+    border-bottom-right-radius: 0.5rem;
+  }
+
+  .xl\:rounded-bl-lg {
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  .xl\:rounded-tl-full {
+    border-top-left-radius: 9999px;
+  }
+
+  .xl\:rounded-tr-full {
+    border-top-right-radius: 9999px;
+  }
+
+  .xl\:rounded-br-full {
+    border-bottom-right-radius: 9999px;
+  }
+
+  .xl\:rounded-bl-full {
+    border-bottom-left-radius: 9999px;
+  }
+
+  .xl\:border-solid {
+    border-style: solid;
+  }
+
+  .xl\:border-dashed {
+    border-style: dashed;
+  }
+
+  .xl\:border-dotted {
+    border-style: dotted;
+  }
+
+  .xl\:border-double {
+    border-style: double;
+  }
+
+  .xl\:border-none {
+    border-style: none;
+  }
+
+  .xl\:border-0 {
+    border-width: 0;
+  }
+
+  .xl\:border-2 {
+    border-width: 2px;
+  }
+
+  .xl\:border-4 {
+    border-width: 4px;
+  }
+
+  .xl\:border-8 {
+    border-width: 8px;
+  }
+
+  .xl\:border {
+    border-width: 1px;
+  }
+
+  .xl\:border-t-0 {
+    border-top-width: 0;
+  }
+
+  .xl\:border-r-0 {
+    border-right-width: 0;
+  }
+
+  .xl\:border-b-0 {
+    border-bottom-width: 0;
+  }
+
+  .xl\:border-l-0 {
+    border-left-width: 0;
+  }
+
+  .xl\:border-t-2 {
+    border-top-width: 2px;
+  }
+
+  .xl\:border-r-2 {
+    border-right-width: 2px;
+  }
+
+  .xl\:border-b-2 {
+    border-bottom-width: 2px;
+  }
+
+  .xl\:border-l-2 {
+    border-left-width: 2px;
+  }
+
+  .xl\:border-t-4 {
+    border-top-width: 4px;
+  }
+
+  .xl\:border-r-4 {
+    border-right-width: 4px;
+  }
+
+  .xl\:border-b-4 {
+    border-bottom-width: 4px;
+  }
+
+  .xl\:border-l-4 {
+    border-left-width: 4px;
+  }
+
+  .xl\:border-t-8 {
+    border-top-width: 8px;
+  }
+
+  .xl\:border-r-8 {
+    border-right-width: 8px;
+  }
+
+  .xl\:border-b-8 {
+    border-bottom-width: 8px;
+  }
+
+  .xl\:border-l-8 {
+    border-left-width: 8px;
+  }
+
+  .xl\:border-t {
+    border-top-width: 1px;
+  }
+
+  .xl\:border-r {
+    border-right-width: 1px;
+  }
+
+  .xl\:border-b {
+    border-bottom-width: 1px;
+  }
+
+  .xl\:border-l {
+    border-left-width: 1px;
+  }
+
+  .xl\:box-border {
+    box-sizing: border-box;
+  }
+
+  .xl\:box-content {
+    box-sizing: content-box;
+  }
+
+  .xl\:cursor-auto {
+    cursor: auto;
+  }
+
+  .xl\:cursor-default {
+    cursor: default;
+  }
+
+  .xl\:cursor-pointer {
+    cursor: pointer;
+  }
+
+  .xl\:cursor-wait {
+    cursor: wait;
+  }
+
+  .xl\:cursor-text {
+    cursor: text;
+  }
+
+  .xl\:cursor-move {
+    cursor: move;
+  }
+
+  .xl\:cursor-not-allowed {
+    cursor: not-allowed;
+  }
+
+  .xl\:block {
+    display: block;
+  }
+
+  .xl\:inline-block {
+    display: inline-block;
+  }
+
+  .xl\:inline {
+    display: inline;
+  }
+
+  .xl\:flex {
+    display: flex;
+  }
+
+  .xl\:inline-flex {
+    display: inline-flex;
+  }
+
+  .xl\:table {
+    display: table;
+  }
+
+  .xl\:table-caption {
+    display: table-caption;
+  }
+
+  .xl\:table-cell {
+    display: table-cell;
+  }
+
+  .xl\:table-column {
+    display: table-column;
+  }
+
+  .xl\:table-column-group {
+    display: table-column-group;
+  }
+
+  .xl\:table-footer-group {
+    display: table-footer-group;
+  }
+
+  .xl\:table-header-group {
+    display: table-header-group;
+  }
+
+  .xl\:table-row-group {
+    display: table-row-group;
+  }
+
+  .xl\:table-row {
+    display: table-row;
+  }
+
+  .xl\:flow-root {
+    display: flow-root;
+  }
+
+  .xl\:grid {
+    display: grid;
+  }
+
+  .xl\:inline-grid {
+    display: inline-grid;
+  }
+
+  .xl\:contents {
+    display: contents;
+  }
+
+  .xl\:hidden {
+    display: none;
+  }
+
+  .xl\:flex-row {
+    flex-direction: row;
+  }
+
+  .xl\:flex-row-reverse {
+    flex-direction: row-reverse;
+  }
+
+  .xl\:flex-col {
+    flex-direction: column;
+  }
+
+  .xl\:flex-col-reverse {
+    flex-direction: column-reverse;
+  }
+
+  .xl\:flex-wrap {
+    flex-wrap: wrap;
+  }
+
+  .xl\:flex-wrap-reverse {
+    flex-wrap: wrap-reverse;
+  }
+
+  .xl\:flex-no-wrap {
+    flex-wrap: nowrap;
+  }
+
+  .xl\:place-items-auto {
+    place-items: auto;
+  }
+
+  .xl\:place-items-start {
+    place-items: start;
+  }
+
+  .xl\:place-items-end {
+    place-items: end;
+  }
+
+  .xl\:place-items-center {
+    place-items: center;
+  }
+
+  .xl\:place-items-stretch {
+    place-items: stretch;
+  }
+
+  .xl\:place-content-center {
+    place-content: center;
+  }
+
+  .xl\:place-content-start {
+    place-content: start;
+  }
+
+  .xl\:place-content-end {
+    place-content: end;
+  }
+
+  .xl\:place-content-between {
+    place-content: space-between;
+  }
+
+  .xl\:place-content-around {
+    place-content: space-around;
+  }
+
+  .xl\:place-content-evenly {
+    place-content: space-evenly;
+  }
+
+  .xl\:place-content-stretch {
+    place-content: stretch;
+  }
+
+  .xl\:place-self-auto {
+    place-self: auto;
+  }
+
+  .xl\:place-self-start {
+    place-self: start;
+  }
+
+  .xl\:place-self-end {
+    place-self: end;
+  }
+
+  .xl\:place-self-center {
+    place-self: center;
+  }
+
+  .xl\:place-self-stretch {
+    place-self: stretch;
+  }
+
+  .xl\:items-start {
+    align-items: flex-start;
+  }
+
+  .xl\:items-end {
+    align-items: flex-end;
+  }
+
+  .xl\:items-center {
+    align-items: center;
+  }
+
+  .xl\:items-baseline {
+    align-items: baseline;
+  }
+
+  .xl\:items-stretch {
+    align-items: stretch;
+  }
+
+  .xl\:content-center {
+    align-content: center;
+  }
+
+  .xl\:content-start {
+    align-content: flex-start;
+  }
+
+  .xl\:content-end {
+    align-content: flex-end;
+  }
+
+  .xl\:content-between {
+    align-content: space-between;
+  }
+
+  .xl\:content-around {
+    align-content: space-around;
+  }
+
+  .xl\:content-evenly {
+    align-content: space-evenly;
+  }
+
+  .xl\:self-auto {
+    align-self: auto;
+  }
+
+  .xl\:self-start {
+    align-self: flex-start;
+  }
+
+  .xl\:self-end {
+    align-self: flex-end;
+  }
+
+  .xl\:self-center {
+    align-self: center;
+  }
+
+  .xl\:self-stretch {
+    align-self: stretch;
+  }
+
+  .xl\:justify-items-auto {
+    justify-items: auto;
+  }
+
+  .xl\:justify-items-start {
+    justify-items: start;
+  }
+
+  .xl\:justify-items-end {
+    justify-items: end;
+  }
+
+  .xl\:justify-items-center {
+    justify-items: center;
+  }
+
+  .xl\:justify-items-stretch {
+    justify-items: stretch;
+  }
+
+  .xl\:justify-start {
+    justify-content: flex-start;
+  }
+
+  .xl\:justify-end {
+    justify-content: flex-end;
+  }
+
+  .xl\:justify-center {
+    justify-content: center;
+  }
+
+  .xl\:justify-between {
+    justify-content: space-between;
+  }
+
+  .xl\:justify-around {
+    justify-content: space-around;
+  }
+
+  .xl\:justify-evenly {
+    justify-content: space-evenly;
+  }
+
+  .xl\:justify-self-auto {
+    justify-self: auto;
+  }
+
+  .xl\:justify-self-start {
+    justify-self: start;
+  }
+
+  .xl\:justify-self-end {
+    justify-self: end;
+  }
+
+  .xl\:justify-self-center {
+    justify-self: center;
+  }
+
+  .xl\:justify-self-stretch {
+    justify-self: stretch;
+  }
+
+  .xl\:flex-1 {
+    flex: 1 1 0%;
+  }
+
+  .xl\:flex-auto {
+    flex: 1 1 auto;
+  }
+
+  .xl\:flex-initial {
+    flex: 0 1 auto;
+  }
+
+  .xl\:flex-none {
+    flex: none;
+  }
+
+  .xl\:flex-grow-0 {
+    flex-grow: 0;
+  }
+
+  .xl\:flex-grow {
+    flex-grow: 1;
+  }
+
+  .xl\:flex-shrink-0 {
+    flex-shrink: 0;
+  }
+
+  .xl\:flex-shrink {
+    flex-shrink: 1;
+  }
+
+  .xl\:order-1 {
+    order: 1;
+  }
+
+  .xl\:order-2 {
+    order: 2;
+  }
+
+  .xl\:order-3 {
+    order: 3;
+  }
+
+  .xl\:order-4 {
+    order: 4;
+  }
+
+  .xl\:order-5 {
+    order: 5;
+  }
+
+  .xl\:order-6 {
+    order: 6;
+  }
+
+  .xl\:order-7 {
+    order: 7;
+  }
+
+  .xl\:order-8 {
+    order: 8;
+  }
+
+  .xl\:order-9 {
+    order: 9;
+  }
+
+  .xl\:order-10 {
+    order: 10;
+  }
+
+  .xl\:order-11 {
+    order: 11;
+  }
+
+  .xl\:order-12 {
+    order: 12;
+  }
+
+  .xl\:order-first {
+    order: -9999;
+  }
+
+  .xl\:order-last {
+    order: 9999;
+  }
+
+  .xl\:order-none {
+    order: 0;
+  }
+
+  .xl\:float-right {
+    float: right;
+  }
+
+  .xl\:float-left {
+    float: left;
+  }
+
+  .xl\:float-none {
+    float: none;
+  }
+
+  .xl\:clearfix:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+
+  .xl\:clear-left {
+    clear: left;
+  }
+
+  .xl\:clear-right {
+    clear: right;
+  }
+
+  .xl\:clear-both {
+    clear: both;
+  }
+
+  .xl\:clear-none {
+    clear: none;
+  }
+
+  .xl\:font-sans {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  }
+
+  .xl\:font-serif {
+    font-family: Georgia, Cambria, "Times New Roman", Times, serif;
+  }
+
+  .xl\:font-mono {
+    font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  }
+
+  .xl\:font-hairline {
+    font-weight: 100;
+  }
+
+  .xl\:font-thin {
+    font-weight: 200;
+  }
+
+  .xl\:font-light {
+    font-weight: 300;
+  }
+
+  .xl\:font-normal {
+    font-weight: 400;
+  }
+
+  .xl\:font-medium {
+    font-weight: 500;
+  }
+
+  .xl\:font-semibold {
+    font-weight: 600;
+  }
+
+  .xl\:font-bold {
+    font-weight: 700;
+  }
+
+  .xl\:font-extrabold {
+    font-weight: 800;
+  }
+
+  .xl\:font-black {
+    font-weight: 900;
+  }
+
+  .xl\:hover\:font-hairline:hover {
+    font-weight: 100;
+  }
+
+  .xl\:hover\:font-thin:hover {
+    font-weight: 200;
+  }
+
+  .xl\:hover\:font-light:hover {
+    font-weight: 300;
+  }
+
+  .xl\:hover\:font-normal:hover {
+    font-weight: 400;
+  }
+
+  .xl\:hover\:font-medium:hover {
+    font-weight: 500;
+  }
+
+  .xl\:hover\:font-semibold:hover {
+    font-weight: 600;
+  }
+
+  .xl\:hover\:font-bold:hover {
+    font-weight: 700;
+  }
+
+  .xl\:hover\:font-extrabold:hover {
+    font-weight: 800;
+  }
+
+  .xl\:hover\:font-black:hover {
+    font-weight: 900;
+  }
+
+  .xl\:focus\:font-hairline:focus {
+    font-weight: 100;
+  }
+
+  .xl\:focus\:font-thin:focus {
+    font-weight: 200;
+  }
+
+  .xl\:focus\:font-light:focus {
+    font-weight: 300;
+  }
+
+  .xl\:focus\:font-normal:focus {
+    font-weight: 400;
+  }
+
+  .xl\:focus\:font-medium:focus {
+    font-weight: 500;
+  }
+
+  .xl\:focus\:font-semibold:focus {
+    font-weight: 600;
+  }
+
+  .xl\:focus\:font-bold:focus {
+    font-weight: 700;
+  }
+
+  .xl\:focus\:font-extrabold:focus {
+    font-weight: 800;
+  }
+
+  .xl\:focus\:font-black:focus {
+    font-weight: 900;
+  }
+
+  .xl\:h-0 {
+    height: 0;
+  }
+
+  .xl\:h-1 {
+    height: 0.25rem;
+  }
+
+  .xl\:h-2 {
+    height: 0.5rem;
+  }
+
+  .xl\:h-3 {
+    height: 0.75rem;
+  }
+
+  .xl\:h-4 {
+    height: 1rem;
+  }
+
+  .xl\:h-5 {
+    height: 1.25rem;
+  }
+
+  .xl\:h-6 {
+    height: 1.5rem;
+  }
+
+  .xl\:h-8 {
+    height: 2rem;
+  }
+
+  .xl\:h-10 {
+    height: 2.5rem;
+  }
+
+  .xl\:h-12 {
+    height: 3rem;
+  }
+
+  .xl\:h-16 {
+    height: 4rem;
+  }
+
+  .xl\:h-20 {
+    height: 5rem;
+  }
+
+  .xl\:h-24 {
+    height: 6rem;
+  }
+
+  .xl\:h-32 {
+    height: 8rem;
+  }
+
+  .xl\:h-40 {
+    height: 10rem;
+  }
+
+  .xl\:h-48 {
+    height: 12rem;
+  }
+
+  .xl\:h-56 {
+    height: 14rem;
+  }
+
+  .xl\:h-64 {
+    height: 16rem;
+  }
+
+  .xl\:h-auto {
+    height: auto;
+  }
+
+  .xl\:h-px {
+    height: 1px;
+  }
+
+  .xl\:h-full {
+    height: 100%;
+  }
+
+  .xl\:h-screen {
+    height: 100vh;
+  }
+
+  .xl\:text-xs {
+    font-size: 0.75rem;
+  }
+
+  .xl\:text-sm {
+    font-size: 0.875rem;
+  }
+
+  .xl\:text-base {
+    font-size: 1rem;
+  }
+
+  .xl\:text-lg {
+    font-size: 1.125rem;
+  }
+
+  .xl\:text-xl {
+    font-size: 1.25rem;
+  }
+
+  .xl\:text-2xl {
+    font-size: 1.5rem;
+  }
+
+  .xl\:text-3xl {
+    font-size: 1.875rem;
+  }
+
+  .xl\:text-4xl {
+    font-size: 2.25rem;
+  }
+
+  .xl\:text-5xl {
+    font-size: 3rem;
+  }
+
+  .xl\:text-6xl {
+    font-size: 4rem;
+  }
+
+  .xl\:leading-3 {
+    line-height: .75rem;
+  }
+
+  .xl\:leading-4 {
+    line-height: 1rem;
+  }
+
+  .xl\:leading-5 {
+    line-height: 1.25rem;
+  }
+
+  .xl\:leading-6 {
+    line-height: 1.5rem;
+  }
+
+  .xl\:leading-7 {
+    line-height: 1.75rem;
+  }
+
+  .xl\:leading-8 {
+    line-height: 2rem;
+  }
+
+  .xl\:leading-9 {
+    line-height: 2.25rem;
+  }
+
+  .xl\:leading-10 {
+    line-height: 2.5rem;
+  }
+
+  .xl\:leading-none {
+    line-height: 1;
+  }
+
+  .xl\:leading-tight {
+    line-height: 1.25;
+  }
+
+  .xl\:leading-snug {
+    line-height: 1.375;
+  }
+
+  .xl\:leading-normal {
+    line-height: 1.5;
+  }
+
+  .xl\:leading-relaxed {
+    line-height: 1.625;
+  }
+
+  .xl\:leading-loose {
+    line-height: 2;
+  }
+
+  .xl\:list-inside {
+    list-style-position: inside;
+  }
+
+  .xl\:list-outside {
+    list-style-position: outside;
+  }
+
+  .xl\:list-none {
+    list-style-type: none;
+  }
+
+  .xl\:list-disc {
+    list-style-type: disc;
+  }
+
+  .xl\:list-decimal {
+    list-style-type: decimal;
+  }
+
+  .xl\:m-0 {
+    margin: 0;
+  }
+
+  .xl\:m-1 {
+    margin: 0.25rem;
+  }
+
+  .xl\:m-2 {
+    margin: 0.5rem;
+  }
+
+  .xl\:m-3 {
+    margin: 0.75rem;
+  }
+
+  .xl\:m-4 {
+    margin: 1rem;
+  }
+
+  .xl\:m-5 {
+    margin: 1.25rem;
+  }
+
+  .xl\:m-6 {
+    margin: 1.5rem;
+  }
+
+  .xl\:m-8 {
+    margin: 2rem;
+  }
+
+  .xl\:m-10 {
+    margin: 2.5rem;
+  }
+
+  .xl\:m-12 {
+    margin: 3rem;
+  }
+
+  .xl\:m-16 {
+    margin: 4rem;
+  }
+
+  .xl\:m-20 {
+    margin: 5rem;
+  }
+
+  .xl\:m-24 {
+    margin: 6rem;
+  }
+
+  .xl\:m-32 {
+    margin: 8rem;
+  }
+
+  .xl\:m-40 {
+    margin: 10rem;
+  }
+
+  .xl\:m-48 {
+    margin: 12rem;
+  }
+
+  .xl\:m-56 {
+    margin: 14rem;
+  }
+
+  .xl\:m-64 {
+    margin: 16rem;
+  }
+
+  .xl\:m-auto {
+    margin: auto;
+  }
+
+  .xl\:m-px {
+    margin: 1px;
+  }
+
+  .xl\:-m-1 {
+    margin: -0.25rem;
+  }
+
+  .xl\:-m-2 {
+    margin: -0.5rem;
+  }
+
+  .xl\:-m-3 {
+    margin: -0.75rem;
+  }
+
+  .xl\:-m-4 {
+    margin: -1rem;
+  }
+
+  .xl\:-m-5 {
+    margin: -1.25rem;
+  }
+
+  .xl\:-m-6 {
+    margin: -1.5rem;
+  }
+
+  .xl\:-m-8 {
+    margin: -2rem;
+  }
+
+  .xl\:-m-10 {
+    margin: -2.5rem;
+  }
+
+  .xl\:-m-12 {
+    margin: -3rem;
+  }
+
+  .xl\:-m-16 {
+    margin: -4rem;
+  }
+
+  .xl\:-m-20 {
+    margin: -5rem;
+  }
+
+  .xl\:-m-24 {
+    margin: -6rem;
+  }
+
+  .xl\:-m-32 {
+    margin: -8rem;
+  }
+
+  .xl\:-m-40 {
+    margin: -10rem;
+  }
+
+  .xl\:-m-48 {
+    margin: -12rem;
+  }
+
+  .xl\:-m-56 {
+    margin: -14rem;
+  }
+
+  .xl\:-m-64 {
+    margin: -16rem;
+  }
+
+  .xl\:-m-px {
+    margin: -1px;
+  }
+
+  .xl\:my-0 {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  .xl\:mx-0 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .xl\:my-1 {
+    margin-top: 0.25rem;
+    margin-bottom: 0.25rem;
+  }
+
+  .xl\:mx-1 {
+    margin-left: 0.25rem;
+    margin-right: 0.25rem;
+  }
+
+  .xl\:my-2 {
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .xl\:mx-2 {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+  }
+
+  .xl\:my-3 {
+    margin-top: 0.75rem;
+    margin-bottom: 0.75rem;
+  }
+
+  .xl\:mx-3 {
+    margin-left: 0.75rem;
+    margin-right: 0.75rem;
+  }
+
+  .xl\:my-4 {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+  }
+
+  .xl\:mx-4 {
+    margin-left: 1rem;
+    margin-right: 1rem;
+  }
+
+  .xl\:my-5 {
+    margin-top: 1.25rem;
+    margin-bottom: 1.25rem;
+  }
+
+  .xl\:mx-5 {
+    margin-left: 1.25rem;
+    margin-right: 1.25rem;
+  }
+
+  .xl\:my-6 {
+    margin-top: 1.5rem;
+    margin-bottom: 1.5rem;
+  }
+
+  .xl\:mx-6 {
+    margin-left: 1.5rem;
+    margin-right: 1.5rem;
+  }
+
+  .xl\:my-8 {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+  }
+
+  .xl\:mx-8 {
+    margin-left: 2rem;
+    margin-right: 2rem;
+  }
+
+  .xl\:my-10 {
+    margin-top: 2.5rem;
+    margin-bottom: 2.5rem;
+  }
+
+  .xl\:mx-10 {
+    margin-left: 2.5rem;
+    margin-right: 2.5rem;
+  }
+
+  .xl\:my-12 {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+  }
+
+  .xl\:mx-12 {
+    margin-left: 3rem;
+    margin-right: 3rem;
+  }
+
+  .xl\:my-16 {
+    margin-top: 4rem;
+    margin-bottom: 4rem;
+  }
+
+  .xl\:mx-16 {
+    margin-left: 4rem;
+    margin-right: 4rem;
+  }
+
+  .xl\:my-20 {
+    margin-top: 5rem;
+    margin-bottom: 5rem;
+  }
+
+  .xl\:mx-20 {
+    margin-left: 5rem;
+    margin-right: 5rem;
+  }
+
+  .xl\:my-24 {
+    margin-top: 6rem;
+    margin-bottom: 6rem;
+  }
+
+  .xl\:mx-24 {
+    margin-left: 6rem;
+    margin-right: 6rem;
+  }
+
+  .xl\:my-32 {
+    margin-top: 8rem;
+    margin-bottom: 8rem;
+  }
+
+  .xl\:mx-32 {
+    margin-left: 8rem;
+    margin-right: 8rem;
+  }
+
+  .xl\:my-40 {
+    margin-top: 10rem;
+    margin-bottom: 10rem;
+  }
+
+  .xl\:mx-40 {
+    margin-left: 10rem;
+    margin-right: 10rem;
+  }
+
+  .xl\:my-48 {
+    margin-top: 12rem;
+    margin-bottom: 12rem;
+  }
+
+  .xl\:mx-48 {
+    margin-left: 12rem;
+    margin-right: 12rem;
+  }
+
+  .xl\:my-56 {
+    margin-top: 14rem;
+    margin-bottom: 14rem;
+  }
+
+  .xl\:mx-56 {
+    margin-left: 14rem;
+    margin-right: 14rem;
+  }
+
+  .xl\:my-64 {
+    margin-top: 16rem;
+    margin-bottom: 16rem;
+  }
+
+  .xl\:mx-64 {
+    margin-left: 16rem;
+    margin-right: 16rem;
+  }
+
+  .xl\:my-auto {
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  .xl\:mx-auto {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .xl\:my-px {
+    margin-top: 1px;
+    margin-bottom: 1px;
+  }
+
+  .xl\:mx-px {
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .xl\:-my-1 {
+    margin-top: -0.25rem;
+    margin-bottom: -0.25rem;
+  }
+
+  .xl\:-mx-1 {
+    margin-left: -0.25rem;
+    margin-right: -0.25rem;
+  }
+
+  .xl\:-my-2 {
+    margin-top: -0.5rem;
+    margin-bottom: -0.5rem;
+  }
+
+  .xl\:-mx-2 {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+  }
+
+  .xl\:-my-3 {
+    margin-top: -0.75rem;
+    margin-bottom: -0.75rem;
+  }
+
+  .xl\:-mx-3 {
+    margin-left: -0.75rem;
+    margin-right: -0.75rem;
+  }
+
+  .xl\:-my-4 {
+    margin-top: -1rem;
+    margin-bottom: -1rem;
+  }
+
+  .xl\:-mx-4 {
+    margin-left: -1rem;
+    margin-right: -1rem;
+  }
+
+  .xl\:-my-5 {
+    margin-top: -1.25rem;
+    margin-bottom: -1.25rem;
+  }
+
+  .xl\:-mx-5 {
+    margin-left: -1.25rem;
+    margin-right: -1.25rem;
+  }
+
+  .xl\:-my-6 {
+    margin-top: -1.5rem;
+    margin-bottom: -1.5rem;
+  }
+
+  .xl\:-mx-6 {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+  }
+
+  .xl\:-my-8 {
+    margin-top: -2rem;
+    margin-bottom: -2rem;
+  }
+
+  .xl\:-mx-8 {
+    margin-left: -2rem;
+    margin-right: -2rem;
+  }
+
+  .xl\:-my-10 {
+    margin-top: -2.5rem;
+    margin-bottom: -2.5rem;
+  }
+
+  .xl\:-mx-10 {
+    margin-left: -2.5rem;
+    margin-right: -2.5rem;
+  }
+
+  .xl\:-my-12 {
+    margin-top: -3rem;
+    margin-bottom: -3rem;
+  }
+
+  .xl\:-mx-12 {
+    margin-left: -3rem;
+    margin-right: -3rem;
+  }
+
+  .xl\:-my-16 {
+    margin-top: -4rem;
+    margin-bottom: -4rem;
+  }
+
+  .xl\:-mx-16 {
+    margin-left: -4rem;
+    margin-right: -4rem;
+  }
+
+  .xl\:-my-20 {
+    margin-top: -5rem;
+    margin-bottom: -5rem;
+  }
+
+  .xl\:-mx-20 {
+    margin-left: -5rem;
+    margin-right: -5rem;
+  }
+
+  .xl\:-my-24 {
+    margin-top: -6rem;
+    margin-bottom: -6rem;
+  }
+
+  .xl\:-mx-24 {
+    margin-left: -6rem;
+    margin-right: -6rem;
+  }
+
+  .xl\:-my-32 {
+    margin-top: -8rem;
+    margin-bottom: -8rem;
+  }
+
+  .xl\:-mx-32 {
+    margin-left: -8rem;
+    margin-right: -8rem;
+  }
+
+  .xl\:-my-40 {
+    margin-top: -10rem;
+    margin-bottom: -10rem;
+  }
+
+  .xl\:-mx-40 {
+    margin-left: -10rem;
+    margin-right: -10rem;
+  }
+
+  .xl\:-my-48 {
+    margin-top: -12rem;
+    margin-bottom: -12rem;
+  }
+
+  .xl\:-mx-48 {
+    margin-left: -12rem;
+    margin-right: -12rem;
+  }
+
+  .xl\:-my-56 {
+    margin-top: -14rem;
+    margin-bottom: -14rem;
+  }
+
+  .xl\:-mx-56 {
+    margin-left: -14rem;
+    margin-right: -14rem;
+  }
+
+  .xl\:-my-64 {
+    margin-top: -16rem;
+    margin-bottom: -16rem;
+  }
+
+  .xl\:-mx-64 {
+    margin-left: -16rem;
+    margin-right: -16rem;
+  }
+
+  .xl\:-my-px {
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .xl\:-mx-px {
+    margin-left: -1px;
+    margin-right: -1px;
+  }
+
+  .xl\:mt-0 {
+    margin-top: 0;
+  }
+
+  .xl\:mr-0 {
+    margin-right: 0;
+  }
+
+  .xl\:mb-0 {
+    margin-bottom: 0;
+  }
+
+  .xl\:ml-0 {
+    margin-left: 0;
+  }
+
+  .xl\:mt-1 {
+    margin-top: 0.25rem;
+  }
+
+  .xl\:mr-1 {
+    margin-right: 0.25rem;
+  }
+
+  .xl\:mb-1 {
+    margin-bottom: 0.25rem;
+  }
+
+  .xl\:ml-1 {
+    margin-left: 0.25rem;
+  }
+
+  .xl\:mt-2 {
+    margin-top: 0.5rem;
+  }
+
+  .xl\:mr-2 {
+    margin-right: 0.5rem;
+  }
+
+  .xl\:mb-2 {
+    margin-bottom: 0.5rem;
+  }
+
+  .xl\:ml-2 {
+    margin-left: 0.5rem;
+  }
+
+  .xl\:mt-3 {
+    margin-top: 0.75rem;
+  }
+
+  .xl\:mr-3 {
+    margin-right: 0.75rem;
+  }
+
+  .xl\:mb-3 {
+    margin-bottom: 0.75rem;
+  }
+
+  .xl\:ml-3 {
+    margin-left: 0.75rem;
+  }
+
+  .xl\:mt-4 {
+    margin-top: 1rem;
+  }
+
+  .xl\:mr-4 {
+    margin-right: 1rem;
+  }
+
+  .xl\:mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  .xl\:ml-4 {
+    margin-left: 1rem;
+  }
+
+  .xl\:mt-5 {
+    margin-top: 1.25rem;
+  }
+
+  .xl\:mr-5 {
+    margin-right: 1.25rem;
+  }
+
+  .xl\:mb-5 {
+    margin-bottom: 1.25rem;
+  }
+
+  .xl\:ml-5 {
+    margin-left: 1.25rem;
+  }
+
+  .xl\:mt-6 {
+    margin-top: 1.5rem;
+  }
+
+  .xl\:mr-6 {
+    margin-right: 1.5rem;
+  }
+
+  .xl\:mb-6 {
+    margin-bottom: 1.5rem;
+  }
+
+  .xl\:ml-6 {
+    margin-left: 1.5rem;
+  }
+
+  .xl\:mt-8 {
+    margin-top: 2rem;
+  }
+
+  .xl\:mr-8 {
+    margin-right: 2rem;
+  }
+
+  .xl\:mb-8 {
+    margin-bottom: 2rem;
+  }
+
+  .xl\:ml-8 {
+    margin-left: 2rem;
+  }
+
+  .xl\:mt-10 {
+    margin-top: 2.5rem;
+  }
+
+  .xl\:mr-10 {
+    margin-right: 2.5rem;
+  }
+
+  .xl\:mb-10 {
+    margin-bottom: 2.5rem;
+  }
+
+  .xl\:ml-10 {
+    margin-left: 2.5rem;
+  }
+
+  .xl\:mt-12 {
+    margin-top: 3rem;
+  }
+
+  .xl\:mr-12 {
+    margin-right: 3rem;
+  }
+
+  .xl\:mb-12 {
+    margin-bottom: 3rem;
+  }
+
+  .xl\:ml-12 {
+    margin-left: 3rem;
+  }
+
+  .xl\:mt-16 {
+    margin-top: 4rem;
+  }
+
+  .xl\:mr-16 {
+    margin-right: 4rem;
+  }
+
+  .xl\:mb-16 {
+    margin-bottom: 4rem;
+  }
+
+  .xl\:ml-16 {
+    margin-left: 4rem;
+  }
+
+  .xl\:mt-20 {
+    margin-top: 5rem;
+  }
+
+  .xl\:mr-20 {
+    margin-right: 5rem;
+  }
+
+  .xl\:mb-20 {
+    margin-bottom: 5rem;
+  }
+
+  .xl\:ml-20 {
+    margin-left: 5rem;
+  }
+
+  .xl\:mt-24 {
+    margin-top: 6rem;
+  }
+
+  .xl\:mr-24 {
+    margin-right: 6rem;
+  }
+
+  .xl\:mb-24 {
+    margin-bottom: 6rem;
+  }
+
+  .xl\:ml-24 {
+    margin-left: 6rem;
+  }
+
+  .xl\:mt-32 {
+    margin-top: 8rem;
+  }
+
+  .xl\:mr-32 {
+    margin-right: 8rem;
+  }
+
+  .xl\:mb-32 {
+    margin-bottom: 8rem;
+  }
+
+  .xl\:ml-32 {
+    margin-left: 8rem;
+  }
+
+  .xl\:mt-40 {
+    margin-top: 10rem;
+  }
+
+  .xl\:mr-40 {
+    margin-right: 10rem;
+  }
+
+  .xl\:mb-40 {
+    margin-bottom: 10rem;
+  }
+
+  .xl\:ml-40 {
+    margin-left: 10rem;
+  }
+
+  .xl\:mt-48 {
+    margin-top: 12rem;
+  }
+
+  .xl\:mr-48 {
+    margin-right: 12rem;
+  }
+
+  .xl\:mb-48 {
+    margin-bottom: 12rem;
+  }
+
+  .xl\:ml-48 {
+    margin-left: 12rem;
+  }
+
+  .xl\:mt-56 {
+    margin-top: 14rem;
+  }
+
+  .xl\:mr-56 {
+    margin-right: 14rem;
+  }
+
+  .xl\:mb-56 {
+    margin-bottom: 14rem;
+  }
+
+  .xl\:ml-56 {
+    margin-left: 14rem;
+  }
+
+  .xl\:mt-64 {
+    margin-top: 16rem;
+  }
+
+  .xl\:mr-64 {
+    margin-right: 16rem;
+  }
+
+  .xl\:mb-64 {
+    margin-bottom: 16rem;
+  }
+
+  .xl\:ml-64 {
+    margin-left: 16rem;
+  }
+
+  .xl\:mt-auto {
+    margin-top: auto;
+  }
+
+  .xl\:mr-auto {
+    margin-right: auto;
+  }
+
+  .xl\:mb-auto {
+    margin-bottom: auto;
+  }
+
+  .xl\:ml-auto {
+    margin-left: auto;
+  }
+
+  .xl\:mt-px {
+    margin-top: 1px;
+  }
+
+  .xl\:mr-px {
+    margin-right: 1px;
+  }
+
+  .xl\:mb-px {
+    margin-bottom: 1px;
+  }
+
+  .xl\:ml-px {
+    margin-left: 1px;
+  }
+
+  .xl\:-mt-1 {
+    margin-top: -0.25rem;
+  }
+
+  .xl\:-mr-1 {
+    margin-right: -0.25rem;
+  }
+
+  .xl\:-mb-1 {
+    margin-bottom: -0.25rem;
+  }
+
+  .xl\:-ml-1 {
+    margin-left: -0.25rem;
+  }
+
+  .xl\:-mt-2 {
+    margin-top: -0.5rem;
+  }
+
+  .xl\:-mr-2 {
+    margin-right: -0.5rem;
+  }
+
+  .xl\:-mb-2 {
+    margin-bottom: -0.5rem;
+  }
+
+  .xl\:-ml-2 {
+    margin-left: -0.5rem;
+  }
+
+  .xl\:-mt-3 {
+    margin-top: -0.75rem;
+  }
+
+  .xl\:-mr-3 {
+    margin-right: -0.75rem;
+  }
+
+  .xl\:-mb-3 {
+    margin-bottom: -0.75rem;
+  }
+
+  .xl\:-ml-3 {
+    margin-left: -0.75rem;
+  }
+
+  .xl\:-mt-4 {
+    margin-top: -1rem;
+  }
+
+  .xl\:-mr-4 {
+    margin-right: -1rem;
+  }
+
+  .xl\:-mb-4 {
+    margin-bottom: -1rem;
+  }
+
+  .xl\:-ml-4 {
+    margin-left: -1rem;
+  }
+
+  .xl\:-mt-5 {
+    margin-top: -1.25rem;
+  }
+
+  .xl\:-mr-5 {
+    margin-right: -1.25rem;
+  }
+
+  .xl\:-mb-5 {
+    margin-bottom: -1.25rem;
+  }
+
+  .xl\:-ml-5 {
+    margin-left: -1.25rem;
+  }
+
+  .xl\:-mt-6 {
+    margin-top: -1.5rem;
+  }
+
+  .xl\:-mr-6 {
+    margin-right: -1.5rem;
+  }
+
+  .xl\:-mb-6 {
+    margin-bottom: -1.5rem;
+  }
+
+  .xl\:-ml-6 {
+    margin-left: -1.5rem;
+  }
+
+  .xl\:-mt-8 {
+    margin-top: -2rem;
+  }
+
+  .xl\:-mr-8 {
+    margin-right: -2rem;
+  }
+
+  .xl\:-mb-8 {
+    margin-bottom: -2rem;
+  }
+
+  .xl\:-ml-8 {
+    margin-left: -2rem;
+  }
+
+  .xl\:-mt-10 {
+    margin-top: -2.5rem;
+  }
+
+  .xl\:-mr-10 {
+    margin-right: -2.5rem;
+  }
+
+  .xl\:-mb-10 {
+    margin-bottom: -2.5rem;
+  }
+
+  .xl\:-ml-10 {
+    margin-left: -2.5rem;
+  }
+
+  .xl\:-mt-12 {
+    margin-top: -3rem;
+  }
+
+  .xl\:-mr-12 {
+    margin-right: -3rem;
+  }
+
+  .xl\:-mb-12 {
+    margin-bottom: -3rem;
+  }
+
+  .xl\:-ml-12 {
+    margin-left: -3rem;
+  }
+
+  .xl\:-mt-16 {
+    margin-top: -4rem;
+  }
+
+  .xl\:-mr-16 {
+    margin-right: -4rem;
+  }
+
+  .xl\:-mb-16 {
+    margin-bottom: -4rem;
+  }
+
+  .xl\:-ml-16 {
+    margin-left: -4rem;
+  }
+
+  .xl\:-mt-20 {
+    margin-top: -5rem;
+  }
+
+  .xl\:-mr-20 {
+    margin-right: -5rem;
+  }
+
+  .xl\:-mb-20 {
+    margin-bottom: -5rem;
+  }
+
+  .xl\:-ml-20 {
+    margin-left: -5rem;
+  }
+
+  .xl\:-mt-24 {
+    margin-top: -6rem;
+  }
+
+  .xl\:-mr-24 {
+    margin-right: -6rem;
+  }
+
+  .xl\:-mb-24 {
+    margin-bottom: -6rem;
+  }
+
+  .xl\:-ml-24 {
+    margin-left: -6rem;
+  }
+
+  .xl\:-mt-32 {
+    margin-top: -8rem;
+  }
+
+  .xl\:-mr-32 {
+    margin-right: -8rem;
+  }
+
+  .xl\:-mb-32 {
+    margin-bottom: -8rem;
+  }
+
+  .xl\:-ml-32 {
+    margin-left: -8rem;
+  }
+
+  .xl\:-mt-40 {
+    margin-top: -10rem;
+  }
+
+  .xl\:-mr-40 {
+    margin-right: -10rem;
+  }
+
+  .xl\:-mb-40 {
+    margin-bottom: -10rem;
+  }
+
+  .xl\:-ml-40 {
+    margin-left: -10rem;
+  }
+
+  .xl\:-mt-48 {
+    margin-top: -12rem;
+  }
+
+  .xl\:-mr-48 {
+    margin-right: -12rem;
+  }
+
+  .xl\:-mb-48 {
+    margin-bottom: -12rem;
+  }
+
+  .xl\:-ml-48 {
+    margin-left: -12rem;
+  }
+
+  .xl\:-mt-56 {
+    margin-top: -14rem;
+  }
+
+  .xl\:-mr-56 {
+    margin-right: -14rem;
+  }
+
+  .xl\:-mb-56 {
+    margin-bottom: -14rem;
+  }
+
+  .xl\:-ml-56 {
+    margin-left: -14rem;
+  }
+
+  .xl\:-mt-64 {
+    margin-top: -16rem;
+  }
+
+  .xl\:-mr-64 {
+    margin-right: -16rem;
+  }
+
+  .xl\:-mb-64 {
+    margin-bottom: -16rem;
+  }
+
+  .xl\:-ml-64 {
+    margin-left: -16rem;
+  }
+
+  .xl\:-mt-px {
+    margin-top: -1px;
+  }
+
+  .xl\:-mr-px {
+    margin-right: -1px;
+  }
+
+  .xl\:-mb-px {
+    margin-bottom: -1px;
+  }
+
+  .xl\:-ml-px {
+    margin-left: -1px;
+  }
+
+  .xl\:max-h-full {
+    max-height: 100%;
+  }
+
+  .xl\:max-h-screen {
+    max-height: 100vh;
+  }
+
+  .xl\:max-w-none {
+    max-width: none;
+  }
+
+  .xl\:max-w-xs {
+    max-width: 20rem;
+  }
+
+  .xl\:max-w-sm {
+    max-width: 24rem;
+  }
+
+  .xl\:max-w-md {
+    max-width: 28rem;
+  }
+
+  .xl\:max-w-lg {
+    max-width: 32rem;
+  }
+
+  .xl\:max-w-xl {
+    max-width: 36rem;
+  }
+
+  .xl\:max-w-2xl {
+    max-width: 42rem;
+  }
+
+  .xl\:max-w-3xl {
+    max-width: 48rem;
+  }
+
+  .xl\:max-w-4xl {
+    max-width: 56rem;
+  }
+
+  .xl\:max-w-5xl {
+    max-width: 64rem;
+  }
+
+  .xl\:max-w-6xl {
+    max-width: 72rem;
+  }
+
+  .xl\:max-w-full {
+    max-width: 100%;
+  }
+
+  .xl\:max-w-screen-sm {
+    max-width: 640px;
+  }
+
+  .xl\:max-w-screen-md {
+    max-width: 768px;
+  }
+
+  .xl\:max-w-screen-lg {
+    max-width: 1024px;
+  }
+
+  .xl\:max-w-screen-xl {
+    max-width: 1280px;
+  }
+
+  .xl\:min-h-0 {
+    min-height: 0;
+  }
+
+  .xl\:min-h-full {
+    min-height: 100%;
+  }
+
+  .xl\:min-h-screen {
+    min-height: 100vh;
+  }
+
+  .xl\:min-w-0 {
+    min-width: 0;
+  }
+
+  .xl\:min-w-full {
+    min-width: 100%;
+  }
+
+  .xl\:object-contain {
+    -o-object-fit: contain;
+       object-fit: contain;
+  }
+
+  .xl\:object-cover {
+    -o-object-fit: cover;
+       object-fit: cover;
+  }
+
+  .xl\:object-fill {
+    -o-object-fit: fill;
+       object-fit: fill;
+  }
+
+  .xl\:object-none {
+    -o-object-fit: none;
+       object-fit: none;
+  }
+
+  .xl\:object-scale-down {
+    -o-object-fit: scale-down;
+       object-fit: scale-down;
+  }
+
+  .xl\:object-bottom {
+    -o-object-position: bottom;
+       object-position: bottom;
+  }
+
+  .xl\:object-center {
+    -o-object-position: center;
+       object-position: center;
+  }
+
+  .xl\:object-left {
+    -o-object-position: left;
+       object-position: left;
+  }
+
+  .xl\:object-left-bottom {
+    -o-object-position: left bottom;
+       object-position: left bottom;
+  }
+
+  .xl\:object-left-top {
+    -o-object-position: left top;
+       object-position: left top;
+  }
+
+  .xl\:object-right {
+    -o-object-position: right;
+       object-position: right;
+  }
+
+  .xl\:object-right-bottom {
+    -o-object-position: right bottom;
+       object-position: right bottom;
+  }
+
+  .xl\:object-right-top {
+    -o-object-position: right top;
+       object-position: right top;
+  }
+
+  .xl\:object-top {
+    -o-object-position: top;
+       object-position: top;
+  }
+
+  .xl\:opacity-0 {
+    opacity: 0;
+  }
+
+  .xl\:opacity-25 {
+    opacity: 0.25;
+  }
+
+  .xl\:opacity-50 {
+    opacity: 0.5;
+  }
+
+  .xl\:opacity-75 {
+    opacity: 0.75;
+  }
+
+  .xl\:opacity-100 {
+    opacity: 1;
+  }
+
+  .xl\:hover\:opacity-0:hover {
+    opacity: 0;
+  }
+
+  .xl\:hover\:opacity-25:hover {
+    opacity: 0.25;
+  }
+
+  .xl\:hover\:opacity-50:hover {
+    opacity: 0.5;
+  }
+
+  .xl\:hover\:opacity-75:hover {
+    opacity: 0.75;
+  }
+
+  .xl\:hover\:opacity-100:hover {
+    opacity: 1;
+  }
+
+  .xl\:focus\:opacity-0:focus {
+    opacity: 0;
+  }
+
+  .xl\:focus\:opacity-25:focus {
+    opacity: 0.25;
+  }
+
+  .xl\:focus\:opacity-50:focus {
+    opacity: 0.5;
+  }
+
+  .xl\:focus\:opacity-75:focus {
+    opacity: 0.75;
+  }
+
+  .xl\:focus\:opacity-100:focus {
+    opacity: 1;
+  }
+
+  .xl\:outline-none {
+    outline: 0;
+  }
+
+  .xl\:focus\:outline-none:focus {
+    outline: 0;
+  }
+
+  .xl\:overflow-auto {
+    overflow: auto;
+  }
+
+  .xl\:overflow-hidden {
+    overflow: hidden;
+  }
+
+  .xl\:overflow-visible {
+    overflow: visible;
+  }
+
+  .xl\:overflow-scroll {
+    overflow: scroll;
+  }
+
+  .xl\:overflow-x-auto {
+    overflow-x: auto;
+  }
+
+  .xl\:overflow-y-auto {
+    overflow-y: auto;
+  }
+
+  .xl\:overflow-x-hidden {
+    overflow-x: hidden;
+  }
+
+  .xl\:overflow-y-hidden {
+    overflow-y: hidden;
+  }
+
+  .xl\:overflow-x-visible {
+    overflow-x: visible;
+  }
+
+  .xl\:overflow-y-visible {
+    overflow-y: visible;
+  }
+
+  .xl\:overflow-x-scroll {
+    overflow-x: scroll;
+  }
+
+  .xl\:overflow-y-scroll {
+    overflow-y: scroll;
+  }
+
+  .xl\:scrolling-touch {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .xl\:scrolling-auto {
+    -webkit-overflow-scrolling: auto;
+  }
+
+  .xl\:overscroll-auto {
+    -ms-scroll-chaining: chained;
+        overscroll-behavior: auto;
+  }
+
+  .xl\:overscroll-contain {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: contain;
+  }
+
+  .xl\:overscroll-none {
+    -ms-scroll-chaining: none;
+        overscroll-behavior: none;
+  }
+
+  .xl\:overscroll-y-auto {
+    overscroll-behavior-y: auto;
+  }
+
+  .xl\:overscroll-y-contain {
+    overscroll-behavior-y: contain;
+  }
+
+  .xl\:overscroll-y-none {
+    overscroll-behavior-y: none;
+  }
+
+  .xl\:overscroll-x-auto {
+    overscroll-behavior-x: auto;
+  }
+
+  .xl\:overscroll-x-contain {
+    overscroll-behavior-x: contain;
+  }
+
+  .xl\:overscroll-x-none {
+    overscroll-behavior-x: none;
+  }
+
+  .xl\:p-0 {
+    padding: 0;
+  }
+
+  .xl\:p-1 {
+    padding: 0.25rem;
+  }
+
+  .xl\:p-2 {
+    padding: 0.5rem;
+  }
+
+  .xl\:p-3 {
+    padding: 0.75rem;
+  }
+
+  .xl\:p-4 {
+    padding: 1rem;
+  }
+
+  .xl\:p-5 {
+    padding: 1.25rem;
+  }
+
+  .xl\:p-6 {
+    padding: 1.5rem;
+  }
+
+  .xl\:p-8 {
+    padding: 2rem;
+  }
+
+  .xl\:p-10 {
+    padding: 2.5rem;
+  }
+
+  .xl\:p-12 {
+    padding: 3rem;
+  }
+
+  .xl\:p-16 {
+    padding: 4rem;
+  }
+
+  .xl\:p-20 {
+    padding: 5rem;
+  }
+
+  .xl\:p-24 {
+    padding: 6rem;
+  }
+
+  .xl\:p-32 {
+    padding: 8rem;
+  }
+
+  .xl\:p-40 {
+    padding: 10rem;
+  }
+
+  .xl\:p-48 {
+    padding: 12rem;
+  }
+
+  .xl\:p-56 {
+    padding: 14rem;
+  }
+
+  .xl\:p-64 {
+    padding: 16rem;
+  }
+
+  .xl\:p-px {
+    padding: 1px;
+  }
+
+  .xl\:py-0 {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .xl\:px-0 {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .xl\:py-1 {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+  }
+
+  .xl\:px-1 {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+
+  .xl\:py-2 {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+
+  .xl\:px-2 {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+  }
+
+  .xl\:py-3 {
+    padding-top: 0.75rem;
+    padding-bottom: 0.75rem;
+  }
+
+  .xl\:px-3 {
+    padding-left: 0.75rem;
+    padding-right: 0.75rem;
+  }
+
+  .xl\:py-4 {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+  }
+
+  .xl\:px-4 {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .xl\:py-5 {
+    padding-top: 1.25rem;
+    padding-bottom: 1.25rem;
+  }
+
+  .xl\:px-5 {
+    padding-left: 1.25rem;
+    padding-right: 1.25rem;
+  }
+
+  .xl\:py-6 {
+    padding-top: 1.5rem;
+    padding-bottom: 1.5rem;
+  }
+
+  .xl\:px-6 {
+    padding-left: 1.5rem;
+    padding-right: 1.5rem;
+  }
+
+  .xl\:py-8 {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+
+  .xl\:px-8 {
+    padding-left: 2rem;
+    padding-right: 2rem;
+  }
+
+  .xl\:py-10 {
+    padding-top: 2.5rem;
+    padding-bottom: 2.5rem;
+  }
+
+  .xl\:px-10 {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+
+  .xl\:py-12 {
+    padding-top: 3rem;
+    padding-bottom: 3rem;
+  }
+
+  .xl\:px-12 {
+    padding-left: 3rem;
+    padding-right: 3rem;
+  }
+
+  .xl\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .xl\:px-16 {
+    padding-left: 4rem;
+    padding-right: 4rem;
+  }
+
+  .xl\:py-20 {
+    padding-top: 5rem;
+    padding-bottom: 5rem;
+  }
+
+  .xl\:px-20 {
+    padding-left: 5rem;
+    padding-right: 5rem;
+  }
+
+  .xl\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .xl\:px-24 {
+    padding-left: 6rem;
+    padding-right: 6rem;
+  }
+
+  .xl\:py-32 {
+    padding-top: 8rem;
+    padding-bottom: 8rem;
+  }
+
+  .xl\:px-32 {
+    padding-left: 8rem;
+    padding-right: 8rem;
+  }
+
+  .xl\:py-40 {
+    padding-top: 10rem;
+    padding-bottom: 10rem;
+  }
+
+  .xl\:px-40 {
+    padding-left: 10rem;
+    padding-right: 10rem;
+  }
+
+  .xl\:py-48 {
+    padding-top: 12rem;
+    padding-bottom: 12rem;
+  }
+
+  .xl\:px-48 {
+    padding-left: 12rem;
+    padding-right: 12rem;
+  }
+
+  .xl\:py-56 {
+    padding-top: 14rem;
+    padding-bottom: 14rem;
+  }
+
+  .xl\:px-56 {
+    padding-left: 14rem;
+    padding-right: 14rem;
+  }
+
+  .xl\:py-64 {
+    padding-top: 16rem;
+    padding-bottom: 16rem;
+  }
+
+  .xl\:px-64 {
+    padding-left: 16rem;
+    padding-right: 16rem;
+  }
+
+  .xl\:py-px {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+
+  .xl\:px-px {
+    padding-left: 1px;
+    padding-right: 1px;
+  }
+
+  .xl\:pt-0 {
+    padding-top: 0;
+  }
+
+  .xl\:pr-0 {
+    padding-right: 0;
+  }
+
+  .xl\:pb-0 {
+    padding-bottom: 0;
+  }
+
+  .xl\:pl-0 {
+    padding-left: 0;
+  }
+
+  .xl\:pt-1 {
+    padding-top: 0.25rem;
+  }
+
+  .xl\:pr-1 {
+    padding-right: 0.25rem;
+  }
+
+  .xl\:pb-1 {
+    padding-bottom: 0.25rem;
+  }
+
+  .xl\:pl-1 {
+    padding-left: 0.25rem;
+  }
+
+  .xl\:pt-2 {
+    padding-top: 0.5rem;
+  }
+
+  .xl\:pr-2 {
+    padding-right: 0.5rem;
+  }
+
+  .xl\:pb-2 {
+    padding-bottom: 0.5rem;
+  }
+
+  .xl\:pl-2 {
+    padding-left: 0.5rem;
+  }
+
+  .xl\:pt-3 {
+    padding-top: 0.75rem;
+  }
+
+  .xl\:pr-3 {
+    padding-right: 0.75rem;
+  }
+
+  .xl\:pb-3 {
+    padding-bottom: 0.75rem;
+  }
+
+  .xl\:pl-3 {
+    padding-left: 0.75rem;
+  }
+
+  .xl\:pt-4 {
+    padding-top: 1rem;
+  }
+
+  .xl\:pr-4 {
+    padding-right: 1rem;
+  }
+
+  .xl\:pb-4 {
+    padding-bottom: 1rem;
+  }
+
+  .xl\:pl-4 {
+    padding-left: 1rem;
+  }
+
+  .xl\:pt-5 {
+    padding-top: 1.25rem;
+  }
+
+  .xl\:pr-5 {
+    padding-right: 1.25rem;
+  }
+
+  .xl\:pb-5 {
+    padding-bottom: 1.25rem;
+  }
+
+  .xl\:pl-5 {
+    padding-left: 1.25rem;
+  }
+
+  .xl\:pt-6 {
+    padding-top: 1.5rem;
+  }
+
+  .xl\:pr-6 {
+    padding-right: 1.5rem;
+  }
+
+  .xl\:pb-6 {
+    padding-bottom: 1.5rem;
+  }
+
+  .xl\:pl-6 {
+    padding-left: 1.5rem;
+  }
+
+  .xl\:pt-8 {
+    padding-top: 2rem;
+  }
+
+  .xl\:pr-8 {
+    padding-right: 2rem;
+  }
+
+  .xl\:pb-8 {
+    padding-bottom: 2rem;
+  }
+
+  .xl\:pl-8 {
+    padding-left: 2rem;
+  }
+
+  .xl\:pt-10 {
+    padding-top: 2.5rem;
+  }
+
+  .xl\:pr-10 {
+    padding-right: 2.5rem;
+  }
+
+  .xl\:pb-10 {
+    padding-bottom: 2.5rem;
+  }
+
+  .xl\:pl-10 {
+    padding-left: 2.5rem;
+  }
+
+  .xl\:pt-12 {
+    padding-top: 3rem;
+  }
+
+  .xl\:pr-12 {
+    padding-right: 3rem;
+  }
+
+  .xl\:pb-12 {
+    padding-bottom: 3rem;
+  }
+
+  .xl\:pl-12 {
+    padding-left: 3rem;
+  }
+
+  .xl\:pt-16 {
+    padding-top: 4rem;
+  }
+
+  .xl\:pr-16 {
+    padding-right: 4rem;
+  }
+
+  .xl\:pb-16 {
+    padding-bottom: 4rem;
+  }
+
+  .xl\:pl-16 {
+    padding-left: 4rem;
+  }
+
+  .xl\:pt-20 {
+    padding-top: 5rem;
+  }
+
+  .xl\:pr-20 {
+    padding-right: 5rem;
+  }
+
+  .xl\:pb-20 {
+    padding-bottom: 5rem;
+  }
+
+  .xl\:pl-20 {
+    padding-left: 5rem;
+  }
+
+  .xl\:pt-24 {
+    padding-top: 6rem;
+  }
+
+  .xl\:pr-24 {
+    padding-right: 6rem;
+  }
+
+  .xl\:pb-24 {
+    padding-bottom: 6rem;
+  }
+
+  .xl\:pl-24 {
+    padding-left: 6rem;
+  }
+
+  .xl\:pt-32 {
+    padding-top: 8rem;
+  }
+
+  .xl\:pr-32 {
+    padding-right: 8rem;
+  }
+
+  .xl\:pb-32 {
+    padding-bottom: 8rem;
+  }
+
+  .xl\:pl-32 {
+    padding-left: 8rem;
+  }
+
+  .xl\:pt-40 {
+    padding-top: 10rem;
+  }
+
+  .xl\:pr-40 {
+    padding-right: 10rem;
+  }
+
+  .xl\:pb-40 {
+    padding-bottom: 10rem;
+  }
+
+  .xl\:pl-40 {
+    padding-left: 10rem;
+  }
+
+  .xl\:pt-48 {
+    padding-top: 12rem;
+  }
+
+  .xl\:pr-48 {
+    padding-right: 12rem;
+  }
+
+  .xl\:pb-48 {
+    padding-bottom: 12rem;
+  }
+
+  .xl\:pl-48 {
+    padding-left: 12rem;
+  }
+
+  .xl\:pt-56 {
+    padding-top: 14rem;
+  }
+
+  .xl\:pr-56 {
+    padding-right: 14rem;
+  }
+
+  .xl\:pb-56 {
+    padding-bottom: 14rem;
+  }
+
+  .xl\:pl-56 {
+    padding-left: 14rem;
+  }
+
+  .xl\:pt-64 {
+    padding-top: 16rem;
+  }
+
+  .xl\:pr-64 {
+    padding-right: 16rem;
+  }
+
+  .xl\:pb-64 {
+    padding-bottom: 16rem;
+  }
+
+  .xl\:pl-64 {
+    padding-left: 16rem;
+  }
+
+  .xl\:pt-px {
+    padding-top: 1px;
+  }
+
+  .xl\:pr-px {
+    padding-right: 1px;
+  }
+
+  .xl\:pb-px {
+    padding-bottom: 1px;
+  }
+
+  .xl\:pl-px {
+    padding-left: 1px;
+  }
+
+  .xl\:placeholder-transparent::-moz-placeholder {
+    color: transparent;
+  }
+
+  .xl\:placeholder-transparent:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .xl\:placeholder-transparent::placeholder {
+    color: transparent;
+  }
+
+  .xl\:placeholder-current::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:placeholder-current:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:placeholder-current::placeholder {
+    color: currentColor;
+  }
+
+  .xl\:placeholder-black::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-black:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-black::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-white::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-white:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-white::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-gray-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-red-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-orange-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-yellow-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-green-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-teal-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-blue-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-indigo-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-purple-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-100::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-200::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-200:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-200::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-300::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-300:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-300::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-400::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-400:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-400::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-500::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-500:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-500::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-600::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-600:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-600::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-700::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-700:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-700::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-800::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-800:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-800::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-900::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-900:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-pink-900::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-transparent:focus::-moz-placeholder {
+    color: transparent;
+  }
+
+  .xl\:focus\:placeholder-transparent:focus:-ms-input-placeholder {
+    color: transparent;
+  }
+
+  .xl\:focus\:placeholder-transparent:focus::placeholder {
+    color: transparent;
+  }
+
+  .xl\:focus\:placeholder-current:focus::-moz-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:focus\:placeholder-current:focus:-ms-input-placeholder {
+    color: currentColor;
+  }
+
+  .xl\:focus\:placeholder-current:focus::placeholder {
+    color: currentColor;
+  }
+
+  .xl\:focus\:placeholder-black:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-black:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-black:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-white:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-white:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-white:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-gray-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-red-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-orange-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-yellow-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-green-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-teal-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-blue-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-indigo-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-purple-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-100:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-200:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-200:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-200:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-300:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-300:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-300:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-400:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-400:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-400:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-500:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-500:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-500:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-600:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-600:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-600:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-700:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-700:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-700:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-800:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-800:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-800:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-900:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-900:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:focus\:placeholder-pink-900:focus::placeholder {
+    --placeholder-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--placeholder-opacity));
+  }
+
+  .xl\:placeholder-opacity-0::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:placeholder-opacity-0:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:placeholder-opacity-0::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:placeholder-opacity-25::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:placeholder-opacity-25:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:placeholder-opacity-25::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:placeholder-opacity-50::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:placeholder-opacity-50:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:placeholder-opacity-50::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:placeholder-opacity-75::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:placeholder-opacity-75:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:placeholder-opacity-75::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:placeholder-opacity-100::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:placeholder-opacity-100:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:placeholder-opacity-100::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:focus\:placeholder-opacity-0:focus::-moz-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:focus\:placeholder-opacity-0:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:focus\:placeholder-opacity-0:focus::placeholder {
+    --placeholder-opacity: 0;
+  }
+
+  .xl\:focus\:placeholder-opacity-25:focus::-moz-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:focus\:placeholder-opacity-25:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:focus\:placeholder-opacity-25:focus::placeholder {
+    --placeholder-opacity: 0.25;
+  }
+
+  .xl\:focus\:placeholder-opacity-50:focus::-moz-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:focus\:placeholder-opacity-50:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:focus\:placeholder-opacity-50:focus::placeholder {
+    --placeholder-opacity: 0.5;
+  }
+
+  .xl\:focus\:placeholder-opacity-75:focus::-moz-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:focus\:placeholder-opacity-75:focus:-ms-input-placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:focus\:placeholder-opacity-75:focus::placeholder {
+    --placeholder-opacity: 0.75;
+  }
+
+  .xl\:focus\:placeholder-opacity-100:focus::-moz-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:focus\:placeholder-opacity-100:focus:-ms-input-placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:focus\:placeholder-opacity-100:focus::placeholder {
+    --placeholder-opacity: 1;
+  }
+
+  .xl\:pointer-events-none {
+    pointer-events: none;
+  }
+
+  .xl\:pointer-events-auto {
+    pointer-events: auto;
+  }
+
+  .xl\:static {
+    position: static;
+  }
+
+  .xl\:fixed {
+    position: fixed;
+  }
+
+  .xl\:absolute {
+    position: absolute;
+  }
+
+  .xl\:relative {
+    position: relative;
+  }
+
+  .xl\:sticky {
+    position: -webkit-sticky;
+    position: sticky;
+  }
+
+  .xl\:inset-0 {
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .xl\:inset-auto {
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  .xl\:inset-y-0 {
+    top: 0;
+    bottom: 0;
+  }
+
+  .xl\:inset-x-0 {
+    right: 0;
+    left: 0;
+  }
+
+  .xl\:inset-y-auto {
+    top: auto;
+    bottom: auto;
+  }
+
+  .xl\:inset-x-auto {
+    right: auto;
+    left: auto;
+  }
+
+  .xl\:top-0 {
+    top: 0;
+  }
+
+  .xl\:right-0 {
+    right: 0;
+  }
+
+  .xl\:bottom-0 {
+    bottom: 0;
+  }
+
+  .xl\:left-0 {
+    left: 0;
+  }
+
+  .xl\:top-auto {
+    top: auto;
+  }
+
+  .xl\:right-auto {
+    right: auto;
+  }
+
+  .xl\:bottom-auto {
+    bottom: auto;
+  }
+
+  .xl\:left-auto {
+    left: auto;
+  }
+
+  .xl\:resize-none {
+    resize: none;
+  }
+
+  .xl\:resize-y {
+    resize: vertical;
+  }
+
+  .xl\:resize-x {
+    resize: horizontal;
+  }
+
+  .xl\:resize {
+    resize: both;
+  }
+
+  .xl\:shadow-xs {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:shadow-sm {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:shadow {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:shadow-md {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:shadow-lg {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:shadow-xl {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .xl\:shadow-2xl {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .xl\:shadow-inner {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:shadow-outline {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .xl\:shadow-none {
+    box-shadow: none;
+  }
+
+  .xl\:hover\:shadow-xs:hover {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:hover\:shadow-sm:hover {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:hover\:shadow:hover {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:hover\:shadow-md:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:hover\:shadow-lg:hover {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:hover\:shadow-xl:hover {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .xl\:hover\:shadow-2xl:hover {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .xl\:hover\:shadow-inner:hover {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:hover\:shadow-outline:hover {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .xl\:hover\:shadow-none:hover {
+    box-shadow: none;
+  }
+
+  .xl\:focus\:shadow-xs:focus {
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:focus\:shadow-sm:focus {
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:focus\:shadow:focus {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:focus\:shadow-md:focus {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:focus\:shadow-lg:focus {
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  }
+
+  .xl\:focus\:shadow-xl:focus {
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+  }
+
+  .xl\:focus\:shadow-2xl:focus {
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+  }
+
+  .xl\:focus\:shadow-inner:focus {
+    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
+  }
+
+  .xl\:focus\:shadow-outline:focus {
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
+  }
+
+  .xl\:focus\:shadow-none:focus {
+    box-shadow: none;
+  }
+
+  .xl\:fill-current {
+    fill: currentColor;
+  }
+
+  .xl\:stroke-current {
+    stroke: currentColor;
+  }
+
+  .xl\:stroke-0 {
+    stroke-width: 0;
+  }
+
+  .xl\:stroke-1 {
+    stroke-width: 1;
+  }
+
+  .xl\:stroke-2 {
+    stroke-width: 2;
+  }
+
+  .xl\:table-auto {
+    table-layout: auto;
+  }
+
+  .xl\:table-fixed {
+    table-layout: fixed;
+  }
+
+  .xl\:text-left {
+    text-align: left;
+  }
+
+  .xl\:text-center {
+    text-align: center;
+  }
+
+  .xl\:text-right {
+    text-align: right;
+  }
+
+  .xl\:text-justify {
+    text-align: justify;
+  }
+
+  .xl\:text-transparent {
+    color: transparent;
+  }
+
+  .xl\:text-current {
+    color: currentColor;
+  }
+
+  .xl\:text-black {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .xl\:text-white {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .xl\:text-gray-100 {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .xl\:text-gray-200 {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .xl\:text-gray-300 {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .xl\:text-gray-400 {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .xl\:text-gray-500 {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .xl\:text-gray-600 {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .xl\:text-gray-700 {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .xl\:text-gray-800 {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .xl\:text-gray-900 {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .xl\:text-red-100 {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .xl\:text-red-200 {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .xl\:text-red-300 {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .xl\:text-red-400 {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .xl\:text-red-500 {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .xl\:text-red-600 {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .xl\:text-red-700 {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .xl\:text-red-800 {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .xl\:text-red-900 {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .xl\:text-orange-100 {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .xl\:text-orange-200 {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .xl\:text-orange-300 {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .xl\:text-orange-400 {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .xl\:text-orange-500 {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .xl\:text-orange-600 {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .xl\:text-orange-700 {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .xl\:text-orange-800 {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .xl\:text-orange-900 {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-100 {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-200 {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-300 {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-400 {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-500 {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-600 {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-700 {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-800 {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .xl\:text-yellow-900 {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .xl\:text-green-100 {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .xl\:text-green-200 {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .xl\:text-green-300 {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .xl\:text-green-400 {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .xl\:text-green-500 {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .xl\:text-green-600 {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .xl\:text-green-700 {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .xl\:text-green-800 {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .xl\:text-green-900 {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .xl\:text-teal-100 {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .xl\:text-teal-200 {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .xl\:text-teal-300 {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .xl\:text-teal-400 {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .xl\:text-teal-500 {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .xl\:text-teal-600 {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .xl\:text-teal-700 {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .xl\:text-teal-800 {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .xl\:text-teal-900 {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .xl\:text-blue-100 {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .xl\:text-blue-200 {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .xl\:text-blue-300 {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .xl\:text-blue-400 {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .xl\:text-blue-500 {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .xl\:text-blue-600 {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .xl\:text-blue-700 {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .xl\:text-blue-800 {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .xl\:text-blue-900 {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-100 {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-200 {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-300 {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-400 {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-500 {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-600 {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-700 {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-800 {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .xl\:text-indigo-900 {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .xl\:text-purple-100 {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .xl\:text-purple-200 {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .xl\:text-purple-300 {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .xl\:text-purple-400 {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .xl\:text-purple-500 {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .xl\:text-purple-600 {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .xl\:text-purple-700 {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .xl\:text-purple-800 {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .xl\:text-purple-900 {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .xl\:text-pink-100 {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .xl\:text-pink-200 {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .xl\:text-pink-300 {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .xl\:text-pink-400 {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .xl\:text-pink-500 {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .xl\:text-pink-600 {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .xl\:text-pink-700 {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .xl\:text-pink-800 {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .xl\:text-pink-900 {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-transparent:hover {
+    color: transparent;
+  }
+
+  .xl\:hover\:text-current:hover {
+    color: currentColor;
+  }
+
+  .xl\:hover\:text-black:hover {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-white:hover {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-100:hover {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-200:hover {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-300:hover {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-400:hover {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-500:hover {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-600:hover {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-700:hover {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-800:hover {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-gray-900:hover {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-100:hover {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-200:hover {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-300:hover {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-400:hover {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-500:hover {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-600:hover {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-700:hover {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-800:hover {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-red-900:hover {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-100:hover {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-200:hover {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-300:hover {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-400:hover {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-500:hover {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-600:hover {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-700:hover {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-800:hover {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-orange-900:hover {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-100:hover {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-200:hover {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-300:hover {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-400:hover {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-500:hover {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-600:hover {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-700:hover {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-800:hover {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-yellow-900:hover {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-100:hover {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-200:hover {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-300:hover {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-400:hover {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-500:hover {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-600:hover {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-700:hover {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-800:hover {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-green-900:hover {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-100:hover {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-200:hover {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-300:hover {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-400:hover {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-500:hover {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-600:hover {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-700:hover {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-800:hover {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-teal-900:hover {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-100:hover {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-200:hover {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-300:hover {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-400:hover {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-500:hover {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-600:hover {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-700:hover {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-800:hover {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-blue-900:hover {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-100:hover {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-200:hover {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-300:hover {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-400:hover {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-500:hover {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-600:hover {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-700:hover {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-800:hover {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-indigo-900:hover {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-100:hover {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-200:hover {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-300:hover {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-400:hover {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-500:hover {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-600:hover {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-700:hover {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-800:hover {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-purple-900:hover {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-100:hover {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-200:hover {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-300:hover {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-400:hover {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-500:hover {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-600:hover {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-700:hover {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-800:hover {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .xl\:hover\:text-pink-900:hover {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-transparent:focus {
+    color: transparent;
+  }
+
+  .xl\:focus\:text-current:focus {
+    color: currentColor;
+  }
+
+  .xl\:focus\:text-black:focus {
+    --text-opacity: 1;
+    color: #000;
+    color: rgba(0, 0, 0, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-white:focus {
+    --text-opacity: 1;
+    color: #fff;
+    color: rgba(255, 255, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-100:focus {
+    --text-opacity: 1;
+    color: #f7fafc;
+    color: rgba(247, 250, 252, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-200:focus {
+    --text-opacity: 1;
+    color: #edf2f7;
+    color: rgba(237, 242, 247, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-300:focus {
+    --text-opacity: 1;
+    color: #e2e8f0;
+    color: rgba(226, 232, 240, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-400:focus {
+    --text-opacity: 1;
+    color: #cbd5e0;
+    color: rgba(203, 213, 224, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-500:focus {
+    --text-opacity: 1;
+    color: #a0aec0;
+    color: rgba(160, 174, 192, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-600:focus {
+    --text-opacity: 1;
+    color: #718096;
+    color: rgba(113, 128, 150, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-700:focus {
+    --text-opacity: 1;
+    color: #4a5568;
+    color: rgba(74, 85, 104, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-800:focus {
+    --text-opacity: 1;
+    color: #2d3748;
+    color: rgba(45, 55, 72, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-gray-900:focus {
+    --text-opacity: 1;
+    color: #1a202c;
+    color: rgba(26, 32, 44, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-100:focus {
+    --text-opacity: 1;
+    color: #fff5f5;
+    color: rgba(255, 245, 245, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-200:focus {
+    --text-opacity: 1;
+    color: #fed7d7;
+    color: rgba(254, 215, 215, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-300:focus {
+    --text-opacity: 1;
+    color: #feb2b2;
+    color: rgba(254, 178, 178, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-400:focus {
+    --text-opacity: 1;
+    color: #fc8181;
+    color: rgba(252, 129, 129, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-500:focus {
+    --text-opacity: 1;
+    color: #f56565;
+    color: rgba(245, 101, 101, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-600:focus {
+    --text-opacity: 1;
+    color: #e53e3e;
+    color: rgba(229, 62, 62, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-700:focus {
+    --text-opacity: 1;
+    color: #c53030;
+    color: rgba(197, 48, 48, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-800:focus {
+    --text-opacity: 1;
+    color: #9b2c2c;
+    color: rgba(155, 44, 44, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-red-900:focus {
+    --text-opacity: 1;
+    color: #742a2a;
+    color: rgba(116, 42, 42, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-100:focus {
+    --text-opacity: 1;
+    color: #fffaf0;
+    color: rgba(255, 250, 240, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-200:focus {
+    --text-opacity: 1;
+    color: #feebc8;
+    color: rgba(254, 235, 200, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-300:focus {
+    --text-opacity: 1;
+    color: #fbd38d;
+    color: rgba(251, 211, 141, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-400:focus {
+    --text-opacity: 1;
+    color: #f6ad55;
+    color: rgba(246, 173, 85, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-500:focus {
+    --text-opacity: 1;
+    color: #ed8936;
+    color: rgba(237, 137, 54, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-600:focus {
+    --text-opacity: 1;
+    color: #dd6b20;
+    color: rgba(221, 107, 32, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-700:focus {
+    --text-opacity: 1;
+    color: #c05621;
+    color: rgba(192, 86, 33, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-800:focus {
+    --text-opacity: 1;
+    color: #9c4221;
+    color: rgba(156, 66, 33, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-orange-900:focus {
+    --text-opacity: 1;
+    color: #7b341e;
+    color: rgba(123, 52, 30, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-100:focus {
+    --text-opacity: 1;
+    color: #fffff0;
+    color: rgba(255, 255, 240, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-200:focus {
+    --text-opacity: 1;
+    color: #fefcbf;
+    color: rgba(254, 252, 191, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-300:focus {
+    --text-opacity: 1;
+    color: #faf089;
+    color: rgba(250, 240, 137, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-400:focus {
+    --text-opacity: 1;
+    color: #f6e05e;
+    color: rgba(246, 224, 94, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-500:focus {
+    --text-opacity: 1;
+    color: #ecc94b;
+    color: rgba(236, 201, 75, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-600:focus {
+    --text-opacity: 1;
+    color: #d69e2e;
+    color: rgba(214, 158, 46, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-700:focus {
+    --text-opacity: 1;
+    color: #b7791f;
+    color: rgba(183, 121, 31, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-800:focus {
+    --text-opacity: 1;
+    color: #975a16;
+    color: rgba(151, 90, 22, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-yellow-900:focus {
+    --text-opacity: 1;
+    color: #744210;
+    color: rgba(116, 66, 16, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-100:focus {
+    --text-opacity: 1;
+    color: #f0fff4;
+    color: rgba(240, 255, 244, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-200:focus {
+    --text-opacity: 1;
+    color: #c6f6d5;
+    color: rgba(198, 246, 213, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-300:focus {
+    --text-opacity: 1;
+    color: #9ae6b4;
+    color: rgba(154, 230, 180, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-400:focus {
+    --text-opacity: 1;
+    color: #68d391;
+    color: rgba(104, 211, 145, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-500:focus {
+    --text-opacity: 1;
+    color: #48bb78;
+    color: rgba(72, 187, 120, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-600:focus {
+    --text-opacity: 1;
+    color: #38a169;
+    color: rgba(56, 161, 105, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-700:focus {
+    --text-opacity: 1;
+    color: #2f855a;
+    color: rgba(47, 133, 90, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-800:focus {
+    --text-opacity: 1;
+    color: #276749;
+    color: rgba(39, 103, 73, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-green-900:focus {
+    --text-opacity: 1;
+    color: #22543d;
+    color: rgba(34, 84, 61, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-100:focus {
+    --text-opacity: 1;
+    color: #e6fffa;
+    color: rgba(230, 255, 250, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-200:focus {
+    --text-opacity: 1;
+    color: #b2f5ea;
+    color: rgba(178, 245, 234, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-300:focus {
+    --text-opacity: 1;
+    color: #81e6d9;
+    color: rgba(129, 230, 217, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-400:focus {
+    --text-opacity: 1;
+    color: #4fd1c5;
+    color: rgba(79, 209, 197, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-500:focus {
+    --text-opacity: 1;
+    color: #38b2ac;
+    color: rgba(56, 178, 172, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-600:focus {
+    --text-opacity: 1;
+    color: #319795;
+    color: rgba(49, 151, 149, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-700:focus {
+    --text-opacity: 1;
+    color: #2c7a7b;
+    color: rgba(44, 122, 123, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-800:focus {
+    --text-opacity: 1;
+    color: #285e61;
+    color: rgba(40, 94, 97, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-teal-900:focus {
+    --text-opacity: 1;
+    color: #234e52;
+    color: rgba(35, 78, 82, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-100:focus {
+    --text-opacity: 1;
+    color: #ebf8ff;
+    color: rgba(235, 248, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-200:focus {
+    --text-opacity: 1;
+    color: #bee3f8;
+    color: rgba(190, 227, 248, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-300:focus {
+    --text-opacity: 1;
+    color: #90cdf4;
+    color: rgba(144, 205, 244, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-400:focus {
+    --text-opacity: 1;
+    color: #63b3ed;
+    color: rgba(99, 179, 237, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-500:focus {
+    --text-opacity: 1;
+    color: #4299e1;
+    color: rgba(66, 153, 225, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-600:focus {
+    --text-opacity: 1;
+    color: #3182ce;
+    color: rgba(49, 130, 206, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-700:focus {
+    --text-opacity: 1;
+    color: #2b6cb0;
+    color: rgba(43, 108, 176, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-800:focus {
+    --text-opacity: 1;
+    color: #2c5282;
+    color: rgba(44, 82, 130, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-blue-900:focus {
+    --text-opacity: 1;
+    color: #2a4365;
+    color: rgba(42, 67, 101, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-100:focus {
+    --text-opacity: 1;
+    color: #ebf4ff;
+    color: rgba(235, 244, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-200:focus {
+    --text-opacity: 1;
+    color: #c3dafe;
+    color: rgba(195, 218, 254, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-300:focus {
+    --text-opacity: 1;
+    color: #a3bffa;
+    color: rgba(163, 191, 250, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-400:focus {
+    --text-opacity: 1;
+    color: #7f9cf5;
+    color: rgba(127, 156, 245, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-500:focus {
+    --text-opacity: 1;
+    color: #667eea;
+    color: rgba(102, 126, 234, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-600:focus {
+    --text-opacity: 1;
+    color: #5a67d8;
+    color: rgba(90, 103, 216, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-700:focus {
+    --text-opacity: 1;
+    color: #4c51bf;
+    color: rgba(76, 81, 191, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-800:focus {
+    --text-opacity: 1;
+    color: #434190;
+    color: rgba(67, 65, 144, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-indigo-900:focus {
+    --text-opacity: 1;
+    color: #3c366b;
+    color: rgba(60, 54, 107, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-100:focus {
+    --text-opacity: 1;
+    color: #faf5ff;
+    color: rgba(250, 245, 255, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-200:focus {
+    --text-opacity: 1;
+    color: #e9d8fd;
+    color: rgba(233, 216, 253, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-300:focus {
+    --text-opacity: 1;
+    color: #d6bcfa;
+    color: rgba(214, 188, 250, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-400:focus {
+    --text-opacity: 1;
+    color: #b794f4;
+    color: rgba(183, 148, 244, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-500:focus {
+    --text-opacity: 1;
+    color: #9f7aea;
+    color: rgba(159, 122, 234, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-600:focus {
+    --text-opacity: 1;
+    color: #805ad5;
+    color: rgba(128, 90, 213, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-700:focus {
+    --text-opacity: 1;
+    color: #6b46c1;
+    color: rgba(107, 70, 193, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-800:focus {
+    --text-opacity: 1;
+    color: #553c9a;
+    color: rgba(85, 60, 154, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-purple-900:focus {
+    --text-opacity: 1;
+    color: #44337a;
+    color: rgba(68, 51, 122, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-100:focus {
+    --text-opacity: 1;
+    color: #fff5f7;
+    color: rgba(255, 245, 247, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-200:focus {
+    --text-opacity: 1;
+    color: #fed7e2;
+    color: rgba(254, 215, 226, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-300:focus {
+    --text-opacity: 1;
+    color: #fbb6ce;
+    color: rgba(251, 182, 206, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-400:focus {
+    --text-opacity: 1;
+    color: #f687b3;
+    color: rgba(246, 135, 179, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-500:focus {
+    --text-opacity: 1;
+    color: #ed64a6;
+    color: rgba(237, 100, 166, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-600:focus {
+    --text-opacity: 1;
+    color: #d53f8c;
+    color: rgba(213, 63, 140, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-700:focus {
+    --text-opacity: 1;
+    color: #b83280;
+    color: rgba(184, 50, 128, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-800:focus {
+    --text-opacity: 1;
+    color: #97266d;
+    color: rgba(151, 38, 109, var(--text-opacity));
+  }
+
+  .xl\:focus\:text-pink-900:focus {
+    --text-opacity: 1;
+    color: #702459;
+    color: rgba(112, 36, 89, var(--text-opacity));
+  }
+
+  .xl\:text-opacity-0 {
+    --text-opacity: 0;
+  }
+
+  .xl\:text-opacity-25 {
+    --text-opacity: 0.25;
+  }
+
+  .xl\:text-opacity-50 {
+    --text-opacity: 0.5;
+  }
+
+  .xl\:text-opacity-75 {
+    --text-opacity: 0.75;
+  }
+
+  .xl\:text-opacity-100 {
+    --text-opacity: 1;
+  }
+
+  .xl\:hover\:text-opacity-0:hover {
+    --text-opacity: 0;
+  }
+
+  .xl\:hover\:text-opacity-25:hover {
+    --text-opacity: 0.25;
+  }
+
+  .xl\:hover\:text-opacity-50:hover {
+    --text-opacity: 0.5;
+  }
+
+  .xl\:hover\:text-opacity-75:hover {
+    --text-opacity: 0.75;
+  }
+
+  .xl\:hover\:text-opacity-100:hover {
+    --text-opacity: 1;
+  }
+
+  .xl\:focus\:text-opacity-0:focus {
+    --text-opacity: 0;
+  }
+
+  .xl\:focus\:text-opacity-25:focus {
+    --text-opacity: 0.25;
+  }
+
+  .xl\:focus\:text-opacity-50:focus {
+    --text-opacity: 0.5;
+  }
+
+  .xl\:focus\:text-opacity-75:focus {
+    --text-opacity: 0.75;
+  }
+
+  .xl\:focus\:text-opacity-100:focus {
+    --text-opacity: 1;
+  }
+
+  .xl\:italic {
+    font-style: italic;
+  }
+
+  .xl\:not-italic {
+    font-style: normal;
+  }
+
+  .xl\:uppercase {
+    text-transform: uppercase;
+  }
+
+  .xl\:lowercase {
+    text-transform: lowercase;
+  }
+
+  .xl\:capitalize {
+    text-transform: capitalize;
+  }
+
+  .xl\:normal-case {
+    text-transform: none;
+  }
+
+  .xl\:underline {
+    text-decoration: underline;
+  }
+
+  .xl\:line-through {
+    text-decoration: line-through;
+  }
+
+  .xl\:no-underline {
+    text-decoration: none;
+  }
+
+  .xl\:hover\:underline:hover {
+    text-decoration: underline;
+  }
+
+  .xl\:hover\:line-through:hover {
+    text-decoration: line-through;
+  }
+
+  .xl\:hover\:no-underline:hover {
+    text-decoration: none;
+  }
+
+  .xl\:focus\:underline:focus {
+    text-decoration: underline;
+  }
+
+  .xl\:focus\:line-through:focus {
+    text-decoration: line-through;
+  }
+
+  .xl\:focus\:no-underline:focus {
+    text-decoration: none;
+  }
+
+  .xl\:antialiased {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+
+  .xl\:subpixel-antialiased {
+    -webkit-font-smoothing: auto;
+    -moz-osx-font-smoothing: auto;
+  }
+
+  .xl\:ordinal, .xl\:slashed-zero, .xl\:lining-nums, .xl\:oldstyle-nums, .xl\:proportional-nums, .xl\:tabular-nums, .xl\:diagonal-fractions, .xl\:stacked-fractions {
+    --font-variant-numeric-ordinal: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-slashed-zero: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-figure: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-spacing: var(--tailwind-empty,/*!*/ /*!*/);
+    --font-variant-numeric-fraction: var(--tailwind-empty,/*!*/ /*!*/);
+    font-variant-numeric: var(--font-variant-numeric-ordinal) var(--font-variant-numeric-slashed-zero) var(--font-variant-numeric-figure) var(--font-variant-numeric-spacing) var(--font-variant-numeric-fraction);
+  }
+
+  .xl\:normal-nums {
+    font-variant-numeric: normal;
+  }
+
+  .xl\:ordinal {
+    --font-variant-numeric-ordinal: ordinal;
+  }
+
+  .xl\:slashed-zero {
+    --font-variant-numeric-slashed-zero: slashed-zero;
+  }
+
+  .xl\:lining-nums {
+    --font-variant-numeric-figure: lining-nums;
+  }
+
+  .xl\:oldstyle-nums {
+    --font-variant-numeric-figure: oldstyle-nums;
+  }
+
+  .xl\:proportional-nums {
+    --font-variant-numeric-spacing: proportional-nums;
+  }
+
+  .xl\:tabular-nums {
+    --font-variant-numeric-spacing: tabular-nums;
+  }
+
+  .xl\:diagonal-fractions {
+    --font-variant-numeric-fraction: diagonal-fractions;
+  }
+
+  .xl\:stacked-fractions {
+    --font-variant-numeric-fraction: stacked-fractions;
+  }
+
+  .xl\:tracking-tighter {
+    letter-spacing: -0.05em;
+  }
+
+  .xl\:tracking-tight {
+    letter-spacing: -0.025em;
+  }
+
+  .xl\:tracking-normal {
+    letter-spacing: 0;
+  }
+
+  .xl\:tracking-wide {
+    letter-spacing: 0.025em;
+  }
+
+  .xl\:tracking-wider {
+    letter-spacing: 0.05em;
+  }
+
+  .xl\:tracking-widest {
+    letter-spacing: 0.1em;
+  }
+
+  .xl\:select-none {
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+  }
+
+  .xl\:select-text {
+    -webkit-user-select: text;
+       -moz-user-select: text;
+        -ms-user-select: text;
+            user-select: text;
+  }
+
+  .xl\:select-all {
+    -webkit-user-select: all;
+       -moz-user-select: all;
+        -ms-user-select: all;
+            user-select: all;
+  }
+
+  .xl\:select-auto {
+    -webkit-user-select: auto;
+       -moz-user-select: auto;
+        -ms-user-select: auto;
+            user-select: auto;
+  }
+
+  .xl\:align-baseline {
+    vertical-align: baseline;
+  }
+
+  .xl\:align-top {
+    vertical-align: top;
+  }
+
+  .xl\:align-middle {
+    vertical-align: middle;
+  }
+
+  .xl\:align-bottom {
+    vertical-align: bottom;
+  }
+
+  .xl\:align-text-top {
+    vertical-align: text-top;
+  }
+
+  .xl\:align-text-bottom {
+    vertical-align: text-bottom;
+  }
+
+  .xl\:visible {
+    visibility: visible;
+  }
+
+  .xl\:invisible {
+    visibility: hidden;
+  }
+
+  .xl\:whitespace-normal {
+    white-space: normal;
+  }
+
+  .xl\:whitespace-no-wrap {
+    white-space: nowrap;
+  }
+
+  .xl\:whitespace-pre {
+    white-space: pre;
+  }
+
+  .xl\:whitespace-pre-line {
+    white-space: pre-line;
+  }
+
+  .xl\:whitespace-pre-wrap {
+    white-space: pre-wrap;
+  }
+
+  .xl\:break-normal {
+    overflow-wrap: normal;
+    word-break: normal;
+  }
+
+  .xl\:break-words {
+    overflow-wrap: break-word;
+  }
+
+  .xl\:break-all {
+    word-break: break-all;
+  }
+
+  .xl\:truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .xl\:w-0 {
+    width: 0;
+  }
+
+  .xl\:w-1 {
+    width: 0.25rem;
+  }
+
+  .xl\:w-2 {
+    width: 0.5rem;
+  }
+
+  .xl\:w-3 {
+    width: 0.75rem;
+  }
+
+  .xl\:w-4 {
+    width: 1rem;
+  }
+
+  .xl\:w-5 {
+    width: 1.25rem;
+  }
+
+  .xl\:w-6 {
+    width: 1.5rem;
+  }
+
+  .xl\:w-8 {
+    width: 2rem;
+  }
+
+  .xl\:w-10 {
+    width: 2.5rem;
+  }
+
+  .xl\:w-12 {
+    width: 3rem;
+  }
+
+  .xl\:w-16 {
+    width: 4rem;
+  }
+
+  .xl\:w-20 {
+    width: 5rem;
+  }
+
+  .xl\:w-24 {
+    width: 6rem;
+  }
+
+  .xl\:w-32 {
+    width: 8rem;
+  }
+
+  .xl\:w-40 {
+    width: 10rem;
+  }
+
+  .xl\:w-48 {
+    width: 12rem;
+  }
+
+  .xl\:w-56 {
+    width: 14rem;
+  }
+
+  .xl\:w-64 {
+    width: 16rem;
+  }
+
+  .xl\:w-auto {
+    width: auto;
+  }
+
+  .xl\:w-px {
+    width: 1px;
+  }
+
+  .xl\:w-1\/2 {
+    width: 50%;
+  }
+
+  .xl\:w-1\/3 {
+    width: 33.333333%;
+  }
+
+  .xl\:w-2\/3 {
+    width: 66.666667%;
+  }
+
+  .xl\:w-1\/4 {
+    width: 25%;
+  }
+
+  .xl\:w-2\/4 {
+    width: 50%;
+  }
+
+  .xl\:w-3\/4 {
+    width: 75%;
+  }
+
+  .xl\:w-1\/5 {
+    width: 20%;
+  }
+
+  .xl\:w-2\/5 {
+    width: 40%;
+  }
+
+  .xl\:w-3\/5 {
+    width: 60%;
+  }
+
+  .xl\:w-4\/5 {
+    width: 80%;
+  }
+
+  .xl\:w-1\/6 {
+    width: 16.666667%;
+  }
+
+  .xl\:w-2\/6 {
+    width: 33.333333%;
+  }
+
+  .xl\:w-3\/6 {
+    width: 50%;
+  }
+
+  .xl\:w-4\/6 {
+    width: 66.666667%;
+  }
+
+  .xl\:w-5\/6 {
+    width: 83.333333%;
+  }
+
+  .xl\:w-1\/12 {
+    width: 8.333333%;
+  }
+
+  .xl\:w-2\/12 {
+    width: 16.666667%;
+  }
+
+  .xl\:w-3\/12 {
+    width: 25%;
+  }
+
+  .xl\:w-4\/12 {
+    width: 33.333333%;
+  }
+
+  .xl\:w-5\/12 {
+    width: 41.666667%;
+  }
+
+  .xl\:w-6\/12 {
+    width: 50%;
+  }
+
+  .xl\:w-7\/12 {
+    width: 58.333333%;
+  }
+
+  .xl\:w-8\/12 {
+    width: 66.666667%;
+  }
+
+  .xl\:w-9\/12 {
+    width: 75%;
+  }
+
+  .xl\:w-10\/12 {
+    width: 83.333333%;
+  }
+
+  .xl\:w-11\/12 {
+    width: 91.666667%;
+  }
+
+  .xl\:w-full {
+    width: 100%;
+  }
+
+  .xl\:w-screen {
+    width: 100vw;
+  }
+
+  .xl\:z-0 {
+    z-index: 0;
+  }
+
+  .xl\:z-10 {
+    z-index: 10;
+  }
+
+  .xl\:z-20 {
+    z-index: 20;
+  }
+
+  .xl\:z-30 {
+    z-index: 30;
+  }
+
+  .xl\:z-40 {
+    z-index: 40;
+  }
+
+  .xl\:z-50 {
+    z-index: 50;
+  }
+
+  .xl\:z-auto {
+    z-index: auto;
+  }
+
+  .xl\:gap-0 {
+    grid-gap: 0;
+    gap: 0;
+  }
+
+  .xl\:gap-1 {
+    grid-gap: 0.25rem;
+    gap: 0.25rem;
+  }
+
+  .xl\:gap-2 {
+    grid-gap: 0.5rem;
+    gap: 0.5rem;
+  }
+
+  .xl\:gap-3 {
+    grid-gap: 0.75rem;
+    gap: 0.75rem;
+  }
+
+  .xl\:gap-4 {
+    grid-gap: 1rem;
+    gap: 1rem;
+  }
+
+  .xl\:gap-5 {
+    grid-gap: 1.25rem;
+    gap: 1.25rem;
+  }
+
+  .xl\:gap-6 {
+    grid-gap: 1.5rem;
+    gap: 1.5rem;
+  }
+
+  .xl\:gap-8 {
+    grid-gap: 2rem;
+    gap: 2rem;
+  }
+
+  .xl\:gap-10 {
+    grid-gap: 2.5rem;
+    gap: 2.5rem;
+  }
+
+  .xl\:gap-12 {
+    grid-gap: 3rem;
+    gap: 3rem;
+  }
+
+  .xl\:gap-16 {
+    grid-gap: 4rem;
+    gap: 4rem;
+  }
+
+  .xl\:gap-20 {
+    grid-gap: 5rem;
+    gap: 5rem;
+  }
+
+  .xl\:gap-24 {
+    grid-gap: 6rem;
+    gap: 6rem;
+  }
+
+  .xl\:gap-32 {
+    grid-gap: 8rem;
+    gap: 8rem;
+  }
+
+  .xl\:gap-40 {
+    grid-gap: 10rem;
+    gap: 10rem;
+  }
+
+  .xl\:gap-48 {
+    grid-gap: 12rem;
+    gap: 12rem;
+  }
+
+  .xl\:gap-56 {
+    grid-gap: 14rem;
+    gap: 14rem;
+  }
+
+  .xl\:gap-64 {
+    grid-gap: 16rem;
+    gap: 16rem;
+  }
+
+  .xl\:gap-px {
+    grid-gap: 1px;
+    gap: 1px;
+  }
+
+  .xl\:col-gap-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .xl\:col-gap-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .xl\:col-gap-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .xl\:col-gap-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .xl\:col-gap-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .xl\:col-gap-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .xl\:col-gap-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .xl\:col-gap-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .xl\:col-gap-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .xl\:col-gap-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .xl\:col-gap-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .xl\:col-gap-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .xl\:col-gap-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .xl\:col-gap-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .xl\:col-gap-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .xl\:col-gap-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .xl\:col-gap-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .xl\:col-gap-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .xl\:col-gap-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .xl\:gap-x-0 {
+    grid-column-gap: 0;
+    -moz-column-gap: 0;
+         column-gap: 0;
+  }
+
+  .xl\:gap-x-1 {
+    grid-column-gap: 0.25rem;
+    -moz-column-gap: 0.25rem;
+         column-gap: 0.25rem;
+  }
+
+  .xl\:gap-x-2 {
+    grid-column-gap: 0.5rem;
+    -moz-column-gap: 0.5rem;
+         column-gap: 0.5rem;
+  }
+
+  .xl\:gap-x-3 {
+    grid-column-gap: 0.75rem;
+    -moz-column-gap: 0.75rem;
+         column-gap: 0.75rem;
+  }
+
+  .xl\:gap-x-4 {
+    grid-column-gap: 1rem;
+    -moz-column-gap: 1rem;
+         column-gap: 1rem;
+  }
+
+  .xl\:gap-x-5 {
+    grid-column-gap: 1.25rem;
+    -moz-column-gap: 1.25rem;
+         column-gap: 1.25rem;
+  }
+
+  .xl\:gap-x-6 {
+    grid-column-gap: 1.5rem;
+    -moz-column-gap: 1.5rem;
+         column-gap: 1.5rem;
+  }
+
+  .xl\:gap-x-8 {
+    grid-column-gap: 2rem;
+    -moz-column-gap: 2rem;
+         column-gap: 2rem;
+  }
+
+  .xl\:gap-x-10 {
+    grid-column-gap: 2.5rem;
+    -moz-column-gap: 2.5rem;
+         column-gap: 2.5rem;
+  }
+
+  .xl\:gap-x-12 {
+    grid-column-gap: 3rem;
+    -moz-column-gap: 3rem;
+         column-gap: 3rem;
+  }
+
+  .xl\:gap-x-16 {
+    grid-column-gap: 4rem;
+    -moz-column-gap: 4rem;
+         column-gap: 4rem;
+  }
+
+  .xl\:gap-x-20 {
+    grid-column-gap: 5rem;
+    -moz-column-gap: 5rem;
+         column-gap: 5rem;
+  }
+
+  .xl\:gap-x-24 {
+    grid-column-gap: 6rem;
+    -moz-column-gap: 6rem;
+         column-gap: 6rem;
+  }
+
+  .xl\:gap-x-32 {
+    grid-column-gap: 8rem;
+    -moz-column-gap: 8rem;
+         column-gap: 8rem;
+  }
+
+  .xl\:gap-x-40 {
+    grid-column-gap: 10rem;
+    -moz-column-gap: 10rem;
+         column-gap: 10rem;
+  }
+
+  .xl\:gap-x-48 {
+    grid-column-gap: 12rem;
+    -moz-column-gap: 12rem;
+         column-gap: 12rem;
+  }
+
+  .xl\:gap-x-56 {
+    grid-column-gap: 14rem;
+    -moz-column-gap: 14rem;
+         column-gap: 14rem;
+  }
+
+  .xl\:gap-x-64 {
+    grid-column-gap: 16rem;
+    -moz-column-gap: 16rem;
+         column-gap: 16rem;
+  }
+
+  .xl\:gap-x-px {
+    grid-column-gap: 1px;
+    -moz-column-gap: 1px;
+         column-gap: 1px;
+  }
+
+  .xl\:row-gap-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .xl\:row-gap-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .xl\:row-gap-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .xl\:row-gap-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .xl\:row-gap-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .xl\:row-gap-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .xl\:row-gap-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .xl\:row-gap-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .xl\:row-gap-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .xl\:row-gap-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .xl\:row-gap-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .xl\:row-gap-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .xl\:row-gap-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .xl\:row-gap-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .xl\:row-gap-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .xl\:row-gap-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .xl\:row-gap-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .xl\:row-gap-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .xl\:row-gap-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .xl\:gap-y-0 {
+    grid-row-gap: 0;
+    row-gap: 0;
+  }
+
+  .xl\:gap-y-1 {
+    grid-row-gap: 0.25rem;
+    row-gap: 0.25rem;
+  }
+
+  .xl\:gap-y-2 {
+    grid-row-gap: 0.5rem;
+    row-gap: 0.5rem;
+  }
+
+  .xl\:gap-y-3 {
+    grid-row-gap: 0.75rem;
+    row-gap: 0.75rem;
+  }
+
+  .xl\:gap-y-4 {
+    grid-row-gap: 1rem;
+    row-gap: 1rem;
+  }
+
+  .xl\:gap-y-5 {
+    grid-row-gap: 1.25rem;
+    row-gap: 1.25rem;
+  }
+
+  .xl\:gap-y-6 {
+    grid-row-gap: 1.5rem;
+    row-gap: 1.5rem;
+  }
+
+  .xl\:gap-y-8 {
+    grid-row-gap: 2rem;
+    row-gap: 2rem;
+  }
+
+  .xl\:gap-y-10 {
+    grid-row-gap: 2.5rem;
+    row-gap: 2.5rem;
+  }
+
+  .xl\:gap-y-12 {
+    grid-row-gap: 3rem;
+    row-gap: 3rem;
+  }
+
+  .xl\:gap-y-16 {
+    grid-row-gap: 4rem;
+    row-gap: 4rem;
+  }
+
+  .xl\:gap-y-20 {
+    grid-row-gap: 5rem;
+    row-gap: 5rem;
+  }
+
+  .xl\:gap-y-24 {
+    grid-row-gap: 6rem;
+    row-gap: 6rem;
+  }
+
+  .xl\:gap-y-32 {
+    grid-row-gap: 8rem;
+    row-gap: 8rem;
+  }
+
+  .xl\:gap-y-40 {
+    grid-row-gap: 10rem;
+    row-gap: 10rem;
+  }
+
+  .xl\:gap-y-48 {
+    grid-row-gap: 12rem;
+    row-gap: 12rem;
+  }
+
+  .xl\:gap-y-56 {
+    grid-row-gap: 14rem;
+    row-gap: 14rem;
+  }
+
+  .xl\:gap-y-64 {
+    grid-row-gap: 16rem;
+    row-gap: 16rem;
+  }
+
+  .xl\:gap-y-px {
+    grid-row-gap: 1px;
+    row-gap: 1px;
+  }
+
+  .xl\:grid-flow-row {
+    grid-auto-flow: row;
+  }
+
+  .xl\:grid-flow-col {
+    grid-auto-flow: column;
+  }
+
+  .xl\:grid-flow-row-dense {
+    grid-auto-flow: row dense;
+  }
+
+  .xl\:grid-flow-col-dense {
+    grid-auto-flow: column dense;
+  }
+
+  .xl\:grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-5 {
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-6 {
+    grid-template-columns: repeat(6, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-7 {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-8 {
+    grid-template-columns: repeat(8, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-9 {
+    grid-template-columns: repeat(9, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-10 {
+    grid-template-columns: repeat(10, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-11 {
+    grid-template-columns: repeat(11, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-12 {
+    grid-template-columns: repeat(12, minmax(0, 1fr));
+  }
+
+  .xl\:grid-cols-none {
+    grid-template-columns: none;
+  }
+
+  .xl\:col-auto {
+    grid-column: auto;
+  }
+
+  .xl\:col-span-1 {
+    grid-column: span 1 / span 1;
+  }
+
+  .xl\:col-span-2 {
+    grid-column: span 2 / span 2;
+  }
+
+  .xl\:col-span-3 {
+    grid-column: span 3 / span 3;
+  }
+
+  .xl\:col-span-4 {
+    grid-column: span 4 / span 4;
+  }
+
+  .xl\:col-span-5 {
+    grid-column: span 5 / span 5;
+  }
+
+  .xl\:col-span-6 {
+    grid-column: span 6 / span 6;
+  }
+
+  .xl\:col-span-7 {
+    grid-column: span 7 / span 7;
+  }
+
+  .xl\:col-span-8 {
+    grid-column: span 8 / span 8;
+  }
+
+  .xl\:col-span-9 {
+    grid-column: span 9 / span 9;
+  }
+
+  .xl\:col-span-10 {
+    grid-column: span 10 / span 10;
+  }
+
+  .xl\:col-span-11 {
+    grid-column: span 11 / span 11;
+  }
+
+  .xl\:col-span-12 {
+    grid-column: span 12 / span 12;
+  }
+
+  .xl\:col-start-1 {
+    grid-column-start: 1;
+  }
+
+  .xl\:col-start-2 {
+    grid-column-start: 2;
+  }
+
+  .xl\:col-start-3 {
+    grid-column-start: 3;
+  }
+
+  .xl\:col-start-4 {
+    grid-column-start: 4;
+  }
+
+  .xl\:col-start-5 {
+    grid-column-start: 5;
+  }
+
+  .xl\:col-start-6 {
+    grid-column-start: 6;
+  }
+
+  .xl\:col-start-7 {
+    grid-column-start: 7;
+  }
+
+  .xl\:col-start-8 {
+    grid-column-start: 8;
+  }
+
+  .xl\:col-start-9 {
+    grid-column-start: 9;
+  }
+
+  .xl\:col-start-10 {
+    grid-column-start: 10;
+  }
+
+  .xl\:col-start-11 {
+    grid-column-start: 11;
+  }
+
+  .xl\:col-start-12 {
+    grid-column-start: 12;
+  }
+
+  .xl\:col-start-13 {
+    grid-column-start: 13;
+  }
+
+  .xl\:col-start-auto {
+    grid-column-start: auto;
+  }
+
+  .xl\:col-end-1 {
+    grid-column-end: 1;
+  }
+
+  .xl\:col-end-2 {
+    grid-column-end: 2;
+  }
+
+  .xl\:col-end-3 {
+    grid-column-end: 3;
+  }
+
+  .xl\:col-end-4 {
+    grid-column-end: 4;
+  }
+
+  .xl\:col-end-5 {
+    grid-column-end: 5;
+  }
+
+  .xl\:col-end-6 {
+    grid-column-end: 6;
+  }
+
+  .xl\:col-end-7 {
+    grid-column-end: 7;
+  }
+
+  .xl\:col-end-8 {
+    grid-column-end: 8;
+  }
+
+  .xl\:col-end-9 {
+    grid-column-end: 9;
+  }
+
+  .xl\:col-end-10 {
+    grid-column-end: 10;
+  }
+
+  .xl\:col-end-11 {
+    grid-column-end: 11;
+  }
+
+  .xl\:col-end-12 {
+    grid-column-end: 12;
+  }
+
+  .xl\:col-end-13 {
+    grid-column-end: 13;
+  }
+
+  .xl\:col-end-auto {
+    grid-column-end: auto;
+  }
+
+  .xl\:grid-rows-1 {
+    grid-template-rows: repeat(1, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-2 {
+    grid-template-rows: repeat(2, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-3 {
+    grid-template-rows: repeat(3, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-4 {
+    grid-template-rows: repeat(4, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-5 {
+    grid-template-rows: repeat(5, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-6 {
+    grid-template-rows: repeat(6, minmax(0, 1fr));
+  }
+
+  .xl\:grid-rows-none {
+    grid-template-rows: none;
+  }
+
+  .xl\:row-auto {
+    grid-row: auto;
+  }
+
+  .xl\:row-span-1 {
+    grid-row: span 1 / span 1;
+  }
+
+  .xl\:row-span-2 {
+    grid-row: span 2 / span 2;
+  }
+
+  .xl\:row-span-3 {
+    grid-row: span 3 / span 3;
+  }
+
+  .xl\:row-span-4 {
+    grid-row: span 4 / span 4;
+  }
+
+  .xl\:row-span-5 {
+    grid-row: span 5 / span 5;
+  }
+
+  .xl\:row-span-6 {
+    grid-row: span 6 / span 6;
+  }
+
+  .xl\:row-start-1 {
+    grid-row-start: 1;
+  }
+
+  .xl\:row-start-2 {
+    grid-row-start: 2;
+  }
+
+  .xl\:row-start-3 {
+    grid-row-start: 3;
+  }
+
+  .xl\:row-start-4 {
+    grid-row-start: 4;
+  }
+
+  .xl\:row-start-5 {
+    grid-row-start: 5;
+  }
+
+  .xl\:row-start-6 {
+    grid-row-start: 6;
+  }
+
+  .xl\:row-start-7 {
+    grid-row-start: 7;
+  }
+
+  .xl\:row-start-auto {
+    grid-row-start: auto;
+  }
+
+  .xl\:row-end-1 {
+    grid-row-end: 1;
+  }
+
+  .xl\:row-end-2 {
+    grid-row-end: 2;
+  }
+
+  .xl\:row-end-3 {
+    grid-row-end: 3;
+  }
+
+  .xl\:row-end-4 {
+    grid-row-end: 4;
+  }
+
+  .xl\:row-end-5 {
+    grid-row-end: 5;
+  }
+
+  .xl\:row-end-6 {
+    grid-row-end: 6;
+  }
+
+  .xl\:row-end-7 {
+    grid-row-end: 7;
+  }
+
+  .xl\:row-end-auto {
+    grid-row-end: auto;
+  }
+
+  .xl\:transform {
+    --transform-translate-x: 0;
+    --transform-translate-y: 0;
+    --transform-rotate: 0;
+    --transform-skew-x: 0;
+    --transform-skew-y: 0;
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+    transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y));
+  }
+
+  .xl\:transform-none {
+    transform: none;
+  }
+
+  .xl\:origin-center {
+    transform-origin: center;
+  }
+
+  .xl\:origin-top {
+    transform-origin: top;
+  }
+
+  .xl\:origin-top-right {
+    transform-origin: top right;
+  }
+
+  .xl\:origin-right {
+    transform-origin: right;
+  }
+
+  .xl\:origin-bottom-right {
+    transform-origin: bottom right;
+  }
+
+  .xl\:origin-bottom {
+    transform-origin: bottom;
+  }
+
+  .xl\:origin-bottom-left {
+    transform-origin: bottom left;
+  }
+
+  .xl\:origin-left {
+    transform-origin: left;
+  }
+
+  .xl\:origin-top-left {
+    transform-origin: top left;
+  }
+
+  .xl\:scale-0 {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .xl\:scale-50 {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .xl\:scale-75 {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .xl\:scale-90 {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .xl\:scale-95 {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .xl\:scale-100 {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .xl\:scale-105 {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:scale-110 {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:scale-125 {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:scale-150 {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:scale-x-0 {
+    --transform-scale-x: 0;
+  }
+
+  .xl\:scale-x-50 {
+    --transform-scale-x: .5;
+  }
+
+  .xl\:scale-x-75 {
+    --transform-scale-x: .75;
+  }
+
+  .xl\:scale-x-90 {
+    --transform-scale-x: .9;
+  }
+
+  .xl\:scale-x-95 {
+    --transform-scale-x: .95;
+  }
+
+  .xl\:scale-x-100 {
+    --transform-scale-x: 1;
+  }
+
+  .xl\:scale-x-105 {
+    --transform-scale-x: 1.05;
+  }
+
+  .xl\:scale-x-110 {
+    --transform-scale-x: 1.1;
+  }
+
+  .xl\:scale-x-125 {
+    --transform-scale-x: 1.25;
+  }
+
+  .xl\:scale-x-150 {
+    --transform-scale-x: 1.5;
+  }
+
+  .xl\:scale-y-0 {
+    --transform-scale-y: 0;
+  }
+
+  .xl\:scale-y-50 {
+    --transform-scale-y: .5;
+  }
+
+  .xl\:scale-y-75 {
+    --transform-scale-y: .75;
+  }
+
+  .xl\:scale-y-90 {
+    --transform-scale-y: .9;
+  }
+
+  .xl\:scale-y-95 {
+    --transform-scale-y: .95;
+  }
+
+  .xl\:scale-y-100 {
+    --transform-scale-y: 1;
+  }
+
+  .xl\:scale-y-105 {
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:scale-y-110 {
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:scale-y-125 {
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:scale-y-150 {
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:hover\:scale-0:hover {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .xl\:hover\:scale-50:hover {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .xl\:hover\:scale-75:hover {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .xl\:hover\:scale-90:hover {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .xl\:hover\:scale-95:hover {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .xl\:hover\:scale-100:hover {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .xl\:hover\:scale-105:hover {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:hover\:scale-110:hover {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:hover\:scale-125:hover {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:hover\:scale-150:hover {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:hover\:scale-x-0:hover {
+    --transform-scale-x: 0;
+  }
+
+  .xl\:hover\:scale-x-50:hover {
+    --transform-scale-x: .5;
+  }
+
+  .xl\:hover\:scale-x-75:hover {
+    --transform-scale-x: .75;
+  }
+
+  .xl\:hover\:scale-x-90:hover {
+    --transform-scale-x: .9;
+  }
+
+  .xl\:hover\:scale-x-95:hover {
+    --transform-scale-x: .95;
+  }
+
+  .xl\:hover\:scale-x-100:hover {
+    --transform-scale-x: 1;
+  }
+
+  .xl\:hover\:scale-x-105:hover {
+    --transform-scale-x: 1.05;
+  }
+
+  .xl\:hover\:scale-x-110:hover {
+    --transform-scale-x: 1.1;
+  }
+
+  .xl\:hover\:scale-x-125:hover {
+    --transform-scale-x: 1.25;
+  }
+
+  .xl\:hover\:scale-x-150:hover {
+    --transform-scale-x: 1.5;
+  }
+
+  .xl\:hover\:scale-y-0:hover {
+    --transform-scale-y: 0;
+  }
+
+  .xl\:hover\:scale-y-50:hover {
+    --transform-scale-y: .5;
+  }
+
+  .xl\:hover\:scale-y-75:hover {
+    --transform-scale-y: .75;
+  }
+
+  .xl\:hover\:scale-y-90:hover {
+    --transform-scale-y: .9;
+  }
+
+  .xl\:hover\:scale-y-95:hover {
+    --transform-scale-y: .95;
+  }
+
+  .xl\:hover\:scale-y-100:hover {
+    --transform-scale-y: 1;
+  }
+
+  .xl\:hover\:scale-y-105:hover {
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:hover\:scale-y-110:hover {
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:hover\:scale-y-125:hover {
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:hover\:scale-y-150:hover {
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:focus\:scale-0:focus {
+    --transform-scale-x: 0;
+    --transform-scale-y: 0;
+  }
+
+  .xl\:focus\:scale-50:focus {
+    --transform-scale-x: .5;
+    --transform-scale-y: .5;
+  }
+
+  .xl\:focus\:scale-75:focus {
+    --transform-scale-x: .75;
+    --transform-scale-y: .75;
+  }
+
+  .xl\:focus\:scale-90:focus {
+    --transform-scale-x: .9;
+    --transform-scale-y: .9;
+  }
+
+  .xl\:focus\:scale-95:focus {
+    --transform-scale-x: .95;
+    --transform-scale-y: .95;
+  }
+
+  .xl\:focus\:scale-100:focus {
+    --transform-scale-x: 1;
+    --transform-scale-y: 1;
+  }
+
+  .xl\:focus\:scale-105:focus {
+    --transform-scale-x: 1.05;
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:focus\:scale-110:focus {
+    --transform-scale-x: 1.1;
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:focus\:scale-125:focus {
+    --transform-scale-x: 1.25;
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:focus\:scale-150:focus {
+    --transform-scale-x: 1.5;
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:focus\:scale-x-0:focus {
+    --transform-scale-x: 0;
+  }
+
+  .xl\:focus\:scale-x-50:focus {
+    --transform-scale-x: .5;
+  }
+
+  .xl\:focus\:scale-x-75:focus {
+    --transform-scale-x: .75;
+  }
+
+  .xl\:focus\:scale-x-90:focus {
+    --transform-scale-x: .9;
+  }
+
+  .xl\:focus\:scale-x-95:focus {
+    --transform-scale-x: .95;
+  }
+
+  .xl\:focus\:scale-x-100:focus {
+    --transform-scale-x: 1;
+  }
+
+  .xl\:focus\:scale-x-105:focus {
+    --transform-scale-x: 1.05;
+  }
+
+  .xl\:focus\:scale-x-110:focus {
+    --transform-scale-x: 1.1;
+  }
+
+  .xl\:focus\:scale-x-125:focus {
+    --transform-scale-x: 1.25;
+  }
+
+  .xl\:focus\:scale-x-150:focus {
+    --transform-scale-x: 1.5;
+  }
+
+  .xl\:focus\:scale-y-0:focus {
+    --transform-scale-y: 0;
+  }
+
+  .xl\:focus\:scale-y-50:focus {
+    --transform-scale-y: .5;
+  }
+
+  .xl\:focus\:scale-y-75:focus {
+    --transform-scale-y: .75;
+  }
+
+  .xl\:focus\:scale-y-90:focus {
+    --transform-scale-y: .9;
+  }
+
+  .xl\:focus\:scale-y-95:focus {
+    --transform-scale-y: .95;
+  }
+
+  .xl\:focus\:scale-y-100:focus {
+    --transform-scale-y: 1;
+  }
+
+  .xl\:focus\:scale-y-105:focus {
+    --transform-scale-y: 1.05;
+  }
+
+  .xl\:focus\:scale-y-110:focus {
+    --transform-scale-y: 1.1;
+  }
+
+  .xl\:focus\:scale-y-125:focus {
+    --transform-scale-y: 1.25;
+  }
+
+  .xl\:focus\:scale-y-150:focus {
+    --transform-scale-y: 1.5;
+  }
+
+  .xl\:rotate-0 {
+    --transform-rotate: 0;
+  }
+
+  .xl\:rotate-45 {
+    --transform-rotate: 45deg;
+  }
+
+  .xl\:rotate-90 {
+    --transform-rotate: 90deg;
+  }
+
+  .xl\:rotate-180 {
+    --transform-rotate: 180deg;
+  }
+
+  .xl\:-rotate-180 {
+    --transform-rotate: -180deg;
+  }
+
+  .xl\:-rotate-90 {
+    --transform-rotate: -90deg;
+  }
+
+  .xl\:-rotate-45 {
+    --transform-rotate: -45deg;
+  }
+
+  .xl\:hover\:rotate-0:hover {
+    --transform-rotate: 0;
+  }
+
+  .xl\:hover\:rotate-45:hover {
+    --transform-rotate: 45deg;
+  }
+
+  .xl\:hover\:rotate-90:hover {
+    --transform-rotate: 90deg;
+  }
+
+  .xl\:hover\:rotate-180:hover {
+    --transform-rotate: 180deg;
+  }
+
+  .xl\:hover\:-rotate-180:hover {
+    --transform-rotate: -180deg;
+  }
+
+  .xl\:hover\:-rotate-90:hover {
+    --transform-rotate: -90deg;
+  }
+
+  .xl\:hover\:-rotate-45:hover {
+    --transform-rotate: -45deg;
+  }
+
+  .xl\:focus\:rotate-0:focus {
+    --transform-rotate: 0;
+  }
+
+  .xl\:focus\:rotate-45:focus {
+    --transform-rotate: 45deg;
+  }
+
+  .xl\:focus\:rotate-90:focus {
+    --transform-rotate: 90deg;
+  }
+
+  .xl\:focus\:rotate-180:focus {
+    --transform-rotate: 180deg;
+  }
+
+  .xl\:focus\:-rotate-180:focus {
+    --transform-rotate: -180deg;
+  }
+
+  .xl\:focus\:-rotate-90:focus {
+    --transform-rotate: -90deg;
+  }
+
+  .xl\:focus\:-rotate-45:focus {
+    --transform-rotate: -45deg;
+  }
+
+  .xl\:translate-x-0 {
+    --transform-translate-x: 0;
+  }
+
+  .xl\:translate-x-1 {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .xl\:translate-x-2 {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .xl\:translate-x-3 {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .xl\:translate-x-4 {
+    --transform-translate-x: 1rem;
+  }
+
+  .xl\:translate-x-5 {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .xl\:translate-x-6 {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .xl\:translate-x-8 {
+    --transform-translate-x: 2rem;
+  }
+
+  .xl\:translate-x-10 {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .xl\:translate-x-12 {
+    --transform-translate-x: 3rem;
+  }
+
+  .xl\:translate-x-16 {
+    --transform-translate-x: 4rem;
+  }
+
+  .xl\:translate-x-20 {
+    --transform-translate-x: 5rem;
+  }
+
+  .xl\:translate-x-24 {
+    --transform-translate-x: 6rem;
+  }
+
+  .xl\:translate-x-32 {
+    --transform-translate-x: 8rem;
+  }
+
+  .xl\:translate-x-40 {
+    --transform-translate-x: 10rem;
+  }
+
+  .xl\:translate-x-48 {
+    --transform-translate-x: 12rem;
+  }
+
+  .xl\:translate-x-56 {
+    --transform-translate-x: 14rem;
+  }
+
+  .xl\:translate-x-64 {
+    --transform-translate-x: 16rem;
+  }
+
+  .xl\:translate-x-px {
+    --transform-translate-x: 1px;
+  }
+
+  .xl\:-translate-x-1 {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .xl\:-translate-x-2 {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .xl\:-translate-x-3 {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .xl\:-translate-x-4 {
+    --transform-translate-x: -1rem;
+  }
+
+  .xl\:-translate-x-5 {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .xl\:-translate-x-6 {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .xl\:-translate-x-8 {
+    --transform-translate-x: -2rem;
+  }
+
+  .xl\:-translate-x-10 {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .xl\:-translate-x-12 {
+    --transform-translate-x: -3rem;
+  }
+
+  .xl\:-translate-x-16 {
+    --transform-translate-x: -4rem;
+  }
+
+  .xl\:-translate-x-20 {
+    --transform-translate-x: -5rem;
+  }
+
+  .xl\:-translate-x-24 {
+    --transform-translate-x: -6rem;
+  }
+
+  .xl\:-translate-x-32 {
+    --transform-translate-x: -8rem;
+  }
+
+  .xl\:-translate-x-40 {
+    --transform-translate-x: -10rem;
+  }
+
+  .xl\:-translate-x-48 {
+    --transform-translate-x: -12rem;
+  }
+
+  .xl\:-translate-x-56 {
+    --transform-translate-x: -14rem;
+  }
+
+  .xl\:-translate-x-64 {
+    --transform-translate-x: -16rem;
+  }
+
+  .xl\:-translate-x-px {
+    --transform-translate-x: -1px;
+  }
+
+  .xl\:-translate-x-full {
+    --transform-translate-x: -100%;
+  }
+
+  .xl\:-translate-x-1\/2 {
+    --transform-translate-x: -50%;
+  }
+
+  .xl\:translate-x-1\/2 {
+    --transform-translate-x: 50%;
+  }
+
+  .xl\:translate-x-full {
+    --transform-translate-x: 100%;
+  }
+
+  .xl\:translate-y-0 {
+    --transform-translate-y: 0;
+  }
+
+  .xl\:translate-y-1 {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .xl\:translate-y-2 {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .xl\:translate-y-3 {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .xl\:translate-y-4 {
+    --transform-translate-y: 1rem;
+  }
+
+  .xl\:translate-y-5 {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .xl\:translate-y-6 {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .xl\:translate-y-8 {
+    --transform-translate-y: 2rem;
+  }
+
+  .xl\:translate-y-10 {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .xl\:translate-y-12 {
+    --transform-translate-y: 3rem;
+  }
+
+  .xl\:translate-y-16 {
+    --transform-translate-y: 4rem;
+  }
+
+  .xl\:translate-y-20 {
+    --transform-translate-y: 5rem;
+  }
+
+  .xl\:translate-y-24 {
+    --transform-translate-y: 6rem;
+  }
+
+  .xl\:translate-y-32 {
+    --transform-translate-y: 8rem;
+  }
+
+  .xl\:translate-y-40 {
+    --transform-translate-y: 10rem;
+  }
+
+  .xl\:translate-y-48 {
+    --transform-translate-y: 12rem;
+  }
+
+  .xl\:translate-y-56 {
+    --transform-translate-y: 14rem;
+  }
+
+  .xl\:translate-y-64 {
+    --transform-translate-y: 16rem;
+  }
+
+  .xl\:translate-y-px {
+    --transform-translate-y: 1px;
+  }
+
+  .xl\:-translate-y-1 {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .xl\:-translate-y-2 {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .xl\:-translate-y-3 {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .xl\:-translate-y-4 {
+    --transform-translate-y: -1rem;
+  }
+
+  .xl\:-translate-y-5 {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .xl\:-translate-y-6 {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .xl\:-translate-y-8 {
+    --transform-translate-y: -2rem;
+  }
+
+  .xl\:-translate-y-10 {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .xl\:-translate-y-12 {
+    --transform-translate-y: -3rem;
+  }
+
+  .xl\:-translate-y-16 {
+    --transform-translate-y: -4rem;
+  }
+
+  .xl\:-translate-y-20 {
+    --transform-translate-y: -5rem;
+  }
+
+  .xl\:-translate-y-24 {
+    --transform-translate-y: -6rem;
+  }
+
+  .xl\:-translate-y-32 {
+    --transform-translate-y: -8rem;
+  }
+
+  .xl\:-translate-y-40 {
+    --transform-translate-y: -10rem;
+  }
+
+  .xl\:-translate-y-48 {
+    --transform-translate-y: -12rem;
+  }
+
+  .xl\:-translate-y-56 {
+    --transform-translate-y: -14rem;
+  }
+
+  .xl\:-translate-y-64 {
+    --transform-translate-y: -16rem;
+  }
+
+  .xl\:-translate-y-px {
+    --transform-translate-y: -1px;
+  }
+
+  .xl\:-translate-y-full {
+    --transform-translate-y: -100%;
+  }
+
+  .xl\:-translate-y-1\/2 {
+    --transform-translate-y: -50%;
+  }
+
+  .xl\:translate-y-1\/2 {
+    --transform-translate-y: 50%;
+  }
+
+  .xl\:translate-y-full {
+    --transform-translate-y: 100%;
+  }
+
+  .xl\:hover\:translate-x-0:hover {
+    --transform-translate-x: 0;
+  }
+
+  .xl\:hover\:translate-x-1:hover {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .xl\:hover\:translate-x-2:hover {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .xl\:hover\:translate-x-3:hover {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .xl\:hover\:translate-x-4:hover {
+    --transform-translate-x: 1rem;
+  }
+
+  .xl\:hover\:translate-x-5:hover {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .xl\:hover\:translate-x-6:hover {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .xl\:hover\:translate-x-8:hover {
+    --transform-translate-x: 2rem;
+  }
+
+  .xl\:hover\:translate-x-10:hover {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .xl\:hover\:translate-x-12:hover {
+    --transform-translate-x: 3rem;
+  }
+
+  .xl\:hover\:translate-x-16:hover {
+    --transform-translate-x: 4rem;
+  }
+
+  .xl\:hover\:translate-x-20:hover {
+    --transform-translate-x: 5rem;
+  }
+
+  .xl\:hover\:translate-x-24:hover {
+    --transform-translate-x: 6rem;
+  }
+
+  .xl\:hover\:translate-x-32:hover {
+    --transform-translate-x: 8rem;
+  }
+
+  .xl\:hover\:translate-x-40:hover {
+    --transform-translate-x: 10rem;
+  }
+
+  .xl\:hover\:translate-x-48:hover {
+    --transform-translate-x: 12rem;
+  }
+
+  .xl\:hover\:translate-x-56:hover {
+    --transform-translate-x: 14rem;
+  }
+
+  .xl\:hover\:translate-x-64:hover {
+    --transform-translate-x: 16rem;
+  }
+
+  .xl\:hover\:translate-x-px:hover {
+    --transform-translate-x: 1px;
+  }
+
+  .xl\:hover\:-translate-x-1:hover {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .xl\:hover\:-translate-x-2:hover {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .xl\:hover\:-translate-x-3:hover {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .xl\:hover\:-translate-x-4:hover {
+    --transform-translate-x: -1rem;
+  }
+
+  .xl\:hover\:-translate-x-5:hover {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .xl\:hover\:-translate-x-6:hover {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .xl\:hover\:-translate-x-8:hover {
+    --transform-translate-x: -2rem;
+  }
+
+  .xl\:hover\:-translate-x-10:hover {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .xl\:hover\:-translate-x-12:hover {
+    --transform-translate-x: -3rem;
+  }
+
+  .xl\:hover\:-translate-x-16:hover {
+    --transform-translate-x: -4rem;
+  }
+
+  .xl\:hover\:-translate-x-20:hover {
+    --transform-translate-x: -5rem;
+  }
+
+  .xl\:hover\:-translate-x-24:hover {
+    --transform-translate-x: -6rem;
+  }
+
+  .xl\:hover\:-translate-x-32:hover {
+    --transform-translate-x: -8rem;
+  }
+
+  .xl\:hover\:-translate-x-40:hover {
+    --transform-translate-x: -10rem;
+  }
+
+  .xl\:hover\:-translate-x-48:hover {
+    --transform-translate-x: -12rem;
+  }
+
+  .xl\:hover\:-translate-x-56:hover {
+    --transform-translate-x: -14rem;
+  }
+
+  .xl\:hover\:-translate-x-64:hover {
+    --transform-translate-x: -16rem;
+  }
+
+  .xl\:hover\:-translate-x-px:hover {
+    --transform-translate-x: -1px;
+  }
+
+  .xl\:hover\:-translate-x-full:hover {
+    --transform-translate-x: -100%;
+  }
+
+  .xl\:hover\:-translate-x-1\/2:hover {
+    --transform-translate-x: -50%;
+  }
+
+  .xl\:hover\:translate-x-1\/2:hover {
+    --transform-translate-x: 50%;
+  }
+
+  .xl\:hover\:translate-x-full:hover {
+    --transform-translate-x: 100%;
+  }
+
+  .xl\:hover\:translate-y-0:hover {
+    --transform-translate-y: 0;
+  }
+
+  .xl\:hover\:translate-y-1:hover {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .xl\:hover\:translate-y-2:hover {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .xl\:hover\:translate-y-3:hover {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .xl\:hover\:translate-y-4:hover {
+    --transform-translate-y: 1rem;
+  }
+
+  .xl\:hover\:translate-y-5:hover {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .xl\:hover\:translate-y-6:hover {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .xl\:hover\:translate-y-8:hover {
+    --transform-translate-y: 2rem;
+  }
+
+  .xl\:hover\:translate-y-10:hover {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .xl\:hover\:translate-y-12:hover {
+    --transform-translate-y: 3rem;
+  }
+
+  .xl\:hover\:translate-y-16:hover {
+    --transform-translate-y: 4rem;
+  }
+
+  .xl\:hover\:translate-y-20:hover {
+    --transform-translate-y: 5rem;
+  }
+
+  .xl\:hover\:translate-y-24:hover {
+    --transform-translate-y: 6rem;
+  }
+
+  .xl\:hover\:translate-y-32:hover {
+    --transform-translate-y: 8rem;
+  }
+
+  .xl\:hover\:translate-y-40:hover {
+    --transform-translate-y: 10rem;
+  }
+
+  .xl\:hover\:translate-y-48:hover {
+    --transform-translate-y: 12rem;
+  }
+
+  .xl\:hover\:translate-y-56:hover {
+    --transform-translate-y: 14rem;
+  }
+
+  .xl\:hover\:translate-y-64:hover {
+    --transform-translate-y: 16rem;
+  }
+
+  .xl\:hover\:translate-y-px:hover {
+    --transform-translate-y: 1px;
+  }
+
+  .xl\:hover\:-translate-y-1:hover {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .xl\:hover\:-translate-y-2:hover {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .xl\:hover\:-translate-y-3:hover {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .xl\:hover\:-translate-y-4:hover {
+    --transform-translate-y: -1rem;
+  }
+
+  .xl\:hover\:-translate-y-5:hover {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .xl\:hover\:-translate-y-6:hover {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .xl\:hover\:-translate-y-8:hover {
+    --transform-translate-y: -2rem;
+  }
+
+  .xl\:hover\:-translate-y-10:hover {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .xl\:hover\:-translate-y-12:hover {
+    --transform-translate-y: -3rem;
+  }
+
+  .xl\:hover\:-translate-y-16:hover {
+    --transform-translate-y: -4rem;
+  }
+
+  .xl\:hover\:-translate-y-20:hover {
+    --transform-translate-y: -5rem;
+  }
+
+  .xl\:hover\:-translate-y-24:hover {
+    --transform-translate-y: -6rem;
+  }
+
+  .xl\:hover\:-translate-y-32:hover {
+    --transform-translate-y: -8rem;
+  }
+
+  .xl\:hover\:-translate-y-40:hover {
+    --transform-translate-y: -10rem;
+  }
+
+  .xl\:hover\:-translate-y-48:hover {
+    --transform-translate-y: -12rem;
+  }
+
+  .xl\:hover\:-translate-y-56:hover {
+    --transform-translate-y: -14rem;
+  }
+
+  .xl\:hover\:-translate-y-64:hover {
+    --transform-translate-y: -16rem;
+  }
+
+  .xl\:hover\:-translate-y-px:hover {
+    --transform-translate-y: -1px;
+  }
+
+  .xl\:hover\:-translate-y-full:hover {
+    --transform-translate-y: -100%;
+  }
+
+  .xl\:hover\:-translate-y-1\/2:hover {
+    --transform-translate-y: -50%;
+  }
+
+  .xl\:hover\:translate-y-1\/2:hover {
+    --transform-translate-y: 50%;
+  }
+
+  .xl\:hover\:translate-y-full:hover {
+    --transform-translate-y: 100%;
+  }
+
+  .xl\:focus\:translate-x-0:focus {
+    --transform-translate-x: 0;
+  }
+
+  .xl\:focus\:translate-x-1:focus {
+    --transform-translate-x: 0.25rem;
+  }
+
+  .xl\:focus\:translate-x-2:focus {
+    --transform-translate-x: 0.5rem;
+  }
+
+  .xl\:focus\:translate-x-3:focus {
+    --transform-translate-x: 0.75rem;
+  }
+
+  .xl\:focus\:translate-x-4:focus {
+    --transform-translate-x: 1rem;
+  }
+
+  .xl\:focus\:translate-x-5:focus {
+    --transform-translate-x: 1.25rem;
+  }
+
+  .xl\:focus\:translate-x-6:focus {
+    --transform-translate-x: 1.5rem;
+  }
+
+  .xl\:focus\:translate-x-8:focus {
+    --transform-translate-x: 2rem;
+  }
+
+  .xl\:focus\:translate-x-10:focus {
+    --transform-translate-x: 2.5rem;
+  }
+
+  .xl\:focus\:translate-x-12:focus {
+    --transform-translate-x: 3rem;
+  }
+
+  .xl\:focus\:translate-x-16:focus {
+    --transform-translate-x: 4rem;
+  }
+
+  .xl\:focus\:translate-x-20:focus {
+    --transform-translate-x: 5rem;
+  }
+
+  .xl\:focus\:translate-x-24:focus {
+    --transform-translate-x: 6rem;
+  }
+
+  .xl\:focus\:translate-x-32:focus {
+    --transform-translate-x: 8rem;
+  }
+
+  .xl\:focus\:translate-x-40:focus {
+    --transform-translate-x: 10rem;
+  }
+
+  .xl\:focus\:translate-x-48:focus {
+    --transform-translate-x: 12rem;
+  }
+
+  .xl\:focus\:translate-x-56:focus {
+    --transform-translate-x: 14rem;
+  }
+
+  .xl\:focus\:translate-x-64:focus {
+    --transform-translate-x: 16rem;
+  }
+
+  .xl\:focus\:translate-x-px:focus {
+    --transform-translate-x: 1px;
+  }
+
+  .xl\:focus\:-translate-x-1:focus {
+    --transform-translate-x: -0.25rem;
+  }
+
+  .xl\:focus\:-translate-x-2:focus {
+    --transform-translate-x: -0.5rem;
+  }
+
+  .xl\:focus\:-translate-x-3:focus {
+    --transform-translate-x: -0.75rem;
+  }
+
+  .xl\:focus\:-translate-x-4:focus {
+    --transform-translate-x: -1rem;
+  }
+
+  .xl\:focus\:-translate-x-5:focus {
+    --transform-translate-x: -1.25rem;
+  }
+
+  .xl\:focus\:-translate-x-6:focus {
+    --transform-translate-x: -1.5rem;
+  }
+
+  .xl\:focus\:-translate-x-8:focus {
+    --transform-translate-x: -2rem;
+  }
+
+  .xl\:focus\:-translate-x-10:focus {
+    --transform-translate-x: -2.5rem;
+  }
+
+  .xl\:focus\:-translate-x-12:focus {
+    --transform-translate-x: -3rem;
+  }
+
+  .xl\:focus\:-translate-x-16:focus {
+    --transform-translate-x: -4rem;
+  }
+
+  .xl\:focus\:-translate-x-20:focus {
+    --transform-translate-x: -5rem;
+  }
+
+  .xl\:focus\:-translate-x-24:focus {
+    --transform-translate-x: -6rem;
+  }
+
+  .xl\:focus\:-translate-x-32:focus {
+    --transform-translate-x: -8rem;
+  }
+
+  .xl\:focus\:-translate-x-40:focus {
+    --transform-translate-x: -10rem;
+  }
+
+  .xl\:focus\:-translate-x-48:focus {
+    --transform-translate-x: -12rem;
+  }
+
+  .xl\:focus\:-translate-x-56:focus {
+    --transform-translate-x: -14rem;
+  }
+
+  .xl\:focus\:-translate-x-64:focus {
+    --transform-translate-x: -16rem;
+  }
+
+  .xl\:focus\:-translate-x-px:focus {
+    --transform-translate-x: -1px;
+  }
+
+  .xl\:focus\:-translate-x-full:focus {
+    --transform-translate-x: -100%;
+  }
+
+  .xl\:focus\:-translate-x-1\/2:focus {
+    --transform-translate-x: -50%;
+  }
+
+  .xl\:focus\:translate-x-1\/2:focus {
+    --transform-translate-x: 50%;
+  }
+
+  .xl\:focus\:translate-x-full:focus {
+    --transform-translate-x: 100%;
+  }
+
+  .xl\:focus\:translate-y-0:focus {
+    --transform-translate-y: 0;
+  }
+
+  .xl\:focus\:translate-y-1:focus {
+    --transform-translate-y: 0.25rem;
+  }
+
+  .xl\:focus\:translate-y-2:focus {
+    --transform-translate-y: 0.5rem;
+  }
+
+  .xl\:focus\:translate-y-3:focus {
+    --transform-translate-y: 0.75rem;
+  }
+
+  .xl\:focus\:translate-y-4:focus {
+    --transform-translate-y: 1rem;
+  }
+
+  .xl\:focus\:translate-y-5:focus {
+    --transform-translate-y: 1.25rem;
+  }
+
+  .xl\:focus\:translate-y-6:focus {
+    --transform-translate-y: 1.5rem;
+  }
+
+  .xl\:focus\:translate-y-8:focus {
+    --transform-translate-y: 2rem;
+  }
+
+  .xl\:focus\:translate-y-10:focus {
+    --transform-translate-y: 2.5rem;
+  }
+
+  .xl\:focus\:translate-y-12:focus {
+    --transform-translate-y: 3rem;
+  }
+
+  .xl\:focus\:translate-y-16:focus {
+    --transform-translate-y: 4rem;
+  }
+
+  .xl\:focus\:translate-y-20:focus {
+    --transform-translate-y: 5rem;
+  }
+
+  .xl\:focus\:translate-y-24:focus {
+    --transform-translate-y: 6rem;
+  }
+
+  .xl\:focus\:translate-y-32:focus {
+    --transform-translate-y: 8rem;
+  }
+
+  .xl\:focus\:translate-y-40:focus {
+    --transform-translate-y: 10rem;
+  }
+
+  .xl\:focus\:translate-y-48:focus {
+    --transform-translate-y: 12rem;
+  }
+
+  .xl\:focus\:translate-y-56:focus {
+    --transform-translate-y: 14rem;
+  }
+
+  .xl\:focus\:translate-y-64:focus {
+    --transform-translate-y: 16rem;
+  }
+
+  .xl\:focus\:translate-y-px:focus {
+    --transform-translate-y: 1px;
+  }
+
+  .xl\:focus\:-translate-y-1:focus {
+    --transform-translate-y: -0.25rem;
+  }
+
+  .xl\:focus\:-translate-y-2:focus {
+    --transform-translate-y: -0.5rem;
+  }
+
+  .xl\:focus\:-translate-y-3:focus {
+    --transform-translate-y: -0.75rem;
+  }
+
+  .xl\:focus\:-translate-y-4:focus {
+    --transform-translate-y: -1rem;
+  }
+
+  .xl\:focus\:-translate-y-5:focus {
+    --transform-translate-y: -1.25rem;
+  }
+
+  .xl\:focus\:-translate-y-6:focus {
+    --transform-translate-y: -1.5rem;
+  }
+
+  .xl\:focus\:-translate-y-8:focus {
+    --transform-translate-y: -2rem;
+  }
+
+  .xl\:focus\:-translate-y-10:focus {
+    --transform-translate-y: -2.5rem;
+  }
+
+  .xl\:focus\:-translate-y-12:focus {
+    --transform-translate-y: -3rem;
+  }
+
+  .xl\:focus\:-translate-y-16:focus {
+    --transform-translate-y: -4rem;
+  }
+
+  .xl\:focus\:-translate-y-20:focus {
+    --transform-translate-y: -5rem;
+  }
+
+  .xl\:focus\:-translate-y-24:focus {
+    --transform-translate-y: -6rem;
+  }
+
+  .xl\:focus\:-translate-y-32:focus {
+    --transform-translate-y: -8rem;
+  }
+
+  .xl\:focus\:-translate-y-40:focus {
+    --transform-translate-y: -10rem;
+  }
+
+  .xl\:focus\:-translate-y-48:focus {
+    --transform-translate-y: -12rem;
+  }
+
+  .xl\:focus\:-translate-y-56:focus {
+    --transform-translate-y: -14rem;
+  }
+
+  .xl\:focus\:-translate-y-64:focus {
+    --transform-translate-y: -16rem;
+  }
+
+  .xl\:focus\:-translate-y-px:focus {
+    --transform-translate-y: -1px;
+  }
+
+  .xl\:focus\:-translate-y-full:focus {
+    --transform-translate-y: -100%;
+  }
+
+  .xl\:focus\:-translate-y-1\/2:focus {
+    --transform-translate-y: -50%;
+  }
+
+  .xl\:focus\:translate-y-1\/2:focus {
+    --transform-translate-y: 50%;
+  }
+
+  .xl\:focus\:translate-y-full:focus {
+    --transform-translate-y: 100%;
+  }
+
+  .xl\:skew-x-0 {
+    --transform-skew-x: 0;
+  }
+
+  .xl\:skew-x-3 {
+    --transform-skew-x: 3deg;
+  }
+
+  .xl\:skew-x-6 {
+    --transform-skew-x: 6deg;
+  }
+
+  .xl\:skew-x-12 {
+    --transform-skew-x: 12deg;
+  }
+
+  .xl\:-skew-x-12 {
+    --transform-skew-x: -12deg;
+  }
+
+  .xl\:-skew-x-6 {
+    --transform-skew-x: -6deg;
+  }
+
+  .xl\:-skew-x-3 {
+    --transform-skew-x: -3deg;
+  }
+
+  .xl\:skew-y-0 {
+    --transform-skew-y: 0;
+  }
+
+  .xl\:skew-y-3 {
+    --transform-skew-y: 3deg;
+  }
+
+  .xl\:skew-y-6 {
+    --transform-skew-y: 6deg;
+  }
+
+  .xl\:skew-y-12 {
+    --transform-skew-y: 12deg;
+  }
+
+  .xl\:-skew-y-12 {
+    --transform-skew-y: -12deg;
+  }
+
+  .xl\:-skew-y-6 {
+    --transform-skew-y: -6deg;
+  }
+
+  .xl\:-skew-y-3 {
+    --transform-skew-y: -3deg;
+  }
+
+  .xl\:hover\:skew-x-0:hover {
+    --transform-skew-x: 0;
+  }
+
+  .xl\:hover\:skew-x-3:hover {
+    --transform-skew-x: 3deg;
+  }
+
+  .xl\:hover\:skew-x-6:hover {
+    --transform-skew-x: 6deg;
+  }
+
+  .xl\:hover\:skew-x-12:hover {
+    --transform-skew-x: 12deg;
+  }
+
+  .xl\:hover\:-skew-x-12:hover {
+    --transform-skew-x: -12deg;
+  }
+
+  .xl\:hover\:-skew-x-6:hover {
+    --transform-skew-x: -6deg;
+  }
+
+  .xl\:hover\:-skew-x-3:hover {
+    --transform-skew-x: -3deg;
+  }
+
+  .xl\:hover\:skew-y-0:hover {
+    --transform-skew-y: 0;
+  }
+
+  .xl\:hover\:skew-y-3:hover {
+    --transform-skew-y: 3deg;
+  }
+
+  .xl\:hover\:skew-y-6:hover {
+    --transform-skew-y: 6deg;
+  }
+
+  .xl\:hover\:skew-y-12:hover {
+    --transform-skew-y: 12deg;
+  }
+
+  .xl\:hover\:-skew-y-12:hover {
+    --transform-skew-y: -12deg;
+  }
+
+  .xl\:hover\:-skew-y-6:hover {
+    --transform-skew-y: -6deg;
+  }
+
+  .xl\:hover\:-skew-y-3:hover {
+    --transform-skew-y: -3deg;
+  }
+
+  .xl\:focus\:skew-x-0:focus {
+    --transform-skew-x: 0;
+  }
+
+  .xl\:focus\:skew-x-3:focus {
+    --transform-skew-x: 3deg;
+  }
+
+  .xl\:focus\:skew-x-6:focus {
+    --transform-skew-x: 6deg;
+  }
+
+  .xl\:focus\:skew-x-12:focus {
+    --transform-skew-x: 12deg;
+  }
+
+  .xl\:focus\:-skew-x-12:focus {
+    --transform-skew-x: -12deg;
+  }
+
+  .xl\:focus\:-skew-x-6:focus {
+    --transform-skew-x: -6deg;
+  }
+
+  .xl\:focus\:-skew-x-3:focus {
+    --transform-skew-x: -3deg;
+  }
+
+  .xl\:focus\:skew-y-0:focus {
+    --transform-skew-y: 0;
+  }
+
+  .xl\:focus\:skew-y-3:focus {
+    --transform-skew-y: 3deg;
+  }
+
+  .xl\:focus\:skew-y-6:focus {
+    --transform-skew-y: 6deg;
+  }
+
+  .xl\:focus\:skew-y-12:focus {
+    --transform-skew-y: 12deg;
+  }
+
+  .xl\:focus\:-skew-y-12:focus {
+    --transform-skew-y: -12deg;
+  }
+
+  .xl\:focus\:-skew-y-6:focus {
+    --transform-skew-y: -6deg;
+  }
+
+  .xl\:focus\:-skew-y-3:focus {
+    --transform-skew-y: -3deg;
+  }
+
+  .xl\:transition-none {
+    transition-property: none;
+  }
+
+  .xl\:transition-all {
+    transition-property: all;
+  }
+
+  .xl\:transition {
+    transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
+  }
+
+  .xl\:transition-colors {
+    transition-property: background-color, border-color, color, fill, stroke;
+  }
+
+  .xl\:transition-opacity {
+    transition-property: opacity;
+  }
+
+  .xl\:transition-shadow {
+    transition-property: box-shadow;
+  }
+
+  .xl\:transition-transform {
+    transition-property: transform;
+  }
+
+  .xl\:ease-linear {
+    transition-timing-function: linear;
+  }
+
+  .xl\:ease-in {
+    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+  }
+
+  .xl\:ease-out {
+    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+
+  .xl\:ease-in-out {
+    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  .xl\:duration-75 {
+    transition-duration: 75ms;
+  }
+
+  .xl\:duration-100 {
+    transition-duration: 100ms;
+  }
+
+  .xl\:duration-150 {
+    transition-duration: 150ms;
+  }
+
+  .xl\:duration-200 {
+    transition-duration: 200ms;
+  }
+
+  .xl\:duration-300 {
+    transition-duration: 300ms;
+  }
+
+  .xl\:duration-500 {
+    transition-duration: 500ms;
+  }
+
+  .xl\:duration-700 {
+    transition-duration: 700ms;
+  }
+
+  .xl\:duration-1000 {
+    transition-duration: 1000ms;
+  }
+
+  .xl\:delay-75 {
+    transition-delay: 75ms;
+  }
+
+  .xl\:delay-100 {
+    transition-delay: 100ms;
+  }
+
+  .xl\:delay-150 {
+    transition-delay: 150ms;
+  }
+
+  .xl\:delay-200 {
+    transition-delay: 200ms;
+  }
+
+  .xl\:delay-300 {
+    transition-delay: 300ms;
+  }
+
+  .xl\:delay-500 {
+    transition-delay: 500ms;
+  }
+
+  .xl\:delay-700 {
+    transition-delay: 700ms;
+  }
+
+  .xl\:delay-1000 {
+    transition-delay: 1000ms;
+  }
+
+  .xl\:animate-none {
+    -webkit-animation: none;
+            animation: none;
+  }
+
+  .xl\:animate-spin {
+    -webkit-animation: spin 1s linear infinite;
+            animation: spin 1s linear infinite;
+  }
+
+  .xl\:animate-ping {
+    -webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+            animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+  }
+
+  .xl\:animate-pulse {
+    -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  }
+
+  .xl\:animate-bounce {
+    -webkit-animation: bounce 1s infinite;
+            animation: bounce 1s infinite;
+  }
+}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/registry.dat b/users/wpcarro/website/sandbox/learnpianochords/registry.dat
new file mode 100644
index 0000000000..a73307ccda
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/registry.dat
Binary files differdiff --git a/users/wpcarro/website/sandbox/learnpianochords/shell.nix b/users/wpcarro/website/sandbox/learnpianochords/shell.nix
new file mode 100644
index 0000000000..afcc0f4d36
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs.elmPackages; [
+    elm
+    elm-format
+    elm-live
+  ];
+}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/FlashCard.elm b/users/wpcarro/website/sandbox/learnpianochords/src/FlashCard.elm
new file mode 100644
index 0000000000..a491752939
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/FlashCard.elm
@@ -0,0 +1,42 @@
+module FlashCard exposing (render)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Responsive
+import State
+import Tailwind
+import Theory
+
+
+render :
+    { chord : Theory.Chord
+    , visible : Bool
+    }
+    -> Html State.Msg
+render { chord, visible } =
+    let
+        classes =
+            [ "bg-white"
+            , "fixed"
+            , "top-0"
+            , "left-0"
+            , "z-30"
+            , "w-screen"
+            , "h-screen"
+            , Tailwind.if_ visible "opacity-100" "opacity-0"
+            ]
+    in
+    button
+        [ classes |> Tailwind.use |> class ]
+        [ h1
+            [ [ "text-center"
+              , "transform"
+              , "-rotate-90"
+              , Responsive.h1
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ text (Theory.viewChord chord) ]
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Icon.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Icon.elm
new file mode 100644
index 0000000000..2c8626b092
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Icon.elm
@@ -0,0 +1,44 @@
+module Icon exposing (..)
+
+import Svg exposing (node, svg)
+import Svg.Attributes exposing (..)
+import UI
+
+
+svgColor color =
+    let
+        classes =
+            case color of
+                UI.Primary ->
+                    [ "text-gray-500", "fill-current" ]
+
+                UI.Secondary ->
+                    [ "text-gray-300", "fill-current" ]
+    in
+    class <| String.join " " classes
+
+
+cog =
+    svg [ class "icon-cog", viewBox "0 0 24 24", xmlLang "http://www.w3.org/2000/svg" ]
+        [ Svg.path
+            [ svgColor UI.Primary
+            , d "M6.8 3.45c.87-.52 1.82-.92 2.83-1.17a2.5 2.5 0 0 0 4.74 0c1.01.25 1.96.65 2.82 1.17a2.5 2.5 0 0 0 3.36 3.36c.52.86.92 1.8 1.17 2.82a2.5 2.5 0 0 0 0 4.74c-.25 1.01-.65 1.96-1.17 2.82a2.5 2.5 0 0 0-3.36 3.36c-.86.52-1.8.92-2.82 1.17a2.5 2.5 0 0 0-4.74 0c-1.01-.25-1.96-.65-2.82-1.17a2.5 2.5 0 0 0-3.36-3.36 9.94 9.94 0 0 1-1.17-2.82 2.5 2.5 0 0 0 0-4.74c.25-1.01.65-1.96 1.17-2.82a2.5 2.5 0 0 0 3.36-3.36zM12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"
+            , fill "red"
+            ]
+            []
+        , node "circle"
+            [ svgColor UI.Secondary, cx "12", cy "12", r "2" ]
+            []
+        ]
+
+
+close =
+    svg [ class "icon-close", viewBox "0 0 24 24", xmlLang "http://www.w3.org/2000/svg" ]
+        [ Svg.path
+            [ svgColor UI.Primary
+            , d "M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"
+            , fill "red"
+            , fillRule "evenodd"
+            ]
+            []
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Main.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Main.elm
new file mode 100644
index 0000000000..b066fb2f6f
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Main.elm
@@ -0,0 +1,44 @@
+module Main exposing (main)
+
+import Browser
+import Html exposing (..)
+import Misc
+import Overview
+import Practice
+import Preferences
+import State
+import Time exposing (..)
+
+
+subscriptions : State.Model -> Sub State.Msg
+subscriptions model =
+    if model.isPaused then
+        Sub.none
+
+    else
+        Sub.batch
+            [ Time.every (model.tempo * 2 |> Misc.bpmToMilliseconds |> toFloat) (\_ -> State.ToggleFlashCard)
+            , Time.every (model.tempo |> Misc.bpmToMilliseconds |> toFloat) (\_ -> State.NextChord)
+            ]
+
+
+view : State.Model -> Html State.Msg
+view model =
+    case model.view of
+        State.Preferences ->
+            Preferences.render model
+
+        State.Practice ->
+            Practice.render model
+
+        State.Overview ->
+            Overview.render model
+
+
+main =
+    Browser.element
+        { init = \() -> ( State.init, Cmd.none )
+        , subscriptions = subscriptions
+        , update = State.update
+        , view = view
+        }
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Misc.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Misc.elm
new file mode 100644
index 0000000000..288d7a825f
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Misc.elm
@@ -0,0 +1,59 @@
+module Misc exposing (..)
+
+import Array exposing (Array)
+
+
+comesAfter : a -> List a -> Maybe a
+comesAfter x xs =
+    case xs of
+        [] ->
+            Nothing
+
+        _ :: [] ->
+            Nothing
+
+        y :: z :: rest ->
+            if y == x then
+                Just z
+
+            else
+                comesAfter x (z :: rest)
+
+
+comesBefore : a -> List a -> Maybe a
+comesBefore x xs =
+    case xs of
+        [] ->
+            Nothing
+
+        _ :: [] ->
+            Nothing
+
+        y :: z :: rest ->
+            if z == x then
+                Just y
+
+            else
+                comesBefore x (z :: rest)
+
+
+find : (a -> Bool) -> List a -> Maybe a
+find pred xs =
+    case xs |> List.filter pred of
+        [] ->
+            Nothing
+
+        x :: _ ->
+            Just x
+
+
+{-| Return the number of milliseconds that elapse during an interval in a
+`target` bpm.
+-}
+bpmToMilliseconds : Int -> Int
+bpmToMilliseconds target =
+    let
+        msPerMinute =
+            1000 * 60
+    in
+    round (toFloat msPerMinute / toFloat target)
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Overview.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Overview.elm
new file mode 100644
index 0000000000..628b52d79d
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Overview.elm
@@ -0,0 +1,122 @@
+module Overview exposing (render)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Responsive
+import State
+import Tailwind
+import UI
+
+
+header1 : String -> Html msg
+header1 copy =
+    h2
+        [ [ "text-center"
+          , "pt-24"
+          , "pb-12"
+          , Responsive.h1
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ text copy ]
+
+
+header2 : String -> Html msg
+header2 copy =
+    h2
+        [ [ "text-center"
+          , "pb-10"
+          , Responsive.h2
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ text copy ]
+
+
+paragraph : String -> Html msg
+paragraph copy =
+    p
+        [ [ "pb-10"
+          , Responsive.h3
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        [ text copy ]
+
+
+sect : { title : String, copy : List String } -> Html msg
+sect { title, copy } =
+    section [] (header2 title :: (copy |> List.map paragraph))
+
+
+numberedList : List String -> Html msg
+numberedList items =
+    ol
+        [ [ "list-inside"
+          , "list-decimal"
+          , Responsive.h3
+          ]
+            |> Tailwind.use
+            |> class
+        ]
+        (items |> List.map (\x -> li [ [ "pb-10" ] |> Tailwind.use |> class ] [ text x ]))
+
+
+render : State.Model -> Html State.Msg
+render model =
+    div [ [ "container", "mx-auto" ] |> Tailwind.use |> class ]
+        [ header1 "Welcome to LearnPianoChords.app!"
+        , paragraph """
+                     Learn Piano Chords helps piano players master chords.
+                     """
+        , paragraph """
+                     Chords are the building blocks songwriters use to create
+                     music. Whether you're a performer or songwriter, you need
+                     to understand chords to unlock your full musical potential.
+                     """
+        , paragraph """
+                     I think that if practicing is enjoyable, students will
+                     practice more. Practice doesn’t make perfect; perfect
+                     practice makes perfect.
+                     """
+        , section []
+            [ header2 "Ready to get started?"
+            , numberedList
+                [ """
+                   Sit down at the piano.
+                   """
+                , """
+                   Set the tempo at which you would like to practice.
+                   """
+                , """
+                   Select the key or keys in which you would like to
+                   practice.
+                   """
+                , """
+                   When you are ready, close the preferences pane. We will show
+                   you the name of a chord, and you should play that chord on
+                   the piano.
+                 """
+                , """
+                   If you don't know how to play the chord, toggle the piano
+                   viewer to see the notes.
+                   """
+                , """
+                   At any point while you're training, press the screen to pause
+                   or resume your practice.
+                   """
+                ]
+            ]
+        , div [ [ "text-center", "py-20" ] |> Tailwind.use |> class ]
+            [ UI.simpleButton
+                { label = "Let's get started"
+                , handleClick = State.SetView State.Preferences
+                , color = UI.Secondary
+                , classes = []
+                }
+            ]
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Piano.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Piano.elm
new file mode 100644
index 0000000000..d231f14674
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Piano.elm
@@ -0,0 +1,194 @@
+module Piano exposing (render)
+
+import Browser
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import List.Extra
+import Theory
+import UI
+
+
+type alias KeyMarkup a =
+    { offset : Int
+    , isHighlit : Bool
+    , note : Theory.Note
+    , isRootNote : Bool
+    }
+    -> Html a
+
+
+type alias Props =
+    { chord : Maybe Theory.Chord
+    , firstNote : Theory.Note
+    , lastNote : Theory.Note
+    }
+
+
+naturalThickness : Int
+naturalThickness =
+    105
+
+
+accidentalThickness : Int
+accidentalThickness =
+    round (toFloat naturalThickness / 2.0)
+
+
+{-| Convert an integer into its pixel representation for CSS.
+-}
+pixelate : Int -> String
+pixelate x =
+    String.fromInt x ++ "px"
+
+
+{-| Return the markup for either a white or a black key.
+-}
+pianoKey : KeyMarkup a
+pianoKey { offset, isHighlit, note, isRootNote } =
+    let
+        { natColor, accColor, hiColor, rootColor } =
+            { natColor = "bg-white"
+            , accColor = "bg-black"
+            , hiColor = "bg-red-400"
+            , rootColor = "bg-red-600"
+            }
+
+        sharedClasses =
+            [ "box-border"
+            , "absolute"
+            , "border"
+            , "border-black"
+            ]
+
+        { keyLength, keyThickness, keyColor, offsetEdge, extraClasses } =
+            case Theory.keyClass note of
+                Theory.Natural ->
+                    { keyLength = "w-screen"
+                    , keyThickness = naturalThickness
+                    , keyColor = natColor
+                    , offsetEdge = "top"
+                    , extraClasses = []
+                    }
+
+                Theory.Accidental ->
+                    { keyLength = "w-2/3"
+                    , keyThickness = accidentalThickness
+                    , keyColor = accColor
+                    , offsetEdge = "top"
+                    , extraClasses = [ "z-10" ]
+                    }
+    in
+    div
+        [ class
+            (case ( isHighlit, isRootNote ) of
+                ( False, _ ) ->
+                    keyColor
+
+                ( True, True ) ->
+                    rootColor
+
+                ( True, False ) ->
+                    hiColor
+            )
+        , class keyLength
+        , style "height" (pixelate keyThickness)
+        , style offsetEdge (String.fromInt offset ++ "px")
+        , class <| String.join " " (List.concat [ sharedClasses, extraClasses ])
+        ]
+        []
+
+
+{-| A section of the piano consisting of all twelve notes.
+-}
+keys :
+    { start : Theory.Note
+    , end : Theory.Note
+    , highlitNotes : List Theory.Note
+    , rootNote : Maybe Theory.Note
+    }
+    -> List (Html a)
+keys { start, end, highlitNotes, rootNote } =
+    let
+        isHighlit note =
+            List.member note highlitNotes
+
+        spacing prevOffset prev curr =
+            case ( Theory.keyClass prev, Theory.keyClass curr ) of
+                ( Theory.Natural, Theory.Accidental ) ->
+                    prevOffset + naturalThickness - round (toFloat accidentalThickness / 2)
+
+                ( Theory.Accidental, Theory.Natural ) ->
+                    prevOffset + round (toFloat accidentalThickness / 2)
+
+                ( Theory.Natural, Theory.Natural ) ->
+                    prevOffset + naturalThickness
+
+                -- This pattern should never hit.
+                _ ->
+                    prevOffset
+
+        ( _, _, notes ) =
+            Theory.notesFromRange start end
+                |> List.reverse
+                |> List.foldl
+                    (\curr ( prevOffset, prev, result ) ->
+                        case ( prevOffset, prev ) of
+                            ( Nothing, Nothing ) ->
+                                ( Just 0
+                                , Just curr
+                                , pianoKey
+                                    { offset = 0
+                                    , isHighlit = List.member curr highlitNotes
+                                    , note = curr
+                                    , isRootNote =
+                                        rootNote
+                                            |> Maybe.map (\x -> x == curr)
+                                            |> Maybe.withDefault False
+                                    }
+                                    :: result
+                                )
+
+                            ( Just po, Just p ) ->
+                                let
+                                    offset =
+                                        spacing po p curr
+                                in
+                                ( Just offset
+                                , Just curr
+                                , pianoKey
+                                    { offset = offset
+                                    , isHighlit = List.member curr highlitNotes
+                                    , note = curr
+                                    , isRootNote =
+                                        rootNote
+                                            |> Maybe.map (\x -> x == curr)
+                                            |> Maybe.withDefault False
+                                    }
+                                    :: result
+                                )
+
+                            -- This pattern should never hit.
+                            _ ->
+                                ( Nothing, Nothing, [] )
+                    )
+                    ( Nothing, Nothing, [] )
+    in
+    notes
+
+
+{-| Return the HTML that renders a piano representation.
+-}
+render : Props -> Html a
+render { chord } =
+    div [ style "display" "flex" ]
+        (keys
+            { start = Theory.G3
+            , end = Theory.C6
+            , rootNote = chord |> Maybe.map .note
+            , highlitNotes =
+                chord
+                    |> Maybe.andThen Theory.notesForChord
+                    |> Maybe.withDefault []
+            }
+        )
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Practice.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Practice.elm
new file mode 100644
index 0000000000..5d87bcee50
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Practice.elm
@@ -0,0 +1,61 @@
+module Practice exposing (render)
+
+import FlashCard
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Icon
+import Piano
+import State
+import Tailwind
+import Theory
+import UI
+
+
+openPreferences : Html State.Msg
+openPreferences =
+    button
+        [ class "w-48 h-48 absolute left-0 top-0 z-50"
+        , onClick (State.SetView State.Preferences)
+        ]
+        [ Icon.cog ]
+
+
+render : State.Model -> Html State.Msg
+render model =
+    let
+        ( handleClick, buttonText ) =
+            if model.isPaused then
+                ( State.Play, "Tap to practice" )
+
+            else
+                ( State.Pause, "" )
+    in
+    div []
+        [ openPreferences
+        , case model.selectedChord of
+            Just chord ->
+                FlashCard.render
+                    { chord = chord
+                    , visible = model.showFlashCard
+                    }
+
+            Nothing ->
+                -- Here I'm abusing the overlayButton component to render text
+                -- horizontally. I should support a UI component for this.
+                UI.overlayButton
+                    { label = "Get ready..."
+                    , handleClick = State.DoNothing
+                    , isVisible = True
+                    }
+        , UI.overlayButton
+            { label = buttonText
+            , handleClick = handleClick
+            , isVisible = model.isPaused
+            }
+        , Piano.render
+            { chord = model.selectedChord
+            , firstNote = model.firstNote
+            , lastNote = model.lastNote
+            }
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Preferences.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Preferences.elm
new file mode 100644
index 0000000000..59e6c8234c
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Preferences.elm
@@ -0,0 +1,148 @@
+module Preferences exposing (render)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Icon
+import Responsive
+import State
+import Tailwind
+import Tempo
+import Theory
+import UI
+
+
+selectKey :
+    State.Model
+    ->
+        { relativeMajor : Theory.Key
+        , relativeMinor : Theory.Key
+        }
+    -> Html State.Msg
+selectKey model { relativeMajor, relativeMinor } =
+    let
+        active key =
+            List.member key model.whitelistedKeys
+
+        buttonLabel major minor =
+            Theory.viewKey major ++ ", " ++ Theory.viewKey minor
+    in
+    div [ class "flex pt-0" ]
+        [ UI.textToggleButton
+            { label = buttonLabel relativeMajor relativeMinor
+            , handleClick = State.ToggleKey relativeMajor
+            , classes = [ "flex-1" ]
+            , toggled = active relativeMajor
+            }
+        ]
+
+
+inversionCheckboxes : State.Model -> Html State.Msg
+inversionCheckboxes model =
+    div []
+        [ h2
+            [ [ "text-gray-500"
+              , "text-center"
+              , "pt-10"
+              , Responsive.h2
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ text "Select inversions" ]
+        , ul
+            [ [ "flex", "justify-center" ] |> Tailwind.use |> class ]
+            (Theory.allInversions
+                |> List.map
+                    (\inversion ->
+                        li []
+                            [ UI.textToggleButton
+                                { label = Theory.inversionName inversion
+                                , handleClick = State.ToggleInversion inversion
+                                , classes = []
+                                , toggled = List.member inversion model.whitelistedInversions
+                                }
+                            ]
+                    )
+            )
+        ]
+
+
+keyCheckboxes : State.Model -> Html State.Msg
+keyCheckboxes model =
+    let
+        majorKey pitchClass =
+            { pitchClass = pitchClass, mode = Theory.MajorMode }
+
+        minorKey pitchClass =
+            { pitchClass = pitchClass, mode = Theory.MinorMode }
+
+        circleOfFifths =
+            [ ( Theory.C, Theory.A )
+            , ( Theory.G, Theory.E )
+            , ( Theory.D, Theory.B )
+            , ( Theory.A, Theory.F_sharp )
+            , ( Theory.E, Theory.C_sharp )
+            , ( Theory.B, Theory.G_sharp )
+            , ( Theory.F_sharp, Theory.D_sharp )
+            , ( Theory.C_sharp, Theory.A_sharp )
+            , ( Theory.G_sharp, Theory.F )
+            , ( Theory.D_sharp, Theory.C )
+            , ( Theory.A_sharp, Theory.G )
+            , ( Theory.F, Theory.D )
+            ]
+    in
+    div []
+        [ h2
+            [ [ "text-gray-500"
+              , "text-center"
+              , "pt-10"
+              , Responsive.h2
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ text "Select keys" ]
+        , ul []
+            (circleOfFifths
+                |> List.map
+                    (\( major, minor ) ->
+                        selectKey model
+                            { relativeMajor = majorKey major
+                            , relativeMinor = minorKey minor
+                            }
+                    )
+            )
+        ]
+
+
+closePreferences : Html State.Msg
+closePreferences =
+    button
+        [ [ "w-48"
+          , "lg:w-32"
+          , "h-48"
+          , "lg:h-32"
+          , "absolute"
+          , "right-0"
+          , "top-0"
+          , "z-10"
+          ]
+            |> Tailwind.use
+            |> class
+        , onClick (State.SetView State.Practice)
+        ]
+        [ Icon.close ]
+
+
+render : State.Model -> Html State.Msg
+render model =
+    div [ class "pt-10 pb-20 px-10" ]
+        [ closePreferences
+        , Tempo.render
+            { tempo = model.tempo
+            , handleInput = State.SetTempo
+            }
+        , inversionCheckboxes model
+        , keyCheckboxes model
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Responsive.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Responsive.elm
new file mode 100644
index 0000000000..5d97161df6
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Responsive.elm
@@ -0,0 +1,19 @@
+module Responsive exposing (..)
+
+{-| Returns a string containing all of the Tailwind selectors we use to size
+h2-sized elements across various devices. -}
+h1 : String
+h1 =
+    "text-6xl lg:text-4xl"
+
+{-| Returns a string containing all of the Tailwind selectors we use to size
+h2-sized elements across various devices. -}
+h2 : String
+h2 =
+    "text-5xl lg:text-3xl"
+
+{-| Returns a string containing all of the Tailwind selectors we use to size
+h3-sized elements across various devices. -}
+h3 : String
+h3 =
+    "text-4xl lg:text-2xl"
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/State.elm b/users/wpcarro/website/sandbox/learnpianochords/src/State.elm
new file mode 100644
index 0000000000..678fb0f9aa
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/State.elm
@@ -0,0 +1,179 @@
+module State exposing (..)
+
+import Random
+import Random.List
+import Theory
+
+
+type Msg
+    = NextChord
+    | NewChord Theory.Chord
+    | Play
+    | Pause
+    | SetTempo String
+    | ToggleInversion Theory.ChordInversion
+    | ToggleKey Theory.Key
+    | DoNothing
+    | SetView View
+    | ToggleFlashCard
+
+
+type View
+    = Preferences
+    | Practice
+    | Overview
+
+
+type alias Model =
+    { whitelistedChords : List Theory.Chord
+    , whitelistedChordTypes : List Theory.ChordType
+    , whitelistedInversions : List Theory.ChordInversion
+    , whitelistedPitchClasses : List Theory.PitchClass
+    , whitelistedKeys : List Theory.Key
+    , selectedChord : Maybe Theory.Chord
+    , isPaused : Bool
+    , tempo : Int
+    , firstNote : Theory.Note
+    , lastNote : Theory.Note
+    , view : View
+    , showFlashCard : Bool
+    }
+
+
+{-| The initial state for the application.
+-}
+init : Model
+init =
+    let
+        ( firstNote, lastNote ) =
+            ( Theory.C3, Theory.C6 )
+
+        inversions =
+            [ Theory.Root ]
+
+        chordTypes =
+            Theory.allChordTypes
+
+        pitchClasses =
+            Theory.allPitchClasses
+
+        keys =
+            [ { pitchClass = Theory.C, mode = Theory.MajorMode } ]
+    in
+    { whitelistedChords =
+        keys
+            |> List.concatMap Theory.chordsForKey
+            |> List.filter (\chord -> List.member chord.chordInversion inversions)
+    , whitelistedChordTypes = chordTypes
+    , whitelistedInversions = inversions
+    , whitelistedPitchClasses = pitchClasses
+    , whitelistedKeys = keys
+    , selectedChord = Nothing
+    , isPaused = True
+    , tempo = 10
+    , firstNote = firstNote
+    , lastNote = lastNote
+    , view = Overview
+    , showFlashCard = True
+    }
+
+
+{-| Now that we have state, we need a function to change the state.
+-}
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        DoNothing ->
+            ( model, Cmd.none )
+
+        SetView x ->
+            ( { model
+                | view = x
+                , isPaused = True
+              }
+            , Cmd.none
+            )
+
+        NewChord chord ->
+            ( { model | selectedChord = Just chord }
+            , Cmd.none
+            )
+
+        NextChord ->
+            ( model
+            , Random.generate
+                (\x ->
+                    case x of
+                        ( Just chord, _ ) ->
+                            NewChord chord
+
+                        ( Nothing, _ ) ->
+                            DoNothing
+                )
+                (Random.List.choose model.whitelistedChords)
+            )
+
+        Play ->
+            ( { model | isPaused = False }
+            , Cmd.none
+            )
+
+        Pause ->
+            ( { model | isPaused = True }
+            , Cmd.none
+            )
+
+        ToggleInversion inversion ->
+            let
+                inversions =
+                    if List.member inversion model.whitelistedInversions then
+                        List.filter ((/=) inversion) model.whitelistedInversions
+
+                    else
+                        inversion :: model.whitelistedInversions
+            in
+            ( { model
+                | whitelistedInversions = inversions
+                , whitelistedChords =
+                    model.whitelistedKeys
+                        |> List.concatMap Theory.chordsForKey
+                        |> List.filter (\chord -> List.member chord.chordInversion inversions)
+              }
+            , Cmd.none
+            )
+
+        ToggleKey key ->
+            let
+                keys =
+                    if List.member key model.whitelistedKeys then
+                        List.filter ((/=) key) model.whitelistedKeys
+
+                    else
+                        key :: model.whitelistedKeys
+            in
+            ( { model
+                | whitelistedKeys = keys
+                , whitelistedChords =
+                    keys
+                        |> List.concatMap Theory.chordsForKey
+                        |> List.filter (\chord -> List.member chord.chordInversion model.whitelistedInversions)
+                , selectedChord = Nothing
+              }
+            , Cmd.none
+            )
+
+        SetTempo tempo ->
+            ( { model
+                | tempo =
+                    case String.toInt tempo of
+                        Just x ->
+                            x
+
+                        Nothing ->
+                            model.tempo
+              }
+            , Cmd.none
+            )
+
+        ToggleFlashCard ->
+            ( { model | showFlashCard = not model.showFlashCard }, Cmd.none )
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Tailwind.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Tailwind.elm
new file mode 100644
index 0000000000..57d419db5a
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Tailwind.elm
@@ -0,0 +1,29 @@
+module Tailwind exposing (..)
+
+{-| Functions to make Tailwind development in Elm even more pleasant.
+-}
+
+
+{-| Conditionally use `class` selection when `condition` is true.
+-}
+when : Bool -> String -> String
+when condition class =
+    if condition then
+        class
+
+    else
+        ""
+
+
+if_ : Bool -> String -> String -> String
+if_ condition whenTrue whenFalse =
+    if condition then
+        whenTrue
+
+    else
+        whenFalse
+
+
+use : List String -> String
+use styles =
+    String.join " " styles
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Tempo.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Tempo.elm
new file mode 100644
index 0000000000..041313614f
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Tempo.elm
@@ -0,0 +1,33 @@
+module Tempo exposing (render)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Responsive
+import Tailwind
+import UI
+
+
+type alias Props msg =
+    { tempo : Int
+    , handleInput : String -> msg
+    }
+
+
+render : Props msg -> Html msg
+render { tempo, handleInput } =
+    div [ class "text-center" ]
+        [ p
+            [ [ "py-10"
+              , Responsive.h2
+              ]
+                |> Tailwind.use
+                |> class
+            ]
+            [ text (String.fromInt tempo ++ " BPM") ]
+        , UI.textField
+            { placeholderText = "Set tempo..."
+            , handleInput = handleInput
+            , classes = []
+            }
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/Theory.elm b/users/wpcarro/website/sandbox/learnpianochords/src/Theory.elm
new file mode 100644
index 0000000000..7f54832c97
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/Theory.elm
@@ -0,0 +1,1100 @@
+module Theory exposing (..)
+
+import Array exposing (Array)
+import Dict exposing (Dict)
+import List.Extra
+import Maybe.Extra
+import Misc
+
+
+{-| Notes are the individuals sounds that we use to create music. Think: "do re
+mi fa so la ti do".
+
+Note: Technically a "C-sharp" is also a "D-flat", but I will model accidentals
+(i.e. sharps and flats) as sharps and represent the ambiguity when I render the
+underlying state of the application.
+
+Note: There are "notes" like A, B, D-flat, and then there are notes like "middle
+C", also denoted in scientific pitch notation as C4. I'm unsure of what to call
+each of these, and my application does not model scientific pitch notation yet,
+so these non-scientific pitch denote values are "notes" for now.
+
+-}
+type Note
+    = C1
+    | C_sharp1
+    | D1
+    | D_sharp1
+    | E1
+    | F1
+    | F_sharp1
+    | G1
+    | G_sharp1
+    | A1
+    | A_sharp1
+    | B1
+    | C2
+    | C_sharp2
+    | D2
+    | D_sharp2
+    | E2
+    | F2
+    | F_sharp2
+    | G2
+    | G_sharp2
+    | A2
+    | A_sharp2
+    | B2
+    | C3
+    | C_sharp3
+    | D3
+    | D_sharp3
+    | E3
+    | F3
+    | F_sharp3
+    | G3
+    | G_sharp3
+    | A3
+    | A_sharp3
+    | B3
+    | C4
+    | C_sharp4
+    | D4
+    | D_sharp4
+    | E4
+    | F4
+    | F_sharp4
+    | G4
+    | G_sharp4
+    | A4
+    | A_sharp4
+    | B4
+    | C5
+    | C_sharp5
+    | D5
+    | D_sharp5
+    | E5
+    | F5
+    | F_sharp5
+    | G5
+    | G_sharp5
+    | A5
+    | A_sharp5
+    | B5
+    | C6
+    | C_sharp6
+    | D6
+    | D_sharp6
+    | E6
+    | F6
+    | F_sharp6
+    | G6
+    | G_sharp6
+    | A6
+    | A_sharp6
+    | B6
+    | C7
+    | C_sharp7
+    | D7
+    | D_sharp7
+    | E7
+    | F7
+    | F_sharp7
+    | G7
+    | G_sharp7
+    | A7
+    | A_sharp7
+    | B7
+    | C8
+
+
+{-| I alluded to this concept in the Note type's documentation. These are the
+letters of notes. For instance C2, C3, C4 are all instances of C.
+-}
+type PitchClass
+    = C
+    | C_sharp
+    | D
+    | D_sharp
+    | E
+    | F
+    | F_sharp
+    | G
+    | G_sharp
+    | A
+    | A_sharp
+    | B
+
+
+{-| Encode whether you are traversing "up" or "down" intervals
+-}
+type StepDirection
+    = Up
+    | Down
+
+
+{-| One can measure the difference between between notes using intervals.
+-}
+type Interval
+    = Half
+    | NHalves Int
+    | Whole
+    | MajorThird
+    | MinorThird
+    | PerfectFifth
+    | AugmentedFifth
+    | DiminishedFifth
+    | MajorSeventh
+    | DominantSeventh
+
+
+{-| Add direction to a distance on the piano.
+-}
+type alias IntervalVector =
+    { interval : Interval
+    , direction : StepDirection
+    }
+
+
+{-| A bundle of notes which are usually, but not necessarily harmonious.
+-}
+type alias Chord =
+    { note : Note
+    , chordType : ChordType
+    , chordInversion : ChordInversion
+    }
+
+
+{-| Many possible chords exist. This type encodes the possibilities. I am
+tempted to model these in a more "DRY" way, but I worry that this abstraction
+may cause more problems than it solves.
+-}
+type ChordType
+    = Major
+    | Sus2
+    | Sus4
+    | Major7
+    | MajorDominant7
+    | Minor
+    | MinorMajor7
+    | MinorDominant7
+    | Augmented
+    | AugmentedDominant7
+    | Diminished
+    | DiminishedDominant7
+    | DiminishedMajor7
+
+
+{-| On a piano, a triad can be played three ways. As a rule-of-thumb, The number
+of ways a pianist can play a chord is equal to the number of notes in the chord
+itself.
+-}
+type ChordInversion
+    = Root
+    | First
+    | Second
+
+
+{-| Whether a given note is a white key or a black key.
+-}
+type KeyClass
+    = Natural
+    | Accidental
+
+
+{-| Songs are written in one or more keys, which define the notes and therefore
+chords that harmonize with one another.
+-}
+type alias Key =
+    { pitchClass : PitchClass
+    , mode : Mode
+    }
+
+
+{-| We create "scales" by enumerating the notes of a given key. These keys are
+defined by the "tonic" note and the "mode". I thought about including Ionian,
+Dorian, Phrygian, etc., but in the I would like to avoid over-abstracting this
+early on, so I'm going to err on the side of overly concrete until I have a
+better idea of the extent of this project.
+-}
+type Mode
+    = BluesMode
+    | MajorMode
+    | MinorMode
+
+
+type alias NoteMetadata =
+    { note : Note
+    , label : String
+    , pitchClass : PitchClass
+    , natural : Bool
+    }
+
+
+{-| An integer representing which note in a given scale to play.
+-}
+type alias ScaleDegree =
+    Int
+
+
+{-| Returns the Note in the cental octave of the piano for a given
+PitchClass. For example, C4 -- or "middle C" -- for C.
+-}
+noteInCentralOctave : PitchClass -> Note
+noteInCentralOctave pitchClass =
+    case pitchClass of
+        C ->
+            C4
+
+        C_sharp ->
+            C_sharp4
+
+        D ->
+            D4
+
+        D_sharp ->
+            D_sharp4
+
+        E ->
+            E4
+
+        F ->
+            F4
+
+        F_sharp ->
+            F_sharp4
+
+        G ->
+            G4
+
+        G_sharp ->
+            G_sharp4
+
+        A ->
+            A4
+
+        A_sharp ->
+            A_sharp4
+
+        B ->
+            B4
+
+
+{-| Return the human-readable version of a chord inversion.
+-}
+inversionName : ChordInversion -> String
+inversionName inversion =
+    case inversion of
+        Root ->
+            "Root"
+
+        First ->
+            "First"
+
+        Second ->
+            "Second"
+
+
+{-| Return the human-readable version of a chord type.
+-}
+chordTypeName : ChordType -> String
+chordTypeName chordType =
+    case chordType of
+        Major ->
+            "major"
+
+        Sus2 ->
+            "suspended 2"
+
+        Sus4 ->
+            "suspended 4"
+
+        Major7 ->
+            "major 7th"
+
+        MajorDominant7 ->
+            "major dominant 7th"
+
+        Minor ->
+            "minor"
+
+        MinorMajor7 ->
+            "minor major 7th"
+
+        MinorDominant7 ->
+            "minor dominant 7th"
+
+        Augmented ->
+            "augmented"
+
+        AugmentedDominant7 ->
+            "augmented dominant 7th"
+
+        Diminished ->
+            "diminished"
+
+        DiminishedDominant7 ->
+            "diminished dominant 7th"
+
+        DiminishedMajor7 ->
+            "diminished major 7th"
+
+
+{-| Return the note that is one half step away from `note` in the direction,
+`dir`.
+In the case of stepping up or down from the end of the piano, this returns a
+Maybe.
+-}
+halfStep : StepDirection -> Note -> Maybe Note
+halfStep dir note =
+    let
+        everyNote =
+            notesFromRange C2 C8
+    in
+    case dir of
+        Up ->
+            Misc.comesAfter note everyNote
+
+        Down ->
+            Misc.comesBefore note everyNote
+
+
+{-| Return a list of steps to take away from the root note to return back to the
+root note for a given mode.
+-}
+intervalsForMode : Mode -> List IntervalVector
+intervalsForMode mode =
+    let
+        up x =
+            { direction = Up, interval = x }
+
+        down x =
+            { direction = Down, interval = x }
+    in
+    case mode of
+        MajorMode ->
+            List.map up [ Whole, Whole, Half, Whole, Whole, Whole ]
+
+        MinorMode ->
+            List.map up [ Whole, Half, Whole, Whole, Half, Whole ]
+
+        BluesMode ->
+            List.map up [ MinorThird, Whole, Half, Half, MinorThird ]
+
+
+{-| Return a list of the intervals that a chord. Each interval measures
+the distance away from the root-note of the chord.
+-}
+intervalsForChordType : ChordType -> ChordInversion -> List IntervalVector
+intervalsForChordType chordType chordInversion =
+    let
+        up x =
+            { direction = Up, interval = x }
+
+        down x =
+            { direction = Down, interval = x }
+    in
+    case ( chordType, chordInversion ) of
+        -- Major
+        ( Major, Root ) ->
+            [ up MajorThird, up PerfectFifth ]
+
+        ( Major, First ) ->
+            [ down (NHalves 5), down (NHalves 8) ]
+
+        ( Major, Second ) ->
+            [ down (NHalves 5), up MajorThird ]
+
+        -- Sus2
+        ( Sus2, Root ) ->
+            [ up Whole, up PerfectFifth ]
+
+        ( Sus2, First ) ->
+            [ down (NHalves 10), down (NHalves 5) ]
+
+        ( Sus2, Second ) ->
+            [ down (NHalves 5), up Whole ]
+
+        -- Sus4
+        ( Sus4, Root ) ->
+            [ up (NHalves 5), up PerfectFifth ]
+
+        ( Sus4, First ) ->
+            [ down (NHalves 7), down (NHalves 5) ]
+
+        ( Sus4, Second ) ->
+            [ down (NHalves 5), up (NHalves 5) ]
+
+        -- Major7
+        ( Major7, Root ) ->
+            [ up MajorThird, up PerfectFifth, up MajorSeventh ]
+
+        ( Major7, First ) ->
+            down Half :: intervalsForChordType Major chordInversion
+
+        ( Major7, Second ) ->
+            down Half :: intervalsForChordType Major chordInversion
+
+        -- MajorDominant7
+        ( MajorDominant7, Root ) ->
+            up DominantSeventh :: intervalsForChordType Major chordInversion
+
+        ( MajorDominant7, First ) ->
+            down Whole :: intervalsForChordType Major chordInversion
+
+        ( MajorDominant7, Second ) ->
+            down Whole :: intervalsForChordType Major chordInversion
+
+        -- Minor
+        ( Minor, Root ) ->
+            [ up MinorThird, up PerfectFifth ]
+
+        ( Minor, First ) ->
+            [ down (NHalves 5), down (NHalves 9) ]
+
+        ( Minor, Second ) ->
+            [ down (NHalves 5), up MinorThird ]
+
+        -- MinorMajor7
+        ( MinorMajor7, Root ) ->
+            up MajorSeventh :: intervalsForChordType Minor chordInversion
+
+        ( MinorMajor7, First ) ->
+            down Half :: intervalsForChordType Minor chordInversion
+
+        ( MinorMajor7, Second ) ->
+            down Half :: intervalsForChordType Minor chordInversion
+
+        -- MinorDominant7
+        ( MinorDominant7, Root ) ->
+            up DominantSeventh :: intervalsForChordType Minor chordInversion
+
+        ( MinorDominant7, First ) ->
+            down Whole :: intervalsForChordType Minor chordInversion
+
+        ( MinorDominant7, Second ) ->
+            down Whole :: intervalsForChordType Minor chordInversion
+
+        -- Augmented
+        ( Augmented, Root ) ->
+            [ up MajorThird, up AugmentedFifth ]
+
+        ( Augmented, First ) ->
+            [ down (NHalves 8), down (NHalves 4) ]
+
+        ( Augmented, Second ) ->
+            [ down (NHalves 4), up MajorThird ]
+
+        -- AugmentedDominant7
+        ( AugmentedDominant7, Root ) ->
+            up DominantSeventh :: intervalsForChordType Augmented chordInversion
+
+        ( AugmentedDominant7, First ) ->
+            down Whole :: intervalsForChordType Augmented chordInversion
+
+        ( AugmentedDominant7, Second ) ->
+            down Whole :: intervalsForChordType Augmented chordInversion
+
+        -- Diminished
+        ( Diminished, Root ) ->
+            [ up MinorThird, up DiminishedFifth ]
+
+        ( Diminished, First ) ->
+            [ down (NHalves 6), down (NHalves 9) ]
+
+        ( Diminished, Second ) ->
+            [ down (NHalves 6), up MinorThird ]
+
+        -- DiminishedDominant7
+        ( DiminishedDominant7, Root ) ->
+            up DominantSeventh :: intervalsForChordType Diminished chordInversion
+
+        ( DiminishedDominant7, First ) ->
+            down Whole :: intervalsForChordType Diminished chordInversion
+
+        ( DiminishedDominant7, Second ) ->
+            down Whole :: intervalsForChordType Diminished chordInversion
+
+        -- DiminishedMajor7
+        ( DiminishedMajor7, Root ) ->
+            up MajorSeventh :: intervalsForChordType Diminished chordInversion
+
+        ( DiminishedMajor7, First ) ->
+            down Half :: intervalsForChordType Diminished chordInversion
+
+        ( DiminishedMajor7, Second ) ->
+            down Half :: intervalsForChordType Diminished chordInversion
+
+
+{-| Return the note in the direction, `dir`, away from `note` `s` intervals
+-}
+step : IntervalVector -> Note -> Maybe Note
+step { direction, interval } note =
+    let
+        doStep int =
+            step { direction = direction, interval = int }
+    in
+    case interval of
+        Half ->
+            halfStep direction note
+
+        NHalves n ->
+            List.repeat n
+                { direction = direction
+                , interval = Half
+                }
+                |> (\x -> walkNotes x note)
+                |> Maybe.andThen (List.reverse >> List.head)
+
+        Whole ->
+            note
+                |> doStep Half
+                |> Maybe.andThen (doStep Half)
+
+        MinorThird ->
+            note
+                |> doStep Whole
+                |> Maybe.andThen (doStep Half)
+
+        MajorThird ->
+            note
+                |> doStep Whole
+                |> Maybe.andThen (doStep Whole)
+
+        PerfectFifth ->
+            note
+                |> doStep MajorThird
+                |> Maybe.andThen (doStep MinorThird)
+
+        AugmentedFifth ->
+            note
+                |> doStep PerfectFifth
+                |> Maybe.andThen (doStep Half)
+
+        DiminishedFifth ->
+            note
+                |> doStep MajorThird
+                |> Maybe.andThen (doStep Whole)
+
+        MajorSeventh ->
+            note
+                |> doStep PerfectFifth
+                |> Maybe.andThen (doStep MajorThird)
+
+        DominantSeventh ->
+            note
+                |> doStep PerfectFifth
+                |> Maybe.andThen (doStep MinorThird)
+
+
+{-| Returns a list of all of the notes away from a give `note`.
+
+  - The 0th element is applied to `note`.
+  - The 1st element is applied to the result of the previous operation.
+  - The 2nd element is applied to the result of the previous operation.
+  - and so on...until all of the `steps` are exhausted.
+
+In the case where applying any of the steps would result in running off of
+either edge of the piano, this function returns a Nothing.
+
+-}
+walkNotes : List IntervalVector -> Note -> Maybe (List Note)
+walkNotes steps note =
+    doWalkNotes steps note [] |> Maybe.map List.reverse
+
+
+{-| Recursive helper for `walkNotes`.
+-}
+doWalkNotes : List IntervalVector -> Note -> List Note -> Maybe (List Note)
+doWalkNotes steps note result =
+    case steps of
+        [] ->
+            Just (note :: result)
+
+        s :: rest ->
+            case step s note of
+                Just x ->
+                    doWalkNotes rest x (note :: result)
+
+                Nothing ->
+                    Nothing
+
+
+{-| Return the KeyClass for a given `note`.
+-}
+keyClass : Note -> KeyClass
+keyClass note =
+    if isNatural note then
+        Natural
+
+    else
+        Accidental
+
+
+{-| Return the PitchClass for a given note.
+-}
+classifyNote : Note -> PitchClass
+classifyNote note =
+    note |> getNoteMetadata |> .pitchClass
+
+
+{-| Return a list of the notes that comprise a `chord`
+-}
+notesForChord : Chord -> Maybe (List Note)
+notesForChord { note, chordType, chordInversion } =
+    intervalsForChordType chordType chordInversion
+        |> List.map (\interval -> step interval note)
+        |> Maybe.Extra.combine
+        |> Maybe.map (\notes -> note :: notes)
+
+
+{-| Return the scale for a given `key`.
+-}
+notesForKey : Key -> List Note
+notesForKey { pitchClass, mode } =
+    let
+        origin =
+            noteInCentralOctave pitchClass
+    in
+    case walkNotes (intervalsForMode mode) origin of
+        -- We should never hit the Nothing case here.
+        Nothing ->
+            []
+
+        Just scale ->
+            scale
+
+
+{-| Return true if `note` is a black key.
+-}
+isAccidental : Note -> Bool
+isAccidental note =
+    note |> isNatural |> not
+
+
+{-| Return true if `note` is a white key.
+-}
+isNatural : Note -> Bool
+isNatural note =
+    note |> getNoteMetadata |> .natural
+
+
+{-| Return a list of all of the notes that we know about.
+Only return the notes within the range `start` and `end`.
+-}
+notesFromRange : Note -> Note -> List Note
+notesFromRange start end =
+    noteMetadata
+        |> Array.toList
+        |> List.map .note
+        |> List.Extra.dropWhile ((/=) start)
+        |> List.Extra.takeWhile ((/=) end)
+
+
+{-| Return a list of all of the chord inversions about which we know.
+-}
+allInversions : List ChordInversion
+allInversions =
+    [ Root, First, Second ]
+
+
+{-| Return a list of all of the chord types about which we know.
+-}
+allChordTypes : List ChordType
+allChordTypes =
+    [ Major
+    , Sus2
+    , Sus4
+    , Major7
+    , MajorDominant7
+    , Minor
+    , MinorMajor7
+    , MinorDominant7
+    , Augmented
+    , AugmentedDominant7
+    , Diminished
+    , DiminishedDominant7
+    , DiminishedMajor7
+    ]
+
+
+{-| Return a list of all of the key modes about which we know.
+-}
+allModes : List Mode
+allModes =
+    [ MajorMode, MinorMode, BluesMode ]
+
+
+{-| Return a list of all of the keys about which we know.
+-}
+allKeys : List Key
+allKeys =
+    allPitchClasses
+        |> List.Extra.andThen
+            (\pitchClass ->
+                allModes
+                    |> List.Extra.andThen
+                        (\mode ->
+                            [ { pitchClass = pitchClass
+                              , mode = mode
+                              }
+                            ]
+                        )
+            )
+
+
+{-| Return an array of every note on a piano.
+Note: Currently this piano has 85 keys, but modern pianos have 88 keys. I would
+prefer to have 88 keys, but it's not urgent.
+-}
+noteMetadata : Array NoteMetadata
+noteMetadata =
+    Array.fromList
+        [ { note = A1, label = "A1", pitchClass = A, natural = True }
+        , { note = A_sharp1, label = "A♯/B♭1", pitchClass = A_sharp, natural = False }
+        , { note = B1, label = "B1", pitchClass = B, natural = True }
+        , { note = C1, label = "C1", pitchClass = C, natural = True }
+        , { note = C_sharp1, label = "C♯/D♭1", pitchClass = C_sharp, natural = False }
+        , { note = D1, label = "D1", pitchClass = D, natural = True }
+        , { note = D_sharp1, label = "D♯/E♭1", pitchClass = D_sharp, natural = False }
+        , { note = E1, label = "E1", pitchClass = E, natural = True }
+        , { note = F1, label = "F1", pitchClass = F, natural = True }
+        , { note = F_sharp1, label = "F♯/G♭1", pitchClass = F_sharp, natural = False }
+        , { note = G1, label = "G1", pitchClass = G, natural = True }
+        , { note = G_sharp1, label = "G♯/A♭1", pitchClass = G_sharp, natural = False }
+        , { note = A2, label = "A2", pitchClass = A, natural = True }
+        , { note = A_sharp2, label = "A♯/B♭2", pitchClass = A_sharp, natural = False }
+        , { note = B2, label = "B2", pitchClass = B, natural = True }
+        , { note = C2, label = "C2", pitchClass = C, natural = True }
+        , { note = C_sharp2, label = "C♯/D♭2", pitchClass = C_sharp, natural = False }
+        , { note = D2, label = "D2", pitchClass = D, natural = True }
+        , { note = D_sharp2, label = "D♯/E♭2", pitchClass = D_sharp, natural = False }
+        , { note = E2, label = "E2", pitchClass = E, natural = True }
+        , { note = F2, label = "F2", pitchClass = F, natural = True }
+        , { note = F_sharp2, label = "F♯/G♭2", pitchClass = F_sharp, natural = False }
+        , { note = G2, label = "G2", pitchClass = G, natural = True }
+        , { note = G_sharp2, label = "G♯/A♭2", pitchClass = G_sharp, natural = False }
+        , { note = A3, label = "A3", pitchClass = A, natural = True }
+        , { note = A_sharp3, label = "A♯/B♭3", pitchClass = A_sharp, natural = False }
+        , { note = B3, label = "B3", pitchClass = B, natural = True }
+        , { note = C3, label = "C3", pitchClass = C, natural = True }
+        , { note = C_sharp3, label = "C♯/D♭3", pitchClass = C_sharp, natural = False }
+        , { note = D3, label = "D3", pitchClass = D, natural = True }
+        , { note = D_sharp3, label = "D♯/E♭3", pitchClass = D_sharp, natural = False }
+        , { note = E3, label = "E3", pitchClass = E, natural = True }
+        , { note = F3, label = "F3", pitchClass = F, natural = True }
+        , { note = F_sharp3, label = "F♯/G♭3", pitchClass = F_sharp, natural = False }
+        , { note = G3, label = "G3", pitchClass = G, natural = True }
+        , { note = G_sharp3, label = "G♯/A♭3", pitchClass = G_sharp, natural = False }
+        , { note = A4, label = "A4", pitchClass = A, natural = True }
+        , { note = A_sharp4, label = "A♯/B♭4", pitchClass = A_sharp, natural = False }
+        , { note = B4, label = "B4", pitchClass = B, natural = True }
+        , { note = C4, label = "C4", pitchClass = C, natural = True }
+        , { note = C_sharp4, label = "C♯/D♭4", pitchClass = C_sharp, natural = False }
+        , { note = D4, label = "D4", pitchClass = D, natural = True }
+        , { note = D_sharp4, label = "D♯/E♭4", pitchClass = D_sharp, natural = False }
+        , { note = E4, label = "E4", pitchClass = E, natural = True }
+        , { note = F4, label = "F4", pitchClass = F, natural = True }
+        , { note = F_sharp4, label = "F♯/G♭4", pitchClass = F_sharp, natural = False }
+        , { note = G4, label = "G4", pitchClass = G, natural = True }
+        , { note = G_sharp4, label = "G♯/A♭4", pitchClass = G_sharp, natural = False }
+        , { note = A5, label = "A5", pitchClass = A, natural = True }
+        , { note = A_sharp5, label = "A♯/B♭5", pitchClass = A_sharp, natural = False }
+        , { note = B5, label = "B5", pitchClass = B, natural = True }
+        , { note = C5, label = "C5", pitchClass = C, natural = True }
+        , { note = C_sharp5, label = "C♯/D♭5", pitchClass = C_sharp, natural = False }
+        , { note = D5, label = "D5", pitchClass = D, natural = True }
+        , { note = D_sharp5, label = "D♯/E♭5", pitchClass = D_sharp, natural = False }
+        , { note = E5, label = "E5", pitchClass = E, natural = True }
+        , { note = F5, label = "F5", pitchClass = F, natural = True }
+        , { note = F_sharp5, label = "F♯/G♭5", pitchClass = F_sharp, natural = False }
+        , { note = G5, label = "G5", pitchClass = G, natural = True }
+        , { note = G_sharp5, label = "G♯/A♭5", pitchClass = G_sharp, natural = False }
+        , { note = A6, label = "A6", pitchClass = A, natural = True }
+        , { note = A_sharp6, label = "A♯/B♭6", pitchClass = A_sharp, natural = False }
+        , { note = B6, label = "B6", pitchClass = B, natural = True }
+        , { note = C6, label = "C6", pitchClass = C, natural = True }
+        , { note = C_sharp6, label = "C♯/D♭6", pitchClass = C_sharp, natural = False }
+        , { note = D6, label = "D6", pitchClass = D, natural = True }
+        , { note = D_sharp6, label = "D♯/E♭6", pitchClass = D_sharp, natural = False }
+        , { note = E6, label = "E6", pitchClass = E, natural = True }
+        , { note = F6, label = "F6", pitchClass = F, natural = True }
+        , { note = F_sharp6, label = "F♯/G♭6", pitchClass = F_sharp, natural = False }
+        , { note = G6, label = "G6", pitchClass = G, natural = True }
+        , { note = G_sharp6, label = "G♯/A♭6", pitchClass = G_sharp, natural = False }
+        , { note = A7, label = "A7", pitchClass = A, natural = True }
+        , { note = A_sharp7, label = "A♯/B♭7", pitchClass = A_sharp, natural = False }
+        , { note = B7, label = "B7", pitchClass = B, natural = True }
+        , { note = C7, label = "C7", pitchClass = C, natural = True }
+        , { note = C_sharp7, label = "C♯/D♭7", pitchClass = C_sharp, natural = False }
+        , { note = D7, label = "D7", pitchClass = D, natural = True }
+        , { note = D_sharp7, label = "D♯/E♭7", pitchClass = D_sharp, natural = False }
+        , { note = E7, label = "E7", pitchClass = E, natural = True }
+        , { note = F7, label = "F7", pitchClass = F, natural = True }
+        , { note = F_sharp7, label = "F♯/G♭7", pitchClass = F_sharp, natural = False }
+        , { note = G7, label = "G7", pitchClass = G, natural = True }
+        , { note = G_sharp7, label = "G♯/A♭7", pitchClass = G_sharp, natural = False }
+        , { note = C8, label = "C8", pitchClass = C, natural = True }
+        ]
+
+
+{-| Mapping of note data to commonly needed metadata for that note.
+-}
+getNoteMetadata : Note -> NoteMetadata
+getNoteMetadata note =
+    case Array.get (noteAsNumber note) noteMetadata of
+        Just metadata ->
+            metadata
+
+        -- This case should never hit, so we just return C1 to appease the
+        -- compiler.
+        Nothing ->
+            getNoteMetadata C1
+
+
+{-| Return the numeric representation of `note` to ues when comparing two
+notes.
+-}
+noteAsNumber : Note -> Int
+noteAsNumber note =
+    let
+        result =
+            noteMetadata
+                |> Array.toList
+                |> List.indexedMap Tuple.pair
+                |> Misc.find (\( _, x ) -> x.note == note)
+    in
+    case result of
+        Nothing ->
+            0
+
+        Just ( i, _ ) ->
+            i
+
+
+{-| Return true if all of the notes that comprise `chord` can be played on a
+piano whose keys begin at `start` and end at `end`.
+-}
+chordWithinRange : Note -> Note -> Chord -> Bool
+chordWithinRange start end chord =
+    case notesForChord chord of
+        Just notes ->
+            let
+                nums =
+                    List.map noteAsNumber notes
+
+                lo =
+                    List.minimum nums |> Maybe.withDefault (noteAsNumber start)
+
+                hi =
+                    List.maximum nums |> Maybe.withDefault (noteAsNumber end)
+            in
+            lo >= noteAsNumber start && hi < noteAsNumber end
+
+        Nothing ->
+            False
+
+
+{-| Return a list of all of the pitch classes that we know about.
+-}
+allPitchClasses : List PitchClass
+allPitchClasses =
+    [ C
+    , C_sharp
+    , D
+    , D_sharp
+    , E
+    , F
+    , F_sharp
+    , G
+    , G_sharp
+    , A
+    , A_sharp
+    , B
+    ]
+
+
+{-| Return a list of all of the chords that we know about.
+Only create chords from the range of notes delimited by the range `start` and
+`end`.
+-}
+allChords :
+    { start : Note
+    , end : Note
+    , inversions : List ChordInversion
+    , chordTypes : List ChordType
+    , pitchClasses : List PitchClass
+    }
+    -> List Chord
+allChords { start, end, inversions, chordTypes, pitchClasses } =
+    let
+        notes =
+            notesFromRange start end
+                |> List.filter (\note -> List.member (classifyNote note) pitchClasses)
+    in
+    notes
+        |> List.Extra.andThen
+            (\note ->
+                chordTypes
+                    |> List.Extra.andThen
+                        (\chordType ->
+                            inversions
+                                |> List.Extra.andThen
+                                    (\inversion ->
+                                        [ { note = note
+                                          , chordType = chordType
+                                          , chordInversion = inversion
+                                          }
+                                        ]
+                                    )
+                        )
+            )
+        |> List.filter (chordWithinRange start end)
+
+
+{-| Return a human-readable format of `note`.
+-}
+viewNote : Note -> String
+viewNote note =
+    note |> getNoteMetadata |> .label
+
+
+{-| Return a human-readable format of `chord`.
+-}
+viewChord : Chord -> String
+viewChord { note, chordType, chordInversion } =
+    viewPitchClass (classifyNote note) ++ " " ++ chordTypeName chordType ++ " " ++ inversionName chordInversion ++ " position"
+
+
+{-| Return a human-readable format of `pitchClass`.
+-}
+viewPitchClass : PitchClass -> String
+viewPitchClass pitchClass =
+    case pitchClass of
+        C ->
+            "C"
+
+        C_sharp ->
+            "C♯/D♭"
+
+        D ->
+            "D"
+
+        D_sharp ->
+            "D♯/E♭"
+
+        E ->
+            "E"
+
+        F ->
+            "F"
+
+        F_sharp ->
+            "F♯/G♭"
+
+        G ->
+            "G"
+
+        G_sharp ->
+            "G♯/A♭"
+
+        A ->
+            "A"
+
+        A_sharp ->
+            "A♯/B♭"
+
+        B ->
+            "B"
+
+
+viewMode : Mode -> String
+viewMode mode =
+    case mode of
+        MajorMode ->
+            "major"
+
+        MinorMode ->
+            "minor"
+
+        BluesMode ->
+            "blues"
+
+
+{-| Return the human-readable format of `key`.
+-}
+viewKey : Key -> String
+viewKey { pitchClass, mode } =
+    viewPitchClass pitchClass ++ " " ++ viewMode mode
+
+
+{-| Returns a pairing of a scale-degree to the type of chord at that scale
+degree.
+-}
+practiceChordsForMode : Mode -> Dict ScaleDegree ChordType
+practiceChordsForMode mode =
+    case mode of
+        MajorMode ->
+            Dict.fromList
+                [ ( 1, Major )
+                , ( 2, Minor )
+                , ( 3, Minor )
+                , ( 4, Major )
+                , ( 5, Major )
+                , ( 6, Minor )
+                , ( 7, Diminished )
+                ]
+
+        MinorMode ->
+            Dict.fromList
+                [ ( 1, Minor )
+                , ( 2, Diminished )
+                , ( 3, Major )
+                , ( 4, Minor )
+                , ( 5, Minor )
+                , ( 6, Major )
+                , ( 7, Major )
+                ]
+
+        BluesMode ->
+            Dict.fromList
+                [ ( 1, MajorDominant7 )
+
+                -- While many refer to the blues progression as a I-IV-V, the IV
+                -- chord is really a MajorDominant7 made from the third scale
+                -- degree.
+                , ( 3, MajorDominant7 )
+                , ( 5, MajorDominant7 )
+                ]
+
+
+{-| Returns a list of chords for a particular `key`.
+-}
+chordsForKey : Key -> List Chord
+chordsForKey key =
+    let
+        chords =
+            practiceChordsForMode key.mode
+    in
+    notesForKey key
+        |> List.indexedMap
+            (\i note ->
+                case Dict.get (i + 1) chords of
+                    Nothing ->
+                        Nothing
+
+                    Just chordType ->
+                        Just
+                            (allInversions
+                                |> List.Extra.andThen
+                                    (\inversion ->
+                                        [ { note = note
+                                          , chordType = chordType
+                                          , chordInversion = inversion
+                                          }
+                                        ]
+                                    )
+                            )
+            )
+        |> Maybe.Extra.values
+        |> List.concat
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/UI.elm b/users/wpcarro/website/sandbox/learnpianochords/src/UI.elm
new file mode 100644
index 0000000000..a6876c4f8a
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/UI.elm
@@ -0,0 +1,159 @@
+module UI exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Responsive
+import Tailwind
+
+
+type Color
+    = Primary
+    | Secondary
+
+
+bgForColor : Color -> String
+bgForColor color =
+    case color of
+        Primary ->
+            "bg-gray-600"
+
+        Secondary ->
+            "bg-gray-300"
+
+
+textForColor : Color -> String
+textForColor color =
+    case color of
+        Primary ->
+            "text-white"
+
+        Secondary ->
+            "text-black"
+
+
+simpleButton :
+    { label : String
+    , handleClick : msg
+    , color : Color
+    , classes : List String
+    }
+    -> Html msg
+simpleButton { label, handleClick, color, classes } =
+    let
+        buttonClasses =
+            [ bgForColor color
+            , textForColor color
+            , "py-10"
+            , "lg:py-6"
+            , "px-20"
+            , "lg:px-12"
+            , "rounded-lg"
+            , Responsive.h2
+            ]
+    in
+    button
+        [ class (Tailwind.use <| List.concat [ buttonClasses, classes ])
+        , onClick handleClick
+        ]
+        [ text label ]
+
+
+textToggleButton :
+    { label : String
+    , handleClick : msg
+    , classes : List String
+    , toggled : Bool
+    }
+    -> Html msg
+textToggleButton { label, toggled, handleClick, classes } =
+    let
+        ( textColor, textTreatment ) =
+            if toggled then
+                ( "text-red-600", "underline" )
+
+            else
+                ( "text-black", "no-underline" )
+
+        buttonClasses =
+            [ textColor
+            , textTreatment
+            , "py-8"
+            , "lg:py-5"
+            , "px-10"
+            , "lg:px-6"
+            , Responsive.h2
+            ]
+    in
+    button
+        [ class (Tailwind.use <| List.concat [ buttonClasses, classes ])
+        , onClick handleClick
+        ]
+        [ text label ]
+
+
+textField :
+    { placeholderText : String
+    , handleInput : String -> msg
+    , classes : List String
+    }
+    -> Html msg
+textField { placeholderText, handleInput, classes } =
+    let
+        inputClasses =
+            [ "w-full"
+            , "py-10"
+            , "lg:py-6"
+            , "px-16"
+            , "lg:px-10"
+            , "border"
+            , "rounded-lg"
+            , Responsive.h2
+            ]
+    in
+    input
+        [ class (Tailwind.use <| List.concat [ inputClasses, classes ])
+        , onInput handleInput
+        , placeholder placeholderText
+        ]
+        []
+
+
+overlayButton :
+    { label : String
+    , handleClick : msg
+    , isVisible : Bool
+    }
+    -> Html msg
+overlayButton { label, handleClick, isVisible } =
+    let
+        classes =
+            [ "fixed"
+            , "top-0"
+            , "left-0"
+            , "block"
+            , "z-40"
+            , "w-screen"
+            , "h-screen"
+            , Tailwind.if_ isVisible "opacity-100" "opacity-0"
+            ]
+    in
+    button
+        [ classes |> Tailwind.use |> class
+        , style "background-color" "rgba(0,0,0,1.0)"
+        , onClick handleClick
+        ]
+        [ h1
+            [ style "-webkit-text-stroke-width" "2px"
+            , style "-webkit-text-stroke-color" "black"
+            , class <|
+                Tailwind.use
+                    [ "transform"
+                    , "-rotate-90"
+                    , "text-white"
+                    , "font-mono"
+                    , Responsive.h1
+                    ]
+            ]
+            [ text label ]
+        ]
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/.envrc b/users/wpcarro/website/sandbox/learnpianochords/src/server/.envrc
new file mode 100644
index 0000000000..9e714732fe
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/.envrc
@@ -0,0 +1,7 @@
+source_up
+use_nix
+export SERVER_PORT=3000
+export CLIENT_PORT=8000
+# TODO(wpcarro): Prefer age-nix solution if possible.
+export GOOGLE_CLIENT_ID="$(jq -j '.google | .clientId' < $WPCARRO/secrets.json)"
+export STRIPE_API_KEY="$(jq -j '.stripe | .apiKey' < $WPCARRO/secrets.json)"
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/.ghci b/users/wpcarro/website/sandbox/learnpianochords/src/server/.ghci
new file mode 100644
index 0000000000..151d070ca1
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/.ghci
@@ -0,0 +1,7 @@
+:set prompt "> "
+:set -Wall
+
+:set -XOverloadedStrings
+:set -XNoImplicitPrelude
+:set -XRecordWildCards
+:set -XTypeApplications
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/API.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/API.hs
new file mode 100644
index 0000000000..fe3671e7aa
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/API.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+--------------------------------------------------------------------------------
+module API where
+--------------------------------------------------------------------------------
+import Servant.API
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+type API = "verify"
+           :> ReqBody '[JSON] T.VerifyGoogleSignInRequest
+           :> Post '[JSON] NoContent
+      :<|> "create-payment-intent"
+           :> ReqBody '[JSON] T.PaymentIntent
+           :> Post '[JSON] T.CreatePaymentIntentResponse
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/App.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/App.hs
new file mode 100644
index 0000000000..b7a31457b7
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/App.hs
@@ -0,0 +1,57 @@
+--------------------------------------------------------------------------------
+module App where
+--------------------------------------------------------------------------------
+import RIO hiding (Handler)
+import Servant
+import API
+import Data.String.Conversions (cs)
+import Control.Monad.IO.Class (liftIO)
+import Network.Wai.Middleware.Cors
+import GoogleSignIn (EncodedJWT(..), ValidationResult(..))
+import Utils
+
+import qualified Network.Wai.Handler.Warp as Warp
+import qualified GoogleSignIn
+import qualified Stripe
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+server :: T.Context -> Server API
+server ctx@T.Context{..} = verifyGoogleSignIn
+                      :<|> createPaymentIntent
+  where
+    verifyGoogleSignIn :: T.VerifyGoogleSignInRequest -> Handler NoContent
+    verifyGoogleSignIn T.VerifyGoogleSignInRequest{..} = do
+      validationResult <- liftIO $ GoogleSignIn.validateJWT False (EncodedJWT idToken)
+      case validationResult of
+        Valid _ -> do
+          -- If GoogleLinkedAccounts has email from JWT:
+          --   create a new session for email
+          -- Else:
+          --   Redirect the SPA to the sign-up / payment page
+          pure NoContent
+        err -> do
+          throwError err401 { errBody = err |> GoogleSignIn.explainResult |> cs }
+
+    createPaymentIntent :: T.PaymentIntent -> Handler T.CreatePaymentIntentResponse
+    createPaymentIntent pmt = do
+      clientSecret <- liftIO $ Stripe.createPaymentIntent ctx pmt
+      pure T.CreatePaymentIntentResponse{..}
+
+run :: T.App
+run = do
+  ctx@T.Context{..} <- ask
+  ctx
+    |> server
+    |> serve (Proxy @API)
+    |> cors (const $ Just corsPolicy)
+    |> Warp.run contextServerPort
+    |> liftIO
+  pure $ Right ()
+  where
+    corsPolicy :: CorsResourcePolicy
+    corsPolicy = simpleCorsResourcePolicy
+      { corsOrigins = Just (["http://localhost:8000"], True)
+      , corsMethods = simpleMethods ++ ["PUT", "PATCH", "DELETE", "OPTIONS"]
+      , corsRequestHeaders = simpleHeaders ++ ["Content-Type", "Authorization"]
+      }
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/Fixtures.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/Fixtures.hs
new file mode 100644
index 0000000000..7c153e4228
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/Fixtures.hs
@@ -0,0 +1,67 @@
+--------------------------------------------------------------------------------
+module Fixtures where
+--------------------------------------------------------------------------------
+import RIO
+import Web.JWT
+import Utils
+
+import qualified Data.Map as Map
+import qualified GoogleSignIn
+import qualified TestUtils
+import qualified Data.Time.Clock.POSIX as POSIX
+import qualified System.IO.Unsafe as Unsafe
+--------------------------------------------------------------------------------
+
+-- | These are the JWT fields that I'd like to overwrite in the `googleJWT`
+-- function.
+data JWTFields = JWTFields
+  { overwriteSigner :: Signer
+  , overwriteAuds :: [StringOrURI]
+  , overwriteIss :: StringOrURI
+  , overwriteExp :: NumericDate
+  }
+
+defaultJWTFields :: JWTFields
+defaultJWTFields = do
+  let tenDaysFromToday = POSIX.getPOSIXTime
+                         |> Unsafe.unsafePerformIO
+                         |> (\x -> x * 60 * 60 * 25 * 10)
+                         |> numericDate
+                         |> TestUtils.unsafeJust
+  JWTFields
+    { overwriteSigner = hmacSecret "secret"
+    , overwriteAuds = ["771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com"]
+                      |> fmap TestUtils.unsafeStringOrURI
+    , overwriteIss = TestUtils.unsafeStringOrURI "accounts.google.com"
+    , overwriteExp = tenDaysFromToday
+    }
+
+googleJWT :: JWTFields -> GoogleSignIn.EncodedJWT
+googleJWT JWTFields{..} =
+  encodeSigned signer jwtHeader claimSet
+  |> GoogleSignIn.EncodedJWT
+  where
+    signer :: Signer
+    signer = overwriteSigner
+
+    jwtHeader :: JOSEHeader
+    jwtHeader = JOSEHeader
+      { typ = Just "JWT"
+      , cty = Nothing
+      , alg = Just RS256
+      , kid = Just "f05415b13acb9590f70df862765c655f5a7a019e"
+      }
+
+    claimSet :: JWTClaimsSet
+    claimSet = JWTClaimsSet
+      { iss = Just overwriteIss
+      , sub = stringOrURI "114079822315085727057"
+      , aud = overwriteAuds |> Right |> Just
+      -- TODO: Replace date creation with a human-readable date constructor.
+      , Web.JWT.exp = Just overwriteExp
+      , nbf = Nothing
+      -- TODO: Replace date creation with a human-readable date constructor.
+      , iat = numericDate 1596752853
+      , unregisteredClaims = ClaimsMap (Map.fromList [])
+      , jti = stringOrURI "0d3d7fa1fe05bedec0a91c88294936b2b4d1b13c"
+      }
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/GoogleSignIn.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/GoogleSignIn.hs
new file mode 100644
index 0000000000..dcccadcb70
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/GoogleSignIn.hs
@@ -0,0 +1,111 @@
+--------------------------------------------------------------------------------
+module GoogleSignIn where
+--------------------------------------------------------------------------------
+import RIO
+import Data.String.Conversions (cs)
+import Web.JWT
+import Utils
+
+import qualified Network.HTTP.Simple as HTTP
+import qualified Data.Text as Text
+import qualified Web.JWT as JWT
+import qualified Data.Time.Clock.POSIX as POSIX
+--------------------------------------------------------------------------------
+
+newtype EncodedJWT = EncodedJWT Text
+  deriving (Show)
+
+newtype DecodedJWT = DecodedJWT (JWT UnverifiedJWT)
+  deriving (Show)
+
+instance Eq DecodedJWT where
+  (DecodedJWT _) == (DecodedJWT _) = True
+
+data ValidationResult
+  = Valid DecodedJWT
+  | CannotDecodeJWT
+  | GoogleSaysInvalid Text
+  | NoMatchingClientIDs [StringOrURI]
+  | WrongIssuer StringOrURI
+  | StringOrURIParseFailure Text
+  | TimeConversionFailure
+  | MissingRequiredClaim Text
+  | StaleExpiry NumericDate
+  deriving (Eq, Show)
+
+-- | Returns True when the supplied `jwt` meets the following criteria:
+-- * The token has been signed by Google
+-- * The value of `aud` matches my Google client's ID
+-- * The value of `iss` matches is "accounts.google.com" or
+--   "https://accounts.google.com"
+-- * The `exp` time has not passed
+--
+-- Set `skipHTTP` to `True` to avoid making the network request for testing.
+validateJWT :: Bool
+           -> EncodedJWT
+           -> IO ValidationResult
+validateJWT skipHTTP (EncodedJWT encodedJWT) = do
+  case encodedJWT |> decode of
+    Nothing -> pure CannotDecodeJWT
+    Just jwt -> do
+      if skipHTTP then
+        continue jwt
+      else do
+        let request = "https://oauth2.googleapis.com/tokeninfo"
+                      |> HTTP.setRequestQueryString [ ( "id_token", Just (cs encodedJWT) ) ]
+        res <- HTTP.httpLBS request
+        if HTTP.getResponseStatusCode res /= 200 then
+          pure $ GoogleSaysInvalid (res |> HTTP.getResponseBody |> cs)
+        else
+          continue jwt
+  where
+    continue :: JWT UnverifiedJWT -> IO ValidationResult
+    continue jwt = do
+      let audValues :: [StringOrURI]
+          audValues = jwt |> claims |> auds
+          expectedClientID :: Text
+          expectedClientID = "771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com"
+          expectedIssuers :: [Text]
+          expectedIssuers = [ "accounts.google.com"
+                            , "https://accounts.google.com"
+                            ]
+          mExpectedClientID :: Maybe StringOrURI
+          mExpectedClientID = stringOrURI expectedClientID
+          mExpectedIssuers :: Maybe [StringOrURI]
+          mExpectedIssuers = expectedIssuers |> traverse stringOrURI
+      case (mExpectedClientID, mExpectedIssuers) of
+        (Nothing, _) -> pure $ StringOrURIParseFailure expectedClientID
+        (_, Nothing) -> pure $ StringOrURIParseFailure (Text.unwords expectedIssuers)
+        (Just clientID, Just parsedIssuers) ->
+          -- TODO: Prefer reading clientID from a config. I'm thinking of the
+          -- AppContext type having my Configuration
+          if not $ clientID `elem` audValues then
+            pure $ NoMatchingClientIDs audValues
+          else
+            case (jwt |> claims |> iss, jwt |> claims |> JWT.exp) of
+              (Nothing, _) -> pure $ MissingRequiredClaim "iss"
+              (_, Nothing) -> pure $ MissingRequiredClaim "exp"
+              (Just jwtIssuer, Just jwtExpiry) ->
+                if not $ jwtIssuer `elem` parsedIssuers then
+                  pure $ WrongIssuer jwtIssuer
+                else do
+                  mCurrentTime <- POSIX.getPOSIXTime |> fmap numericDate
+                  case mCurrentTime of
+                    Nothing -> pure TimeConversionFailure
+                    Just currentTime ->
+                      if not $ currentTime <= jwtExpiry then
+                        pure $ StaleExpiry jwtExpiry
+                      else
+                        pure $ jwt |> DecodedJWT |> Valid
+
+-- | Attempt to explain the `ValidationResult` to a human.
+explainResult :: ValidationResult -> String
+explainResult (Valid _) = "Everything appears to be valid"
+explainResult CannotDecodeJWT = "We had difficulty decoding the provided JWT"
+explainResult (GoogleSaysInvalid x) = "After checking with Google, they claimed that the provided JWT was invalid: " ++ cs x
+explainResult (NoMatchingClientIDs audFields) = "None of the values in the `aud` field on the provided JWT match our client ID: " ++ show audFields
+explainResult (WrongIssuer issuer) = "The `iss` field in the provided JWT does not match what we expect: " ++ show issuer
+explainResult (StringOrURIParseFailure x) = "We had difficulty parsing values as URIs" ++ show x
+explainResult TimeConversionFailure = "We had difficulty converting the current time to a value we can use to compare with the JWT's `exp` field"
+explainResult (MissingRequiredClaim claim) = "Your JWT is missing the following claim: " ++ cs claim
+explainResult (StaleExpiry x) = "The `exp` field on your JWT has expired" ++ x |> show |> cs
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/Main.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/Main.hs
new file mode 100644
index 0000000000..228c3363bc
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/Main.hs
@@ -0,0 +1,37 @@
+--------------------------------------------------------------------------------
+module Main where
+--------------------------------------------------------------------------------
+import RIO
+import Prelude (putStr, putStrLn)
+
+import qualified Types as T
+import qualified System.Envy as Envy
+import qualified App
+--------------------------------------------------------------------------------
+
+-- | Attempt to read environment variables from the system and initialize the
+-- Context data type for our application.
+getAppContext :: IO (Either String T.Context)
+getAppContext = do
+  mEnv <- Envy.decodeEnv
+  case mEnv of
+    Left err -> pure $ Left err
+    Right T.Env{..} -> pure $ Right T.Context
+      { contextGoogleClientID = envGoogleClientID
+      , contextStripeAPIKey = envStripeAPIKey
+      , contextServerPort = envServerPort
+      , contextClientPort = envClientPort
+      }
+
+main :: IO ()
+main = do
+  mContext <- getAppContext
+  case mContext of
+    Left err -> putStrLn err
+    Right ctx -> do
+      result <- runRIO ctx App.run
+      case result of
+        Left err -> do
+          putStr "Something went wrong when executing the application: "
+          putStrLn $ show err
+        Right _ -> putStrLn "The application successfully executed."
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/Spec.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/Spec.hs
new file mode 100644
index 0000000000..3c476bbf7b
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/Spec.hs
@@ -0,0 +1,74 @@
+--------------------------------------------------------------------------------
+module Spec where
+--------------------------------------------------------------------------------
+import RIO
+import Test.Hspec
+import Utils
+import Web.JWT (numericDate, decode)
+import GoogleSignIn (EncodedJWT(..), DecodedJWT(..), ValidationResult(..))
+
+import qualified GoogleSignIn
+import qualified Fixtures as F
+import qualified TestUtils
+import qualified Data.Time.Clock.POSIX as POSIX
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = hspec $ do
+  describe "GoogleSignIn" $
+    describe "validateJWT" $ do
+      let validateJWT' = GoogleSignIn.validateJWT True
+      it "returns a decode error when an incorrectly encoded JWT is used" $ do
+        validateJWT' (GoogleSignIn.EncodedJWT "rubbish") `shouldReturn` CannotDecodeJWT
+
+      it "returns validation error when the aud field doesn't match my client ID" $ do
+        let auds = ["wrong-client-id"]
+                   |> fmap TestUtils.unsafeStringOrURI
+            encodedJWT = F.defaultJWTFields { F.overwriteAuds = auds }
+                         |> F.googleJWT
+        validateJWT' encodedJWT `shouldReturn` NoMatchingClientIDs auds
+
+      it "returns validation success when one of the aud fields matches my client ID" $ do
+        let auds = ["wrong-client-id", "771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com"]
+                   |> fmap TestUtils.unsafeStringOrURI
+            encodedJWT@(EncodedJWT jwt) =
+              F.defaultJWTFields { F.overwriteAuds = auds }
+              |> F.googleJWT
+            decodedJWT = jwt |> decode |> TestUtils.unsafeJust |> DecodedJWT
+        validateJWT' encodedJWT `shouldReturn` Valid decodedJWT
+
+      it "returns validation error when one of the iss field doesn't match accounts.google.com or https://accounts.google.com" $ do
+        let erroneousIssuer = TestUtils.unsafeStringOrURI "not-accounts.google.com"
+            encodedJWT = F.defaultJWTFields { F.overwriteIss = erroneousIssuer }
+                         |> F.googleJWT
+        validateJWT' encodedJWT `shouldReturn` WrongIssuer erroneousIssuer
+
+      it "returns validation success when the iss field matches accounts.google.com or https://accounts.google.com" $ do
+        let erroneousIssuer = TestUtils.unsafeStringOrURI "https://accounts.google.com"
+            encodedJWT@(EncodedJWT jwt) =
+              F.defaultJWTFields { F.overwriteIss = erroneousIssuer }
+              |> F.googleJWT
+            decodedJWT = jwt |> decode |> TestUtils.unsafeJust |> DecodedJWT
+        validateJWT' encodedJWT `shouldReturn` Valid decodedJWT
+
+      it "fails validation when the exp field has expired" $ do
+        let mErroneousExp = numericDate 0
+        case mErroneousExp of
+          Nothing -> True `shouldBe` False
+          Just erroneousExp -> do
+            let encodedJWT = F.defaultJWTFields { F.overwriteExp = erroneousExp }
+                             |> F.googleJWT
+            validateJWT' encodedJWT `shouldReturn` StaleExpiry erroneousExp
+
+      it "passes validation when the exp field is current" $ do
+        mFreshExp <- POSIX.getPOSIXTime
+                     |> fmap (\x -> x * 60 * 60 * 24 * 10) -- 10 days later
+                     |> fmap numericDate
+        case mFreshExp of
+          Nothing -> True `shouldBe` False
+          Just freshExp -> do
+            let encodedJWT@(EncodedJWT jwt) =
+                  F.defaultJWTFields { F.overwriteExp = freshExp }
+                  |> F.googleJWT
+                decodedJWT = jwt |> decode |> TestUtils.unsafeJust |> DecodedJWT
+            validateJWT' encodedJWT `shouldReturn` Valid decodedJWT
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/Stripe.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/Stripe.hs
new file mode 100644
index 0000000000..5370b90abe
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/Stripe.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE DataKinds #-}
+--------------------------------------------------------------------------------
+module Stripe where
+--------------------------------------------------------------------------------
+import RIO
+import Prelude (print)
+import Data.String.Conversions (cs)
+import Data.Aeson
+import Network.HTTP.Req
+
+import qualified Types as T
+--------------------------------------------------------------------------------
+
+endpoint :: Text -> Url 'Https
+endpoint slug =
+  https "api.stripe.com" /: "v1" /: slug
+
+post :: (FromJSON b) => Text -> Text -> T.PaymentIntent -> IO (JsonResponse b)
+post apiKey slug T.PaymentIntent{..} = runReq defaultHttpConfig $ do
+  let params = "amount" =: paymentIntentAmount
+            <> "currency" =: paymentIntentCurrency
+  req POST (endpoint slug) (ReqBodyUrlEnc params) jsonResponse (oAuth2Bearer (cs apiKey))
+
+createPaymentIntent :: T.Context -> T.PaymentIntent -> IO T.Secret
+createPaymentIntent T.Context{..} pmtIntent = do
+  res <- post contextStripeAPIKey "payment_intents" pmtIntent
+  let T.StripePaymentIntent{..} = responseBody res :: T.StripePaymentIntent
+  pure pmtIntentClientSecret
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/TestUtils.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/TestUtils.hs
new file mode 100644
index 0000000000..24054bf47a
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/TestUtils.hs
@@ -0,0 +1,17 @@
+--------------------------------------------------------------------------------
+module TestUtils where
+--------------------------------------------------------------------------------
+import RIO
+import Web.JWT
+import Data.String.Conversions (cs)
+--------------------------------------------------------------------------------
+
+unsafeStringOrURI :: String -> StringOrURI
+unsafeStringOrURI x =
+  case stringOrURI (cs x) of
+    Nothing -> error $ "Failed to convert to StringOrURI: " ++ x
+    Just res -> res
+
+unsafeJust :: Maybe a -> a
+unsafeJust Nothing = error "Attempted to force a Nothing to be a something"
+unsafeJust (Just x) = x
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/Types.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/Types.hs
new file mode 100644
index 0000000000..4a72865153
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/Types.hs
@@ -0,0 +1,146 @@
+--------------------------------------------------------------------------------G
+module Types where
+--------------------------------------------------------------------------------
+import RIO
+import Data.Aeson
+import Network.HTTP.Req
+import Web.Internal.HttpApiData (ToHttpApiData(..))
+import System.Envy (FromEnv, fromEnv, env)
+--------------------------------------------------------------------------------
+
+-- | Read from .envrc
+data Env = Env
+  { envGoogleClientID :: !Text
+  , envServerPort :: !Int
+  , envClientPort :: !Int
+  , envStripeAPIKey :: !Text
+  } deriving (Eq, Show)
+
+instance FromEnv Env where
+  fromEnv _ = do
+    envGoogleClientID <- env "GOOGLE_CLIENT_ID"
+    envStripeAPIKey <- env "STRIPE_API_KEY"
+    envServerPort <- env "SERVER_PORT"
+    envClientPort <- env "CLIENT_PORT"
+    pure Env {..}
+
+-- | Application context: a combination of Env and additional values.
+data Context = Context
+  { contextGoogleClientID :: !Text
+  , contextStripeAPIKey :: !Text
+  , contextServerPort :: !Int
+  , contextClientPort :: !Int
+  }
+
+-- | Top-level except for our application, as RIO recommends defining.
+type Failure = ()
+
+-- | When our app executes along the "happy path" this is the type of result it
+-- produces.
+type Success = ()
+
+-- | This is our application monad.
+type AppM = RIO Context
+
+-- | The concrete type of our application.
+type App = AppM (Either Failure Success)
+
+data VerifyGoogleSignInRequest = VerifyGoogleSignInRequest
+  { idToken :: !Text
+  } deriving (Eq, Show)
+
+instance FromJSON VerifyGoogleSignInRequest where
+  parseJSON = withObject "VerifyGoogleSignInRequest" $ \x -> do
+    idToken <- x .: "idToken"
+    pure VerifyGoogleSignInRequest{..}
+
+data GoogleLinkedAccount = GoogleLinkedAccount
+  {
+  -- { googleLinkedAccountUUID :: UUID
+  -- , googleLinkedAccountEmail :: Email
+  -- , googleLinkedAccountTsCreated :: Timestamp
+    googleLinkedAccountGivenName :: !(Maybe Text)
+  , googleLinkedAccountFamilyName :: !(Maybe Text)
+  , googleLinkedAccountFullName :: !(Maybe Text)
+  -- , googleLinkedAccountPictureURL :: URL
+  -- , googleLinkedAccountLocale :: Maybe Locale
+  } deriving (Eq, Show)
+
+data PayingCustomer = PayingCustomer
+  {
+  -- { payingCustomerAccountUUID :: UUID
+  -- , payingCustomerTsCreated :: Timestamp
+  } deriving (Eq, Show)
+
+data Session = Session
+  {
+  -- { sessionUUID :: UUID
+  -- , sessionAccountUUID :: UUID
+  -- , sessionTsCreated :: Timestamp
+  } deriving (Eq, Show)
+
+data CurrencyCode = USD
+  deriving (Eq, Show)
+
+instance ToJSON CurrencyCode where
+  toJSON USD = String "usd"
+
+instance FromJSON CurrencyCode where
+  parseJSON = withText "CurrencyCode" $ \x ->
+    case x of
+      "usd" -> pure USD
+      _ -> fail "Expected a valid currency code like: \"usd\""
+
+instance ToHttpApiData CurrencyCode where
+  toQueryParam USD = "usd"
+
+data PaymentIntent = PaymentIntent
+  { paymentIntentAmount :: !Int
+  , paymentIntentCurrency :: !CurrencyCode
+  } deriving (Eq, Show)
+
+instance ToJSON PaymentIntent where
+  toJSON PaymentIntent{..} =
+    object [ "amount" .= paymentIntentAmount
+           , "currency" .= paymentIntentCurrency
+           ]
+
+instance FromJSON PaymentIntent where
+  parseJSON = withObject "" $ \x -> do
+    paymentIntentAmount <- x .: "amount"
+    paymentIntentCurrency <- x .: "currency"
+    pure PaymentIntent{..}
+
+instance QueryParam PaymentIntent where
+  queryParam = undefined
+
+-- All applications have their secrets... Using the secret type ensures that no
+-- sensitive information will get printed to the screen.
+newtype Secret = Secret Text deriving (Eq)
+
+instance Show Secret where
+  show (Secret _) = "[REDACTED]"
+
+instance ToJSON Secret where
+  toJSON (Secret x) = toJSON x
+
+instance FromJSON Secret where
+  parseJSON = withText "Secret" $ \x -> pure $ Secret x
+
+data CreatePaymentIntentResponse = CreatePaymentIntentResponse
+  { clientSecret :: Secret
+  } deriving (Eq, Show)
+
+instance ToJSON CreatePaymentIntentResponse where
+  toJSON CreatePaymentIntentResponse{..} =
+    object [ "clientSecret" .= clientSecret
+           ]
+
+data StripePaymentIntent = StripePaymentIntent
+  { pmtIntentClientSecret :: Secret
+  } deriving (Eq, Show)
+
+instance FromJSON StripePaymentIntent where
+  parseJSON = withObject "StripeCreatePaymentIntentResponse" $ \x -> do
+    pmtIntentClientSecret <- x .: "client_secret"
+    pure StripePaymentIntent{..}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/Utils.hs b/users/wpcarro/website/sandbox/learnpianochords/src/server/Utils.hs
new file mode 100644
index 0000000000..2f401af2fb
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/Utils.hs
@@ -0,0 +1,8 @@
+--------------------------------------------------------------------------------
+module Utils where
+--------------------------------------------------------------------------------
+import Data.Function ((&))
+--------------------------------------------------------------------------------
+
+(|>) :: a -> (a -> b) -> b
+(|>) = (&)
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/default.nix b/users/wpcarro/website/sandbox/learnpianochords/src/server/default.nix
new file mode 100644
index 0000000000..262693ae82
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/default.nix
@@ -0,0 +1,28 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.program {
+  name = "server";
+  srcs = builtins.path {
+    path = ./.;
+    name = "LearnPianoChords-server-src";
+  };
+  ghcExtensions = [
+    "OverloadedStrings"
+    "NoImplicitPrelude"
+    "RecordWildCards"
+    "TypeApplications"
+  ];
+  deps = hpkgs: with hpkgs; [
+    servant-server
+    aeson
+    wai-cors
+    warp
+    jwt
+    unordered-containers
+    base64
+    http-conduit
+    rio
+    envy
+    req
+  ];
+}
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/index.html b/users/wpcarro/website/sandbox/learnpianochords/src/server/index.html
new file mode 100644
index 0000000000..459a5c8c82
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/index.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Google Sign-in</title>
+    <script src="https://apis.google.com/js/platform.js" async defer></script>
+    <meta name="google-signin-client_id" content="771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com">
+  </head>
+  <body>
+    <div class="g-signin2" data-onsuccess="onSignIn"></div>
+    <a href="#" onclick="signOut();">Sign out</a>
+    <script>
+     function onSignIn(googleUser) {
+       var idToken = googleUser.getAuthResponse().id_token;
+       fetch('http://localhost:3000/verify', {
+         method: 'POST',
+         headers: {
+           'Content-Type': 'application/json',
+         },
+         body: JSON.stringify({
+           idToken: idToken,
+         })
+       })
+         .then(x => console.log(x))
+         .catch(err => console.error(err));
+     }
+     function signOut() {
+       var auth2 = gapi.auth2.getAuthInstance();
+       auth2.signOut().then(function () {
+         console.log('User signed out.');
+       });
+     }
+    </script>
+  </body>
+</html>
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/init.sql b/users/wpcarro/website/sandbox/learnpianochords/src/server/init.sql
new file mode 100644
index 0000000000..c220bd4406
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/init.sql
@@ -0,0 +1,41 @@
+BEGIN TRANSACTION;
+
+DROP TABLE IF EXISTS GoogleLinkedAccounts;
+DROP TABLE IF EXISTS PayingCustomers;
+DROP TABLE IF EXISTS Sessions;
+
+-- Store some of the information that Google provides to us from the JWT.
+CREATE TABLE GoogleLinkedAccounts (
+  accountUUID TEXT CHECK(LENGTH(uuid) == 36) NOT NULL UNIQUE,
+  email TEXT NOT NULL UNIQUE,
+  tsCreated TEXT NOT NULL, -- 'YYYY-MM-DD HH:MM:SS'
+  givenName TEXT,
+  familyName TEXT,
+  fullName TEXT,
+  pictureURL TEXT,
+  locale TEXT,
+  PRIMARY KEY (accountUUID)
+);
+
+-- Track which of our customers have a paid account.
+-- Defines a one-to-one relationship between:
+--   GoogleLinkedAccounts and PayingCustomers
+CREATE TABLE PayingCustomers (
+  accountUUID TEXT,
+  tsCreated TEXT,
+  PRIMARY KEY (accountUUID),
+  FOREIGN KEY (accountUUID) REFERENCES GoogleLinkedAccounts ON DELETE CASCADE
+);
+
+-- Define mobile and web sessions for our users.
+-- Defines a one-to-many relationship between:
+--   GoogleLinkedAccounts and Sessions
+CREATE TABLE Sessions (
+  sessionUUID TEXT CHECK(LENGTH(sessionUUID) == 36) NOT NULL UNIQUE,
+  accountUUID TEXT,
+  tsCreated TEXT NOT NULL, -- 'YYYY-MM-DD HH:MM:SS'
+  PRIMARY KEY (sessionUUID)
+  FOREIGN KEY(accountUUID) REFERENCES GoogleLinkedAccounts ON DELETE CASCADE
+);
+
+COMMIT;
diff --git a/users/wpcarro/website/sandbox/learnpianochords/src/server/shell.nix b/users/wpcarro/website/sandbox/learnpianochords/src/server/shell.nix
new file mode 100644
index 0000000000..6ec8264470
--- /dev/null
+++ b/users/wpcarro/website/sandbox/learnpianochords/src/server/shell.nix
@@ -0,0 +1,18 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: with hpkgs; [
+    hspec
+    servant-server
+    aeson
+    wai-cors
+    warp
+    jwt
+    unordered-containers
+    base64
+    http-conduit
+    rio
+    envy
+    req
+  ];
+}
diff --git a/users/wpcarro/website/sandbox/typo-po/README.md b/users/wpcarro/website/sandbox/typo-po/README.md
new file mode 100644
index 0000000000..9efe53eccd
--- /dev/null
+++ b/users/wpcarro/website/sandbox/typo-po/README.md
@@ -0,0 +1,10 @@
+# Typo-po
+
+Have you ever published a blog post with typos? Or perhaps you've shared a blog
+post draft with a group of friends to solicit their feedback. If anyone reads
+your blog post and finds places where they can correct your typos or suggest
+grammatical improvements they can use typo-po.
+
+## What's with the name?
+
+We police your typos. We prefer po-po to police though. Send us donuts.
diff --git a/users/wpcarro/zoo/.envrc b/users/wpcarro/zoo/.envrc
new file mode 100644
index 0000000000..a4a62da526
--- /dev/null
+++ b/users/wpcarro/zoo/.envrc
@@ -0,0 +1,2 @@
+source_up
+use_nix
diff --git a/users/wpcarro/zoo/.ghci b/users/wpcarro/zoo/.ghci
new file mode 100644
index 0000000000..fcae90c298
--- /dev/null
+++ b/users/wpcarro/zoo/.ghci
@@ -0,0 +1,5 @@
+:set prompt "> "
+:set -Wall
+:set -XOverloadedStrings
+:set -XRecordWildCards
+:set -XTypeApplications
diff --git a/users/wpcarro/zoo/Main.hs b/users/wpcarro/zoo/Main.hs
new file mode 100644
index 0000000000..c18edbed96
--- /dev/null
+++ b/users/wpcarro/zoo/Main.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+--------------------------------------------------------------------------------
+module Main where
+--------------------------------------------------------------------------------
+import RIO hiding (Handler)
+import RIO.Text
+import RIO.Time
+import Servant
+import Data.Time.Clock.POSIX
+import Prelude (read)
+import Text.ParserCombinators.ReadP
+
+import qualified Network.Wai.Handler.Warp as Warp
+--------------------------------------------------------------------------------
+
+type Api = "run"
+           :> QueryParam' '[Required] "offset" Text
+           :> Get '[JSON] UTCTime
+      :<|> "hello"
+           :> QueryParam "name" Text
+           :> Get '[JSON] Text
+
+server :: Server Api
+server = compute :<|> hello
+  where
+    compute :: Text -> Handler UTCTime
+    compute x = do
+      case parseInput x of
+        Nothing -> throwError err401
+        Just req -> do
+          res <- liftIO $ shiftTime req
+          pure res
+    hello :: Maybe Text -> Handler Text
+    hello mName =
+      case mName of
+        Nothing -> pure "Hello, world!"
+        Just name -> pure $ RIO.Text.concat ["Hello, ", name]
+
+data ShiftTimeRequest = ShiftTimeRequest
+  { shiftSeconds :: Int
+  , shiftMinutes :: Int
+  , shiftHours :: Int
+  , shiftDays :: Int
+  , shiftWeeks :: Int
+  , shiftMonths :: Int
+  , shiftQuarters :: Int
+  , shiftYears :: Int
+  } deriving (Eq, Show)
+
+instance Semigroup ShiftTimeRequest where
+  (ShiftTimeRequest as am ah ad aw amonths aq ay) <> (ShiftTimeRequest bs bm bh bd bw bmonths bq by) =
+    ShiftTimeRequest
+    { shiftSeconds = as + bs
+    , shiftMinutes = am + bm
+    , shiftHours = ah + bh
+    , shiftDays = ad + bd
+    , shiftWeeks = aw + bw
+    , shiftMonths = amonths + bmonths
+    , shiftQuarters = aq + bq
+    , shiftYears = ay + by
+    }
+
+instance Monoid ShiftTimeRequest where
+  mempty = defaultShiftTimeRequest
+
+defaultShiftTimeRequest :: ShiftTimeRequest
+defaultShiftTimeRequest = ShiftTimeRequest
+  { shiftSeconds = 0
+  , shiftMinutes = 0
+  , shiftHours = 0
+  , shiftDays = 0
+  , shiftWeeks = 0
+  , shiftMonths = 0
+  , shiftQuarters = 0
+  , shiftYears = 0
+  }
+
+-- This basically broken because it doesn't account for:
+-- Exhales... time stuff
+--   - Leap seconds, leap days, leap years...
+--   - Months like February having 28 days and others having 31
+--   - other things that I'm probably not considering
+toSeconds :: ShiftTimeRequest -> NominalDiffTime
+toSeconds ShiftTimeRequest{..} = do
+  let minutes = 60
+      hours = minutes * 60
+      days = hours * 24
+      weeks = days * 7
+      months = weeks * 4
+      quarters = months * 3
+      years = days * 365
+  fromIntegral $ shiftSeconds +
+    shiftMinutes * minutes +
+    shiftHours * hours +
+    shiftDays * days +
+    shiftWeeks * weeks +
+    shiftMonths * months +
+    shiftQuarters * quarters +
+    shiftYears * years
+
+shiftTime :: ShiftTimeRequest -> IO UTCTime
+shiftTime req = do
+  t <- getPOSIXTime
+  let t' = t + toSeconds req
+  pure $ posixSecondsToUTCTime t'
+
+data Unit = Second
+          | Minute
+          | Hour
+          | Day
+          | Week
+          | Month
+          | Quarter
+          | Year
+  deriving (Eq, Show)
+
+digit :: ReadP Char
+digit =
+  satisfy (\c -> c >= '0' && c <= '9')
+
+unit :: ReadP Unit
+unit = do
+  c <- get
+  case c of
+    's' -> pure Second
+    'm' -> pure Minute
+    'h' -> pure Hour
+    'd' -> pure Day
+    'w' -> pure Week
+    'M' -> pure Month
+    'q' -> pure Quarter
+    'y' -> pure Year
+    _ -> fail $ "We don't support this unit: " ++ show c
+
+request :: ReadP ShiftTimeRequest
+request = do
+  negative <- option Nothing $ fmap Just (satisfy (== '-'))
+  n <- read <$> many1 digit
+  u <- unit
+  let amt = if isJust negative then -1 * n else n
+  case u of
+    Second  -> pure $ defaultShiftTimeRequest { shiftSeconds = amt }
+    Minute  -> pure $ defaultShiftTimeRequest { shiftMinutes = amt }
+    Hour    -> pure $ defaultShiftTimeRequest { shiftHours = amt }
+    Day     -> pure $ defaultShiftTimeRequest { shiftDays = amt }
+    Week    -> pure $ defaultShiftTimeRequest { shiftWeeks = amt }
+    Month   -> pure $ defaultShiftTimeRequest { shiftMonths = amt }
+    Quarter -> pure $ defaultShiftTimeRequest { shiftQuarters = amt }
+    Year    -> pure $ defaultShiftTimeRequest { shiftYears = amt }
+
+parseInput :: Text -> Maybe ShiftTimeRequest
+parseInput x =
+  case readP_to_S (manyTill request eof) (unpack x) of
+    [(xs, "")] -> Just $ mconcat xs
+    _ -> Nothing
+
+main :: IO ()
+main = Warp.run 8000 $ serve (Proxy @Api) server
diff --git a/users/wpcarro/zoo/Spec.hs b/users/wpcarro/zoo/Spec.hs
new file mode 100644
index 0000000000..ba3f71d7c7
--- /dev/null
+++ b/users/wpcarro/zoo/Spec.hs
@@ -0,0 +1,54 @@
+--------------------------------------------------------------------------------
+module Spec where
+--------------------------------------------------------------------------------
+import RIO
+import Test.Hspec
+import Test.QuickCheck
+import Main hiding (main)
+
+import qualified RIO.Text as Text
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = hspec $ do
+  describe "Main" $ do
+    it "handles seconds" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "s"]) ==
+        (Just defaultShiftTimeRequest { shiftSeconds = x })
+
+    it "handles minutes" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "m"]) ==
+        (Just defaultShiftTimeRequest { shiftMinutes = x })
+
+    it "handles hours" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "h"]) ==
+        (Just defaultShiftTimeRequest { shiftHours = x })
+
+    it "handles days" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "d"]) ==
+        (Just defaultShiftTimeRequest { shiftDays = x })
+
+    it "handles weeks" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "w"]) ==
+        (Just defaultShiftTimeRequest { shiftWeeks = x })
+
+    it "handles months" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "M"]) ==
+        (Just defaultShiftTimeRequest { shiftMonths = x })
+
+    it "handles quarters" $ do
+      property $ \x -> parseTime (Text.concat [x & show & Text.pack, "q"]) ==
+        (Just defaultShiftTimeRequest { shiftQuarters = x })
+
+    it "handles multiple shifts" $ do
+      parseTime "1s-20m5h0d-4w100M-3y2q" ==
+        (Just $ ShiftTimeRequest
+          { shiftSeconds = 1
+          , shiftMinutes = -20
+          , shiftHours = 5
+          , shiftDays = 0
+          , shiftWeeks = -4
+          , shiftMonths = 100
+          , shiftQuarters = 2
+          , shiftYears = -3
+          })
diff --git a/users/wpcarro/zoo/default.nix b/users/wpcarro/zoo/default.nix
new file mode 100644
index 0000000000..312a6cbd76
--- /dev/null
+++ b/users/wpcarro/zoo/default.nix
@@ -0,0 +1,21 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.program {
+  name = "zoo";
+  srcs = builtins.path {
+    path = ./.;
+    name = "zoo-src";
+  };
+  ghcExtensions = [
+    "OverloadedStrings"
+    "NoImplicitPrelude"
+    "RecordWildCards"
+    "TypeApplications"
+  ];
+  deps = hpkgs: with hpkgs; [
+    servant-server
+    aeson
+    warp
+    rio
+  ];
+}
diff --git a/users/wpcarro/zoo/shell.nix b/users/wpcarro/zoo/shell.nix
new file mode 100644
index 0000000000..5978d5b4d0
--- /dev/null
+++ b/users/wpcarro/zoo/shell.nix
@@ -0,0 +1,10 @@
+{ depot, ... }:
+
+depot.users.wpcarro.buildHaskell.shell {
+  deps = hpkgs: with hpkgs; [
+    servant-server
+    aeson
+    warp
+    rio
+  ];
+}
diff --git a/users/zseri/.gitignore b/users/zseri/.gitignore
new file mode 100644
index 0000000000..b8553ace55
--- /dev/null
+++ b/users/zseri/.gitignore
@@ -0,0 +1,2 @@
+.#*
+target/
diff --git a/users/zseri/OWNERS b/users/zseri/OWNERS
new file mode 100644
index 0000000000..4f56571214
--- /dev/null
+++ b/users/zseri/OWNERS
@@ -0,0 +1,3 @@
+inherited: false
+owners:
+  - zseri
diff --git a/users/zseri/dbwospof.md b/users/zseri/dbwospof.md
new file mode 100644
index 0000000000..f1d68cde06
--- /dev/null
+++ b/users/zseri/dbwospof.md
@@ -0,0 +1,112 @@
+# distributed build without single points of failure
+
+## problem statement
+> If we want to distribute a build across several build nodes, and want to avoid
+> a "single point of failure", what needs to be considered?
+
+## motivation
+
+* distribute the build across several build nodes, because some packages take
+  extremely long to build
+  (e.g. `firefox`, `thunderbird`, `qtwebengine`, `webkitgtk`, ...)
+* avoid a centralised setup like e.g. with Hydra, because we want to keep using
+  an on-demand workflow as usual with Nix
+  (e.g. `nixos-rebuild` on each host when necessary).
+
+## list of abbreviations
+
+<dl>
+  <dt>CA</dt>		<dd>content-addressed</dd>
+  <dt>drv</dt>		<dd>derivation</dd>
+  <dt>FOD</dt>		<dd>fixed-output derivation</dd>
+  <dt>IHA</dt>		<dd>input-hash-addressed</dd>
+  <dt>inhash</dt>	<dd>input hash</dd>
+  <dt>outhash</dt>	<dd>output hash</dd>
+</dl>
+
+## build graph
+
+The build graph can't be easily distributed. It is instead left on the coordinator,
+and the build nodes just get individual build jobs, which just consist of
+derivations (and some information about how to get the inputs from some central
+or distributed store (e.g. Ceph), this may be transmitted "out of band").
+
+## inhash-exclusive
+
+It is necessary that each derivation build is exclusive in the sense that
+the same derivation is never build multiple times simultaneously, because
+this otherwise either wastes compute resources (obviously) and, in the case
+of non-deterministic builds, increases complexity
+(the store needs to decide which result to prefer, and the build nodes with
+"losing" build results need to pull the "winning" build results from the store,
+replacing the local version). Although this might be unnecessary in case
+of IHA drvs, enforcing it always reduces the amount of possible suprising
+results when mixing CA drvs and IHA drvs.
+
+## what can be meaningfully distributed
+
+The following is strongly opinionated, but I consider the following
+(based upon the original build graph implementation from yzix 12.2021):
+* We can't push the entire build graph to each build node, because they would
+  overlap 100%, and thus create extreme contention on the inhash-exclusive lock
+* We could try to split the build graph into multiple parts with independent
+  inputs (partitioning), but this can be really complex, and I'm not sure
+  if it is worth it... This also basically excludes the yzix node types
+  [ `Eval`, `AssertEqual` ] (should be done by the evaluator).
+  Implementing this option however would make an abort of a build graph
+  (the simple variant does not kill running tasks,
+  just stop new tasks from being scheduled) really hard, and complex to get right.
+* It does not make sense to distribute "node tasks" across build nodes which
+  almost exclusively interact with the store, and are not CPU-bound, but I/O bound.
+  This applies to most, if not all, useful FODs. It applies to the yzix node types
+  [ `Dump`, `UnDump`, `Fetch`, `Require` ] (should be performed by evaluator+store).
+* TODO: figure out how to do forced rebuilds (e.g. also prefer a node which is not
+  the build node of the previous realisation of that task)
+
+## coarse per-derivation workflow
+
+```
+    derivation
+    |        |
+    |        |
+   key     build
+    |        |
+    |        |
+    V        V
+  inhash  outhash
+    |     (either CA or IHA)
+     \      /
+      \    /
+       \  /
+    realisation
+```
+
+## build results
+
+Just for completeness, two build results are currently considered:
+
+* success: the build succeeded, and the result is uploaded to the central store
+* failure: the build failed (e.g. build process terminated via error exit code or was killed)
+* another case might be "partial": the build succeeded, but uploading to the
+  central store failed (the result is only available on the build node that built it).
+  This case is interesting, because we don't need to rerun the build, just the upload step
+  needs to be fixed/done semi-manually (e.g. maybe the central store ran out of storage,
+  or the network was unavailable)
+
+## build task queue
+
+It is naïve to think that something like a queue via `rabbitmq` (`AMQP`) or `MQTT`
+suffices, because some requirements are missing:
+
+1. some way to push build results to the clients, and these should be associated
+  to the build inputs (a hacky way might use multiple queues for that, e.g.
+  a `tasks` input queue and a `done` output queue).
+2. some way to lock "inhashes" (see section inhash-exclusive).
+
+The second point is somewhat easy to realise using `etcd`, and using the `watch`
+mechanism it can be used to simulate a queue, and the inhash-addressing of
+queued derivations can be seamlessly integrated.
+
+TODO: maybe we want to adjust the priorities of tasks in the queue, but Nix currently
+doesn't seem to do this, so consider this only when it starts to make sense as a
+performance or lag optimization.
diff --git a/users/zseri/store-ref-scanner/.gitignore b/users/zseri/store-ref-scanner/.gitignore
new file mode 100644
index 0000000000..5a44eef09a
--- /dev/null
+++ b/users/zseri/store-ref-scanner/.gitignore
@@ -0,0 +1 @@
+/Cargo.lock
diff --git a/users/zseri/store-ref-scanner/Cargo.toml b/users/zseri/store-ref-scanner/Cargo.toml
new file mode 100644
index 0000000000..836d90e883
--- /dev/null
+++ b/users/zseri/store-ref-scanner/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "store-ref-scanner"
+version = "0.1.0"
+description = "scanner/extractor of Nix-like store paths from byte arrays/streams"
+license = "MIT OR Apache-2.0"
+categories = ["no-std", "parsing"]
+edition = "2021"
+homepage = "https://cs.tvl.fyi/depot/-/tree/users/zseri/store-ref-scanner"
+include = ["/src"]
+
+[dependencies]
diff --git a/users/zseri/store-ref-scanner/default.nix b/users/zseri/store-ref-scanner/default.nix
new file mode 100644
index 0000000000..38f3fd64ec
--- /dev/null
+++ b/users/zseri/store-ref-scanner/default.nix
@@ -0,0 +1,49 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  sourceFilter = name: type:
+    let
+      baseName = builtins.baseNameOf (builtins.toString name);
+    in
+    (baseName == "Cargo.toml")
+    || (type == "directory" && baseName == "src")
+    || (lib.hasSuffix ".rs" baseName)
+  ;
+in
+
+pkgs.buildRustCrate rec {
+  pname = "store-ref-scanner";
+  crateName = "store-ref-scanner";
+  version = "0.1.0";
+  edition = "2021";
+  src = lib.cleanSourceWith { filter = sourceFilter; src = ./.; };
+
+  passthru.tests = pkgs.buildRustCrate {
+    pname = "store-ref-scanner-tests";
+    inherit crateName src version edition;
+    buildTests = true;
+    postInstall = ''
+      set -ex
+      export RUST_BACKTRACE=1
+      # recreate a file hierarchy as when running tests with cargo
+      # the source for test data
+      # build outputs
+      testRoot=target/debug
+      mkdir -p $testRoot
+      chmod +w -R .
+      # test harness executables are suffixed with a hash,
+      # like cargo does this allows to prevent name collision
+      # with the main executables of the crate
+      hash=$(basename $out)
+      ls -lasR $out
+      for file in $out/tests/*; do
+        f=$testRoot/$(basename $file)-$hash
+        cp $file $f
+        $f 2>&1 | tee -a $out/tests.log
+      done
+      rm -rf $out/tests
+      set +ex
+    '';
+  };
+
+}
diff --git a/users/zseri/store-ref-scanner/fuzz/.gitignore b/users/zseri/store-ref-scanner/fuzz/.gitignore
new file mode 100644
index 0000000000..b400c27826
--- /dev/null
+++ b/users/zseri/store-ref-scanner/fuzz/.gitignore
@@ -0,0 +1,2 @@
+corpus
+artifacts
diff --git a/users/zseri/store-ref-scanner/fuzz/Cargo.lock b/users/zseri/store-ref-scanner/fuzz/Cargo.lock
new file mode 100644
index 0000000000..7395dec05e
--- /dev/null
+++ b/users/zseri/store-ref-scanner/fuzz/Cargo.lock
@@ -0,0 +1,44 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arbitrary"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "510c76ecefdceada737ea728f4f9a84bd2e1ef29f1ba555e560940fe279954de"
+
+[[package]]
+name = "cc"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36a9a84a6e8b55dfefb04235e55edb2b9a2a18488fcae777a6bdaa6f06f1deb3"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
+
+[[package]]
+name = "store-ref-scanner"
+version = "0.1.0"
+
+[[package]]
+name = "store-ref-scanner-fuzz"
+version = "0.0.0"
+dependencies = [
+ "libfuzzer-sys",
+ "store-ref-scanner",
+]
diff --git a/users/zseri/store-ref-scanner/fuzz/Cargo.toml b/users/zseri/store-ref-scanner/fuzz/Cargo.toml
new file mode 100644
index 0000000000..1832be0032
--- /dev/null
+++ b/users/zseri/store-ref-scanner/fuzz/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "store-ref-scanner-fuzz"
+version = "0.0.0"
+authors = ["Automatically generated"]
+publish = false
+edition = "2018"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+
+[dependencies.store-ref-scanner]
+path = ".."
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[[bin]]
+name = "hbm-roundtrip"
+path = "fuzz_targets/hbm-roundtrip.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "nocrash"
+path = "fuzz_targets/nocrash.rs"
+test = false
+doc = false
+
+[profile.release]
+incremental = false
+overflow-checks = true
+panic = "abort"
diff --git a/users/zseri/store-ref-scanner/fuzz/fuzz_targets/hbm-roundtrip.rs b/users/zseri/store-ref-scanner/fuzz/fuzz_targets/hbm-roundtrip.rs
new file mode 100644
index 0000000000..9e21a7738a
--- /dev/null
+++ b/users/zseri/store-ref-scanner/fuzz/fuzz_targets/hbm-roundtrip.rs
@@ -0,0 +1,10 @@
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+fuzz_target!(|data: [u8; 16]| {
+    use store_ref_scanner::HalfBytesMask;
+    let a = HalfBytesMask(data);
+    let b = a.into_expanded();
+    let c = HalfBytesMask::from_expanded(b);
+    assert_eq!(a, c);
+});
diff --git a/users/zseri/store-ref-scanner/fuzz/fuzz_targets/nocrash.rs b/users/zseri/store-ref-scanner/fuzz/fuzz_targets/nocrash.rs
new file mode 100644
index 0000000000..48100a628d
--- /dev/null
+++ b/users/zseri/store-ref-scanner/fuzz/fuzz_targets/nocrash.rs
@@ -0,0 +1,9 @@
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+fuzz_target!(|data: &[u8]| {
+    use store_ref_scanner::{StoreRefScanner, StoreSpec};
+
+    StoreRefScanner::new(&data[..], &StoreSpec::DFL_NIX2).count();
+    StoreRefScanner::new(&data[..], &StoreSpec::DFL_YZIX1).count();
+});
diff --git a/users/zseri/store-ref-scanner/src/hbm.rs b/users/zseri/store-ref-scanner/src/hbm.rs
new file mode 100644
index 0000000000..2520efd836
--- /dev/null
+++ b/users/zseri/store-ref-scanner/src/hbm.rs
@@ -0,0 +1,167 @@
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct HalfBytesMask(pub [u8; 16]);
+
+#[allow(clippy::as_conversions, clippy::zero_prefixed_literal)]
+impl HalfBytesMask {
+    pub const B32_REVSHA256: HalfBytesMask =
+        HalfBytesMask([0, 0, 0, 0, 0, 0, 255, 3, 0, 0, 0, 0, 222, 127, 207, 7]);
+
+    pub const B64_BLAKE2B256: HalfBytesMask = HalfBytesMask([
+        0, 0, 0, 0, 0, 8, 255, 3, 254, 255, 255, 135, 254, 255, 255, 7,
+    ]);
+
+    pub const DFL_REST: HalfBytesMask = HalfBytesMask([
+        0, 0, 0, 0, 0, 104, 255, 163, 254, 255, 255, 135, 254, 255, 255, 7,
+    ]);
+
+    #[inline]
+    pub const fn from_expanded(x: [bool; 128]) -> Self {
+        let mut ret = [0u8; 16];
+        let mut idx = 0;
+        while idx < 16 {
+            let fin = idx * 8;
+            let mut idx2 = 0;
+            while idx2 < 8 {
+                if x[fin + idx2] {
+                    ret[idx] += (1 << idx2) as u8;
+                }
+                idx2 += 1;
+            }
+            idx += 1;
+        }
+        Self(ret)
+    }
+
+    /// create a mask by allowing all characters via the mask which are included in the given string
+    pub fn from_bytes(s: &[u8]) -> Self {
+        s.iter().fold(Self([0u8; 16]), |mut ret, &i| {
+            ret.set(i, true);
+            ret
+        })
+    }
+
+    pub const fn into_expanded(self) -> [bool; 128] {
+        let Self(ihbm) = self;
+        let mut ret = [false; 128];
+        let mut idx = 0;
+        while idx < 16 {
+            let fin = idx * 8;
+            let curi = ihbm[idx];
+            let mut idx2 = 0;
+            while idx2 < 8 {
+                ret[fin + idx2] = (curi >> idx2) & 0b1 != 0;
+                idx2 += 1;
+            }
+            idx += 1;
+        }
+        ret
+    }
+
+    pub fn contains(&self, byte: u8) -> bool {
+        if byte >= 0x80 {
+            false
+        } else {
+            (self.0[usize::from(byte / 8)] >> u32::from(byte % 8)) & 0b1 != 0
+        }
+    }
+
+    pub fn set(&mut self, byte: u8, allow: bool) {
+        if byte >= 0x80 {
+            if cfg!(debug_assertions) {
+                panic!(
+                    "tried to manipulate invalid byte {:?} in HalfBytesMask",
+                    byte
+                );
+            } else {
+                return;
+            }
+        }
+        let block = &mut self.0[usize::from(byte / 8)];
+        let bitpat = (1 << u32::from(byte % 8)) as u8;
+        if allow {
+            *block |= bitpat;
+        } else {
+            *block &= !bitpat;
+        }
+    }
+
+    #[cfg(test)]
+    fn count_ones(&self) -> u8 {
+        self.0
+            .iter()
+            .map(|i| i.count_ones())
+            .sum::<u32>()
+            .try_into()
+            .unwrap()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn maskbase() {
+        assert_eq!(HalfBytesMask::B32_REVSHA256.count_ones(), 32);
+        assert_eq!(HalfBytesMask::B64_BLAKE2B256.count_ones(), 64);
+    }
+
+    #[test]
+    fn non_ascii() {
+        for i in 0x80..=0xff {
+            assert!(!HalfBytesMask::DFL_REST.contains(i));
+        }
+    }
+
+    #[test]
+    fn dflmask() {
+        assert_eq!(
+            HalfBytesMask::from_expanded(
+                [
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                ]
+                .map(|i| i != 0)
+            ),
+            Default::default(),
+        );
+
+        assert_eq!(
+            HalfBytesMask::from_expanded(
+                [
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
+                    1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1,
+                    1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+                ]
+                .map(|i| i != 0)
+            ),
+            HalfBytesMask::B32_REVSHA256,
+        );
+
+        assert_eq!(
+            HalfBytesMask::from_expanded(
+                [
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1,
+                    1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1,
+                    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+                ]
+                .map(|i| i != 0)
+            ),
+            HalfBytesMask::B64_BLAKE2B256,
+        );
+
+        assert_eq!(
+            HalfBytesMask::from_bytes(
+                b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-._?="
+            ),
+            HalfBytesMask::DFL_REST,
+        );
+    }
+}
diff --git a/users/zseri/store-ref-scanner/src/lib.rs b/users/zseri/store-ref-scanner/src/lib.rs
new file mode 100644
index 0000000000..0f86a769fe
--- /dev/null
+++ b/users/zseri/store-ref-scanner/src/lib.rs
@@ -0,0 +1,215 @@
+#![no_std]
+#![forbid(clippy::cast_ptr_alignment, trivial_casts, unconditional_recursion)]
+#![deny(clippy::as_conversions)]
+
+mod hbm;
+pub use hbm::HalfBytesMask;
+
+mod spec;
+pub use spec::*;
+
+/// limit maximal length of store basename
+const BASENAME_MAXLEN: usize = 255;
+
+/// this is a trait which implements the interface of possible inputs
+/// (usually byte slices)
+pub trait ScannerInput: AsRef<[u8]> + Sized {
+    /// Splits the input into two at the given index.
+    /// Afterwards self contains elements [at, len), and the returned input part contains elements [0, at).
+    fn split_to(&mut self, at: usize) -> Self;
+    fn finish(&mut self);
+}
+
+impl ScannerInput for &[u8] {
+    fn split_to(&mut self, at: usize) -> Self {
+        let (a, b) = self.split_at(at);
+        *self = b;
+        a
+    }
+
+    fn finish(&mut self) {
+        *self = &[];
+    }
+}
+
+impl ScannerInput for &mut [u8] {
+    fn split_to(&mut self, at: usize) -> Self {
+        // Lifetime dance taken from `impl Write for &mut [u8]`.
+        // Taken from crate `std`.
+        let (a, b) = core::mem::take(self).split_at_mut(at);
+        *self = b;
+        a
+    }
+
+    fn finish(&mut self) {
+        *self = &mut [];
+    }
+}
+
+/// this is the primary structure of this crate
+///
+/// it represents a scanner which scans binary slices for store references,
+/// and implements an iterator interfaces which returns these as byte slices.
+pub struct StoreRefScanner<'x, Input: 'x> {
+    input: Input,
+    spec: &'x StoreSpec<'x>,
+}
+
+impl<'x, Input> StoreRefScanner<'x, Input>
+where
+    Input: ScannerInput + 'x,
+{
+    pub fn new(input: Input, spec: &'x StoreSpec<'x>) -> Self {
+        for i in [&spec.valid_hashbytes, &spec.valid_restbytes] {
+            for j in [b'\0', b' ', b'\t', b'\n', b'/', b'\\'] {
+                assert!(!i.contains(j));
+            }
+        }
+        Self { input, spec }
+    }
+}
+
+impl<'x, Input: 'x> Iterator for StoreRefScanner<'x, Input>
+where
+    Input: ScannerInput + 'x,
+{
+    type Item = Input;
+
+    fn next(&mut self) -> Option<Input> {
+        let hbl: usize = self.spec.hashbytes_len.into();
+        'outer: while !self.input.as_ref().is_empty() {
+            if !self.spec.path_to_store.is_empty() {
+                let p2sas = self.spec.path_to_store;
+                while !self.input.as_ref().starts_with(p2sas.as_bytes()) {
+                    if self.input.as_ref().is_empty() {
+                        break 'outer;
+                    }
+                    self.input.split_to(1);
+                }
+                self.input.split_to(p2sas.len());
+                if self.input.as_ref().is_empty() {
+                    break 'outer;
+                }
+            }
+            let hsep = matches!(self.input.as_ref().iter().next(), Some(b'/') | Some(b'\\'));
+            self.input.split_to(1);
+            if hsep && self.spec.check_rest(self.input.as_ref()) {
+                // we have found a valid hash
+                // rest contains the store basename and all following components
+                // now let's search for the end
+                // and then cut off possible following components after the basename
+                let rlen = self
+                    .input
+                    .as_ref()
+                    .iter()
+                    .enumerate()
+                    .take(BASENAME_MAXLEN)
+                    .skip(hbl)
+                    .find(|&(_, &i)| !self.spec.valid_restbytes.contains(i))
+                    .map(|(eosp, _)| eosp)
+                    .unwrap_or_else(|| core::cmp::min(BASENAME_MAXLEN, self.input.as_ref().len()));
+                return Some(self.input.split_to(rlen));
+            }
+        }
+        self.input.finish();
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    extern crate alloc;
+    use alloc::{vec, vec::Vec};
+
+    #[test]
+    fn simple_nix2() {
+        let drv: &[u8] = br#"
+            Derive([("out","","r:sha256","")],[("/nix/store/2ax7bvjdfkzim69q957i0jlg0nvmapg0-util-linux-2.37.2.drv",["dev"]),("/nix/store/6b55ssmh8pzqsc4q4kw1yl3kqvr4fvqj-bash-5.1-p12.drv",["out"]),("/nix/store/fp2vx24kczlzv84avds28wyzsmrn8kyv-source.drv",["out"]),("/nix/store/s6c2lm5hpsvdwnxq9y1g3ngncghjzc3k-stdenv-linux.drv",["out"]),("/nix/store/xlnzpf4mzghi8vl0krabrgcbnqk5qjf3-pkg-config-wrapper-0.29.2.drv",["out"])],["/nix/store/03sl46khd8gmjpsad7223m32ma965vy9-fix-static.patch","/nix/store/2q3z7587yhlz0i2xvfvvap42zk5carlv-bcache-udev-modern.patch","/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],"x86_64-linux","/0g15yibzzi3rmw29gqlbms05x9dbghbvh61v1qggydvmzh3bginw/bin/bash",["-e","/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],[("buildInputs","/0sdk1r4l43yw4g6lmqdhd92vhdfhlwz3m76jxzvzsqsv63czw2km"),("builder","/0g15yibzzi3rmw29gqlbms05x9dbghbvh61v1qggydvmzh3bginw/bin/bash"),("configureFlags",""),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("doCheck",""),("doInstallCheck",""),("makeFlags","PREFIX=/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9 UDEVLIBDIR=/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9/lib/udev/"),("name","bcache-tools-1.0.7"),("nativeBuildInputs","/1kw0rwgdyq9q69wmmsa5d2kap6p52b0yldbzi4w17bhcq5g5cp2f"),("out","/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"),("outputHashAlgo","sha256"),("outputHashMode","recursive"),("outputs","out"),("patches","/nix/store/2q3z7587yhlz0i2xvfvvap42zk5carlv-bcache-udev-modern.patch /nix/store/03sl46khd8gmjpsad7223m32ma965vy9-fix-static.patch"),("pname","bcache-tools"),("preBuild","sed -e \"s|/bin/sh|/0g15yibzzi3rmw29gqlbms05x9dbghbvh61v1qggydvmzh3bginw/bin/sh|\" -i *.rules\n"),("preInstall","mkdir -p \"$out/sbin\" \"$out/lib/udev/rules.d\" \"$out/share/man/man8\"\n"),("prePatch","sed -e \"/INSTALL.*initramfs\\/hook/d\" \\\n    -e \"/INSTALL.*initcpio\\/install/d\" \\\n    -e \"/INSTALL.*dracut\\/module-setup.sh/d\" \\\n    -e \"s/pkg-config/$PKG_CONFIG/\" \\\n    -i Makefile\n"),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("src","/nix/store/6izcafvfcbz19chi7hl20834g0fa043n-source"),("stdenv","/01ncyv8bxibj0imgfvmxgqy648n697bachil6aw6i46g1jk0bbds"),("strictDeps",""),("system","x86_64-linux"),("version","1.0.7")])
+        "#;
+        // we convert everything into strings because it is way easier to compare elements in error messages
+        let refs: Vec<&str> = StoreRefScanner::new(drv, &StoreSpec::DFL_NIX2)
+            .map(|i| core::str::from_utf8(i).unwrap())
+            .collect();
+        let refs_expect: Vec<&[u8]> = vec![
+            b"2ax7bvjdfkzim69q957i0jlg0nvmapg0-util-linux-2.37.2.drv",
+            b"6b55ssmh8pzqsc4q4kw1yl3kqvr4fvqj-bash-5.1-p12.drv",
+            b"fp2vx24kczlzv84avds28wyzsmrn8kyv-source.drv",
+            b"s6c2lm5hpsvdwnxq9y1g3ngncghjzc3k-stdenv-linux.drv",
+            b"xlnzpf4mzghi8vl0krabrgcbnqk5qjf3-pkg-config-wrapper-0.29.2.drv",
+            b"03sl46khd8gmjpsad7223m32ma965vy9-fix-static.patch",
+            b"2q3z7587yhlz0i2xvfvvap42zk5carlv-bcache-udev-modern.patch",
+            b"9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh",
+            b"9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh",
+            b"2q3z7587yhlz0i2xvfvvap42zk5carlv-bcache-udev-modern.patch",
+            b"03sl46khd8gmjpsad7223m32ma965vy9-fix-static.patch",
+            b"6izcafvfcbz19chi7hl20834g0fa043n-source",
+        ];
+        let refs_expect: Vec<&str> = refs_expect
+            .into_iter()
+            .map(|i| core::str::from_utf8(i).unwrap())
+            .collect();
+        assert_eq!(refs, refs_expect);
+    }
+
+    #[test]
+    fn simple_yzix1() {
+        // I haven't yet produced any yzix derivation which included /yzixs absolute paths...
+        let fake: &[u8] = br#"
+            /yzixs/4Zx1PBoft1YyAuKdhjAY1seZFHloxQ+8voHQRkRMuys:         ASCII text
+            /yzixs/dNE3yogD4JHKHzNa2t3jQMZddT8wjqlMDB0naDIFo0A:         ASCII text
+            /yzixs/FMluSVOHLc4bxX7F4lBCXafNljBnDn+rAM5HzG7k8LI:         unified diff output, ASCII text
+            /yzixs/g2G3GRL87hGEdw9cq2BZWqDQP_HeHSPRLbJ9P9KH+HI:         unified diff output, ASCII text
+            /yzixs/H08Av1ZAONwFdzVLpFQm0Sc0dvyk0sbnk82waoBig7I:         ASCII text
+            /yzixs/IndARQp+gaGDLS3K+PeyXdaRqAcCyS3EIbRXkkYjC94:         unified diff output, ASCII text
+            /yzixs/IrLPnbkEolTAuWRxkXpuvVs6Imb1iB6wUJcI+fxWwkU:         POSIX shell script, ASCII text executable
+            /yzixs/JsS_H3n3TSh2R6fiIzgOPZdjSmRkV71vGxstJJKPmr4:         unified diff output, ASCII text
+            /yzixs/LZ6pQh1x8DRxZ2IYzetBRS4LuE__IXFjpOfQPxHVwpw:         unified diff output, ASCII text
+            /yzixs/mEi2RPep9daRs0JUvwt1JsDfgYSph5sH_+_ihwn8IGQ:         ASCII text
+            /yzixs/nd4DyljinP3auDMHL_LrpsRJkWQpSHQK2jqtyyzWcBA:         POSIX shell script, ASCII text executable
+            /yzixs/nzpaknF0_ONSHtd0i_e1E3pkLF1QPeJQhAB7x9Ogo_M:         unified diff output, ASCII text
+            /yzixs/UZ3uzVUUMC1gKGLw6tg_aLFwoFrJedXB3xbhEgQOaiY:         unified diff output, ASCII text
+            /yzixs/VKyXxKTXsDGxYJ24YgbvCc1bZkA5twp3TC+Gbi4Kwd8:         unified diff output, ASCII text
+            /yzixs/VPJMl8O1xkc1LsJznpoQrCrQO0Iy+ODCPsgoUBLiRZc:         unified diff output, ASCII text
+            /yzixs/W6r1ow001ASHRj+gtRfyj9Fb_gCO_pBztX8WhYXVdIc:         unified diff output, ASCII text
+            /yzixs/xvwEcXIob_rQynUEtQiQbwaDXEobTVKEGaBMir9oH9k:         unified diff output, ASCII text
+            /yzixs/ZPvQbRJrtyeSITvW3FUZvw99hhNOO3CFqGgmWgScxcg:         ASCII text
+        "#;
+        let refs: Vec<&str> = StoreRefScanner::new(fake, &StoreSpec::DFL_YZIX1)
+            .map(|i| core::str::from_utf8(i).unwrap())
+            .collect();
+        let refs_expect: Vec<&[u8]> = vec![
+            b"4Zx1PBoft1YyAuKdhjAY1seZFHloxQ+8voHQRkRMuys",
+            b"dNE3yogD4JHKHzNa2t3jQMZddT8wjqlMDB0naDIFo0A",
+            b"FMluSVOHLc4bxX7F4lBCXafNljBnDn+rAM5HzG7k8LI",
+            b"g2G3GRL87hGEdw9cq2BZWqDQP_HeHSPRLbJ9P9KH+HI",
+            b"H08Av1ZAONwFdzVLpFQm0Sc0dvyk0sbnk82waoBig7I",
+            b"IndARQp+gaGDLS3K+PeyXdaRqAcCyS3EIbRXkkYjC94",
+            b"IrLPnbkEolTAuWRxkXpuvVs6Imb1iB6wUJcI+fxWwkU",
+            b"JsS_H3n3TSh2R6fiIzgOPZdjSmRkV71vGxstJJKPmr4",
+            b"LZ6pQh1x8DRxZ2IYzetBRS4LuE__IXFjpOfQPxHVwpw",
+            b"mEi2RPep9daRs0JUvwt1JsDfgYSph5sH_+_ihwn8IGQ",
+            b"nd4DyljinP3auDMHL_LrpsRJkWQpSHQK2jqtyyzWcBA",
+            b"nzpaknF0_ONSHtd0i_e1E3pkLF1QPeJQhAB7x9Ogo_M",
+            b"UZ3uzVUUMC1gKGLw6tg_aLFwoFrJedXB3xbhEgQOaiY",
+            b"VKyXxKTXsDGxYJ24YgbvCc1bZkA5twp3TC+Gbi4Kwd8",
+            b"VPJMl8O1xkc1LsJznpoQrCrQO0Iy+ODCPsgoUBLiRZc",
+            b"W6r1ow001ASHRj+gtRfyj9Fb_gCO_pBztX8WhYXVdIc",
+            b"xvwEcXIob_rQynUEtQiQbwaDXEobTVKEGaBMir9oH9k",
+            b"ZPvQbRJrtyeSITvW3FUZvw99hhNOO3CFqGgmWgScxcg",
+        ];
+        let refs_expect: Vec<&str> = refs_expect
+            .into_iter()
+            .map(|i| core::str::from_utf8(i).unwrap())
+            .collect();
+        assert_eq!(refs, refs_expect);
+    }
+
+    #[test]
+    fn just_store() {
+        for i in [&StoreSpec::DFL_NIX2, &StoreSpec::DFL_YZIX1] {
+            let refs: Vec<&[u8]> = StoreRefScanner::new(i.path_to_store.as_bytes(), i).collect();
+            assert!(refs.is_empty());
+        }
+    }
+}
diff --git a/users/zseri/store-ref-scanner/src/spec.rs b/users/zseri/store-ref-scanner/src/spec.rs
new file mode 100644
index 0000000000..79da0842c5
--- /dev/null
+++ b/users/zseri/store-ref-scanner/src/spec.rs
@@ -0,0 +1,40 @@
+use crate::hbm::HalfBytesMask;
+
+pub struct StoreSpec<'path> {
+    /// path to store without trailing slash
+    pub path_to_store: &'path str,
+
+    /// compressed map of allowed ASCII characters in hash part
+    pub valid_hashbytes: HalfBytesMask,
+
+    /// compressed map of allowed ASCII characters in part after hash
+    pub valid_restbytes: HalfBytesMask,
+
+    /// exact length of hash part of store paths
+    pub hashbytes_len: u8,
+}
+
+impl StoreSpec<'_> {
+    pub(crate) fn check_rest(&self, rest: &[u8]) -> bool {
+        let hbl = self.hashbytes_len.into();
+        rest.iter()
+            .take(hbl)
+            .take_while(|&&i| self.valid_hashbytes.contains(i))
+            .count()
+            == hbl
+    }
+
+    pub const DFL_NIX2: StoreSpec<'static> = StoreSpec {
+        path_to_store: "/nix/store",
+        valid_hashbytes: HalfBytesMask::B32_REVSHA256,
+        valid_restbytes: HalfBytesMask::DFL_REST,
+        hashbytes_len: 32,
+    };
+
+    pub const DFL_YZIX1: StoreSpec<'static> = StoreSpec {
+        path_to_store: "/yzixs",
+        valid_hashbytes: HalfBytesMask::B64_BLAKE2B256,
+        valid_restbytes: HalfBytesMask::DFL_REST,
+        hashbytes_len: 43,
+    };
+}
diff --git a/views/.skip-subtree b/views/.skip-subtree
new file mode 100644
index 0000000000..d33c73476b
--- /dev/null
+++ b/views/.skip-subtree
@@ -0,0 +1,2 @@
+//views is not part of the depot build tree, see the README for more
+information.
diff --git a/views/README.md b/views/README.md
new file mode 100644
index 0000000000..83464d5ee2
--- /dev/null
+++ b/views/README.md
@@ -0,0 +1,6 @@
+depot views
+===========
+
+This folder contains external views of depot content, defined using
+josh workspaces. See the individual views for a description of their
+content and usage information.
diff --git a/views/kit/README.md b/views/kit/README.md
new file mode 100644
index 0000000000..c7319ef419
--- /dev/null
+++ b/views/kit/README.md
@@ -0,0 +1,25 @@
+The TVL Kit
+===========
+
+[![Build status](https://badge.buildkite.com/cd7240a881c7e77c3ed8cc040f81734623f57038563b37213d.svg?branch=canon)](https://buildkite.com/tvl/tvl-kit)
+
+This folder contains a publicly available version of the core TVL
+tooling, currently comprising of:
+
+* `buildkite`: TVL tooling for dynamically generating Buildkite
+  pipelines with Nix.
+* `buildGo`: Nix-based build system for Go.
+* `readTree`: Nix library to dynamically compute attribute trees
+  corresponding to the physical layout of a repository.
+* `besadii`: Configurable Gerrit/Buildkite integration hook.
+* `magrathea`: Command-line tool for working with TVL-style monorepos
+
+It can be accessed via git by cloning it as such:
+
+    git clone https://code.tvl.fyi/depot.git:workspace=views/kit.git tvl-kit
+
+If you are looking at this within the TVL depot, you can see the
+[josh][] configuration in `workspace.josh`. You will find the projects
+at slightly different paths within the depot.
+
+[josh]: https://github.com/josh-project/josh/
diff --git a/views/kit/buildkite.yml b/views/kit/buildkite.yml
new file mode 100644
index 0000000000..0bba63726d
--- /dev/null
+++ b/views/kit/buildkite.yml
@@ -0,0 +1,16 @@
+# Build pipeline for the filtered //views/kit workspace of depot. This
+# pipeline is triggered by each build of canon.
+#
+# Pipeline status is visible on https://buildkite.com/tvl/tvl-kit
+
+steps:
+  - command: "nix-build --no-out-link -A besadii"
+    label: ":nix: besadii"
+
+  - command: "nix-build --no-out-link -A magrathea"
+    label: ":nix: magrathea"
+
+  - label: ":nix: lazy-deps"
+    command: |
+      nix-build -E 'with import ./. {}; lazy-deps { mg.attr = "magrathea"; }'
+      result/bin/mg
diff --git a/views/kit/default.nix b/views/kit/default.nix
new file mode 100644
index 0000000000..47c6088a46
--- /dev/null
+++ b/views/kit/default.nix
@@ -0,0 +1,31 @@
+# Externally importable TVL depot stack. This is intended to be called
+# with a supplied package set, otherwise the package set currently in
+# use by the TVL depot will be used.
+#
+# For now, readTree is not used inside of this configuration to keep
+# it simple. Adding it may be useful if we set up test scaffolding
+# around the exported workspace.
+
+{ pkgs ? (import ./nixpkgs {
+    depotOverlays = false;
+    depot.third_party.sources = import ./sources { };
+  })
+, ...
+}:
+
+pkgs.lib.fix (self: {
+  besadii = import ./besadii {
+    depot.nix.buildGo = self.buildGo;
+  };
+
+  buildGo = import ./buildGo { inherit pkgs; };
+
+  buildkite = import ./buildkite {
+    inherit pkgs;
+    depot.nix.readTree = self.readTree;
+  };
+
+  lazy-deps = import ./lazy-deps { inherit pkgs; };
+  magrathea = import ./magrathea { inherit pkgs; };
+  readTree = import ./readTree { };
+})
diff --git a/views/kit/workspace.josh b/views/kit/workspace.josh
new file mode 100644
index 0000000000..bdf5503397
--- /dev/null
+++ b/views/kit/workspace.josh
@@ -0,0 +1,13 @@
+::LICENSE
+besadii = :/ops/besadii
+:/nix:[
+    ::buildGo/
+    ::buildkite/
+    ::lazy-deps/
+    ::readTree/
+]
+:/third_party:[
+    ::nixpkgs/
+    ::sources/
+]
+magrathea = :/tools/magrathea
diff --git a/web/atom-feed/default.nix b/web/atom-feed/default.nix
new file mode 100644
index 0000000000..fca69e20fa
--- /dev/null
+++ b/web/atom-feed/default.nix
@@ -0,0 +1,153 @@
+# This file defines functions for generating an Atom feed.
+
+{ depot, lib, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) foldl' map readFile replaceStrings sort;
+  inherit (lib) concatStrings concatStringsSep max removeSuffix;
+  inherit (pkgs) runCommandNoCC;
+
+  # 'link' describes a related link to a feed, or feed element.
+  #
+  # https://validator.w3.org/feed/docs/atom.html#link
+  link = struct "link" {
+    rel = string;
+    href = string;
+  };
+
+  # 'entry' describes a feed entry, for example a single post on a
+  # blog. Some optional fields have been omitted.
+  #
+  # https://validator.w3.org/feed/docs/atom.html#requiredEntryElements
+  entry = struct "entry" {
+    # Identifies the entry using a universally unique and permanent URI.
+    id = string;
+
+    # Contains a human readable title for the entry. This value should
+    # not be blank.
+    title = string;
+
+    # Content of the entry.
+    content = option string;
+
+    # Indicates the last time the entry was modified in a significant
+    # way (in seconds since epoch).
+    updated = int;
+
+    # Names authors of the entry. Recommended element.
+    authors = option (list string);
+
+    # Related web pages, such as the web location of a blog post.
+    links = option (list link);
+
+    # Conveys a short summary, abstract, or excerpt of the entry.
+    summary = option string;
+
+    # Contains the time of the initial creation or first availability
+    # of the entry.
+    published = option int;
+
+    # Conveys information about rights, e.g. copyrights, held in and
+    # over the entry.
+    rights = option string;
+  };
+
+  # 'feed' describes the metadata of the Atom feed itself.
+  #
+  # Some optional fields have been omitted.
+  #
+  # https://validator.w3.org/feed/docs/atom.html#requiredFeedElements
+  feed = struct "feed" {
+    # Identifies the feed using a universally unique and permanent URI.
+    id = string;
+
+    # Contains a human readable title for the feed.
+    title = string;
+
+    # Indicates the last time the feed was modified in a significant
+    # way (in seconds since epoch). Will be calculated based on most
+    # recently updated entry if unset.
+    updated = option int;
+
+    # Entries contained within the feed.
+    entries = list entry;
+
+    # Names authors of the feed. Recommended element.
+    authors = option (list string);
+
+    # Related web locations. Recommended element.
+    links = option (list link);
+
+    # Conveys information about rights, e.g. copyrights, held in and
+    # over the feed.
+    rights = option string;
+
+    # Contains a human-readable description or subtitle for the feed.
+    subtitle = option string;
+  };
+
+  # Feed generation functions:
+
+  renderEpoch = epoch: removeSuffix "\n" (readFile (runCommandNoCC "date-${toString epoch}" { } ''
+    date --date='@${toString epoch}' --utc --iso-8601='seconds' > $out
+  ''));
+
+  escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
+
+  elem = name: content: ''<${name}>${escape content}</${name}>'';
+
+  renderLink = defun [ link string ] (l: ''
+    <link href="${escape l.href}" rel="${escape l.rel}" />
+  '');
+
+  # Technically the author element can also contain 'uri' and 'email'
+  # fields, but they are not used for the purpose of this feed and are
+  # omitted.
+  renderAuthor = author: ''<author><name>${escape author}</name></author>'';
+
+  renderEntry = defun [ entry string ] (e: ''
+    <entry>
+      ${elem "title" e.title}
+      ${elem "id" e.id}
+      ${elem "updated" (renderEpoch e.updated)}
+      ${if e ? published
+        then elem "published" (renderEpoch e.published)
+        else ""
+      }
+      ${if e ? content
+        then ''<content type="html">${escape e.content}</content>''
+        else ""
+      }
+      ${if e ? summary then elem "summary" e.summary else ""}
+      ${concatStrings (map renderAuthor (e.authors or []))}
+      ${if e ? subtitle then elem "subtitle" e.subtitle else ""}
+      ${if e ? rights then elem "rights" e.rights else ""}
+      ${concatStrings (map renderLink (e.links or []))}
+    </entry>
+  '');
+
+  mostRecentlyUpdated = defun [ (list entry) int ] (entries:
+    foldl' max 0 (map (e: e.updated) entries)
+  );
+
+  sortEntries = sort (a: b: a.published > b.published);
+
+  renderFeed = defun [ feed string ] (f: ''
+    <?xml version="1.0" encoding="utf-8"?>
+    <feed xmlns="http://www.w3.org/2005/Atom">
+      ${elem "id" f.id}
+      ${elem "title" f.title}
+      ${elem "updated" (renderEpoch (f.updated or (mostRecentlyUpdated f.entries)))}
+      ${concatStringsSep "\n" (map renderAuthor (f.authors or []))}
+      ${if f ? subtitle then elem "subtitle" f.subtitle else ""}
+      ${if f ? rights then elem "rights" f.rights else ""}
+      ${concatStrings (map renderLink (f.links or []))}
+      ${concatStrings (map renderEntry (sortEntries f.entries))}
+    </feed>
+  '');
+in
+{
+  inherit entry feed renderFeed renderEpoch;
+}
diff --git a/web/atward/.gitignore b/web/atward/.gitignore
new file mode 100644
index 0000000000..29e65519ba
--- /dev/null
+++ b/web/atward/.gitignore
@@ -0,0 +1,3 @@
+result
+/target
+**/*.rs.bk
diff --git a/web/atward/Cargo.lock b/web/atward/Cargo.lock
new file mode 100644
index 0000000000..b18f55faa1
--- /dev/null
+++ b/web/atward/Cargo.lock
@@ -0,0 +1,668 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "ascii"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+
+[[package]]
+name = "atward"
+version = "0.1.0"
+dependencies = [
+ "regex",
+ "rouille",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "deflate"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70"
+dependencies = [
+ "adler32",
+ "gzip-header",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gzip-header"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba"
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "libc"
+version = "0.2.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
+
+[[package]]
+name = "log"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rouille"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18b2380c42510ef4a28b5f228a174c801e0dec590103e215e60812e2e2f34d05"
+dependencies = [
+ "base64",
+ "brotli",
+ "chrono",
+ "deflate",
+ "filetime",
+ "multipart",
+ "num_cpus",
+ "percent-encoding",
+ "rand",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "threadpool",
+ "time",
+ "tiny_http",
+ "url",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
+dependencies = [
+ "sha1_smol",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
+name = "syn"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+dependencies = [
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "tiny_http"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39"
+dependencies = [
+ "ascii",
+ "chrono",
+ "chunked_transfer",
+ "log",
+ "url",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/web/atward/Cargo.toml b/web/atward/Cargo.toml
new file mode 100644
index 0000000000..2ecd10f96f
--- /dev/null
+++ b/web/atward/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "atward"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+edition = "2018"
+
+[dependencies]
+regex = "1.5"
+rouille = "3.5"
diff --git a/web/atward/build.rs b/web/atward/build.rs
new file mode 100644
index 0000000000..90a2f35cd1
--- /dev/null
+++ b/web/atward/build.rs
@@ -0,0 +1,55 @@
+//! Build script that can be used outside of Nix builds to inject the
+//! ATWARD_INDEX_HTML variable when building in development mode.
+//!
+//! Note that this script assumes that atward is in a checkout of the
+//! TVL depot.
+
+use std::process::Command;
+
+static ATWARD_INDEX_HTML: &str = "ATWARD_INDEX_HTML";
+static ERROR_MESSAGE: &str = r#"Failed to build index page.
+
+When building during development, atward expects to be in a checkout
+of the TVL depot. This is required to automatically build the index
+page that is needed at compile time.
+
+As atward can not automatically detect the location of the page,
+you must set the `ATWARD_INDEX_HTML` environment variable to the
+right path.
+
+The expected page is build using the files in //web/atward/indexHtml
+in the depot."#;
+
+fn main() {
+    // Do nothing if the variable is already set (e.g. via Nix)
+    if let Ok(_) = std::env::var(ATWARD_INDEX_HTML) {
+        return;
+    }
+
+    // Otherwise ask Nix to build it and inject the result.
+    let output = Command::new("nix-build")
+        .arg("-A")
+        .arg("web.atward.indexHtml")
+        // ... assuming atward is at //web/atward ...
+        .arg("../..")
+        .output()
+        .expect(ERROR_MESSAGE);
+
+    if !output.status.success() {
+        eprintln!(
+            "{}\nNix output: {}",
+            ERROR_MESSAGE,
+            String::from_utf8_lossy(&output.stderr)
+        );
+        return;
+    }
+
+    let out_path = String::from_utf8(output.stdout)
+        .expect("Nix returned invalid output after building index page");
+
+    // Return an instruction to Cargo that will set the environment
+    // variable during rustc calls.
+    //
+    // https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-envvarvalue
+    println!("cargo:rustc-env={}={}", ATWARD_INDEX_HTML, out_path.trim());
+}
diff --git a/web/atward/default.nix b/web/atward/default.nix
new file mode 100644
index 0000000000..f3ed04345c
--- /dev/null
+++ b/web/atward/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  override = x: {
+    ATWARD_INDEX_HTML = depot.web.atward.indexHtml;
+  };
+}
diff --git a/web/atward/indexHtml/default.nix b/web/atward/indexHtml/default.nix
new file mode 100644
index 0000000000..3af808b898
--- /dev/null
+++ b/web/atward/indexHtml/default.nix
@@ -0,0 +1,99 @@
+{ depot, ... }:
+
+depot.web.tvl.template {
+  title = "atward";
+  content = ''
+    atward
+    ======
+
+    ----------
+
+    **atward** is [TVL's](https://tvl.fyi/) search
+    service. It can be configured as a browser search engine for easy
+    access to TVL bugs, code reviews, code paths and more.
+
+    ### Setting up atward
+
+    To configure atward, add a search engine to your browser with the
+    following search string: `https://at.tvl.fyi/?q=%s`
+    Consider setting a shortcut, for example **t** or **tvl**.
+    You can now quickly access TVL resources by typing something
+    like <kbd>t b/42</kbd> in your URL bar to get to the bug with ID
+    42.
+
+
+    ### Supported queries
+
+    The following query types are supported in atward:
+
+    * <kbd>b/42</kbd> - access bugs with ID 42
+    * <kbd>cl/3087</kbd> - access changelist with ID 3087
+    * <kbd>//web/atward</kbd> - open the **//web/atward** path in TVLs monorepo
+    * <kbd>r/3002</kbd> - access revision 3002 in cgit
+
+    When given a short host name (e.g. <kbd>todo</kbd> or
+    <kbd>cl</kbd>), atward will redirect to the appropriate `tvl.fyi`
+    domain.
+
+    ### Configuration
+
+    Some behaviour of atward can be configured by adding query
+    parameters to the search string:
+
+    * <kbd>cs=true</kbd> - use Sourcegraph instead of cgit to view code
+
+
+    In some browsers (like Firefox) users can not edit query
+    parameters for search engines. As an alternative configuration can
+    be supplied via cookies with the same names as the configuration
+    parameters.
+
+    The form below can set this configuration:
+    <form class="cheddar-callout cheddar-todo">
+      <input type="checkbox"
+             id="cs-setting"
+             name="cs-setting"
+             onchange="saveSetting(this, 'cs');">
+      <label for="cs-setting">Use Sourcegraph instead of cgit</label>
+    </form>
+
+    <noscript>
+      <p class="cheddar-callout cheddar-warning">
+        The form above only works with Javascript enabled. Only a few
+        lines of Javascript are used, and they are licensed under a
+        free-software license (MIT).
+      </p>
+    </noscript>
+
+    ### Source code
+
+    atward's source code lives at
+    [//web/atward](https://at.tvl.fyi/?q=%2F%2Fweb%2Fatward).
+  '';
+  extraHead = ''
+    <script>
+      /* Initialise the state of all settings. */
+      function loadSettings() {
+          loadSetting(document.getElementById('cs-setting'), 'cs');
+      }
+
+      /* Initialise the state of a setting from a cookie. */
+      function loadSetting(checkbox, name) {
+          if (document.cookie.split(';').some(function(cookie) {
+              return cookie.indexOf(`''${name}=true`) >= 0;
+          })) {
+              checkbox.checked = true;
+          }
+      }
+
+      /* Persist the state of a checkbox in a cookie */
+      function saveSetting(checkbox, name) {
+          console.log(`setting atward parameter '''''${name}' to ''${checkbox.checked.toString()}`);
+          document.cookie = `''${name}=''${checkbox.checked.toString()};`;
+      }
+
+      document.addEventListener('DOMContentLoaded', loadSettings);
+    </script>
+    <link rel="search" type="application/opensearchdescription+xml" title="TVL Search" href="https://at.tvl.fyi/opensearch.xml">
+  '';
+}
diff --git a/web/atward/src/main.rs b/web/atward/src/main.rs
new file mode 100644
index 0000000000..eb2603a226
--- /dev/null
+++ b/web/atward/src/main.rs
@@ -0,0 +1,200 @@
+//! Atward implements TVL's redirection service, living at
+//! atward.tvl.fyi
+//!
+//! This service is designed to be added as a search engine to web
+//! browsers and attempts to send users to useful locations based on
+//! their search query (falling back to another search engine).
+use regex::Regex;
+use rouille::input::cookies;
+use rouille::{Request, Response};
+
+#[cfg(test)]
+mod tests;
+
+/// A query handler supported by atward. It consists of a pattern on
+/// which to match and trigger the query, and a function to execute
+/// that returns the target URL.
+struct Handler {
+    /// Regular expression on which to match the query string.
+    pattern: Regex,
+
+    /// Function to construct the target URL. If the pattern matches,
+    /// this is invoked with the captured matches and the entire URI.
+    ///
+    /// Returning `None` causes atward to fall through to the next
+    /// query (and eventually to the default search engine).
+    target: for<'s> fn(&Query, regex::Captures<'s>) -> Option<String>,
+}
+
+/// An Atward query supplied by a user.
+#[derive(Debug, PartialEq)]
+struct Query {
+    /// Query string itself.
+    query: String,
+
+    /// Should Sourcegraph be used instead of cgit?
+    cs: bool,
+}
+
+/// Helper function for setting a parameter based on a query
+/// parameter.
+fn query_setting(req: &Request, config: &mut bool, param: &str) {
+    match req.get_param(param) {
+        Some(s) if s == "true" => *config = true,
+        Some(s) if s == "false" => *config = false,
+        _ => {}
+    }
+}
+
+impl Query {
+    fn from_request(req: &Request) -> Option<Query> {
+        // First extract the actual search query ...
+        let mut query = match req.get_param("q") {
+            Some(query) => Query { query, cs: false },
+            None => return None,
+        };
+
+        // ... then apply settings to it. Settings in query parameters
+        // take precedence over cookies.
+        for cookie in cookies(req) {
+            match cookie {
+                ("cs", "true") => {
+                    query.cs = true;
+                }
+                _ => {}
+            }
+        }
+
+        query_setting(req, &mut query.cs, "cs");
+
+        Some(query)
+    }
+}
+
+#[cfg(test)]
+impl From<&str> for Query {
+    fn from(query: &str) -> Query {
+        Query {
+            query: query.to_string(),
+            cs: false,
+        }
+    }
+}
+
+/// Create a URL to a file (and, optionally, specific line) in cgit.
+fn cgit_url(path: &str) -> String {
+    if path.ends_with(".md") {
+        format!("https://code.tvl.fyi/about/{}", path)
+    } else {
+        format!("https://code.tvl.fyi/tree/{}", path)
+    }
+}
+
+/// Create a URL to a path in Sourcegraph.
+fn sourcegraph_path_url(path: &str) -> String {
+    format!("https://cs.tvl.fyi/depot/-/tree/{}", path)
+}
+/// Definition of all supported query handlers in atward.
+fn handlers() -> Vec<Handler> {
+    vec![
+        // Bug IDs (e.g. b/123)
+        Handler {
+            pattern: Regex::new("^b/(?P<bug>\\d+)$").unwrap(),
+            target: |_, captures| Some(format!("https://b.tvl.fyi/{}", &captures["bug"])),
+        },
+        // Changelists (e.g. cl/42)
+        Handler {
+            pattern: Regex::new("^cl/(?P<cl>\\d+)$").unwrap(),
+            target: |_, captures| Some(format!("https://cl.tvl.fyi/{}", &captures["cl"])),
+        },
+        // Non-parameterised short hostnames should redirect to $host.tvl.fyi
+        Handler {
+            pattern: Regex::new("^(?P<host>b|cl|cs|code|at|todo)$").unwrap(),
+            target: |_, captures| Some(format!("https://{}.tvl.fyi/", &captures["host"])),
+        },
+        // Depot revisions (e.g. r/3002)
+        Handler {
+            pattern: Regex::new("^r/(?P<rev>\\d+)$").unwrap(),
+            target: |_, captures| {
+                Some(format!(
+                    "https://code.tvl.fyi/commit/?id=refs/r/{}",
+                    &captures["rev"]
+                ))
+            },
+        },
+        // Depot paths (e.g. //web/atward or //ops/nixos/whitby/default.nix)
+        // TODO(tazjin): Add support for specifying lines in a query parameter
+        Handler {
+            pattern: Regex::new("^//(?P<path>[a-zA-Z].*)?$").unwrap(),
+            target: |query, captures| {
+                // Pass an empty string if the path is missing, to
+                // redirect to the depot root.
+                let path = captures.name("path").map(|m| m.as_str()).unwrap_or("");
+
+                if query.cs {
+                    Some(sourcegraph_path_url(path))
+                } else {
+                    Some(cgit_url(path))
+                }
+            },
+        },
+    ]
+}
+
+/// Attempt to match against all known query types, and return the
+/// destination URL if one is found.
+fn dispatch(handlers: &[Handler], query: &Query) -> Option<String> {
+    for handler in handlers {
+        if let Some(captures) = handler.pattern.captures(&query.query) {
+            if let Some(destination) = (handler.target)(query, captures) {
+                return Some(destination);
+            }
+        }
+    }
+
+    None
+}
+
+/// Return the opensearch.xml file which is required for adding atward
+/// as a search engine in Firefox.
+fn opensearch() -> Response {
+    Response::text(include_str!("opensearch.xml"))
+        .with_unique_header("Content-Type", "application/opensearchdescription+xml")
+}
+
+/// Render the atward index page which gives users some information
+/// about how to use the service.
+fn index() -> Response {
+    Response::html(include_str!(env!("ATWARD_INDEX_HTML")))
+}
+
+/// Render the fallback page which informs users that their query is
+/// unsupported.
+fn fallback() -> Response {
+    Response::text("error for emphasis that i am angery and the query whimchst i angery atward")
+        .with_status_code(404)
+}
+
+fn main() {
+    let queries = handlers();
+    let address = std::env::var("ATWARD_LISTEN_ADDRESS")
+        .expect("ATWARD_LISTEN_ADDRESS environment variable must be set");
+
+    rouille::start_server(&address, move |request| {
+        rouille::log(&request, std::io::stderr(), || {
+            if request.url() == "/opensearch.xml" {
+                return opensearch();
+            }
+
+            let query = match Query::from_request(&request) {
+                Some(q) => q,
+                None => return index(),
+            };
+
+            match dispatch(&queries, &query) {
+                None => fallback(),
+                Some(destination) => Response::redirect_303(destination),
+            }
+        })
+    });
+}
diff --git a/web/atward/src/opensearch.xml b/web/atward/src/opensearch.xml
new file mode 100644
index 0000000000..6033987f5b
--- /dev/null
+++ b/web/atward/src/opensearch.xml
@@ -0,0 +1,8 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+  <ShortName>TVL</ShortName>
+  <Description>The Virus Lounge Search</Description>
+  <InputEncoding>UTF-8</InputEncoding>
+  <Url type="text/html" template="https://at.tvl.fyi/">
+    <Param name="q" value="{searchTerms}"/>
+  </Url>
+</OpenSearchDescription>
diff --git a/web/atward/src/tests.rs b/web/atward/src/tests.rs
new file mode 100644
index 0000000000..a23f96ee9a
--- /dev/null
+++ b/web/atward/src/tests.rs
@@ -0,0 +1,187 @@
+use super::*;
+
+#[test]
+fn bug_query() {
+    assert_eq!(
+        dispatch(&handlers(), &"b/42".into()),
+        Some("https://b.tvl.fyi/42".to_string())
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"something only mentioning b/42".into()),
+        None,
+    );
+    assert_eq!(dispatch(&handlers(), &"b/invalid".into()), None,);
+}
+
+#[test]
+fn cl_query() {
+    assert_eq!(
+        dispatch(&handlers(), &"cl/42".into()),
+        Some("https://cl.tvl.fyi/42".to_string())
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"something only mentioning cl/42".into()),
+        None,
+    );
+    assert_eq!(dispatch(&handlers(), &"cl/invalid".into()), None,);
+}
+
+#[test]
+fn depot_path_cgit_query() {
+    assert_eq!(
+        dispatch(&handlers(), &"//web/atward/default.nix".into()),
+        Some("https://code.tvl.fyi/tree/web/atward/default.nix".to_string()),
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"//nix/readTree/README.md".into()),
+        Some("https://code.tvl.fyi/about/nix/readTree/README.md".to_string()),
+    );
+
+    assert_eq!(dispatch(&handlers(), &"/not/a/depot/path".into()), None);
+}
+
+#[test]
+fn depot_path_sourcegraph_query() {
+    assert_eq!(
+        dispatch(
+            &handlers(),
+            &Query {
+                query: "//web/atward/default.nix".to_string(),
+                cs: true,
+            }
+        ),
+        Some("https://cs.tvl.fyi/depot/-/tree/web/atward/default.nix".to_string()),
+    );
+
+    assert_eq!(
+        dispatch(
+            &handlers(),
+            &Query {
+                query: "/not/a/depot/path".to_string(),
+                cs: true,
+            }
+        ),
+        None
+    );
+}
+
+#[test]
+fn depot_root_cgit_query() {
+    assert_eq!(
+        dispatch(
+            &handlers(),
+            &Query {
+                query: "//".to_string(),
+                cs: false,
+            }
+        ),
+        Some("https://code.tvl.fyi/tree/".to_string()),
+    );
+}
+
+#[test]
+fn plain_host_queries() {
+    assert_eq!(
+        dispatch(&handlers(), &"cs".into()),
+        Some("https://cs.tvl.fyi/".to_string()),
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"cl".into()),
+        Some("https://cl.tvl.fyi/".to_string()),
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"b".into()),
+        Some("https://b.tvl.fyi/".to_string()),
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"todo".into()),
+        Some("https://todo.tvl.fyi/".to_string()),
+    );
+}
+
+#[test]
+fn request_to_query() {
+    assert_eq!(
+        Query::from_request(&Request::fake_http("GET", "/?q=b%2F42", vec![], vec![]))
+            .expect("request should parse to a query"),
+        Query {
+            query: "b/42".to_string(),
+            cs: false,
+        },
+    );
+
+    assert_eq!(
+        Query::from_request(&Request::fake_http("GET", "/", vec![], vec![])),
+        None
+    );
+}
+
+#[test]
+fn settings_from_cookie() {
+    assert_eq!(
+        Query::from_request(&Request::fake_http(
+            "GET",
+            "/?q=b%2F42",
+            vec![("Cookie".to_string(), "cs=true;".to_string())],
+            vec![]
+        ))
+        .expect("request should parse to a query"),
+        Query {
+            query: "b/42".to_string(),
+            cs: true,
+        },
+    );
+}
+
+#[test]
+fn settings_from_query_parameter() {
+    assert_eq!(
+        Query::from_request(&Request::fake_http(
+            "GET",
+            "/?q=b%2F42&cs=true",
+            vec![],
+            vec![]
+        ))
+        .expect("request should parse to a query"),
+        Query {
+            query: "b/42".to_string(),
+            cs: true,
+        },
+    );
+
+    // Query parameter should override cookie
+    assert_eq!(
+        Query::from_request(&Request::fake_http(
+            "GET",
+            "/?q=b%2F42&cs=false",
+            vec![("Cookie".to_string(), "cs=true;".to_string())],
+            vec![]
+        ))
+        .expect("request should parse to a query"),
+        Query {
+            query: "b/42".to_string(),
+            cs: false,
+        },
+    );
+}
+
+#[test]
+fn depot_revision_query() {
+    assert_eq!(
+        dispatch(&handlers(), &"r/3002".into()),
+        Some("https://code.tvl.fyi/commit/?id=refs/r/3002".to_string())
+    );
+
+    assert_eq!(
+        dispatch(&handlers(), &"something only mentioning r/3002".into()),
+        None,
+    );
+
+    assert_eq!(dispatch(&handlers(), &"r/invalid".into()), None,);
+}
diff --git a/web/blog/default.nix b/web/blog/default.nix
new file mode 100644
index 0000000000..f55c33a63a
--- /dev/null
+++ b/web/blog/default.nix
@@ -0,0 +1,63 @@
+# This creates the static files that make up my blog from the Markdown
+# files in this repository.
+#
+# All blog posts are rendered from Markdown by cheddar.
+{ depot, lib, pkgs, ... }@args:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) readFile;
+  inherit (depot.nix) renderMarkdown;
+  inherit (depot.web) atom-feed;
+  inherit (lib) singleton;
+
+  # Type definition for a single blog post.
+  post = struct "blog-post" {
+    key = string;
+    title = string;
+    date = int;
+
+    # Optional time at which this post was last updated.
+    updated = option int;
+
+    # Path to the Markdown file containing the post content.
+    content = path;
+
+    # Should this post be included in the index? (defaults to true)
+    listed = option bool;
+
+    # Is this a draft? (adds a banner indicating that the link should
+    # not be shared)
+    draft = option bool;
+
+    # Previously each post title had a numeric ID. For these numeric
+    # IDs, redirects are generated so that old URLs stay compatible.
+    oldKey = option string;
+  };
+
+  # Rendering fragments for the HTML version of the blog.
+  fragments = import ./fragments.nix args;
+
+  # Functions for generating feeds for these blogs using //web/atom-feed.
+  toFeedEntry = { baseUrl, ... }: defun [ post atom-feed.entry ] (post: rec {
+    id = "${baseUrl}/${post.key}";
+    title = post.title;
+    content = readFile (renderMarkdown post.content);
+    published = post.date;
+    updated = post.updated or post.date;
+
+    links = singleton {
+      rel = "alternate";
+      href = id;
+    };
+  });
+in
+{
+  inherit post toFeedEntry;
+  inherit (fragments) renderPost;
+
+  # Helper function to determine whether a post should be included in
+  # listings (on homepages, feeds, ...)
+  includePost = post: !(fragments.isDraft post) && !(fragments.isUnlisted post);
+}
diff --git a/web/blog/fragments.nix b/web/blog/fragments.nix
new file mode 100644
index 0000000000..19d62fa474
--- /dev/null
+++ b/web/blog/fragments.nix
@@ -0,0 +1,96 @@
+# This file defines various fragments of the blog, such as the header
+# and footer, as functions that receive arguments to be templated into
+# them.
+#
+# An entire post is rendered by `renderPost`, which assembles the
+# fragments together in a runCommand execution.
+{ depot, lib, pkgs, ... }:
+
+let
+  inherit (builtins) filter map hasAttr replaceStrings;
+  inherit (pkgs) runCommandNoCC writeText;
+  inherit (depot.nix) renderMarkdown;
+
+  staticUrl = "https://static.tvl.fyi/${depot.web.static.drvHash}";
+
+  # Generate a post list for all listed, non-draft posts.
+  isDraft = post: (hasAttr "draft" post) && post.draft;
+  isUnlisted = post: (hasAttr "listed" post) && !post.listed;
+
+  escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
+
+  header = name: title: ''
+    <!DOCTYPE html>
+    <head>
+      <meta charset="utf-8">
+      <meta name="viewport" content="width=device-width, initial-scale=1">
+      <meta name="description" content="${escape name}">
+      <link rel="stylesheet" type="text/css" href="${staticUrl}/tvl.css" media="all">
+      <link rel="icon" type="image/webp" href="/static/favicon.webp">
+      <link rel="alternate" type="application/atom+xml" title="Atom Feed" href="https://tvl.fyi/feed.atom">
+      <title>${escape name}: ${escape title}</title>
+    </head>
+    <body class="light">
+      <header>
+        <h1><a class="blog-title" href="/">${escape name}</a> </h1>
+        <hr>
+      </header>
+  '';
+
+  fullFooter = content: ''
+      <hr>
+      <footer>
+        ${content}
+      </footer>
+    </body>
+  '';
+
+  draftWarning = writeText "draft.html" ''
+    <p class="cheddar-callout cheddar-warning">
+      <b>Note:</b> This post is a <b>draft</b>! Please do not share
+      the link to it without asking first.
+    </p>
+    <hr>
+  '';
+
+  unlistedWarning = writeText "unlisted.html" ''
+    <p class="cheddar-callout cheddar-warning">
+      <b>Note:</b> This post is <b>unlisted</b>! Please do not share
+      the link to it without asking first.
+    </p>
+    <hr>
+  '';
+
+  renderPost = { name, footer, ... }: post: runCommandNoCC "${post.key}.html" { } ''
+    cat ${writeText "header.html" (header name post.title)} > $out
+
+    # Write the post title & date
+    echo '<article><h2 class="inline">${escape post.title}</h2>' >> $out
+    echo '<aside class="date">' >> $out
+    date --date="@${toString post.date}" '+%Y-%m-%d' >> $out
+    ${
+      if post ? updated
+      then ''date --date="@${toString post.updated}" '+ (updated %Y-%m-%d)' >> $out''
+      else ""
+    }
+    echo '</aside>' >> $out
+
+    ${
+      # Add a warning to draft/unlisted posts to make it clear that
+      # people should not share the post.
+
+      if (isDraft post) then "cat ${draftWarning} >> $out"
+      else if (isUnlisted post) then "cat ${unlistedWarning} >> $out"
+      else "# Your ads could be here?"
+    }
+
+    # Write the actual post through cheddar's about-filter mechanism
+    cat ${renderMarkdown post.content} >> $out
+    echo '</article>' >> $out
+
+    cat ${writeText "footer.html" (fullFooter footer)} >> $out
+  '';
+in
+{
+  inherit isDraft isUnlisted renderPost;
+}
diff --git a/web/bubblegum/OWNERS b/web/bubblegum/OWNERS
new file mode 100644
index 0000000000..f16dd105d7
--- /dev/null
+++ b/web/bubblegum/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/web/bubblegum/README.md b/web/bubblegum/README.md
new file mode 100644
index 0000000000..48c2ba6e03
--- /dev/null
+++ b/web/bubblegum/README.md
@@ -0,0 +1,68 @@
+# //web/bubblegum
+
+`bubblegum` is a CGI programming library for the Nix expression language.
+It provides a few helpers to make writing CGI scripts which are executable
+using [//nix/nint](../../nix/nint/README.md) convenient.
+
+An example nix.cgi script looks like this (don't worry about the shebang
+too much, you can use `web.bubblegum.writeCGI` to set this up without
+thinking twice):
+
+```nix
+#!/usr/bin/env nint --arg depot '(import /path/to/depot {})'
+{ depot, ... }:
+
+let
+  inherit (depot.web.bubblegum)
+    respond
+    ;
+in
+
+respond "OK" {
+  "Content-type" = "text/html";
+  # further headers…
+} ''
+  <!doctype html>
+  <html>
+    <head>
+      <meta charset="utf-8">
+      <title>hello world</title>
+    </head>
+    <body>
+      hello world!
+    </body>
+  </html>
+''
+```
+
+As you can see, the core component of `bubblegum` is the `respond`
+function which takes three arguments:
+
+* The response status as the textual representation which is also
+  returned to the client in the HTTP protocol, e. g. `"OK"`,
+  `"Not Found"`, `"Bad Request"`, …
+
+* An attribute set mapping header names to header values to be sent.
+
+* The response body as a string.
+
+Additionally it exposes a few helpers for working with the CGI
+environment like `pathInfo` which is a wrapper around
+`builtins.getEnv "PATH_INFO"`. The documentation for all exposed
+helpers is inlined in [default.nix](./default.nix) (you should be
+able to use `nixdoc` to render it).
+
+For deployment purposes it is recommended to use `writeCGI` which
+takes a nix CGI script in the form of a derivation, path or string
+and builds an executable nix CGI script which has the correct shebang
+set and is automatically passed a version of depot from the nix store,
+so the script has access to the `bubblegum` library.
+
+For example nix CGI scripts and a working deployment using `thttpd`
+see the [examples directory](./examples). You can also start a local
+server running the examples like this:
+
+```
+$ nix-build -A web.bubblegum.examples && ./result
+# navigate to http://localhost:9000
+```
diff --git a/web/bubblegum/default.nix b/web/bubblegum/default.nix
new file mode 100644
index 0000000000..528d73032b
--- /dev/null
+++ b/web/bubblegum/default.nix
@@ -0,0 +1,267 @@
+{ depot, lib, pkgs, ... }:
+
+let
+
+  inherit (depot.nix)
+    runExecline
+    getBins
+    utils
+    sparseTree
+    nint
+    ;
+
+  minimalDepot = sparseTree depot.path.origSrc [
+    # general depot things
+    "default.nix"
+    "nix/readTree"
+    # nixpkgs for lib and packages
+    "third_party/nixpkgs"
+    "third_party/overlays"
+    # bubblegum and its dependencies
+    "web/bubblegum"
+    "nix/runExecline"
+    "nix/utils"
+    "nix/sparseTree"
+    # tvix docs for svg demo
+    "tvix/docs"
+    # for blog.nix
+    "users/sterni/nix"
+  ];
+
+  statusCodes = {
+    # 1xx
+    "Continue" = 100;
+    "Switching Protocols" = 101;
+    "Processing" = 102;
+    "Early Hints" = 103;
+    # 2xx
+    "OK" = 200;
+    "Created" = 201;
+    "Accepted" = 202;
+    "Non-Authoritative Information" = 203;
+    "No Content" = 204;
+    "Reset Content" = 205;
+    "Partial Content" = 206;
+    "Multi Status" = 207;
+    "Already Reported" = 208;
+    "IM Used" = 226;
+    # 3xx
+    "Multiple Choices" = 300;
+    "Moved Permanently" = 301;
+    "Found" = 302;
+    "See Other" = 303;
+    "Not Modified" = 304;
+    "Use Proxy" = 305;
+    "Switch Proxy" = 306;
+    "Temporary Redirect" = 307;
+    "Permanent Redirect" = 308;
+    # 4xx
+    "Bad Request" = 400;
+    "Unauthorized" = 401;
+    "Payment Required" = 402;
+    "Forbidden" = 403;
+    "Not Found" = 404;
+    "Method Not Allowed" = 405;
+    "Not Acceptable" = 406;
+    "Proxy Authentication Required" = 407;
+    "Request Timeout" = 408;
+    "Conflict" = 409;
+    "Gone" = 410;
+    "Length Required" = 411;
+    "Precondition Failed" = 412;
+    "Payload Too Large" = 413;
+    "URI Too Long" = 414;
+    "Unsupported Media Type" = 415;
+    "Range Not Satisfiable" = 416;
+    "Expectation Failed" = 417;
+    "I'm a teapot" = 418;
+    "Misdirected Request" = 421;
+    "Unprocessable Entity" = 422;
+    "Locked" = 423;
+    "Failed Dependency" = 424;
+    "Too Early" = 425;
+    "Upgrade Required" = 426;
+    "Precondition Required" = 428;
+    "Too Many Requests" = 429;
+    "Request Header Fields Too Large" = 431;
+    "Unavailable For Legal Reasons" = 451;
+    # 5xx
+    "Internal Server Error" = 500;
+    "Not Implemented" = 501;
+    "Bad Gateway" = 502;
+    "Service Unavailable" = 503;
+    "Gateway Timeout" = 504;
+    "HTTP Version Not Supported" = 505;
+    "Variant Also Negotiates" = 506;
+    "Insufficient Storage" = 507;
+    "Loop Detected" = 508;
+    "Not Extended" = 510;
+    "Network Authentication Required" = 511;
+  };
+
+  /* Generate a CGI response. Takes three arguments:
+
+     1. Status of the response as a string which is
+        the descriptive name in the protocol, e. g.
+        `"OK"`, `"Not Found"` etc.
+     2. Attribute set describing extra headers to
+        send, keys and values should both be strings.
+     3. Response content as a string.
+
+     See the [README](./README.md) for an example.
+
+    Type: either int string -> attrs string -> string -> string
+  */
+  respond =
+    # response status as an integer (status code) or its
+    # textual representation in the HTTP protocol.
+    # See `statusCodes` for a list of valid options.
+    statusArg:
+    # headers as an attribute set of strings
+    headers:
+    # response body as a string
+    bodyArg:
+    let
+      status =
+        if builtins.isInt statusArg
+        then {
+          code = statusArg;
+          line = lib.findFirst
+            (line: statusCodes."${line}" == statusArg)
+            null
+            (builtins.attrNames statusCodes);
+        } else if builtins.isString statusArg then {
+          code = statusCodes."${statusArg}" or null;
+          line = statusArg;
+        } else {
+          code = null;
+          line = null;
+        };
+      renderedHeaders = lib.concatStrings
+        (lib.mapAttrsToList (n: v: "${n}: ${toString v}\r\n") headers);
+      internalError = msg: respond 500
+        {
+          Content-type = "text/plain";
+        } "bubblegum error: ${msg}";
+      body = builtins.tryEval bodyArg;
+    in
+    if status.code == null || status.line == null
+    then internalError "Invalid status ${lib.generators.toPretty {} statusArg}."
+    else if !body.success
+    then internalError "Unknown evaluation error in user code"
+    else
+      lib.concatStrings [
+        "Status: ${toString status.code} ${status.line}\r\n"
+        renderedHeaders
+        "\r\n"
+        body.value
+      ];
+
+  /* Returns the value of the `SCRIPT_NAME` environment
+     variable used by CGI.
+  */
+  scriptName = builtins.getEnv "SCRIPT_NAME";
+
+  /* Returns the value of the `PATH_INFO` environment
+     variable used by CGI. All cases that could be
+     considered as the CGI script's root (i. e.
+     `PATH_INFO` is empty or `/`) is mapped to `"/"`
+     for convenience.
+  */
+  pathInfo =
+    let
+      p = builtins.getEnv "PATH_INFO";
+    in
+    if builtins.stringLength p == 0
+    then "/"
+    else p;
+
+  /* Helper function which converts a path from the
+     root of the CGI script (i. e. something which
+     could be the content of `PATH_INFO`) to an
+     absolute path from the web root by also
+     utilizing `scriptName`.
+
+     Type: string -> string
+  */
+  absolutePath = path:
+    if builtins.substring 0 1 path == "/"
+    then "${scriptName}${path}"
+    else "${scriptName}/${path}";
+
+  bins = getBins pkgs.coreutils [ "env" "tee" "cat" "printf" "chmod" ]
+    // getBins nint [ "nint" ];
+
+  /* Type: args -> either path derivation string -> derivation
+  */
+  writeCGI =
+    {
+      # if given sets the `PATH` to search for `nix-instantiate`
+      # Useful when using for example thttpd which unsets `PATH`
+      # in the CGI environment.
+      binPath ? ""
+      # name of the resulting derivation. Defaults to `baseNameOf`
+      # the input path or name of the input derivation.
+      # Must be given if the input is a string.
+    , name ? null
+    , ...
+    }@args:
+    input:
+    let
+      drvName =
+        if builtins.isString input || args ? name
+        then args.name
+        else utils.storePathName input;
+      script =
+        if builtins.isPath input || lib.isDerivation input
+        then input
+        else if builtins.isString input
+        then pkgs.writeText "${drvName}-source" input
+        else builtins.throw "Unsupported input: ${lib.generators.toPretty {} input}";
+      shebang = lib.concatStringsSep " " ([
+        "#!${bins.env}"
+        # use the slightly cursed /usr/bin/env -S which allows us
+        # to pass any number of arguments to our interpreter
+        # instead of maximum one using plain shebang which considers
+        # everything after the first space as the second argument.
+        "-S"
+      ] ++ lib.optionals (builtins.stringLength binPath > 0) [
+        "PATH=${binPath}"
+      ] ++ [
+        "${bins.nint}"
+        # always pass depot so scripts can use this library
+        "--arg depot '(import ${minimalDepot} {})'"
+      ]);
+    in
+    runExecline.local drvName { } [
+      "importas"
+      "out"
+      "out"
+      "pipeline"
+      [
+        "foreground"
+        [
+          "if"
+          [ bins.printf "%s\n" shebang ]
+        ]
+        "if"
+        [ bins.cat script ]
+      ]
+      "if"
+      [ bins.tee "$out" ]
+      "if"
+      [ bins.chmod "+x" "$out" ]
+      "exit"
+      "0"
+    ];
+
+in
+{
+  inherit
+    respond
+    pathInfo
+    scriptName
+    absolutePath
+    writeCGI
+    ;
+}
diff --git a/web/bubblegum/examples/blog.nix b/web/bubblegum/examples/blog.nix
new file mode 100644
index 0000000000..76b91168b8
--- /dev/null
+++ b/web/bubblegum/examples/blog.nix
@@ -0,0 +1,143 @@
+{ depot, ... }:
+
+let
+  inherit (depot.third_party.nixpkgs)
+    lib
+    ;
+
+  inherit (depot.users.sterni.nix)
+    url
+    fun
+    string
+    ;
+
+  inherit (depot.web.bubblegum)
+    pathInfo
+    scriptName
+    respond
+    absolutePath
+    ;
+
+  # substituted using substituteAll in default.nix
+  blogdir = "@blogdir@";
+  # blogdir = toString ./posts; # for local testing
+
+  parseDate = post:
+    let
+      matched = builtins.match "/?([0-9]+)-([0-9]+)-([0-9]+)-.+" post;
+    in
+    if matched == null
+    then [ 0 0 0 ]
+    else builtins.map builtins.fromJSON matched;
+
+  parseTitle = post:
+    let
+      matched = builtins.match "/?[0-9]+-[0-9]+-[0-9]+-(.+).html" post;
+    in
+    if matched == null
+    then "no title"
+    else builtins.head matched;
+
+  dateAtLeast = a: b:
+    builtins.all fun.id
+      (lib.zipListsWith (partA: partB: partA >= partB) a b);
+
+  byPostDate = a: b:
+    dateAtLeast (parseDate a) (parseDate b);
+
+  posts = builtins.sort byPostDate
+    (builtins.attrNames
+      (lib.filterAttrs (_: v: v == "regular")
+        (builtins.readDir blogdir)));
+
+  generic = { title, inner, ... }: ''
+    <!doctype html>
+    <html>
+      <head>
+        <meta charset="utf-8">
+        <title>${title}</title>
+        <style>a:link, a:visited { color: blue; }</style>
+      </head>
+      <body>
+      ${inner}
+      </body>
+    </html>
+  '';
+
+  index = posts: ''
+    <main>
+      <h1>blog posts</h1>
+      <ul>
+  '' + lib.concatMapStrings
+    (post: ''
+      <li>
+        <a href="${absolutePath (url.encode {} post)}">${parseTitle post}</a>
+      </li>
+    '')
+    posts + ''
+      </ul>
+    </main>
+  '';
+
+  formatDate =
+    let
+      # Assume we never deal with years < 1000
+      formatDigit = d: string.fit
+        {
+          char = "0";
+          width = 2;
+        }
+        (toString d);
+    in
+    lib.concatMapStringsSep "-" formatDigit;
+
+  post = title: post: ''
+    <main>
+      <h1>${title}</h1>
+      <div id="content">
+        ${builtins.readFile (blogdir + "/" + post)}
+      </div>
+    </main>
+    <footer>
+      <p>Posted on ${formatDate (parseDate post)}</p>
+      <nav><a href="${scriptName}">index</a></nav>
+    </footer>
+  '';
+
+  validatePathInfo = pathInfo:
+    let
+      chars = string.toChars pathInfo;
+    in
+    builtins.length chars > 1
+    && !(builtins.elem "/" (builtins.tail chars));
+
+  response =
+    if pathInfo == "/"
+    then {
+      title = "blog";
+      status = 200;
+      inner = index posts;
+    }
+    else if !(validatePathInfo pathInfo)
+    then {
+      title = "Bad Request";
+      status = 400;
+      inner = "No slashes in post names 😡";
+    }
+    # CGI should already url.decode for us
+    else if builtins.pathExists (blogdir + "/" + pathInfo)
+    then rec {
+      title = parseTitle pathInfo;
+      status = 200;
+      inner = post title pathInfo;
+    } else {
+      title = "Not Found";
+      status = 404;
+      inner = "<h1>404 — not found</h1>";
+    };
+in
+respond response.status
+{
+  "Content-type" = "text/html";
+}
+  (generic response)
diff --git a/web/bubblegum/examples/default.nix b/web/bubblegum/examples/default.nix
new file mode 100644
index 0000000000..89482f93ea
--- /dev/null
+++ b/web/bubblegum/examples/default.nix
@@ -0,0 +1,82 @@
+{ depot, pkgs, lib, ... }:
+
+let
+
+  scripts = [
+    ./hello.nix
+    ./derivation-svg.nix
+    (substituteAll {
+      src = ./blog.nix;
+      # by making this a plain string this
+      # can be something outside the nix store!
+      blogdir = ./posts;
+    })
+  ];
+
+  inherit (depot.nix)
+    writeExecline
+    runExecline
+    getBins
+    ;
+
+  inherit (depot.web.bubblegum)
+    writeCGI
+    ;
+
+  inherit (pkgs)
+    runCommandLocal
+    substituteAll
+    ;
+
+  bins = (getBins pkgs.thttpd [ "thttpd" ])
+    // (getBins pkgs.coreutils [ "printf" "cp" "mkdir" ]);
+
+  webRoot =
+    let
+      copyScripts = lib.concatMap
+        (path:
+          let
+            cgi = writeCGI
+              {
+                # assume we are on NixOS since thttpd doesn't set PATH.
+                # using third_party.nix is tricky because not everyone
+                # has a tvix daemon running.
+                binPath = "/run/current-system/sw/bin";
+              }
+              path;
+          in
+          [
+            "if"
+            [ bins.cp cgi "\${out}/${cgi.name}" ]
+          ])
+        scripts;
+    in
+    runExecline.local "webroot" { } ([
+      "importas"
+      "out"
+      "out"
+      "if"
+      [ bins.mkdir "-p" "$out" ]
+    ] ++ copyScripts);
+
+  port = 9000;
+
+in
+writeExecline "serve-examples" { } [
+  "foreground"
+  [
+    bins.printf
+    "%s\n"
+    "Running on http://localhost:${toString port}"
+  ]
+  "${bins.thttpd}"
+  "-D"
+  "-p"
+  (toString port)
+  "-l"
+  "/dev/stderr"
+  "-c"
+  "*.nix"
+  "-d"
+  webRoot
+]
diff --git a/web/bubblegum/examples/derivation-svg.nix b/web/bubblegum/examples/derivation-svg.nix
new file mode 100644
index 0000000000..9a625afb55
--- /dev/null
+++ b/web/bubblegum/examples/derivation-svg.nix
@@ -0,0 +1,13 @@
+# Warning: this is *very* slow on the first request
+{ depot, ... }:
+
+let
+  inherit (depot.web.bubblegum)
+    respond
+    ;
+in
+respond "OK"
+{
+  Content-type = "image/svg+xml";
+}
+  (builtins.readFile "${depot.tvix.docs.svg}/component-flow.svg")
diff --git a/web/bubblegum/examples/hello.nix b/web/bubblegum/examples/hello.nix
new file mode 100644
index 0000000000..bd4891f7d6
--- /dev/null
+++ b/web/bubblegum/examples/hello.nix
@@ -0,0 +1,94 @@
+{ depot, ... }:
+
+let
+  inherit (depot.third_party.nixpkgs)
+    lib
+    ;
+
+  inherit (depot.web.bubblegum)
+    pathInfo
+    respond
+    absolutePath
+    ;
+
+  routes = {
+    "/" = {
+      status = "OK";
+      title = "index";
+      content = ''
+        Hello World!
+      '';
+    };
+    "/clock" = {
+      status = "OK";
+      title = "clock";
+      content = ''
+        It is ${toString builtins.currentTime}s since 1970-01-01 00:00 UTC.
+      '';
+    };
+    "/coffee" = {
+      status = "I'm a teapot";
+      title = "coffee";
+      content = ''
+        No coffee, I'm afraid
+      '';
+    };
+    "/type-error" = {
+      status = 666;
+      title = "bad usage";
+      content = ''
+        Never gonna see this.
+      '';
+    };
+    "/eval-error" = {
+      status = "OK";
+      title = "evaluation error";
+      content = builtins.throw "lol";
+    };
+  };
+
+  notFound = {
+    status = "Not Found";
+    title = "404";
+    content = ''
+      This page doesn't exist.
+    '';
+  };
+
+  navigation =
+    lib.concatStrings (lib.mapAttrsToList
+      (p: v: "<li><a href=\"${absolutePath p}\">${v.title}</a></li>")
+      routes);
+
+  template = { title, content, ... }: ''
+    <!doctype html>
+    <html lang="en">
+    <head>
+      <meta charset="utf-8">
+      <title>${title}</title>
+      <style>a:link, a:visited { color: blue; }</style>
+    </head>
+    <body>
+      <hgroup>
+      <h1><code>//web/bubblegum</code></h1>
+      <h2>example app</h2>
+      </hgroup>
+      <header>
+        <nav>
+          <ul>${navigation}</ul>
+        </nav>
+      </header>
+      <main>
+        <p>${content}</p>
+      </main>
+    </body>
+  '';
+
+  response = routes."${pathInfo}" or notFound;
+
+in
+respond response.status
+{
+  "Content-type" = "text/html";
+}
+  (template response)
diff --git a/web/bubblegum/examples/posts/2021-04-01-hello.html b/web/bubblegum/examples/posts/2021-04-01-hello.html
new file mode 100644
index 0000000000..3c58be7953
--- /dev/null
+++ b/web/bubblegum/examples/posts/2021-04-01-hello.html
@@ -0,0 +1,3 @@
+<p>
+  This is it, the peak of cursed.
+</p>
diff --git a/web/bubblegum/examples/posts/2021-04-02-a second post.html b/web/bubblegum/examples/posts/2021-04-02-a second post.html
new file mode 100644
index 0000000000..050586c302
--- /dev/null
+++ b/web/bubblegum/examples/posts/2021-04-02-a second post.html
@@ -0,0 +1,7 @@
+<p>
+  <ul>
+    <li>✅ sorting</li>
+    <li>✅ url encoding (admire the spaces!)</li>
+    <li>✅ classic Nix regex based parsing</li>
+  </ul>
+</p>
diff --git a/web/converse/.gitignore b/web/converse/.gitignore
new file mode 100644
index 0000000000..548206b0b2
--- /dev/null
+++ b/web/converse/.gitignore
@@ -0,0 +1,3 @@
+.envrc
+/target/
+**/*.rs.bk
diff --git a/web/converse/Cargo.lock b/web/converse/Cargo.lock
new file mode 100644
index 0000000000..7f72be076e
--- /dev/null
+++ b/web/converse/Cargo.lock
@@ -0,0 +1,3069 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "actix"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c616db5fa4b0c40702fb75201c2af7f8aa8f3a2e2c1dda3b0655772aa949666"
+dependencies = [
+ "actix_derive",
+ "bitflags 1.3.2",
+ "bytes",
+ "crossbeam-channel",
+ "failure",
+ "fnv",
+ "futures",
+ "libc",
+ "log 0.4.16",
+ "parking_lot 0.7.1",
+ "smallvec 0.6.14",
+ "tokio",
+ "tokio-codec",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-signal",
+ "tokio-tcp",
+ "tokio-timer",
+ "trust-dns-proto 0.5.0",
+ "trust-dns-resolver",
+ "uuid",
+]
+
+[[package]]
+name = "actix-net"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bebfbe6629e0131730746718c9e032b58f02c6ce06ed7c982b9fef6c8545acd"
+dependencies = [
+ "actix",
+ "bytes",
+ "futures",
+ "log 0.4.16",
+ "mio",
+ "net2",
+ "num_cpus",
+ "slab 0.4.6",
+ "tokio",
+ "tokio-codec",
+ "tokio-current-thread",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-tcp",
+ "tokio-timer",
+ "tower-service",
+ "trust-dns-resolver",
+]
+
+[[package]]
+name = "actix-web"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0ac60f86c65a50b140139f499f4f7c6e49e4b5d88fbfba08e4e3975991f7bf4"
+dependencies = [
+ "actix",
+ "actix-net",
+ "base64 0.10.1",
+ "bitflags 1.3.2",
+ "brotli2",
+ "byteorder",
+ "bytes",
+ "cookie",
+ "encoding",
+ "failure",
+ "flate2",
+ "futures",
+ "futures-cpupool",
+ "h2",
+ "http",
+ "httparse",
+ "language-tags",
+ "lazy_static",
+ "lazycell",
+ "log 0.4.16",
+ "mime",
+ "mime_guess",
+ "mio",
+ "net2",
+ "num_cpus",
+ "parking_lot 0.7.1",
+ "percent-encoding 1.0.1",
+ "rand 0.6.5",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sha1",
+ "slab 0.4.6",
+ "smallvec 0.6.14",
+ "time 0.1.43",
+ "tokio",
+ "tokio-current-thread",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-tcp",
+ "tokio-timer",
+ "url 1.7.2",
+ "v_htmlescape",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "actix_derive"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e9431455322ae393d43a2ba1ef96b8080573c0fc23b196219efedfb6ba69"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aead"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
+dependencies = [
+ "aes-soft",
+ "aesni",
+ "cipher",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "aes-soft"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
+dependencies = [
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aesni"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
+dependencies = [
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr 2.4.1",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "ascii"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+
+[[package]]
+name = "askama"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a57e2df69b9fdc5ec981be522d8aff87bd90947b68864f30bf34570e6c5b227a"
+dependencies = [
+ "askama_derive",
+ "askama_shared",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284fd159afab7796f2d15a442bdba4af7e085f8389196b03115018993202fe4e"
+dependencies = [
+ "askama_shared",
+ "nom 3.2.1",
+ "quote 0.5.2",
+ "syn 0.13.11",
+]
+
+[[package]]
+name = "askama_shared"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72764966f241ed1143792ec29805df7eec322800b2b402781d86339e66f512b6"
+dependencies = [
+ "error-chain 0.11.0",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "autocfg"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "miniz_oxide 0.4.4",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
+dependencies = [
+ "byteorder",
+ "safemem",
+]
+
+[[package]]
+name = "base64"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "brotli-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "brotli2"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
+dependencies = [
+ "brotli-sys",
+ "libc",
+]
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr 2.4.1",
+ "safemem",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
+dependencies = [
+ "byteorder",
+ "iovec",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time 0.1.43",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags 1.3.2",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "comrak"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "378397d3ac20b5b0b8b29fa2e1bf20546a6d6cedd1bc18a422dfb41384129e29"
+dependencies = [
+ "clap",
+ "entities",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "regex",
+ "twoway",
+ "typed-arena",
+ "unicode_categories",
+]
+
+[[package]]
+name = "converse"
+version = "0.1.0"
+dependencies = [
+ "actix",
+ "actix-web",
+ "askama",
+ "chrono",
+ "comrak",
+ "crimp",
+ "curl",
+ "diesel",
+ "env_logger",
+ "failure",
+ "futures",
+ "hyper",
+ "log 0.4.16",
+ "md5",
+ "mime_guess",
+ "pq-sys",
+ "pulldown-cmark",
+ "r2d2",
+ "rand 0.4.6",
+ "rouille",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tokio",
+ "tokio-timer",
+ "url 1.7.2",
+ "url_serde",
+]
+
+[[package]]
+name = "cookie"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80f6044740a4a516b8aac14c140cdf35c1a640b1bd6b98b6224e49143b2f1566"
+dependencies = [
+ "aes-gcm",
+ "base64 0.13.0",
+ "hkdf",
+ "hmac",
+ "percent-encoding 2.1.0",
+ "rand 0.8.5",
+ "sha2",
+ "time 0.1.43",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpuid-bool"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "crimp"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe8f9a320ad9c1a2e3bacedaa281587bd297fb10a10179fd39f777049d04794"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa"
+dependencies = [
+ "crossbeam-utils 0.6.6",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils 0.7.2",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if 0.1.10",
+ "crossbeam-utils 0.7.2",
+ "lazy_static",
+ "maybe-uninit",
+ "memoffset",
+ "scopeguard 1.1.0",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
+dependencies = [
+ "cfg-if 0.1.10",
+ "crossbeam-utils 0.7.2",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
+dependencies = [
+ "cfg-if 0.1.10",
+ "lazy_static",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if 0.1.10",
+ "lazy_static",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "ctr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2 0.4.4",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.53+curl-7.82.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8092905a5a9502c312f223b2775f57ec5c5b715f9a15ee9d2a8591d1364a0352"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "deflate"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70"
+dependencies = [
+ "adler32",
+ "gzip-header",
+]
+
+[[package]]
+name = "diesel"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d"
+dependencies = [
+ "bitflags 1.3.2",
+ "byteorder",
+ "chrono",
+ "diesel_derives",
+ "pq-sys",
+ "r2d2",
+]
+
+[[package]]
+name = "diesel_derives"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
+dependencies = [
+ "proc-macro2 1.0.37",
+ "quote 1.0.18",
+ "syn 1.0.91",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dtoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
+
+[[package]]
+name = "encoding"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
+dependencies = [
+ "encoding-index-japanese",
+ "encoding-index-korean",
+ "encoding-index-simpchinese",
+ "encoding-index-singlebyte",
+ "encoding-index-tradchinese",
+]
+
+[[package]]
+name = "encoding-index-japanese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-korean"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-simpchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-singlebyte"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-tradchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding_index_tests"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
+
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
+[[package]]
+name = "env_logger"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38"
+dependencies = [
+ "atty",
+ "humantime",
+ "log 0.4.16",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "error-chain"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6930e04918388a9a2e41d518c25cf679ccafe26733fb4127dbf21993f2575d46"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
+name = "error-chain"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
+name = "failure"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
+dependencies = [
+ "backtrace",
+ "failure_derive",
+]
+
+[[package]]
+name = "failure_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
+dependencies = [
+ "proc-macro2 1.0.37",
+ "quote 1.0.18",
+ "syn 1.0.91",
+ "synstructure",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.2.13",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crc32fast",
+ "libc",
+ "miniz-sys",
+ "miniz_oxide 0.5.1",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding 2.1.0",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags 1.3.2",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
+[[package]]
+name = "futures"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
+
+[[package]]
+name = "futures-cpupool"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
+dependencies = [
+ "futures",
+ "num_cpus",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check 0.9.4",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "ghash"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "gimli"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
+
+[[package]]
+name = "gzip-header"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
+name = "h2"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "fnv",
+ "futures",
+ "http",
+ "indexmap",
+ "log 0.4.16",
+ "slab 0.4.6",
+ "string",
+ "tokio-io",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "hostname"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
+dependencies = [
+ "libc",
+ "match_cfg",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "http"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 0.4.8",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba"
+
+[[package]]
+name = "humantime"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+dependencies = [
+ "quick-error",
+]
+
+[[package]]
+name = "hyper"
+version = "0.11.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34a590ca09d341e94cddf8e5af0bbccde205d5fbc2fa3c09dd67c7f85cea59d7"
+dependencies = [
+ "base64 0.9.3",
+ "bytes",
+ "futures",
+ "futures-cpupool",
+ "httparse",
+ "iovec",
+ "language-tags",
+ "log 0.4.16",
+ "mime",
+ "net2",
+ "percent-encoding 1.0.1",
+ "relay",
+ "time 0.1.43",
+ "tokio-core",
+ "tokio-io",
+ "tokio-proto",
+ "tokio-service",
+ "unicase",
+ "want",
+]
+
+[[package]]
+name = "idna"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+dependencies = [
+ "autocfg 1.1.0",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ipconfig"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f7eadeaf4b52700de180d147c4805f199854600b36faa963d91114827b2ffc"
+dependencies = [
+ "error-chain 0.8.1",
+ "socket2 0.3.19",
+ "widestring",
+ "winapi 0.3.9",
+ "winreg",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
+[[package]]
+name = "lock_api"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
+dependencies = [
+ "owning_ref",
+ "scopeguard 0.3.3",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
+dependencies = [
+ "scopeguard 1.1.0",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg 1.1.0",
+ "scopeguard 1.1.0",
+]
+
+[[package]]
+name = "log"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
+dependencies = [
+ "log 0.4.16",
+]
+
+[[package]]
+name = "log"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "lru-cache"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "match_cfg"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+[[package]]
+name = "md5"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
+
+[[package]]
+name = "memchr"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "memoffset"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz-sys"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9e3ae51cea1576ceba0dde3d484d30e6e5b86dee0b2d412fe3a16a15c98202"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log 0.4.16",
+ "miow",
+ "net2",
+ "slab 0.4.6",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log 0.4.16",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand 0.8.5",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "nom"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b"
+dependencies = [
+ "memchr 1.0.2",
+]
+
+[[package]]
+name = "nom"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+dependencies = [
+ "memchr 2.4.1",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9"
+dependencies = [
+ "memchr 2.4.1",
+]
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
+dependencies = [
+ "autocfg 1.1.0",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "owning_ref"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
+dependencies = [
+ "lock_api 0.1.5",
+ "parking_lot_core 0.4.0",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
+dependencies = [
+ "lock_api 0.3.4",
+ "parking_lot_core 0.6.2",
+ "rustc_version",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api 0.4.7",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
+dependencies = [
+ "libc",
+ "rand 0.6.5",
+ "rustc_version",
+ "smallvec 0.6.14",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
+dependencies = [
+ "cfg-if 0.1.10",
+ "cloudabi",
+ "libc",
+ "redox_syscall 0.1.57",
+ "rustc_version",
+ "smallvec 0.6.14",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.13",
+ "smallvec 1.8.0",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pest"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fce5d8b5cc33983fc74f78ad552b5522ab41442c4ca91606e4236eb4b5ceefc"
+
+[[package]]
+name = "pest_derive"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3294f437119209b084c797604295f40227cffa35c57220b1e99a6ff3bf8ee4"
+dependencies = [
+ "pest",
+ "quote 0.3.15",
+ "syn 0.11.11",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "polyval"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd"
+dependencies = [
+ "cpuid-bool",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "pq-sys"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dfb5e575ef93a1b7b2a381d47ba7c5d4e4f73bff37cee932195de769aad9a54"
+dependencies = [
+ "vcpkg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
+dependencies = [
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6fdf85cda6cadfae5428a54661d431330b312bc767ddbc57adbedc24da66e32"
+dependencies = [
+ "bitflags 0.9.1",
+ "getopts",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
+
+[[package]]
+name = "quote"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
+dependencies = [
+ "proc-macro2 0.3.8",
+]
+
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2 1.0.37",
+]
+
+[[package]]
+name = "r2d2"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
+dependencies = [
+ "log 0.4.16",
+ "parking_lot 0.11.2",
+ "scheduled-thread-pool",
+]
+
+[[package]]
+name = "rand"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
+dependencies = [
+ "libc",
+ "rand 0.4.6",
+]
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.8",
+ "libc",
+ "rand_chacha 0.1.1",
+ "rand_core 0.4.2",
+ "rand_hc",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr 2.4.1",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "relay"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a"
+dependencies = [
+ "futures",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "resolv-conf"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a"
+dependencies = [
+ "hostname",
+ "quick-error",
+]
+
+[[package]]
+name = "rouille"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18b2380c42510ef4a28b5f228a174c801e0dec590103e215e60812e2e2f34d05"
+dependencies = [
+ "base64 0.13.0",
+ "brotli",
+ "chrono",
+ "deflate",
+ "filetime",
+ "multipart",
+ "num_cpus",
+ "percent-encoding 2.1.0",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "threadpool",
+ "time 0.3.9",
+ "tiny_http",
+ "url 2.2.2",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "scheduled-thread-pool"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
+dependencies = [
+ "parking_lot 0.11.2",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
+
+[[package]]
+name = "scopeguard"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2 1.0.37",
+ "quote 1.0.18",
+ "syn 1.0.91",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+dependencies = [
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a"
+dependencies = [
+ "dtoa",
+ "itoa 0.4.8",
+ "serde",
+ "url 1.7.2",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
+dependencies = [
+ "sha1_smol",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer",
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
+
+[[package]]
+name = "slab"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
+
+[[package]]
+name = "smallvec"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013"
+
+[[package]]
+name = "smallvec"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
+dependencies = [
+ "maybe-uninit",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "socket2"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "string"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
+dependencies = [
+ "quote 0.3.15",
+ "synom",
+ "unicode-xid 0.0.4",
+]
+
+[[package]]
+name = "syn"
+version = "0.13.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b"
+dependencies = [
+ "proc-macro2 0.3.8",
+ "quote 0.5.2",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
+dependencies = [
+ "proc-macro2 1.0.37",
+ "quote 1.0.18",
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "synom"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
+dependencies = [
+ "unicode-xid 0.0.4",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
+dependencies = [
+ "proc-macro2 1.0.37",
+ "quote 1.0.18",
+ "syn 1.0.91",
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "take"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5"
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if 1.0.0",
+ "fastrand",
+ "libc",
+ "redox_syscall 0.2.13",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "time"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+dependencies = [
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "tiny_http"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39"
+dependencies = [
+ "ascii",
+ "chrono",
+ "chunked_transfer",
+ "log 0.4.16",
+ "url 2.2.2",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
+dependencies = [
+ "bytes",
+ "futures",
+ "mio",
+ "num_cpus",
+ "tokio-codec",
+ "tokio-current-thread",
+ "tokio-executor",
+ "tokio-fs",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-sync",
+ "tokio-tcp",
+ "tokio-threadpool",
+ "tokio-timer",
+ "tokio-udp",
+ "tokio-uds",
+]
+
+[[package]]
+name = "tokio-codec"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b"
+dependencies = [
+ "bytes",
+ "futures",
+ "tokio-io",
+]
+
+[[package]]
+name = "tokio-core"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87b1395334443abca552f63d4f61d0486f12377c2ba8b368e523f89e828cffd4"
+dependencies = [
+ "bytes",
+ "futures",
+ "iovec",
+ "log 0.4.16",
+ "mio",
+ "scoped-tls",
+ "tokio",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-timer",
+]
+
+[[package]]
+name = "tokio-current-thread"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e"
+dependencies = [
+ "futures",
+ "tokio-executor",
+]
+
+[[package]]
+name = "tokio-executor"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671"
+dependencies = [
+ "crossbeam-utils 0.7.2",
+ "futures",
+]
+
+[[package]]
+name = "tokio-fs"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4"
+dependencies = [
+ "futures",
+ "tokio-io",
+ "tokio-threadpool",
+]
+
+[[package]]
+name = "tokio-io"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674"
+dependencies = [
+ "bytes",
+ "futures",
+ "log 0.4.16",
+]
+
+[[package]]
+name = "tokio-proto"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389"
+dependencies = [
+ "futures",
+ "log 0.3.9",
+ "net2",
+ "rand 0.3.23",
+ "slab 0.3.0",
+ "smallvec 0.2.1",
+ "take",
+ "tokio-core",
+ "tokio-io",
+ "tokio-service",
+]
+
+[[package]]
+name = "tokio-reactor"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351"
+dependencies = [
+ "crossbeam-utils 0.7.2",
+ "futures",
+ "lazy_static",
+ "log 0.4.16",
+ "mio",
+ "num_cpus",
+ "parking_lot 0.9.0",
+ "slab 0.4.6",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-sync",
+]
+
+[[package]]
+name = "tokio-service"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162"
+dependencies = [
+ "futures",
+]
+
+[[package]]
+name = "tokio-signal"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0c34c6e548f101053321cba3da7cbb87a610b85555884c41b07da2eb91aff12"
+dependencies = [
+ "futures",
+ "libc",
+ "mio",
+ "mio-uds",
+ "signal-hook-registry",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-reactor",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tokio-sync"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee"
+dependencies = [
+ "fnv",
+ "futures",
+]
+
+[[package]]
+name = "tokio-tcp"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72"
+dependencies = [
+ "bytes",
+ "futures",
+ "iovec",
+ "mio",
+ "tokio-io",
+ "tokio-reactor",
+]
+
+[[package]]
+name = "tokio-threadpool"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-queue",
+ "crossbeam-utils 0.7.2",
+ "futures",
+ "lazy_static",
+ "log 0.4.16",
+ "num_cpus",
+ "slab 0.4.6",
+ "tokio-executor",
+]
+
+[[package]]
+name = "tokio-timer"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296"
+dependencies = [
+ "crossbeam-utils 0.7.2",
+ "futures",
+ "slab 0.4.6",
+ "tokio-executor",
+]
+
+[[package]]
+name = "tokio-udp"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82"
+dependencies = [
+ "bytes",
+ "futures",
+ "log 0.4.16",
+ "mio",
+ "tokio-codec",
+ "tokio-io",
+ "tokio-reactor",
+]
+
+[[package]]
+name = "tokio-uds"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0"
+dependencies = [
+ "bytes",
+ "futures",
+ "iovec",
+ "libc",
+ "log 0.4.16",
+ "mio",
+ "mio-uds",
+ "tokio-codec",
+ "tokio-io",
+ "tokio-reactor",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b32f72af77f1bfe3d3d4da8516a238ebe7039b51dd8637a09841ac7f16d2c987"
+dependencies = [
+ "futures",
+]
+
+[[package]]
+name = "trust-dns-proto"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0838272e89f1c693b4df38dc353412e389cf548ceed6f9fd1af5a8d6e0e7cf74"
+dependencies = [
+ "byteorder",
+ "failure",
+ "futures",
+ "idna 0.1.5",
+ "lazy_static",
+ "log 0.4.16",
+ "rand 0.5.6",
+ "smallvec 0.6.14",
+ "socket2 0.3.19",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-tcp",
+ "tokio-timer",
+ "tokio-udp",
+ "url 1.7.2",
+]
+
+[[package]]
+name = "trust-dns-proto"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09144f0992b0870fa8d2972cc069cbf1e3c0fda64d1f3d45c4d68d0e0b52ad4e"
+dependencies = [
+ "byteorder",
+ "failure",
+ "futures",
+ "idna 0.1.5",
+ "lazy_static",
+ "log 0.4.16",
+ "rand 0.5.6",
+ "smallvec 0.6.14",
+ "socket2 0.3.19",
+ "tokio-executor",
+ "tokio-io",
+ "tokio-reactor",
+ "tokio-tcp",
+ "tokio-timer",
+ "tokio-udp",
+ "url 1.7.2",
+]
+
+[[package]]
+name = "trust-dns-resolver"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9f877f7a1ad821ab350505e1f1b146a4960402991787191d6d8cab2ce2de2c"
+dependencies = [
+ "cfg-if 0.1.10",
+ "failure",
+ "futures",
+ "ipconfig",
+ "lazy_static",
+ "log 0.4.16",
+ "lru-cache",
+ "resolv-conf",
+ "smallvec 0.6.14",
+ "tokio",
+ "trust-dns-proto 0.6.3",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee2aa4715743892880f70885373966c83d73ef1b0838a664ef0c76fffd35e7c2"
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr 2.4.1",
+]
+
+[[package]]
+name = "typed-arena"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check 0.9.4",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "universal-hash"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "url"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
+dependencies = [
+ "encoding",
+ "idna 0.1.5",
+ "matches",
+ "percent-encoding 1.0.1",
+]
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna 0.2.3",
+ "matches",
+ "percent-encoding 2.1.0",
+]
+
+[[package]]
+name = "url_serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74e7d099f1ee52f823d4bdd60c93c3602043c728f5db3b97bdb548467f7bddea"
+dependencies = [
+ "serde",
+ "url 1.7.2",
+]
+
+[[package]]
+name = "uuid"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
+dependencies = [
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "v_escape"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6"
+dependencies = [
+ "v_escape_derive",
+]
+
+[[package]]
+name = "v_escape_derive"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
+dependencies = [
+ "nom 4.2.3",
+ "proc-macro2 1.0.37",
+ "quote 1.0.18",
+ "syn 1.0.91",
+]
+
+[[package]]
+name = "v_htmlescape"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41"
+dependencies = [
+ "cfg-if 0.1.10",
+ "v_escape",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "want"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a05d9d966753fa4b5c8db73fcab5eed4549cfe0e1e4e66911e5564a0085c35d1"
+dependencies = [
+ "futures",
+ "log 0.4.16",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "widestring"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7157704c2e12e3d2189c507b7482c52820a16dfa4465ba91add92f266667cadb"
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
diff --git a/web/converse/Cargo.toml b/web/converse/Cargo.toml
new file mode 100644
index 0000000000..c77101144d
--- /dev/null
+++ b/web/converse/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "converse"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+license = "GPL-3.0"
+
+[dependencies]
+actix = "0.7"
+actix-web = "0.7"
+askama = "0.6"
+chrono = { version = "0.4", features = ["serde"] }
+comrak = "0.2"
+crimp = "0.2"
+diesel = { version = "1.2", features = ["postgres", "chrono", "r2d2"]}
+env_logger = "0.5"
+failure = "0.1"
+futures = "0.1"
+hyper = "0.11"
+log = "0.4"
+md5 = "0.3.7"
+mime_guess = "2.0.0-alpha"
+pq-sys = "=0.4.4"
+r2d2 = "0.8"
+rand = "0.4"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
+tokio = "0.1"
+tokio-timer = "0.2"
+url = "1.7"
+url_serde = "0.2"
+curl = "*" # bounded by crimp
+rouille = "3.0"
+
+[build-dependencies]
+pulldown-cmark = "0.1"
+askama = "0.6"
diff --git a/web/converse/LICENSE b/web/converse/LICENSE
new file mode 100644
index 0000000000..f288702d2f
--- /dev/null
+++ b/web/converse/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/web/converse/README.md b/web/converse/README.md
new file mode 100644
index 0000000000..e7a3f70aec
--- /dev/null
+++ b/web/converse/README.md
@@ -0,0 +1,14 @@
+Converse
+========
+
+Welcome to Converse, a work-in-progress forum software written in
+Rust. The intention behind Converse is to provide a simple forum-like
+experience.
+
+There is not a lot of documentation about Converse yet and it has
+several known issues. Also note that Converse is being developed for a
+specific use-case and is not going to be a forum feature kitchen-sink
+like most classical forum softwares.
+
+Better documentation is forthcoming once the remaining basics have
+been taken care of.
diff --git a/web/converse/build.rs b/web/converse/build.rs
new file mode 100644
index 0000000000..89e3e6b1ab
--- /dev/null
+++ b/web/converse/build.rs
@@ -0,0 +1,5 @@
+extern crate askama;
+
+fn main() {
+    askama::rerun_if_templates_changed();
+}
diff --git a/web/converse/default.nix b/web/converse/default.nix
new file mode 100644
index 0000000000..cc2dab7f88
--- /dev/null
+++ b/web/converse/default.nix
@@ -0,0 +1,7 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  buildInputs = with pkgs; [ openssl postgresql.lib ];
+  nativeBuildInputs = [ pkgs.pkgconfig ];
+}
diff --git a/web/converse/envrc.example b/web/converse/envrc.example
new file mode 100644
index 0000000000..f255d269fe
--- /dev/null
+++ b/web/converse/envrc.example
@@ -0,0 +1,14 @@
+# This is an example configuration for running converse during
+# development using direnv:
+# https://github.com/direnv/direnv
+#
+# The OIDC actor is configured with bogus values as disabling logins
+# never causes it to run anyways.
+
+export DATABASE_URL=postgres://converse:converse@localhost/converse
+export RUST_LOG=info
+export OIDC_DISCOVERY_URL=https://does.not.matter.com/
+export OIDC_CLIENT_ID=some-client-id
+export OIDC_CLIENT_SECRET='some-client-secret'
+export BASE_URL=http://localhost:4567
+export REQUIRE_LOGIN=false
diff --git a/web/converse/migrations/.gitkeep b/web/converse/migrations/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/web/converse/migrations/.gitkeep
diff --git a/web/converse/migrations/00000000000000_diesel_initial_setup/down.sql b/web/converse/migrations/00000000000000_diesel_initial_setup/down.sql
new file mode 100644
index 0000000000..a9f5260911
--- /dev/null
+++ b/web/converse/migrations/00000000000000_diesel_initial_setup/down.sql
@@ -0,0 +1,6 @@
+-- This file was automatically created by Diesel to setup helper functions
+-- and other internal bookkeeping. This file is safe to edit, any future
+-- changes will be added to existing projects as new migrations.
+
+DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
+DROP FUNCTION IF EXISTS diesel_set_updated_at();
diff --git a/web/converse/migrations/00000000000000_diesel_initial_setup/up.sql b/web/converse/migrations/00000000000000_diesel_initial_setup/up.sql
new file mode 100644
index 0000000000..d68895b1a7
--- /dev/null
+++ b/web/converse/migrations/00000000000000_diesel_initial_setup/up.sql
@@ -0,0 +1,36 @@
+-- This file was automatically created by Diesel to setup helper functions
+-- and other internal bookkeeping. This file is safe to edit, any future
+-- changes will be added to existing projects as new migrations.
+
+
+
+
+-- Sets up a trigger for the given table to automatically set a column called
+-- `updated_at` whenever the row is modified (unless `updated_at` was included
+-- in the modified columns)
+--
+-- # Example
+--
+-- ```sql
+-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
+--
+-- SELECT diesel_manage_updated_at('users');
+-- ```
+CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
+BEGIN
+    EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
+                    FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
+BEGIN
+    IF (
+        NEW IS DISTINCT FROM OLD AND
+        NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
+    ) THEN
+        NEW.updated_at := current_timestamp;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
diff --git a/web/converse/migrations/2018-04-08-133240_create_posts/down.sql b/web/converse/migrations/2018-04-08-133240_create_posts/down.sql
new file mode 100644
index 0000000000..8ebcef8e13
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-133240_create_posts/down.sql
@@ -0,0 +1,2 @@
+DROP TABLE posts;
+DROP TABLE threads;
diff --git a/web/converse/migrations/2018-04-08-133240_create_posts/up.sql b/web/converse/migrations/2018-04-08-133240_create_posts/up.sql
new file mode 100644
index 0000000000..7cf601fa2c
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-133240_create_posts/up.sql
@@ -0,0 +1,13 @@
+CREATE TABLE threads (
+  id SERIAL PRIMARY KEY,
+  title VARCHAR NOT NULL,
+  body TEXT NOT NULL,
+  posted TIMESTAMPTZ NOT NULL
+);
+
+CREATE TABLE posts (
+  id SERIAL PRIMARY KEY,
+  thread SERIAL REFERENCES threads (id),
+  body TEXT NOT NULL,
+  posted TIMESTAMPTZ NOT NULL
+);
diff --git a/web/converse/migrations/2018-04-08-161017_joinable_posts/down.sql b/web/converse/migrations/2018-04-08-161017_joinable_posts/down.sql
new file mode 100644
index 0000000000..cc7874b410
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-161017_joinable_posts/down.sql
@@ -0,0 +1 @@
+ALTER TABLE posts RENAME COLUMN thread_id TO thread;
diff --git a/web/converse/migrations/2018-04-08-161017_joinable_posts/up.sql b/web/converse/migrations/2018-04-08-161017_joinable_posts/up.sql
new file mode 100644
index 0000000000..9713de6cfc
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-161017_joinable_posts/up.sql
@@ -0,0 +1 @@
+ALTER TABLE posts RENAME COLUMN thread TO thread_id;
diff --git a/web/converse/migrations/2018-04-08-172739_default_posted/down.sql b/web/converse/migrations/2018-04-08-172739_default_posted/down.sql
new file mode 100644
index 0000000000..64dc07e066
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-172739_default_posted/down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE threads ALTER COLUMN posted DROP DEFAULT;
+ALTER TABLE posts ALTER COLUMN posted DROP DEFAULT;
diff --git a/web/converse/migrations/2018-04-08-172739_default_posted/up.sql b/web/converse/migrations/2018-04-08-172739_default_posted/up.sql
new file mode 100644
index 0000000000..afca8181cc
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-172739_default_posted/up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE threads ALTER COLUMN posted SET DEFAULT (NOW() AT TIME ZONE 'UTC');
+ALTER TABLE posts ALTER COLUMN posted SET DEFAULT (NOW() AT TIME ZONE 'UTC');
diff --git a/web/converse/migrations/2018-04-08-182319_add_authors/down.sql b/web/converse/migrations/2018-04-08-182319_add_authors/down.sql
new file mode 100644
index 0000000000..8ad8179080
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-182319_add_authors/down.sql
@@ -0,0 +1,5 @@
+ALTER TABLE threads DROP COLUMN author_name;
+ALTER TABLE threads DROP COLUMN author_email;
+
+ALTER TABLE posts DROP COLUMN author_name;
+ALTER TABLE posts DROP COLUMN author_email;
diff --git a/web/converse/migrations/2018-04-08-182319_add_authors/up.sql b/web/converse/migrations/2018-04-08-182319_add_authors/up.sql
new file mode 100644
index 0000000000..ad5beb9f67
--- /dev/null
+++ b/web/converse/migrations/2018-04-08-182319_add_authors/up.sql
@@ -0,0 +1,10 @@
+-- This migration adds an 'author' column to the thread & post table.
+-- Authors don't currently exist as independent objects in the
+-- database as most user management is simply delegated to the OIDC
+-- provider.
+
+ALTER TABLE threads ADD COLUMN author_name VARCHAR NOT NULL DEFAULT 'anonymous';
+ALTER TABLE threads ADD COLUMN author_email VARCHAR NOT NULL DEFAULT 'unknown@example.org';
+
+ALTER TABLE posts ADD COLUMN author_name VARCHAR NOT NULL DEFAULT 'anonymous';
+ALTER TABLE posts ADD COLUMN author_email VARCHAR NOT NULL DEFAULT 'unknown@example.org';
diff --git a/web/converse/migrations/2018-04-14-140818_posts_only_in_posts/down.sql b/web/converse/migrations/2018-04-14-140818_posts_only_in_posts/down.sql
new file mode 100644
index 0000000000..bb6528f33d
--- /dev/null
+++ b/web/converse/migrations/2018-04-14-140818_posts_only_in_posts/down.sql
@@ -0,0 +1 @@
+ALTER TABLE threads ADD COLUMN body TEXT NOT NULL DEFAULT '';
diff --git a/web/converse/migrations/2018-04-14-140818_posts_only_in_posts/up.sql b/web/converse/migrations/2018-04-14-140818_posts_only_in_posts/up.sql
new file mode 100644
index 0000000000..07ff9a1196
--- /dev/null
+++ b/web/converse/migrations/2018-04-14-140818_posts_only_in_posts/up.sql
@@ -0,0 +1,6 @@
+-- Instead of storing the thread OP in the thread table, this will
+-- make it a post as well.
+-- At the time at which this migration was created no important data
+-- existed in any converse instances, so data is not moved.
+
+ALTER TABLE threads DROP COLUMN body;
diff --git a/web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/down.sql b/web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/down.sql
new file mode 100644
index 0000000000..a67ada3d4a
--- /dev/null
+++ b/web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/down.sql
@@ -0,0 +1,2 @@
+DROP VIEW thread_index;
+ALTER TABLE threads DROP COLUMN sticky;
diff --git a/web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/up.sql b/web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/up.sql
new file mode 100644
index 0000000000..74a559e35d
--- /dev/null
+++ b/web/converse/migrations/2018-04-14-153202_add_stickies_improve_index/up.sql
@@ -0,0 +1,21 @@
+-- Add support for stickies in threads
+ALTER TABLE threads ADD COLUMN sticky BOOLEAN NOT NULL DEFAULT FALSE;
+
+-- CREATE a simple view that returns the list of threads ordered by
+-- the last post that occured in the thread.
+CREATE VIEW thread_index AS
+  SELECT t.id AS thread_id,
+         t.title AS title,
+         t.author_name AS thread_author,
+         t.posted AS created,
+         t.sticky AS sticky,
+         p.id AS post_id,
+         p.author_name AS post_author,
+         p.posted AS posted
+    FROM threads t
+    JOIN (SELECT DISTINCT ON (thread_id)
+           id, thread_id, author_name, posted
+          FROM posts
+          ORDER BY thread_id, id DESC) AS p
+    ON t.id = p.thread_id
+    ORDER BY t.sticky DESC, p.id DESC;
diff --git a/web/converse/migrations/2018-04-14-170750_search-index/down.sql b/web/converse/migrations/2018-04-14-170750_search-index/down.sql
new file mode 100644
index 0000000000..c57e662902
--- /dev/null
+++ b/web/converse/migrations/2018-04-14-170750_search-index/down.sql
@@ -0,0 +1,2 @@
+DROP INDEX idx_fts_search;
+DROP MATERIALIZED VIEW search_index;
diff --git a/web/converse/migrations/2018-04-14-170750_search-index/up.sql b/web/converse/migrations/2018-04-14-170750_search-index/up.sql
new file mode 100644
index 0000000000..6b7d90eca6
--- /dev/null
+++ b/web/converse/migrations/2018-04-14-170750_search-index/up.sql
@@ -0,0 +1,21 @@
+-- Prepare a materialised view containing the tsvector data for all
+-- threads and posts. This view is indexed using a GIN-index to enable
+-- performant full-text searches.
+--
+-- For now the query language is hardcoded to be English.
+
+CREATE MATERIALIZED VIEW search_index AS
+  SELECT p.id AS post_id,
+         p.author_name AS author,
+         t.id AS thread_id,
+         t.title AS title,
+         p.body AS body,
+         setweight(to_tsvector('english', t.title), 'B') ||
+         setweight(to_tsvector('english', p.body), 'A') ||
+         setweight(to_tsvector('simple', t.author_name), 'C') ||
+         setweight(to_tsvector('simple', p.author_name), 'C') AS document
+    FROM posts p
+    JOIN threads t
+    ON t.id = p.thread_id;
+
+CREATE INDEX idx_fts_search ON search_index USING gin(document);
diff --git a/web/converse/migrations/2018-05-01-141548_add-users/down.sql b/web/converse/migrations/2018-05-01-141548_add-users/down.sql
new file mode 100644
index 0000000000..61fd222e18
--- /dev/null
+++ b/web/converse/migrations/2018-05-01-141548_add-users/down.sql
@@ -0,0 +1,63 @@
+-- First restore the old columns:
+ALTER TABLE threads ADD COLUMN author_name VARCHAR;
+ALTER TABLE threads ADD COLUMN author_email VARCHAR;
+ALTER TABLE posts ADD COLUMN author_name VARCHAR;
+ALTER TABLE posts ADD COLUMN author_email VARCHAR;
+
+-- Then select the data back into them:
+UPDATE threads SET author_name = users.name,
+                   author_email = users.email
+  FROM users
+  WHERE threads.user_id = users.id;
+
+UPDATE posts SET author_name = users.name,
+                 author_email = users.email
+  FROM users
+  WHERE posts.user_id = users.id;
+
+-- add the constraints back:
+ALTER TABLE threads ALTER COLUMN author_name SET NOT NULL;
+ALTER TABLE threads ALTER COLUMN author_email SET NOT NULL;
+ALTER TABLE posts ALTER COLUMN author_name SET NOT NULL;
+ALTER TABLE posts ALTER COLUMN author_email SET NOT NULL;
+
+-- reset the index view:
+CREATE OR REPLACE VIEW thread_index AS
+  SELECT t.id AS thread_id,
+         t.title AS title,
+         t.author_name AS thread_author,
+         t.posted AS created,
+         t.sticky AS sticky,
+         p.id AS post_id,
+         p.author_name AS post_author,
+         p.posted AS posted
+    FROM threads t
+    JOIN (SELECT DISTINCT ON (thread_id)
+           id, thread_id, author_name, posted
+          FROM posts
+          ORDER BY thread_id, id DESC) AS p
+    ON t.id = p.thread_id
+    ORDER BY t.sticky DESC, p.id DESC;
+
+-- reset the search view:
+DROP MATERIALIZED VIEW search_index;
+CREATE MATERIALIZED VIEW search_index AS
+  SELECT p.id AS post_id,
+         p.author_name AS author,
+         t.id AS thread_id,
+         t.title AS title,
+         p.body AS body,
+         setweight(to_tsvector('english', t.title), 'B') ||
+         setweight(to_tsvector('english', p.body), 'A') ||
+         setweight(to_tsvector('simple', t.author_name), 'C') ||
+         setweight(to_tsvector('simple', p.author_name), 'C') AS document
+    FROM posts p
+    JOIN threads t
+    ON t.id = p.thread_id;
+
+CREATE INDEX idx_fts_search ON search_index USING gin(document);
+
+-- and drop the users table and columns:
+ALTER TABLE posts DROP COLUMN user_id;
+ALTER TABLE threads DROP COLUMN user_id;
+DROP TABLE users;
diff --git a/web/converse/migrations/2018-05-01-141548_add-users/up.sql b/web/converse/migrations/2018-05-01-141548_add-users/up.sql
new file mode 100644
index 0000000000..fcb7133e8e
--- /dev/null
+++ b/web/converse/migrations/2018-05-01-141548_add-users/up.sql
@@ -0,0 +1,83 @@
+-- This query creates a users table and migrates the existing user
+-- information (from the posts table) into it.
+
+CREATE TABLE users (
+  id SERIAL PRIMARY KEY,
+  email VARCHAR NOT NULL UNIQUE,
+  name VARCHAR NOT NULL,
+  admin BOOLEAN NOT NULL DEFAULT false
+);
+
+-- Insert the 'anonymous' user explicitly:
+INSERT INTO users (name, email)
+  VALUES ('Anonymous', 'anonymous@nothing.org');
+
+INSERT INTO users (id, email, name)
+  SELECT nextval('users_id_seq'),
+         author_email AS email,
+         author_name AS name
+  FROM posts
+  WHERE author_email != 'anonymous@nothing.org'
+  GROUP BY name, email;
+
+-- Create the 'user_id' column in the relevant tables (initially
+-- without a not-null constraint) and populate it with the data
+-- selected above:
+ALTER TABLE posts ADD COLUMN user_id INTEGER REFERENCES users (id);
+UPDATE posts SET user_id = users.id
+  FROM users
+  WHERE users.email = posts.author_email;
+
+ALTER TABLE threads ADD COLUMN user_id INTEGER REFERENCES users (id);
+UPDATE threads SET user_id = users.id
+  FROM users
+  WHERE users.email = threads.author_email;
+
+-- Add the constraints:
+ALTER TABLE posts ALTER COLUMN user_id SET NOT NULL;
+ALTER TABLE threads ALTER COLUMN user_id SET NOT NULL;
+
+-- Update the index view:
+CREATE OR REPLACE VIEW thread_index AS
+  SELECT t.id AS thread_id,
+         t.title AS title,
+         ta.name AS thread_author,
+         t.posted AS created,
+         t.sticky AS sticky,
+         p.id AS post_id,
+         pa.name AS post_author,
+         p.posted AS posted
+    FROM threads t
+    JOIN (SELECT DISTINCT ON (thread_id)
+           id, thread_id, user_id, posted
+          FROM posts
+          ORDER BY thread_id, id DESC) AS p
+    ON t.id = p.thread_id
+    JOIN users ta ON ta.id = t.user_id
+    JOIN users pa ON pa.id = p.user_id
+    ORDER BY t.sticky DESC, p.id DESC;
+
+-- Update the search view:
+DROP MATERIALIZED VIEW search_index;
+CREATE MATERIALIZED VIEW search_index AS
+  SELECT p.id AS post_id,
+         pa.name AS author,
+         t.id AS thread_id,
+         t.title AS title,
+         p.body AS body,
+         setweight(to_tsvector('english', t.title), 'B') ||
+         setweight(to_tsvector('english', p.body), 'A') ||
+         setweight(to_tsvector('simple', ta.name), 'C') ||
+         setweight(to_tsvector('simple', pa.name), 'C') AS document
+    FROM posts p
+    JOIN threads t ON t.id = p.thread_id
+    JOIN users ta ON ta.id = t.user_id
+    JOIN users pa ON pa.id = p.user_id;
+
+CREATE INDEX idx_fts_search ON search_index USING gin(document);
+
+-- And drop the old fields:
+ALTER TABLE posts DROP COLUMN author_name;
+ALTER TABLE posts DROP COLUMN author_email;
+ALTER TABLE threads DROP COLUMN author_name;
+ALTER TABLE threads DROP COLUMN author_email;
diff --git a/web/converse/migrations/2018-05-01-183232_simplified-post-view/down.sql b/web/converse/migrations/2018-05-01-183232_simplified-post-view/down.sql
new file mode 100644
index 0000000000..0f14732f38
--- /dev/null
+++ b/web/converse/migrations/2018-05-01-183232_simplified-post-view/down.sql
@@ -0,0 +1 @@
+DROP VIEW simple_posts;
diff --git a/web/converse/migrations/2018-05-01-183232_simplified-post-view/up.sql b/web/converse/migrations/2018-05-01-183232_simplified-post-view/up.sql
new file mode 100644
index 0000000000..280fef8700
--- /dev/null
+++ b/web/converse/migrations/2018-05-01-183232_simplified-post-view/up.sql
@@ -0,0 +1,11 @@
+-- Creates a view for listing posts akin to the post table before
+-- splitting out users. This exists to avoid having to do joining
+-- logic and such inside of the application.
+
+CREATE VIEW simple_posts AS
+  SELECT p.id AS id,
+         thread_id, body, posted, user_id,
+         users.name AS author_name,
+         users.email AS author_email
+  FROM posts p
+  JOIN users ON users.id = p.user_id;
diff --git a/web/converse/migrations/2018-05-25-160648_add_closed_column/down.sql b/web/converse/migrations/2018-05-25-160648_add_closed_column/down.sql
new file mode 100644
index 0000000000..fb2a98c0af
--- /dev/null
+++ b/web/converse/migrations/2018-05-25-160648_add_closed_column/down.sql
@@ -0,0 +1 @@
+ALTER TABLE threads DROP COLUMN closed;
diff --git a/web/converse/migrations/2018-05-25-160648_add_closed_column/up.sql b/web/converse/migrations/2018-05-25-160648_add_closed_column/up.sql
new file mode 100644
index 0000000000..d7d4c44da2
--- /dev/null
+++ b/web/converse/migrations/2018-05-25-160648_add_closed_column/up.sql
@@ -0,0 +1 @@
+ALTER TABLE threads ADD COLUMN closed BOOLEAN NOT NULL DEFAULT false;
diff --git a/web/converse/migrations/2018-05-25-161939_add_closed_to_index/down.sql b/web/converse/migrations/2018-05-25-161939_add_closed_to_index/down.sql
new file mode 100644
index 0000000000..1063fdc882
--- /dev/null
+++ b/web/converse/migrations/2018-05-25-161939_add_closed_to_index/down.sql
@@ -0,0 +1,30 @@
+-- Update the index view:
+DROP VIEW thread_index;
+CREATE VIEW thread_index AS
+  SELECT t.id AS thread_id,
+         t.title AS title,
+         ta.name AS thread_author,
+         t.posted AS created,
+         t.sticky AS sticky,
+         p.id AS post_id,
+         pa.name AS post_author,
+         p.posted AS posted
+    FROM threads t
+    JOIN (SELECT DISTINCT ON (thread_id)
+           id, thread_id, user_id, posted
+          FROM posts
+          ORDER BY thread_id, id DESC) AS p
+    ON t.id = p.thread_id
+    JOIN users ta ON ta.id = t.user_id
+    JOIN users pa ON pa.id = p.user_id
+    ORDER BY t.sticky DESC, p.id DESC;
+
+-- Update the post view:
+DROP VIEW simple_posts;
+CREATE VIEW simple_posts AS
+  SELECT p.id AS id,
+         thread_id, body, posted, user_id,
+         users.name AS author_name,
+         users.email AS author_email
+  FROM posts p
+  JOIN users ON users.id = p.user_id;
diff --git a/web/converse/migrations/2018-05-25-161939_add_closed_to_index/up.sql b/web/converse/migrations/2018-05-25-161939_add_closed_to_index/up.sql
new file mode 100644
index 0000000000..87580a2f3f
--- /dev/null
+++ b/web/converse/migrations/2018-05-25-161939_add_closed_to_index/up.sql
@@ -0,0 +1,35 @@
+-- Update the index view:
+DROP VIEW thread_index;
+CREATE VIEW thread_index AS
+  SELECT t.id AS thread_id,
+         t.title AS title,
+         ta.name AS thread_author,
+         t.posted AS created,
+         t.sticky AS sticky,
+         t.closed AS closed,
+         p.id AS post_id,
+         pa.name AS post_author,
+         p.posted AS posted
+    FROM threads t
+    JOIN (SELECT DISTINCT ON (thread_id)
+           id, thread_id, user_id, posted
+          FROM posts
+          ORDER BY thread_id, id DESC) AS p
+    ON t.id = p.thread_id
+    JOIN users ta ON ta.id = t.user_id
+    JOIN users pa ON pa.id = p.user_id
+    ORDER BY t.sticky DESC, p.id DESC;
+
+-- Update post view:
+DROP VIEW simple_posts;
+CREATE VIEW simple_posts AS
+  SELECT p.id AS id,
+         thread_id, body,
+         p.posted AS posted,
+         p.user_id AS user_id,
+         threads.closed AS closed,
+         users.name AS author_name,
+         users.email AS author_email
+  FROM posts p
+  JOIN users ON users.id = p.user_id
+  JOIN threads ON threads.id = p.thread_id;
diff --git a/web/converse/src/db.rs b/web/converse/src/db.rs
new file mode 100644
index 0000000000..a0d8915504
--- /dev/null
+++ b/web/converse/src/db.rs
@@ -0,0 +1,317 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+//! This module implements the database executor, which holds the
+//! database connection and performs queries on it.
+
+use crate::errors::{ConverseError, Result};
+use crate::models::*;
+use actix::prelude::*;
+use diesel::prelude::*;
+use diesel::r2d2::{ConnectionManager, Pool};
+use diesel::sql_types::Text;
+use diesel::{self, sql_query};
+
+/// Raw PostgreSQL query used to perform full-text search on posts
+/// with a supplied phrase. For now, the query language is hardcoded
+/// to English and only "plain" queries (i.e. no searches for exact
+/// matches or more advanced query syntax) are supported.
+const SEARCH_QUERY: &'static str = r#"
+WITH search_query (query) AS (VALUES (plainto_tsquery('english', $1)))
+SELECT post_id,
+       thread_id,
+       author,
+       title,
+       ts_headline('english', body, query) AS headline
+  FROM search_index, search_query
+  WHERE document @@ query
+  ORDER BY ts_rank(document, query) DESC
+  LIMIT 50
+"#;
+
+const REFRESH_QUERY: &'static str = "REFRESH MATERIALIZED VIEW search_index";
+
+pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);
+
+impl DbExecutor {
+    /// Request a list of threads.
+    // TODO(tazjin): This should support pagination.
+    pub fn list_threads(&self) -> Result<Vec<ThreadIndex>> {
+        use crate::schema::thread_index::dsl::*;
+
+        let conn = self.0.get()?;
+        let results = thread_index.load::<ThreadIndex>(&conn)?;
+        Ok(results)
+    }
+
+    /// Look up a user based on their email-address. If the user does
+    /// not exist, it is created.
+    pub fn lookup_or_create_user(&self, user_email: &str, user_name: &str) -> Result<User> {
+        use crate::schema::users;
+        use crate::schema::users::dsl::*;
+
+        let conn = self.0.get()?;
+
+        let opt_user = users.filter(email.eq(email)).first(&conn).optional()?;
+
+        if let Some(user) = opt_user {
+            Ok(user)
+        } else {
+            let new_user = NewUser {
+                email: user_email.to_string(),
+                name: user_name.to_string(),
+            };
+
+            let user: User = diesel::insert_into(users::table)
+                .values(&new_user)
+                .get_result(&conn)?;
+
+            info!("Created new user {} with ID {}", new_user.email, user.id);
+
+            Ok(user)
+        }
+    }
+
+    /// Fetch a specific thread and return it with its posts.
+    pub fn get_thread(&self, thread_id: i32) -> Result<(Thread, Vec<SimplePost>)> {
+        use crate::schema::simple_posts::dsl::id;
+        use crate::schema::threads::dsl::*;
+
+        let conn = self.0.get()?;
+        let thread_result: Thread = threads.find(thread_id).first(&conn)?;
+
+        let post_list = SimplePost::belonging_to(&thread_result)
+            .order_by(id.asc())
+            .load::<SimplePost>(&conn)?;
+
+        Ok((thread_result, post_list))
+    }
+
+    /// Fetch a specific post.
+    pub fn get_post(&self, post_id: i32) -> Result<SimplePost> {
+        use crate::schema::simple_posts::dsl::*;
+        let conn = self.0.get()?;
+        Ok(simple_posts.find(post_id).first(&conn)?)
+    }
+
+    /// Update the content of a post.
+    pub fn update_post(&self, post_id: i32, post_text: String) -> Result<Post> {
+        use crate::schema::posts::dsl::*;
+        let conn = self.0.get()?;
+        let updated = diesel::update(posts.find(post_id))
+            .set(body.eq(post_text))
+            .get_result(&conn)?;
+
+        Ok(updated)
+    }
+
+    /// Create a new thread.
+    pub fn create_thread(&self, new_thread: NewThread, post_text: String) -> Result<Thread> {
+        use crate::schema::{posts, threads};
+
+        let conn = self.0.get()?;
+
+        conn.transaction::<Thread, ConverseError, _>(|| {
+            // First insert the thread structure itself
+            let thread: Thread = diesel::insert_into(threads::table)
+                .values(&new_thread)
+                .get_result(&conn)?;
+
+            // ... then create the first post in the thread.
+            let new_post = NewPost {
+                thread_id: thread.id,
+                body: post_text,
+                user_id: new_thread.user_id,
+            };
+
+            diesel::insert_into(posts::table)
+                .values(&new_post)
+                .execute(&conn)?;
+
+            Ok(thread)
+        })
+    }
+
+    /// Create a new post.
+    pub fn create_post(&self, new_post: NewPost) -> Result<Post> {
+        use crate::schema::posts;
+
+        let conn = self.0.get()?;
+
+        let closed: bool = {
+            use crate::schema::threads::dsl::*;
+            threads
+                .select(closed)
+                .find(new_post.thread_id)
+                .first(&conn)?
+        };
+
+        if closed {
+            return Err(ConverseError::ThreadClosed {
+                id: new_post.thread_id,
+            });
+        }
+
+        Ok(diesel::insert_into(posts::table)
+            .values(&new_post)
+            .get_result(&conn)?)
+    }
+
+    /// Search for posts.
+    pub fn search_posts(&self, query: String) -> Result<Vec<SearchResult>> {
+        let conn = self.0.get()?;
+
+        let search_results = sql_query(SEARCH_QUERY)
+            .bind::<Text, _>(query)
+            .get_results::<SearchResult>(&conn)?;
+
+        Ok(search_results)
+    }
+
+    /// Trigger a refresh of the view used for full-text searching.
+    pub fn refresh_search_view(&self) -> Result<()> {
+        let conn = self.0.get()?;
+        debug!("Refreshing search_index view in DB");
+        sql_query(REFRESH_QUERY).execute(&conn)?;
+        Ok(())
+    }
+}
+
+// Old actor implementation:
+
+impl Actor for DbExecutor {
+    type Context = SyncContext<Self>;
+}
+
+/// Message used to look up a user based on their email-address. If
+/// the user does not exist, it is created.
+pub struct LookupOrCreateUser {
+    pub email: String,
+    pub name: String,
+}
+
+message!(LookupOrCreateUser, Result<User>);
+
+impl Handler<LookupOrCreateUser> for DbExecutor {
+    type Result = <LookupOrCreateUser as Message>::Result;
+
+    fn handle(&mut self, _: LookupOrCreateUser, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to fetch a specific thread. Returns the thread and
+/// its posts.
+pub struct GetThread(pub i32);
+message!(GetThread, Result<(Thread, Vec<SimplePost>)>);
+
+impl Handler<GetThread> for DbExecutor {
+    type Result = <GetThread as Message>::Result;
+
+    fn handle(&mut self, _: GetThread, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to fetch a specific post.
+#[derive(Deserialize, Debug)]
+pub struct GetPost {
+    pub id: i32,
+}
+
+message!(GetPost, Result<SimplePost>);
+
+impl Handler<GetPost> for DbExecutor {
+    type Result = <GetPost as Message>::Result;
+
+    fn handle(&mut self, _: GetPost, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to update the content of a post.
+#[derive(Deserialize)]
+pub struct UpdatePost {
+    pub post_id: i32,
+    pub post: String,
+}
+
+message!(UpdatePost, Result<Post>);
+
+impl Handler<UpdatePost> for DbExecutor {
+    type Result = Result<Post>;
+
+    fn handle(&mut self, _: UpdatePost, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to create a new thread
+pub struct CreateThread {
+    pub new_thread: NewThread,
+    pub post: String,
+}
+message!(CreateThread, Result<Thread>);
+
+impl Handler<CreateThread> for DbExecutor {
+    type Result = <CreateThread as Message>::Result;
+
+    fn handle(&mut self, _: CreateThread, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to create a new reply
+pub struct CreatePost(pub NewPost);
+message!(CreatePost, Result<Post>);
+
+impl Handler<CreatePost> for DbExecutor {
+    type Result = <CreatePost as Message>::Result;
+
+    fn handle(&mut self, _: CreatePost, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message used to search for posts
+#[derive(Deserialize)]
+pub struct SearchPosts {
+    pub query: String,
+}
+message!(SearchPosts, Result<Vec<SearchResult>>);
+
+impl Handler<SearchPosts> for DbExecutor {
+    type Result = <SearchPosts as Message>::Result;
+
+    fn handle(&mut self, _: SearchPosts, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
+
+/// Message that triggers a refresh of the view used for full-text
+/// searching.
+pub struct RefreshSearchView;
+message!(RefreshSearchView, Result<()>);
+
+impl Handler<RefreshSearchView> for DbExecutor {
+    type Result = Result<()>;
+
+    fn handle(&mut self, _: RefreshSearchView, _: &mut Self::Context) -> Self::Result {
+        unimplemented!()
+    }
+}
diff --git a/web/converse/src/errors.rs b/web/converse/src/errors.rs
new file mode 100644
index 0000000000..a4bd69023b
--- /dev/null
+++ b/web/converse/src/errors.rs
@@ -0,0 +1,139 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+//! This module defines custom error types using the `failure`-crate.
+//! Links to foreign error types (such as database connection errors)
+//! are established in a similar way as was tradition in
+//! `error_chain`, albeit manually.
+
+use actix_web::http::StatusCode;
+use actix_web::{HttpResponse, ResponseError};
+use std::result;
+
+// Modules with foreign errors:
+use {actix, actix_web, askama, diesel, r2d2, tokio_timer};
+
+pub type Result<T> = result::Result<T, ConverseError>;
+pub type ConverseResult<T> = result::Result<T, ConverseError>;
+
+#[derive(Debug, Fail)]
+pub enum ConverseError {
+    #[fail(display = "an internal Converse error occured: {}", reason)]
+    InternalError { reason: String },
+
+    #[fail(display = "a database error occured: {}", error)]
+    Database { error: diesel::result::Error },
+
+    #[fail(display = "a database connection pool error occured: {}", error)]
+    ConnectionPool { error: r2d2::Error },
+
+    #[fail(display = "a template rendering error occured: {}", reason)]
+    Template { reason: String },
+
+    #[fail(display = "error occured during request handling: {}", error)]
+    ActixWeb { error: actix_web::Error },
+
+    #[fail(display = "error occured running timer: {}", error)]
+    Timer { error: tokio_timer::Error },
+
+    #[fail(display = "user {} does not have permission to edit post {}", user, id)]
+    PostEditForbidden { user: i32, id: i32 },
+
+    #[fail(display = "thread {} is closed and can not be responded to", id)]
+    ThreadClosed { id: i32 },
+
+    #[fail(display = "JSON serialisation failed: {}", error)]
+    Serialisation { error: serde_json::Error },
+
+    // This variant is used as a catch-all for wrapping
+    // actix-web-compatible response errors, such as the errors it
+    // throws itself.
+    #[fail(display = "Actix response error: {}", error)]
+    Actix { error: Box<dyn ResponseError> },
+}
+
+// Establish conversion links to foreign errors:
+
+impl From<diesel::result::Error> for ConverseError {
+    fn from(error: diesel::result::Error) -> ConverseError {
+        ConverseError::Database { error }
+    }
+}
+
+impl From<r2d2::Error> for ConverseError {
+    fn from(error: r2d2::Error) -> ConverseError {
+        ConverseError::ConnectionPool { error }
+    }
+}
+
+impl From<askama::Error> for ConverseError {
+    fn from(error: askama::Error) -> ConverseError {
+        ConverseError::Template {
+            reason: format!("{}", error),
+        }
+    }
+}
+
+impl From<actix::MailboxError> for ConverseError {
+    fn from(error: actix::MailboxError) -> ConverseError {
+        ConverseError::Actix {
+            error: Box::new(error),
+        }
+    }
+}
+
+impl From<actix_web::Error> for ConverseError {
+    fn from(error: actix_web::Error) -> ConverseError {
+        ConverseError::ActixWeb { error }
+    }
+}
+
+impl From<serde_json::Error> for ConverseError {
+    fn from(error: serde_json::Error) -> ConverseError {
+        ConverseError::Serialisation { error }
+    }
+}
+
+impl From<curl::Error> for ConverseError {
+    fn from(error: curl::Error) -> ConverseError {
+        ConverseError::InternalError {
+            reason: format!("error during HTTP request: {}", error),
+        }
+    }
+}
+
+impl From<tokio_timer::Error> for ConverseError {
+    fn from(error: tokio_timer::Error) -> ConverseError {
+        ConverseError::Timer { error }
+    }
+}
+
+// Support conversion of error type into HTTP error responses:
+
+impl ResponseError for ConverseError {
+    fn error_response(&self) -> HttpResponse {
+        // Everything is mapped to internal server errors for now.
+        match *self {
+            ConverseError::ThreadClosed { id } => HttpResponse::SeeOther()
+                .header("Location", format!("/thread/{}#post-reply", id))
+                .finish(),
+            _ => HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
+                .body(format!("An error occured: {}", self)),
+        }
+    }
+}
diff --git a/web/converse/src/handlers.rs b/web/converse/src/handlers.rs
new file mode 100644
index 0000000000..49f9dcf974
--- /dev/null
+++ b/web/converse/src/handlers.rs
@@ -0,0 +1,391 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+//! This module contains the implementation of converse's actix-web
+//! HTTP handlers.
+//!
+//! Most handlers have an associated rendering function using one of
+//! the tera templates stored in the `/templates` directory in the
+//! project root.
+
+use crate::db::*;
+use crate::errors::{ConverseError, ConverseResult};
+use crate::models::*;
+use crate::oidc::*;
+use crate::render::*;
+use actix::prelude::*;
+use actix_web;
+use actix_web::http::Method;
+use actix_web::middleware::identity::RequestIdentity;
+use actix_web::middleware::{Middleware, Started};
+use actix_web::*;
+use futures::Future;
+
+use rouille::{Request, Response};
+
+type ConverseResponse = Box<dyn Future<Item = HttpResponse, Error = ConverseError>>;
+
+const HTML: &'static str = "text/html";
+const ANONYMOUS: i32 = 1;
+const NEW_THREAD_LENGTH_ERR: &'static str = "Title and body can not be empty!";
+
+/// Represents the state carried by the web server actors.
+pub struct AppState {
+    /// Address of the database actor
+    pub db: Addr<DbExecutor>,
+
+    /// Address of the OIDC actor
+    pub oidc: Addr<OidcExecutor>,
+
+    /// Address of the rendering actor
+    pub renderer: Addr<Renderer>,
+}
+
+/// Serve the forum's index page.
+pub fn forum_index_rouille(db: &DbExecutor) -> ConverseResult<Response> {
+    let threads = db.list_threads()?;
+    Ok(Response::html(index_page(threads)?))
+}
+
+pub fn forum_index(_: State<AppState>) -> ConverseResponse {
+    unimplemented!()
+}
+
+/// Returns the ID of the currently logged in user. If there is no ID
+/// present, the ID of the anonymous user will be returned.
+pub fn get_user_id(req: &HttpRequest<AppState>) -> i32 {
+    if let Some(id) = req.identity() {
+        // If this .expect() call is triggered, someone is likely
+        // attempting to mess with their cookies. These requests can
+        // be allowed to fail without further ado.
+        id.parse().expect("Session cookie contained invalid data!")
+    } else {
+        ANONYMOUS
+    }
+}
+
+pub fn get_user_id_rouille(_req: &Request) -> i32 {
+    // TODO(tazjin): Implement session support in rouille somehow.
+    ANONYMOUS
+}
+
+pub fn forum_thread_rouille(
+    req: &Request,
+    db: &DbExecutor,
+    thread_id: i32,
+) -> ConverseResult<Response> {
+    let user = get_user_id_rouille(&req);
+    let thread = db.get_thread(thread_id)?;
+    Ok(Response::html(thread_page(user, thread.0, thread.1)?))
+}
+
+/// This handler retrieves and displays a single forum thread.
+pub fn forum_thread(
+    _: State<AppState>,
+    _: HttpRequest<AppState>,
+    _: Path<i32>,
+) -> ConverseResponse {
+    unimplemented!()
+}
+
+/// This handler presents the user with the "New Thread" form.
+pub fn new_thread(state: State<AppState>) -> ConverseResponse {
+    state
+        .renderer
+        .send(NewThreadPage::default())
+        .flatten()
+        .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
+        .responder()
+}
+
+#[derive(Deserialize)]
+pub struct NewThreadForm {
+    pub title: String,
+    pub post: String,
+}
+
+/// This handler receives a "New thread"-form and redirects the user
+/// to the new thread after creation.
+pub fn submit_thread(
+    (state, input, req): (State<AppState>, Form<NewThreadForm>, HttpRequest<AppState>),
+) -> ConverseResponse {
+    // Trim whitespace out of inputs:
+    let input = NewThreadForm {
+        title: input.title.trim().into(),
+        post: input.post.trim().into(),
+    };
+
+    // Perform simple validation and abort here if it fails:
+    if input.title.is_empty() || input.post.is_empty() {
+        return state
+            .renderer
+            .send(NewThreadPage {
+                alerts: vec![NEW_THREAD_LENGTH_ERR],
+                title: Some(input.title),
+                post: Some(input.post),
+            })
+            .flatten()
+            .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
+            .responder();
+    }
+
+    let user_id = get_user_id(&req);
+
+    let new_thread = NewThread {
+        user_id,
+        title: input.title,
+    };
+
+    let msg = CreateThread {
+        new_thread,
+        post: input.post,
+    };
+
+    state
+        .db
+        .send(msg)
+        .from_err()
+        .and_then(move |res| {
+            let thread = res?;
+            info!(
+                "Created new thread \"{}\" with ID {}",
+                thread.title, thread.id
+            );
+            Ok(HttpResponse::SeeOther()
+                .header("Location", format!("/thread/{}", thread.id))
+                .finish())
+        })
+        .responder()
+}
+
+#[derive(Deserialize)]
+pub struct NewPostForm {
+    pub thread_id: i32,
+    pub post: String,
+}
+
+/// This handler receives a "Reply"-form and redirects the user to the
+/// new post after creation.
+pub fn reply_thread(
+    state: State<AppState>,
+    input: Form<NewPostForm>,
+    req: HttpRequest<AppState>,
+) -> ConverseResponse {
+    let user_id = get_user_id(&req);
+
+    let new_post = NewPost {
+        user_id,
+        thread_id: input.thread_id,
+        body: input.post.trim().into(),
+    };
+
+    state
+        .db
+        .send(CreatePost(new_post))
+        .flatten()
+        .from_err()
+        .and_then(move |post| {
+            info!("Posted reply {} to thread {}", post.id, post.thread_id);
+            Ok(HttpResponse::SeeOther()
+                .header(
+                    "Location",
+                    format!("/thread/{}#post-{}", post.thread_id, post.id),
+                )
+                .finish())
+        })
+        .responder()
+}
+
+/// This handler presents the user with the form to edit a post. If
+/// the user attempts to edit a post that they do not have access to,
+/// they are currently ungracefully redirected back to the post
+/// itself.
+pub fn edit_form(
+    state: State<AppState>,
+    req: HttpRequest<AppState>,
+    query: Path<GetPost>,
+) -> ConverseResponse {
+    let user_id = get_user_id(&req);
+
+    state
+        .db
+        .send(query.into_inner())
+        .flatten()
+        .from_err()
+        .and_then(move |post| {
+            if user_id != 1 && post.user_id == user_id {
+                return Ok(post);
+            }
+
+            Err(ConverseError::PostEditForbidden {
+                user: user_id,
+                id: post.id,
+            })
+        })
+        .and_then(move |post| {
+            let edit_msg = EditPostPage {
+                id: post.id,
+                post: post.body,
+            };
+
+            state.renderer.send(edit_msg).from_err()
+        })
+        .flatten()
+        .map(|page| HttpResponse::Ok().content_type(HTML).body(page))
+        .responder()
+}
+
+/// This handler "executes" an edit to a post if the current user owns
+/// the edited post.
+pub fn edit_post(
+    state: State<AppState>,
+    req: HttpRequest<AppState>,
+    update: Form<UpdatePost>,
+) -> ConverseResponse {
+    let user_id = get_user_id(&req);
+
+    state
+        .db
+        .send(GetPost { id: update.post_id })
+        .flatten()
+        .from_err()
+        .and_then(move |post| {
+            if user_id != 1 && post.user_id == user_id {
+                Ok(())
+            } else {
+                Err(ConverseError::PostEditForbidden {
+                    user: user_id,
+                    id: post.id,
+                })
+            }
+        })
+        .and_then(move |_| state.db.send(update.0).from_err())
+        .flatten()
+        .map(|updated| {
+            HttpResponse::SeeOther()
+                .header(
+                    "Location",
+                    format!("/thread/{}#post-{}", updated.thread_id, updated.id),
+                )
+                .finish()
+        })
+        .responder()
+}
+
+/// This handler executes a full-text search on the forum database and
+/// displays the results to the user.
+pub fn search_forum(state: State<AppState>, query: Query<SearchPosts>) -> ConverseResponse {
+    let query_string = query.query.clone();
+    state
+        .db
+        .send(query.into_inner())
+        .flatten()
+        .and_then(move |results| {
+            state
+                .renderer
+                .send(SearchResultPage {
+                    results,
+                    query: query_string,
+                })
+                .from_err()
+        })
+        .flatten()
+        .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
+        .responder()
+}
+
+/// This handler initiates an OIDC login.
+pub fn login(state: State<AppState>) -> ConverseResponse {
+    state
+        .oidc
+        .send(GetLoginUrl)
+        .from_err()
+        .and_then(|url| {
+            Ok(HttpResponse::TemporaryRedirect()
+                .header("Location", url)
+                .finish())
+        })
+        .responder()
+}
+
+/// This handler handles an OIDC callback (i.e. completed login).
+///
+/// Upon receiving the callback, a token is retrieved from the OIDC
+/// provider and a user lookup is performed. If a user with a matching
+/// email-address is found in the database, it is logged in -
+/// otherwise a new user is created.
+pub fn callback(
+    state: State<AppState>,
+    data: Form<CodeResponse>,
+    req: HttpRequest<AppState>,
+) -> ConverseResponse {
+    state
+        .oidc
+        .send(RetrieveToken(data.0))
+        .flatten()
+        .map(|author| LookupOrCreateUser {
+            email: author.email,
+            name: author.name,
+        })
+        .and_then(move |msg| state.db.send(msg).from_err())
+        .flatten()
+        .and_then(move |user| {
+            info!("Completed login for user {} ({})", user.email, user.id);
+            req.remember(user.id.to_string());
+            Ok(HttpResponse::SeeOther().header("Location", "/").finish())
+        })
+        .responder()
+}
+
+/// This is an extension trait to enable easy serving of embedded
+/// static content.
+///
+/// It is intended to be called with `include_bytes!()` when setting
+/// up the actix-web application.
+pub trait EmbeddedFile {
+    fn static_file(self, path: &'static str, content: &'static [u8]) -> Self;
+}
+
+impl EmbeddedFile for App<AppState> {
+    fn static_file(self, path: &'static str, content: &'static [u8]) -> Self {
+        self.route(path, Method::GET, move |_: HttpRequest<_>| {
+            let mime = format!("{}", mime_guess::from_path(path).first_or_octet_stream());
+            HttpResponse::Ok().content_type(mime.as_str()).body(content)
+        })
+    }
+}
+
+/// Middleware used to enforce logins unceremoniously.
+pub struct RequireLogin;
+
+impl<S> Middleware<S> for RequireLogin {
+    fn start(&self, req: &HttpRequest<S>) -> actix_web::Result<Started> {
+        let logged_in = req.identity().is_some();
+        let is_oidc_req = req.path().starts_with("/oidc");
+
+        if !is_oidc_req && !logged_in {
+            Ok(Started::Response(
+                HttpResponse::SeeOther()
+                    .header("Location", "/oidc/login")
+                    .finish(),
+            ))
+        } else {
+            Ok(Started::Done)
+        }
+    }
+}
diff --git a/web/converse/src/main.rs b/web/converse/src/main.rs
new file mode 100644
index 0000000000..78d0241600
--- /dev/null
+++ b/web/converse/src/main.rs
@@ -0,0 +1,239 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+extern crate askama;
+
+#[macro_use]
+extern crate diesel;
+
+#[macro_use]
+extern crate failure;
+
+#[macro_use]
+extern crate log;
+
+#[macro_use]
+extern crate serde_derive;
+
+extern crate actix;
+extern crate actix_web;
+extern crate chrono;
+extern crate comrak;
+extern crate crimp;
+extern crate curl;
+extern crate env_logger;
+extern crate futures;
+extern crate hyper;
+extern crate md5;
+extern crate mime_guess;
+extern crate r2d2;
+extern crate rand;
+extern crate rouille;
+extern crate serde;
+extern crate serde_json;
+extern crate tokio;
+extern crate tokio_timer;
+extern crate url;
+extern crate url_serde;
+
+/// Simple macro used to reduce boilerplate when defining actor
+/// message types.
+macro_rules! message {
+    ( $t:ty, $r:ty ) => {
+        impl Message for $t {
+            type Result = $r;
+        }
+    };
+}
+
+pub mod db;
+pub mod errors;
+pub mod handlers;
+pub mod models;
+pub mod oidc;
+pub mod render;
+pub mod schema;
+
+use crate::db::*;
+use crate::handlers::*;
+use crate::oidc::OidcExecutor;
+use crate::render::Renderer;
+use actix::prelude::*;
+use actix_web::http::Method;
+use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
+use actix_web::middleware::Logger;
+use actix_web::*;
+use diesel::pg::PgConnection;
+use diesel::r2d2::{ConnectionManager, Pool};
+use rand::{OsRng, Rng};
+use std::env;
+
+fn config(name: &str) -> String {
+    env::var(name).expect(&format!("{} must be set", name))
+}
+
+fn config_default(name: &str, default: &str) -> String {
+    env::var(name).unwrap_or(default.into())
+}
+
+fn start_db_executor() -> Addr<DbExecutor> {
+    info!("Initialising database connection pool ...");
+    let db_url = config("DATABASE_URL");
+
+    let manager = ConnectionManager::<PgConnection>::new(db_url);
+    let pool = Pool::builder()
+        .build(manager)
+        .expect("Failed to initialise DB pool");
+
+    SyncArbiter::start(2, move || DbExecutor(pool.clone()))
+}
+
+fn schedule_search_refresh(db: Addr<DbExecutor>) {
+    use std::thread;
+    use std::time::{Duration, Instant};
+    use tokio::prelude::*;
+    use tokio::timer::Interval;
+
+    let task = Interval::new(Instant::now(), Duration::from_secs(60))
+        .from_err()
+        .for_each(move |_| db.send(db::RefreshSearchView).flatten())
+        .map_err(|err| error!("Error while updating search view: {}", err));
+
+    thread::spawn(|| tokio::run(task));
+}
+
+fn start_oidc_executor(base_url: &str) -> Addr<OidcExecutor> {
+    info!("Initialising OIDC integration ...");
+    let oidc_url = config("OIDC_DISCOVERY_URL");
+    let oidc_config =
+        oidc::load_oidc(&oidc_url).expect("Failed to retrieve OIDC discovery document");
+
+    let oidc = oidc::OidcExecutor {
+        oidc_config,
+        client_id: config("OIDC_CLIENT_ID"),
+        client_secret: config("OIDC_CLIENT_SECRET"),
+        redirect_uri: format!("{}/oidc/callback", base_url),
+    };
+
+    oidc.start()
+}
+
+fn start_renderer() -> Addr<Renderer> {
+    let comrak = comrak::ComrakOptions {
+        github_pre_lang: true,
+        ext_strikethrough: true,
+        ext_table: true,
+        ext_autolink: true,
+        ext_tasklist: true,
+        ext_footnotes: true,
+        ext_tagfilter: true,
+        ..Default::default()
+    };
+
+    Renderer { comrak }.start()
+}
+
+fn gen_session_key() -> [u8; 64] {
+    let mut key_bytes = [0; 64];
+    let mut rng = OsRng::new().expect("Failed to retrieve RNG for key generation");
+    rng.fill_bytes(&mut key_bytes);
+
+    key_bytes
+}
+
+fn start_http_server(
+    base_url: String,
+    db_addr: Addr<DbExecutor>,
+    oidc_addr: Addr<OidcExecutor>,
+    renderer_addr: Addr<Renderer>,
+) {
+    info!("Initialising HTTP server ...");
+    let bind_host = config_default("CONVERSE_BIND_HOST", "127.0.0.1:4567");
+    let key = gen_session_key();
+    let require_login = config_default("REQUIRE_LOGIN", "true".into()) == "true";
+
+    server::new(move || {
+        let state = AppState {
+            db: db_addr.clone(),
+            oidc: oidc_addr.clone(),
+            renderer: renderer_addr.clone(),
+        };
+
+        let identity = IdentityService::new(
+            CookieIdentityPolicy::new(&key)
+                .name("converse_auth")
+                .path("/")
+                .secure(base_url.starts_with("https")),
+        );
+
+        let app = App::with_state(state)
+            .middleware(Logger::default())
+            .middleware(identity)
+            .resource("/", |r| r.method(Method::GET).with(forum_index))
+            .resource("/thread/new", |r| r.method(Method::GET).with(new_thread))
+            .resource("/thread/submit", |r| {
+                r.method(Method::POST).with(submit_thread)
+            })
+            .resource("/thread/reply", |r| {
+                r.method(Method::POST).with(reply_thread)
+            })
+            .resource("/thread/{id}", |r| r.method(Method::GET).with(forum_thread))
+            .resource("/post/{id}/edit", |r| r.method(Method::GET).with(edit_form))
+            .resource("/post/edit", |r| r.method(Method::POST).with(edit_post))
+            .resource("/search", |r| r.method(Method::GET).with(search_forum))
+            .resource("/oidc/login", |r| r.method(Method::GET).with(login))
+            .resource("/oidc/callback", |r| r.method(Method::POST).with(callback))
+            .static_file(
+                "/static/highlight.css",
+                include_bytes!("../static/highlight.css"),
+            )
+            .static_file(
+                "/static/highlight.js",
+                include_bytes!("../static/highlight.js"),
+            )
+            .static_file("/static/styles.css", include_bytes!("../static/styles.css"));
+
+        if require_login {
+            app.middleware(RequireLogin)
+        } else {
+            app
+        }
+    })
+    .bind(&bind_host)
+    .expect(&format!("Could not bind on '{}'", bind_host))
+    .start();
+}
+
+fn main() {
+    env_logger::init();
+
+    info!("Welcome to Converse! Hold on tight while we're getting ready.");
+    let sys = actix::System::new("converse");
+
+    let base_url = config("BASE_URL");
+
+    let db_addr = start_db_executor();
+    let oidc_addr = start_oidc_executor(&base_url);
+    let renderer_addr = start_renderer();
+
+    schedule_search_refresh(db_addr.clone());
+
+    start_http_server(base_url, db_addr, oidc_addr, renderer_addr);
+
+    sys.run();
+}
diff --git a/web/converse/src/models.rs b/web/converse/src/models.rs
new file mode 100644
index 0000000000..63b15fbed0
--- /dev/null
+++ b/web/converse/src/models.rs
@@ -0,0 +1,127 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+use crate::schema::{posts, simple_posts, threads, users};
+use chrono::prelude::{DateTime, Utc};
+use diesel::sql_types::{Integer, Text};
+
+/// Represents a single user in the Converse database. Converse does
+/// not handle logins itself, but rather looks them up based on the
+/// email address received from an OIDC provider.
+#[derive(Identifiable, Queryable, Serialize)]
+pub struct User {
+    pub id: i32,
+    pub name: String,
+    pub email: String,
+    pub admin: bool,
+}
+
+#[derive(Identifiable, Queryable, Serialize, Associations)]
+#[belongs_to(User)]
+pub struct Thread {
+    pub id: i32,
+    pub title: String,
+    pub posted: DateTime<Utc>,
+    pub sticky: bool,
+    pub user_id: i32,
+    pub closed: bool,
+}
+
+#[derive(Identifiable, Queryable, Serialize, Associations)]
+#[belongs_to(Thread)]
+#[belongs_to(User)]
+pub struct Post {
+    pub id: i32,
+    pub thread_id: i32,
+    pub body: String,
+    pub posted: DateTime<Utc>,
+    pub user_id: i32,
+}
+
+/// This struct is used as the query result type for the simplified
+/// post view, which already joins user information in the database.
+#[derive(Identifiable, Queryable, Serialize, Associations)]
+#[belongs_to(Thread)]
+pub struct SimplePost {
+    pub id: i32,
+    pub thread_id: i32,
+    pub body: String,
+    pub posted: DateTime<Utc>,
+    pub user_id: i32,
+    pub closed: bool,
+    pub author_name: String,
+    pub author_email: String,
+}
+
+/// This struct is used as the query result type for the thread index
+/// view, which lists the index of threads ordered by the last post in
+/// each thread.
+#[derive(Queryable, Serialize)]
+pub struct ThreadIndex {
+    pub thread_id: i32,
+    pub title: String,
+    pub thread_author: String,
+    pub created: DateTime<Utc>,
+    pub sticky: bool,
+    pub closed: bool,
+    pub post_id: i32,
+    pub post_author: String,
+    pub posted: DateTime<Utc>,
+}
+
+#[derive(Deserialize, Insertable)]
+#[table_name = "threads"]
+pub struct NewThread {
+    pub title: String,
+    pub user_id: i32,
+}
+
+#[derive(Deserialize, Insertable)]
+#[table_name = "users"]
+pub struct NewUser {
+    pub email: String,
+    pub name: String,
+}
+
+#[derive(Deserialize, Insertable)]
+#[table_name = "posts"]
+pub struct NewPost {
+    pub thread_id: i32,
+    pub body: String,
+    pub user_id: i32,
+}
+
+/// This struct models the response of a full-text search query. It
+/// does not use a table/schema definition struct like the other
+/// tables, as no table of this type actually exists.
+#[derive(QueryableByName, Debug, Serialize)]
+pub struct SearchResult {
+    #[sql_type = "Integer"]
+    pub post_id: i32,
+    #[sql_type = "Integer"]
+    pub thread_id: i32,
+    #[sql_type = "Text"]
+    pub author: String,
+    #[sql_type = "Text"]
+    pub title: String,
+
+    /// Headline represents the result of Postgres' ts_headline()
+    /// function, which highlights search terms in the search results.
+    #[sql_type = "Text"]
+    pub headline: String,
+}
diff --git a/web/converse/src/oidc.rs b/web/converse/src/oidc.rs
new file mode 100644
index 0000000000..75e3eabc88
--- /dev/null
+++ b/web/converse/src/oidc.rs
@@ -0,0 +1,170 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+//! This module provides authentication via OIDC compliant
+//! authentication sources.
+//!
+//! Currently Converse only supports a single OIDC provider. Note that
+//! this has so far only been tested with Office365.
+
+use crate::errors::*;
+use actix::prelude::*;
+use crimp::Request;
+use curl::easy::Form;
+use url::Url;
+use url_serde;
+
+/// This structure represents the contents of an OIDC discovery
+/// document.
+#[derive(Deserialize, Debug, Clone)]
+pub struct OidcConfig {
+    #[serde(with = "url_serde")]
+    authorization_endpoint: Url,
+    token_endpoint: String,
+    userinfo_endpoint: String,
+
+    scopes_supported: Vec<String>,
+    issuer: String,
+}
+
+#[derive(Clone, Debug)]
+pub struct OidcExecutor {
+    pub client_id: String,
+    pub client_secret: String,
+    pub redirect_uri: String,
+    pub oidc_config: OidcConfig,
+}
+
+/// This struct represents the form response returned by an OIDC
+/// provider with the `code`.
+#[derive(Debug, Deserialize)]
+pub struct CodeResponse {
+    pub code: String,
+}
+
+/// This struct represents the data extracted from the ID token and
+/// stored in the user's session.
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Author {
+    pub name: String,
+    pub email: String,
+}
+
+impl Actor for OidcExecutor {
+    type Context = Context<Self>;
+}
+
+/// Message used to request the login URL:
+pub struct GetLoginUrl; // TODO: Add a nonce parameter stored in session.
+message!(GetLoginUrl, String);
+
+impl Handler<GetLoginUrl> for OidcExecutor {
+    type Result = String;
+
+    fn handle(&mut self, _: GetLoginUrl, _: &mut Self::Context) -> Self::Result {
+        let mut url: Url = self.oidc_config.authorization_endpoint.clone();
+        {
+            let mut params = url.query_pairs_mut();
+            params.append_pair("client_id", &self.client_id);
+            params.append_pair("response_type", "code");
+            params.append_pair("scope", "openid");
+            params.append_pair("redirect_uri", &self.redirect_uri);
+            params.append_pair("response_mode", "form_post");
+        }
+        return url.into_string();
+    }
+}
+
+/// Message used to request the token from the returned code and
+/// retrieve userinfo from the appropriate endpoint.
+pub struct RetrieveToken(pub CodeResponse);
+message!(RetrieveToken, Result<Author>);
+
+#[derive(Debug, Deserialize)]
+struct TokenResponse {
+    access_token: String,
+}
+
+// TODO: This is currently hardcoded to Office365 fields.
+#[derive(Debug, Deserialize)]
+struct Userinfo {
+    name: String,
+    unique_name: String, // email in office365
+}
+
+impl Handler<RetrieveToken> for OidcExecutor {
+    type Result = Result<Author>;
+
+    fn handle(&mut self, msg: RetrieveToken, _: &mut Self::Context) -> Self::Result {
+        debug!("Received OAuth2 code, requesting access_token");
+
+        let mut form = Form::new();
+        form.part("client_id")
+            .contents(&self.client_id.as_bytes())
+            .add()
+            .expect("critical error: invalid form data");
+
+        form.part("client_secret")
+            .contents(&self.client_secret.as_bytes())
+            .add()
+            .expect("critical error: invalid form data");
+
+        form.part("grant_type")
+            .contents("authorization_code".as_bytes())
+            .add()
+            .expect("critical error: invalid form data");
+
+        form.part("code")
+            .contents(&msg.0.code.as_bytes())
+            .add()
+            .expect("critical error: invalid form data");
+
+        form.part("redirect_uri")
+            .contents(&self.redirect_uri.as_bytes())
+            .add()
+            .expect("critical error: invalid form data");
+
+        let response = Request::post(&self.oidc_config.token_endpoint)
+            .user_agent(concat!("converse-", env!("CARGO_PKG_VERSION")))?
+            .form(form)
+            .send()?;
+
+        debug!("Received token response: {:?}", response);
+        let token: TokenResponse = response.as_json()?.body;
+
+        let bearer = format!("Bearer {}", token.access_token);
+        let user: Userinfo = Request::get(&self.oidc_config.userinfo_endpoint)
+            .user_agent(concat!("converse-", env!("CARGO_PKG_VERSION")))?
+            .header("Authorization", &bearer)?
+            .send()?
+            .as_json()?
+            .body;
+
+        Ok(Author {
+            name: user.name,
+            email: user.unique_name,
+        })
+    }
+}
+
+/// Convenience function to attempt loading an OIDC discovery document
+/// from a specified URL:
+pub fn load_oidc(url: &str) -> Result<OidcConfig> {
+    let config: OidcConfig = Request::get(url).send()?.as_json()?.body;
+    Ok(config)
+}
diff --git a/web/converse/src/render.rs b/web/converse/src/render.rs
new file mode 100644
index 0000000000..d06af12bd9
--- /dev/null
+++ b/web/converse/src/render.rs
@@ -0,0 +1,245 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+//! This module defines a rendering actor used for processing Converse
+//! data into whatever format is needed by the templates and rendering
+//! them.
+
+use crate::errors::*;
+use crate::models::*;
+use actix::prelude::*;
+use askama::Template;
+use chrono::prelude::{DateTime, Utc};
+use comrak::{markdown_to_html, ComrakOptions};
+use md5;
+use std::fmt;
+
+pub struct Renderer {
+    pub comrak: ComrakOptions,
+}
+
+impl Actor for Renderer {
+    type Context = actix::Context<Self>;
+}
+
+/// Represents a data formatted for human consumption
+#[derive(Debug)]
+struct FormattedDate(DateTime<Utc>);
+
+impl fmt::Display for FormattedDate {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.0.format("%a %d %B %Y, %R"))
+    }
+}
+
+#[derive(Debug)]
+struct IndexThread {
+    id: i32,
+    title: String,
+    sticky: bool,
+    closed: bool,
+    posted: FormattedDate,
+    author_name: String,
+    post_author: String,
+}
+
+#[derive(Template)]
+#[template(path = "index.html")]
+struct IndexPageTemplate {
+    threads: Vec<IndexThread>,
+}
+
+// "Renderable" structures with data transformations applied.
+#[derive(Debug)]
+struct RenderablePost {
+    id: i32,
+    body: String,
+    posted: FormattedDate,
+    author_name: String,
+    author_gravatar: String,
+    editable: bool,
+}
+
+/// This structure represents the transformed thread data with
+/// Markdown rendering and other changes applied.
+#[derive(Template)]
+#[template(path = "thread.html")]
+struct RenderableThreadPage {
+    id: i32,
+    title: String,
+    closed: bool,
+    posts: Vec<RenderablePost>,
+}
+
+/// Helper function for computing Gravatar links.
+fn md5_hex(input: &[u8]) -> String {
+    format!("{:x}", md5::compute(input))
+}
+
+/// The different types of editing modes supported by the editing
+/// template:
+#[derive(Debug, PartialEq)]
+pub enum EditingMode {
+    NewThread,
+    PostReply,
+    EditPost,
+}
+
+impl Default for EditingMode {
+    fn default() -> EditingMode {
+        EditingMode::NewThread
+    }
+}
+
+/// This is the template used for rendering the new thread, edit post
+/// and reply to thread forms.
+#[derive(Template, Default)]
+#[template(path = "post.html")]
+pub struct FormTemplate {
+    /// Which editing mode is to be used by the template?
+    pub mode: EditingMode,
+
+    /// Potential alerts to display to the user (e.g. input validation
+    /// results)
+    pub alerts: Vec<&'static str>,
+
+    /// Either the title to be used in the subject field or the title
+    /// of the thread the user is responding to.
+    pub title: Option<String>,
+
+    /// Body of the post being edited, if present.
+    pub post: Option<String>,
+
+    /// ID of the thread being replied to or the post being edited.
+    pub id: Option<i32>,
+}
+
+/// Message used to render new thread page.
+///
+/// It can optionally contain a vector of warnings to display to the
+/// user in alert boxes, such as input validation errors.
+#[derive(Default)]
+pub struct NewThreadPage {
+    pub alerts: Vec<&'static str>,
+    pub title: Option<String>,
+    pub post: Option<String>,
+}
+message!(NewThreadPage, Result<String>);
+
+impl Handler<NewThreadPage> for Renderer {
+    type Result = Result<String>;
+
+    fn handle(&mut self, msg: NewThreadPage, _: &mut Self::Context) -> Self::Result {
+        let ctx = FormTemplate {
+            alerts: msg.alerts,
+            title: msg.title,
+            post: msg.post,
+            ..Default::default()
+        };
+        ctx.render().map_err(|e| e.into())
+    }
+}
+
+/// Message used to render post editing page.
+pub struct EditPostPage {
+    pub id: i32,
+    pub post: String,
+}
+message!(EditPostPage, Result<String>);
+
+impl Handler<EditPostPage> for Renderer {
+    type Result = Result<String>;
+
+    fn handle(&mut self, msg: EditPostPage, _: &mut Self::Context) -> Self::Result {
+        let ctx = FormTemplate {
+            mode: EditingMode::EditPost,
+            id: Some(msg.id),
+            post: Some(msg.post),
+            ..Default::default()
+        };
+
+        ctx.render().map_err(|e| e.into())
+    }
+}
+
+/// Message used to render search results
+#[derive(Template)]
+#[template(path = "search.html")]
+pub struct SearchResultPage {
+    pub query: String,
+    pub results: Vec<SearchResult>,
+}
+message!(SearchResultPage, Result<String>);
+
+impl Handler<SearchResultPage> for Renderer {
+    type Result = Result<String>;
+
+    fn handle(&mut self, msg: SearchResultPage, _: &mut Self::Context) -> Self::Result {
+        msg.render().map_err(|e| e.into())
+    }
+}
+
+// TODO: actor-free implementation below
+
+/// Render the index page for the given thread list.
+pub fn index_page(threads: Vec<ThreadIndex>) -> Result<String> {
+    let threads: Vec<IndexThread> = threads
+        .into_iter()
+        .map(|thread| IndexThread {
+            id: thread.thread_id,
+            title: thread.title, // escape_html(&thread.title),
+            sticky: thread.sticky,
+            closed: thread.closed,
+            posted: FormattedDate(thread.posted),
+            author_name: thread.thread_author,
+            post_author: thread.post_author,
+        })
+        .collect();
+
+    let tpl = IndexPageTemplate { threads };
+    tpl.render().map_err(|e| e.into())
+}
+
+// Render the page of a given thread.
+pub fn thread_page(user: i32, thread: Thread, posts: Vec<SimplePost>) -> Result<String> {
+    let posts = posts
+        .into_iter()
+        .map(|post| {
+            let editable = user != 1 && post.user_id == user;
+
+            let comrak = ComrakOptions::default(); // TODO(tazjin): cheddar
+            RenderablePost {
+                id: post.id,
+                body: markdown_to_html(&post.body, &comrak),
+                posted: FormattedDate(post.posted),
+                author_name: post.author_name.clone(),
+                author_gravatar: md5_hex(post.author_email.as_bytes()),
+                editable,
+            }
+        })
+        .collect();
+
+    let renderable = RenderableThreadPage {
+        posts,
+        closed: thread.closed,
+        id: thread.id,
+        title: thread.title,
+    };
+
+    Ok(renderable.render()?)
+}
diff --git a/web/converse/src/schema.rs b/web/converse/src/schema.rs
new file mode 100644
index 0000000000..520af43422
--- /dev/null
+++ b/web/converse/src/schema.rs
@@ -0,0 +1,83 @@
+// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su>
+//
+// This file is part of Converse.
+//
+// This program is free software: you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see
+// <https://www.gnu.org/licenses/>.
+
+table! {
+    posts (id) {
+        id -> Int4,
+        thread_id -> Int4,
+        body -> Text,
+        posted -> Timestamptz,
+        user_id -> Int4,
+    }
+}
+
+table! {
+    threads (id) {
+        id -> Int4,
+        title -> Varchar,
+        posted -> Timestamptz,
+        sticky -> Bool,
+        user_id -> Int4,
+        closed -> Bool,
+    }
+}
+
+table! {
+    users (id) {
+        id -> Int4,
+        email -> Varchar,
+        name -> Varchar,
+        admin -> Bool,
+    }
+}
+
+// Note: Manually inserted as print-schema does not add views.
+table! {
+    simple_posts (id) {
+        id -> Int4,
+        thread_id -> Int4,
+        body -> Text,
+        posted -> Timestamptz,
+        user_id -> Int4,
+        closed -> Bool,
+        author_name -> Text,
+        author_email -> Text,
+    }
+}
+
+// Note: Manually inserted as print-schema does not add views.
+table! {
+    thread_index (thread_id) {
+        thread_id -> Int4,
+        title -> Text,
+        thread_author -> Text,
+        created -> Timestamptz,
+        sticky -> Bool,
+        closed -> Bool,
+        post_id -> Int4,
+        post_author -> Text,
+        posted -> Timestamptz,
+    }
+}
+
+joinable!(posts -> threads (thread_id));
+joinable!(posts -> users (user_id));
+joinable!(threads -> users (user_id));
+joinable!(simple_posts -> threads (thread_id));
+
+allow_tables_to_appear_in_same_query!(posts, threads, users, simple_posts,);
diff --git a/web/converse/static/highlight.css b/web/converse/static/highlight.css
new file mode 100644
index 0000000000..791932b87e
--- /dev/null
+++ b/web/converse/static/highlight.css
@@ -0,0 +1,99 @@
+/*
+
+github.com style (c) Vasily Polovnyov <vast@whiteants.net>
+
+*/
+
+.hljs {
+  display: block;
+  overflow-x: auto;
+  padding: 0.5em;
+  color: #333;
+  background: #f8f8f8;
+}
+
+.hljs-comment,
+.hljs-quote {
+  color: #998;
+  font-style: italic;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-subst {
+  color: #333;
+  font-weight: bold;
+}
+
+.hljs-number,
+.hljs-literal,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-tag .hljs-attr {
+  color: #008080;
+}
+
+.hljs-string,
+.hljs-doctag {
+  color: #d14;
+}
+
+.hljs-title,
+.hljs-section,
+.hljs-selector-id {
+  color: #900;
+  font-weight: bold;
+}
+
+.hljs-subst {
+  font-weight: normal;
+}
+
+.hljs-type,
+.hljs-class .hljs-title {
+  color: #458;
+  font-weight: bold;
+}
+
+.hljs-tag,
+.hljs-name,
+.hljs-attribute {
+  color: #000080;
+  font-weight: normal;
+}
+
+.hljs-regexp,
+.hljs-link {
+  color: #009926;
+}
+
+.hljs-symbol,
+.hljs-bullet {
+  color: #990073;
+}
+
+.hljs-built_in,
+.hljs-builtin-name {
+  color: #0086b3;
+}
+
+.hljs-meta {
+  color: #999;
+  font-weight: bold;
+}
+
+.hljs-deletion {
+  background: #fdd;
+}
+
+.hljs-addition {
+  background: #dfd;
+}
+
+.hljs-emphasis {
+  font-style: italic;
+}
+
+.hljs-strong {
+  font-weight: bold;
+}
diff --git a/web/converse/static/highlight.js b/web/converse/static/highlight.js
new file mode 100644
index 0000000000..9ca7d97ba3
--- /dev/null
+++ b/web/converse/static/highlight.js
@@ -0,0 +1,2 @@
+/*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
+!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset<r[0].offset?e:r:"start"===r[0].event?e:r:e.length?e:r}function o(e){function r(e){return" "+e.nodeName+'="'+n(e.value).replace('"',"&quot;")+'"'}s+="<"+t(e)+E.map.call(e.attributes,r).join("")+">"}function u(e){s+="</"+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='<span class="'+a,o=t?"":C;return i+=e+'">',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"<unnamed>")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"<br>":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="</span>",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("erlang",function(e){var r="[a-z'][a-zA-Z0-9_']*",c="("+r+":"+r+"|"+r+")",b={keyword:"after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if let not of orelse|10 query receive rem try when xor",literal:"false true"},i=e.C("%","$"),n={cN:"number",b:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",r:0},a={b:"fun\\s+"+r+"/\\d+"},d={b:c+"\\(",e:"\\)",rB:!0,r:0,c:[{b:c,r:0},{b:"\\(",e:"\\)",eW:!0,rE:!0,r:0}]},o={b:"{",e:"}",r:0},t={b:"\\b_([A-Z][A-Za-z0-9_]*)?",r:0},f={b:"[A-Z][a-zA-Z0-9_]*",r:0},l={b:"#"+e.UIR,r:0,rB:!0,c:[{b:"#"+e.UIR,r:0},{b:"{",e:"}",r:0}]},s={bK:"fun receive if try case",e:"end",k:b};s.c=[i,a,e.inherit(e.ASM,{cN:""}),s,d,e.QSM,n,o,t,f,l];var u=[i,a,s,d,e.QSM,n,o,t,f,l];d.c[1].c=u,o.c=u,l.c[1].c=u;var h={cN:"params",b:"\\(",e:"\\)",c:u};return{aliases:["erl"],k:b,i:"(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))",c:[{cN:"function",b:"^"+r+"\\s*\\(",e:"->",rB:!0,i:"\\(|#|//|/\\*|\\\\|:|;",c:[h,e.inherit(e.TM,{b:r})],starts:{e:";|\\.",k:b,c:u}},i,{b:"^-",e:"\\.",r:0,eE:!0,rB:!0,l:"-"+e.IR,k:"-module -record -undef -export -ifdef -ifndef -author -copyright -doc -vsn -import -include -include_lib -compile -define -else -endif -file -behaviour -behavior -spec",c:[h]},n,e.QSM,l,t,f,o,{b:/\.$/}]}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",literal:"null false true"},t={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},r=e.inherit(t,{i:/\n/}),a={cN:"subst",b:"{",e:"}",k:i},c=e.inherit(a,{i:/\n/}),n={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,c]},s={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},a]},o=e.inherit(s,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},c]});a.c=[s,n,t,e.ASM,e.QSM,e.CNM,e.CBCM],c.c=[o,n,r,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\n/})];var l={v:[s,n,t,e.ASM,e.QSM]},b=e.IR+"(<"+e.IR+"(\\s*,\\s*"+e.IR+")*>)?(\\[\\])?";return{aliases:["csharp"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:"<!--|-->"},{b:"</?",e:">"}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},l,e.CNM,{bK:"class interface",e:/[{;=]/,i:/[^\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{cN:"meta",b:"^\\s*\\[",eB:!0,e:"\\]",eE:!0,c:[{cN:"meta-string",b:/"/,e:/"/}]},{bK:"new return throw await else",r:0},{cN:"function",b:"("+b+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("swift",function(e){var i={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},t={cN:"type",b:"\\b[A-Z][\\wÀ-ʸ']*",r:0},n=e.C("/\\*","\\*/",{c:["self"]}),r={cN:"subst",b:/\\\(/,e:"\\)",k:i,c:[]},a={cN:"number",b:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",r:0},o=e.inherit(e.QSM,{c:[r,e.BE]});return r.c=[a],{k:i,c:[o,e.CLCM,n,t,a,{cN:"function",bK:"func",e:"{",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{b:/</,e:/>/},{cN:"params",b:/\(/,e:/\)/,endsParent:!0,k:i,c:["self",a,o,e.CBCM,{b:":"}],i:/["']/}],i:/\[|%/},{cN:"class",bK:"struct protocol class extension enum",k:i,e:"\\{",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{cN:"meta",b:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},{bK:"import",e:/$/,c:[e.CLCM,n]}]}});hljs.registerLanguage("elixir",function(e){var r="[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?",n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",b="and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote",c={cN:"subst",b:"#\\{",e:"}",l:r,k:b},a={cN:"string",c:[e.BE,c],v:[{b:/'/,e:/'/},{b:/"/,e:/"/}]},i={cN:"function",bK:"def defp defmacro",e:/\B\b/,c:[e.inherit(e.TM,{b:r,endsParent:!0})]},l=e.inherit(i,{cN:"class",bK:"defimpl defmodule defprotocol defrecord",e:/\bdo\b|$|;/}),s=[a,e.HCM,l,i,{cN:"symbol",b:":(?!\\s)",c:[a,{b:n}],r:0},{cN:"symbol",b:r+":",r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"->"},{b:"("+e.RSR+")\\s*",c:[e.HCM,{cN:"regexp",i:"\\n",c:[e.BE,c],v:[{b:"/",e:"/[a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];return c.c=s,{l:r,k:b,c:s}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("erlang-repl",function(e){return{k:{built_in:"spawn spawn_link self",keyword:"after and andalso|10 band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse|10 query receive rem try when xor"},c:[{cN:"meta",b:"^[0-9]+> ",r:10},e.C("%","$"),{cN:"number",b:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",r:0},e.ASM,e.QSM,{b:"\\?(::)?([A-Z]\\w*(::)?)+"},{b:"->"},{b:"ok"},{b:"!"},{b:"(\\b[a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*)|(\\b[a-z'][a-zA-Z0-9_']*)",r:0},{b:"[A-Z][a-zA-Z0-9_']*",r:0}]}});hljs.registerLanguage("elm",function(e){var i={v:[e.C("--","$"),e.C("{-","-}",{c:["self"]})]},t={cN:"type",b:"\\b[A-Z][\\w']*",r:0},c={b:"\\(",e:"\\)",i:'"',c:[{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},i]},n={b:"{",e:"}",c:c.c};return{k:"let in if then else case of where module import exposing type alias as infix infixl infixr port effect command subscription",c:[{bK:"port effect module",e:"exposing",k:"port effect module where command subscription exposing",c:[c,i],i:"\\W\\.|;"},{b:"import",e:"$",k:"import as exposing",c:[c,i],i:"\\W\\.|;"},{b:"type",e:"$",k:"type alias",c:[t,c,n,i]},{bK:"infix infixl infixr",e:"$",c:[e.CNM,i]},{b:"port",e:"$",k:"port",c:[i]},e.QSM,e.CNM,t,e.inherit(e.TM,{b:"^[_a-z][\\w']*"}),i,{b:"->|<-"}],i:/;/}});hljs.registerLanguage("clojure",function(e){var t={"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},c=e.inherit(e.QSM,{i:null}),i=e.C(";","$",{r:0}),d={cN:"literal",b:/\b(true|false|nil)\b/},l={b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p=e.C("\\^\\{","\\}"),u={cN:"symbol",b:"[:]{1,2}"+n},f={b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"name",b:n,starts:h},b=[f,c,m,p,i,u,l,s,d,o];return f.c=[e.C("comment",""),y,h],h.c=b,l.c=b,p.c=[l],{aliases:["clj"],i:/\S/,c:[f,c,m,p,i,u,l,s,d]}});hljs.registerLanguage("dns",function(d){return{aliases:["bind","zone"],k:{keyword:"IN A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT"},c:[d.C(";","$",{r:0}),{cN:"meta",b:/^\$(TTL|GENERATE|INCLUDE|ORIGIN)\b/},{cN:"number",b:"((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\b"},{cN:"number",b:"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b"},d.inherit(d.NM,{b:/\b\d+[dhwm]?/})]}});hljs.registerLanguage("dockerfile",function(e){return{aliases:["docker"],cI:!0,k:"from maintainer expose env arg user onbuild stopsignal",c:[e.HCM,e.ASM,e.QSM,e.NM,{bK:"run cmd entrypoint volume add copy workdir label healthcheck shell",starts:{e:/[^\\]\n/,sL:"bash"}}],i:"</"}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/</,r:0,c:[{cN:"attr",b:e,r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"<!DOCTYPE",e:">",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("<!--","-->",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"<style(?=\\s|>|$)",e:">",k:{name:"style"},c:[t],starts:{e:"</style>",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"<script(?=\\s|>|$)",e:">",k:{name:"script"},c:[t],starts:{e:"</script>",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"</?",e:"/?>",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",w=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage("lua",function(e){var t="\\[=*\\[",a="\\]=*\\]",r={b:t,e:a,c:["self"]},n=[e.C("--(?!"+t+")","$"),e.C("--"+t,a,{c:[r],r:10})];return{l:e.UIR,k:{literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},c:n.concat([{cN:"function",bK:"function",e:"\\)",c:[e.inherit(e.TM,{b:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{cN:"params",b:"\\(",eW:!0,c:n}].concat(n)},e.CNM,e.ASM,e.QSM,{cN:"string",b:t,e:a,c:[r],r:5}])}});hljs.registerLanguage("shell",function(s){return{aliases:["console"],c:[{cN:"meta",b:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{e:"$",sL:"bash"}}]}});hljs.registerLanguage("scheme",function(e){var t="[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",r="(\\-|\\+)?\\d+([./]\\d+)?",a=r+"[+\\-]"+r+"i",i={"builtin-name":"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"},n={cN:"meta",b:"^#!",e:"$"},c={cN:"literal",b:"(#t|#f|#\\\\"+t+"|#\\\\.)"},l={cN:"number",v:[{b:r,r:0},{b:a,r:0},{b:"#b[0-1]+(/[0-1]+)?"},{b:"#o[0-7]+(/[0-7]+)?"},{b:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},s=e.QSM,o=[e.C(";","$",{r:0}),e.C("#\\|","\\|#")],u={b:t,r:0},p={cN:"symbol",b:"'"+t},d={eW:!0,r:0},m={v:[{b:/'/},{b:"`"}],c:[{b:"\\(",e:"\\)",c:["self",c,s,l,u,p]}]},g={cN:"name",b:t,l:t,k:i},h={b:/lambda/,eW:!0,rB:!0,c:[g,{b:/\(/,e:/\)/,endsParent:!0,c:[u]}]},b={v:[{b:"\\(",e:"\\)"},{b:"\\[",e:"\\]"}],c:[h,g,d]};return d.c=[c,l,s,u,p,m,b].concat(o),{i:/\S/,c:[n,l,s,p,m,b].concat(o)}});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage("typescript",function(e){var r={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"};return{aliases:["ts"],k:r,c:[{cN:"meta",b:/^\s*['"]use strict['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+e.IR+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:e.IR},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:["self",e.CLCM,e.CBCM]}]}]}],r:0},{cN:"function",b:"function",e:/[\{;]/,eE:!0,k:r,c:["self",e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:[e.CLCM,e.CBCM],i:/["'\(]/}],i:/%/,r:0},{bK:"constructor",e:/\{/,eE:!0,c:["self",{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:[e.CLCM,e.CBCM],i:/["'\(]/}]},{b:/module\./,k:{built_in:"module"},r:0},{bK:"module",e:/\{/,eE:!0},{bK:"interface",e:/\{/,eE:!0,k:"interface extends"},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("lisp",function(b){var e="[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",c="\\|[^]*?\\|",r="(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",a={cN:"meta",b:"^#!",e:"$"},l={cN:"literal",b:"\\b(t{1}|nil)\\b"},n={cN:"number",v:[{b:r,r:0},{b:"#(b|B)[0-1]+(/[0-1]+)?"},{b:"#(o|O)[0-7]+(/[0-7]+)?"},{b:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{b:"#(c|C)\\("+r+" +"+r,e:"\\)"}]},i=b.inherit(b.QSM,{i:null}),t=b.C(";","$",{r:0}),s={b:"\\*",e:"\\*"},u={cN:"symbol",b:"[:&]"+e},d={b:e,r:0},f={b:c},m={b:"\\(",e:"\\)",c:["self",l,i,n,d]},o={c:[n,i,s,u,m,d],v:[{b:"['`]\\(",e:"\\)"},{b:"\\(quote ",e:"\\)",k:{name:"quote"}},{b:"'"+c}]},v={v:[{b:"'"+e},{b:"#'"+e+"(::"+e+")*"}]},N={b:"\\(\\s*",e:"\\)"},A={eW:!0,r:0};return N.c=[{cN:"name",v:[{b:e},{b:c}]},A],A.c=[o,v,N,l,n,i,t,s,u,f,d],{i:/\S/,c:[n,a,l,i,t,o,v,N,d]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/</,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("java",function(e){var a="[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",t=a+"(<"+a+"(\\s*,\\s*"+a+")*>)?",r="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",s="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",c={cN:"number",b:s,r:0};return{aliases:["jsp"],k:r,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:r,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:r,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},c,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("clojure-repl",function(e){return{c:[{cN:"meta",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure"}}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U)?L?"',e:'"',i:"\\n",c:[t.BE]},{b:'(u8?|U)?R"',e:'"',c:[t.BE]},{b:"'\\\\?.",e:"'",i:"."}]},s={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:/<[^\n>]*>/,e:/$/,i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp"],k:c,i:"</",c:n.concat([i,{b:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",e:">",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e]},t.CLCM,t.CBCM,i]},{cN:"class",bK:"class struct",e:/[{;:]/,c:[{b:/</,e:/>/,c:["self"]},t.TM]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("kotlin",function(e){var t={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit initinterface annotation data sealed internal infix operator out by constructor super trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},r={cN:"keyword",b:/\b(break|continue|return|this)\b/,starts:{c:[{cN:"symbol",b:/@\w+/}]}},i={cN:"symbol",b:e.UIR+"@"},n={cN:"subst",b:"\\${",e:"}",c:[e.ASM,e.CNM]},a={cN:"variable",b:"\\$"+e.UIR},c={cN:"string",v:[{b:'"""',e:'"""',c:[a,n]},{b:"'",e:"'",i:/\n/,c:[e.BE]},{b:'"',e:'"',i:/\n/,c:[e.BE,a,n]}]},s={cN:"meta",b:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UIR+")?"},o={cN:"meta",b:"@"+e.UIR,c:[{b:/\(/,e:/\)/,c:[e.inherit(c,{cN:"meta-string"})]}]};return{k:t,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,r,i,s,o,{cN:"function",bK:"fun",e:"[(]|$",rB:!0,eE:!0,k:t,i:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,r:5,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"type",b:/</,e:/>/,k:"reified",r:0},{cN:"params",b:/\(/,e:/\)/,endsParent:!0,k:t,r:0,c:[{b:/:/,e:/[=,\/]/,eW:!0,c:[{cN:"type",b:e.UIR},e.CLCM,e.CBCM],r:0},e.CLCM,e.CBCM,s,o,c,e.CNM]},e.CBCM]},{cN:"class",bK:"class interface trait",e:/[:\{(]|$/,eE:!0,i:"extends implements",c:[{bK:"public protected internal private constructor"},e.UTM,{cN:"type",b:/</,e:/>/,eB:!0,eE:!0,r:0},{cN:"type",b:/[,:]\s*/,e:/[<\(,]|$/,eB:!0,rE:!0},s,o]},c,{cN:"meta",b:"^#!/usr/bin/env",e:"$",i:"\n"},e.CNM]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("makefile",function(e){var i={cN:"variable",v:[{b:"\\$\\("+e.UIR+"\\)",c:[e.BE]},{b:/\$[@%<?\^\+\*]/}]},r={cN:"string",b:/"/,e:/"/,c:[e.BE,i]},a={cN:"variable",b:/\$\([\w-]+\s/,e:/\)/,k:{built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"},c:[i]},n={b:"^"+e.UIR+"\\s*[:+?]?=",i:"\\n",rB:!0,c:[{b:"^"+e.UIR,e:"[:+?]?=",eE:!0}]},t={cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},l={cN:"section",b:/^[^\s]+:/,e:/$/,c:[i]};return{aliases:["mk","mak"],k:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath",l:/[\w-]+/,c:[e.HCM,i,r,a,n,t,l]}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("rust",function(e){var t="([ui](8|16|32|64|128|size)|f(32|64))?",r="alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default",n="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{aliases:["rs"],k:{keyword:r,literal:"true false Some None Ok Err",built_in:n},l:e.IR+"!?",i:"</",c:[e.CLCM,e.C("/\\*","\\*/",{c:["self"]}),e.inherit(e.QSM,{b:/b?"/,i:null}),{cN:"string",v:[{b:/r(#*)"(.|\n)*?"\1(?!#)/},{b:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{cN:"symbol",b:/'[a-zA-Z_][a-zA-Z0-9_]*/},{cN:"number",v:[{b:"\\b0b([01_]+)"+t},{b:"\\b0o([0-7_]+)"+t},{b:"\\b0x([A-Fa-f0-9_]+)"+t},{b:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)"+t}],r:0},{cN:"function",bK:"fn",e:"(\\(|<)",eE:!0,c:[e.UTM]},{cN:"meta",b:"#\\!?\\[",e:"\\]",c:[{cN:"meta-string",b:/"/,e:/"/}]},{cN:"class",bK:"type",e:";",c:[e.inherit(e.UTM,{endsParent:!0})],i:"\\S"},{cN:"class",bK:"trait enum struct union",e:"{",c:[e.inherit(e.UTM,{endsParent:!0})],i:"[\\w\\d]"},{b:e.IR+"::",k:{built_in:n}},{b:"->"}]}});hljs.registerLanguage("haskell",function(e){var i={v:[e.C("--","$"),e.C("{-","-}",{c:["self"]})]},a={cN:"meta",b:"{-#",e:"#-}"},l={cN:"meta",b:"^#",e:"$"},c={cN:"type",b:"\\b[A-Z][\\w']*",r:0},n={b:"\\(",e:"\\)",i:'"',c:[a,l,{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TM,{b:"[_a-z][\\w']*"}),i]},s={b:"{",e:"}",c:n.c};return{aliases:["hs"],k:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",c:[{bK:"module",e:"where",k:"module where",c:[n,i],i:"\\W\\.|;"},{b:"\\bimport\\b",e:"$",k:"import qualified as hiding",c:[n,i],i:"\\W\\.|;"},{cN:"class",b:"^(\\s*)?(class|instance)\\b",e:"where",k:"class family instance where",c:[c,n,i]},{cN:"class",b:"\\b(data|(new)?type)\\b",e:"$",k:"data family type newtype deriving",c:[a,c,n,s,i]},{bK:"default",e:"$",c:[c,n,i]},{bK:"infix infixl infixr",e:"$",c:[e.CNM,i]},{b:"\\bforeign\\b",e:"$",k:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",c:[c,e.QSM,i]},{cN:"meta",b:"#!\\/usr\\/bin\\/env runhaskell",e:"$"},a,l,e.QSM,e.CNM,c,e.inherit(e.TM,{b:"^[_a-z][\\w']*"}),i,{b:"->|<-"}]}});hljs.registerLanguage("nix",function(e){var r={keyword:"rec with let in inherit assert if else then",literal:"true false or and null",built_in:"import abort baseNameOf dirOf isNull builtins map removeAttrs throw toString derivation"},t={cN:"subst",b:/\$\{/,e:/}/,k:r},i={b:/[a-zA-Z0-9-_]+(\s*=)/,rB:!0,r:0,c:[{cN:"attr",b:/\S+/}]},s={cN:"string",c:[t],v:[{b:"''",e:"''"},{b:'"',e:'"'}]},a=[e.NM,e.HCM,e.CBCM,s,i];return t.c=a,{aliases:["nixos"],k:r,c:a}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("go",function(e){var t={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],k:t,i:"</",c:[e.CLCM,e.CBCM,{cN:"string",v:[e.QSM,{b:"'",e:"[^\\\\]'"},{b:"`",e:"`"}]},{cN:"number",v:[{b:e.CNR+"[dflsi]",r:1},e.CNM]},{b:/:=/},{cN:"function",bK:"func",e:/\s*\{/,eE:!0,c:[e.TM,{cN:"params",b:/\(/,e:/\)/,k:t,i:/["']/}]}]}});hljs.registerLanguage("gradle",function(e){return{cI:!0,k:{keyword:"task project allprojects subprojects artifacts buildscript configurations dependencies repositories sourceSets description delete from into include exclude source classpath destinationDir includes options sourceCompatibility targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant def abstract break case catch continue default do else extends final finally for if implements instanceof native new private protected public return static switch synchronized throw throws transient try volatile while strictfp package import false null super this true antlrtask checkstyle codenarc copy boolean byte char class double float int interface long short void compile runTime file fileTree abs any append asList asWritable call collect compareTo count div dump each eachByte eachFile eachLine every find findAll flatten getAt getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter newReader newWriter next plus pop power previous print println push putAt read readBytes readLines reverse reverseEach round size sort splitEachLine step subMap times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader withStream withWriter withWriterAppend write writeLine"},c:[e.CLCM,e.CBCM,e.ASM,e.QSM,e.NM,e.RM]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}|	)",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.initHighlightingOnLoad();
diff --git a/web/converse/static/styles.css b/web/converse/static/styles.css
new file mode 100644
index 0000000000..b57fd899ea
--- /dev/null
+++ b/web/converse/static/styles.css
@@ -0,0 +1,145 @@
+* :not(.material-icons) {
+  font-family: 'Ubuntu', sans-serif;
+}
+
+code, pre, code * {
+    font-family: 'Ubuntu Mono', monospace !important;
+}
+
+.cvs-title, .thread-link {
+    text-decoration: none;
+}
+
+.thread-list-item:hover {
+    background-color: #f5f5f5;
+}
+
+.thread-link {
+    padding: 5px;
+    padding-top: 10px;
+}
+
+.thread-title {
+    padding-right: 15vw;
+}
+
+.search-field {
+    margin-right: 15px;
+    max-width: 200px;
+}
+
+.thread-author {
+    font-style: italic;
+    font-size: 85%;
+}
+
+@media only screen and (min-width: 768px) {
+    .converse main {
+        padding-top: 10px;
+        padding-bottom: 10px;
+    }
+}
+
+.mdl-list__item-text-body {
+    max-height: 40px;
+}
+
+.thread-divider:after {
+    border-bottom: 1px solid rgba(0,0,0,.13);
+    content:"";
+    position: absolute;
+    width: 80%;
+}
+
+html, body {
+  margin: 0;
+  padding: 0;
+}
+.converse .mdl-layout__header-row {
+  padding-left: 40px;
+}
+.converse .mdl-layout.is-small-screen .mdl-layout__header-row h3 {
+  font-size: inherit;
+}
+.converse .mdl-card {
+  height: auto;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: column;
+      -ms-flex-direction: column;
+          flex-direction: column;
+}
+.converse .mdl-card > * {
+  height: auto;
+}
+.converse .mdl-card .mdl-card__supporting-text {
+  margin: 40px;
+  -webkit-flex-grow: 1;
+      -ms-flex-positive: 1;
+          flex-grow: 1;
+  padding: 0;
+  color: inherit;
+  width: calc(100% - 80px);
+}
+.mdl-demo.converse .mdl-card__supporting-text h4 {
+  margin-top: 0;
+  margin-bottom: 20px;
+}
+.converse .mdl-card__actions {
+  margin: 0;
+  padding: 4px 40px;
+  color: inherit;
+}
+.converse section.section--center {
+  max-width: 860px;
+}
+.converse .mdl-card .avatar-card {
+    display: flex;
+    flex-direction: column;
+    text-align: center;
+    margin-top: 30px;
+}
+.desktop-avatar {
+    width: 80px;
+    margin-right: auto;
+    margin-left: auto;
+}
+.mobile-avatar {
+    width: 30px;
+    border-radius: 8px;
+    margin-bottom: 5px;
+}
+.mobile-date {
+    text-decoration: none;
+}
+.converse .mdl-card .post-box {
+    margin: 20px;
+}
+.converse .mdl-card .post-actions {
+    display: flex;
+    padding-right: 5px;
+}
+.post-action {
+    margin: 5px;
+    margin-bottom: 10px;
+}
+.converse section.post-section {
+    padding: 5px;
+}
+.post-date {
+    text-decoration: none;
+    font-size: 80%;
+}
+.mdl-layout__content {
+    flex: 1 0 auto;
+}
+.converse .reply-box {
+    padding-top: 10px;
+}
+.search-result {
+    margin: 8px;
+}
+.search-result .mdl-button {
+    margin: 3px;
+}
diff --git a/web/converse/templates/index.html b/web/converse/templates/index.html
new file mode 100644
index 0000000000..07d6e7bfcf
--- /dev/null
+++ b/web/converse/templates/index.html
@@ -0,0 +1,80 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
+    <title>Converse: Index</title>
+
+    <!-- TODO -->
+    <meta http-equiv="Content-Security-Policy" content="script-src https://code.getmdl.io 'self';">
+    <!-- <link rel="shortcut icon" href="images/favicon.png"> -->
+
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+    <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.blue_grey-orange.min.css" />
+    <link rel="stylesheet" href="/static/styles.css">
+  </head>
+  <body class="converse mdl-base mdl-color-text--grey-700 mdl-color--grey-100">
+    <div class="mdl-layout mdl-layout--fixed-header mdl-js-layout mdl-color--grey-100">
+      <header class="mdl-layout__header mdl-layout__header--scroll mdl-color--primary-dark">
+        <div class="mdl-layout__header-row">
+          <a href="/" class="mdl-layout-title mdl-color-text--blue-grey-50 cvs-title">Converse</a>
+          <div class="mdl-layout-spacer"></div>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-color-text--blue-grey-50 search-field">
+            <form method="get" action="/search">
+              <input class="mdl-textfield__input" type="search" id="search-query" aria-label="Search" name="query">
+              <label class="mdl-textfield__label mdl-color-text--blue-grey-100" for="search-query">Search query...</label>
+              <input type="submit" hidden />
+            </form>
+          </div>
+          &nbsp;
+          <a href="/thread/new">
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
+              New Thread
+            </button>
+          </a>
+        </div>
+      </header>
+      <main class="mdl-layout__content">
+        <section class="section--center mdl-grid mdl-grid--no-spacing mdl-shadow--2dp">
+          <div class="mdl-card mdl-cell mdl-cell--12-col">
+            <div class="mdl-card__supporting-text mdl-grid">
+              <h4 class="mdl-cell mdl-cell--12-col">Latest threads:</h4>
+              <ul class="mdl-list">
+                {% for thread in threads %}
+                <li class="mdl-list__item thread-list-item mdl-list__item--three-line">
+                  <a class="thread-link mdl-color-text--grey-800" href="/thread/{{ thread.id }}">
+                    <span class="mdl-list__item-primary-content {% if loop.index < threads.len() %}thread-divider{% endif %}">
+                      <button class="mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-button--colored mdl-list__item-icon">
+                        <i class="material-icons">
+                          {% if thread.sticky %}
+                            announcement
+                          {% else if thread.closed %}
+                            lock
+                          {% else %}
+                            library_books
+                          {% endif %}
+                        </i>
+                      </button>
+                      <span class="thread-title">{{ thread.title }}<span class="thread-author"> by {{ thread.author_name }}</span></span>
+                      <span class="mdl-list__item-text-body">
+                        Last reply by {{ thread.post_author }} on {{ thread.posted }}.
+                      </span>
+                    </span>
+                  </a>
+                </li>
+                {% endfor %}
+              </ul>
+            </div>
+          </div>
+        </section>
+      </main>
+      <footer class="mdl-mini-footer">
+        <div class="mdl-mini-footer--right-section">
+          <p>Powered by <a href="https://code.tvl.fyi/about/web/converse">Converse</a></p>
+        </div>
+      </footer>
+      </div>
+      <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+  </body>
+</html>
diff --git a/web/converse/templates/post.html b/web/converse/templates/post.html
new file mode 100644
index 0000000000..8ea2df1cd3
--- /dev/null
+++ b/web/converse/templates/post.html
@@ -0,0 +1,124 @@
+{#
+  This template is shared by the new thread, reply and post-editing pages.
+
+  The main display differences between the different editing styles are the
+  headline of the page ("Submit new thread", "Reply to thread", "Edit post")
+  and whether or not the subject line field is displayed in the input form.
+
+  Every one of these pages can have a variable length list of alerts submitted
+  into the template, which will be rendered as Boostrap alert boxes above the
+  user input form.
+#}
+
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
+    <title>Converse: Post</title>
+
+    <meta http-equiv="Content-Security-Policy" content="script-src https://code.getmdl.io 'self';">
+    <!-- <link rel="shortcut icon" href="images/favicon.png"> -->
+
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+    <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.blue_grey-orange.min.css" />
+    <link rel="stylesheet" href="/static/styles.css">
+  </head>
+  <body class="converse mdl-base mdl-color-text--grey-700 mdl-color--grey-100">
+    <div class="mdl-layout mdl-layout--fixed-header mdl-js-layout mdl-color--grey-100">
+      <header class="mdl-layout__header mdl-layout__header--scroll mdl-color--primary-dark">
+        <div class="mdl-layout__header-row">
+          <a href="/" class="mdl-layout-title mdl-color-text--blue-grey-50 cvs-title">
+          {% match mode %}
+            {% when EditingMode::NewThread %}
+              Converse: Submit new thread
+            {% when EditingMode::PostReply %}
+              Converse: Reply to thread
+            {% when EditingMode::EditPost %}
+              Converse: Edit post
+          {% endmatch %}
+          </a>
+          <div class="mdl-layout-spacer"></div>
+          <a href="/">
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
+              Back to index
+            </button>
+          </a>
+        </div>
+      </header>
+      <main class="mdl-layout__content mdl-grid">
+        <div class="mdl-card mdl-shadow--2dp mdl-cell--8-col">
+          {% match mode %}
+            {% when EditingMode::NewThread %}
+              <form action="/thread/submit" method="post">
+            {% when EditingMode::PostReply %}
+              <form action="/thread/reply" method="post">
+            {% when EditingMode::EditPost %}
+              <form action="/post/edit" method="post">
+          {% endmatch %}
+            {% match mode %}
+              {% when EditingMode::PostReply %}
+                <input type="hidden" id="thread_id" name="thread_id" value="{{ id.unwrap() }}">
+              {% when EditingMode::EditPost %}
+                <input type="hidden" id="thread_id" name="post_id" value="{{ id.unwrap() }}">
+              {% else %}
+                {# no post ID when making a new thread #}
+            {% endmatch %}
+            <div class="mdl-card__supporting-text">
+              {% for alert in alerts %}
+              <span class="mdl-chip mdl-color--red-200">
+                <span class="mdl-chip__text">{{ alert }}&nbsp;</span>
+              </span>
+              {% endfor %}
+              {% if mode == EditingMode::NewThread %}
+              <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-cell--12-col">
+                <input class="mdl-textfield__input" type="text" id="title" name="title" aria-label="thread title" required {% match title %}{% when Some with (title_text) %}value="{{ title_text }}"{% else %}{# Nothing! #}{% endmatch %}>
+                <label class="mdl-textfield__label" for="title">Thread title</label>
+              </div>
+              {% endif %}
+              <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-cell--12-col">
+                <textarea class="mdl-textfield__input" type="text" rows="25" id="post" name="post" aria-label="post content" required>
+                  {%- match post -%}
+                    {%- when Some with (post_text) -%}
+                      {{- post_text -}}
+                    {%- else -%}
+                      {# Nothing! #}
+                  {%- endmatch -%}
+                </textarea>
+                <label class="mdl-textfield__label" for="body">Content ...</label>
+              </div>
+            </div>
+            <div class="mdl-card__actions">
+              <input class="mdl-button mdl-button--raised mdl-button--colored mdl-js-button mdl-js-ripple-effect" type="submit" value="Submit!">
+            </div>
+          </form>
+        </div>
+        <div class="mdl-card mdl-shadow--2dp mdl-cell--4-col">
+          <div class="mdl-card__title mdl-card--border">
+            Quick Markdown primer:
+          </div>
+          <div class="mdl-card__supporting-text">
+            <p>
+              Remember that you can use <a href="https://daringfireball.net/projects/markdown/basics"><strong>Markdown</strong></a> when
+              writing your posts:
+            </p>
+            <p><i>*italic text*</i></p>
+            <p><strong>**bold text**</strong></p>
+            <p><s>~strikethrough text~</s></p>
+            <p><code>[link text](https://some.link.com/)</code></p>
+            <p><code>![image text](https://foo.com/thing.jpg)</code></p>
+            <p>Use <code>*</code> or <code>-</code> to enumerate lists.</p>
+            <p>See Markdown documentation for more information!</p>
+          </div>
+        </div>
+      </main>
+      <footer class="mdl-mini-footer">
+        <div class="mdl-mini-footer--right-section">
+          <p>Powered by <a href="https://code.tvl.fyi/about/web/converse">Converse</a></p>
+        </div>
+      </footer>
+      </div>
+      <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+  </body>
+</html>
diff --git a/web/converse/templates/search.html b/web/converse/templates/search.html
new file mode 100644
index 0000000000..418440e028
--- /dev/null
+++ b/web/converse/templates/search.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
+        <title>Converse: Search results</title>
+
+        <!-- TODO -->
+        <meta http-equiv="Content-Security-Policy" content="script-src https://code.getmdl.io 'self';">
+        <!-- <link rel="shortcut icon" href="images/favicon.png"> -->
+
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.blue_grey-orange.min.css" />
+        <link rel="stylesheet" href="/static/styles.css">
+    </head>
+    <body class="converse mdl-base mdl-color-text--grey-700 mdl-color--grey-100">
+        <div class="mdl-layout mdl-layout--fixed-header mdl-js-layout mdl-color--grey-100">
+            <header class="mdl-layout__header mdl-layout__header--scroll mdl-color--primary-dark">
+                <div class="mdl-layout__header-row">
+                    <a href="/" class="mdl-layout-title mdl-color-text--blue-grey-50 cvs-title">Converse</a>
+                    <div class="mdl-layout-spacer"></div>
+                    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-color-text--blue-grey-50 search-field">
+                        <form method="get" action="/search">
+                            <input class="mdl-textfield__input" type="search" id="search-query" aria-label="Search" name="query">
+                            <label class="mdl-textfield__label mdl-color-text--blue-grey-100" for="search-query">Search query...</label>
+                            <input type="submit" hidden />
+                        </form>
+                    </div>
+                    &nbsp;
+                    <a href="/">
+                        <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
+                            Back to index
+                        </button>
+                    </a>
+                </div>
+            </header>
+            <main class="mdl-layout__content">
+                <section class="section--center mdl-grid">
+                    <div class="mdl-cell--12-col">
+                        <h4>Search results for '{{ query }}':</h4>
+                    </div>
+                    {% for result in results %}
+                        <div class="mdl-card mdl-cell--6-col search-result mdl-shadow--2dp">
+                            <div class="mdl-card__supporting-text">
+                                <p>Posted in '{{ result.title }}' by {{ result.author }}:</p>
+                                <p>{{ result.headline|safe }}</p>
+                            </div>
+                            <div class="mdl-card__actions mdl-card--border post-actions">
+                                <div class="mdl-layout-spacer"></div>
+                                <a class="mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-button--colored" href="/thread/{{ result.thread_id }}#post-{{ result.post_id }}">
+                                    <i class="material-icons">arrow_forward</i>
+                                </a>
+                            </div>
+                        </div>
+                    {% endfor %}
+                </section>
+            </main>
+            <footer class="mdl-mini-footer">
+                <div class="mdl-mini-footer--right-section">
+                    <p>Powered by <a href="https://code.tvl.fyi/about/web/converse">Converse</a></p>
+                </div>
+            </footer>
+        </div>
+        <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+    </body>
+</html>
diff --git a/web/converse/templates/thread.html b/web/converse/templates/thread.html
new file mode 100644
index 0000000000..66ec23de1f
--- /dev/null
+++ b/web/converse/templates/thread.html
@@ -0,0 +1,111 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
+        <title>Converse: {{ title }}</title>
+
+        <!-- TODO -->
+        <meta http-equiv="Content-Security-Policy" content="script-src https://code.getmdl.io 'self';">
+        <!-- <link rel="shortcut icon" href="images/favicon.png"> -->
+
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu+Mono">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.blue_grey-orange.min.css" />
+        <link rel="stylesheet" href="/static/styles.css">
+        <!-- Syntax highlighting for code -->
+        <link rel="stylesheet" href="/static/highlight.css">
+        <style>img { max-width:100%; height:auto; }</style>
+        <script src="/static/highlight.js"></script>
+    </head>
+    <body class="converse mdl-base mdl-color-text--grey-700 mdl-color--grey-100">
+        <div class="mdl-layout mdl-layout--fixed-header mdl-js-layout mdl-color--grey-100">
+            <header class="mdl-layout__header mdl-color--primary-dark">
+                <div class="mdl-layout__header-row">
+                    <a href="/" class="mdl-layout-title mdl-color-text--blue-grey-50 cvs-title">Converse: {{ title }}</a>
+                    <div class="mdl-layout-spacer"></div>
+                    <a href="/">
+                        <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
+                            Back to index
+                        </button>
+                    </a>
+                </div>
+            </header>
+            <main class="mdl-layout__content">
+                {% for post in posts -%}
+                    <section id="post-{{ post.id }}" class="section--center mdl-grid mdl-grid--no-spacing">
+                        <!-- card to display avatars on desktop -->
+                        <div class="mdl-card mdl-shadow--2dp mdl-cell--2-col mdl-cell--hide-phone mdl-cell--hide-tablet avatar-box">
+                            <div class="avatar-card">
+                                <img class="desktop-avatar" src="https://www.gravatar.com/avatar/{{ post.author_gravatar }}?d=monsterid&s=160" />
+                                <p class="user-name">{{ post.author_name }}</p>
+                            </div>
+                        </div>
+                        <!-- card for main post content -->
+                        <div class="mdl-card mdl-shadow--2dp post-box mdl-cell--10-col">
+                            <!-- card section for displaying user & post information on mobile -->
+                            <div class="mdl-card__supporting-text mdl-card--border mdl-cell--hide-desktop mdl-color-text--blue-grey-500 mobile-user">
+                                <img class="mobile-avatar" src="https://www.gravatar.com/avatar/{{ post.author_gravatar }}?d=monsterid"/>
+                                <span>&nbsp;{{ post.author_name }} posted on </span>
+                                <a class="mdl-color-text--blue-grey-500 mobile-date" href="/thread/{{ id }}#post-{{ post.id }}">{{ post.posted }}</a>
+                            </div>
+                            <!-- card section to display post date on desktop -->
+                            <div class="mdl-card__menu mdl-cell--hide-phone mdl-cell--hide-tablet">
+                                <a class="post-date mdl-color-text--blue-grey-500" href="/thread/{{ id }}#post-{{ post.id }}">{{ post.posted }}</a>
+                            </div>
+                            <!-- card section for actual post content -->
+                            <div class="mdl-card__supporting-text post-box">{{ post.body|safe }}</div>
+                            <!-- card section for post actions -->
+                            <div class="mdl-card__actions post-actions">
+                                <div class="mdl-layout-spacer"></div>
+
+                                {% if post.editable %}
+                                    <a href="/post/{{ post.id }}/edit" class="mdl-button mdl-js-button mdl-button--accent" id="edit-post-{{ post.id }}" aria-label="Edit post">
+                                        <i class="material-icons">edit</i>
+                                        <span class="mdl-tooltip mdl-tooltip--top" for="edit-post-{{ post.id }}">Edit post</span>
+                                    </a>
+                                {% endif %}
+                                <button class="mdl-button mdl-js-button mdl-button--accent" id="quote-post-{{ post.id }}" aria-label="Quote post" disabled>
+                                    <i class="material-icons">reply</i>
+                                    <span class="mdl-tooltip mdl-tooltip--top" for="quote-post-{{ post.id }}">Quote post</span>
+                                </button>
+                            </div>
+                        </div>
+                    </section>
+                {% endfor %}
+
+                <!-- section for writing a response on the same page -->
+                <section id="post-reply" class="section--center mdl-grid mdl-grid--no-spacing reply-box">
+                    <div class="mdl-card mdl-shadow--2dp mdl-cell--12-col">
+                        {% if closed %}
+                        <div class="mdl-card__supporting-text">
+                            This thread is <b>closed</b> and can no longer be responded to.
+                        </div>
+                        {% else %}
+                        <form id="reply-form" action="/thread/reply" method="post">
+                            <input type="hidden" id="thread_id" name="thread_id" value="{{ id }}">
+
+                            <div class="mdl-card__supporting-text">
+                                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-cell--12-col">
+                                    <textarea class="mdl-textfield__input" type="text" rows="8" id="post" name="post" aria-label="reply content"></textarea>
+                                    <label class="mdl-textfield__label" for="post">Write a reply</label>
+                                </div>
+                                <button class="mdl-button mdl-button--raised mdl-button--primary mdl-js-button mdl-js-ripple-effect" type="submit">
+                                    Post!
+                                </button>
+                            </div>
+                        </form>
+                        {% endif %}
+                    </div>
+                </section>
+            </main>
+            <footer class="mdl-mini-footer">
+                <div class="mdl-mini-footer--right-section">
+                    <p>Powered by <a href="https://code.tvl.fyi/about/web/converse">Converse</a></p>
+                </div>
+            </footer>
+        </div>
+        <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
+    </body>
+</html>
diff --git a/web/converse/todo.org b/web/converse/todo.org
new file mode 100644
index 0000000000..379f10753a
--- /dev/null
+++ b/web/converse/todo.org
@@ -0,0 +1,13 @@
+* DONE Pin *-versions in cargo.toml
+* DONE Markdown support
+* DONE Post ordering as expected
+* DONE Stickies!
+* DONE Search
+* DONE Post editing
+* TODO Configurable number of DB workers
+* TODO Match certain types of Diesel errors (esp. for "not found")
+* TODO Sketch out categories vs. tags system
+* TODO Quote button
+* TODO Multiquote buttons
+* TODO Pagination
+* TODO Multi-thread guest accounts
diff --git a/web/panettone/.envrc b/web/panettone/.envrc
new file mode 100644
index 0000000000..be81feddb1
--- /dev/null
+++ b/web/panettone/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
\ No newline at end of file
diff --git a/web/panettone/.gitignore b/web/panettone/.gitignore
new file mode 100644
index 0000000000..be303db032
--- /dev/null
+++ b/web/panettone/.gitignore
@@ -0,0 +1 @@
+*.fasl
diff --git a/web/panettone/OWNERS b/web/panettone/OWNERS
new file mode 100644
index 0000000000..b2b0acc303
--- /dev/null
+++ b/web/panettone/OWNERS
@@ -0,0 +1,5 @@
+inherited: true
+owners:
+  - grfn
+  - tazjin
+  - sterni
diff --git a/web/panettone/default.nix b/web/panettone/default.nix
new file mode 100644
index 0000000000..283f834994
--- /dev/null
+++ b/web/panettone/default.nix
@@ -0,0 +1,55 @@
+{ depot, ... }:
+
+depot.nix.buildLisp.program {
+  name = "panettone";
+
+  deps = with depot.third_party.lisp; [
+    bordeaux-threads
+    cl-json
+    cl-ppcre
+    cl-smtp
+    cl-who
+    defclass-std
+    drakma
+    easy-routes
+    hunchentoot
+    lass
+    local-time
+    postmodern
+
+    depot.lisp.klatre
+  ];
+
+  srcs = [
+    ./panettone.asd
+    ./src/packages.lisp
+    ./src/util.lisp
+    ./src/css.lisp
+    ./src/email.lisp
+    ./src/inline-markdown.lisp
+    ./src/authentication.lisp
+    ./src/model.lisp
+    ./src/irc.lisp
+    ./src/panettone.lisp
+  ];
+
+  tests = {
+    deps = with depot.third_party.lisp; [
+      fiveam
+    ];
+
+    srcs = [
+      ./test/package.lisp
+      ./test/model_test.lisp
+      ./test/inline-markdown_test.lisp
+      ./test/util_test.lisp
+    ];
+
+    expression = "(fiveam:run!)";
+  };
+
+  brokenOn = [
+    "ecl" # dependencies use dynamic cffi
+    "ccl" # The value NIL is not of the expected type STRING. when loading model.lisp
+  ];
+}
diff --git a/web/panettone/docker-compose.yml b/web/panettone/docker-compose.yml
new file mode 100644
index 0000000000..84723667e6
--- /dev/null
+++ b/web/panettone/docker-compose.yml
@@ -0,0 +1,11 @@
+version: '3.4'
+services:
+  postgres:
+    image: postgres:11
+    restart: always
+    environment:
+      POSTGRES_USER: panettone
+      POSTGRES_PASSWORD: password
+      POSTGRES_DB: panettone
+    ports:
+      - 127.0.0.1:5432:5432
diff --git a/web/panettone/panettone.asd b/web/panettone/panettone.asd
new file mode 100644
index 0000000000..4d44e50fd3
--- /dev/null
+++ b/web/panettone/panettone.asd
@@ -0,0 +1,6 @@
+(asdf:defsystem "panettone"
+  :description "A simple issue tracker"
+  :serial t
+  :components ((:file "packages")
+               (:file "css")
+               (:file "pannetone")))
diff --git a/web/panettone/shell.nix b/web/panettone/shell.nix
new file mode 100644
index 0000000000..483481ca9a
--- /dev/null
+++ b/web/panettone/shell.nix
@@ -0,0 +1,15 @@
+{ depot ? import ../.. { } }:
+
+with depot.third_party.nixpkgs;
+
+mkShell {
+  buildInputs = [
+    docker-compose
+    postgresql
+  ];
+
+  PGPASSWORD = "password";
+  PGHOST = "localhost";
+  PGUSER = "panettone";
+  PGDATABASE = "panettone";
+}
diff --git a/web/panettone/src/.gitignore b/web/panettone/src/.gitignore
new file mode 100644
index 0000000000..10aa5440d8
--- /dev/null
+++ b/web/panettone/src/.gitignore
@@ -0,0 +1,2 @@
+# I use this as the out-link for my local lisp dev env
+sbcl
diff --git a/web/panettone/src/authentication.lisp b/web/panettone/src/authentication.lisp
new file mode 100644
index 0000000000..3ce07aa8d7
--- /dev/null
+++ b/web/panettone/src/authentication.lisp
@@ -0,0 +1,115 @@
+(in-package :panettone.authentication)
+
+(defvar *user* nil
+  "The currently logged-in user")
+
+(defclass/std user ()
+  ((cn dn mail displayname :type string)))
+
+;; Migrating user authentication to OAuth2 necessitates some temporary
+;; workarounds while other parts of the panettone code are being
+;; amended appropriately.
+
+(defun fake-dn (username)
+  "Users are no longer read directly from LDAP, but everything in
+panettone is keyed on the DNs. This function constructs matching
+'fake' DNs."
+  (format nil "cn=~A,ou=users,dc=tvl,dc=fyi" username))
+
+(defun find-user-by-dn (dn)
+  "Previously this function looked up users in LDAP based on their DN,
+however panettone now does not have direct access to a user database.
+
+For most cases only the username is needed, which can be parsed out of
+the user, however email addresses are temporarily not available."
+  (let ((username
+          (car (uiop:split-string (subseq dn 3) :separator '(#\,)))))
+    (make-instance
+     'user
+     :dn dn
+     :cn username
+     :displayname username
+     :mail nil)))
+
+;; Implementation of standard OAuth2 authorisation flow.
+
+(defvar *oauth2-auth-endpoint* nil)
+(defvar *oauth2-token-endpoint* nil)
+(defvar *oauth2-client-id* nil)
+(defvar *oauth2-client-secret* nil)
+
+(defvar *oauth2-redirect-uri*
+  (or (uiop:getenv "OAUTH2_REDIRECT_URI")
+      "https://b.tvl.fyi/auth"))
+
+(defun initialise-oauth2 ()
+  "Initialise all settings needed for OAuth2"
+
+  (setq *oauth2-auth-endpoint*
+        (or (uiop:getenv "OAUTH2_AUTH_ENDPOINT")
+            "https://auth.tvl.fyi/auth/realms/TVL/protocol/openid-connect/auth"))
+
+  (setq *oauth2-token-endpoint*
+        (or (uiop:getenv "OAUTH2_TOKEN_ENDPOINT")
+            "https://auth.tvl.fyi/auth/realms/TVL/protocol/openid-connect/token"))
+
+  (setq *oauth2-client-id*
+        (or (uiop:getenv "OAUTH2_CLIENT_ID")
+            "panettone"))
+
+  (setq *oauth2-client-secret*
+        (or (uiop:getenv "OAUTH2_CLIENT_SECRET")
+            (error "OAUTH2_CLIENT_SECRET must be set!"))))
+
+(defun auth-url ()
+  (format nil "~A?response_type=code&client_id=~A&redirect_uri=~A"
+          *oauth2-auth-endpoint*
+          (drakma:url-encode *oauth2-client-id* :utf-8)
+          (drakma:url-encode *oauth2-redirect-uri* :utf-8)))
+
+(defun claims-to-user (claims)
+  (let ((username (cdr (assoc :preferred--username claims)))
+        (email (cdr (assoc :email claims))))
+    (make-instance
+     'user
+     :dn (fake-dn username)
+     :cn username
+     :mail email
+     ;; TODO(tazjin): Figure out actual displayName mapping in tokens.
+     :displayname username)))
+
+(defun fetch-token (code)
+  "Fetches the access token on completion of user authentication through
+the OAuth2 endpoint and returns the resulting user object."
+
+  (multiple-value-bind (body status)
+      (drakma:http-request *oauth2-token-endpoint*
+                           :method :post
+                           :parameters `(("grant_type" . "authorization_code")
+                                         ("client_id" . ,*oauth2-client-id*)
+                                         ("client_secret" . ,*oauth2-client-secret*)
+                                         ("redirect_uri" . ,*oauth2-redirect-uri*)
+                                         ("code" . ,code))
+                           :external-format-out :utf-8
+                           :want-stream t)
+    (if (/= status 200)
+        (error "Authentication failed: ~A (~A)~%"
+               (alexandria:read-stream-content-into-string body)
+               status)
+
+        ;; Returned JWT contains username and email, we can populate
+        ;; all fields from that.
+        (progn
+          (setf (flexi-streams:flexi-stream-external-format body) :utf-8)
+          (let* ((response (cl-json:decode-json body))
+                 (access-token (cdr (assoc :access--token response)))
+                 (payload (cadr (uiop:split-string access-token :separator '(#\.))))
+                 (claims (cl-json:decode-json-from-string
+                          (base64:base64-string-to-string
+                           ;; The JWT spec specifies that base64 strings
+                           ;; embedded in jwts are *not* padded, but the common
+                           ;; lisp base64 library doesn't know how to deal with
+                           ;; that - we need to add those extra padding
+                           ;; characters here.
+                           (panettone.util:add-missing-base64-padding payload)))))
+            (claims-to-user claims))))))
diff --git a/web/panettone/src/css.lisp b/web/panettone/src/css.lisp
new file mode 100644
index 0000000000..aa753cb50f
--- /dev/null
+++ b/web/panettone/src/css.lisp
@@ -0,0 +1,223 @@
+(in-package :panettone.css)
+(declaim (optimize (safety 3)))
+
+(defun button (selector)
+  `((,selector
+     :background-color "var(--success)"
+     :padding "0.5rem"
+     :text-decoration "none"
+     :transition "box-shadow" "0.15s" "ease-in-out")
+
+    ((:and ,selector :hover)
+     :box-shadow "0.25rem" "0.25rem" "0" "0" "rgba(0,0,0,0.08)")
+
+    ((:and ,selector (:or :active :focus))
+     :box-shadow "0.1rem" "0.1rem" "0" "0" "rgba(0,0,0,0.05)"
+     :outline "none"
+     :border "none")))
+
+(defparameter markdown-styles
+  `((blockquote
+     :border-left "5px" "solid" "var(--light)"-gray
+     :padding-left "1rem"
+     :margin-left "0rem")
+    (pre
+     :overflow-x "auto")))
+
+(defparameter issue-list-styles
+  `((.issue-list
+     :list-style-type "none"
+     :padding-left 0
+
+     (.issue-subject
+      :font-weight "bold")
+
+     (li
+      :padding-bottom "1rem")
+
+     ((li + li)
+      :border-top "1px" "solid" "var(--gray)")
+
+     (a
+      :text-decoration "none"
+      :display "block")
+
+     ((:and a :hover)
+      :outline "none"
+
+      (.issue-subject
+       :color "var(--primary)")))
+
+    (.comment-count
+     :color "var(--gray)")))
+
+(defparameter issue-history-styles
+  `((.issue-history
+     :list-style "none"
+     :border-top "1px" "solid" "var(--gray)"
+     :padding-top "1rem"
+     :padding-left "2rem"
+
+     (.comment-info
+      :color "var(--gray)"
+      :margin 0
+      :padding-top "1rem"
+
+      (a :text-decoration "none")
+      ((:and a :hover)
+       :text-decoration "underline"))
+
+     ((:or .comment .event)
+      :padding-top "1rem"
+      :padding-bottom "1rem"
+      :border-bottom "1px" "solid" "var(--gray)"
+
+      (p :margin 0))
+
+     ((:and (:or .comment .event) :target)
+      :border-color "var(--primary)"
+      :border-bottom-width "3px")
+
+     (.event
+      :color "var(--gray)"))))
+
+(defparameter form-styles
+  `(((:or (:and input (:or (:= type "text")
+                           (:= type "password")))
+          textarea)
+     :width "100%"
+     :padding "0.5rem"
+     :outline "none"
+     :border-top "none"
+     :border-left "none"
+     :border-right "none"
+     :border-bottom "1px" "solid" "var(--gray)"
+     :margin-bottom "1rem")
+
+    (textarea
+     :resize "vertical")
+
+    ((:and input (:= type "submit"))
+     :-webkit-appearance "none"
+     :border "none"
+     :cursor "pointer"
+     :font-size "1rem")
+
+    ,@(button '(:and input (:= type "submit")))
+
+    (.form-link
+     ((:and input (:= type "submit"))
+      :background-color "initial"
+      :color "inherit"
+      :padding 0
+      :text-decoration "underline")
+
+     ((:and input (:= type "submit")
+            (:or :hover :active :focus))
+      :box-shadow 0 0 0 0))
+
+    (.form-group
+     :margin-top "1rem")
+
+    (label.checkbox
+     :cursor "pointer")))
+
+(defparameter issue-styles
+  `((.issue-info
+     :display "flex"
+     :justify-content "space-between"
+     :align-items "center"
+
+     ,@(button '.edit-issue)
+
+     (.created-by-at
+      :flex 1)
+
+     (.edit-issue
+      :background-color "var(--light)"-gray
+      :flex 0
+      :margin-right "0.5rem")
+
+     (.close-issue
+      :background-color "var(--failure)"))))
+
+(defparameter styles
+  `(,@form-styles
+    ,@issue-list-styles
+    ,@issue-styles
+    ,@issue-history-styles
+    ,@markdown-styles
+
+    (body
+     :font-family "sans-serif"
+     :color "var(--text)"
+     :background "var(--bg)"
+     :--text "rgb(24, 24, 24)"
+     :--bg "white"
+     :--gray "#8D8D8D"
+     :--primary "rgb(106, 154, 255)"
+     :--primary-light "rgb(150, 166, 200)"
+     :--success "rgb(168, 249, 166)"
+     :--failure "rgb(247, 167, 167)"
+     :--light-gray "#EEE")
+
+    (:media "(prefers-color-scheme: dark)"
+      (body
+        :--text "rgb(240, 240, 240)"
+        :--bg "black"
+        :--gray "#8D8D8D"
+        :--primary "rgb(106, 154, 255)"
+        :--primary-light "rgb(150, 166, 200)"
+        :--success "rgb(14, 130, 11)"
+        :--failure "rgb(124, 14, 14)"
+        :--light-gray "#222"))
+
+    (a :color "inherit")
+
+    (.content
+     :max-width "800px"
+     :margin "0 auto")
+
+    (header
+     :display "flex"
+     :align-items "center"
+     :border-bottom "1px" "solid" "var(--text)"
+     :margin-bottom "1rem"
+
+     (h1
+      :padding 0
+      :flex 1)
+
+     (.issue-number
+      :color "var(--gray)"
+      :font-size "1.5rem"))
+
+    (nav
+     :display "flex"
+     :color "var(--gray)"
+     :justify-content "space-between"
+
+     (.nav-group
+      :display "flex"
+      (>*
+       :margin-left "0.5rem")))
+
+    (footer
+     :border-top "1px" "solid" "var(--gray)"
+     :padding-top "1rem"
+     :margin-top "1rem"
+     :color "var(--gray)")
+
+    ,@(button '.new-issue)
+
+    (.alert
+     :padding "0.5rem"
+     :margin-bottom "1rem"
+     :background-color "var(--failure)")
+
+    (.login-form
+     :max-width "300px"
+     :margin "0 auto")
+
+    (.created-by-at
+     :color "var(--gray)")))
diff --git a/web/panettone/src/email.lisp b/web/panettone/src/email.lisp
new file mode 100644
index 0000000000..66ea299858
--- /dev/null
+++ b/web/panettone/src/email.lisp
@@ -0,0 +1,49 @@
+(in-package :panettone.email)
+(declaim (optimize (safety 3)))
+
+(defvar *smtp-server* "localhost"
+  "The host for SMTP connections")
+
+(defvar *smtp-server-port* 2525
+  "The port for SMTP connections")
+
+(defvar *notification-from* "tvlbot@tazj.in"
+  "The email address to send email notifications from")
+
+(defvar *notification-from-display-name* "Panettone"
+  "The Display Name to use when sending email notifications")
+
+(defvar *notification-subject-prefix* "[panettone]"
+  "String to prefix all email subjects with")
+
+(defun send-email-notification (&key to subject message)
+  "Sends an email to TO with the given SUBJECT and MESSAGE, using the current
+values of `*smtp-server*', `*smtp-server-port*' and `*email-notification-from*'"
+  (let ((subject (if *notification-subject-prefix*
+                     (format nil "~A ~A"
+                             *notification-subject-prefix*
+                             subject)
+                     subject)))
+    (cl-smtp:send-email
+     *smtp-server*
+     *notification-from*
+     to
+     subject
+     message
+     :port *smtp-server-port*
+     :display-name *notification-from-display-name*)))
+
+(defun user-has-email-notifications-enabled-p (dn)
+  "Returns T if the user with the given DN has enabled email notifications"
+  (enable-email-notifications-p (settings-for-user dn)))
+
+(defun notify-user (dn &key subject message)
+  "Sends an email notification to the user with DN with the given SUBJECT and
+  MESSAGE, iff that user has not disabled email notifications"
+  (when (user-has-email-notifications-enabled-p dn)
+    (when-let* ((user (find-user-by-dn dn))
+                (user-mail (mail user)))
+      (send-email-notification
+       :to user-mail
+       :subject subject
+       :message message))))
diff --git a/web/panettone/src/inline-markdown.lisp b/web/panettone/src/inline-markdown.lisp
new file mode 100644
index 0000000000..e49293519b
--- /dev/null
+++ b/web/panettone/src/inline-markdown.lisp
@@ -0,0 +1,127 @@
+(in-package :panettone.inline-markdown)
+(declaim (optimize (safety 3)))
+
+(define-constant +inline-markup-types+
+  '(("~~" :del)
+    ("*"  :em)
+    ("`"  :code))
+  :test #'equal)
+
+(defun next-token (mkdn &optional (escaped nil))
+  "Parses and returns the next token from the beginning of
+  an inline markdown string which is not altered. The resulting
+  tokens are either :normal (normal text), :special (syntactically
+  significant) or :escaped (escaped using \\). If the string is
+  empty, a pseudo-token named :endofinput is returned. Return value
+  is a list where the first element is the token type, the second
+  the token content and optionally the third the markup type."
+  ; special tokens are syntactically significant characters
+  ; or strings for our inline markdown subset. “normal” tokens
+  ; the strings in between
+  (let* ((special-toks #.'(cons (list "\\" :escape) +inline-markup-types+))
+         (toks (loop
+                 for tok in special-toks
+                 for pos = (search (car tok) mkdn)
+                 when pos collect (cons tok pos)))
+         (next-tok
+           (unless (null toks)
+             (reduce (lambda (a b) (if (< (cdr a) (cdr b)) a b)) toks))))
+    (cond
+      ; end of input
+      ((= (length mkdn) 0) (list :endofinput ""))
+      ; no special tokens, just return entire string
+      ((null next-tok) (list :normal mkdn))
+      ; special token, but not at the beginning of the string
+      ; so we return everything until the special token as
+      ; a string
+      ((> (cdr next-tok) 0) (list :normal (subseq mkdn 0 (cdr next-tok))))
+      ; \ at the beginning of the string: we get the next
+      ; token and mark it as escaped unless we are already
+      ; escaping in which case we just return the backslash
+      ; as a special token
+      ((eq (cadr (car next-tok)) :escape)
+       (if escaped
+         (list :special "\\")
+         (list :escaped
+               (next-token (subseq mkdn 1) t))))
+      ; any other special token at the beginning of the string
+      ; here we also pass the markup type as a third list element
+      ; to prevent unnecessesary lookups
+      (t (list :special
+               (subseq mkdn 0 (length (car (car next-tok))))
+               (cadr (car next-tok)))))))
+
+(defun token-length (tok-type tok-str)
+  "Returns the string length consumed by a call
+  to next-token returning the given token type and string."
+  (check-type tok-type symbol)
+  (if (eq tok-type :escaped)
+    ; backslash + length of escaped token
+    (progn
+      (check-type tok-str list)
+      (1+ (token-length (car tok-str) (cadr tok-str))))
+    (progn
+      (check-type tok-str string)
+      (length tok-str))))
+
+(defun write-tag (tag pos &optional (target *standard-output*))
+  "Wrapper around who:convert-tag-to-string-list to
+  only output a single :opening or :closing tag."
+  (check-type tag symbol)
+  (check-type pos symbol)
+  (let
+    ((index
+       (cond
+         ((eq pos :opening) 0)
+         ((eq pos :closing) 3)
+         (t (error 'simple-type-error)))))
+    (dolist
+      (tag-part (subseq
+                  (who:convert-tag-to-string-list tag nil nil nil)
+                  index (+ index 3)))
+      (write-string tag-part target))))
+
+(defun render-inline-markdown (s &optional (target *standard-output*) (in :normal))
+  "Render inline markdown, a subset of markdown safe to render
+  inside inline elements. The resulting html is directly written
+  to a specified stream or *standard-output* to integrate well
+  with cl-who."
+  (check-type s string)
+  (check-type target stream)
+  (loop
+    for (tok-type tok-str tok-markup) = (next-token s)
+    do (setq s (subseq s (token-length tok-type tok-str)))
+    when (eq tok-type :endofinput)
+    return ""
+    when (eq tok-type :normal)
+    do (write-string (who:escape-string tok-str) target)
+    when (eq tok-type :escaped)
+    do (progn
+         ; if normal tokens are escaped we treat the \ as if it were \\
+         ;
+         ; TODO(sterni): maybe also use the :normal behavior in :code except for #\`.
+         (when (eq (car tok-str) :normal)
+           (write-char #\\ target))
+         (write-string (who:escape-string (cadr tok-str)) target))
+    when (eq tok-type :special)
+    do (cond
+         ; we are on the outer level and encounter a special token:
+         ; render surrounding tags and call ourselves to render
+         ; inner content.
+         ((eq in :normal)
+          (progn
+            (write-tag tok-markup :opening target)
+            (setq s (render-inline-markdown s target tok-markup))
+            (write-tag tok-markup :closing target)))
+         ; we are on the inner level and encounter the token that initiated
+         ; our markup again, meaning we need to return to the outer level.
+         ; we return the remaining string to be consumed.
+         ((eq in tok-markup) (return s))
+         ; remaining case: we are on the inner level and encounter different markup.
+
+         ; we don't support nested markup for simplicity reasons, so instead we
+         ; just render any nested markdown tokens as if they were escaped. This
+         ; only eliminates the slight use case for nesting :em inside :del, but
+         ; shouldn't be too bad. As a side effect this is the precise behavior
+         ; we want for :code.
+         (t (write-string (who:escape-string tok-str) target)))))
diff --git a/web/panettone/src/irc.lisp b/web/panettone/src/irc.lisp
new file mode 100644
index 0000000000..2ab72a2e39
--- /dev/null
+++ b/web/panettone/src/irc.lisp
@@ -0,0 +1,35 @@
+;;;; Using irccat to send IRC notifications
+
+(in-package :panettone.irc)
+
+(defun noping (s)
+  (format nil "~A~A~A"
+          (char s 0)
+          #\ZERO_WIDTH_SPACE
+          (subseq s 1)))
+
+(defun get-irccat-config ()
+  "Reads the IRCCATHOST and IRCCATPORT environment variables, and returns them
+as two values"
+  (destructuring-bind (host port)
+      (mapcar #'uiop:getenvp '("IRCCATHOST" "IRCCATPORT"))
+    (if (and host port)
+        (values host (parse-integer port))
+        (values "localhost" 4722))))
+
+(defun send-irc-notification (body &key channel)
+  "Sends BODY to the IRC channel CHANNEL (starting with #),
+if an IRCCat server is configured (using the IRCCATHOST and IRCCATPORT
+environment variables).
+May signal a condition if sending fails."
+  (multiple-value-bind (irchost ircport) (get-irccat-config)
+    (when irchost
+      (let ((socket (socket-connect irchost ircport)))
+        (unwind-protect
+             (progn
+               (format (socket-stream socket) "~@[~A ~]~A~A~%"
+                       channel
+                       #\ZERO_WIDTH_SPACE
+                       body)
+               (finish-output (socket-stream socket)))
+          (ignore-errors (socket-close socket)))))))
diff --git a/web/panettone/src/model.lisp b/web/panettone/src/model.lisp
new file mode 100644
index 0000000000..c54a0ae474
--- /dev/null
+++ b/web/panettone/src/model.lisp
@@ -0,0 +1,420 @@
+(in-package :panettone.model)
+(declaim (optimize (safety 3)))
+
+(defvar *pg-spec* nil
+  "Connection spec for use with the with-connection macro. Needs to be
+initialised at launch time.")
+
+(defun make-pg-spec ()
+  "Construct the Postgres connection spec from the environment."
+  (list (or (uiop:getenvp "PGDATABASE") "panettone")
+        (or (uiop:getenvp "PGUSER") "panettone")
+        (or (uiop:getenvp "PGPASSWORD") "password")
+        (or (uiop:getenvp "PGHOST") "localhost")
+
+        :port (or (integer-env "PGPORT") 5432)
+        :application-name "panettone"
+        :pooled-p t))
+
+(defun prepare-db-connections ()
+  "Initialises the connection spec used for all Postgres connections."
+  (setq *pg-spec* (make-pg-spec)))
+
+;;;
+;;; Schema
+;;;
+
+(defclass user-settings ()
+  ((user-dn :col-type string :initarg :user-dn :accessor user-dn)
+   (enable-email-notifications
+    :col-type boolean
+    :initarg :enable-email-notifications
+    :accessor enable-email-notifications-p
+    :initform t
+    :col-default t))
+  (:metaclass dao-class)
+  (:keys user-dn)
+  (:table-name user_settings)
+  (:documentation
+   "Panettone settings for an individual user DN"))
+
+(deftable (user-settings "user_settings")
+  (!dao-def))
+
+(defun settings-for-user (dn)
+  "Retrieve the settings for the user with the given DN, creating a new row in
+  the database if not yet present"
+  (or
+   (car
+    (query-dao
+     'user-settings
+     (:select '* :from 'user-settings :where (:= 'user-dn dn))))
+   (insert-dao (make-instance 'user-settings :user-dn dn))))
+
+(defun update-user-settings (settings &rest attrs)
+  "Update the fields of the settings for USER with the given ATTRS, which is a
+  plist of slot and value"
+  (check-type settings user-settings)
+  (when-let ((set-fields
+              (iter
+                (for slot in '(enable-email-notifications))
+                (for new-value = (getf attrs slot))
+                (appending
+                 (progn
+                   (setf (slot-value settings slot) new-value)
+                   (list slot new-value))))))
+    (execute
+     (sql-compile
+      `(:update user-settings
+        :set ,@set-fields
+        :where (:= user-dn ,(user-dn settings)))))))
+
+
+(define-constant +issue-statuses+ '(:open :closed)
+  :test #'equal)
+
+(deftype issue-status ()
+  "Type specifier for the status of an `issue'"
+  (cons 'member +issue-statuses+))
+
+(defun ddl/create-issue-status ()
+  "Issue DDL to create the `issue-status' type, if it doesn't exist"
+  (unless (query (:select (:exists (:select 1
+                                    :from 'pg_type
+                                    :where (:= 'typname "issue_status"))))
+                 :single)
+    (query (sql-compile
+            `(:create-enum issue-status ,+issue-statuses+)))))
+
+(defclass has-created-at ()
+  ((created-at :col-type timestamp
+               :col-default (local-time:now)
+               :initarg :created-at
+               :accessor created-at))
+  (:metaclass dao-class))
+
+(defun created-at->timestamp (object)
+  (assert (slot-exists-p object 'created-at))
+  (unless (or (not (slot-boundp object 'created-at))
+              (typep (slot-value object 'created-at) 'local-time:timestamp))
+    (setf (slot-value object 'created-at)
+          (local-time:universal-to-timestamp (created-at object)))))
+
+(defmethod initialize-instance :after
+    ((obj has-created-at) &rest initargs &key &allow-other-keys)
+  (declare (ignore initargs))
+  (created-at->timestamp obj))
+
+(defun keyword->str (kw) (string-downcase (symbol-name kw)))
+(defun str->keyword (st) (alexandria:make-keyword (string-upcase st)))
+
+(defclass issue (has-created-at)
+  ((id :col-type serial :initarg :id :accessor id)
+   (subject :col-type string :initarg :subject :accessor subject)
+   (body :col-type string :initarg :body :accessor body :col-default "")
+   (author-dn :col-type string :initarg :author-dn :accessor author-dn)
+   (comments :type list :accessor issue-comments)
+   (events :type list :accessor issue-events)
+   (num-comments :type integer :accessor num-comments)
+   (status :col-type issue_status
+           :initarg :status
+           :accessor status
+           :initform :open
+           :col-default "open"
+           :col-export keyword->str
+           :col-import str->keyword))
+  (:metaclass dao-class)
+  (:keys id)
+  (:table-name issues)
+  (:documentation
+   "Issues are the primary entity in the Panettone database. An issue is
+   reported by a user, has a subject and an optional body, and can be either
+   open or closed"))
+
+(defmethod cl-postgres:to-sql-string ((kw (eql :open)))
+  (cl-postgres:to-sql-string "open"))
+(defmethod cl-postgres:to-sql-string ((kw (eql :closed)))
+  (cl-postgres:to-sql-string "closed"))
+(defmethod cl-postgres:to-sql-string ((ts local-time:timestamp))
+  (cl-postgres:to-sql-string
+   (local-time:timestamp-to-unix ts)))
+
+(defmethod initialize-instance :after
+    ((issue issue) &rest initargs &key &allow-other-keys)
+  (declare (ignore initargs))
+  (unless (symbolp (status issue))
+    (setf (status issue)
+          (intern (string-upcase (status issue))
+                  "KEYWORD"))))
+
+(deftable issue (!dao-def))
+
+(defclass issue-comment (has-created-at)
+  ((id :col-type integer :col-identity t :initarg :id :accessor id)
+   (body :col-type string :initarg :body :accessor body)
+   (author-dn :col-type string :initarg :author-dn :accessor author-dn)
+   (issue-id :col-type integer :initarg :issue-id :accessor :user-id))
+  (:metaclass dao-class)
+  (:keys id)
+  (:table-name issue_comments)
+  (:documentation "Comments on an `issue'"))
+(deftable (issue-comment "issue_comments")
+  (!dao-def)
+  (!foreign 'issues 'issue-id 'id :on-delete :cascade :on-update :cascade))
+
+(defclass issue-event (has-created-at)
+  ((id :col-type integer :col-identity t :initarg :id :accessor id)
+   (issue-id :col-type integer
+             :initarg :issue-id
+             :accessor issue-id)
+   (acting-user-dn :col-type string
+                   :initarg :acting-user-dn
+                   :accessor acting-user-dn)
+   (field :col-type (or string db-null)
+          :initarg :field
+          :accessor field)
+   (previous-value :col-type (or string db-null)
+                   :initarg :previous-value
+                   :accessor previous-value)
+   (new-value :col-type (or string db-null)
+              :initarg :new-value
+              :accessor new-value))
+  (:metaclass dao-class)
+  (:keys id)
+  (:table-name issue_events)
+  (:documentation "Events that have occurred for an issue.
+
+If a field has been changed on an issue, the SYMBOL-NAME of that slot will be in
+FIELD, its previous value will be formatted using ~A into PREVIOUS-VALUE, and
+its new value will be formatted using ~A into NEW-VALUE"))
+
+(deftable (issue-event "issue_events")
+  (!dao-def)
+  (!foreign 'issues 'issue-id 'id :on-delete :cascade :on-update :cascade))
+
+(define-constant +all-tables+
+    '(issue
+      issue-comment
+      issue-event
+      user-settings)
+  :test #'equal)
+
+(defun ddl/create-tables ()
+  "Issue DDL to create all tables, if they don't already exist."
+  (dolist (table +all-tables+)
+    (unless (table-exists-p (dao-table-name table))
+      (create-table table))))
+
+(defun ddl/init ()
+  "Idempotently initialize the full database schema for Panettone"
+  (ddl/create-issue-status)
+  (ddl/create-tables))
+
+;;;
+;;; Querying
+;;;
+
+(define-condition issue-not-found (error)
+  ((id :type integer
+       :initarg :id
+       :reader not-found-id
+       :documentation "ID of the issue that was not found"))
+  (:documentation
+   "Error condition for when an issue requested by ID is not found"))
+
+(defun get-issue (id)
+  "Look up the 'issue with the given ID and return it, or signal a condition of
+type `ISSUE-NOT-FOUND'."
+  (restart-case
+      (or (get-dao 'issue id)
+          (error 'issue-not-found :id id))
+    (different-id (new-id)
+      :report "Use a different issue ID"
+      :interactive (lambda ()
+                     (format t "Enter a new ID: ")
+                     (multiple-value-list (eval (read))))
+      (get-issue new-id))))
+
+(defun issue-exists-p (id)
+  "Returns `T' if an issue with the given ID exists"
+  (query
+   (:select (:exists (:select 1
+                      :from 'issues
+                      :where (:= 'id id))))
+   :single))
+
+(defun list-issues (&key status (with '(:num-comments)))
+  "Return a list of all issues with the given STATUS (or all if nil), ordered by
+  ID descending. If WITH contains `:NUM-COMMENTS' (the default) each issue will
+  have the `num-comments' slot filled with the number of comments on that issue
+  (to avoid N+1 queries)."
+  (let* ((condition (unless (null status)
+                      `(:where (:= status $1))))
+         (select (if (find :num-comments with)
+                     `(:select issues.* (:as (:count issue-comments.id)
+                                             num-comments)
+                               :from issues
+                               :left-join issue-comments
+                               :on (:= issues.id issue-comments.issue-id)
+                               ,@condition
+                               :group-by issues.id)
+                     `(:select * :from issues ,@condition)))
+         (query (sql-compile
+                 `(:order-by ,select (:desc id)))))
+    (with-column-writers ('num_comments 'num-comments)
+      (query-dao 'issue query status))))
+
+(defmethod count-comments ((issue-id integer))
+  "Return the number of comments for the given ISSUE-ID."
+  (query
+   (:select (:count '*)
+    :from 'issue-comments
+    :where (:= 'issue-id issue-id))
+   :single))
+
+(defmethod slot-unbound (cls (issue issue) (slot (eql 'comments)))
+  (declare (ignore cls) (ignore slot))
+  (setf (issue-comments issue) (issue-comments (id issue))))
+
+(defmethod issue-comments ((issue-id integer))
+  "Return a list of all comments with the given ISSUE-ID, sorted oldest first.
+NOTE: This makes a database query, so be wary of N+1 queries"
+  (query-dao
+   'issue-comment
+   (:order-by
+    (:select '*
+     :from 'issue-comments
+     :where (:= 'issue-id issue-id))
+    (:asc 'created-at))))
+
+(defmethod slot-unbound (cls (issue issue) (slot (eql 'events)))
+  (declare (ignore cls) (ignore slot))
+  (setf (issue-events issue) (issue-events (id issue))))
+
+(defmethod issue-events ((issue-id integer))
+  "Return a list of all events with the given ISSUE-ID, sorted oldest first.
+NOTE: This makes a database query, so be wary of N+1 queries"
+  (query-dao
+   'issue-event
+   (:order-by
+    (:select '*
+     :from 'issue-events
+     :where (:= 'issue-id issue-id))
+    (:asc 'created-at))))
+
+;;;
+;;; Writing
+;;;
+
+(defun record-issue-event
+    (issue-id &key
+                field
+                previous-value
+                new-value)
+  "Record in the database that the user identified by `AUTHN:*USER*' updated
+ISSUE-ID, and return the resulting `ISSUE-EVENT'. If no user is currently
+authenticated, warn and no-op"
+  (check-type issue-id (integer))
+  (check-type field (or null symbol))
+  (if authn:*user*
+      (insert-dao
+       (make-instance 'issue-event
+                      :issue-id issue-id
+                      :acting-user-dn (authn:dn authn:*user*)
+                      :field (symbol-name field)
+                      :previous-value (when previous-value
+                                        (format nil "~A" previous-value))
+                      :new-value (when new-value
+                                   (format nil "~A" new-value))))
+      (warn "Performing operation as unauthenticated user")))
+
+(defun create-issue (&rest attrs)
+  "Insert a new issue into the database with the given ATTRS, which should be
+a plist of initforms, and return an instance of `issue'"
+  (insert-dao (apply #'make-instance 'issue attrs)))
+
+(defun delete-issue (issue)
+  (delete-dao issue))
+
+(defun set-issue-status (issue-id status)
+  "Set the status of the issue with the given ISSUE-ID to STATUS in the db. If
+the issue doesn't exist, signals `issue-not-found'"
+  (check-type issue-id integer)
+  (check-type status issue-status)
+  (let ((original-status (query (:select 'status
+                                 :from 'issues
+                                 :where (:= 'id issue-id))
+                                :single)))
+    (when (zerop (execute (:update 'issues
+                           :set 'status (cl-postgres:to-sql-string status)
+                           :where (:= 'id issue-id))))
+      (error 'issue-not-found :id issue-id))
+    (record-issue-event
+     issue-id
+     :field 'status
+     :previous-value (string-upcase original-status)
+     :new-value status)
+    (values)))
+
+(defun update-issue (issue &rest attrs)
+  "Update the fields of ISSUE with the given ATTRS, which is a plist of slot and
+value, and record events for the updates"
+  (let ((set-fields
+          (iter (for slot in '(subject body))
+            (for new-value = (getf attrs slot))
+            (appending
+             (let ((previous-value (slot-value issue slot)))
+               (when (and new-value (not (equalp
+                                          new-value
+                                          previous-value)))
+                 (record-issue-event (id issue)
+                                     :field slot
+                                     :previous-value previous-value
+                                     :new-value new-value)
+                 (setf (slot-value issue slot) new-value)
+                 (list slot new-value)))))))
+    (execute
+     (sql-compile
+      `(:update issues
+        :set ,@set-fields
+        :where (:= id ,(id issue)))))))
+
+(defun create-issue-comment (&rest attrs &key issue-id &allow-other-keys)
+  "Insert a new issue comment into the database with the given ATTRS and
+ISSUE-ID, which should be a plist of initforms, and return an instance of
+`issue-comment'. If no issue exists with `ID' ISSUE-ID, signals
+`issue-not-found'."
+  (unless (issue-exists-p issue-id)
+    (error 'issue-not-found :id issue-id))
+  (insert-dao (apply #'make-instance 'issue-comment :issue-id issue-id attrs)))
+
+(defun issue-commenter-dns (issue-id)
+  "Returns a list of all the dns of users who have commented on ISSUE-ID"
+  (query (:select 'author-dn :distinct
+          :from 'issue-comments
+          :where (:= 'issue-id issue-id))
+         :column))
+
+(defun issue-subscribers (issue-id)
+  "Returns a list of user DNs who should receive notifications for actions taken
+  on ISSUE-ID.
+
+Currently this is implemented as the author of issue plus all the users who have
+commented on the issue, but in the future we likely want to also allow
+explicitly subscribing to / unsubscribing from individual issues."
+  (let ((issue (get-issue issue-id)))
+    (adjoin (author-dn issue)
+            (issue-commenter-dns issue-id)
+            :test #'equal)))
+
+
+(comment
+ (ddl/init)
+ (make-instance 'issue :subject "test")
+ (create-issue :subject "test"
+               :author-dn "cn=grfn,ou=users,dc=tvl,dc=fyi")
+
+ (issue-commenter-dns 1)
+ (issue-subscribers 1)
+
+ )
diff --git a/web/panettone/src/packages.lisp b/web/panettone/src/packages.lisp
new file mode 100644
index 0000000000..4ff4c070f0
--- /dev/null
+++ b/web/panettone/src/packages.lisp
@@ -0,0 +1,87 @@
+(defpackage panettone.util
+  (:use :cl :klatre)
+  (:import-from :alexandria :when-let)
+  (:export :integer-env :add-missing-base64-padding))
+
+(defpackage panettone.css
+  (:use :cl :lass)
+  (:export :styles))
+
+(defpackage panettone.inline-markdown
+  (:use :cl)
+  (:import-from :alexandria :define-constant)
+  (:export :render-inline-markdown))
+
+(defpackage panettone.irc
+  (:nicknames :irc)
+  (:use :cl :usocket)
+  (:export :noping :send-irc-notification))
+
+(defpackage :panettone.authentication
+  (:nicknames :authn)
+  (:use :cl :panettone.util :klatre)
+  (:import-from :defclass-std :defclass/std)
+  (:import-from :alexandria :when-let :with-gensyms)
+  (:export
+   :*user*
+   :auth-url
+   :fetch-token
+   :user :cn :dn :mail :displayname
+   :find-user-by-dn
+   :initialise-oauth2))
+
+(defpackage panettone.model
+  (:nicknames :model)
+  (:use :cl :panettone.util :klatre :postmodern :iterate)
+  (:import-from :alexandria :if-let :when-let :define-constant)
+  (:export
+   :prepare-db-connections
+   :ddl/init
+   :*pg-spec*
+
+   :user-settings
+   :user-dn :enable-email-notifications-p :settings-for-user
+   :update-user-settings :enable-email-notifications
+
+   :issue :issue-comment :issue-event
+   :id :subject :body :author-dn :issue-id :status :created-at :acting-user-dn
+   :field :previous-value :new-value
+
+   :get-issue :issue-exists-p :list-issues :create-issue :set-issue-status
+   :update-issue :delete-issue :issue-not-found :not-found-id
+
+   :issue-events
+
+   :issue-comments :num-comments :create-issue-comment
+   :issue-commenter-dns :issue-subscribers))
+
+(defpackage panettone.email
+  (:nicknames :email)
+  (:use :cl)
+  (:import-from :alexandria :when-let :when-let*)
+  (:import-from :panettone.model
+   :settings-for-user :enable-email-notifications-p)
+  (:import-from :panettone.authentication
+   :find-user-by-dn :mail :displayname)
+  (:export
+   :*smtp-server* :*smtp-server-port* :*notification-from*
+   :*notification-from-display-name* :*notification-subject-prefix*
+   :notify-user :send-email-notification))
+
+(defpackage panettone
+  (:use :cl :klatre :easy-routes :iterate
+        :panettone.util
+        :panettone.authentication
+        :panettone.inline-markdown)
+  (:import-from :defclass-std :defclass/std)
+  (:import-from :alexandria :if-let :when-let :switch :alist-hash-table)
+  (:import-from :cl-ppcre :split)
+  (:import-from :bordeaux-threads :make-thread)
+  (:import-from
+   :panettone.model
+   :id :subject :body :author-dn :issue-id :status :created-at
+   :field :previous-value :new-value :acting-user-dn
+   :*pg-spec*)
+  (:import-from :panettone.irc :send-irc-notification)
+  (:shadow :next)
+  (:export :start-panettone :config :main))
diff --git a/web/panettone/src/panettone.lisp b/web/panettone/src/panettone.lisp
new file mode 100644
index 0000000000..d87ac5ed46
--- /dev/null
+++ b/web/panettone/src/panettone.lisp
@@ -0,0 +1,650 @@
+(in-package :panettone)
+(declaim (optimize (safety 3)))
+
+(defvar *cheddar-url* "http://localhost:4238")
+
+(defgeneric render-markdown (markdown)
+  (:documentation
+   "Render the argument, or the elements of the argument, as markdown, and return
+   the same structure"))
+
+(defun request-markdown-from-cheddar (input)
+  "Send the CL value INPUT encoded as JSON to cheddar's
+  markdown endpoint and return the decoded response."
+  (let ((s (drakma:http-request
+            (concatenate 'string
+                         *cheddar-url*
+                         "/markdown")
+            :accept "application/json"
+            :method :post
+            :content-type "application/json"
+            :external-format-out :utf-8
+            :content (json:encode-json-to-string input)
+            :want-stream t)))
+    (setf (flexi-streams:flexi-stream-external-format s) :utf-8)
+    (cl-json:decode-json s)))
+
+(defmethod render-markdown ((markdown string))
+  (cdr (assoc :markdown
+              (request-markdown-from-cheddar
+               `((markdown . ,markdown))))))
+
+(defmethod render-markdown ((markdown hash-table))
+  (alist-hash-table
+   (request-markdown-from-cheddar markdown)))
+
+(defun markdownify-comment-bodies (comments)
+  "Convert the bodies of the given list of comments to markdown in-place using
+  Cheddar, and return nothing"
+  (let ((in (make-hash-table))
+        (comment-table (make-hash-table)))
+    (dolist (comment comments)
+      (when (typep comment 'model:issue-comment)
+        (setf (gethash (id comment) in) (body comment))
+        (setf (gethash (id comment) comment-table) comment)))
+    (let ((res (render-markdown in)))
+      (iter (for (comment-id markdown-body) in-hashtable res)
+        (let ((comment-id (parse-integer (symbol-name comment-id))))
+          (setf (slot-value (gethash comment-id comment-table)
+                            'model:body)
+                markdown-body)))))
+  (values))
+
+;;;
+;;; Views
+;;;
+
+(defvar *title* "Panettone")
+
+(eval-when (:compile-toplevel :load-toplevel)
+  (setf (who:html-mode) :html5))
+
+(defun render/nav ()
+  (who:with-html-output (*standard-output*)
+    (:nav
+     (if (find (car (split "\\?" (hunchentoot:request-uri*) :limit 2))
+               (list "/" "/issues/closed")
+               :test #'string=)
+         (who:htm (:span :class "placeholder"))
+         (who:htm (:a :href "/" "All Issues")))
+     (if *user*
+         (who:htm
+          (:div :class "nav-group"
+           (:a :href "/settings" "Settings")
+           (:form :class "form-link log-out"
+                  :method "post"
+                  :action "/logout"
+                  (:input :type "submit" :value "Log Out"))))
+         (who:htm
+          (:a :href
+              (format nil
+                      "/auth?original-uri=~A"
+                      (drakma:url-encode (hunchentoot:request-uri*)
+                                         :utf-8))
+              "Log In"))))))
+
+(defun author (object)
+  (find-user-by-dn (author-dn object)))
+
+(defun displayname-if-known (user)
+  (or (when user (displayname user))
+      "unknown"))
+
+(defmacro render ((&key
+                     (footer t)
+                     (header t))
+                  &body body)
+  `(who:with-html-output-to-string (*standard-output* nil :prologue t)
+     (:html
+      :lang "en"
+      (:head
+       (:title (who:esc *title*))
+       (:link :rel "stylesheet" :type "text/css" :href "/main.css")
+       (:meta :name "viewport"
+              :content "width=device-width,initial-scale=1"))
+      (:body
+       (:div
+        :class "content"
+        (when ,header
+          (who:htm
+           (render/nav)))
+        ,@body
+        (when ,footer
+          (who:htm
+           (:footer
+            (render/nav)))))))))
+
+(defun form-button (&key
+                      class
+                      input-class
+                      href
+                      label
+                      (method "post"))
+  (who:with-html-output (*standard-output*)
+    (:form :class class
+           :method method
+           :action href
+           (:input :type "submit"
+                   :class input-class
+                   :value label))))
+
+(defun render/alert (message)
+  "Render an alert box for MESSAGE, if non-null"
+  (check-type message (or null string))
+  (who:with-html-output (*standard-output*)
+    (when message
+      (who:htm (:div :class "alert" (who:esc message))))))
+
+(defun render/settings ()
+  (let ((settings (model:settings-for-user (dn *user*))))
+    (render ()
+      (:div
+       :class "settings-page"
+       (:header
+        (:h1 "Settings"))
+       (:form
+        :method :post :action "/settings"
+        (:div
+         (:label :class "checkbox"
+          (:input :type "checkbox"
+                  :name "enable-email-notifications"
+                  :id "enable-email-notifications"
+                  :checked (model:enable-email-notifications-p
+                            settings))
+          "Enable Email Notifications"))
+        (:div :class "form-group"
+         (:input :type "submit"
+                 :value "Save Settings")))))))
+
+(defun created-by-at (issue)
+  (check-type issue model:issue)
+  (who:with-html-output (*standard-output*)
+    (:span :class "created-by-at"
+           "Opened by "
+           (:span :class "username"
+                  (who:esc (displayname-if-known
+                             (author issue))))
+           " at "
+           (:span :class "timestamp"
+                  (who:esc
+                   (format-dottime (created-at issue)))))))
+
+(defun render/issue-list (&key issues)
+  (who:with-html-output (*standard-output*)
+    (:ol
+     :class "issue-list"
+     (dolist (issue issues)
+       (let ((issue-id (model:id issue)))
+         (who:htm
+          (:li
+           (:a :href (format nil "/issues/~A" issue-id)
+               (:p
+                (:span :class "issue-subject"
+                       (render-inline-markdown (subject issue))))
+               (:span :class "issue-number"
+                      (who:esc (format nil "#~A" issue-id)))
+               " - "
+               (created-by-at issue)
+               (let ((num-comments (length (model:issue-comments issue))))
+                 (unless (zerop num-comments)
+                   (who:htm
+                    (:span :class "comment-count"
+                           " - "
+                           (who:esc
+                            (format nil "~A comment~:p" num-comments))))))))))))))
+
+(defun render/index (&key issues)
+  (render ()
+    (:header
+     (:h1 "Issues")
+     (when *user*
+       (who:htm
+        (:a
+         :class "new-issue"
+         :href "/issues/new" "New Issue"))))
+    (:main
+     (:div
+      :class "issue-links"
+      (:a :href "/issues/closed" "View closed issues"))
+     (render/issue-list :issues issues))))
+
+(defun render/closed-issues (&key issues)
+  (render ()
+    (:header
+     (:h1 "Closed issues"))
+    (:main
+     (:div
+      :class "issue-links"
+      (:a :href "/" "View open isues"))
+     (render/issue-list :issues issues))))
+
+(defun render/issue-form (&optional issue message)
+  (let ((editing (and issue (id issue))))
+    (render ()
+      (:header
+       (:h1
+        (who:esc
+         (if editing "Edit Issue" "New Issue"))))
+      (:main
+       (render/alert message)
+       (:form :method "post"
+              :action (if editing
+                          (format nil "/issues/~A"
+                                  (id issue))
+                          "/issues")
+              :class "issue-form"
+              (:div
+               (:input :type "text"
+                       :id "subject"
+                       :name "subject"
+                       :placeholder "Subject"
+                       :value (when editing
+                                (who:escape-string
+                                  (subject issue)))))
+
+              (:div
+               (:textarea :name "body"
+                          :placeholder "Description"
+                          :rows 10
+                          (who:esc
+                           (when editing
+                             (body issue)))))
+
+              (:input :type "submit"
+                      :value
+                      (if editing
+                          "Save Issue"
+                          "Create Issue")))))))
+
+(defun render/new-comment (issue-id)
+  (who:with-html-output (*standard-output*)
+    (:form
+     :class "new-comment"
+     :method "post"
+     :action (format nil "/issues/~A/comments" issue-id)
+     (:div
+      (:textarea :name "body"
+                 :placeholder "Leave a comment"
+                 :rows 5))
+     (:input :type "submit"
+             :value "Comment"))))
+
+(defgeneric render/issue-history-item (item))
+
+(defmethod render/issue-history-item ((comment model:issue-comment))
+  (let ((fragment (format nil "comment-~A" (id comment))))
+    (who:with-html-output (*standard-output*)
+      (:li
+       :class "comment"
+       :id fragment
+       (:p (who:str (body comment)))
+       (:p
+        :class "comment-info"
+        (:span :class "username"
+               (who:esc
+                 (displayname-if-known (author comment)))
+               " at "
+               (:a :href (concatenate 'string "#" fragment)
+                   (who:esc (format-dottime (created-at comment))))))))))
+
+(defmethod render/issue-history-item ((event model:issue-event))
+  (let ((user (find-user-by-dn (acting-user-dn event)))
+        (fragment (format nil "event-~A" (id event))))
+    (who:with-html-output (*standard-output*)
+      (:li
+       :class "event"
+       :id fragment
+       (who:esc (displayname-if-known user))
+       (switch ((field event) :test #'string=)
+         ("STATUS"
+          (who:htm
+           (who:esc
+            (switch ((new-value event) :test #'string=)
+              ("OPEN" " reopened ")
+              ("CLOSED" " closed ")))
+           " this issue "))
+         ("BODY" (who:htm " updated the body of this issue"))
+         (t
+          (who:htm
+           " changed the "
+           (who:esc (string-downcase (field event)))
+           " of this issue from \""
+           (who:esc (previous-value event))
+           "\" to \""
+           (who:esc (new-value event))
+           "\"")))
+       " at "
+       (who:esc (format-dottime (created-at event)))))))
+
+(defun render/issue (issue)
+  (check-type issue model:issue)
+  (let ((issue-id (id issue))
+        (issue-status (status issue)))
+    (render ()
+      (:header
+       (:h1 (render-inline-markdown (subject issue)))
+       (:div :class "issue-number"
+             (who:esc (format nil "#~A" issue-id))))
+      (:main
+       (:div
+        :class "issue-info"
+        (created-by-at issue)
+
+        (when *user*
+          (who:htm
+           (when (string= (author-dn issue)
+                          (dn *user*))
+             (who:htm
+              (:a :class "edit-issue"
+                  :href (format nil "/issues/~A/edit"
+                                issue-id)
+                  "Edit")))
+           (form-button
+            :class "set-issue-status"
+            :href (format nil "/issues/~A/~A"
+                          issue-id
+                          (case issue-status
+                            (:open "close")
+                            (:closed "open")))
+            :input-class (case issue-status
+                           (:open "close-issue")
+                           (:closed "open-issue"))
+            :label (case issue-status
+                     (:open "Close")
+                     (:closed "Reopen"))))))
+       (:p (who:str (render-markdown (body issue))))
+       (let* ((comments (model:issue-comments issue))
+              (events (model:issue-events issue))
+              (history (merge 'list
+                              comments
+                              events
+                              #'local-time:timestamp<
+                              :key #'created-at)))
+         (markdownify-comment-bodies comments)
+         (when (or history *user*)
+           (who:htm
+            (:ol
+             :class "issue-history"
+             (dolist (item history)
+               (render/issue-history-item item))
+             (when *user*
+               (render/new-comment (id issue)))))))))))
+
+(defun render/not-found (entity-type)
+  (render ()
+    (:h1 (who:esc entity-type) " Not Found")))
+
+;;;
+;;; HTTP handlers
+;;;
+
+(defun send-email-for-issue
+    (issue-id &key subject (message ""))
+  "Send an email notification to all subscribers to the given issue with the
+given subject an body (in a thread, to avoid blocking)"
+  (let ((current-user *user*))
+    (bordeaux-threads:make-thread
+     (lambda ()
+       (pomo:with-connection *pg-spec*
+         (dolist (user-dn (model:issue-subscribers issue-id))
+           (when (not (equal (dn current-user) user-dn))
+             (email:notify-user
+              user-dn
+              :subject subject
+              :message message))))))))
+
+(defun link-to-issue (issue-id)
+  (format nil "https://b.tvl.fyi/issues/~A" issue-id))
+
+(defun @auth-optional (next)
+  (let ((*user* (hunchentoot:session-value 'user)))
+    (funcall next)))
+
+(defun @auth (next)
+  (if-let ((*user* (hunchentoot:session-value 'user)))
+    (funcall next)
+    (hunchentoot:redirect
+     (format nil "/auth?original-uri=~A"
+             (drakma:url-encode
+              (hunchentoot:request-uri*)
+              :utf-8)))))
+
+(defun @db (next)
+  "Decorator for handlers that use the database, wrapped in a transaction."
+  (pomo:with-connection *pg-spec*
+    (pomo:with-transaction ()
+      (catch
+          ;; 'hunchentoot:handler-done is unexported, but is used by functions
+          ;; like hunchentoot:redirect to nonlocally abort the request handler -
+          ;; this doesn't mean an error occurred, so we need to catch it here to
+          ;; make the transaction still get committed
+          (intern "HANDLER-DONE" "HUNCHENTOOT")
+        (funcall next)))))
+
+(defun @handle-issue-not-found (next)
+  (handler-case (funcall next)
+    (model:issue-not-found (err)
+      (render/not-found
+       (format nil "Issue #~A" (model:not-found-id err))))))
+
+(defroute auth-handler ("/auth" :method :get :decorators (@auth-optional)) ()
+  (if-let ((code (hunchentoot:get-parameter "code")))
+    (let ((user (fetch-token code)))
+      (setf (hunchentoot:session-value 'user) user)
+      (hunchentoot:redirect (or (hunchentoot:session-value 'original-uri) "/")))
+
+    (progn
+      (when-let ((original-uri (hunchentoot:get-parameter "original-uri")))
+        (setf (hunchentoot:session-value 'original-uri) original-uri))
+      (hunchentoot:redirect (authn:auth-url)))))
+
+(defroute logout ("/logout" :method :post) ()
+  (hunchentoot:delete-session-value 'user)
+  (hunchentoot:redirect "/"))
+
+(defroute index ("/" :decorators (@auth-optional @db)) ()
+  (let ((issues (model:list-issues :status :open)))
+    (render/index :issues issues)))
+
+(defroute settings ("/settings" :method :get :decorators (@auth @db)) ()
+  (render/settings))
+
+(defroute save-settings ("/settings" :method :post :decorators (@auth @db))
+    (&post enable-email-notifications)
+  (let ((settings (model:settings-for-user (dn *user*))))
+    (model:update-user-settings
+     settings
+     'model:enable-email-notifications enable-email-notifications)
+    (render/settings)))
+
+(defroute handle-closed-issues
+    ("/issues/closed" :decorators (@auth-optional @db)) ()
+  (let ((issues (model:list-issues :status :closed)))
+    (render/closed-issues :issues issues)))
+
+(defroute new-issue ("/issues/new" :decorators (@auth)) ()
+  (render/issue-form))
+
+(defroute handle-create-issue
+    ("/issues" :method :post :decorators (@auth @db))
+    (&post subject body)
+  (if (string= subject "")
+      (render/issue-form
+       (make-instance 'model:issue :subject subject :body body)
+       "Subject is required")
+      (let ((issue
+              (model:create-issue :subject subject
+                                  :body body
+                                  :author-dn (dn *user*))))
+        (send-irc-notification
+         (format nil
+                 "b/~A: \"~A\" opened by ~A - https://b.tvl.fyi/issues/~A"
+                 (id issue)
+                 subject
+                 (irc:noping (cn *user*))
+                 (id issue))
+         :channel (or (uiop:getenvp "ISSUECHANNEL")
+                      "#tvl"))
+        (hunchentoot:redirect
+         (format nil "/issues/~A" (id issue))))))
+
+(defroute show-issue
+    ("/issues/:id" :decorators (@auth-optional @handle-issue-not-found @db))
+    (&path (id 'integer))
+  (let* ((issue (model:get-issue id))
+         (*title* (format nil "~A | Panettone"
+                          (subject issue))))
+    (render/issue issue)))
+
+(defroute edit-issue
+    ("/issues/:id/edit" :decorators (@auth @handle-issue-not-found @db))
+    (&path (id 'integer))
+  (let* ((issue (model:get-issue id))
+         (*title* "Edit Issue | Panettone"))
+    (render/issue-form issue)))
+
+(defroute update-issue
+    ("/issues/:id" :decorators (@auth @handle-issue-not-found @db)
+                   ;; NOTE: this should be a put, but we're all HTML forms
+                   ;; right now and those don't support PUT
+                   :method :post)
+    (&path (id 'integer) &post subject body)
+  (let ((issue (model:get-issue id)))
+    ;; only the original author can edit an issue
+    (if (string-equal (author-dn issue)
+                      (dn *user*))
+        (progn
+          (model:update-issue issue
+                              'model:subject subject
+                              'model:body body)
+          (hunchentoot:redirect (format nil "/issues/~A" id)))
+        (render/not-found "Issue"))))
+
+(defroute handle-create-comment
+    ("/issues/:id/comments"
+     :decorators (@auth @handle-issue-not-found @db)
+     :method :post)
+    (&path (id 'integer) &post body)
+  (flet ((redirect-to-issue ()
+           (hunchentoot:redirect (format nil "/issues/~A" id))))
+    (cond
+      ((string= body "")
+       (redirect-to-issue))
+      (:else
+       (model:create-issue-comment
+        :issue-id id
+        :body body
+        :author-dn (dn *user*))
+
+       (let ((issue (model:get-issue id)))
+         (send-email-for-issue
+          id
+          :subject (format nil "~A commented on b/~A: \"~A\""
+                           (displayname *user*)
+                           id
+                           (subject issue))
+          :message (format nil "~A~%~%~A"
+                           body
+                           (link-to-issue id))))
+       (redirect-to-issue)))))
+
+(defroute close-issue
+    ("/issues/:id/close" :decorators (@auth @handle-issue-not-found @db)
+                         :method :post)
+    (&path (id 'integer))
+  (model:set-issue-status id :closed)
+  (let ((issue (model:get-issue id)))
+    (send-irc-notification
+     (format nil
+             "b/~A: \"~A\" closed by ~A - ~A"
+             id
+             (subject issue)
+             (irc:noping (cn *user*))
+             (link-to-issue id))
+     :channel (or (uiop:getenvp "ISSUECHANNEL")
+                  "#tvl"))
+    (send-email-for-issue
+     id
+     :subject (format nil "b/~A: \"~A\" closed by ~A"
+                      id
+                      (subject issue)
+                      (displayname *user*))
+     :message (link-to-issue id)))
+  (hunchentoot:redirect (format nil "/issues/~A" id)))
+
+(defroute open-issue
+    ("/issues/:id/open" :decorators (@auth @db)
+                        :method :post)
+    (&path (id 'integer))
+  (model:set-issue-status id :open)
+  (let ((issue (model:get-issue id)))
+    (send-irc-notification
+     (format nil
+             "b/~A: \"~A\" reopened by ~A - ~A"
+             id
+             (subject issue)
+             (irc:noping (cn *user*))
+             (link-to-issue id))
+     :channel (or (uiop:getenvp "ISSUECHANNEL")
+                  "#tvl"))
+    (send-email-for-issue
+     id
+     :subject (format nil "b/~A: \"~A\" reopened by ~A"
+                      id
+                      (subject issue)
+                      (displayname *user*))
+     :message (link-to-issue id)))
+  (hunchentoot:redirect (format nil "/issues/~A" id)))
+
+(defroute styles ("/main.css") ()
+  (setf (hunchentoot:content-type*) "text/css")
+  (apply #'lass:compile-and-write panettone.css:styles))
+
+(defvar *acceptor* nil
+  "Hunchentoot acceptor for Panettone's web server.")
+
+(defun migrate-db ()
+  "Migrate the database to the latest version of the schema"
+  (pomo:with-connection *pg-spec*
+    (model:ddl/init)))
+
+(defun start-panettone (&key port session-secret)
+  (authn:initialise-oauth2)
+  (model:prepare-db-connections)
+  (migrate-db)
+
+  (when session-secret
+    (setq hunchentoot:*session-secret* session-secret))
+
+  (setq hunchentoot:*session-max-time* (* 60 60 24 90))
+
+  (setq *acceptor*
+        (make-instance 'easy-routes:routes-acceptor :port port))
+  (hunchentoot:start *acceptor*))
+
+(defun main ()
+  (let ((port (integer-env "PANETTONE_PORT" :default 6161))
+        (cheddar-url (uiop:getenvp "CHEDDAR_URL"))
+        (session-secret (uiop:getenvp "SESSION_SECRET")))
+    (when cheddar-url (setq *cheddar-url* cheddar-url))
+    (setq hunchentoot:*show-lisp-backtraces-p* nil)
+    (setq hunchentoot:*log-lisp-backtraces-p* nil)
+
+    (start-panettone :port port
+                     :session-secret session-secret)
+
+    (format t "launched panettone on port ~A~%" port)
+
+    (sb-thread:join-thread
+     (find-if (lambda (th)
+                (string= (sb-thread:thread-name th)
+                         (format nil "hunchentoot-listener-*:~A" port)))
+              (sb-thread:list-all-threads)))))
+
+(comment
+ (setq hunchentoot:*catch-errors-p* nil)
+ ;; to setup an ssh tunnel to cheddar+irccat for development:
+ ;; ssh -N -L 4238:localhost:4238 -L 4722:localhost:4722 whitby.tvl.fyi
+ (start-panettone :port 6161
+                  :session-secret "session-secret")
+ )
diff --git a/web/panettone/src/util.lisp b/web/panettone/src/util.lisp
new file mode 100644
index 0000000000..2abedf7b8f
--- /dev/null
+++ b/web/panettone/src/util.lisp
@@ -0,0 +1,15 @@
+(in-package :panettone.util)
+
+(defun integer-env (var &key default)
+  (or
+   (when-let ((str (uiop:getenvp var)))
+     (try-parse-integer str))
+   default))
+
+(defun add-missing-base64-padding (s)
+  "Add any missing padding characters to the (un-padded) base64 string `S', such
+that it can be successfully decoded by the `BASE64' package"
+  ;; I apologize
+  (let* ((needed-padding (mod (length s) 4))
+         (pad-chars (if (zerop needed-padding) 0 (- 4 needed-padding))))
+    (format nil "~A~v@{~A~:*~}" s pad-chars "=")))
diff --git a/web/panettone/test/inline-markdown_test.lisp b/web/panettone/test/inline-markdown_test.lisp
new file mode 100644
index 0000000000..bb90750436
--- /dev/null
+++ b/web/panettone/test/inline-markdown_test.lisp
@@ -0,0 +1,54 @@
+(in-package :panettone.tests)
+(declaim (optimize (safety 3)))
+
+(defmacro inline-markdown-unit-test (name input expected)
+  `(test ,name
+     (is (equal
+           ,expected
+           (with-output-to-string (*standard-output*)
+             (render-inline-markdown ,input))))))
+
+(inline-markdown-unit-test
+  inline-markdown-typical-test
+  "hello *world*, here is ~~no~~ `code`!"
+  "hello <em>world</em>, here is <del>no</del> <code>code</code>!")
+
+(inline-markdown-unit-test
+  inline-markdown-two-emphasize-types-test
+  "*stress* *this*"
+  "<em>stress</em> <em>this</em>")
+
+(inline-markdown-unit-test
+  inline-markdown-html-escaping-test
+  "<tag>öäü"
+  "&lt;tag&gt;&#xF6;&#xE4;&#xFC;")
+
+(inline-markdown-unit-test
+  inline-markdown-nesting-test
+  "`inside code *anything* goes`, but also ~~*here*~~"
+  "<code>inside code *anything* goes</code>, but also <del>*here*</del>")
+
+(inline-markdown-unit-test
+  inline-markdown-escaping-test
+  "A backslash \\\\ shows: \\*, \\` and \\~~"
+  "A backslash \\ shows: *, ` and ~~")
+
+(inline-markdown-unit-test
+  inline-markdown-nested-escaping-test
+  "`prevent \\`code\\` from ending, but never stand alone \\\\`"
+  "<code>prevent `code` from ending, but never stand alone \\</code>")
+
+(inline-markdown-unit-test
+  inline-markdown-escape-normal-tokens-test
+  "\\Normal tokens \\escaped?"
+  "\\Normal tokens \\escaped?")
+
+(inline-markdown-unit-test
+  inline-markdown-no-unclosed-tags-test
+  "A tag, once opened, *must be closed"
+  "A tag, once opened, <em>must be closed</em>")
+
+(inline-markdown-unit-test
+  inline-markdown-unicode-safe
+  "Does Unicode 👨‍👨‍👧‍👦 break \\👩🏾‍🦰 tokenization?"
+  "Does Unicode &#x1F468;&#x200D;&#x1F468;&#x200D;&#x1F467;&#x200D;&#x1F466; break \\&#x1F469;&#x1F3FE;&#x200D;&#x1F9B0; tokenization?")
diff --git a/web/panettone/test/irc_test.lisp b/web/panettone/test/irc_test.lisp
new file mode 100644
index 0000000000..0224836cbc
--- /dev/null
+++ b/web/panettone/test/irc_test.lisp
@@ -0,0 +1,5 @@
+(in-package :panettone.tests)
+(declaim (optimize (safety 3)))
+
+(test noping-test
+  (is (not (equal "grfn" (panettone.irc:noping "grfn")))))
diff --git a/web/panettone/test/model_test.lisp b/web/panettone/test/model_test.lisp
new file mode 100644
index 0000000000..e4cd78a65a
--- /dev/null
+++ b/web/panettone/test/model_test.lisp
@@ -0,0 +1,13 @@
+(in-package :panettone.tests)
+(declaim (optimize (safety 3)))
+
+(test initialize-issue-status-test
+  (let ((issue (make-instance 'model:issue :status "open")))
+    (is (eq :open (model:status issue)))))
+
+(test initialize-issue-created-at-test
+  (let* ((time (get-universal-time))
+         (issue (make-instance 'model:issue :created-at time)))
+    (is (local-time:timestamp=
+         (local-time:universal-to-timestamp time)
+         (model:created-at issue)))))
diff --git a/web/panettone/test/package.lisp b/web/panettone/test/package.lisp
new file mode 100644
index 0000000000..d2a2f97420
--- /dev/null
+++ b/web/panettone/test/package.lisp
@@ -0,0 +1,3 @@
+(defpackage :panettone.tests
+  (:use :cl :klatre :fiveam
+        :panettone.inline-markdown))
diff --git a/web/panettone/test/util_test.lisp b/web/panettone/test/util_test.lisp
new file mode 100644
index 0000000000..ff52d916cb
--- /dev/null
+++ b/web/panettone/test/util_test.lisp
@@ -0,0 +1,9 @@
+(in-package :panettone.tests)
+(declaim (optimize (safety 3)))
+
+(test add-missing-base64-padding-test
+  (is (string=
+       "abcdef"
+       (base64:base64-string-to-string
+        (panettone.util:add-missing-base64-padding
+         "YWJjZGVm")))))
diff --git a/web/static/default.nix b/web/static/default.nix
new file mode 100644
index 0000000000..9eaeb0ec14
--- /dev/null
+++ b/web/static/default.nix
@@ -0,0 +1,29 @@
+# Expose all static assets as a folder. The derivation contains a
+# `drvHash` attribute which can be used for cache-busting.
+{ depot, lib, pkgs, ... }:
+
+let
+  storeDirLength = with builtins; (stringLength storeDir) + 1;
+  logo = depot.web.tvl.logo;
+in
+lib.fix (self: pkgs.runCommand "tvl-static"
+{
+  passthru = {
+    # Preserving the string context here makes little sense: While we are
+    # referencing this derivation, we are not doing so via the nix store,
+    # so it makes little sense for Nix to police the references.
+    drvHash = builtins.unsafeDiscardStringContext (
+      lib.substring storeDirLength 32 self.drvPath
+    );
+  };
+} ''
+  mkdir $out
+  cp -r ${./.}/* $out
+  cp ${logo.pastelRainbow} $out/logo-animated.svg
+  cp ${logo.bluePng} $out/logo-blue.png
+  cp ${logo.greenPng} $out/logo-green.png
+  cp ${logo.orangePng} $out/logo-orange.png
+  cp ${logo.purplePng} $out/logo-purple.png
+  cp ${logo.redPng} $out/logo-red.png
+  cp ${logo.yellowPng} $out/logo-yellow.png
+'')
diff --git a/web/static/favicon.webp b/web/static/favicon.webp
new file mode 100644
index 0000000000..741cdbc64f
--- /dev/null
+++ b/web/static/favicon.webp
Binary files differdiff --git a/web/static/files/adisbladis_tazjin_tvix.webp b/web/static/files/adisbladis_tazjin_tvix.webp
new file mode 100644
index 0000000000..38e004543d
--- /dev/null
+++ b/web/static/files/adisbladis_tazjin_tvix.webp
Binary files differdiff --git a/web/static/files/flokli_tazjin_tvix.webp b/web/static/files/flokli_tazjin_tvix.webp
new file mode 100644
index 0000000000..4c0b94903f
--- /dev/null
+++ b/web/static/files/flokli_tazjin_tvix.webp
Binary files differdiff --git a/web/static/jetbrains-mono-bold-italic.woff2 b/web/static/jetbrains-mono-bold-italic.woff2
new file mode 100644
index 0000000000..34b5c69ae1
--- /dev/null
+++ b/web/static/jetbrains-mono-bold-italic.woff2
Binary files differdiff --git a/web/static/jetbrains-mono-bold.woff2 b/web/static/jetbrains-mono-bold.woff2
new file mode 100644
index 0000000000..84a008af7e
--- /dev/null
+++ b/web/static/jetbrains-mono-bold.woff2
Binary files differdiff --git a/web/static/jetbrains-mono-italic.woff2 b/web/static/jetbrains-mono-italic.woff2
new file mode 100644
index 0000000000..85fd468789
--- /dev/null
+++ b/web/static/jetbrains-mono-italic.woff2
Binary files differdiff --git a/web/static/jetbrains-mono.woff2 b/web/static/jetbrains-mono.woff2
new file mode 100644
index 0000000000..d5b94cb9e7
--- /dev/null
+++ b/web/static/jetbrains-mono.woff2
Binary files differdiff --git a/web/static/tvl.css b/web/static/tvl.css
new file mode 100644
index 0000000000..aea4d426ea
--- /dev/null
+++ b/web/static/tvl.css
@@ -0,0 +1,183 @@
+/* Jetbrains Mono font from https://www.jetbrains.com/lp/mono/
+   licensed under Apache 2.0. Thanks, Jetbrains! */
+@font-face {
+    font-family: jetbrains-mono;
+    src: url(jetbrains-mono.woff2);
+}
+
+@font-face {
+    font-family: jetbrains-mono;
+    font-weight: bold;
+    src: url(jetbrains-mono-bold.woff2);
+}
+
+@font-face {
+    font-family: jetbrains-mono;
+    font-style: italic;
+    src: url(jetbrains-mono-italic.woff2);
+}
+
+@font-face {
+    font-family: jetbrains-mono;
+    font-weight: bold;
+    font-style: italic;
+    src: url(jetbrains-mono-bold-italic.woff2);
+}
+
+/* Generic-purpose styling */
+
+body {
+    max-width: 800px;
+    margin: 40px auto;
+    line-height: 1.6;
+    font-size: 18px;
+    padding: 0 10px;
+    font-family: jetbrains-mono, monospace;
+}
+
+p, a :not(.uncoloured-link) {
+    color: inherit;
+}
+
+h1, h2, h3 {
+    line-height: 1.2
+}
+
+/* Homepage styling */
+
+.dark {
+    background-color: #181818;
+    color: #e4e4ef;
+}
+
+.dark-link, .interblag-title {
+    color: #96a6c8;
+}
+
+.entry-container {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+}
+
+.interblag-title {
+    text-decoration: none;
+}
+
+.entry {
+    width: 42%;
+    margin: 5px;
+    padding-left: 7px;
+    padding-right: 5px;
+    border: 2px solid;
+    border-radius: 5px;
+    flex-grow: 1;
+    text-decoration: none;
+}
+
+.misc {
+    color: #73c936;
+    border-color: #73c936;
+}
+
+.blog {
+    color: #268bd2;
+    border-color: #268bd2;
+}
+
+.project {
+    color: #ff4f58;
+    border-color: #ff4f58;
+}
+
+.entry-title {
+    color: inherit !important;
+    font-weight: bold;
+    text-decoration: none;
+}
+
+.entry-date {
+    font-style: italic;
+}
+
+/* Blog styling */
+
+.light {
+    color: #383838;
+}
+
+.blog-title {
+    color: inherit;
+    text-decoration: none;
+}
+
+.footer {
+    text-align: right;
+}
+
+.date {
+    text-align: right;
+    font-style: italic;
+    float: right;
+}
+
+.inline {
+    display: inline;
+}
+
+.lod {
+    text-align: center;
+}
+
+.uncoloured-link {
+    color: inherit;
+}
+
+pre {
+    width: 100%;
+    overflow: auto;
+}
+
+img {
+    max-width: 100%;
+}
+
+.cheddar-callout {
+    display: block;
+    padding: 10px;
+}
+
+.cheddar-question {
+    color: #3367d6;
+    background-color: #e8f0fe;
+}
+
+.cheddar-todo {
+    color: #616161;
+    background-color: #eeeeee;
+}
+
+.cheddar-tip {
+    color: #00796b;
+    background-color: #e0f2f1;
+}
+
+.cheddar-warning {
+    color: #a52714;
+    background-color: #fbe9e7;
+}
+
+kbd {
+    background-color: #eee;
+    border-radius: 3px;
+    border: 1px solid #b4b4b4;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
+    color: #333;
+    display: inline-block;
+    font-size: .85em;
+    font-weight: 700;
+    line-height: 1;
+    padding: 2px 4px;
+    white-space: nowrap;
+}
diff --git a/web/todolist/default.nix b/web/todolist/default.nix
new file mode 100644
index 0000000000..c37a655559
--- /dev/null
+++ b/web/todolist/default.nix
@@ -0,0 +1,116 @@
+# Generates a simple web view of open TODOs in the depot.
+#
+# Only TODOs that match the form 'TODO($username)' are considered, and
+# only for users that are known to us.
+{ depot, lib, pkgs, ... }:
+
+let
+  inherit (pkgs)
+    jq
+    ripgrep
+    runCommandNoCC
+    writeTextFile
+    ;
+
+  inherit (builtins)
+    elem
+    filter
+    fromJSON
+    head
+    readFile
+    map
+    ;
+
+  inherit (lib) concatStringsSep;
+
+  inherit (depot.nix.yants)
+    defun
+    int
+    string
+    struct
+    ;
+
+  knownUsers = map (u: u.username) depot.ops.users;
+
+  todo = struct {
+    file = string;
+    line = int;
+    todo = string;
+    user = string;
+  };
+
+  allTodos = fromJSON (readFile (runCommandNoCC "depot-todos.json" { } ''
+    ${ripgrep}/bin/rg --json 'TODO\(\w+\):.*$' ${depot.path} | \
+      ${jq}/bin/jq -s -f ${./extract-todos.jq} > $out
+  ''));
+
+  knownUserTodos = filter (todos: elem (head todos).user knownUsers) allTodos;
+
+  fileLink = defun [ todo string ] (t:
+    ''<a style="color: inherit;"
+         href="https://cs.tvl.fyi/depot/-/blob/${t.file}#L${toString t.line}">
+      //${t.file}:${toString t.line}</a>'');
+
+  todoElement = defun [ todo string ] (t: ''
+    <p>${fileLink t}:</p>
+    <blockquote>${t.todo}</blockquote>
+
+  '');
+
+  userParagraph = todos:
+    let user = (head todos).user;
+    in ''
+      <p>
+        <h3>
+          <a style="color:inherit; text-decoration: none;"
+             name="${user}"
+             href="#${user}">${user}</a>
+        </h3>
+        ${concatStringsSep "\n" (map todoElement todos)}
+      </p>
+      <hr>
+    '';
+
+  staticUrl = "https://static.tvl.fyi/${depot.web.static.drvHash}";
+
+in
+writeTextFile {
+  name = "tvl-todos";
+  destination = "/index.html";
+  text = ''
+    <!DOCTYPE html>
+    <head>
+      <meta charset="utf-8">
+      <meta name="viewport" content="width=device-width, initial-scale=1">
+      <meta name="description" content="TVL's todo-list">
+      <link rel="stylesheet" type="text/css" media="all" href="${staticUrl}/tvl.css">
+      <link rel="icon" type="image/webp" href="${staticUrl}/favicon.webp">
+      <title>TVL's todo-list</title>
+      <style>
+        svg {
+          max-width: inherit;
+          height: auto;
+        }
+      </style>
+    </head>
+    <body class="dark">
+      <header>
+        <h1><a class="blog-title" href="/">The Virus Lounge's todo-list</a> </h1>
+        <hr>
+      </header>
+      <main>
+      ${concatStringsSep "\n" (map userParagraph knownUserTodos)}
+      </main>
+      <footer>
+        <p class="footer">
+          <a class="uncoloured-link" href="https://tvl.fyi">homepage</a>
+          |
+          <a class="uncoloured-link" href="https://cs.tvl.fyi/depot/-/blob/README.md">code</a>
+          |
+          <a class="uncoloured-link" href="https://cl.tvl.fyi">reviews</a>
+        </p>
+        <p class="lod">ಠ_ಠ</p>
+      </footer>
+    </body>
+  '';
+}
diff --git a/web/todolist/extract-todos.jq b/web/todolist/extract-todos.jq
new file mode 100644
index 0000000000..d4e0476c40
--- /dev/null
+++ b/web/todolist/extract-todos.jq
@@ -0,0 +1,30 @@
+# Simple jq script to extract all TODO comments in the code base from
+# ripgrep's JSON output.
+#
+# This assumes that the filter used is something like 'TODO\(\w+\):'
+
+# Capture the username and todo entry from an input string.
+def capture_todo:
+  capture("TODO\\((?<user>\\w+)\\):\\s+(?<todo>.*)$");
+
+# Construct a structure with only the fields we need to populate the
+# page.
+def simplify_match:
+  .data as $data
+  | (.data.submatches[0].match.text | capture_todo) as $capture
+  | {
+     file: ($data | .path.text | sub("/nix/store/.+-depot/"; "")),
+     line: ($data | .line_number),
+     todo: ($capture | .todo),
+     user: ($capture | .user),
+     };
+
+# Group all matches first by the user and return them in sorted order
+# by the file in which they appear. This matches the presentation
+# order on the website.
+def group_by_user: .
+    | group_by(.user)
+    | map(sort_by(.file));
+
+# main:
+map(select(.type == "match") | simplify_match) | group_by_user
diff --git a/web/tvl/blog/default.nix b/web/tvl/blog/default.nix
new file mode 100644
index 0000000000..963bb635e3
--- /dev/null
+++ b/web/tvl/blog/default.nix
@@ -0,0 +1,18 @@
+{ depot, ... }:
+
+{
+  config = {
+    name = "TVL's blog";
+    footer = depot.web.tvl.footer { };
+    baseUrl = "https://tvl.fyi/blog";
+  };
+
+  posts = [
+    {
+      key = "rewriting-nix";
+      title = "Tvix: We are rewriting Nix";
+      date = 1638381387;
+      content = ./rewriting-nix.md;
+    }
+  ];
+}
diff --git a/web/tvl/blog/rewriting-nix.md b/web/tvl/blog/rewriting-nix.md
new file mode 100644
index 0000000000..1b143e5b0e
--- /dev/null
+++ b/web/tvl/blog/rewriting-nix.md
@@ -0,0 +1,90 @@
+Evaluating the Nix programming language, used by the Nix package
+manager, is currently very slow. This becomes apparent in all projects
+written in Nix that are not just simple package definitions, for
+example:
+
+* the NixOS module system
+* TVL projects like
+  [`//nix/yants`](https://at.tvl.fyi/?q=%2F%2Fnix%2Fyants) and
+  [`//web/bubblegum`](https://at.tvl.fyi/?q=%2F%2Fweb%2Fbubblegum).
+* the code that [generates build
+  instructions](https://at.tvl.fyi/?q=%2F%2Fops%2Fpipelines) for TVL's
+  [CI setup](https://tvl.fyi/builds)
+
+Whichever project you pick, they all suffer from issues with the
+language implementation. At TVL, it takes us close to a minute to
+create the CI instructions for our monorepo at the moment - despite it
+being a plain Nix evaluation. Running our Nix-native build systems for
+[Go](https://code.tvl.fyi/about/nix/buildGo) and [Common
+Lisp](https://code.tvl.fyi/about/nix/buildLisp) takes much more time
+than we would like.
+
+Some time last year a few of us got together and started investigating
+ways to modernise the current architecture of Nix and figure out how
+to improve the speed of some of the components. We created over [250
+commits](https://cl.tvl.fyi/q/topic:tvix) in our fork of the Nix 2.3
+codebase at the time, tried [performance
+experiments](https://cl.tvl.fyi/c/depot/+/1123/) aimed at improving
+the current evaluator and fought [gnarly
+bugs](https://cl.tvl.fyi/c/depot/+/1504).
+
+After a while we realised that we were treading water: Some of our
+ideas are too architecturally divergent from Nix to be done on top of
+the existing codebase, and the memory model of Nix causes significant
+headaches when trying to do any kind of larger change.
+
+We needed an alternative approach and started brainstorming on a bent
+whiteboard in a small flat in Hurghada, Egypt.
+
+![flokli & tazjin brainstorming](https://static.tvl.fyi/latest/files/flokli_tazjin_tvix.webp)
+
+Half a year later we are now ready to announce our new project:
+**Tvix**, a re-imagined Nix with full nixpkgs compatibility. Tvix is
+generously funded [by NLNet](https://nlnet.nl/project/Tvix/) (thanks!)
+and we are ready to start implementing it.
+
+The [Tvix
+architecture](https://code.tvl.fyi/about/tvix/docs/components.md) is
+designed to be modular: It should be possible to write an evaluator
+that plugs in the Guile language (for compatibility with GNU Guix), to
+use arbitrary builders, and to replace the store implementation.
+
+Tvix has these high-level goals:
+
+* Creating an alternative implementation of Nix that is **fully
+  compatible with nixpkgs**.
+
+  The package collection is an enormous effort with hundreds of
+  thousands of commits, encoding expert knowledge about lots of
+  different software and ways of building and managing it. It is a
+  very valuable piece of software and we must be able to reuse it.
+
+* More efficient Nix language evaluation, leading to greatly increased
+  performance.
+
+* No more strict separation of evaluation and build phases: Generating
+  Nix data structures from build artefacts ("IFD") should be supported
+  first-class and not incur significant performance cost.
+
+* Well-defined interaction protocols for how the three different
+  components (evaluator, builder, store) interact.
+
+* A builder implementation using OCI instead of custom sandboxing
+  code.
+
+![adisbladis & tazjin brainstorming](https://static.tvl.fyi/latest/files/adisbladis_tazjin_tvix.webp)
+
+Tvix is not intended to *replace* Nix, instead we want to improve the
+ecosystem by offering an alternative, fast and reliable implementation
+for Nix features that are in use today.
+
+As things ramp up we will be posting more information on this blog,
+for now you can keep an eye on
+[`//tvix`](https://cs.tvl.fyi/depot/-/tree/tvix) in the TVL monorepo
+and subscribe to [our feed](https://tvl.fyi/feed.atom).
+
+Stay tuned!
+
+<span style="font-size: small;">PS: TVL is international, but a lot of
+the development will take place in our office in Moscow. Say hi if
+you're around and interested!</span>
diff --git a/web/tvl/default.nix b/web/tvl/default.nix
new file mode 100644
index 0000000000..262be54c0e
--- /dev/null
+++ b/web/tvl/default.nix
@@ -0,0 +1,131 @@
+{ depot, lib, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) filter;
+  inherit (pkgs) graphviz runCommandNoCC writeText;
+  inherit (depot.web) atom-feed blog tvl;
+
+  listPosts = defun [ (list blog.post) string ] (posts:
+    lib.concatStringsSep "\n" (map (p: "* [${p.title}](blog/${p.key})") posts)
+  );
+
+  postRenderingCommands = defun [ (list blog.post) string ] (posts:
+    lib.concatStringsSep "\n"
+      (map (p: "cp ${blog.renderPost tvl.blog.config p} $out/blog/${p.key}.html") posts)
+  );
+
+  tvlGraph = runCommandNoCC "tvl.svg"
+    {
+      nativeBuildInputs = with pkgs; [ fontconfig freetype cairo jetbrains-mono ];
+    } ''
+    ${graphviz}/bin/neato -Tsvg ${./tvl.dot} > $out
+  '';
+
+  publishedPosts = filter blog.includePost tvl.blog.posts;
+
+  feed = {
+    id = "https://tvl.fyi/";
+    title = "TVL blog";
+    subtitle = "Thoughts and news from The Virus Lounge";
+    authors = [ "tazjin" ]; # TODO(tazjin): Extract from post info
+
+    links = lib.singleton {
+      rel = "self";
+      href = "https://tvl.fyi/feed.atom";
+    };
+
+    entries = map (blog.toFeedEntry tvl.blog.config) publishedPosts;
+  };
+
+  atomFeed = writeText "feed.atom" (atom-feed.renderFeed feed);
+
+  homepage = tvl.template {
+    title = "The Virus Lounge";
+    content = ''
+      The Virus Lounge
+      ================
+
+      ----------------
+
+      <img class="tvl-logo" src="https://static.tvl.fyi/${depot.web.static.drvHash}/logo-animated.svg"
+           alt="Virus with lambda-shaped spike proteins sitting on an armchair">
+
+      Welcome to **The Virus Lounge**. We're a group of people who got
+      together in 2020, when we felt that there was not enough
+      spontaneous socialising on the internet.
+
+      Because of our shared interests in topics like **build systems**
+      and **monorepos** we started working on code together, in our
+      monorepo called the *depot*.
+
+      Feel free to explore the tech we have built so far, all our
+      systems are linked in the footer.
+
+      We mostly hang out on IRC. You can find us in [`#tvl`][tvl-irc]
+      on [hackint][], which is also reachable [via XMPP][hackint-xmpp]
+      at [`#tvl@irc.hackint.org`][tvl-xmpp] (sic!) and [via
+      Matrix][hackint-matrix] at [`#tvl:hackint.org`][tvl-matrix].
+
+      Hackint also provide a [web chat][tvl-webchat].
+
+      [tvl-irc]: ircs://irc.hackint.org:6697/#tvl
+      [hackint]: https://hackint.org/
+      [hackint-xmpp]: https://hackint.org/transport/xmpp
+      [tvl-xmpp]: xmpp:#tvl@irc.hackint.org?join
+      [hackint-matrix]: https://hackint.org/transport/matrix
+      [tvl-matrix]: https://matrix.to/#/#tvl:hackint.org
+      [tvl-webchat]: https://webirc.hackint.org/#ircs://irc.hackint.org/#tvl
+
+      ----------------
+
+      ## Blog
+
+      Here are the most recent TVL blog posts.
+
+      ${listPosts publishedPosts}
+
+      You can also follow our [atom feed](https://tvl.fyi/feed.atom).
+
+      ----------------
+
+      ## Where did all these people come from?
+
+      It's pretty straightforward. Feel free to click on people, too.
+
+      <div class="tvl-graph-container">
+        <!--
+          cheddar leaves HTML inside of HTML alone,
+          so wrapping the SVG prevents it from messing it up
+        -->
+        ${builtins.readFile tvlGraph}
+      </div>
+    '';
+    extraHead = ''
+      <style>
+        .tvl-graph-container {
+          max-width: inherit;
+        }
+
+        .tvl-graph-container svg {
+          max-width: inherit;
+          height: auto;
+        }
+
+        .tvl-logo {
+          width: 60%;
+          display: block;
+          margin-left: auto;
+          margin-right: auto;
+        }
+      </style>
+    '';
+  };
+in
+runCommandNoCC "website" { } ''
+  mkdir -p $out/blog
+  cp ${homepage} $out/index.html
+  ${postRenderingCommands tvl.blog.posts}
+  cp ${atomFeed} $out/feed.atom
+''
diff --git a/web/tvl/footer/default.nix b/web/tvl/footer/default.nix
new file mode 100644
index 0000000000..dc2c963f90
--- /dev/null
+++ b/web/tvl/footer/default.nix
@@ -0,0 +1,21 @@
+# Footer fragment for TVL homepages, used by //web/tvl/template for
+# our static pages and also via //web/blog for blog posts.
+{ lib, ... }:
+
+args: ''
+  <p class="footer">
+    <a class="uncoloured-link" href="https://at.tvl.fyi/?q=%2F%2FREADME.md">code</a>
+    |
+    <a class="uncoloured-link" href="https://cl.tvl.fyi/">reviews</a>
+    |
+    <a class="uncoloured-link" href="https://tvl.fyi/builds">ci</a>
+    |
+    <a class="uncoloured-link" href="https://b.tvl.fyi/">bugs</a>
+    |
+    <a class="uncoloured-link" href="https://todo.tvl.fyi/">todos</a>
+    |
+    <a class="uncoloured-link" href="https://atward.tvl.fyi/">search</a>
+'' + lib.optionalString (args ? extraFooter) args.extraFooter + ''
+  </p>
+  <p class="lod">ಠ_ಠ</p>
+''
diff --git a/web/tvl/logo/default.nix b/web/tvl/logo/default.nix
new file mode 100644
index 0000000000..d9e023946a
--- /dev/null
+++ b/web/tvl/logo/default.nix
@@ -0,0 +1,97 @@
+# Creates an output containing the logo in SVG format (animated and
+# static, one for each background colour) and without animations in
+# PNG.
+{ depot, lib, pkgs, ... }:
+
+let
+  palette = {
+    purple = "#CC99C9";
+    blue = "#9EC1CF";
+    green = "#9EE09E";
+    yellow = "#FDFD97";
+    orange = "#FEB144";
+    red = "#FF6663";
+  };
+
+  staticCss = colour: ''
+    #armchair-background {
+      fill: ${colour};
+    }
+  '';
+
+  # Create an animated CSS that equally spreads out the colours over
+  # the animation duration (1min).
+  animatedCss = colours:
+    let
+      # Calculate at which percentage offset each colour should appear.
+      stepSize = 100 / ((builtins.length colours) - 1);
+      frames = lib.imap0 (idx: colour: { inherit colour; at = idx * stepSize; }) colours;
+      frameCss = frame: "${toString frame.at}% { fill: ${frame.colour}; }";
+    in
+    ''
+      #armchair-background {
+        animation: 30s infinite alternate armchairPalette;
+      }
+
+      @keyframes armchairPalette {
+      ${lib.concatStringsSep "\n" (map frameCss frames)}
+      }
+    '';
+
+  # Dark version of the logo, suitable for light backgrounds.
+  darkCss = armchairCss: ''
+    .structure {
+      fill: #383838;
+    }
+    #letters {
+      fill: #fefefe;
+    }
+    ${armchairCss}
+  '';
+
+  # Light version, suitable for dark backgrounds.
+  lightCss = armchairCss: ''
+    .structure {
+      fill: #e4e4ef;
+    }
+    #letters {
+      fill: #181818;
+    }
+    ${armchairCss}
+  '';
+
+  logoShapes = builtins.readFile ./logo-shapes.svg;
+  logoSvg = style: ''
+    <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="420 860 1640 1500"
+         xmlns:xlink="http://www.w3.org/1999/xlink">
+      <style>${style}</style>
+      ${logoShapes}
+    </svg>
+  '';
+
+in
+depot.nix.readTree.drvTargets (lib.fix (self: {
+  # Expose the logo construction functions.
+  inherit palette darkCss lightCss animatedCss staticCss;
+
+  # Create a TVL logo SVG with the specified style.
+  logoSvg = style: pkgs.writeText "logo.svg" (logoSvg style);
+
+  # Create a PNG of the TVL logo with the specified style and DPI.
+  logoPng = style: dpi: pkgs.runCommandNoCC "logo.png" { } ''
+    ${pkgs.inkscape}/bin/inkscape \
+      --export-area-drawing \
+      --export-background-opacity 0 \
+      --export-dpi ${toString dpi} \
+      ${self.logoSvg style} -o $out
+  '';
+
+  # Animated dark SVG logo with all colours.
+  pastelRainbow = self.logoSvg (darkCss (animatedCss (lib.attrValues palette)));
+}
+
+  # Add individual outputs for static dark logos of each colour.
+  // (lib.mapAttrs'
+  (k: v: lib.nameValuePair "${k}Png"
+    (self.logoPng (darkCss (staticCss v)) 96))
+  palette)))
diff --git a/web/tvl/logo/logo-shapes.svg b/web/tvl/logo/logo-shapes.svg
new file mode 100644
index 0000000000..d35971e0a9
--- /dev/null
+++ b/web/tvl/logo/logo-shapes.svg
@@ -0,0 +1,27 @@
+<polygon id="armchair-background" points="463 2030 567 1814 1904 1814 1978 2030 1935 2169 1720 2155 1590 2311 873 2305 778 2142 570 2186"/>
+<g class="structure">
+  <path id="virusbody" d="M 707.524,1820.74 701.038,1401.58 970,1100 h 542 l 271.37,310.47 -16.99,419.17 -295.93,284.34 -445.46,-4.15 z"/>
+</g>
+<g class="structure" id="lambdas">
+  <!-- virus lambdas and feet, clockwise starting at the top left -->
+  <path id="topleft" d="m 1002,1045 38,75 -65,35 -140,-260 h 78 l 47,80 45,-80 h 45 l 17.39,34.968" />
+  <use id="topright" xlink:href="#topleft" transform="matrix(-1,0,0,1,2482,0)" />
+  <use id="midright" xlink:href="#topleft" transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,3284.799,1331.4128)" />
+  <use id="bottomright" xlink:href="#topleft" transform="matrix(-0.25881905,-0.96592583,-0.96592583,0.25881905,3120.6829,2438.0653)" />
+  <use id="rightfoot" xlink:href="#topleft" transform="matrix(-0.60515932,0.14752194,-0.14752194,-0.60515932,2234.5287,2616.7665)" />
+  <use id="leftfoot" xlink:href="#topleft" transform="matrix(0.60515932,0.14752194,0.14752194,-0.60515932,253.62404,2616.7665)" />
+  <use id="bottomleft" xlink:href="#topleft" transform="rotate(-75,1263.0635,1635.2798)" />
+  <use id="midleft" xlink:href="#topleft" transform="rotate(-45,1209.002,1626.9386)" />
+</g>
+<g class="structure" id="armchair">
+  <path d="M742.781 2172.23s-89.208 93.93-210.767 22.78c-121.56-71.14-124.755-220.09-47.72-318 78.865-100.24 220.899-86.94 221.229-85.38.274 1.3 247.178 196.08 328.597 260.28 16.08 12.68 25.71 20.27 25.71 20.27l-37.68 41.02s-209.519-177.76-290.729-250.45c-9.975 1.38-150.662-67.27-214.983 108.51-24.251 74.65 15.983 145.09 69.889 167.71 91.689 19.32 94.88 1.94 121.523-18.39"/>
+  <path d="M1738.4 2174.64s91.9 88.75 209.97 16.51c118.07-72.25 115.91-216.85 39.26-313.11-78.47-98.55-217.31-83.5-217.61-81.95-.26 1.29-239.43 197.97-318.3 262.8-15.58 12.8-24.9 20.46-24.9 20.46l37.4 40.26s202.73-184.66 281.29-257.92c9.78 1.23 134.36-50.54 211.78 110.07 28.32 92.64-13.71 144.64-66.18 167.81-89.5 20.38-90.29.61-116.63-19.24"/>
+  <path d="m899.02 2276.92 680.44-.32 98.56-134.61 51.64 32.46-121.94 160.78-739.1-1.03-125.507-162.22 54.172-39.79 101.735 144.73Z"/>
+  <path d="m744.143 2173.36 56.05-35.55s-44.914-79.17-102.074-8.6"/>
+  <path d="M1728.8 2176.06c-7.6 2.16-53.69-30.58-53.69-30.58s43.06-84.48 102.63-21.21c59.57 63.27-52.85 47.65-48.94 51.79Z"/>
+</g>
+<g id="letters" fill="#fefefe">
+  <path id="t" d="M970.081 1776.8c-22.214 0-40.017-6.45-53.41-19.35-13.394-12.9-20.09-30.14-20.09-51.7v-158.27h-75.95v-40.18h75.95v-75.95h44.1v75.95h107.799v40.18H940.681v158.27c0 9.15 2.695 16.58 8.085 22.3 5.39 5.72 12.495 8.57 21.315 8.57h73.499v40.18h-73.499Z"/>
+  <path id="v" d="M 1205.77 1776.8 L 1112.18 1507.3 L 1157.75 1507.3 L 1235.66 1742.99 L 1311.12 1507.3 L 1357.18 1507.3 L 1263.59 1776.8 L 1205.77 1776.8 L 1205.77 1776.8 Z"/>
+  <path id="lambda" d="M 1406.18 1776.8 L 1506.14 1511.71 L 1469.88 1419.1 L 1516.92 1419.1 L 1651.18 1776.8 L 1604.14 1776.8 L 1539.95 1601.87 L 1530.64 1571.49 L 1453.71 1776.8 L 1406.18 1776.8 Z"/>
+</g>
diff --git a/web/tvl/template/default.nix b/web/tvl/template/default.nix
new file mode 100644
index 0000000000..6b6a5b0303
--- /dev/null
+++ b/web/tvl/template/default.nix
@@ -0,0 +1,52 @@
+{ depot, pkgs, lib, ... }:
+
+{
+  # content of the <title> tag
+  title
+  # main part of the page, usually wrapped with <main>
+, content
+  # optional extra html to inject into <head>
+, extraHead ? null
+  # optional extra html to inject into <footer>
+, extraFooter ? null
+  # URL at which static assets are located
+, staticUrl ? "https://static.tvl.fyi/${depot.web.static.drvHash}"
+}@args:
+
+let
+  inherit (pkgs) runCommandNoCC lib;
+  inherit (depot.tools) cheddar;
+in
+
+runCommandNoCC "${lib.strings.sanitizeDerivationName title}-index.html"
+{
+  headerPart = ''
+    <!DOCTYPE html>
+    <head>
+      <meta charset="utf-8">
+      <meta name="viewport" content="width=device-width, initial-scale=1">
+      <meta name="description" content="The Virus Lounge">
+      <link rel="stylesheet" type="text/css" href="${staticUrl}/tvl.css" media="all">
+      <link rel="icon" type="image/webp" href="${staticUrl}/favicon.webp">
+      <link rel="alternate" type="application/atom+xml" title="Atom Feed" href="https://tvl.fyi/feed.atom">
+      <title>${title}</title>
+  '' + lib.optionalString (args ? extraHead) extraHead + ''
+    </head>
+    <body class="light">
+  '';
+
+  inherit content;
+
+  footerPart = ''
+      <hr>
+      <footer>
+        ${depot.web.tvl.footer args}
+      </footer>
+    </body>
+  '';
+
+  passAsFile = [ "headerPart" "content" "footerPart" ];
+} ''
+  ${cheddar}/bin/cheddar --about-filter content.md < $contentPath > rendered.html
+  cat $headerPartPath rendered.html $footerPartPath > $out
+''
diff --git a/web/tvl/tvl.dot b/web/tvl/tvl.dot
new file mode 100644
index 0000000000..b28c529e0c
--- /dev/null
+++ b/web/tvl/tvl.dot
@@ -0,0 +1,172 @@
+digraph tvl {
+  node [fontname = "JetBrains Mono"];
+  overlap = false;
+  splines = polyline;
+
+  TVL [style="bold" href="http://tvl.fyi"];
+  tazjin -> TVL [style="bold"];
+
+  // people
+  subgraph {
+    Irenes [href="https://www.pluralpride.com/"];
+    adisbladis [href="http://nixos.expert/"];
+    andi [label="andi-" href="https://andreas.rammhold.de/"];
+    anon1 [color="grey" fontcolor="grey"];
+    aurora [href="https://nonegenderleftfox.aventine.se/"];
+    benjojo [href="https://benjojo.co.uk/"];
+    cynthia [href="https://cynthia.re/"];
+    edef [href="https://edef.eu/files/edef.hs"];
+    ericvolp [href="https://ericv.me"];
+    espes;
+    eta [href="https://theta.eu.org/"];
+    firefly [href="http://firefly.nu/"];
+    flokli [href="https://flokli.de/"];
+    fzakaria [href="https://fzakaria.com/"];
+    ghuntley [href="https://ghuntley.com/"];
+    grfn [href="http://gws.fyi"];
+    htbf [href="https://htbf.dev/"];
+    implr [href="https://twitter.com/implring"];
+    isomer [href="https://www.lorier.net/"];
+    jusrin [href="https://jusrin.dev/"];
+    kn;
+    lassulus;
+    leah2 [href="https://leahneukirchen.org/"];
+    lukegb [href="https://lukegb.com/"];
+    marcusr [href="http://marcus.nordaaker.com/"];
+    mdjnsn;
+    ncl;
+    nikky [href="http://nikky.moe/"];
+    nyanotech [href="https://twitter.com/nyanotech"];
+    poigon;
+    Profpatsch [href="http://profpatsch.de/"];
+    qyliss [href="https://alyssa.is"];
+    seven [href="https://open.spotify.com/user/so7"];
+    spacekookie [href="https://spacekookie.de/"];
+    sterni [href="https://sterni.lv/"];
+    tazjin [href="https://tazj.in/"];
+    wpcarro [href="https://wpcarro.dev/"];
+    yuuko;
+  }
+
+  // companies (blue)
+  subgraph {
+    node [color="#4285f4" fontcolor="#4285f4"];
+    spotify [href="https://www.spotify.com/"];
+    google [href="https://www.google.com/"];
+  }
+
+  // communities? (red)
+  subgraph {
+    node [color="#db4437" fontcolor="#db4437"];
+    eve [href="https://www.eveonline.com/"];
+    nix [href="https://nixos.org/nix/"];
+    ircv3 [href="https://ircv3.net/"];
+    lgbtslack [label="lgbt.tech" href="https://lgbtq.technology/"];
+    muccc [label="µccc" href="https://muc.ccc.de/"];
+    afra [label="AfRA" href="https://afra-berlin.de/"];
+  }
+
+  // special
+  subgraph {
+    baby [color="pink" fontcolor="pink" href="https://cynthia.re/s/baby"];
+    unspecific [color="grey" fontcolor="grey"];
+  }
+
+  // primary edges (how did they end up in TVL?)
+  subgraph {
+    // Direct edges
+    nix -> tazjin;
+    spotify -> tazjin;
+    google -> tazjin;
+    eve -> tazjin;
+    unspecific -> tazjin;
+    edef -> tazjin;
+
+    // via nix
+    adisbladis -> nix;
+    jusrin -> nix;
+    ghuntley -> nix;
+    flokli -> nix;
+    andi -> nix;
+    Profpatsch -> nix;
+    lassulus -> nix;
+
+    // via edef
+    benjojo -> edef;
+    espes -> edef;
+    firefly -> edef;
+    leah2 -> aurora;
+    ncl -> edef;
+    qyliss -> edef;
+
+    // via spotify
+    seven -> spotify;
+
+    // via google
+    htbf -> google;
+    Irenes -> google;
+    isomer -> google;
+    lukegb -> google;
+    wpcarro -> google;
+    fzakaria -> google;
+    mdjnsn -> google;
+
+    // random primary
+    grfn -> wpcarro;
+    anon1 -> google;
+    aurora -> eve;
+    cynthia -> benjojo;
+    eta -> anon1;
+    ericvolp -> lukegb;
+    marcusr -> unspecific;
+    poigon -> eve;
+    implr -> lukegb;
+    afra -> unspecific;
+    nikky -> afra;
+    spacekookie -> qyliss;
+    kn -> flokli;
+    sterni -> Profpatsch;
+    yuuko -> ncl;
+  }
+
+  // secondary edges (how are they connected otherwise?)
+  subgraph {
+    edge [weight=0 style="dotted" color="grey" arrowhead="none"];
+
+    // lgbt slack
+    aurora -> lgbtslack;
+    leah2 -> lgbtslack;
+    edef -> lgbtslack;
+
+    // ircv3
+    eta -> ircv3;
+    firefly -> ircv3;
+
+    // µccc
+    leah2 -> muccc;
+
+    // random
+    leah2 -> edef;
+    lukegb -> isomer;
+    eta -> firefly;
+    cynthia -> firefly;
+    cynthia -> lukegb;
+    implr -> google;
+    nyanotech -> google;
+    lukegb -> benjojo;
+    espes -> benjojo;
+    espes -> aurora;
+    qyliss -> nix;
+    grfn -> nix;
+    edef -> nix;
+    spacekookie -> afra;
+    qyliss -> afra;
+  }
+
+  // baby
+  subgraph {
+    edge [weight=0 style="dotted" color="pink" arrowhead="none"];
+    cynthia -> baby;
+    eta -> baby;
+  }
+}